Many changes

- Fix signature count reseting on update but not changed
- Better handle reloads for extended
- Metered connection warning
- Block all actions when appropriate
- Use a wakelock instead of keeping the screen on
- Increase max scan sizes
- Always check if files can be read before scanning
- Resolve true paths to avoid duplicates from symlinks
- Scan more app paths
- Scan more system paths
- Bump version

Signed-off-by: Tad <tad@spotco.us>
This commit is contained in:
Tad 2023-12-28 22:18:35 -05:00
parent dbb7e98fa8
commit a0f6a00244
No known key found for this signature in database
GPG key ID: B286E9F57A07424B
9 changed files with 190 additions and 89 deletions

View file

@ -6,8 +6,8 @@ android {
applicationId "us.spotco.malwarescanner" applicationId "us.spotco.malwarescanner"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 32 targetSdkVersion 32
versionCode 302 versionCode 305
versionName "3.02" versionName "3.05"
resConfigs 'en', 'af', 'de', 'el', 'es', 'fi', 'fr', 'it', 'pl', 'pt', 'ru', 'tr', 'zh-rCN' resConfigs 'en', 'af', 'de', 'el', 'es', 'fi', 'fr', 'it', 'pl', 'pt', 'ru', 'tr', 'zh-rCN'
} }
buildTypes { buildTypes {
@ -20,7 +20,7 @@ android {
shrinkResources true shrinkResources true
minifyEnabled true minifyEnabled true
zipAlignEnabled true zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
lint { lint {

View file

@ -16,6 +16,7 @@
android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<queries> <queries>
<package android:name="org.torproject.android" /> <package android:name="org.torproject.android" />

View file

@ -52,7 +52,8 @@ class Database {
public static BloomFilter<String> signaturesSHA1 = null; public static BloomFilter<String> signaturesSHA1 = null;
public static BloomFilter<String> signaturesSHA256 = null; public static BloomFilter<String> signaturesSHA256 = null;
public static long signaturesCount = 0; public static long signaturesCount = 0;
public static boolean changed = false; public static boolean changedDownload = false;
public static boolean changedConfig = false;
private static final DateFormat dateFormat = DateFormat.getDateInstance(); private static final DateFormat dateFormat = DateFormat.getDateInstance();
@ -87,7 +88,7 @@ class Database {
if (!Utils.getDatabaseURL(context).equals(Utils.DATABASE_URL_DEFAULT)) { if (!Utils.getDatabaseURL(context).equals(Utils.DATABASE_URL_DEFAULT)) {
log.append(context.getString(R.string.main_database_override, Utils.getDatabaseURL(context)) + "\n"); log.append(context.getString(R.string.main_database_override, Utils.getDatabaseURL(context)) + "\n");
} }
changed = false; changedDownload = false;
boolean onionRouting = prefs.getBoolean("ONION_ROUTING", false); boolean onionRouting = prefs.getBoolean("ONION_ROUTING", false);
new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, Utils.getDatabaseURL(context) + "gpg.key", databasePath + "/gpg.key", Utils.getDatabaseURL(context)); new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, Utils.getDatabaseURL(context) + "gpg.key", databasePath + "/gpg.key", Utils.getDatabaseURL(context));
@ -102,7 +103,6 @@ class Database {
databasePath.mkdir(); databasePath.mkdir();
signatureDatabases.clear(); signatureDatabases.clear();
signaturesCount = 0;
prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE); prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE);
String baseURL = Utils.getDatabaseURL(context); String baseURL = Utils.getDatabaseURL(context);
signatureDatabases.add(new SignatureDatabase(baseURL, "hypatia-md5-bloom.bin")); signatureDatabases.add(new SignatureDatabase(baseURL, "hypatia-md5-bloom.bin"));
@ -119,6 +119,8 @@ class Database {
databaseCurrentlyLoading = true; databaseCurrentlyLoading = true;
initDatabase(context); initDatabase(context);
signaturesCount = 0; signaturesCount = 0;
changedConfig = false;
signaturesMD5Extended = null;
File publicKey = new File(databasePath + "/gpg.key"); File publicKey = new File(databasePath + "/gpg.key");
GPGDetachedSignatureVerifier verifier = new GPGDetachedSignatureVerifier(Utils.getSigningKey(context)); GPGDetachedSignatureVerifier verifier = new GPGDetachedSignatureVerifier(Utils.getSigningKey(context));
for (SignatureDatabase database : signatureDatabases) { for (SignatureDatabase database : signatureDatabases) {
@ -227,7 +229,7 @@ class Database {
fileOutputStream.close(); fileOutputStream.close();
outNew.renameTo(out); //Move the new file into place outNew.renameTo(out); //Move the new file into place
changed = true; changedDownload = true;
publishProgress(url.replaceAll(baseURL, "") publishProgress(url.replaceAll(baseURL, "")
+ "\n\t" + Utils.getContext().getString(R.string.main_database_download_success) + "\n\t" + Utils.getContext().getString(R.string.main_database_download_success)

View file

@ -32,6 +32,7 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.PowerManager;
import android.provider.Settings; import android.provider.Settings;
import android.text.InputType; import android.text.InputType;
import android.text.method.ScrollingMovementMethod; import android.text.method.ScrollingMovementMethod;
@ -75,8 +76,6 @@ public class MainActivity extends Activity {
Utils.setContext(getApplicationContext()); Utils.setContext(getApplicationContext());
setContentView(R.layout.content_main); setContentView(R.layout.content_main);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
logView = findViewById(R.id.txtLogOutput); logView = findViewById(R.id.txtLogOutput);
logView.setMovementMethod(new ScrollingMovementMethod()); logView.setMovementMethod(new ScrollingMovementMethod());
logView.setTextIsSelectable(true); logView.setTextIsSelectable(true);
@ -154,19 +153,36 @@ public class MainActivity extends Activity {
} }
break; break;
case R.id.mnuUpdateDatabase: case R.id.mnuUpdateDatabase:
if (malwareScanner.running) { if (Database.hasDownloadsRunning()) {
logView.append(getString(R.string.lblUpdateRunning) + "\n");
} else if (malwareScanner.running) {
logView.append(getString(R.string.lblScanRunning) + "\n"); logView.append(getString(R.string.lblScanRunning) + "\n");
} else if (!Utils.isNetworkAvailable(this)) { } else if (!Utils.isNetworkAvailable(this)) {
logView.append(getString(R.string.lblNoNetwork) + "\n"); logView.append(getString(R.string.lblNoNetwork) + "\n");
} else if (Database.isDatabaseLoading()) {
logView.append(getString(R.string.lblDatabaseLoading) + "\n");
} else if (Utils.isConnectionMetered(this)) {
int amt = prefs.getBoolean("SIGNATURES_EXTENDED", false) ? 200 : 50;
new AlertDialog.Builder(this)
.setTitle(R.string.confirm_update_title)
.setMessage(getString(R.string.confirm_update_summary, String.valueOf(amt)))
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(getString(android.R.string.yes), (dialog, which) -> {
updateDatabase();
})
.setNegativeButton(getString(android.R.string.no), (dialog, which) -> {
dialog.cancel();
}).show();
} else { } else {
if (prefs.getBoolean("ONION_ROUTING", false)) {
Utils.requestStartOrbot(this);
logView.append(getString(R.string.lblOnionRoutingEnabledHint) + "\n");
}
updateDatabase(); updateDatabase();
} }
break; break;
case R.id.mnuDatabaseServer: case R.id.mnuDatabaseServer:
if (Database.hasDownloadsRunning()) {
logView.append(getString(R.string.lblUpdateRunning) + "\n");
} else if (Database.isDatabaseLoading()) {
logView.append(getString(R.string.lblDatabaseLoading) + "\n");
} else {
AlertDialog.Builder builderServerOverride = new AlertDialog.Builder(this); AlertDialog.Builder builderServerOverride = new AlertDialog.Builder(this);
builderServerOverride.setTitle(getString(R.string.lblDatabaseServer)); builderServerOverride.setTitle(getString(R.string.lblDatabaseServer));
final EditText inputServerOverride = new EditText(this); final EditText inputServerOverride = new EditText(this);
@ -185,8 +201,14 @@ public class MainActivity extends Activity {
dialog.cancel(); dialog.cancel();
}); });
builderServerOverride.show(); builderServerOverride.show();
}
break; break;
case R.id.mnuSigningKey: case R.id.mnuSigningKey:
if (Database.hasDownloadsRunning()) {
logView.append(getString(R.string.lblUpdateRunning) + "\n");
} else if (Database.isDatabaseLoading()) {
logView.append(getString(R.string.lblDatabaseLoading) + "\n");
} else {
AlertDialog.Builder builderKey = new AlertDialog.Builder(this); AlertDialog.Builder builderKey = new AlertDialog.Builder(this);
builderKey.setTitle(getString(R.string.lblSigningKey)); builderKey.setTitle(getString(R.string.lblSigningKey));
final EditText inputKey = new EditText(this); final EditText inputKey = new EditText(this);
@ -199,17 +221,33 @@ public class MainActivity extends Activity {
dialog.cancel(); dialog.cancel();
}); });
builderKey.show(); builderKey.show();
}
break; break;
case R.id.toggleExtended: case R.id.toggleExtended:
if (Database.hasDownloadsRunning()) {
logView.append(getString(R.string.lblUpdateRunning) + "\n");
} else if (Database.isDatabaseLoading()) {
logView.append(getString(R.string.lblDatabaseLoading) + "\n");
} else {
boolean prevExtended = prefs.getBoolean("SIGNATURES_EXTENDED", false);
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(R.string.confirm_extended_title) .setTitle(R.string.confirm_extended_title)
.setMessage(getString(R.string.confirm_extended_summary)) .setMessage(getString(R.string.confirm_extended_summary))
.setIcon(android.R.drawable.ic_menu_compass) .setIcon(android.R.drawable.ic_menu_compass)
.setPositiveButton(getString(android.R.string.yes), (dialog, which) -> prefs.edit().putBoolean("SIGNATURES_EXTENDED", true).apply()) .setPositiveButton(getString(android.R.string.yes), (dialog, which) -> {
prefs.edit().putBoolean("SIGNATURES_EXTENDED", true).apply();
if (!prevExtended) {
Database.changedConfig = true;
}
})
.setNegativeButton(getString(android.R.string.no), (dialog, which) -> { .setNegativeButton(getString(android.R.string.no), (dialog, which) -> {
prefs.edit().putBoolean("SIGNATURES_EXTENDED", false).apply(); prefs.edit().putBoolean("SIGNATURES_EXTENDED", false).apply();
if (prevExtended) {
Database.changedConfig = true;
}
dialog.cancel(); dialog.cancel();
}).show(); }).show();
}
break; break;
case R.id.toggleRealtime: case R.id.toggleRealtime:
if (malwareScanner.running) { if (malwareScanner.running) {
@ -250,8 +288,14 @@ public class MainActivity extends Activity {
break; break;
case R.id.btnStartScan: case R.id.btnStartScan:
if (!malwareScanner.running) { if (!malwareScanner.running) {
if (Database.hasDownloadsRunning()) {
logView.append(getString(R.string.lblUpdateRunning) + "\n");
} else if (Database.isDatabaseLoading()) {
logView.append(getString(R.string.lblDatabaseLoading) + "\n");
} else {
updateScanButton(true); updateScanButton(true);
startScanner(); startScanner();
}
} else { } else {
logView.append("\n" + getString(R.string.main_cancelling_scan) + "\n\n"); logView.append("\n" + getString(R.string.main_cancelling_scan) + "\n\n");
malwareScanner.cancel(true); malwareScanner.cancel(true);
@ -267,20 +311,41 @@ public class MainActivity extends Activity {
HashSet<File> filesToScan = new HashSet<>(); HashSet<File> filesToScan = new HashSet<>();
if (scanSystem) { if (scanSystem) {
filesToScan.add(Environment.getRootDirectory()); filesToScan.add(Environment.getRootDirectory());
filesToScan.add(new File("/"));
filesToScan.add(new File("/apex"));
filesToScan.add(new File("/cache"));
filesToScan.add(new File("/data"));
filesToScan.add(new File("/data/local/tmp"));
filesToScan.add(new File("/firmware"));
filesToScan.add(new File("/odm"));
filesToScan.add(new File("/odm_dlkm"));
filesToScan.add(new File("/product"));
filesToScan.add(new File("/system"));
filesToScan.add(new File("/system_dlkm"));
filesToScan.add(new File("/vendor"));
filesToScan.add(new File("/vendor_dlkm"));
} }
if (scanApps) { if (scanApps) {
for (ApplicationInfo packageInfo : getPackageManager().getInstalledApplications(PackageManager.GET_META_DATA)) { for (ApplicationInfo packageInfo : getPackageManager().getInstalledApplications(PackageManager.GET_META_DATA)) {
filesToScan.add(new File(packageInfo.sourceDir)); filesToScan.add(new File(packageInfo.sourceDir));
//Log.d("Hypatia", "Planning to scan " + packageInfo.sourceDir); filesToScan.add(new File(packageInfo.dataDir));
filesToScan.add(new File(packageInfo.nativeLibraryDir));
filesToScan.add(new File(packageInfo.publicSourceDir));
} }
} }
if (scanInternal) { if (scanInternal) {
filesToScan.add(Environment.getExternalStorageDirectory()); filesToScan.add(Environment.getExternalStorageDirectory());
} }
if (scanExternal) { if (scanExternal) {
filesToScan.add(new File("/storage")); File externalStorage = new File("/storage");
if (externalStorage.exists()) {
filesToScan.add(externalStorage);
}
} }
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Hypatia::ManualScanLock");
wakeLock.acquire(10 * 60 * 1000L); /* 10 minutes */
malwareScanner.executeOnExecutor(Utils.getThreadPoolExecutor(), filesToScan); malwareScanner.executeOnExecutor(Utils.getThreadPoolExecutor(), filesToScan);
new Thread(() -> { new Thread(() -> {
try { try {
@ -288,6 +353,7 @@ public class MainActivity extends Activity {
Thread.sleep(500); Thread.sleep(500);
} }
runOnUiThread(() -> updateScanButton(false)); runOnUiThread(() -> updateScanButton(false));
wakeLock.release();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -295,12 +361,16 @@ public class MainActivity extends Activity {
} }
private void updateDatabase() { private void updateDatabase() {
if (prefs.getBoolean("ONION_ROUTING", false)) {
Utils.requestStartOrbot(this);
logView.append(getString(R.string.lblOnionRoutingEnabledHint) + "\n");
}
new Database(findViewById(R.id.txtLogOutput)); new Database(findViewById(R.id.txtLogOutput));
if (!Database.isDatabaseLoading()) { if (!Database.isDatabaseLoading()) {
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Hypatia::UpdateLock");
wakeLock.acquire(3 * 60 * 1000L); /* 3 minutes */
Database.updateDatabase(this, Database.signatureDatabases); Database.updateDatabase(this, Database.signatureDatabases);
} else {
logView.append(getString(R.string.lblDatabaseLoading) + "\n");
}
Utils.getThreadPoolExecutor().execute(() -> { Utils.getThreadPoolExecutor().execute(() -> {
try { try {
Thread.sleep(500); Thread.sleep(500);
@ -308,14 +378,14 @@ public class MainActivity extends Activity {
while (Database.hasDownloadsRunning()) { while (Database.hasDownloadsRunning()) {
Thread.sleep(500); Thread.sleep(500);
Log.d("Hypatia", "Download in progress, waiting!"); Log.d("Hypatia", "Download in progress, waiting!");
} }
wakeLock.release();
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
runOnUiThread(() -> logView.append(getString(R.string.lblDatabasesUpdated) + "\n")); runOnUiThread(() -> logView.append(getString(R.string.lblDatabasesUpdated) + "\n"));
if (Database.isDatabaseLoaded()) { if (Database.isDatabaseLoaded()) {
if(Database.changed) { if (Database.changedDownload || Database.changedConfig) {
Log.d("Hypatia", "Really reloading database!"); Log.d("Hypatia", "Really reloading database!");
Database.loadDatabase(getApplicationContext(), true, Database.signatureDatabases); Database.loadDatabase(getApplicationContext(), true, Database.signatureDatabases);
} else { } else {
@ -325,6 +395,9 @@ public class MainActivity extends Activity {
Log.d("Hypatia", "Database not loaded, skipping reload!"); Log.d("Hypatia", "Database not loaded, skipping reload!");
} }
}); });
} else {
logView.append(getString(R.string.lblDatabaseLoading) + "\n");
}
} }
private void updateScanButton(boolean running) { private void updateScanButton(boolean running) {

View file

@ -221,6 +221,7 @@ class MalwareScanner extends AsyncTask<HashSet<File>, Object, String> {
spinnerCur = " = "; spinnerCur = " = ";
} }
} }
//Log.d("Hypatia", "Scanning " + file);
} }
filesToScanReal.clear(); filesToScanReal.clear();
publishProgress("\n\t" + context.getString(R.string.main_hashing_done) + "\n", true); publishProgress("\n\t" + context.getString(R.string.main_hashing_done) + "\n", true);

View file

@ -79,7 +79,7 @@ public class MalwareScannerService extends Service {
case FileObserver.MOVED_TO: case FileObserver.MOVED_TO:
case FileObserver.CLOSE_WRITE: case FileObserver.CLOSE_WRITE:
File file = new File(path); File file = new File(path);
if (file.exists() && /*file.length() > 0 &&*/ file.length() <= Utils.MAX_SCAN_SIZE_REALTIME) { if (file.exists() && file.length() <= Utils.MAX_SCAN_SIZE_REALTIME) {
HashSet<File> filesToScan = new HashSet<>(); HashSet<File> filesToScan = new HashSet<>();
filesToScan.add(file); filesToScan.add(file);
new MalwareScanner(null, getApplicationContext(), false).executeOnExecutor(threadPoolExecutor, filesToScan); new MalwareScanner(null, getApplicationContext(), false).executeOnExecutor(threadPoolExecutor, filesToScan);

View file

@ -41,8 +41,8 @@ import java.util.concurrent.atomic.AtomicInteger;
class Utils { class Utils {
private static Context context = null; private static Context context = null;
public final static int MAX_SCAN_SIZE = (1000 * 1000) * 80; //80MB public final static int MAX_SCAN_SIZE = (1000 * 1000) * 500; //500MB
public final static int MAX_SCAN_SIZE_REALTIME = MAX_SCAN_SIZE / 2; //40MB public final static int MAX_SCAN_SIZE_REALTIME = (1000 * 1000) * 250; //250MB
public final static String DATABASE_URL_DEFAULT = "https://divested.dev/MalwareScannerSignatures/"; public final static String DATABASE_URL_DEFAULT = "https://divested.dev/MalwareScannerSignatures/";
public final static String SIGNING_KEY_DEFAULT = "BADFCABDDBF5B694"; public final static String SIGNING_KEY_DEFAULT = "BADFCABDDBF5B694";
@ -75,8 +75,14 @@ class Utils {
public static HashSet<File> getFilesRecursive(File root) { public static HashSet<File> getFilesRecursive(File root) {
HashSet<File> filesAll = new HashSet<>(); HashSet<File> filesAll = new HashSet<>();
if (root.isFile()) { //TODO: Skip this try {
root = root.getCanonicalFile();
} catch (Exception ignored) {
}
if (root.isFile()) {
if (root.canRead() && root.length() <= MAX_SCAN_SIZE) {
filesAll.add(root); filesAll.add(root);
}
return filesAll; return filesAll;
} else { } else {
File[] files = root.listFiles(); File[] files = root.listFiles();
@ -85,7 +91,7 @@ class Utils {
if (f.isDirectory()) { if (f.isDirectory()) {
filesAll.addAll(getFilesRecursive(f)); filesAll.addAll(getFilesRecursive(f));
} else { } else {
if (f.length() <= MAX_SCAN_SIZE && f.canRead()) {//Exclude files larger than limit for performance if (f.isFile() && f.canRead() && f.length() <= MAX_SCAN_SIZE) {//Exclude files larger than limit for performance
filesAll.add(f); filesAll.add(f);
} }
} }
@ -177,6 +183,11 @@ class Utils {
} }
} }
public static boolean isConnectionMetered(Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
return connectivityManager != null && connectivityManager.isActiveNetworkMetered();
}
//Credit: https://stackoverflow.com/a/4239019 //Credit: https://stackoverflow.com/a/4239019
public static boolean isNetworkAvailable(Context context) { public static boolean isNetworkAvailable(Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

View file

@ -53,7 +53,7 @@
<string name="lblNoNetwork">No network connected!</string> <string name="lblNoNetwork">No network connected!</string>
<string name="self_test_result_success">Self test successful.</string> <string name="self_test_result_success">Self test successful.</string>
<string name="self_test_result_failure">Self test failed!</string> <string name="self_test_result_failure">Self test failed!</string>
<string name="lblDatabaseLoading">Database is loading, not updating!</string> <string name="lblDatabaseLoading">Skipping action, database is loading!</string>
<string name="lblDatabasesUpdated">All databases updated!</string> <string name="lblDatabasesUpdated">All databases updated!</string>
<string name="lookupVT">Lookup</string> <string name="lookupVT">Lookup</string>
<string name="deleteFile">Delete</string> <string name="deleteFile">Delete</string>
@ -71,5 +71,8 @@
<string name="lblSelfTest">Write self test files</string> <string name="lblSelfTest">Write self test files</string>
<string name="lblExtendedDatabaseToggle">Extended datatabase</string> <string name="lblExtendedDatabaseToggle">Extended datatabase</string>
<string name="confirm_extended_title">Enable extended database?</string> <string name="confirm_extended_title">Enable extended database?</string>
<string name="confirm_extended_summary">This will enable detection of an additional ~40 million signatures.\nThis requires a 125MB download, will slow down startup by over two minutes, will increase app RAM usage, and will increase the false positive rate.\nThis database only updates quarterly.</string> <string name="confirm_extended_summary">[EXPERIMENTAL]\nThis will enable detection of an additional ~40 million signatures.\nThis requires a 125MB download, will slow down startup by over two minutes, will increase app RAM usage, and will increase the false positive rate.\nThis database only updates quarterly.</string>
<string name="confirm_update_title">Confirm download</string>
<string name="confirm_update_summary">You appear to be on a metered connection. Are you sure you want to update the databases?\nIt may download up to %s megabytes of data.</string>
<string name="lblUpdateRunning">Skipping action, an update is running!</string>
</resources> </resources>

View file

@ -0,0 +1,10 @@
* Fix signature count reseting on update but not changed
* Better handle reloads when extended DB is enabled
* Metered connection warning
* Block all actions when appropriate
* Use a wakelock instead of keeping the screen on
* Increase max scan sizes: 500MB manual, 250MB realtime
* Always check if files can be read before scanning
* Resolve true paths to avoid duplicates from symlinks
* Scan more app paths
* Scan more system paths