Merge branch 'fix-511--move-user-specified-data-to-separate-table' into 'master'

Move user specified data to separate table

Right now, the "Ignore update for version X" and "Ignore all updates for this app" are stored in `fdroid_app`. This means that if a repo is disabled then re-enabled, these preferences are lost. This MR separates out the user specified metadata from the metadata provided by the repository, such that one can change without affecting the other.

For convenience sake, this drops the `fdroid_app` and `fdroid_apk` tables rather than migrating them, and then sets a flag in preferences that forces F-Droid to do an index update when started. This is done _after_ migrating already existing user preferences out of `fdroid_app` and into `fdroid_appPrefs`.

See merge request !372
This commit is contained in:
Hans-Christoph Steiner 2016-08-08 08:59:40 +00:00
commit 2837a235b4
14 changed files with 473 additions and 154 deletions

View File

@ -95,6 +95,11 @@
android:name="org.fdroid.fdroid.data.InstalledAppProvider"
android:exported="false"/>
<provider
android:authorities="org.fdroid.fdroid.data.AppPrefsProvider"
android:name="org.fdroid.fdroid.data.AppPrefsProvider"
android:exported="false"/>
<provider
android:name="org.fdroid.fdroid.installer.ApkFileProvider"
android:authorities="org.fdroid.fdroid.installer.ApkFileProvider"

View File

@ -26,7 +26,6 @@ import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@ -81,6 +80,8 @@ import com.nostra13.universalimageloader.core.assist.ImageScaleType;
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.InstalledAppProvider;
import org.fdroid.fdroid.data.RepoProvider;
@ -317,8 +318,7 @@ public class AppDetails extends AppCompatActivity {
private String activeDownloadUrlString;
private LocalBroadcastManager localBroadcastManager;
private boolean startingIgnoreAll;
private int startingIgnoreThis;
private AppPrefs startingPrefs;
private final Context context = this;
@ -472,10 +472,9 @@ public class AppDetails extends AppCompatActivity {
.edit()
.putString(getPackageNameFromIntent(getIntent()), activeDownloadUrlString)
.apply();
if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
|| app.ignoreThisUpdate != startingIgnoreThis)) {
if (app != null && !app.getPrefs(this).equals(startingPrefs)) {
Utils.debugLog(TAG, "Updating 'ignore updates', as it has changed since we started the activity...");
setIgnoreUpdates(app.packageName, app.ignoreAllUpdates, app.ignoreThisUpdate);
AppPrefsProvider.Helper.update(this, app, app.getPrefs(this));
}
unregisterDownloaderReceiver();
}
@ -646,18 +645,6 @@ public class AppDetails extends AppCompatActivity {
supportInvalidateOptionsMenu();
}
private void setIgnoreUpdates(String packageName, boolean ignoreAll, int ignoreVersionCode) {
Uri uri = AppProvider.getContentUri(packageName);
ContentValues values = new ContentValues(2);
values.put(Schema.AppTable.Cols.IGNORE_ALLUPDATES, ignoreAll ? 1 : 0);
values.put(Schema.AppTable.Cols.IGNORE_THISUPDATE, ignoreVersionCode);
getContentResolver().update(uri, values, null, null);
}
@Override
public Object onRetainCustomNonConfigurationInstance() {
return new ConfigurationChangeHelper(activeDownloadUrlString, app);
@ -711,8 +698,7 @@ public class AppDetails extends AppCompatActivity {
app = newApp;
startingIgnoreAll = app.ignoreAllUpdates;
startingIgnoreThis = app.ignoreThisUpdate;
startingPrefs = app.getPrefs(this).createClone();
}
private void refreshApkList() {
@ -733,7 +719,7 @@ public class AppDetails extends AppCompatActivity {
return true;
}
if (packageManager.getLaunchIntentForPackage(app.packageName) != null && app.canAndWantToUpdate()) {
if (packageManager.getLaunchIntentForPackage(app.packageName) != null && app.canAndWantToUpdate(this)) {
MenuItemCompat.setShowAsAction(menu.add(
Menu.NONE, LAUNCH, 1, R.string.menu_launch)
.setIcon(R.drawable.ic_play_arrow_white),
@ -758,13 +744,13 @@ public class AppDetails extends AppCompatActivity {
menu.add(Menu.NONE, IGNOREALL, 2, R.string.menu_ignore_all)
.setIcon(R.drawable.ic_do_not_disturb_white)
.setCheckable(true)
.setChecked(app.ignoreAllUpdates);
.setChecked(app.getPrefs(context).ignoreAllUpdates);
if (app.hasUpdates()) {
menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this)
.setIcon(R.drawable.ic_do_not_disturb_white)
.setCheckable(true)
.setChecked(app.ignoreThisUpdate >= app.suggestedVersionCode);
.setChecked(app.getPrefs(context).ignoreThisUpdate >= app.suggestedVersionCode);
}
// Ignore on devices without Bluetooth
@ -880,17 +866,17 @@ public class AppDetails extends AppCompatActivity {
return true;
case IGNOREALL:
app.ignoreAllUpdates ^= true;
item.setChecked(app.ignoreAllUpdates);
app.getPrefs(this).ignoreAllUpdates ^= true;
item.setChecked(app.getPrefs(this).ignoreAllUpdates);
return true;
case IGNORETHIS:
if (app.ignoreThisUpdate >= app.suggestedVersionCode) {
app.ignoreThisUpdate = 0;
if (app.getPrefs(this).ignoreThisUpdate >= app.suggestedVersionCode) {
app.getPrefs(this).ignoreThisUpdate = 0;
} else {
app.ignoreThisUpdate = app.suggestedVersionCode;
app.getPrefs(this).ignoreThisUpdate = app.suggestedVersionCode;
}
item.setChecked(app.ignoreThisUpdate > 0);
item.setChecked(app.getPrefs(this).ignoreThisUpdate > 0);
return true;
case SEND_VIA_BLUETOOTH:
@ -1600,7 +1586,7 @@ public class AppDetails extends AppCompatActivity {
installed = true;
statusView.setText(getString(R.string.details_installed, app.installedVersionName));
NfcHelper.setAndroidBeam(appDetails, app.packageName);
if (app.canAndWantToUpdate()) {
if (app.canAndWantToUpdate(appDetails)) {
updateWanted = true;
btMain.setText(R.string.menu_upgrade);
} else {

View File

@ -103,15 +103,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
*/
public String[] requirements;
/**
* 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
*/
public int ignoreThisUpdate;
private AppPrefs prefs;
/**
* To be displayed at 48dp (x1.0)
@ -233,12 +225,6 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
case Cols.REQUIREMENTS:
requirements = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.IGNORE_ALLUPDATES:
ignoreAllUpdates = cursor.getInt(i) == 1;
break;
case Cols.IGNORE_THISUPDATE:
ignoreThisUpdate = cursor.getInt(i);
break;
case Cols.ICON_URL:
iconUrl = cursor.getString(i);
break;
@ -471,8 +457,6 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
values.put(Cols.ANTI_FEATURES, Utils.serializeCommaSeparatedString(antiFeatures));
values.put(Cols.REQUIREMENTS, Utils.serializeCommaSeparatedString(requirements));
values.put(Cols.IS_COMPATIBLE, compatible ? 1 : 0);
values.put(Cols.IGNORE_ALLUPDATES, ignoreAllUpdates ? 1 : 0);
values.put(Cols.IGNORE_THISUPDATE, ignoreThisUpdate);
return values;
}
@ -492,13 +476,21 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
return updates;
}
public AppPrefs getPrefs(Context context) {
if (prefs == null) {
prefs = AppPrefsProvider.Helper.getPrefsOrDefault(context, this);
}
return prefs;
}
/**
* True if there are new versions (apks) available and the user wants
* to be notified about them
*/
public boolean canAndWantToUpdate() {
public boolean canAndWantToUpdate(Context context) {
boolean canUpdate = hasUpdates();
boolean wantsUpdate = !ignoreAllUpdates && ignoreThisUpdate < suggestedVersionCode;
AppPrefs prefs = getPrefs(context);
boolean wantsUpdate = !prefs.ignoreAllUpdates && prefs.ignoreThisUpdate < suggestedVersionCode;
return canUpdate && wantsUpdate && !isFiltered();
}
@ -591,8 +583,6 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
dest.writeStringArray(this.categories);
dest.writeStringArray(this.antiFeatures);
dest.writeStringArray(this.requirements);
dest.writeByte(this.ignoreAllUpdates ? (byte) 1 : (byte) 0);
dest.writeInt(this.ignoreThisUpdate);
dest.writeString(this.iconUrl);
dest.writeString(this.iconUrlLarge);
dest.writeString(this.installedVersionName);
@ -631,8 +621,6 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
this.categories = in.createStringArray();
this.antiFeatures = in.createStringArray();
this.requirements = in.createStringArray();
this.ignoreAllUpdates = in.readByte() != 0;
this.ignoreThisUpdate = in.readInt();
this.iconUrl = in.readString();
this.iconUrlLarge = in.readString();
this.installedVersionName = in.readString();

View File

@ -0,0 +1,39 @@
package org.fdroid.fdroid.data;
public class AppPrefs extends ValueObject {
/**
* 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
*/
public int ignoreThisUpdate;
public AppPrefs(int ignoreThis, boolean ignoreAll) {
ignoreThisUpdate = ignoreThis;
ignoreAllUpdates = ignoreAll;
}
public static AppPrefs createDefault() {
return new AppPrefs(0, false);
}
@Override
public boolean equals(Object o) {
return o != null && o instanceof AppPrefs &&
((AppPrefs) o).ignoreAllUpdates == ignoreAllUpdates &&
((AppPrefs) o).ignoreThisUpdate == ignoreThisUpdate;
}
@Override
public int hashCode() {
return (ignoreThisUpdate + "-" + ignoreAllUpdates).hashCode();
}
public AppPrefs createClone() {
return new AppPrefs(ignoreThisUpdate, ignoreAllUpdates);
}
}

View File

@ -0,0 +1,158 @@
package org.fdroid.fdroid.data;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.fdroid.fdroid.data.Schema.AppPrefsTable;
import org.fdroid.fdroid.data.Schema.AppPrefsTable.Cols;
public class AppPrefsProvider extends FDroidProvider {
public static final class Helper {
private Helper() { }
public static void update(Context context, App app, AppPrefs prefs) {
ContentValues values = new ContentValues(3);
values.put(Cols.IGNORE_ALL_UPDATES, prefs.ignoreAllUpdates);
values.put(Cols.IGNORE_THIS_UPDATE, prefs.ignoreThisUpdate);
if (getPrefsOrNull(context, app) == null) {
values.put(Cols.PACKAGE_NAME, app.packageName);
context.getContentResolver().insert(getContentUri(), values);
} else {
context.getContentResolver().update(getAppUri(app.packageName), values, null, null);
}
}
@NonNull
public static AppPrefs getPrefsOrDefault(Context context, App app) {
AppPrefs prefs = getPrefsOrNull(context, app);
return prefs == null ? AppPrefs.createDefault() : prefs;
}
@Nullable
public static AppPrefs getPrefsOrNull(Context context, App app) {
Cursor cursor = context.getContentResolver().query(getAppUri(app.packageName), Cols.ALL, null, null, null);
if (cursor == null) {
return null;
}
try {
if (cursor.getCount() == 0) {
return null;
}
cursor.moveToFirst();
return new AppPrefs(
cursor.getInt(cursor.getColumnIndexOrThrow(Cols.IGNORE_THIS_UPDATE)),
cursor.getInt(cursor.getColumnIndexOrThrow(Cols.IGNORE_ALL_UPDATES)) > 0);
} finally {
cursor.close();
}
}
}
private class Query extends QueryBuilder {
@Override
protected String getRequiredTables() {
return AppPrefsTable.NAME;
}
@Override
public void addField(String field) {
appendField(field, getTableName());
}
}
private static final String PROVIDER_NAME = "AppPrefsProvider";
private static final UriMatcher MATCHER = new UriMatcher(-1);
private static final String PATH_PACKAGE_NAME = "packageName";
static {
MATCHER.addURI(getAuthority(), PATH_PACKAGE_NAME + "/*", CODE_SINGLE);
}
private static Uri getContentUri() {
return Uri.parse("content://" + getAuthority());
}
public static Uri getAppUri(String packageName) {
return getContentUri().buildUpon().appendPath(PATH_PACKAGE_NAME).appendPath(packageName).build();
}
@Override
protected String getTableName() {
return AppPrefsTable.NAME;
}
@Override
protected String getProviderName() {
return "AppPrefsProvider";
}
public static String getAuthority() {
return AUTHORITY + "." + PROVIDER_NAME;
}
@Override
protected UriMatcher getMatcher() {
return MATCHER;
}
protected QuerySelection querySingle(String packageName) {
final String selection = getTableName() + "." + Cols.PACKAGE_NAME + " = ?";
final String[] args = {packageName};
return new QuerySelection(selection, args);
}
@Override
public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
if (MATCHER.match(uri) != CODE_SINGLE) {
throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri);
}
QuerySelection selection = new QuerySelection(customSelection, selectionArgs)
.add(querySingle(uri.getLastPathSegment()));
Query query = new Query();
query.addSelection(selection);
query.addFields(projection);
query.addOrderBy(sortOrder);
Cursor cursor = LoggingQuery.query(db(), query.toString(), query.getArgs());
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public int delete(Uri uri, String where, String[] whereArgs) {
throw new UnsupportedOperationException("Delete not supported for " + uri + ".");
}
@Override
public Uri insert(Uri uri, ContentValues values) {
db().insertOrThrow(getTableName(), null, values);
getContext().getContentResolver().notifyChange(AppProvider.getCanUpdateUri(), null);
return getAppUri(values.getAsString(Cols.PACKAGE_NAME));
}
@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
if (MATCHER.match(uri) != CODE_SINGLE) {
throw new UnsupportedOperationException("Update not supported for " + uri + ".");
}
QuerySelection query = new QuerySelection(where, whereArgs).add(querySingle(uri.getLastPathSegment()));
int count = db().update(getTableName(), values, query.getSelection(), query.getArgs());
getContext().getContentResolver().notifyChange(AppProvider.getCanUpdateUri(), null);
return count;
}
}

View File

@ -14,6 +14,7 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.ApkTable;
import org.fdroid.fdroid.data.Schema.AppPrefsTable;
import org.fdroid.fdroid.data.Schema.AppTable;
import org.fdroid.fdroid.data.Schema.AppTable.Cols;
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
@ -58,12 +59,6 @@ public class AppProvider extends FDroidProvider {
return cursorToList(cursor);
}
public static List<App> findIgnored(Context context, String[] projection) {
final Uri uri = AppProvider.getIgnoredUri();
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
return cursorToList(cursor);
}
static List<App> cursorToList(Cursor cursor) {
int knownAppCount = cursor != null ? cursor.getCount() : 0;
List<App> apps = new ArrayList<>(knownAppCount);
@ -153,6 +148,10 @@ public class AppProvider extends FDroidProvider {
final Uri fromUpstream = calcAppDetailsFromIndexUri();
context.getContentResolver().update(fromUpstream, null, null, null);
}
public static List<App> findCanUpdate(Context context, String[] projection) {
return cursorToList(context.getContentResolver().query(AppProvider.getCanUpdateUri(), projection, null, null, null));
}
}
/**
@ -186,6 +185,7 @@ public class AppProvider extends FDroidProvider {
protected static class AppQuerySelection extends QuerySelection {
private boolean naturalJoinToInstalled;
private boolean leftJoinPrefs;
AppQuerySelection() {
// The same as no selection, because "1" will always resolve to true when executing the SQL query.
@ -217,12 +217,25 @@ public class AppProvider extends FDroidProvider {
return this;
}
public boolean leftJoinToPrefs() {
return leftJoinPrefs;
}
public AppQuerySelection requireLeftJoinPrefs() {
leftJoinPrefs = true;
return this;
}
public AppQuerySelection add(AppQuerySelection query) {
QuerySelection both = super.add(query);
AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs());
if (this.naturalJoinToInstalled() || query.naturalJoinToInstalled()) {
bothWithJoin.requireNaturalInstalledTable();
}
if (this.leftJoinToPrefs() || query.leftJoinToPrefs()) {
bothWithJoin.requireLeftJoinPrefs();
}
return bothWithJoin;
}
@ -232,6 +245,7 @@ public class AppProvider extends FDroidProvider {
private boolean isSuggestedApkTableAdded;
private boolean requiresInstalledTable;
private boolean requiresLeftJoinToPrefs;
private boolean categoryFieldAdded;
private boolean countFieldAppended;
@ -262,6 +276,9 @@ public class AppProvider extends FDroidProvider {
if (selection.naturalJoinToInstalled()) {
naturalJoinToInstalledTable();
}
if (selection.leftJoinToPrefs()) {
leftJoinToPrefs();
}
}
// TODO: What if the selection requires a natural join, but we first get a left join
@ -276,6 +293,16 @@ public class AppProvider extends FDroidProvider {
}
}
public void leftJoinToPrefs() {
if (!requiresLeftJoinToPrefs) {
leftJoin(
AppPrefsTable.NAME,
"prefs",
"prefs." + AppPrefsTable.Cols.PACKAGE_NAME + " = " + getTableName() + "." + Cols.PACKAGE_NAME);
requiresLeftJoinToPrefs = true;
}
}
public void leftJoinToInstalledTable() {
if (!requiresInstalledTable) {
leftJoin(
@ -377,7 +404,6 @@ public class AppProvider extends FDroidProvider {
private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated";
private static final String PATH_NEWLY_ADDED = "newlyAdded";
private static final String PATH_CATEGORY = "category";
private static final String PATH_IGNORED = "ignored";
private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex";
private static final String PATH_REPO = "repo";
@ -388,8 +414,7 @@ public class AppProvider extends FDroidProvider {
private static final int RECENTLY_UPDATED = NO_APKS + 1;
private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1;
private static final int CATEGORY = NEWLY_ADDED + 1;
private static final int IGNORED = CATEGORY + 1;
private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1;
private static final int CALC_APP_DETAILS_FROM_INDEX = CATEGORY + 1;
private static final int REPO = CALC_APP_DETAILS_FROM_INDEX + 1;
private static final int SEARCH_REPO = REPO + 1;
private static final int SEARCH_INSTALLED = SEARCH_REPO + 1;
@ -398,7 +423,6 @@ public class AppProvider extends FDroidProvider {
static {
MATCHER.addURI(getAuthority(), null, CODE_LIST);
MATCHER.addURI(getAuthority(), PATH_CALC_APP_DETAILS_FROM_INDEX, CALC_APP_DETAILS_FROM_INDEX);
MATCHER.addURI(getAuthority(), PATH_IGNORED, IGNORED);
MATCHER.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED);
MATCHER.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED);
MATCHER.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY);
@ -425,10 +449,6 @@ public class AppProvider extends FDroidProvider {
return Uri.withAppendedPath(getContentUri(), PATH_NEWLY_ADDED);
}
public static Uri getIgnoredUri() {
return Uri.withAppendedPath(getContentUri(), PATH_IGNORED);
}
private static Uri calcAppDetailsFromIndexUri() {
return Uri.withAppendedPath(getContentUri(), PATH_CALC_APP_DETAILS_FROM_INDEX);
}
@ -527,11 +547,16 @@ public class AppProvider extends FDroidProvider {
private AppQuerySelection queryCanUpdate() {
final String app = getTableName();
final String ignoreCurrent = app + "." + Cols.IGNORE_THISUPDATE + "!= " + app + "." + Cols.SUGGESTED_VERSION_CODE;
final String ignoreAll = app + "." + Cols.IGNORE_ALLUPDATES + " != 1";
// Need to use COALESCE because the prefs join may not resolve any rows, which means the
// ignore* fields will be NULL. In that case, we want to instead use a default value of 0.
final String ignoreCurrent = " COALESCE(prefs." + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ", 0) != " + app + "." + Cols.SUGGESTED_VERSION_CODE;
final String ignoreAll = "COALESCE(prefs." + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + ", 0) != 1";
final String ignore = " (" + ignoreCurrent + " AND " + ignoreAll + ") ";
final String where = ignore + " AND " + app + "." + Cols.SUGGESTED_VERSION_CODE + " > installed." + InstalledAppTable.Cols.VERSION_CODE;
return new AppQuerySelection(where).requireNaturalInstalledTable();
return new AppQuerySelection(where).requireNaturalInstalledTable().requireLeftJoinPrefs();
}
private AppQuerySelection queryRepo(long repoId) {
@ -602,19 +627,12 @@ public class AppProvider extends FDroidProvider {
return new AppQuerySelection(selection, args);
}
private AppQuerySelection queryIgnored() {
final String table = getTableName();
final String selection = table + "." + Cols.IGNORE_ALLUPDATES + " = 1 OR " +
table + "." + Cols.IGNORE_THISUPDATE + " >= " + table + "." + Cols.SUGGESTED_VERSION_CODE;
return new AppQuerySelection(selection);
}
private AppQuerySelection queryExcludeSwap() {
// fdroid_repo will have null fields if the LEFT JOIN didn't resolve, e.g. due to there
// being no apks for the app in the result set. In that case, we can't tell if it is from
// a swap repo or not.
final String isSwap = RepoTable.NAME + "." + RepoTable.Cols.IS_SWAP;
final String selection = isSwap + " = 0 OR " + isSwap + " IS NULL";
final String selection = "COALESCE(" + isSwap + ", 0) = 0";
return new AppQuerySelection(selection);
}
@ -717,10 +735,6 @@ public class AppProvider extends FDroidProvider {
selection = selection.add(queryNoApks());
break;
case IGNORED:
selection = selection.add(queryIgnored());
break;
case CATEGORY:
selection = selection.add(queryCategory(uri.getLastPathSegment()));
includeSwap = false;
@ -896,7 +910,7 @@ public class AppProvider extends FDroidProvider {
" WHERE " +
app + "." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + " AND " +
" ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + ApkTable.Cols.IS_COMPATIBLE + " = 1 ) ) " +
" WHERE " + Cols.UPSTREAM_VERSION_CODE + " = 0 OR " + Cols.UPSTREAM_VERSION_CODE + " IS NULL OR " + Cols.SUGGESTED_VERSION_CODE + " IS NULL ";
" WHERE COALESCE(" + Cols.UPSTREAM_VERSION_CODE + ", 0) = 0 OR " + Cols.SUGGESTED_VERSION_CODE + " IS NULL ";
db().execSQL(updateSql);
}

View File

@ -11,6 +11,7 @@ import android.util.Log;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.ApkTable;
import org.fdroid.fdroid.data.Schema.AppPrefsTable;
import org.fdroid.fdroid.data.Schema.AppTable;
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
import org.fdroid.fdroid.data.Schema.RepoTable;
@ -95,12 +96,17 @@ class DBHelper extends SQLiteOpenHelper {
+ AppTable.Cols.ADDED + " string,"
+ AppTable.Cols.LAST_UPDATED + " string,"
+ AppTable.Cols.IS_COMPATIBLE + " int not null,"
+ AppTable.Cols.IGNORE_ALLUPDATES + " int not null,"
+ AppTable.Cols.IGNORE_THISUPDATE + " int not null,"
+ AppTable.Cols.ICON_URL + " text, "
+ AppTable.Cols.ICON_URL_LARGE + " text, "
+ "primary key(" + AppTable.Cols.PACKAGE_NAME + "));";
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 "
+ " );";
private static final String CREATE_TABLE_INSTALLED_APP = "CREATE TABLE " + InstalledAppTable.NAME
+ " ( "
+ InstalledAppTable.Cols.PACKAGE_NAME + " TEXT NOT NULL PRIMARY KEY, "
@ -114,7 +120,7 @@ class DBHelper extends SQLiteOpenHelper {
+ " );";
private static final String DROP_TABLE_INSTALLED_APP = "DROP TABLE " + InstalledAppTable.NAME + ";";
private static final int DB_VERSION = 59;
private static final int DB_VERSION = 60;
private final Context context;
@ -216,9 +222,12 @@ class DBHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase db) {
createAppApk(db);
db.execSQL(CREATE_TABLE_APP);
db.execSQL(CREATE_TABLE_APK);
db.execSQL(CREATE_TABLE_INSTALLED_APP);
db.execSQL(CREATE_TABLE_REPO);
db.execSQL(CREATE_TABLE_APP_PREFS);
ensureIndexes(db);
insertRepo(
db,
@ -296,7 +305,7 @@ class DBHelper extends SQLiteOpenHelper {
// The other tables are transient and can just be reset. Do this after
// the repo table changes though, because it also clears the lastetag
// fields which didn't always exist.
resetTransient(db, oldVersion);
resetTransientPre42(db, oldVersion);
addNameAndDescriptionToRepo(db, oldVersion);
addFingerprintToRepo(db, oldVersion);
@ -317,6 +326,32 @@ class DBHelper extends SQLiteOpenHelper {
addTargetSdkVersionToApk(db, oldVersion);
migrateAppPrimaryKeyToRowId(db, oldVersion);
removeApkPackageNameColumn(db, oldVersion);
addAppPrefsTable(db, oldVersion);
}
private void addAppPrefsTable(SQLiteDatabase db, int oldVersion) {
if (oldVersion >= 60) {
return;
}
Utils.debugLog(TAG, "Creating app preferences table");
db.execSQL(CREATE_TABLE_APP_PREFS);
Utils.debugLog(TAG, "Migrating app preferences to separate table");
db.execSQL(
"INSERT INTO " + AppPrefsTable.NAME + " ("
+ AppPrefsTable.Cols.PACKAGE_NAME + ", "
+ AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ", "
+ AppPrefsTable.Cols.IGNORE_ALL_UPDATES
+ ") SELECT "
+ AppTable.Cols.PACKAGE_NAME + ", "
+ "ignoreThisUpdate, "
+ "ignoreAllUpdates "
+ "FROM " + AppTable.NAME + " "
+ "WHERE ignoreThisUpdate > 0 OR ignoreAllUpdates > 0"
);
resetTransient(db);
}
/**
@ -657,12 +692,27 @@ class DBHelper extends SQLiteOpenHelper {
* their repos (either manually or on a scheduled task), they will update regardless of whether
* they have changed since last update or not.
*/
private void clearRepoEtags(SQLiteDatabase db) {
private static void clearRepoEtags(SQLiteDatabase db) {
Utils.debugLog(TAG, "Clearing repo etags, so next update will not be skipped with \"Repos up to date\".");
db.execSQL("update " + RepoTable.NAME + " set " + RepoTable.Cols.LAST_ETAG + " = NULL");
}
private void resetTransient(SQLiteDatabase db, int oldVersion) {
private void resetTransient(SQLiteDatabase db) {
Utils.debugLog(TAG, "Removing app + apk tables so they can be recreated. Next time F-Droid updates it should trigger an index update.");
context.getSharedPreferences("FDroid", Context.MODE_PRIVATE)
.edit()
.putBoolean("triedEmptyUpdate", false)
.apply();
db.execSQL("DROP TABLE " + AppTable.NAME);
db.execSQL("DROP TABLE " + ApkTable.NAME);
db.execSQL(CREATE_TABLE_APP);
db.execSQL(CREATE_TABLE_APK);
clearRepoEtags(db);
ensureIndexes(db);
}
private void resetTransientPre42(SQLiteDatabase db, int oldVersion) {
// Before version 42, only transient info was stored in here. As of some time
// just before 42 (F-Droid 0.60ish) it now has "ignore this version" info which
// was is specified by the user. We don't want to weely-neely nuke that data.
@ -672,14 +722,10 @@ class DBHelper extends SQLiteOpenHelper {
return;
}
context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit()
.putBoolean("triedEmptyUpdate", false).commit();
.putBoolean("triedEmptyUpdate", false).apply();
db.execSQL("drop table " + AppTable.NAME);
db.execSQL("drop table " + ApkTable.NAME);
clearRepoEtags(db);
createAppApk(db);
}
private static void createAppApk(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_APP);
db.execSQL(CREATE_TABLE_APK);
ensureIndexes(db);
@ -695,6 +741,23 @@ class DBHelper extends SQLiteOpenHelper {
db.execSQL("CREATE INDEX IF NOT EXISTS apk_vercode on " + ApkTable.NAME + " (" + ApkTable.Cols.VERSION_CODE + ");");
db.execSQL("CREATE INDEX IF NOT EXISTS apk_appId on " + ApkTable.NAME + " (" + ApkTable.Cols.APP_ID + ");");
db.execSQL("CREATE INDEX IF NOT EXISTS repoId ON " + ApkTable.NAME + " (" + ApkTable.Cols.REPO_ID + ");");
if (tableExists(db, AppPrefsTable.NAME)) {
Utils.debugLog(TAG, "Ensuring indexes exist for " + AppPrefsTable.NAME);
db.execSQL("CREATE INDEX IF NOT EXISTS appPrefs_packageName on " + AppPrefsTable.NAME + " (" + AppPrefsTable.Cols.PACKAGE_NAME + ");");
db.execSQL("CREATE INDEX IF NOT EXISTS appPrefs_packageName_ignoreAll_ignoreThis on " + AppPrefsTable.NAME + " (" +
AppPrefsTable.Cols.PACKAGE_NAME + ", " +
AppPrefsTable.Cols.IGNORE_ALL_UPDATES + ", " +
AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ");");
}
Utils.debugLog(TAG, "Ensuring indexes exist for " + InstalledAppTable.NAME);
db.execSQL("CREATE INDEX IF NOT EXISTS installedApp_appId_vercode on " + InstalledAppTable.NAME + " (" +
InstalledAppTable.Cols.PACKAGE_NAME + ", " + InstalledAppTable.Cols.VERSION_CODE + ");");
Utils.debugLog(TAG, "Ensuring indexes exist for " + RepoTable.NAME);
db.execSQL("CREATE INDEX IF NOT EXISTS repo_id_isSwap on " + RepoTable.NAME + " (" +
RepoTable.Cols._ID + ", " + RepoTable.Cols.IS_SWAP + ");");
}
/**
@ -722,10 +785,14 @@ class DBHelper extends SQLiteOpenHelper {
+ ApkTable.Cols.TARGET_SDK_VERSION + " integer");
}
private static boolean columnExists(SQLiteDatabase db,
String table, String column) {
private static boolean columnExists(SQLiteDatabase db, String table, String column) {
return db.rawQuery("select * from " + table + " limit 0,1", null)
.getColumnIndex(column) != -1;
}
private static boolean tableExists(SQLiteDatabase db, String table) {
return db.rawQuery("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
new String[] {table}).getCount() > 0;
}
}

View File

@ -22,20 +22,6 @@ public class RepoPersister {
private static final String TAG = "RepoPersister";
/**
* When an app already exists in the db, and we are updating it on the off chance that some
* values changed in the index, some fields should not be updated. Rather, they should be
* ignored, because they were explicitly set by the user, and hence can't be automatically
* overridden by the index.
*
* NOTE: In the future, these attributes will be moved to a join table, so that the app table
* is essentially completely transient, and can be nuked at any time.
*/
private static final String[] APP_FIELDS_TO_IGNORE = {
Schema.AppTable.Cols.IGNORE_ALLUPDATES,
Schema.AppTable.Cols.IGNORE_THISUPDATE,
};
/**
* Crappy benchmark with a Nexus 4, Android 5.0 on a fairly crappy internet connection I get:
* * 25 = 37 seconds
@ -219,13 +205,7 @@ public class RepoPersister {
*/
private ContentProviderOperation updateExistingApp(App app) {
Uri uri = TempAppProvider.getAppUri(app);
ContentValues values = app.toContentValues();
for (final String toIgnore : APP_FIELDS_TO_IGNORE) {
if (values.containsKey(toIgnore)) {
values.remove(toIgnore);
}
}
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
return ContentProviderOperation.newUpdate(uri).withValues(app.toContentValues()).build();
}
/**

View File

@ -9,6 +9,23 @@ import android.provider.BaseColumns;
*/
public interface Schema {
interface AppPrefsTable {
String NAME = "fdroid_appPrefs";
interface Cols extends BaseColumns {
// Join onto app table via packageName, not appId. The corresponding app row could
// be deleted and then re-added in the future with the same metadata but a different
// rowid. This should not cause us to forget the preferences specified by a user.
String PACKAGE_NAME = "packageName";
String IGNORE_ALL_UPDATES = "ignoreAllUpdates";
String IGNORE_THIS_UPDATE = "ignoreThisUpdate";
String[] ALL = {PACKAGE_NAME, IGNORE_ALL_UPDATES, IGNORE_THIS_UPDATE};
}
}
interface AppTable {
String NAME = "fdroid_app";
@ -46,8 +63,6 @@ public interface Schema {
String CATEGORIES = "categories";
String ANTI_FEATURES = "antiFeatures";
String REQUIREMENTS = "requirements";
String IGNORE_ALLUPDATES = "ignoreAllUpdates";
String IGNORE_THISUPDATE = "ignoreThisUpdate";
String ICON_URL = "iconUrl";
String ICON_URL_LARGE = "iconUrlLarge";
@ -66,8 +81,7 @@ public interface Schema {
LICENSE, AUTHOR, EMAIL, WEB_URL, TRACKER_URL, SOURCE_URL,
CHANGELOG_URL, DONATE_URL, BITCOIN_ADDR, LITECOIN_ADDR, FLATTR_ID,
UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,
CATEGORIES, ANTI_FEATURES, REQUIREMENTS, IGNORE_ALLUPDATES,
IGNORE_THISUPDATE, ICON_URL, ICON_URL_LARGE,
CATEGORIES, ANTI_FEATURES, REQUIREMENTS, ICON_URL, ICON_URL_LARGE,
SUGGESTED_VERSION_CODE, SuggestedApk.VERSION_NAME,
InstalledApp.VERSION_CODE, InstalledApp.VERSION_NAME,
InstalledApp.SIGNATURE,

View File

@ -123,7 +123,7 @@ public abstract class AppListAdapter extends CursorAdapter {
final String installedVersionString = app.installedVersionName;
if (app.canAndWantToUpdate() && showStatusUpdate()) {
if (app.canAndWantToUpdate(mContext) && showStatusUpdate()) {
return String.format(upgradeFromTo,
installedVersionString, app.getSuggestedVersionName());
}

View File

@ -50,8 +50,6 @@ public abstract class AppListFragment extends ListFragment implements
AppTable.Cols.InstalledApp.VERSION_NAME,
AppTable.Cols.SuggestedApk.VERSION_NAME,
AppTable.Cols.SUGGESTED_VERSION_CODE,
AppTable.Cols.IGNORE_ALLUPDATES,
AppTable.Cols.IGNORE_THISUPDATE,
AppTable.Cols.REQUIREMENTS, // Needed for filtering apps that require root.
};

View File

@ -191,8 +191,6 @@ public class Assert {
values.put(AppTable.Cols.DESCRIPTION, "test description");
values.put(AppTable.Cols.LICENSE, "GPL?");
values.put(AppTable.Cols.IS_COMPATIBLE, 1);
values.put(AppTable.Cols.IGNORE_ALLUPDATES, 0);
values.put(AppTable.Cols.IGNORE_THISUPDATE, 0);
values.putAll(additionalValues);

View File

@ -0,0 +1,68 @@
package org.fdroid.fdroid.data;
import android.app.Application;
import org.fdroid.fdroid.Assert;
import org.fdroid.fdroid.BuildConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowContentResolver;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
// TODO: Use sdk=24 when Robolectric supports this
@Config(constants = BuildConfig.class, application = Application.class, sdk = 23)
@RunWith(RobolectricGradleTestRunner.class)
public class AppPrefsProviderTest extends FDroidProviderTest {
@Before
public void setup() {
ShadowContentResolver.registerProvider(AppProvider.getAuthority(), new AppProvider());
}
@SuppressWarnings({"PMD.EqualsNull", "EqualsWithItself", "EqualsBetweenInconvertibleTypes", "ObjectEqualsNull"})
@Test
public void prefEquality() {
AppPrefs original = new AppPrefs(101, true);
assertTrue(original.equals(new AppPrefs(101, 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)));
}
@Test
public void newPreferences() {
App withPrefs = Assert.insertApp(context, "com.example.withPrefs", "With Prefs");
App withoutPrefs = Assert.insertApp(context, "com.example.withoutPrefs", "Without Prefs");
assertNull(AppPrefsProvider.Helper.getPrefsOrNull(context, withPrefs));
assertNull(AppPrefsProvider.Helper.getPrefsOrNull(context, withoutPrefs));
AppPrefs defaultPrefs = AppPrefsProvider.Helper.getPrefsOrDefault(context, withPrefs);
assertEquals(0, defaultPrefs.ignoreThisUpdate);
assertFalse(defaultPrefs.ignoreAllUpdates);
AppPrefsProvider.Helper.update(context, withPrefs, new AppPrefs(12, false));
AppPrefs newPrefs = AppPrefsProvider.Helper.getPrefsOrDefault(context, withPrefs);
assertEquals(12, newPrefs.ignoreThisUpdate);
assertFalse(newPrefs.ignoreAllUpdates);
AppPrefsProvider.Helper.update(context, withPrefs, new AppPrefs(14, true));
AppPrefs evenNewerPrefs = AppPrefsProvider.Helper.getPrefsOrDefault(context, withPrefs);
assertEquals(14, evenNewerPrefs.ignoreThisUpdate);
assertTrue(evenNewerPrefs.ignoreAllUpdates);
assertNull(AppPrefsProvider.Helper.getPrefsOrNull(context, withoutPrefs));
}
}

View File

@ -89,9 +89,8 @@ public class AppProviderTest extends FDroidProviderTest {
boolean ignoreAll, int ignoreVercode) {
ContentValues values = new ContentValues(3);
values.put(Cols.SUGGESTED_VERSION_CODE, suggestedVercode);
values.put(Cols.IGNORE_ALLUPDATES, ignoreAll);
values.put(Cols.IGNORE_THISUPDATE, ignoreVercode);
insertApp(packageName, "App: " + packageName, values);
App app = insertApp(packageName, "App: " + packageName, values);
AppPrefsProvider.Helper.update(context, app, new AppPrefs(ignoreVercode, ignoreAll));
InstalledAppTestUtils.install(context, packageName, installedVercode, "v" + installedVercode);
}
@ -113,7 +112,7 @@ public class AppProviderTest extends FDroidProviderTest {
// Can't "update", although can "install"...
App notInstalled = AppProvider.Helper.findByPackageName(r, "not installed");
assertFalse(notInstalled.canAndWantToUpdate());
assertFalse(notInstalled.canAndWantToUpdate(context));
App installedOnlyOneVersionAvailable = AppProvider.Helper.findByPackageName(r, "installed, only one version available");
App installedAlreadyLatestNoIgnore = AppProvider.Helper.findByPackageName(r, "installed, already latest, no ignore");
@ -121,21 +120,21 @@ public class AppProviderTest extends FDroidProviderTest {
App installedAlreadyLatestIgnoreLatest = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore latest");
App installedAlreadyLatestIgnoreOld = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore old");
assertFalse(installedOnlyOneVersionAvailable.canAndWantToUpdate());
assertFalse(installedAlreadyLatestNoIgnore.canAndWantToUpdate());
assertFalse(installedAlreadyLatestIgnoreAll.canAndWantToUpdate());
assertFalse(installedAlreadyLatestIgnoreLatest.canAndWantToUpdate());
assertFalse(installedAlreadyLatestIgnoreOld.canAndWantToUpdate());
assertFalse(installedOnlyOneVersionAvailable.canAndWantToUpdate(context));
assertFalse(installedAlreadyLatestNoIgnore.canAndWantToUpdate(context));
assertFalse(installedAlreadyLatestIgnoreAll.canAndWantToUpdate(context));
assertFalse(installedAlreadyLatestIgnoreLatest.canAndWantToUpdate(context));
assertFalse(installedAlreadyLatestIgnoreOld.canAndWantToUpdate(context));
App installedOldNoIgnore = AppProvider.Helper.findByPackageName(r, "installed, old version, no ignore");
App installedOldIgnoreAll = AppProvider.Helper.findByPackageName(r, "installed, old version, ignore all");
App installedOldIgnoreLatest = AppProvider.Helper.findByPackageName(r, "installed, old version, ignore latest");
App installedOldIgnoreNewerNotLatest = AppProvider.Helper.findByPackageName(r, "installed, old version, ignore newer, but not latest");
assertTrue(installedOldNoIgnore.canAndWantToUpdate());
assertFalse(installedOldIgnoreAll.canAndWantToUpdate());
assertFalse(installedOldIgnoreLatest.canAndWantToUpdate());
assertTrue(installedOldIgnoreNewerNotLatest.canAndWantToUpdate());
assertTrue(installedOldNoIgnore.canAndWantToUpdate(context));
assertFalse(installedOldIgnoreAll.canAndWantToUpdate(context));
assertFalse(installedOldIgnoreLatest.canAndWantToUpdate(context));
assertTrue(installedOldIgnoreNewerNotLatest.canAndWantToUpdate(context));
Cursor canUpdateCursor = r.query(AppProvider.getCanUpdateUri(), Cols.ALL, null, null, null);
assertNotNull(canUpdateCursor);
@ -171,21 +170,27 @@ public class AppProviderTest extends FDroidProviderTest {
assertResultCount(contentResolver, 10, AppProvider.getContentUri(), PROJ);
String[] projection = {Cols.PACKAGE_NAME};
List<App> ignoredApps = AppProvider.Helper.findIgnored(context, projection);
List<App> canUpdateApps = AppProvider.Helper.findCanUpdate(context, projection);
String[] expectedIgnored = {
"installed, already latest, ignore all",
"installed, already latest, ignore latest",
// NOT "installed, already latest, ignore old" - because it
// is should only ignore if "ignored version" is >= suggested
String[] expectedCanUpdate = {
"installed, old version, no ignore",
"installed, old version, ignore newer, but not latest",
// These are ignored because they don't have updates available:
// "installed, only one version available",
// "installed, already latest, no ignore",
// "installed, already latest, ignore old",
// "not installed",
// These four should be ignored due to the app preferences:
// "installed, already latest, ignore all",
// "installed, already latest, ignore latest",
// "installed, old version, ignore all",
// "installed, old version, ignore latest",
"installed, old version, ignore all",
"installed, old version, ignore latest",
// NOT "installed, old version, ignore newer, but not latest"
// for the same reason as above.
};
assertContainsOnlyIds(ignoredApps, expectedIgnored);
assertContainsOnlyIds(canUpdateApps, expectedCanUpdate);
}
private void assertContainsOnlyIds(List<App> actualApps, String[] expectedIds) {
@ -348,7 +353,7 @@ public class AppProviderTest extends FDroidProviderTest {
insertApp(id, name, values);
}
public void insertApp(String id, String name, ContentValues additionalValues) {
public App insertApp(String id, String name, ContentValues additionalValues) {
ContentValues values = new ContentValues();
values.put(Cols.PACKAGE_NAME, id);
@ -359,13 +364,12 @@ public class AppProviderTest extends FDroidProviderTest {
values.put(Cols.DESCRIPTION, "test description");
values.put(Cols.LICENSE, "GPL?");
values.put(Cols.IS_COMPATIBLE, 1);
values.put(Cols.IGNORE_ALLUPDATES, 0);
values.put(Cols.IGNORE_THISUPDATE, 0);
values.putAll(additionalValues);
Uri uri = AppProvider.getContentUri();
contentResolver.insert(uri, values);
return AppProvider.Helper.findByPackageName(context.getContentResolver(), id);
}
}