diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index 1f993b4c8..2a4f6b2f7 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -29,13 +29,10 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.database.ContentObserver; import android.net.Uri; import android.os.Bundle; import android.os.Handler; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.AppBarLayout; import android.support.design.widget.CoordinatorLayout; @@ -60,8 +57,6 @@ import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppPrefsProvider; import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.InstalledApp; -import org.fdroid.fdroid.data.InstalledAppProvider; import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.Installer; @@ -740,40 +735,6 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog installApk(apkToInstall); } - /** - * Attempts to find the installed {@link Apk} from the database. If not found, will lookup the - * {@link InstalledAppProvider} to find the details of the installed app and use that to - * instantiate an {@link Apk} to be returned. - * - * Cases where an {@link Apk} will not be found in the database and for which we fall back to - * the {@link InstalledAppProvider} include: - * + System apps which are provided by a repository, but for which the version code bundled - * with the system is not included in the repository. - * + Regular apps from a repository, where the installed version is old enough that it is no - * longer available in the repository. - * - * @throws IllegalStateException If neither the {@link PackageManager} or the - * {@link InstalledAppProvider} can't find a reference to the installed apk. - */ - @NonNull - private Apk getInstalledApk() { - try { - PackageInfo pi = getPackageManager().getPackageInfo(app.packageName, 0); - Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, pi.packageName, pi.versionCode); - if (apk == null) { - InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(this, pi.packageName); - if (installedApp == null) { - throw new IllegalStateException("No installed app found when trying to uninstall"); - } - apk = new Apk(installedApp); - } - return apk; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - throw new IllegalStateException("Couldn't find installed apk for " + app.packageName, e); - } - } - @Override public void uninstallApk() { Apk apk = app.installedApk; @@ -783,7 +744,10 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog apk = app.getMediaApkifInstalled(getApplicationContext()); if (apk == null) { // When the app isn't a media file - the above workaround refers to this. - apk = getInstalledApk(); + apk = app.getInstalledApk(this); + if (apk == null) { + throw new IllegalStateException("Couldn't find installed apk for " + app.packageName); + } } app.installedApk = apk; } diff --git a/app/src/main/java/org/fdroid/fdroid/data/Apk.java b/app/src/main/java/org/fdroid/fdroid/data/Apk.java index 6fc5ac197..bd2cd1419 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java @@ -241,7 +241,7 @@ public class Apk extends ValueObject implements Comparable, Parcelable { case Cols.Repo.ADDRESS: repoAddress = cursor.getString(i); break; - case Cols.ANTI_FEATURES: + case Cols.AntiFeatures.ANTI_FEATURES: antiFeatures = Utils.parseCommaSeparatedString(cursor.getString(i)); break; } @@ -348,7 +348,7 @@ public class Apk extends ValueObject implements Comparable, Parcelable { values.put(Cols.FEATURES, Utils.serializeCommaSeparatedString(features)); values.put(Cols.NATIVE_CODE, Utils.serializeCommaSeparatedString(nativecode)); values.put(Cols.INCOMPATIBLE_REASONS, Utils.serializeCommaSeparatedString(incompatibleReasons)); - values.put(Cols.ANTI_FEATURES, Utils.serializeCommaSeparatedString(antiFeatures)); + values.put(Cols.AntiFeatures.ANTI_FEATURES, Utils.serializeCommaSeparatedString(antiFeatures)); values.put(Cols.IS_COMPATIBLE, compatible ? 1 : 0); return values; } diff --git a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java index 2b59e139d..f132c6c4d 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java @@ -9,6 +9,10 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; + +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Schema.AntiFeatureTable; +import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable; import org.fdroid.fdroid.data.Schema.ApkTable; import org.fdroid.fdroid.data.Schema.ApkTable.Cols; import org.fdroid.fdroid.data.Schema.AppMetadataTable; @@ -17,8 +21,10 @@ import org.fdroid.fdroid.data.Schema.RepoTable; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; @SuppressWarnings("LineLength") public class ApkProvider extends FDroidProvider { @@ -266,6 +272,10 @@ public class ApkProvider extends FDroidProvider { return ApkTable.NAME; } + protected String getApkAntiFeatureJoinTableName() { + return ApkAntiFeatureJoinTable.NAME; + } + protected String getAppTableName() { return AppMetadataTable.NAME; } @@ -283,6 +293,18 @@ public class ApkProvider extends FDroidProvider { private class Query extends QueryBuilder { private boolean repoTableRequired; + private boolean antiFeaturesRequested; + + /** + * If the query includes anti features, then we group by apk id. This is because joining onto the anti-features + * table will result in multiple result rows for each apk (potentially), so we will GROUP_CONCAT each of the + * anti features into a single comma separated list for each apk. If we are _not_ including anti features, then + * don't group by apk, because when doing a COUNT(*) this will result in the wrong result. + */ + @Override + protected String groupBy() { + return antiFeaturesRequested ? "apk." + Cols.ROW_ID : null; + } @Override protected String getRequiredTables() { @@ -301,6 +323,9 @@ public class ApkProvider extends FDroidProvider { addPackageField(PACKAGE_FIELDS.get(field), field); } else if (REPO_FIELDS.containsKey(field)) { addRepoField(REPO_FIELDS.get(field), field); + } else if (Cols.AntiFeatures.ANTI_FEATURES.equals(field)) { + antiFeaturesRequested = true; + addAntiFeatures(); } else if (field.equals(Cols._ID)) { appendField("rowid", "apk", "_id"); } else if (field.equals(Cols._COUNT)) { @@ -324,6 +349,18 @@ public class ApkProvider extends FDroidProvider { appendField(field, "repo", alias); } + private void addAntiFeatures() { + String apkAntiFeature = "apkAntiFeatureJoin"; + String antiFeature = "antiFeature"; + + leftJoin(getApkAntiFeatureJoinTableName(), apkAntiFeature, + "apk." + Cols.ROW_ID + " = " + apkAntiFeature + "." + ApkAntiFeatureJoinTable.Cols.APK_ID); + + leftJoin(AntiFeatureTable.NAME, antiFeature, + apkAntiFeature + "." + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + " = " + antiFeature + "." + AntiFeatureTable.Cols.ROW_ID); + + appendField("group_concat(" + antiFeature + "." + AntiFeatureTable.Cols.NAME + ") as " + Cols.AntiFeatures.ANTI_FEATURES); + } } private QuerySelection queryPackage(String packageName) { @@ -508,15 +545,73 @@ public class ApkProvider extends FDroidProvider { @Override public Uri insert(Uri uri, ContentValues values) { + boolean saveAntiFeatures = false; + String[] antiFeatures = null; + if (values.containsKey(Cols.AntiFeatures.ANTI_FEATURES)) { + saveAntiFeatures = true; + String antiFeaturesString = values.getAsString(Cols.AntiFeatures.ANTI_FEATURES); + antiFeatures = Utils.parseCommaSeparatedString(antiFeaturesString); + values.remove(Cols.AntiFeatures.ANTI_FEATURES); + } + removeFieldsFromOtherTables(values); validateFields(Cols.ALL, values); long newId = db().insertOrThrow(getTableName(), null, values); + + if (saveAntiFeatures) { + ensureAntiFeatures(antiFeatures, newId); + } + if (!isApplyingBatch()) { getContext().getContentResolver().notifyChange(uri, null); } return getApkUri(newId); } + protected void ensureAntiFeatures(String[] antiFeatures, long apkId) { + db().delete(getApkAntiFeatureJoinTableName(), ApkAntiFeatureJoinTable.Cols.APK_ID + " = ?", new String[] {Long.toString(apkId)}); + if (antiFeatures != null) { + Set antiFeatureSet = new HashSet<>(); + for (String antiFeatureName : antiFeatures) { + + // There is nothing stopping a server repeating a category name in the metadata of + // an app. In order to prevent unique constraint violations, only insert once into + // the join table. + if (antiFeatureSet.contains(antiFeatureName)) { + continue; + } + + antiFeatureSet.add(antiFeatureName); + + long antiFeatureId = ensureAntiFeature(antiFeatureName); + ContentValues categoryValues = new ContentValues(2); + categoryValues.put(ApkAntiFeatureJoinTable.Cols.APK_ID, apkId); + categoryValues.put(ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID, antiFeatureId); + db().insert(getApkAntiFeatureJoinTableName(), null, categoryValues); + } + } + } + + protected long ensureAntiFeature(String antiFeatureName) { + long antiFeatureId = 0; + Cursor cursor = db().query(AntiFeatureTable.NAME, new String[] {AntiFeatureTable.Cols.ROW_ID}, AntiFeatureTable.Cols.NAME + " = ?", new String[]{antiFeatureName}, null, null, null); + if (cursor != null) { + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + antiFeatureId = cursor.getLong(0); + } + cursor.close(); + } + + if (antiFeatureId <= 0) { + ContentValues values = new ContentValues(1); + values.put(AntiFeatureTable.Cols.NAME, antiFeatureName); + antiFeatureId = db().insert(AntiFeatureTable.NAME, null, values); + } + + return antiFeatureId; + } + @Override public int delete(Uri uri, String where, String[] whereArgs) { @@ -549,6 +644,15 @@ public class ApkProvider extends FDroidProvider { throw new UnsupportedOperationException("Cannot update anything other than a single apk."); } + boolean saveAntiFeatures = false; + String[] antiFeatures = null; + if (values.containsKey(Cols.AntiFeatures.ANTI_FEATURES)) { + saveAntiFeatures = true; + String antiFeaturesString = values.getAsString(Cols.AntiFeatures.ANTI_FEATURES); + antiFeatures = Utils.parseCommaSeparatedString(antiFeaturesString); + values.remove(Cols.AntiFeatures.ANTI_FEATURES); + } + validateFields(Cols.ALL, values); removeFieldsFromOtherTables(values); @@ -556,6 +660,19 @@ public class ApkProvider extends FDroidProvider { query = query.add(querySingleWithAppId(uri)); int numRows = db().update(getTableName(), values, query.getSelection(), query.getArgs()); + + if (saveAntiFeatures) { + // Get the database ID of the row we just updated, so that we can join relevant anti features to it. + Cursor result = db().query(getTableName(), new String[]{Cols.ROW_ID}, + query.getSelection(), query.getArgs(), null, null, null); + if (result != null) { + result.moveToFirst(); + long apkId = result.getLong(0); + ensureAntiFeatures(antiFeatures, apkId); + result.close(); + } + } + if (!isApplyingBatch()) { getContext().getContentResolver().notifyChange(uri, null); } diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index 11a8ac0d0..5a7109683 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -82,13 +82,6 @@ public class App extends ValueObject implements Comparable, Parcelable { */ @JsonIgnore public boolean compatible; - /** - * This is primarily for the purpose of saving app metadata when parsing an index.xml file. - * At most other times, we don't particularly care which repo an {@link App} object came from. - * It is pretty much transparent, because the metadata will be populated from the repo with - * the highest priority. The UI doesn't care normally _which_ repo provided the metadata. - * This is required for getting the full URL to the various graphics and screenshots. - */ @JsonIgnore public Apk installedApk; // might be null if not installed @JsonIgnore @@ -107,6 +100,13 @@ public class App extends ValueObject implements Comparable, Parcelable { @JsonIgnore public boolean isApk; + /** + * This is primarily for the purpose of saving app metadata when parsing an index.xml file. + * At most other times, we don't particularly care which repo an {@link App} object came from. + * It is pretty much transparent, because the metadata will be populated from the repo with + * the highest priority. The UI doesn't care normally _which_ repo provided the metadata. + * This is required for getting the full URL to the various graphics and screenshots. + */ @JacksonInject("repoId") public long repoId; @@ -796,6 +796,37 @@ public class App extends ValueObject implements Comparable, Parcelable { apk.sig = Utils.hashBytes(fdroidSig, "md5"); } + /** + * Attempts to find the installed {@link Apk} from the database. If not found, will lookup the + * {@link InstalledAppProvider} to find the details of the installed app and use that to + * instantiate an {@link Apk} to be returned. + * + * Cases where an {@link Apk} will not be found in the database and for which we fall back to + * the {@link InstalledAppProvider} include: + * + System apps which are provided by a repository, but for which the version code bundled + * with the system is not included in the repository. + * + Regular apps from a repository, where the installed version is old enough that it is no + * longer available in the repository. + * + */ + @Nullable + public Apk getInstalledApk(Context context) { + try { + PackageInfo pi = context.getPackageManager().getPackageInfo(this.packageName, 0); + Apk apk = ApkProvider.Helper.findApkFromAnyRepo(context, pi.packageName, pi.versionCode); + if (apk == null) { + InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(context, pi.packageName); + if (installedApp == null) { + throw new IllegalStateException("No installed app found when trying to uninstall"); + } + apk = new Apk(installedApp); + } + return apk; + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + public boolean isValid() { if (TextUtils.isEmpty(this.name) || TextUtils.isEmpty(this.packageName)) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppPrefs.java b/app/src/main/java/org/fdroid/fdroid/data/AppPrefs.java index 1f76e1a6e..fee69d192 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppPrefs.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppPrefs.java @@ -3,37 +3,44 @@ package org.fdroid.fdroid.data; public class AppPrefs extends ValueObject { /** - * True if all updates for this app are to be ignored + * True if all updates for this app are to be ignored. */ public boolean ignoreAllUpdates; /** - * True if the current update for this app is to be ignored + * The version code of the app for which the update should be ignored. */ public int ignoreThisUpdate; - public AppPrefs(int ignoreThis, boolean ignoreAll) { + /** + * Don't notify of vulnerabilities in this app. + */ + public boolean ignoreVulnerabilities; + + public AppPrefs(int ignoreThis, boolean ignoreAll, boolean ignoreVulns) { ignoreThisUpdate = ignoreThis; ignoreAllUpdates = ignoreAll; + ignoreVulnerabilities = ignoreVulns; } public static AppPrefs createDefault() { - return new AppPrefs(0, false); + return new AppPrefs(0, false, false); } @Override public boolean equals(Object o) { return o != null && o instanceof AppPrefs && ((AppPrefs) o).ignoreAllUpdates == ignoreAllUpdates && - ((AppPrefs) o).ignoreThisUpdate == ignoreThisUpdate; + ((AppPrefs) o).ignoreThisUpdate == ignoreThisUpdate && + ((AppPrefs) o).ignoreVulnerabilities == ignoreVulnerabilities; } @Override public int hashCode() { - return (ignoreThisUpdate + "-" + ignoreAllUpdates).hashCode(); + return (ignoreThisUpdate + "-" + ignoreAllUpdates + "-" + ignoreVulnerabilities).hashCode(); } public AppPrefs createClone() { - return new AppPrefs(ignoreThisUpdate, ignoreAllUpdates); + return new AppPrefs(ignoreThisUpdate, ignoreAllUpdates, ignoreVulnerabilities); } } diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppPrefsProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppPrefsProvider.java index 284c559d1..e7a9174be 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppPrefsProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppPrefsProvider.java @@ -20,6 +20,7 @@ public class AppPrefsProvider extends FDroidProvider { ContentValues values = new ContentValues(3); values.put(Cols.IGNORE_ALL_UPDATES, prefs.ignoreAllUpdates); values.put(Cols.IGNORE_THIS_UPDATE, prefs.ignoreThisUpdate); + values.put(Cols.IGNORE_VULNERABILITIES, prefs.ignoreVulnerabilities); if (getPrefsOrNull(context, app) == null) { values.put(Cols.PACKAGE_NAME, app.packageName); @@ -51,7 +52,8 @@ public class AppPrefsProvider extends FDroidProvider { cursor.moveToFirst(); return new AppPrefs( cursor.getInt(cursor.getColumnIndexOrThrow(Cols.IGNORE_THIS_UPDATE)), - cursor.getInt(cursor.getColumnIndexOrThrow(Cols.IGNORE_ALL_UPDATES)) > 0); + cursor.getInt(cursor.getColumnIndexOrThrow(Cols.IGNORE_ALL_UPDATES)) > 0, + cursor.getInt(cursor.getColumnIndexOrThrow(Cols.IGNORE_VULNERABILITIES)) > 0); } finally { cursor.close(); } diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java index 3180d57d2..c58d73faf 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -11,6 +11,7 @@ import android.text.TextUtils; import android.util.Log; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable; import org.fdroid.fdroid.data.Schema.ApkTable; import org.fdroid.fdroid.data.Schema.AppMetadataTable; import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; @@ -133,6 +134,12 @@ public class AppProvider extends FDroidProvider { Uri uri = Uri.withAppendedPath(AppProvider.getContentUri(), PATH_CALC_PREFERRED_METADATA); context.getContentResolver().query(uri, null, null, null, null); } + + public static List findInstalledAppsWithKnownVulns(Context context) { + Uri uri = getInstalledWithKnownVulnsUri(); + Cursor cursor = context.getContentResolver().query(uri, Cols.ALL, null, null, null); + return cursorToList(cursor); + } } /** @@ -150,6 +157,8 @@ public class AppProvider extends FDroidProvider { protected static class AppQuerySelection extends QuerySelection { private boolean naturalJoinToInstalled; + private boolean naturalJoinApks; + private boolean naturalJoinAntiFeatures; private boolean leftJoinPrefs; AppQuerySelection() { @@ -170,6 +179,14 @@ public class AppProvider extends FDroidProvider { return naturalJoinToInstalled; } + public boolean naturalJoinToApks() { + return naturalJoinApks; + } + + public boolean naturalJoinAntiFeatures() { + return naturalJoinAntiFeatures; + } + /** * Tells the query selection that it will need to join onto the installed apps table * when used. This should be called when your query makes use of fields from that table @@ -182,6 +199,22 @@ public class AppProvider extends FDroidProvider { return this; } + /** + * Note that this has large performance implications, so should only be used if you are already limiting + * the result set based on other, more drastic conditions first. + * See https://gitlab.com/fdroid/fdroidclient/issues/1143 for the investigation which identified these + * performance implications. + */ + public AppQuerySelection requireNaturalJoinApks() { + naturalJoinApks = true; + return this; + } + + public AppQuerySelection requireNatrualJoinAntiFeatures() { + naturalJoinAntiFeatures = true; + return this; + } + public boolean leftJoinToPrefs() { return leftJoinPrefs; } @@ -198,9 +231,18 @@ public class AppProvider extends FDroidProvider { bothWithJoin.requireNaturalInstalledTable(); } + if (this.naturalJoinToApks() || query.naturalJoinToApks()) { + bothWithJoin.requireNaturalJoinApks(); + } + if (this.leftJoinToPrefs() || query.leftJoinToPrefs()) { bothWithJoin.requireLeftJoinPrefs(); } + + if (this.naturalJoinAntiFeatures() || query.naturalJoinAntiFeatures()) { + bothWithJoin.requireNatrualJoinAntiFeatures(); + } + return bothWithJoin; } @@ -210,6 +252,8 @@ public class AppProvider extends FDroidProvider { private boolean isSuggestedApkTableAdded; private boolean requiresInstalledTable; + private boolean requiresApkTable; + private boolean requiresAntiFeatures; private boolean requiresLeftJoinToPrefs; private boolean countFieldAppended; @@ -240,9 +284,15 @@ public class AppProvider extends FDroidProvider { if (selection.naturalJoinToInstalled()) { naturalJoinToInstalledTable(); } + if (selection.naturalJoinToApks()) { + naturalJoinToApkTable(); + } if (selection.leftJoinToPrefs()) { leftJoinToPrefs(); } + if (selection.naturalJoinAntiFeatures()) { + naturalJoinAntiFeatures(); + } } // TODO: What if the selection requires a natural join, but we first get a left join @@ -257,6 +307,17 @@ public class AppProvider extends FDroidProvider { } } + public void naturalJoinToApkTable() { + if (!requiresApkTable) { + join( + getApkTableName(), + getApkTableName(), + getApkTableName() + "." + ApkTable.Cols.APP_ID + " = " + getTableName() + "." + Cols.ROW_ID + ); + requiresApkTable = true; + } + } + public void leftJoinToPrefs() { if (!requiresLeftJoinToPrefs) { leftJoin( @@ -277,6 +338,22 @@ public class AppProvider extends FDroidProvider { } } + public void naturalJoinAntiFeatures() { + if (!requiresAntiFeatures) { + join( + getApkAntiFeatureJoinTableName(), + "apkAntiFeature", + "apkAntiFeature." + ApkAntiFeatureJoinTable.Cols.APK_ID + " = " + getApkTableName() + "." + ApkTable.Cols.ROW_ID); + + join( + Schema.AntiFeatureTable.NAME, + "antiFeature", + "antiFeature." + Schema.AntiFeatureTable.Cols.ROW_ID + " = " + "apkAntiFeature." + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID); + + requiresAntiFeatures = true; + } + } + @Override public void addField(String field) { switch (field) { @@ -370,6 +447,7 @@ public class AppProvider extends FDroidProvider { private static final String PATH_CALC_PREFERRED_METADATA = "calcPreferredMetadata"; private static final String PATH_CALC_SUGGESTED_APKS = "calcNonRepoDetailsFromIndex"; private static final String PATH_TOP_FROM_CATEGORY = "topFromCategory"; + private static final String PATH_INSTALLED_WITH_KNOWN_VULNS = "installedWithKnownVulns"; private static final int CAN_UPDATE = CODE_SINGLE + 1; private static final int INSTALLED = CAN_UPDATE + 1; @@ -383,6 +461,7 @@ public class AppProvider extends FDroidProvider { private static final int HIGHEST_PRIORITY = SEARCH_REPO + 1; private static final int CALC_PREFERRED_METADATA = HIGHEST_PRIORITY + 1; private static final int TOP_FROM_CATEGORY = CALC_PREFERRED_METADATA + 1; + private static final int INSTALLED_WITH_KNOWN_VULNS = TOP_FROM_CATEGORY + 1; static { MATCHER.addURI(getAuthority(), null, CODE_LIST); @@ -400,6 +479,7 @@ public class AppProvider extends FDroidProvider { MATCHER.addURI(getAuthority(), PATH_SPECIFIC_APP + "/#/*", CODE_SINGLE); MATCHER.addURI(getAuthority(), PATH_CALC_PREFERRED_METADATA, CALC_PREFERRED_METADATA); MATCHER.addURI(getAuthority(), PATH_TOP_FROM_CATEGORY + "/#/*", TOP_FROM_CATEGORY); + MATCHER.addURI(getAuthority(), PATH_INSTALLED_WITH_KNOWN_VULNS, INSTALLED_WITH_KNOWN_VULNS); } public static Uri getContentUri() { @@ -421,6 +501,12 @@ public class AppProvider extends FDroidProvider { .build(); } + public static Uri getInstalledWithKnownVulnsUri() { + return getContentUri().buildUpon() + .appendPath(PATH_INSTALLED_WITH_KNOWN_VULNS) + .build(); + } + public static Uri getTopFromCategoryUri(String category, int limit) { return getContentUri().buildUpon() .appendPath(PATH_TOP_FROM_CATEGORY) @@ -505,6 +591,10 @@ public class AppProvider extends FDroidProvider { return ApkTable.NAME; } + protected String getApkAntiFeatureJoinTableName() { + return ApkAntiFeatureJoinTable.NAME; + } + @Override protected String getProviderName() { return "AppProvider"; @@ -652,6 +742,24 @@ public class AppProvider extends FDroidProvider { return new AppQuerySelection(selection, args); } + private AppQuerySelection queryInstalledWithKnownVulns() { + String apk = getApkTableName(); + + // Include the hash in this check because otherwise apps with any vulnerable version will + // get returned, rather than just the installed version. + String compareHash = apk + "." + ApkTable.Cols.HASH + " = installed." + InstalledAppTable.Cols.HASH; + String knownVuln = " antiFeature." + Schema.AntiFeatureTable.Cols.NAME + " = 'KnownVuln' "; + String notIgnored = " COALESCE(prefs." + AppPrefsTable.Cols.IGNORE_VULNERABILITIES + ", 0) = 0 "; + + String selection = knownVuln + " AND " + compareHash + " AND " + notIgnored; + + return new AppQuerySelection(selection) + .requireNaturalInstalledTable() + .requireNaturalJoinApks() + .requireNatrualJoinAntiFeatures() + .requireLeftJoinPrefs(); + } + static AppQuerySelection queryPackageNames(String packageNames, String packageNameField) { String[] args = packageNames.split(","); String selection = packageNameField + " IN (" + generateQuestionMarksForInClause(args.length) + ")"; @@ -739,6 +847,11 @@ public class AppProvider extends FDroidProvider { includeSwap = false; break; + case INSTALLED_WITH_KNOWN_VULNS: + selection = selection.add(queryInstalledWithKnownVulns()); + includeSwap = false; + break; + case RECENTLY_UPDATED: String table = getTableName(); String isNew = table + "." + Cols.LAST_UPDATED + " <= " + table + "." + Cols.ADDED + " DESC"; diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 7dddc07b6..8f21df56b 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -34,6 +34,8 @@ import android.util.Log; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Schema.AntiFeatureTable; +import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable; import org.fdroid.fdroid.data.Schema.ApkTable; import org.fdroid.fdroid.data.Schema.CatJoinTable; import org.fdroid.fdroid.data.Schema.PackageTable; @@ -107,8 +109,7 @@ class DBHelper extends SQLiteOpenHelper { + ApkTable.Cols.HASH_TYPE + " string, " + ApkTable.Cols.ADDED_DATE + " string, " + ApkTable.Cols.IS_COMPATIBLE + " int not null, " - + ApkTable.Cols.INCOMPATIBLE_REASONS + " text, " - + ApkTable.Cols.ANTI_FEATURES + " string" + + ApkTable.Cols.INCOMPATIBLE_REASONS + " text" + ");"; static final String CREATE_TABLE_APP_METADATA = "CREATE TABLE " + AppMetadataTable.NAME @@ -157,8 +158,9 @@ class DBHelper extends SQLiteOpenHelper { private static final String CREATE_TABLE_APP_PREFS = "CREATE TABLE " + AppPrefsTable.NAME + " ( " + AppPrefsTable.Cols.PACKAGE_NAME + " TEXT, " - + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + " INT BOOLEAN NOT NULL, " - + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + " INT NOT NULL " + + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + " INT NOT NULL, " + + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + " INT BOOLEAN NOT NULL, " + + AppPrefsTable.Cols.IGNORE_VULNERABILITIES + " INT BOOLEAN NOT NULL " + " );"; private static final String CREATE_TABLE_CATEGORY = "CREATE TABLE " + Schema.CategoryTable.NAME @@ -194,7 +196,19 @@ class DBHelper extends SQLiteOpenHelper { + InstalledAppTable.Cols.HASH + " TEXT NOT NULL" + " );"; - protected static final int DB_VERSION = 74; + private static final String CREATE_TABLE_ANTI_FEATURE = "CREATE TABLE " + AntiFeatureTable.NAME + + " ( " + + AntiFeatureTable.Cols.NAME + " TEXT NOT NULL " + + " );"; + + static final String CREATE_TABLE_APK_ANTI_FEATURE_JOIN = "CREATE TABLE " + ApkAntiFeatureJoinTable.NAME + + " ( " + + ApkAntiFeatureJoinTable.Cols.APK_ID + " INT NOT NULL, " + + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + " INT NOT NULL, " + + "primary key(" + ApkAntiFeatureJoinTable.Cols.APK_ID + ", " + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + ") " + + " );"; + + protected static final int DB_VERSION = 75; private final Context context; @@ -214,6 +228,8 @@ class DBHelper extends SQLiteOpenHelper { db.execSQL(CREATE_TABLE_INSTALLED_APP); db.execSQL(CREATE_TABLE_REPO); db.execSQL(CREATE_TABLE_APP_PREFS); + db.execSQL(CREATE_TABLE_ANTI_FEATURE); + db.execSQL(CREATE_TABLE_APK_ANTI_FEATURE_JOIN); ensureIndexes(db); String[] defaultRepos = context.getResources().getStringArray(R.array.default_repos); @@ -283,6 +299,28 @@ class DBHelper extends SQLiteOpenHelper { addPreferredSignerToApp(db, oldVersion); updatePreferredSignerIfEmpty(db, oldVersion); addIsAppToApp(db, oldVersion); + addApkAntiFeatures(db, oldVersion); + addIgnoreVulnPref(db, oldVersion); + } + + private void addIgnoreVulnPref(SQLiteDatabase db, int oldVersion) { + if (oldVersion >= 74) { + return; + } + + if (!columnExists(db, AppPrefsTable.NAME, AppPrefsTable.Cols.IGNORE_VULNERABILITIES)) { + Utils.debugLog(TAG, "Adding " + AppPrefsTable.Cols.IGNORE_VULNERABILITIES + " field to " + AppPrefsTable.NAME + " table in db."); + db.execSQL("alter table " + AppPrefsTable.NAME + " add column " + AppPrefsTable.Cols.IGNORE_VULNERABILITIES + " boolean;"); + } + } + + private void addApkAntiFeatures(SQLiteDatabase db, int oldVersion) { + if (oldVersion >= 74) { + return; + } + + Log.i(TAG, "Adding anti features on a per-apk basis."); + resetTransient(db); } private void addIsAppToApp(SQLiteDatabase db, int oldVersion) { @@ -436,11 +474,6 @@ class DBHelper extends SQLiteOpenHelper { Utils.debugLog(TAG, "Adding " + RepoTable.Cols.MIRRORS + " field to " + RepoTable.NAME + " table in db."); db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.MIRRORS + " string;"); } - - if (!columnExists(db, ApkTable.NAME, ApkTable.Cols.ANTI_FEATURES)) { - Utils.debugLog(TAG, "Adding " + ApkTable.Cols.ANTI_FEATURES + " field to " + ApkTable.NAME + " table in db."); - db.execSQL("alter table " + ApkTable.NAME + " add column " + ApkTable.Cols.ANTI_FEATURES + " string;"); - } } /** @@ -1059,6 +1092,14 @@ class DBHelper extends SQLiteOpenHelper { db.execSQL("DROP TABLE " + PackageTable.NAME); } + if (tableExists(db, AntiFeatureTable.NAME)) { + db.execSQL("DROP TABLE " + AntiFeatureTable.NAME); + } + + if (tableExists(db, ApkAntiFeatureJoinTable.NAME)) { + db.execSQL("DROP TABLE " + ApkAntiFeatureJoinTable.NAME); + } + db.execSQL("DROP TABLE " + AppMetadataTable.NAME); db.execSQL("DROP TABLE " + ApkTable.NAME); @@ -1067,6 +1108,8 @@ class DBHelper extends SQLiteOpenHelper { db.execSQL(CREATE_TABLE_APK); db.execSQL(CREATE_TABLE_CATEGORY); db.execSQL(CREATE_TABLE_CAT_JOIN); + db.execSQL(CREATE_TABLE_ANTI_FEATURE); + db.execSQL(CREATE_TABLE_APK_ANTI_FEATURE_JOIN); clearRepoEtags(db); ensureIndexes(db); db.setTransactionSuccessful(); diff --git a/app/src/main/java/org/fdroid/fdroid/data/Schema.java b/app/src/main/java/org/fdroid/fdroid/data/Schema.java index fcfdfd6a3..a1f3ac2ad 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java @@ -54,8 +54,9 @@ public interface Schema { String IGNORE_ALL_UPDATES = "ignoreAllUpdates"; String IGNORE_THIS_UPDATE = "ignoreThisUpdate"; + String IGNORE_VULNERABILITIES = "ignoreVulnerabilities"; - String[] ALL = {PACKAGE_NAME, IGNORE_ALL_UPDATES, IGNORE_THIS_UPDATE}; + String[] ALL = {PACKAGE_NAME, IGNORE_ALL_UPDATES, IGNORE_THIS_UPDATE, IGNORE_VULNERABILITIES}; } } @@ -106,6 +107,47 @@ public interface Schema { } } + interface AntiFeatureTable { + + String NAME = "fdroid_antiFeature"; + + interface Cols { + String ROW_ID = "rowid"; + String NAME = "name"; + + String[] ALL = {ROW_ID, NAME}; + } + } + + /** + * An entry in this table signifies that an apk has a particular anti feature. + * @see AntiFeatureTable + * @see ApkTable + */ + interface ApkAntiFeatureJoinTable { + + String NAME = "fdroid_apkAntiFeatureJoin"; + + interface Cols { + /** + * Foreign key to {@link ApkTable}. + * @see ApkTable + */ + String APK_ID = "apkId"; + + /** + * Foreign key to {@link AntiFeatureTable}. + * @see AntiFeatureTable + */ + String ANTI_FEATURE_ID = "antiFeatureId"; + + /** + * @see AppMetadataTable.Cols#ALL_COLS + */ + String[] ALL_COLS = {APK_ID, ANTI_FEATURE_ID}; + } + } + interface AppMetadataTable { String NAME = "fdroid_app"; @@ -258,7 +300,6 @@ public interface Schema { String ADDED_DATE = "added"; String IS_COMPATIBLE = "compatible"; String INCOMPATIBLE_REASONS = "incompatibleReasons"; - String ANTI_FEATURES = "antiFeatures"; interface Repo { String VERSION = "repoVersion"; @@ -269,6 +310,10 @@ public interface Schema { String PACKAGE_NAME = "package_packageName"; } + interface AntiFeatures { + String ANTI_FEATURES = "antiFeatures_commaSeparated"; + } + /** * @see AppMetadataTable.Cols#ALL_COLS */ @@ -277,7 +322,7 @@ public interface Schema { SIZE, SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, TARGET_SDK_VERSION, MAX_SDK_VERSION, OBB_MAIN_FILE, OBB_MAIN_FILE_SHA256, OBB_PATCH_FILE, OBB_PATCH_FILE_SHA256, REQUESTED_PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE, - IS_COMPATIBLE, INCOMPATIBLE_REASONS, ANTI_FEATURES, + IS_COMPATIBLE, INCOMPATIBLE_REASONS, }; /** @@ -289,7 +334,7 @@ public interface Schema { OBB_MAIN_FILE, OBB_MAIN_FILE_SHA256, OBB_PATCH_FILE, OBB_PATCH_FILE_SHA256, REQUESTED_PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE, IS_COMPATIBLE, Repo.VERSION, Repo.ADDRESS, INCOMPATIBLE_REASONS, - ANTI_FEATURES, + AntiFeatures.ANTI_FEATURES, }; } } diff --git a/app/src/main/java/org/fdroid/fdroid/data/TempApkProvider.java b/app/src/main/java/org/fdroid/fdroid/data/TempApkProvider.java index 5a45c827b..3c00ea546 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/TempApkProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/TempApkProvider.java @@ -36,6 +36,11 @@ public class TempApkProvider extends ApkProvider { return TABLE_TEMP_APK; } + @Override + protected String getApkAntiFeatureJoinTableName() { + return TempAppProvider.TABLE_TEMP_APK_ANTI_FEATURE_JOIN; + } + @Override protected String getAppTableName() { return TempAppProvider.TABLE_TEMP_APP; @@ -93,11 +98,23 @@ public class TempApkProvider extends ApkProvider { final SQLiteDatabase db = db(); final String memoryDbName = TempAppProvider.DB; db.execSQL(DBHelper.CREATE_TABLE_APK.replaceFirst(ApkTable.NAME, memoryDbName + "." + getTableName())); + db.execSQL(DBHelper.CREATE_TABLE_APK_ANTI_FEATURE_JOIN.replaceFirst(Schema.ApkAntiFeatureJoinTable.NAME, memoryDbName + "." + getApkAntiFeatureJoinTableName())); String where = ApkTable.NAME + "." + Cols.REPO_ID + " != ?"; String[] whereArgs = new String[]{Long.toString(repoIdBeingUpdated)}; db.execSQL(TempAppProvider.copyData(Cols.ALL_COLS, ApkTable.NAME, memoryDbName + "." + getTableName(), where), whereArgs); + String antiFeaturesWhere = + Schema.ApkAntiFeatureJoinTable.NAME + "." + Schema.ApkAntiFeatureJoinTable.Cols.APK_ID + " IN " + + "(SELECT innerApk." + Cols.ROW_ID + " FROM " + ApkTable.NAME + " AS innerApk " + + "WHERE innerApk." + Cols.REPO_ID + " != ?)"; + + db.execSQL(TempAppProvider.copyData( + Schema.ApkAntiFeatureJoinTable.Cols.ALL_COLS, + Schema.ApkAntiFeatureJoinTable.NAME, + memoryDbName + "." + getApkAntiFeatureJoinTableName(), + antiFeaturesWhere), whereArgs); + db.execSQL("CREATE INDEX IF NOT EXISTS " + memoryDbName + ".apk_appId on " + getTableName() + " (" + Cols.APP_ID + ");"); db.execSQL("CREATE INDEX IF NOT EXISTS " + memoryDbName + ".apk_compatible ON " + getTableName() + " (" + Cols.IS_COMPATIBLE + ");"); } diff --git a/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java index 1f5ebb3a4..0f37a4fb9 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java @@ -30,6 +30,7 @@ public class TempAppProvider extends AppProvider { private static final String PROVIDER_NAME = "TempAppProvider"; static final String TABLE_TEMP_APP = "temp_" + AppMetadataTable.NAME; + static final String TABLE_TEMP_APK_ANTI_FEATURE_JOIN = "temp_" + Schema.ApkAntiFeatureJoinTable.NAME; static final String TABLE_TEMP_CAT_JOIN = "temp_" + CatJoinTable.NAME; private static final String PATH_INIT = "init"; @@ -125,6 +126,10 @@ public class TempAppProvider extends AppProvider { return TempApkProvider.TABLE_TEMP_APK; } + protected String getApkAntiFeatureJoinTableName() { + return TempApkProvider.TABLE_TEMP_APK; + } + @Override public Uri insert(Uri uri, ContentValues values) { switch (MATCHER.match(uri)) { @@ -218,6 +223,7 @@ public class TempAppProvider extends AppProvider { final String tempApp = DB + "." + TABLE_TEMP_APP; final String tempApk = DB + "." + TempApkProvider.TABLE_TEMP_APK; final String tempCatJoin = DB + "." + TABLE_TEMP_CAT_JOIN; + final String tempAntiFeatureJoin = DB + "." + TABLE_TEMP_APK_ANTI_FEATURE_JOIN; final String[] repoArgs = new String[]{Long.toString(repoIdToCommit)}; @@ -230,6 +236,16 @@ public class TempAppProvider extends AppProvider { db.execSQL("DELETE FROM " + CatJoinTable.NAME + " WHERE " + getCatRepoWhere(CatJoinTable.NAME), repoArgs); db.execSQL(copyData(CatJoinTable.Cols.ALL_COLS, tempCatJoin, CatJoinTable.NAME, getCatRepoWhere(tempCatJoin)), repoArgs); + db.execSQL( + "DELETE FROM " + Schema.ApkAntiFeatureJoinTable.NAME + " " + + "WHERE " + getAntiFeatureRepoWhere(Schema.ApkAntiFeatureJoinTable.NAME), repoArgs); + + db.execSQL(copyData( + Schema.ApkAntiFeatureJoinTable.Cols.ALL_COLS, + tempAntiFeatureJoin, + Schema.ApkAntiFeatureJoinTable.NAME, + getAntiFeatureRepoWhere(tempAntiFeatureJoin)), repoArgs); + db.setTransactionSuccessful(); getContext().getContentResolver().notifyChange(AppProvider.getContentUri(), null); @@ -250,4 +266,13 @@ public class TempAppProvider extends AppProvider { return CatJoinTable.Cols.ROW_ID + " IN (" + catRepoSubquery + ")"; } + + private String getAntiFeatureRepoWhere(String antiFeatureTable) { + String subquery = + "SELECT innerApk." + ApkTable.Cols.ROW_ID + " " + + "FROM " + ApkTable.NAME + " AS innerApk " + + "WHERE innerApk." + ApkTable.Cols.REPO_ID + " = ?"; + + return antiFeatureTable + "." + Schema.ApkAntiFeatureJoinTable.Cols.APK_ID + " IN (" + subquery + ")"; + } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java index 93c2f7489..b9a0b1168 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java @@ -93,6 +93,9 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { @Nullable private final Button actionButton; + @Nullable + private final Button secondaryButton; + private final DisplayImageOptions displayImageOptions; @Nullable @@ -137,11 +140,16 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { progressBar = (ProgressBar) itemView.findViewById(R.id.progress_bar); cancelButton = (ImageButton) itemView.findViewById(R.id.cancel_button); actionButton = (Button) itemView.findViewById(R.id.action_button); + secondaryButton = (Button) itemView.findViewById(R.id.secondary_button); if (actionButton != null) { actionButton.setOnClickListener(onActionClicked); } + if (secondaryButton != null) { + secondaryButton.setOnClickListener(onSecondaryButtonClicked); + } + if (cancelButton != null) { cancelButton.setOnClickListener(onCancelDownload); } @@ -213,6 +221,15 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { } } + if (secondaryButton != null) { + if (viewState.shouldShowSecondaryButton()) { + secondaryButton.setVisibility(View.VISIBLE); + secondaryButton.setText(viewState.getSecondaryButtonText()); + } else { + secondaryButton.setVisibility(View.GONE); + } + } + if (progressBar != null) { if (viewState.showProgress()) { progressBar.setVisibility(View.VISIBLE); @@ -388,55 +405,74 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { return; } - // When the button says "Run", then launch the app. - if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.Installed) { - Intent intent = activity.getPackageManager().getLaunchIntentForPackage(currentApp.packageName); - if (intent != null) { - activity.startActivity(intent); + onActionButtonPressed(currentApp); + } + }; - // Once it is explicitly launched by the user, then we can pretty much forget about - // any sort of notification that the app was successfully installed. It should be - // apparent to the user because they just launched it. - AppUpdateStatusManager.getInstance(activity).removeApk(currentStatus.getUniqueKey()); - } + @SuppressWarnings("FieldCanBeLocal") + private final View.OnClickListener onSecondaryButtonClicked = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (currentApp == null) { return; } - if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) { - Uri apkDownloadUri = Uri.parse(currentStatus.apk.getUrl()); - File apkFilePath = ApkCache.getApkDownloadPath(activity, apkDownloadUri); - Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getUrl() + - " to " + apkFilePath); - - // TODO: This seems like a bit of a hack. Is there a better way to do this by changing - // the Installer API so that we can ask it to install without having to get it to fire - // off an intent which we then listen for and action? - final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity); - final BroadcastReceiver receiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - broadcastManager.unregisterReceiver(this); - - if (Installer.ACTION_INSTALL_USER_INTERACTION.equals(intent.getAction())) { - PendingIntent pendingIntent = - intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); - try { - pendingIntent.send(); - } catch (PendingIntent.CanceledException ignored) { } - } - } - }; - - broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(apkDownloadUri)); - Installer installer = InstallerFactory.create(activity, currentStatus.apk); - installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), apkDownloadUri); - } else { - final Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, currentApp); - InstallManagerService.queue(activity, currentApp, suggestedApk); - } + onSecondaryButtonPressed(currentApp); } }; + protected void onActionButtonPressed(@NonNull App app) { + // When the button says "Run", then launch the app. + if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.Installed) { + Intent intent = activity.getPackageManager().getLaunchIntentForPackage(app.packageName); + if (intent != null) { + activity.startActivity(intent); + + // Once it is explicitly launched by the user, then we can pretty much forget about + // any sort of notification that the app was successfully installed. It should be + // apparent to the user because they just launched it. + AppUpdateStatusManager.getInstance(activity).removeApk(currentStatus.getUniqueKey()); + } + return; + } + + if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) { + Uri apkDownloadUri = Uri.parse(currentStatus.apk.getUrl()); + File apkFilePath = ApkCache.getApkDownloadPath(activity, apkDownloadUri); + Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getUrl() + + " to " + apkFilePath); + + // TODO: This seems like a bit of a hack. Is there a better way to do this by changing + // the Installer API so that we can ask it to install without having to get it to fire + // off an intent which we then listen for and action? + final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity); + final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + broadcastManager.unregisterReceiver(this); + + if (Installer.ACTION_INSTALL_USER_INTERACTION.equals(intent.getAction())) { + PendingIntent pendingIntent = + intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); + try { + pendingIntent.send(); + } catch (PendingIntent.CanceledException ignored) { } + } + } + }; + + broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(apkDownloadUri)); + Installer installer = InstallerFactory.create(activity, currentStatus.apk); + installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), apkDownloadUri); + } else { + final Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app); + InstallManagerService.queue(activity, app, suggestedApk); + } + } + + /** To be overridden by subclasses if desired */ + protected void onSecondaryButtonPressed(@NonNull App app) { } + @SuppressWarnings("FieldCanBeLocal") private final View.OnClickListener onCancelDownload = new View.OnClickListener() { @Override diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemState.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemState.java index 2a1e31fe8..2fef4de81 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemState.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemState.java @@ -14,6 +14,7 @@ public class AppListItemState { private final App app; private CharSequence mainText = null; private CharSequence actionButtonText = null; + private CharSequence secondaryButtonText = null; private CharSequence statusText = null; private CharSequence secondaryStatusText = null; private int progressCurrent = -1; @@ -34,6 +35,11 @@ public class AppListItemState { return this; } + public AppListItemState showSecondaryButton(CharSequence label) { + secondaryButtonText = label; + return this; + } + public AppListItemState setStatusText(CharSequence text) { this.statusText = text; return this; @@ -74,6 +80,14 @@ public class AppListItemState { return actionButtonText; } + public boolean shouldShowSecondaryButton() { + return secondaryButtonText != null; + } + + public CharSequence getSecondaryButtonText() { + return secondaryButtonText; + } + public boolean showProgress() { return progressCurrent >= 0; } diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java index dfa046171..a993910a6 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/UpdatesAdapter.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; @@ -20,6 +21,7 @@ import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.views.updates.items.AppStatus; import org.fdroid.fdroid.views.updates.items.AppUpdateData; +import org.fdroid.fdroid.views.updates.items.KnownVulnApp; import org.fdroid.fdroid.views.updates.items.UpdateableApp; import org.fdroid.fdroid.views.updates.items.UpdateableAppsHeader; @@ -65,6 +67,9 @@ import java.util.Set; public class UpdatesAdapter extends RecyclerView.Adapter implements LoaderManager.LoaderCallbacks { + private static final int LOADER_CAN_UPDATE = 289753982; + private static final int LOADER_KNOWN_VULN = 520389740; + private final AdapterDelegatesManager> delegatesManager = new AdapterDelegatesManager<>(); private final List items = new ArrayList<>(); @@ -72,6 +77,7 @@ public class UpdatesAdapter extends RecyclerView.Adapter appsToShowStatus = new ArrayList<>(); private final List updateableApps = new ArrayList<>(); + private final List knownVulnApps = new ArrayList<>(); private boolean showAllUpdateableApps = false; @@ -80,9 +86,11 @@ public class UpdatesAdapter extends RecyclerView.Adapter onCreateLoader(int id, Bundle args) { + Uri uri; + switch (id) { + case LOADER_CAN_UPDATE: + uri = AppProvider.getCanUpdateUri(); + break; + + case LOADER_KNOWN_VULN: + uri = AppProvider.getInstalledWithKnownVulnsUri(); + break; + + default: + throw new IllegalStateException("Unknown loader requested: " + id); + } + return new CursorLoader( - activity, - AppProvider.getCanUpdateUri(), - new String[]{ - Schema.AppMetadataTable.Cols._ID, // Required for cursor loader to work. - Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME, - Schema.AppMetadataTable.Cols.NAME, - Schema.AppMetadataTable.Cols.SUMMARY, - Schema.AppMetadataTable.Cols.IS_COMPATIBLE, - Schema.AppMetadataTable.Cols.LICENSE, - Schema.AppMetadataTable.Cols.ICON, - Schema.AppMetadataTable.Cols.ICON_URL, - Schema.AppMetadataTable.Cols.InstalledApp.VERSION_CODE, - Schema.AppMetadataTable.Cols.InstalledApp.VERSION_NAME, - Schema.AppMetadataTable.Cols.SuggestedApk.VERSION_NAME, - Schema.AppMetadataTable.Cols.SUGGESTED_VERSION_CODE, - Schema.AppMetadataTable.Cols.REQUIREMENTS, // Needed for filtering apps that require root. - Schema.AppMetadataTable.Cols.ANTI_FEATURES, // Needed for filtering apps that require anti-features. - }, - null, - null, - Schema.AppMetadataTable.Cols.NAME - ); + activity, uri, Schema.AppMetadataTable.Cols.ALL, null, null, Schema.AppMetadataTable.Cols.NAME); } @Override public void onLoadFinished(Loader loader, Cursor cursor) { + switch (loader.getId()) { + case LOADER_CAN_UPDATE: + onCanUpdateLoadFinished(cursor); + break; + + case LOADER_KNOWN_VULN: + onKnownVulnLoadFinished(cursor); + break; + } + + populateItems(); + notifyDataSetChanged(); + } + + private void onCanUpdateLoadFinished(Cursor cursor) { updateableApps.clear(); cursor.moveToFirst(); @@ -220,9 +240,16 @@ public class UpdatesAdapter extends RecyclerView.Adapter> { + + private final Activity activity; + + public Delegate(Activity activity) { + this.activity = activity; + } + + @Override + protected boolean isForViewType(@NonNull List items, int position) { + return items.get(position) instanceof KnownVulnApp; + } + + @NonNull + @Override + protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new KnownVulnAppListItemController(activity, activity.getLayoutInflater() + .inflate(R.layout.known_vuln_app_list_item, parent, false)); + } + + @Override + protected void onBindViewHolder(@NonNull List items, int position, + @NonNull RecyclerView.ViewHolder holder, @NonNull List payloads) { + KnownVulnApp app = (KnownVulnApp) items.get(position); + ((KnownVulnAppListItemController) holder).bindModel(app.app); + } + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java new file mode 100644 index 000000000..23f7bea45 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java @@ -0,0 +1,132 @@ +package org.fdroid.fdroid.views.updates.items; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.LocalBroadcastManager; +import android.view.View; + +import org.fdroid.fdroid.AppUpdateStatusManager; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppPrefs; +import org.fdroid.fdroid.data.AppPrefsProvider; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.installer.InstallManagerService; +import org.fdroid.fdroid.installer.Installer; +import org.fdroid.fdroid.installer.InstallerService; +import org.fdroid.fdroid.views.apps.AppListItemController; +import org.fdroid.fdroid.views.apps.AppListItemState; + +/** + * Tell the user that an app they have installed has a known vulnerability. + * The role of this controller is to prompt the user what it is that should be done in response to this + * (e.g. uninstall, update, disable). + */ +public class KnownVulnAppListItemController extends AppListItemController { + public KnownVulnAppListItemController(Activity activity, View itemView) { + super(activity, itemView); + } + + @NonNull + @Override + protected AppListItemState getCurrentViewState( + @NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) { + String mainText; + String actionButtonText; + + Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app); + if (shouldUpgradeInsteadOfUninstall(app, suggestedApk)) { + mainText = activity.getString(R.string.updates__app_with_known_vulnerability__prompt_upgrade, app.name); + actionButtonText = activity.getString(R.string.menu_upgrade); + } else { + mainText = activity.getString(R.string.updates__app_with_known_vulnerability__prompt_uninstall, app.name); + actionButtonText = activity.getString(R.string.menu_uninstall); + } + + return new AppListItemState(app) + .setMainText(mainText) + .showActionButton(actionButtonText) + .showSecondaryButton(activity.getString(R.string.updates__app_with_known_vulnerability__ignore)); + } + + private boolean shouldUpgradeInsteadOfUninstall(@NonNull App app, @Nullable Apk suggestedApk) { + return suggestedApk != null && app.installedVersionCode < suggestedApk.versionCode; + } + + @Override + protected void onActionButtonPressed(@NonNull App app) { + Apk installedApk = app.getInstalledApk(activity); + if (installedApk == null) { + throw new IllegalStateException( + "Tried to upgrade or uninstall app with known vulnerability but it doesn't seem to be installed"); + } + + Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app); + if (shouldUpgradeInsteadOfUninstall(app, suggestedApk)) { + LocalBroadcastManager manager = LocalBroadcastManager.getInstance(activity); + Uri uri = Uri.parse(suggestedApk.getUrl()); + manager.registerReceiver(installReceiver, Installer.getInstallIntentFilter(uri)); + InstallManagerService.queue(activity, app, suggestedApk); + } else { + LocalBroadcastManager manager = LocalBroadcastManager.getInstance(activity); + manager.registerReceiver(installReceiver, Installer.getUninstallIntentFilter(app.packageName)); + InstallerService.uninstall(activity, installedApk); + } + } + + @Override + protected void onSecondaryButtonPressed(@NonNull App app) { + AppPrefs prefs = app.getPrefs(activity); + prefs.ignoreVulnerabilities = true; + AppPrefsProvider.Helper.update(activity, app, prefs); + refreshUpdatesList(); + } + + private void unregisterInstallReceiver() { + LocalBroadcastManager.getInstance(activity).unregisterReceiver(installReceiver); + } + + /** + * Trigger the LoaderManager in UpdatesAdapter to automatically requery for the list of + * apps with known vulnerabilities (i.e. this app should no longer be in that list). + */ + private void refreshUpdatesList() { + activity.getContentResolver().notifyChange(AppProvider.getInstalledWithKnownVulnsUri(), null); + } + + private final BroadcastReceiver installReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case Installer.ACTION_INSTALL_COMPLETE: + case Installer.ACTION_UNINSTALL_COMPLETE: + refreshUpdatesList(); + unregisterInstallReceiver(); + break; + + case Installer.ACTION_INSTALL_INTERRUPTED: + case Installer.ACTION_UNINSTALL_INTERRUPTED: + unregisterInstallReceiver(); + break; + + case Installer.ACTION_INSTALL_USER_INTERACTION: + case Installer.ACTION_UNINSTALL_USER_INTERACTION: + PendingIntent uninstallPendingIntent = + intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); + + try { + uninstallPendingIntent.send(); + } catch (PendingIntent.CanceledException ignored) { } + break; + } + } + }; +} diff --git a/app/src/main/res/drawable/ic_known_vuln_overlay.xml b/app/src/main/res/drawable/ic_known_vuln_overlay.xml new file mode 100644 index 000000000..382d6dfe3 --- /dev/null +++ b/app/src/main/res/drawable/ic_known_vuln_overlay.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/layout/known_vuln_app_list_item.xml b/app/src/main/res/layout/known_vuln_app_list_item.xml new file mode 100644 index 000000000..80a4ff265 --- /dev/null +++ b/app/src/main/res/layout/known_vuln_app_list_item.xml @@ -0,0 +1,78 @@ + + + + + + + + + + +