Merge branch 'issue-1070--known-vuln--ui' into 'master'

Show apps with known vulnerabilities in the updates tab.

Closes #1070

See merge request fdroid/fdroidclient!558
This commit is contained in:
Hans-Christoph Steiner 2017-09-27 12:46:59 +00:00
commit 8bececfa58
27 changed files with 1067 additions and 159 deletions

View File

@ -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;
}

View File

@ -241,7 +241,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, 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<Apk>, 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;
}

View File

@ -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<String> 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);
}

View File

@ -82,13 +82,6 @@ public class App extends ValueObject implements Comparable<App>, 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<App>, 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<App>, 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)) {

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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<App> 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";

View File

@ -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();

View File

@ -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,
};
}
}

View File

@ -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 + ");");
}

View File

@ -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 + ")";
}
}

View File

@ -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,9 +405,26 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
return;
}
onActionButtonPressed(currentApp);
}
};
@SuppressWarnings("FieldCanBeLocal")
private final View.OnClickListener onSecondaryButtonClicked = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentApp == null) {
return;
}
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(currentApp.packageName);
Intent intent = activity.getPackageManager().getLaunchIntentForPackage(app.packageName);
if (intent != null) {
activity.startActivity(intent);
@ -431,11 +465,13 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
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);
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() {

View File

@ -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;
}

View File

@ -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<RecyclerView.ViewHolder>
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int LOADER_CAN_UPDATE = 289753982;
private static final int LOADER_KNOWN_VULN = 520389740;
private final AdapterDelegatesManager<List<AppUpdateData>> delegatesManager = new AdapterDelegatesManager<>();
private final List<AppUpdateData> items = new ArrayList<>();
@ -72,6 +77,7 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
private final List<AppStatus> appsToShowStatus = new ArrayList<>();
private final List<UpdateableApp> updateableApps = new ArrayList<>();
private final List<KnownVulnApp> knownVulnApps = new ArrayList<>();
private boolean showAllUpdateableApps = false;
@ -80,9 +86,11 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
delegatesManager.addDelegate(new AppStatus.Delegate(activity))
.addDelegate(new UpdateableApp.Delegate(activity))
.addDelegate(new UpdateableAppsHeader.Delegate(activity));
.addDelegate(new UpdateableAppsHeader.Delegate(activity))
.addDelegate(new KnownVulnApp.Delegate(activity));
activity.getSupportLoaderManager().initLoader(0, null, this);
activity.getSupportLoaderManager().initLoader(LOADER_CAN_UPDATE, null, this);
activity.getSupportLoaderManager().initLoader(LOADER_KNOWN_VULN, null, this);
}
/**
@ -162,6 +170,10 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
}
}
}
for (KnownVulnApp app : knownVulnApps) {
items.add(app);
}
}
@Override
@ -186,33 +198,41 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
@Override
public Loader<Cursor> 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<Cursor> 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<RecyclerView.ViewHolder
updateableApps.add(new UpdateableApp(activity, new App(cursor)));
cursor.moveToNext();
}
}
populateItems();
notifyDataSetChanged();
private void onKnownVulnLoadFinished(Cursor cursor) {
knownVulnApps.clear();
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
knownVulnApps.add(new KnownVulnApp(activity, new App(cursor)));
cursor.moveToNext();
}
}
@Override

View File

@ -0,0 +1,60 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import java.util.List;
/**
* List of all apps which can be updated, but have not yet been downloaded.
*
* @see KnownVulnApp The data that is bound to this view.
* @see R.layout#known_vuln_app_list_item The view that this binds to.
* @see KnownVulnAppListItemController Used for binding the {@link App} to
* the {@link R.layout#known_vuln_app_list_item}
*/
public class KnownVulnApp extends AppUpdateData {
public final App app;
public KnownVulnApp(Activity activity, App app) {
super(activity);
this.app = app;
}
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
private final Activity activity;
public Delegate(Activity activity) {
this.activity = activity;
}
@Override
protected boolean isForViewType(@NonNull List<AppUpdateData> 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<AppUpdateData> items, int position,
@NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
KnownVulnApp app = (KnownVulnApp) items.get(position);
((KnownVulnAppListItemController) holder).bindModel(app.app);
}
}
}

View File

@ -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;
}
}
};
}

View File

@ -0,0 +1,19 @@
<vector android:height="24dp" android:viewportHeight="12.7"
android:viewportWidth="12.7" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M6.35,6.35m-6.35,0a6.35,6.35 0,1 1,12.7 0a6.35,6.35 0,1 1,-12.7 0"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.52916664"/>
<path android:fillAlpha="1" android:fillColor="#ff3600"
android:pathData="M6.35,6.35m-5.33,0a5.33,5.33 0,1 1,10.661 0a5.33,5.33 0,1 1,-10.661 0"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.52916664"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M5.863,3.778h0.904v3.082h-0.904z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.52916664"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M5.863,8.899l0.904,0l0,-0.973l-0.904,0z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.52916664"/>
</vector>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:clipToPadding="false">
<!-- Ignore ContentDescription because it is kind of meaningless to have TTS read out "App icon"
when it will inevitably read out the name of the app straight after. -->
<ImageView
android:id="@+id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
tools:ignore="ContentDescription" />
<ImageView
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="48dp"
android:layout_marginLeft="48dp"
android:layout_marginTop="40dp"
android:src="@drawable/ic_known_vuln_overlay"
android:scaleType="fitCenter"
tools:ignore="ContentDescription"/>
<TextView
android:id="@+id/app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="We found a vulnerability with VulnApp. We recommend uninstalling this app immediately."
android:textSize="16sp"
android:textColor="?attr/installedApps"
android:ellipsize="end"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginRight="8dp" />
<Button
android:id="@+id/action_button"
style="@style/DetailsPrimaryButtonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/app_name"
tools:text="Uninstall"/>
<Button
android:id="@+id/secondary_button"
style="@style/DetailsSecondaryButtonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toStartOf="@+id/action_button"
app:layout_constraintTop_toBottomOf="@+id/app_name"
tools:text="Ignore"/>
</android.support.constraint.ConstraintLayout>

View File

@ -97,9 +97,12 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="updates__tts__download_app">Download</string>
<string name="updates__tts__download_updates_for_all_apps">Download all updates</string>
<string name="updates__app_with_known_vulnerability__prompt_uninstall">We found a vulnerability with %1$s. We recommend uninstalling this app immediately.</string>
<string name="updates__app_with_known_vulnerability__prompt_upgrade">We found a vulnerability with %1$s. We recommend upgrading to the newest version immediately.</string>
<string name="updates__app_with_known_vulnerability__ignore">Ignore</string>
<string name="updates__hide_updateable_apps">Hide apps</string>
<string name="updates__show_updateable_apps">Show apps</string>
<plurals name="updates__download_updates_for_apps">
<item quantity="one">Download update for %1$d app.</item>
<item quantity="other">Download updates for %1$d apps.</item>

View File

@ -24,6 +24,10 @@
<item name="android:background">@drawable/button_secondary_background_selector</item>
</style>
<style name="DetailsSecondaryButtonStyleSmall" parent="DetailsSecondaryButtonStyle">
<item name="android:padding">8dp</item>
</style>
<style name="DetailsMoreButtonStyle">
<item name="android:padding">5dp</item>
<item name="android:textSize">15sp</item>

View File

@ -0,0 +1,149 @@
package org.fdroid.fdroid;
import android.app.Application;
import android.content.ContentValues;
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.data.FDroidProviderTest;
import org.fdroid.fdroid.data.InstalledAppTestUtils;
import org.fdroid.fdroid.data.Schema;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.List;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@Config(constants = BuildConfig.class, application = Application.class, sdk = 24)
@RunWith(RobolectricTestRunner.class)
public class AntiFeaturesTest extends FDroidProviderTest {
private App notVuln;
private App allVuln;
private App vulnAtV2;
@Before
public void setup() {
Preferences.setup(context);
ContentValues vulnValues = new ContentValues(1);
vulnValues.put(Schema.ApkTable.Cols.AntiFeatures.ANTI_FEATURES, "KnownVuln,ContainsGreenButtons");
vulnAtV2 = Assert.insertApp(context, "com.vuln", "Fixed it");
insertApk(vulnAtV2, 1, false);
insertApk(vulnAtV2, 2, true);
insertApk(vulnAtV2, 3, false);
notVuln = Assert.insertApp(context, "com.not-vuln", "It's Fine");
insertApk(notVuln, 5, false);
insertApk(notVuln, 10, false);
insertApk(notVuln, 15, false);
allVuln = Assert.insertApp(context, "com.all-vuln", "Oops");
insertApk(allVuln, 100, true);
insertApk(allVuln, 101, true);
insertApk(allVuln, 105, true);
AppProvider.Helper.recalculatePreferredMetadata(context);
}
@After
public void tearDown() {
Preferences.clearSingletonForTesting();
}
private static String generateHash(String packageName, int versionCode) {
return packageName + "-" + versionCode;
}
private void insertApk(App app, int versionCode, boolean isVuln) {
ContentValues values = new ContentValues();
values.put(Schema.ApkTable.Cols.HASH, generateHash(app.packageName, versionCode));
if (isVuln) {
values.put(Schema.ApkTable.Cols.AntiFeatures.ANTI_FEATURES, "KnownVuln,ContainsGreenButtons");
}
Assert.insertApk(context, app, versionCode, values);
}
private void install(App app, int versionCode) {
String hash = generateHash(app.packageName, versionCode);
InstalledAppTestUtils.install(context, app.packageName, versionCode, "v" + versionCode, null, hash);
}
@Test
public void noVulnerableApps() {
List<App> installed = AppProvider.Helper.findInstalledAppsWithKnownVulns(context);
assertEquals(0, installed.size());
}
@Test
public void futureVersionIsVulnerable() {
install(vulnAtV2, 1);
List<App> installed = AppProvider.Helper.findInstalledAppsWithKnownVulns(context);
assertEquals(0, installed.size());
}
@Test
public void vulnerableAndAbleToBeUpdated() {
install(vulnAtV2, 2);
List<App> installed = AppProvider.Helper.findInstalledAppsWithKnownVulns(context);
assertEquals(1, installed.size());
assertEquals(vulnAtV2.packageName, installed.get(0).packageName);
}
@Test
public void vulnerableButUpToDate() {
install(vulnAtV2, 3);
List<App> installed = AppProvider.Helper.findInstalledAppsWithKnownVulns(context);
assertEquals(0, installed.size());
}
@Test
public void allVulnerableButIgnored() {
install(allVuln, 101);
List<App> installed = AppProvider.Helper.findInstalledAppsWithKnownVulns(context);
assertEquals(1, installed.size());
App app = installed.get(0);
AppPrefs prefs = app.getPrefs(context);
prefs.ignoreVulnerabilities = true;
AppPrefsProvider.Helper.update(context, app, prefs);
List<App> installedButIgnored = AppProvider.Helper.findInstalledAppsWithKnownVulns(context);
assertEquals(0, installedButIgnored.size());
}
@Test
public void antiFeaturesSaveCorrectly() {
List<Apk> notVulnApks = ApkProvider.Helper.findByPackageName(context, notVuln.packageName);
assertEquals(3, notVulnApks.size());
List<Apk> allVulnApks = ApkProvider.Helper.findByPackageName(context, allVuln.packageName);
assertEquals(3, allVulnApks.size());
for (Apk apk : allVulnApks) {
assertArrayEquals(new String[]{"KnownVuln", "ContainsGreenButtons"}, apk.antiFeatures);
}
List<Apk> vulnAtV2Apks = ApkProvider.Helper.findByPackageName(context, vulnAtV2.packageName);
assertEquals(3, vulnAtV2Apks.size());
for (Apk apk : vulnAtV2Apks) {
if (apk.versionCode == 2) {
assertArrayEquals(new String[]{"KnownVuln", "ContainsGreenButtons"}, apk.antiFeatures);
} else {
assertNull(apk.antiFeatures);
}
}
}
}

View File

@ -22,6 +22,7 @@ import java.util.List;
import static org.fdroid.fdroid.Assert.assertCantDelete;
import static org.fdroid.fdroid.Assert.assertResultCount;
import static org.fdroid.fdroid.Assert.insertApp;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@ -309,10 +310,12 @@ public class ApkProviderTest extends FDroidProviderTest {
assertEquals("com.example", apk.packageName);
assertEquals(10, apk.versionCode);
assertNull(apk.antiFeatures);
assertNull(apk.features);
assertNull(apk.added);
assertNull(apk.hashType);
apk.antiFeatures = new String[] {"KnownVuln", "Other anti feature"};
apk.features = new String[] {"one", "two", "three" };
long dateTimestamp = System.currentTimeMillis();
apk.added = new Date(dateTimestamp);
@ -335,14 +338,8 @@ public class ApkProviderTest extends FDroidProviderTest {
assertEquals("com.example", updatedApk.packageName);
assertEquals(10, updatedApk.versionCode);
assertNotNull(updatedApk.features);
assertNotNull(updatedApk.added);
assertNotNull(updatedApk.hashType);
assertEquals(3, updatedApk.features.length);
assertEquals("one", updatedApk.features[0]);
assertEquals("two", updatedApk.features[1]);
assertEquals("three", updatedApk.features[2]);
assertArrayEquals(new String[]{"KnownVuln", "Other anti feature"}, updatedApk.antiFeatures);
assertArrayEquals(new String[]{"one", "two", "three"}, updatedApk.features);
assertEquals(new Date(dateTimestamp).getYear(), updatedApk.added.getYear());
assertEquals(new Date(dateTimestamp).getMonth(), updatedApk.added.getMonth());
assertEquals(new Date(dateTimestamp).getDay(), updatedApk.added.getDay());

View File

@ -28,16 +28,19 @@ public class AppPrefsProviderTest extends FDroidProviderTest {
@SuppressWarnings({"PMD.EqualsNull", "EqualsWithItself", "EqualsBetweenInconvertibleTypes", "ObjectEqualsNull"})
@Test
public void prefEquality() {
AppPrefs original = new AppPrefs(101, true);
AppPrefs original = new AppPrefs(101, true, true);
assertTrue(original.equals(new AppPrefs(101, true)));
assertTrue(original.equals(new AppPrefs(101, true, true)));
assertTrue(original.equals(original));
assertFalse(original.equals(null));
assertFalse(original.equals("String"));
assertFalse(original.equals(new AppPrefs(102, true)));
assertFalse(original.equals(new AppPrefs(101, false)));
assertFalse(original.equals(new AppPrefs(100, false)));
assertFalse(original.equals(new AppPrefs(102, true, true)));
assertFalse(original.equals(new AppPrefs(101, false, true)));
assertFalse(original.equals(new AppPrefs(100, false, true)));
assertFalse(original.equals(new AppPrefs(102, true, false)));
assertFalse(original.equals(new AppPrefs(101, false, false)));
assertFalse(original.equals(new AppPrefs(100, false, false)));
}
@Test
@ -51,16 +54,19 @@ public class AppPrefsProviderTest extends FDroidProviderTest {
AppPrefs defaultPrefs = AppPrefsProvider.Helper.getPrefsOrDefault(context, withPrefs);
assertEquals(0, defaultPrefs.ignoreThisUpdate);
assertFalse(defaultPrefs.ignoreAllUpdates);
assertFalse(defaultPrefs.ignoreVulnerabilities);
AppPrefsProvider.Helper.update(context, withPrefs, new AppPrefs(12, false));
AppPrefsProvider.Helper.update(context, withPrefs, new AppPrefs(12, false, false));
AppPrefs newPrefs = AppPrefsProvider.Helper.getPrefsOrDefault(context, withPrefs);
assertEquals(12, newPrefs.ignoreThisUpdate);
assertFalse(newPrefs.ignoreAllUpdates);
assertFalse(newPrefs.ignoreVulnerabilities);
AppPrefsProvider.Helper.update(context, withPrefs, new AppPrefs(14, true));
AppPrefsProvider.Helper.update(context, withPrefs, new AppPrefs(14, true, true));
AppPrefs evenNewerPrefs = AppPrefsProvider.Helper.getPrefsOrDefault(context, withPrefs);
assertEquals(14, evenNewerPrefs.ignoreThisUpdate);
assertTrue(evenNewerPrefs.ignoreAllUpdates);
assertTrue(evenNewerPrefs.ignoreVulnerabilities);
assertNull(AppPrefsProvider.Helper.getPrefsOrNull(context, withoutPrefs));
}

View File

@ -97,7 +97,7 @@ public class AppProviderTest extends FDroidProviderTest {
String packageName, int installedVercode, int suggestedVercode,
boolean ignoreAll, int ignoreVercode) {
App app = insertApp(contentResolver, context, packageName, "App: " + packageName, new ContentValues());
AppPrefsProvider.Helper.update(context, app, new AppPrefs(ignoreVercode, ignoreAll));
AppPrefsProvider.Helper.update(context, app, new AppPrefs(ignoreVercode, ignoreAll, false));
ContentValues certValue = new ContentValues(1);
certValue.put(Schema.ApkTable.Cols.SIGNATURE, TestUtils.FDROID_SIG);

View File

@ -22,6 +22,14 @@ public class InstalledAppTestUtils {
String packageName,
int versionCode, String versionName,
@Nullable String signingCert) {
install(context, packageName, versionCode, versionName, signingCert, null);
}
public static void install(Context context,
String packageName,
int versionCode, String versionName,
@Nullable String signingCert,
@Nullable String hash) {
PackageInfo info = new PackageInfo();
info.packageName = packageName;
info.versionCode = versionCode;
@ -31,8 +39,12 @@ public class InstalledAppTestUtils {
if (signingCert != null) {
info.signatures = new Signature[]{new Signature(signingCert)};
}
String hashType = "sha256";
String hash = "00112233445566778899aabbccddeeff";
if (hash == null) {
hash = "00112233445566778899aabbccddeeff";
}
InstalledAppProviderService.insertAppIntoDb(context, info, hashType, hash);
}

View File

@ -21,6 +21,7 @@ import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.FDroidProviderTest;
import org.fdroid.fdroid.data.InstalledAppTestUtils;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.RepoPushRequest;
@ -91,7 +92,7 @@ public class IndexV1UpdaterTest extends FDroidProviderTest {
updater.processIndexV1(indexInputStream, indexEntry, "fakeEtag");
IOUtils.closeQuietly(indexInputStream);
List<App> apps = AppProvider.Helper.all(context.getContentResolver());
assertEquals("53 apps present", 53, apps.size());
assertEquals("63 apps present", 63, apps.size());
String[] packages = {
"fake.app.one",
@ -110,7 +111,7 @@ public class IndexV1UpdaterTest extends FDroidProviderTest {
repos = RepoProvider.Helper.all(context);
assertEquals("One repo", 1, repos.size());
Repo repoFromDb = repos.get(0);
assertEquals("repo.timestamp should be set", 1481222111, repoFromDb.timestamp);
assertEquals("repo.timestamp should be set", 1497639511, repoFromDb.timestamp);
assertEquals("repo.address should be the same", repo.address, repoFromDb.address);
assertEquals("repo.name should be set", "non-public test repo", repoFromDb.name);
assertEquals("repo.maxage should be set", 0, repoFromDb.maxage);
@ -121,6 +122,12 @@ public class IndexV1UpdaterTest extends FDroidProviderTest {
assertEquals("repo.mirrors should have items", 2, repo.mirrors.length);
assertEquals("repo.mirrors first URL", "http://frkcchxlcvnb4m5a.onion/fdroid/repo", repo.mirrors[0]);
assertEquals("repo.mirrors second URL", "http://testy.at.or.at/fdroid/repo", repo.mirrors[1]);
// Make sure the per-apk anti features which are new in index v1 get added correctly.
assertEquals(0, AppProvider.Helper.findInstalledAppsWithKnownVulns(context).size());
InstalledAppTestUtils.install(context, "com.waze", 1019841, "v3.9.5.4", "362488e7be5ea0689b4e97d989ae1404",
"cbbdb8c5dafeccd7dd7b642dde0477d3489e18ac366e3c8473d5c07e5f735a95");
assertEquals(1, AppProvider.Helper.findInstalledAppsWithKnownVulns(context).size());
}
@Test(expected = RepoUpdater.SigningException.class)