Merge branch 'fix-511--database-integer-primary-keys' into 'master'
Use an integer primary key to join `fdroid_app` and `fdroid_apk` rather than the apps package name. **Disclaimer:** I realise this is a big change, but it needs to be done at some point, and it is not amenable to smaller changes, due to the fact that the app/apk relationship is so ingrained throughout F-Droid. Luckily, we have really quite comprehensive test coverage of the F-Droid `ContentProvider`s which helps to confirm that nothing should be majorly broken here. **Some points of note:** This is the first part of implementing #511, whereby the DB is refactored to better support multiple repositories. Instead of joining `fdroid_app` and `fdroid_apk` tables using the package name, join based on an integer id autogenerated by sqlite. By default sqlite calls this `rowid` and it exists for every table, unless you've specified your own `NUMBER AUTO INCREMENT PRIMARY KEY` field. We have not done this for `fdroid_app`, so `rowid` is indeed the key we use in this MR. The package name was previously `id` in both the app and apk tables. Now `fdroid_app` makes use of `rowid` and `fdroid_apk` has a foreign key called `appId`. The `ApkProvider` used to get away with only really querying the `fdroid_apk` table, and thus it didn't have to prefix any of the field names in the query with the table name. However now it always joins onto the `fdroid_app` table also, and as such, there are many places where field names needed to be prefixed with the table name (e.g. the `apk` alias or the `app` alias) to ensure the SQL is unambiguous when fields with the same name exist in both tables. The catch is, we want to reuse helper functions that build fragments of SQL, such as "Query based on package name". These helper functions are used both when updating and deleting apks (where field table prefixes are not allowed) and also in select statements (where they are required). Thus this changes comes with an `includeTableAlias` argument added to many of these methods (e.g. `ApkProvider.queryApp`). There is still a package name column in the `fdroid_apk` table (the `id` field). This will be removed in future MRs and replaced with the package name from the joined `fdroid_app` table. The `RepoPersister` used to dump apps in the db, then dump apks into the db. Now it needs to be a bit more nuanced, and dump apps into the db, _then ask the db what `rowid` was assigned to the apps_. This is then used when dumping the apks into the db. This also required some changes to how the `TempAppProvider` and `AppProvider` interact. In the interests of reusing code, both of these are able to provide operations on a similarly structured table but one is an in memory table (`temp_fdroid_app`) and the other is on disk (`fdroid_app`). In the past this was simpler, because the only interaction with the `TempAppProvider` was by using lists of `ContentOperation`s. Whereas now that we need to ask more substantial questions of the `TempAppProvider` other than "Insert this thing" or "update that thing", we needed to implement the `query` method in `TempAppProvider` similar to how it is in the base class `AppProvider`. As such, the common code for the base class and subclass `query` methods was extracted into `AppProvider.runQuery()`. I tried to minimize the changes to the test suite as much as possible, so that it is possible to verify that they pass under the same conditions as before this change. However some changes were required to support the notion that apks depend on an app and its rowid, whereas this was not the case before. Thus there is some more boilerplate in the tests to ensure that inserting an apk ensures an app entry is present in the db too. See merge request !345
This commit is contained in:
commit
698c517508
@ -70,6 +70,7 @@ connected23:
|
|||||||
# this file changes every time but should not be cached
|
# this file changes every time but should not be cached
|
||||||
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
||||||
- exit $EXITVALUE
|
- exit $EXITVALUE
|
||||||
|
allow_failure: true # remove once install it runs reliably
|
||||||
|
|
||||||
pmd:
|
pmd:
|
||||||
script:
|
script:
|
||||||
|
@ -61,6 +61,11 @@ public class Apk extends ValueObject implements Comparable<Apk> {
|
|||||||
public String repoAddress;
|
public String repoAddress;
|
||||||
public String[] incompatibleReasons;
|
public String[] incompatibleReasons;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The numeric primary key of the App table, which is used to join apks.
|
||||||
|
*/
|
||||||
|
public long appId;
|
||||||
|
|
||||||
public Apk() {
|
public Apk() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,6 +79,9 @@ public class Apk extends ValueObject implements Comparable<Apk> {
|
|||||||
|
|
||||||
for (int i = 0; i < cursor.getColumnCount(); i++) {
|
for (int i = 0; i < cursor.getColumnCount(); i++) {
|
||||||
switch (cursor.getColumnName(i)) {
|
switch (cursor.getColumnName(i)) {
|
||||||
|
case Cols.APP_ID:
|
||||||
|
appId = cursor.getLong(i);
|
||||||
|
break;
|
||||||
case Cols.HASH:
|
case Cols.HASH:
|
||||||
hash = cursor.getString(i);
|
hash = cursor.getString(i);
|
||||||
break;
|
break;
|
||||||
@ -131,10 +139,10 @@ public class Apk extends ValueObject implements Comparable<Apk> {
|
|||||||
case Cols.VERSION_CODE:
|
case Cols.VERSION_CODE:
|
||||||
versionCode = cursor.getInt(i);
|
versionCode = cursor.getInt(i);
|
||||||
break;
|
break;
|
||||||
case Cols.REPO_VERSION:
|
case Cols.Repo.VERSION:
|
||||||
repoVersion = cursor.getInt(i);
|
repoVersion = cursor.getInt(i);
|
||||||
break;
|
break;
|
||||||
case Cols.REPO_ADDRESS:
|
case Cols.Repo.ADDRESS:
|
||||||
repoAddress = cursor.getString(i);
|
repoAddress = cursor.getString(i);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -192,6 +200,7 @@ public class Apk extends ValueObject implements Comparable<Apk> {
|
|||||||
|
|
||||||
public ContentValues toContentValues() {
|
public ContentValues toContentValues() {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(Cols.APP_ID, appId);
|
||||||
values.put(Cols.PACKAGE_NAME, packageName);
|
values.put(Cols.PACKAGE_NAME, packageName);
|
||||||
values.put(Cols.VERSION_NAME, versionName);
|
values.put(Cols.VERSION_NAME, versionName);
|
||||||
values.put(Cols.VERSION_CODE, versionCode);
|
values.put(Cols.VERSION_CODE, versionCode);
|
||||||
|
@ -8,9 +8,9 @@ import android.database.Cursor;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.fdroid.fdroid.Utils;
|
|
||||||
import org.fdroid.fdroid.data.Schema.ApkTable;
|
import org.fdroid.fdroid.data.Schema.ApkTable;
|
||||||
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
|
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
|
||||||
|
import org.fdroid.fdroid.data.Schema.AppTable;
|
||||||
import org.fdroid.fdroid.data.Schema.RepoTable;
|
import org.fdroid.fdroid.data.Schema.RepoTable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -101,13 +101,6 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
return cursorToList(cursor);
|
return cursorToList(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @see org.fdroid.fdroid.data.ApkProvider.Helper#find(Context, Repo, List, String[])
|
|
||||||
*/
|
|
||||||
public static List<Apk> find(Context context, Repo repo, List<App> apps) {
|
|
||||||
return find(context, repo, apps, Cols.ALL);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Apk find(Context context, String packageName, int versionCode, String[] projection) {
|
public static Apk find(Context context, String packageName, int versionCode, String[] projection) {
|
||||||
final Uri uri = getContentUri(packageName, versionCode);
|
final Uri uri = getContentUri(packageName, versionCode);
|
||||||
return find(context, uri, projection);
|
return find(context, uri, projection);
|
||||||
@ -135,7 +128,7 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
String packageName, String[] projection) {
|
String packageName, String[] projection) {
|
||||||
ContentResolver resolver = context.getContentResolver();
|
ContentResolver resolver = context.getContentResolver();
|
||||||
final Uri uri = getAppUri(packageName);
|
final Uri uri = getAppUri(packageName);
|
||||||
final String sort = Cols.VERSION_CODE + " DESC";
|
final String sort = "apk." + Cols.VERSION_CODE + " DESC";
|
||||||
Cursor cursor = resolver.query(uri, projection, null, null, sort);
|
Cursor cursor = resolver.query(uri, projection, null, null, sort);
|
||||||
return cursorToList(cursor);
|
return cursorToList(cursor);
|
||||||
}
|
}
|
||||||
@ -213,10 +206,12 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
private static final UriMatcher MATCHER = new UriMatcher(-1);
|
private static final UriMatcher MATCHER = new UriMatcher(-1);
|
||||||
|
|
||||||
private static final Map<String, String> REPO_FIELDS = new HashMap<>();
|
private static final Map<String, String> REPO_FIELDS = new HashMap<>();
|
||||||
|
private static final Map<String, String> APP_FIELDS = new HashMap<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
REPO_FIELDS.put(Cols.REPO_VERSION, RepoTable.Cols.VERSION);
|
REPO_FIELDS.put(Cols.Repo.VERSION, RepoTable.Cols.VERSION);
|
||||||
REPO_FIELDS.put(Cols.REPO_ADDRESS, RepoTable.Cols.ADDRESS);
|
REPO_FIELDS.put(Cols.Repo.ADDRESS, RepoTable.Cols.ADDRESS);
|
||||||
|
APP_FIELDS.put(Cols.App.PACKAGE_NAME, AppTable.Cols.PACKAGE_NAME);
|
||||||
|
|
||||||
MATCHER.addURI(getAuthority(), PATH_REPO + "/#", CODE_REPO);
|
MATCHER.addURI(getAuthority(), PATH_REPO + "/#", CODE_REPO);
|
||||||
MATCHER.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE);
|
MATCHER.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE);
|
||||||
@ -273,15 +268,6 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri getContentUriForApks(Repo repo, List<Apk> apks) {
|
|
||||||
return getContentUri()
|
|
||||||
.buildUpon()
|
|
||||||
.appendPath(PATH_REPO_APK)
|
|
||||||
.appendPath(Long.toString(repo.id))
|
|
||||||
.appendPath(buildApkString(apks))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intentionally left protected because it will break if apks is larger than
|
* Intentionally left protected because it will break if apks is larger than
|
||||||
* {@link org.fdroid.fdroid.data.ApkProvider#MAX_APKS_TO_QUERY}. Instead of using
|
* {@link org.fdroid.fdroid.data.ApkProvider#MAX_APKS_TO_QUERY}. Instead of using
|
||||||
@ -323,6 +309,10 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
return ApkTable.NAME;
|
return ApkTable.NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String getAppTableName() {
|
||||||
|
return AppTable.NAME;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getProviderName() {
|
protected String getProviderName() {
|
||||||
return PROVIDER_NAME;
|
return PROVIDER_NAME;
|
||||||
@ -333,30 +323,40 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
return MATCHER;
|
return MATCHER;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Query extends QueryBuilder {
|
private class Query extends QueryBuilder {
|
||||||
|
|
||||||
private boolean repoTableRequired;
|
private boolean repoTableRequired;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getRequiredTables() {
|
protected String getRequiredTables() {
|
||||||
return ApkTable.NAME + " AS apk";
|
final String apk = getTableName();
|
||||||
|
final String app = getAppTableName();
|
||||||
|
|
||||||
|
return apk + " AS apk " +
|
||||||
|
" LEFT JOIN " + app + " AS app ON (app." + AppTable.Cols.ROW_ID + " = apk." + Cols.APP_ID + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addField(String field) {
|
public void addField(String field) {
|
||||||
if (REPO_FIELDS.containsKey(field)) {
|
if (APP_FIELDS.containsKey(field)) {
|
||||||
|
addAppField(APP_FIELDS.get(field), field);
|
||||||
|
} else if (REPO_FIELDS.containsKey(field)) {
|
||||||
addRepoField(REPO_FIELDS.get(field), field);
|
addRepoField(REPO_FIELDS.get(field), field);
|
||||||
} else if (field.equals(Cols._ID)) {
|
} else if (field.equals(Cols._ID)) {
|
||||||
appendField("rowid", "apk", "_id");
|
appendField("rowid", "apk", "_id");
|
||||||
} else if (field.equals(Cols._COUNT)) {
|
} else if (field.equals(Cols._COUNT)) {
|
||||||
appendField("COUNT(*) AS " + Cols._COUNT);
|
appendField("COUNT(*) AS " + Cols._COUNT);
|
||||||
} else if (field.equals(Cols._COUNT_DISTINCT)) {
|
} else if (field.equals(Cols._COUNT_DISTINCT)) {
|
||||||
appendField("COUNT(DISTINCT apk." + Cols.PACKAGE_NAME + ") AS " + Cols._COUNT_DISTINCT);
|
appendField("COUNT(DISTINCT apk." + Cols.APP_ID + ") AS " + Cols._COUNT_DISTINCT);
|
||||||
} else {
|
} else {
|
||||||
appendField(field, "apk");
|
appendField(field, "apk");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addAppField(String field, String alias) {
|
||||||
|
appendField(field, "app", alias);
|
||||||
|
}
|
||||||
|
|
||||||
private void addRepoField(String field, String alias) {
|
private void addRepoField(String field, String alias) {
|
||||||
if (!repoTableRequired) {
|
if (!repoTableRequired) {
|
||||||
repoTableRequired = true;
|
repoTableRequired = true;
|
||||||
@ -368,13 +368,23 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private QuerySelection queryApp(String packageName) {
|
private QuerySelection queryApp(String packageName) {
|
||||||
final String selection = Cols.PACKAGE_NAME + " = ? ";
|
return queryApp(packageName, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private QuerySelection queryApp(String packageName, boolean includeTableAlias) {
|
||||||
|
String alias = includeTableAlias ? "apk." : "";
|
||||||
|
final String selection = alias + Cols.APP_ID + " = (" + getAppIdFromPackageNameQuery() + ")";
|
||||||
final String[] args = {packageName};
|
final String[] args = {packageName};
|
||||||
return new QuerySelection(selection, args);
|
return new QuerySelection(selection, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
private QuerySelection querySingle(Uri uri) {
|
private QuerySelection querySingle(Uri uri) {
|
||||||
final String selection = Cols.VERSION_CODE + " = ? and " + Cols.PACKAGE_NAME + " = ? ";
|
return querySingle(uri, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private QuerySelection querySingle(Uri uri, boolean includeAlias) {
|
||||||
|
String alias = includeAlias ? "apk." : "";
|
||||||
|
final String selection = " " + alias + Cols.VERSION_CODE + " = ? and " + alias + Cols.PACKAGE_NAME + " = ? ";
|
||||||
final String[] args = {
|
final String[] args = {
|
||||||
// First (0th) path segment is the word "apk",
|
// First (0th) path segment is the word "apk",
|
||||||
// and we are not interested in it.
|
// and we are not interested in it.
|
||||||
@ -385,22 +395,32 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected QuerySelection queryRepo(long repoId) {
|
protected QuerySelection queryRepo(long repoId) {
|
||||||
final String selection = Cols.REPO_ID + " = ? ";
|
return queryRepo(repoId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected QuerySelection queryRepo(long repoId, boolean includeAlias) {
|
||||||
|
String alias = includeAlias ? "apk." : "";
|
||||||
|
final String selection = alias + Cols.REPO_ID + " = ? ";
|
||||||
final String[] args = {Long.toString(repoId)};
|
final String[] args = {Long.toString(repoId)};
|
||||||
return new QuerySelection(selection, args);
|
return new QuerySelection(selection, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
private QuerySelection queryRepoApps(long repoId, String packageNames) {
|
private QuerySelection queryRepoApps(long repoId, String packageNames) {
|
||||||
return queryRepo(repoId).add(AppProvider.queryApps(packageNames, Cols.PACKAGE_NAME));
|
return queryRepo(repoId).add(AppProvider.queryApps(packageNames, "app." + AppTable.Cols.PACKAGE_NAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected QuerySelection queryApks(String apkKeys) {
|
protected QuerySelection queryApks(String apkKeys) {
|
||||||
|
return queryApks(apkKeys, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected QuerySelection queryApks(String apkKeys, boolean includeAlias) {
|
||||||
final String[] apkDetails = apkKeys.split(",");
|
final String[] apkDetails = apkKeys.split(",");
|
||||||
if (apkDetails.length > MAX_APKS_TO_QUERY) {
|
if (apkDetails.length > MAX_APKS_TO_QUERY) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Cannot query more than " + MAX_APKS_TO_QUERY + ". " +
|
"Cannot query more than " + MAX_APKS_TO_QUERY + ". " +
|
||||||
"You tried to query " + apkDetails.length);
|
"You tried to query " + apkDetails.length);
|
||||||
}
|
}
|
||||||
|
String alias = includeAlias ? "apk." : "";
|
||||||
final String[] args = new String[apkDetails.length * 2];
|
final String[] args = new String[apkDetails.length * 2];
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (int i = 0; i < apkDetails.length; i++) {
|
for (int i = 0; i < apkDetails.length; i++) {
|
||||||
@ -412,11 +432,23 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
if (i != 0) {
|
if (i != 0) {
|
||||||
sb.append(" OR ");
|
sb.append(" OR ");
|
||||||
}
|
}
|
||||||
sb.append(" ( " + Cols.PACKAGE_NAME + " = ? AND " + Cols.VERSION_CODE + " = ? ) ");
|
sb.append(" ( ")
|
||||||
|
.append(alias)
|
||||||
|
.append(Cols.APP_ID)
|
||||||
|
.append(" = (")
|
||||||
|
.append(getAppIdFromPackageNameQuery())
|
||||||
|
.append(") AND ")
|
||||||
|
.append(alias)
|
||||||
|
.append(Cols.VERSION_CODE)
|
||||||
|
.append(" = ? ) ");
|
||||||
}
|
}
|
||||||
return new QuerySelection(sb.toString(), args);
|
return new QuerySelection(sb.toString(), args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getAppIdFromPackageNameQuery() {
|
||||||
|
return "SELECT " + AppTable.Cols.ROW_ID + " FROM " + getAppTableName() + " WHERE " + AppTable.Cols.PACKAGE_NAME + " = ?";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||||
|
|
||||||
@ -464,13 +496,17 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void removeRepoFields(ContentValues values) {
|
private static void removeFieldsFromOtherTables(ContentValues values) {
|
||||||
for (Map.Entry<String, String> repoField : REPO_FIELDS.entrySet()) {
|
for (Map.Entry<String, String> repoField : REPO_FIELDS.entrySet()) {
|
||||||
final String field = repoField.getKey();
|
final String field = repoField.getKey();
|
||||||
if (values.containsKey(field)) {
|
if (values.containsKey(field)) {
|
||||||
Utils.debugLog(TAG, "Cannot insert/update '" + field + "' field " +
|
values.remove(field);
|
||||||
"on apk table, as it belongs to the repo table. " +
|
}
|
||||||
"This field will be ignored.");
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> appField : APP_FIELDS.entrySet()) {
|
||||||
|
final String field = appField.getKey();
|
||||||
|
if (values.containsKey(field)) {
|
||||||
values.remove(field);
|
values.remove(field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -478,7 +514,7 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Uri insert(Uri uri, ContentValues values) {
|
public Uri insert(Uri uri, ContentValues values) {
|
||||||
removeRepoFields(values);
|
removeFieldsFromOtherTables(values);
|
||||||
validateFields(Cols.ALL, values);
|
validateFields(Cols.ALL, values);
|
||||||
db().insertOrThrow(getTableName(), null, values);
|
db().insertOrThrow(getTableName(), null, values);
|
||||||
if (!isApplyingBatch()) {
|
if (!isApplyingBatch()) {
|
||||||
@ -498,15 +534,15 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
switch (MATCHER.match(uri)) {
|
switch (MATCHER.match(uri)) {
|
||||||
|
|
||||||
case CODE_REPO:
|
case CODE_REPO:
|
||||||
query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment())));
|
query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment()), false));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CODE_APP:
|
case CODE_APP:
|
||||||
query = query.add(queryApp(uri.getLastPathSegment()));
|
query = query.add(queryApp(uri.getLastPathSegment(), false));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CODE_APKS:
|
case CODE_APKS:
|
||||||
query = query.add(queryApks(uri.getLastPathSegment()));
|
query = query.add(queryApks(uri.getLastPathSegment(), false));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// TODO: Add tests for this.
|
// TODO: Add tests for this.
|
||||||
@ -542,10 +578,10 @@ public class ApkProvider extends FDroidProvider {
|
|||||||
|
|
||||||
protected int performUpdateUnchecked(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
protected int performUpdateUnchecked(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||||
validateFields(Cols.ALL, values);
|
validateFields(Cols.ALL, values);
|
||||||
removeRepoFields(values);
|
removeFieldsFromOtherTables(values);
|
||||||
|
|
||||||
QuerySelection query = new QuerySelection(where, whereArgs);
|
QuerySelection query = new QuerySelection(where, whereArgs);
|
||||||
query = query.add(querySingle(uri));
|
query = query.add(querySingle(uri, false));
|
||||||
|
|
||||||
int numRows = db().update(getTableName(), values, query.getSelection(), query.getArgs());
|
int numRows = db().update(getTableName(), values, query.getSelection(), query.getArgs());
|
||||||
if (!isApplyingBatch()) {
|
if (!isApplyingBatch()) {
|
||||||
|
@ -130,6 +130,8 @@ public class App extends ValueObject implements Comparable<App> {
|
|||||||
|
|
||||||
public String installedSig;
|
public String installedSig;
|
||||||
|
|
||||||
|
private long id;
|
||||||
|
|
||||||
public static String getIconName(String packageName, int versionCode) {
|
public static String getIconName(String packageName, int versionCode) {
|
||||||
return packageName + "_" + versionCode + ".png";
|
return packageName + "_" + versionCode + ".png";
|
||||||
}
|
}
|
||||||
@ -153,6 +155,9 @@ public class App extends ValueObject implements Comparable<App> {
|
|||||||
for (int i = 0; i < cursor.getColumnCount(); i++) {
|
for (int i = 0; i < cursor.getColumnCount(); i++) {
|
||||||
String n = cursor.getColumnName(i);
|
String n = cursor.getColumnName(i);
|
||||||
switch (n) {
|
switch (n) {
|
||||||
|
case Cols.ROW_ID:
|
||||||
|
id = cursor.getLong(i);
|
||||||
|
break;
|
||||||
case Cols.IS_COMPATIBLE:
|
case Cols.IS_COMPATIBLE:
|
||||||
compatible = cursor.getInt(i) == 1;
|
compatible = cursor.getInt(i) == 1;
|
||||||
break;
|
break;
|
||||||
@ -440,6 +445,8 @@ public class App extends ValueObject implements Comparable<App> {
|
|||||||
public ContentValues toContentValues() {
|
public ContentValues toContentValues() {
|
||||||
|
|
||||||
final ContentValues values = new ContentValues();
|
final ContentValues values = new ContentValues();
|
||||||
|
// Intentionally don't put "ROW_ID" in here, because we don't ever want to change that
|
||||||
|
// primary key generated by sqlite.
|
||||||
values.put(Cols.PACKAGE_NAME, packageName);
|
values.put(Cols.PACKAGE_NAME, packageName);
|
||||||
values.put(Cols.NAME, name);
|
values.put(Cols.NAME, name);
|
||||||
values.put(Cols.SUMMARY, summary);
|
values.put(Cols.SUMMARY, summary);
|
||||||
@ -549,4 +556,8 @@ public class App extends ValueObject implements Comparable<App> {
|
|||||||
}
|
}
|
||||||
return new int[]{minSdkVersion, targetSdkVersion, maxSdkVersion};
|
return new int[]{minSdkVersion, targetSdkVersion, maxSdkVersion};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
return cursorToList(cursor);
|
return cursorToList(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<App> cursorToList(Cursor cursor) {
|
static List<App> cursorToList(Cursor cursor) {
|
||||||
int knownAppCount = cursor != null ? cursor.getCount() : 0;
|
int knownAppCount = cursor != null ? cursor.getCount() : 0;
|
||||||
List<App> apps = new ArrayList<>(knownAppCount);
|
List<App> apps = new ArrayList<>(knownAppCount);
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
@ -153,7 +153,6 @@ public class AppProvider extends FDroidProvider {
|
|||||||
final Uri fromUpstream = calcAppDetailsFromIndexUri();
|
final Uri fromUpstream = calcAppDetailsFromIndexUri();
|
||||||
context.getContentResolver().update(fromUpstream, null, null, null);
|
context.getContentResolver().update(fromUpstream, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -184,7 +183,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
* method from this class will return an instance of this class, that is aware of
|
* method from this class will return an instance of this class, that is aware of
|
||||||
* the install apps table.
|
* the install apps table.
|
||||||
*/
|
*/
|
||||||
private static class AppQuerySelection extends QuerySelection {
|
protected static class AppQuerySelection extends QuerySelection {
|
||||||
|
|
||||||
private boolean naturalJoinToInstalled;
|
private boolean naturalJoinToInstalled;
|
||||||
|
|
||||||
@ -202,10 +201,6 @@ public class AppProvider extends FDroidProvider {
|
|||||||
super(selection, args);
|
super(selection, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppQuerySelection(String selection, List<String> args) {
|
|
||||||
super(selection, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean naturalJoinToInstalled() {
|
public boolean naturalJoinToInstalled() {
|
||||||
return naturalJoinToInstalled;
|
return naturalJoinToInstalled;
|
||||||
}
|
}
|
||||||
@ -247,7 +242,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
final String repo = RepoTable.NAME;
|
final String repo = RepoTable.NAME;
|
||||||
|
|
||||||
return app +
|
return app +
|
||||||
" LEFT JOIN " + apk + " ON (" + apk + "." + ApkTable.Cols.PACKAGE_NAME + " = " + app + "." + Cols.PACKAGE_NAME + ") " +
|
" LEFT JOIN " + apk + " ON (" + apk + "." + ApkTable.Cols.APP_ID + " = " + app + "." + Cols.ROW_ID + ") " +
|
||||||
" LEFT JOIN " + repo + " ON (" + apk + "." + ApkTable.Cols.REPO_ID + " = " + repo + "." + RepoTable.Cols._ID + ") ";
|
" LEFT JOIN " + repo + " ON (" + apk + "." + ApkTable.Cols.REPO_ID + " = " + repo + "." + RepoTable.Cols._ID + ") ";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,7 +254,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
@Override
|
@Override
|
||||||
protected String groupBy() {
|
protected String groupBy() {
|
||||||
// If the count field has been requested, then we want to group all rows together.
|
// If the count field has been requested, then we want to group all rows together.
|
||||||
return countFieldAppended ? null : getTableName() + "." + Cols.PACKAGE_NAME;
|
return countFieldAppended ? null : getTableName() + "." + Cols.ROW_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSelection(AppQuerySelection selection) {
|
public void addSelection(AppQuerySelection selection) {
|
||||||
@ -320,7 +315,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
|
|
||||||
private void appendCountField() {
|
private void appendCountField() {
|
||||||
countFieldAppended = true;
|
countFieldAppended = true;
|
||||||
appendField("COUNT( DISTINCT " + getTableName() + "." + Cols.PACKAGE_NAME + " ) AS " + Cols._COUNT);
|
appendField("COUNT( DISTINCT " + getTableName() + "." + Cols.ROW_ID + " ) AS " + Cols._COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addSuggestedApkVersionField() {
|
private void addSuggestedApkVersionField() {
|
||||||
@ -335,7 +330,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
leftJoin(
|
leftJoin(
|
||||||
getApkTableName(),
|
getApkTableName(),
|
||||||
"suggestedApk",
|
"suggestedApk",
|
||||||
getTableName() + "." + Cols.SUGGESTED_VERSION_CODE + " = suggestedApk." + ApkTable.Cols.VERSION_CODE + " AND " + getTableName() + "." + Cols.PACKAGE_NAME + " = suggestedApk." + ApkTable.Cols.PACKAGE_NAME);
|
getTableName() + "." + Cols.SUGGESTED_VERSION_CODE + " = suggestedApk." + ApkTable.Cols.VERSION_CODE + " AND " + getTableName() + "." + Cols.ROW_ID + " = suggestedApk." + ApkTable.Cols.APP_ID);
|
||||||
}
|
}
|
||||||
appendField(fieldName, "suggestedApk", alias);
|
appendField(fieldName, "suggestedApk", alias);
|
||||||
}
|
}
|
||||||
@ -378,7 +373,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
private static final String PATH_SEARCH_CAN_UPDATE = "searchCanUpdate";
|
private static final String PATH_SEARCH_CAN_UPDATE = "searchCanUpdate";
|
||||||
private static final String PATH_SEARCH_REPO = "searchRepo";
|
private static final String PATH_SEARCH_REPO = "searchRepo";
|
||||||
private static final String PATH_NO_APKS = "noApks";
|
private static final String PATH_NO_APKS = "noApks";
|
||||||
private static final String PATH_APPS = "apps";
|
protected static final String PATH_APPS = "apps";
|
||||||
private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated";
|
private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated";
|
||||||
private static final String PATH_NEWLY_ADDED = "newlyAdded";
|
private static final String PATH_NEWLY_ADDED = "newlyAdded";
|
||||||
private static final String PATH_CATEGORY = "category";
|
private static final String PATH_CATEGORY = "category";
|
||||||
@ -390,8 +385,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
private static final int INSTALLED = CAN_UPDATE + 1;
|
private static final int INSTALLED = CAN_UPDATE + 1;
|
||||||
private static final int SEARCH = INSTALLED + 1;
|
private static final int SEARCH = INSTALLED + 1;
|
||||||
private static final int NO_APKS = SEARCH + 1;
|
private static final int NO_APKS = SEARCH + 1;
|
||||||
private static final int APPS = NO_APKS + 1;
|
private static final int RECENTLY_UPDATED = NO_APKS + 1;
|
||||||
private static final int RECENTLY_UPDATED = APPS + 1;
|
|
||||||
private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1;
|
private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1;
|
||||||
private static final int CATEGORY = NEWLY_ADDED + 1;
|
private static final int CATEGORY = NEWLY_ADDED + 1;
|
||||||
private static final int IGNORED = CATEGORY + 1;
|
private static final int IGNORED = CATEGORY + 1;
|
||||||
@ -416,7 +410,6 @@ public class AppProvider extends FDroidProvider {
|
|||||||
MATCHER.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE);
|
MATCHER.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE);
|
||||||
MATCHER.addURI(getAuthority(), PATH_INSTALLED, INSTALLED);
|
MATCHER.addURI(getAuthority(), PATH_INSTALLED, INSTALLED);
|
||||||
MATCHER.addURI(getAuthority(), PATH_NO_APKS, NO_APKS);
|
MATCHER.addURI(getAuthority(), PATH_NO_APKS, NO_APKS);
|
||||||
MATCHER.addURI(getAuthority(), PATH_APPS + "/*", APPS);
|
|
||||||
MATCHER.addURI(getAuthority(), "*", CODE_SINGLE);
|
MATCHER.addURI(getAuthority(), "*", CODE_SINGLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,20 +459,6 @@ public class AppProvider extends FDroidProvider {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri getContentUri(List<App> apps) {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
for (int i = 0; i < apps.size(); i++) {
|
|
||||||
if (i != 0) {
|
|
||||||
builder.append(',');
|
|
||||||
}
|
|
||||||
builder.append(apps.get(i).packageName);
|
|
||||||
}
|
|
||||||
return getContentUri().buildUpon()
|
|
||||||
.appendPath(PATH_APPS)
|
|
||||||
.appendPath(builder.toString())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Uri getContentUri(App app) {
|
public static Uri getContentUri(App app) {
|
||||||
return getContentUri(app.packageName);
|
return getContentUri(app.packageName);
|
||||||
}
|
}
|
||||||
@ -684,10 +663,6 @@ public class AppProvider extends FDroidProvider {
|
|||||||
return new AppQuerySelection(selection, args);
|
return new AppQuerySelection(selection, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppQuerySelection queryApps(String packageNames) {
|
|
||||||
return queryApps(packageNames, getTableName() + "." + Cols.PACKAGE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
|
public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
|
||||||
AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs);
|
AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs);
|
||||||
@ -742,10 +717,6 @@ public class AppProvider extends FDroidProvider {
|
|||||||
selection = selection.add(queryNoApks());
|
selection = selection.add(queryNoApks());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case APPS:
|
|
||||||
selection = selection.add(queryApps(uri.getLastPathSegment()));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case IGNORED:
|
case IGNORED:
|
||||||
selection = selection.add(queryIgnored());
|
selection = selection.add(queryIgnored());
|
||||||
break;
|
break;
|
||||||
@ -772,6 +743,14 @@ public class AppProvider extends FDroidProvider {
|
|||||||
throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri);
|
throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return runQuery(uri, selection, projection, includeSwap, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method used by both the genuine {@link AppProvider} and the temporary version used
|
||||||
|
* by the repo updater ({@link TempAppProvider}).
|
||||||
|
*/
|
||||||
|
protected Cursor runQuery(Uri uri, AppQuerySelection selection, String[] projection, boolean includeSwap, String sortOrder) {
|
||||||
if (!includeSwap) {
|
if (!includeSwap) {
|
||||||
selection = selection.add(queryExcludeSwap());
|
selection = selection.add(queryExcludeSwap());
|
||||||
}
|
}
|
||||||
@ -973,7 +952,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
apk +
|
apk +
|
||||||
" JOIN " + repo + " ON (" + repo + "." + RepoTable.Cols._ID + " = " + apk + "." + ApkTable.Cols.REPO_ID + ") " +
|
" JOIN " + repo + " ON (" + repo + "." + RepoTable.Cols._ID + " = " + apk + "." + ApkTable.Cols.REPO_ID + ") " +
|
||||||
" WHERE " +
|
" WHERE " +
|
||||||
app + "." + Cols.PACKAGE_NAME + " = " + apk + "." + ApkTable.Cols.PACKAGE_NAME + " AND " +
|
app + "." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + " AND " +
|
||||||
apk + "." + ApkTable.Cols.VERSION_CODE + " = " + app + "." + Cols.SUGGESTED_VERSION_CODE;
|
apk + "." + ApkTable.Cols.VERSION_CODE + " = " + app + "." + Cols.SUGGESTED_VERSION_CODE;
|
||||||
|
|
||||||
return "UPDATE " + app + " SET "
|
return "UPDATE " + app + " SET "
|
||||||
|
@ -65,7 +65,10 @@ class ContentValuesCursor extends AbstractCursor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getLong(int i) {
|
public long getLong(int i) {
|
||||||
throw new IllegalArgumentException("unimplemented");
|
if (values[i] instanceof Long) {
|
||||||
|
return (Long) values[i];
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Value is not a Long");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -47,6 +47,7 @@ class DBHelper extends SQLiteOpenHelper {
|
|||||||
private static final String CREATE_TABLE_APK =
|
private static final String CREATE_TABLE_APK =
|
||||||
"CREATE TABLE " + ApkTable.NAME + " ( "
|
"CREATE TABLE " + ApkTable.NAME + " ( "
|
||||||
+ ApkTable.Cols.PACKAGE_NAME + " text not null, "
|
+ ApkTable.Cols.PACKAGE_NAME + " text not null, "
|
||||||
|
+ ApkTable.Cols.APP_ID + " integer not null, "
|
||||||
+ ApkTable.Cols.VERSION_NAME + " text not null, "
|
+ ApkTable.Cols.VERSION_NAME + " text not null, "
|
||||||
+ ApkTable.Cols.REPO_ID + " integer not null, "
|
+ ApkTable.Cols.REPO_ID + " integer not null, "
|
||||||
+ ApkTable.Cols.HASH + " text not null, "
|
+ ApkTable.Cols.HASH + " text not null, "
|
||||||
@ -114,7 +115,7 @@ class DBHelper extends SQLiteOpenHelper {
|
|||||||
+ " );";
|
+ " );";
|
||||||
private static final String DROP_TABLE_INSTALLED_APP = "DROP TABLE " + InstalledAppTable.NAME + ";";
|
private static final String DROP_TABLE_INSTALLED_APP = "DROP TABLE " + InstalledAppTable.NAME + ";";
|
||||||
|
|
||||||
private static final int DB_VERSION = 57;
|
private static final int DB_VERSION = 58;
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
@ -315,8 +316,34 @@ class DBHelper extends SQLiteOpenHelper {
|
|||||||
requireTimestampInRepos(db, oldVersion);
|
requireTimestampInRepos(db, oldVersion);
|
||||||
recreateInstalledAppTable(db, oldVersion);
|
recreateInstalledAppTable(db, oldVersion);
|
||||||
addTargetSdkVersionToApk(db, oldVersion);
|
addTargetSdkVersionToApk(db, oldVersion);
|
||||||
|
migrateAppPrimaryKeyToRowId(db, oldVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void migrateAppPrimaryKeyToRowId(SQLiteDatabase db, int oldVersion) {
|
||||||
|
if (oldVersion < 58 && !columnExists(db, ApkTable.NAME, ApkTable.Cols.APP_ID)) {
|
||||||
|
db.beginTransaction();
|
||||||
|
try {
|
||||||
|
final String alter = "ALTER TABLE " + ApkTable.NAME + " ADD COLUMN " + ApkTable.Cols.APP_ID + " NUMERIC";
|
||||||
|
Log.i(TAG, "Adding appId foreign key to " + ApkTable.NAME);
|
||||||
|
Utils.debugLog(TAG, alter);
|
||||||
|
db.execSQL(alter);
|
||||||
|
|
||||||
|
final String update = "UPDATE " + ApkTable.NAME + " SET " + ApkTable.Cols.APP_ID + " = ( " +
|
||||||
|
"SELECT app." + AppTable.Cols.ROW_ID + " " +
|
||||||
|
"FROM " + AppTable.NAME + " AS app " +
|
||||||
|
"WHERE " + ApkTable.NAME + "." + ApkTable.Cols.PACKAGE_NAME + " = app." + AppTable.Cols.PACKAGE_NAME + ")";
|
||||||
|
Log.i(TAG, "Updating foreign key from " + ApkTable.NAME + " to " + AppTable.NAME + " to use numeric foreign key.");
|
||||||
|
Utils.debugLog(TAG, update);
|
||||||
|
db.execSQL(update);
|
||||||
|
ensureIndexes(db);
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate repo list to new structure. (No way to change primary
|
* Migrate repo list to new structure. (No way to change primary
|
||||||
* key in sqlite - table must be recreated).
|
* key in sqlite - table must be recreated).
|
||||||
@ -567,10 +594,20 @@ class DBHelper extends SQLiteOpenHelper {
|
|||||||
|
|
||||||
private static void createAppApk(SQLiteDatabase db) {
|
private static void createAppApk(SQLiteDatabase db) {
|
||||||
db.execSQL(CREATE_TABLE_APP);
|
db.execSQL(CREATE_TABLE_APP);
|
||||||
db.execSQL("create index app_id on " + AppTable.NAME + " (" + AppTable.Cols.PACKAGE_NAME + ");");
|
|
||||||
db.execSQL(CREATE_TABLE_APK);
|
db.execSQL(CREATE_TABLE_APK);
|
||||||
db.execSQL("create index apk_vercode on " + ApkTable.NAME + " (" + ApkTable.Cols.VERSION_CODE + ");");
|
ensureIndexes(db);
|
||||||
db.execSQL("create index apk_id on " + ApkTable.NAME + " (" + AppTable.Cols.PACKAGE_NAME + ");");
|
}
|
||||||
|
|
||||||
|
private static void ensureIndexes(SQLiteDatabase db) {
|
||||||
|
Utils.debugLog(TAG, "Ensuring indexes exist for " + AppTable.NAME);
|
||||||
|
db.execSQL("CREATE INDEX IF NOT EXISTS app_id on " + AppTable.NAME + " (" + AppTable.Cols.PACKAGE_NAME + ");");
|
||||||
|
db.execSQL("CREATE INDEX IF NOT EXISTS name on " + AppTable.NAME + " (" + AppTable.Cols.NAME + ");"); // Used for sorting most lists
|
||||||
|
db.execSQL("CREATE INDEX IF NOT EXISTS added on " + AppTable.NAME + " (" + AppTable.Cols.ADDED + ");"); // Used for sorting "newly added"
|
||||||
|
|
||||||
|
Utils.debugLog(TAG, "Ensuring indexes exist for " + ApkTable.NAME);
|
||||||
|
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 apk_id on " + ApkTable.NAME + " (" + AppTable.Cols.PACKAGE_NAME + ");");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,16 +98,19 @@ public class RepoPersister {
|
|||||||
|
|
||||||
if (apksToSave.size() > 0 || appsToSave.size() > 0) {
|
if (apksToSave.size() > 0 || appsToSave.size() > 0) {
|
||||||
Utils.debugLog(TAG, "Flushing details of up to " + MAX_APP_BUFFER + " apps and their packages to the database.");
|
Utils.debugLog(TAG, "Flushing details of up to " + MAX_APP_BUFFER + " apps and their packages to the database.");
|
||||||
flushAppsToDbInBatch();
|
Map<String, Long> appIds = flushAppsToDbInBatch();
|
||||||
flushApksToDbInBatch();
|
flushApksToDbInBatch(appIds);
|
||||||
apksToSave.clear();
|
apksToSave.clear();
|
||||||
appsToSave.clear();
|
appsToSave.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void flushApksToDbInBatch() throws RepoUpdater.UpdateException {
|
private void flushApksToDbInBatch(Map<String, Long> appIds) throws RepoUpdater.UpdateException {
|
||||||
List<Apk> apksToSaveList = new ArrayList<>();
|
List<Apk> apksToSaveList = new ArrayList<>();
|
||||||
for (Map.Entry<String, List<Apk>> entries : apksToSave.entrySet()) {
|
for (Map.Entry<String, List<Apk>> entries : apksToSave.entrySet()) {
|
||||||
|
for (Apk apk : entries.getValue()) {
|
||||||
|
apk.appId = appIds.get(apk.packageName);
|
||||||
|
}
|
||||||
apksToSaveList.addAll(entries.getValue());
|
apksToSaveList.addAll(entries.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,16 +130,43 @@ public class RepoPersister {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void flushAppsToDbInBatch() throws RepoUpdater.UpdateException {
|
/**
|
||||||
|
* Will first insert new or update existing rows in the database for each {@link RepoPersister#appsToSave}.
|
||||||
|
* Then, will query the database for the ID + packageName for each of these apps, so that they
|
||||||
|
* can be returned and the relevant apks can be joined to the app table correctly.
|
||||||
|
*/
|
||||||
|
private Map<String, Long> flushAppsToDbInBatch() throws RepoUpdater.UpdateException {
|
||||||
ArrayList<ContentProviderOperation> appOperations = insertOrUpdateApps(appsToSave);
|
ArrayList<ContentProviderOperation> appOperations = insertOrUpdateApps(appsToSave);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.getContentResolver().applyBatch(TempAppProvider.getAuthority(), appOperations);
|
context.getContentResolver().applyBatch(TempAppProvider.getAuthority(), appOperations);
|
||||||
|
return getIdsForPackages(appsToSave);
|
||||||
} catch (RemoteException | OperationApplicationException e) {
|
} catch (RemoteException | OperationApplicationException e) {
|
||||||
throw new RepoUpdater.UpdateException(repo, "An internal error occurred while updating the database", e);
|
throw new RepoUpdater.UpdateException(repo, "An internal error occurred while updating the database", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Although this might seem counter intuitive - receiving a list of apps, then querying the
|
||||||
|
* database again for info about these apps, it is required because the apps came from the
|
||||||
|
* repo metadata, but we are really interested in their IDs from the database. These IDs only
|
||||||
|
* exist in SQLite and not the repo metadata.
|
||||||
|
*/
|
||||||
|
private Map<String, Long> getIdsForPackages(List<App> apps) {
|
||||||
|
List<String> packageNames = new ArrayList<>(appsToSave.size());
|
||||||
|
for (App app : apps) {
|
||||||
|
packageNames.add(app.packageName);
|
||||||
|
}
|
||||||
|
String[] projection = {Schema.AppTable.Cols.ROW_ID, Schema.AppTable.Cols.PACKAGE_NAME};
|
||||||
|
List<App> fromDb = TempAppProvider.Helper.findByPackageNames(context, packageNames, projection);
|
||||||
|
|
||||||
|
Map<String, Long> ids = new HashMap<>(fromDb.size());
|
||||||
|
for (App app : fromDb) {
|
||||||
|
ids.put(app.packageName, app.getId());
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Depending on whether the {@link App}s have been added to the database previously, this
|
* Depending on whether the {@link App}s have been added to the database previously, this
|
||||||
* will queue up an update or an insert {@link ContentProviderOperation} for each app.
|
* will queue up an update or an insert {@link ContentProviderOperation} for each app.
|
||||||
|
@ -14,7 +14,12 @@ public interface Schema {
|
|||||||
String NAME = "fdroid_app";
|
String NAME = "fdroid_app";
|
||||||
|
|
||||||
interface Cols {
|
interface Cols {
|
||||||
String _ID = "rowid as _id"; // Required for CursorLoaders
|
/**
|
||||||
|
* Same as the primary key {@link Cols#ROW_ID}, except aliased as "_id" instead
|
||||||
|
* of "rowid". Required for {@link android.content.CursorLoader}s.
|
||||||
|
*/
|
||||||
|
String _ID = "rowid as _id";
|
||||||
|
String ROW_ID = "rowid";
|
||||||
String _COUNT = "_count";
|
String _COUNT = "_count";
|
||||||
String IS_COMPATIBLE = "compatible";
|
String IS_COMPATIBLE = "compatible";
|
||||||
String PACKAGE_NAME = "id";
|
String PACKAGE_NAME = "id";
|
||||||
@ -57,7 +62,7 @@ public interface Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String[] ALL = {
|
String[] ALL = {
|
||||||
_ID, IS_COMPATIBLE, PACKAGE_NAME, NAME, SUMMARY, ICON, DESCRIPTION,
|
_ID, ROW_ID, IS_COMPATIBLE, PACKAGE_NAME, NAME, SUMMARY, ICON, DESCRIPTION,
|
||||||
LICENSE, AUTHOR, EMAIL, WEB_URL, TRACKER_URL, SOURCE_URL,
|
LICENSE, AUTHOR, EMAIL, WEB_URL, TRACKER_URL, SOURCE_URL,
|
||||||
CHANGELOG_URL, DONATE_URL, BITCOIN_ADDR, LITECOIN_ADDR, FLATTR_ID,
|
CHANGELOG_URL, DONATE_URL, BITCOIN_ADDR, LITECOIN_ADDR, FLATTR_ID,
|
||||||
UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,
|
UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,
|
||||||
@ -82,6 +87,10 @@ public interface Schema {
|
|||||||
interface Cols extends BaseColumns {
|
interface Cols extends BaseColumns {
|
||||||
String _COUNT_DISTINCT = "countDistinct";
|
String _COUNT_DISTINCT = "countDistinct";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Foreign key to the {@link AppTable}.
|
||||||
|
*/
|
||||||
|
String APP_ID = "appId";
|
||||||
String PACKAGE_NAME = "id";
|
String PACKAGE_NAME = "id";
|
||||||
String VERSION_NAME = "version";
|
String VERSION_NAME = "version";
|
||||||
String REPO_ID = "repo";
|
String REPO_ID = "repo";
|
||||||
@ -101,14 +110,21 @@ public interface Schema {
|
|||||||
String ADDED_DATE = "added";
|
String ADDED_DATE = "added";
|
||||||
String IS_COMPATIBLE = "compatible";
|
String IS_COMPATIBLE = "compatible";
|
||||||
String INCOMPATIBLE_REASONS = "incompatibleReasons";
|
String INCOMPATIBLE_REASONS = "incompatibleReasons";
|
||||||
String REPO_VERSION = "repoVersion";
|
|
||||||
String REPO_ADDRESS = "repoAddress";
|
interface Repo {
|
||||||
|
String VERSION = "repoVersion";
|
||||||
|
String ADDRESS = "repoAddress";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface App {
|
||||||
|
String PACKAGE_NAME = "appPackageName";
|
||||||
|
}
|
||||||
|
|
||||||
String[] ALL = {
|
String[] ALL = {
|
||||||
_ID, PACKAGE_NAME, VERSION_NAME, REPO_ID, HASH, VERSION_CODE, NAME,
|
_ID, APP_ID, PACKAGE_NAME, VERSION_NAME, REPO_ID, HASH, VERSION_CODE, NAME,
|
||||||
SIZE, SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, TARGET_SDK_VERSION, MAX_SDK_VERSION,
|
SIZE, SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, TARGET_SDK_VERSION, MAX_SDK_VERSION,
|
||||||
PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE,
|
PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE,
|
||||||
IS_COMPATIBLE, REPO_VERSION, REPO_ADDRESS, INCOMPATIBLE_REASONS,
|
IS_COMPATIBLE, Repo.VERSION, Repo.ADDRESS, INCOMPATIBLE_REASONS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,11 @@ public class TempApkProvider extends ApkProvider {
|
|||||||
return TABLE_TEMP_APK;
|
return TABLE_TEMP_APK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getAppTableName() {
|
||||||
|
return TempAppProvider.TABLE_TEMP_APP;
|
||||||
|
}
|
||||||
|
|
||||||
public static String getAuthority() {
|
public static String getAuthority() {
|
||||||
return AUTHORITY + "." + PROVIDER_NAME;
|
return AUTHORITY + "." + PROVIDER_NAME;
|
||||||
}
|
}
|
||||||
@ -111,7 +116,7 @@ public class TempApkProvider extends ApkProvider {
|
|||||||
switch (MATCHER.match(uri)) {
|
switch (MATCHER.match(uri)) {
|
||||||
case CODE_REPO_APK:
|
case CODE_REPO_APK:
|
||||||
List<String> pathSegments = uri.getPathSegments();
|
List<String> pathSegments = uri.getPathSegments();
|
||||||
query = query.add(queryRepo(Long.parseLong(pathSegments.get(1)))).add(queryApks(pathSegments.get(2)));
|
query = query.add(queryRepo(Long.parseLong(pathSegments.get(1)), false)).add(queryApks(pathSegments.get(2), false));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -3,9 +3,13 @@ package org.fdroid.fdroid.data;
|
|||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.UriMatcher;
|
import android.content.UriMatcher;
|
||||||
|
import android.database.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteException;
|
import android.database.sqlite.SQLiteException;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.fdroid.fdroid.data.Schema.ApkTable;
|
import org.fdroid.fdroid.data.Schema.ApkTable;
|
||||||
import org.fdroid.fdroid.data.Schema.AppTable;
|
import org.fdroid.fdroid.data.Schema.AppTable;
|
||||||
@ -22,19 +26,21 @@ public class TempAppProvider extends AppProvider {
|
|||||||
|
|
||||||
private static final String PROVIDER_NAME = "TempAppProvider";
|
private static final String PROVIDER_NAME = "TempAppProvider";
|
||||||
|
|
||||||
private static final String TABLE_TEMP_APP = "temp_" + AppTable.NAME;
|
static final String TABLE_TEMP_APP = "temp_" + AppTable.NAME;
|
||||||
|
|
||||||
private static final String PATH_INIT = "init";
|
private static final String PATH_INIT = "init";
|
||||||
private static final String PATH_COMMIT = "commit";
|
private static final String PATH_COMMIT = "commit";
|
||||||
|
|
||||||
private static final int CODE_INIT = 10000;
|
private static final int CODE_INIT = 10000;
|
||||||
private static final int CODE_COMMIT = CODE_INIT + 1;
|
private static final int CODE_COMMIT = CODE_INIT + 1;
|
||||||
|
private static final int APPS = CODE_COMMIT + 1;
|
||||||
|
|
||||||
private static final UriMatcher MATCHER = new UriMatcher(-1);
|
private static final UriMatcher MATCHER = new UriMatcher(-1);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
MATCHER.addURI(getAuthority(), PATH_INIT, CODE_INIT);
|
MATCHER.addURI(getAuthority(), PATH_INIT, CODE_INIT);
|
||||||
MATCHER.addURI(getAuthority(), PATH_COMMIT, CODE_COMMIT);
|
MATCHER.addURI(getAuthority(), PATH_COMMIT, CODE_COMMIT);
|
||||||
|
MATCHER.addURI(getAuthority(), PATH_APPS + "/*", APPS);
|
||||||
MATCHER.addURI(getAuthority(), "*", CODE_SINGLE);
|
MATCHER.addURI(getAuthority(), "*", CODE_SINGLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +61,17 @@ public class TempAppProvider extends AppProvider {
|
|||||||
return Uri.withAppendedPath(getContentUri(), app.packageName);
|
return Uri.withAppendedPath(getContentUri(), app.packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Uri getAppsUri(List<String> apps) {
|
||||||
|
return getContentUri().buildUpon()
|
||||||
|
.appendPath(PATH_APPS)
|
||||||
|
.appendPath(TextUtils.join(",", apps))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppQuerySelection queryApps(String packageNames) {
|
||||||
|
return queryApps(packageNames, getTableName() + ".id");
|
||||||
|
}
|
||||||
|
|
||||||
public static class Helper {
|
public static class Helper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,6 +84,12 @@ public class TempAppProvider extends AppProvider {
|
|||||||
TempApkProvider.Helper.init(context);
|
TempApkProvider.Helper.init(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<App> findByPackageNames(Context context, List<String> packageNames, String[] projection) {
|
||||||
|
Uri uri = getAppsUri(packageNames);
|
||||||
|
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
|
||||||
|
return AppProvider.Helper.cursorToList(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves data from the temp table to the apk table, by removing _EVERYTHING_ from the real
|
* Saves data from the temp table to the apk table, by removing _EVERYTHING_ from the real
|
||||||
* apk table and inserting all of the records from here. The temporary table is then removed.
|
* apk table and inserting all of the records from here. The temporary table is then removed.
|
||||||
@ -116,6 +139,18 @@ public class TempAppProvider extends AppProvider {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
|
||||||
|
AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs);
|
||||||
|
switch (MATCHER.match(uri)) {
|
||||||
|
case APPS:
|
||||||
|
selection = selection.add(queryApps(uri.getLastPathSegment()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.runQuery(uri, selection, projection, true, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
private void ensureTempTableDetached(SQLiteDatabase db) {
|
private void ensureTempTableDetached(SQLiteDatabase db) {
|
||||||
try {
|
try {
|
||||||
db.execSQL("DETACH DATABASE " + DB);
|
db.execSQL("DETACH DATABASE " + DB);
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.fdroid.fdroid.RepoUpdater.UpdateException;
|
import org.fdroid.fdroid.RepoUpdater.UpdateException;
|
||||||
import org.fdroid.fdroid.data.Repo;
|
import org.fdroid.fdroid.data.Repo;
|
||||||
import org.fdroid.fdroid.data.RepoProvider;
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
|
import org.fdroid.fdroid.data.Schema;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.RobolectricGradleTestRunner;
|
import org.robolectric.RobolectricGradleTestRunner;
|
||||||
@ -14,6 +17,7 @@ import org.robolectric.annotation.Config;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
@Config(constants = BuildConfig.class)
|
@Config(constants = BuildConfig.class)
|
||||||
@RunWith(RobolectricGradleTestRunner.class)
|
@RunWith(RobolectricGradleTestRunner.class)
|
||||||
@ -79,4 +83,52 @@ public class AcceptableMultiRepoUpdaterTest extends MultiRepoUpdaterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Repo getMainRepo() {
|
||||||
|
Repo repo = RepoProvider.Helper.findByAddress(context, REPO_MAIN_URI);
|
||||||
|
assertNotNull(repo);
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Repo getArchiveRepo() {
|
||||||
|
Repo repo = RepoProvider.Helper.findByAddress(context, REPO_ARCHIVE_URI);
|
||||||
|
assertNotNull(repo);
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Repo getConflictingRepo() {
|
||||||
|
Repo repo = RepoProvider.Helper.findByAddress(context, REPO_CONFLICTING_URI);
|
||||||
|
assertNotNull(repo);
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOrphanedApps() throws UpdateException {
|
||||||
|
assertEmpty();
|
||||||
|
|
||||||
|
updateArchive();
|
||||||
|
updateMain();
|
||||||
|
updateConflicting();
|
||||||
|
|
||||||
|
assertSomewhatAcceptable();
|
||||||
|
|
||||||
|
disableRepo(getArchiveRepo());
|
||||||
|
disableRepo(getMainRepo());
|
||||||
|
disableRepo(getConflictingRepo());
|
||||||
|
|
||||||
|
RepoProvider.Helper.purgeApps(context, getArchiveRepo());
|
||||||
|
RepoProvider.Helper.purgeApps(context, getMainRepo());
|
||||||
|
RepoProvider.Helper.purgeApps(context, getConflictingRepo());
|
||||||
|
|
||||||
|
assertEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disableRepo(Repo repo) {
|
||||||
|
ContentValues values = new ContentValues(1);
|
||||||
|
values.put(Schema.RepoTable.Cols.IN_USE, 0);
|
||||||
|
RepoProvider.Helper.update(context, repo, values);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import junit.framework.AssertionFailedError;
|
import junit.framework.AssertionFailedError;
|
||||||
|
|
||||||
import org.fdroid.fdroid.data.ApkProvider;
|
import org.fdroid.fdroid.data.ApkProvider;
|
||||||
|
import org.fdroid.fdroid.data.App;
|
||||||
import org.fdroid.fdroid.data.AppProvider;
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||||
import org.fdroid.fdroid.data.Schema.ApkTable;
|
import org.fdroid.fdroid.data.Schema.ApkTable;
|
||||||
@ -174,14 +176,14 @@ public class Assert {
|
|||||||
cursor.close();
|
cursor.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void insertApp(ShadowContentResolver resolver, String appId, String name) {
|
public static App insertApp(Context context, String packageName, String name) {
|
||||||
insertApp(resolver, appId, name, new ContentValues());
|
return insertApp(context, packageName, name, new ContentValues());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void insertApp(ShadowContentResolver resolver, String id, String name, ContentValues additionalValues) {
|
public static App insertApp(Context context, String packageName, String name, ContentValues additionalValues) {
|
||||||
|
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(AppTable.Cols.PACKAGE_NAME, id);
|
values.put(AppTable.Cols.PACKAGE_NAME, packageName);
|
||||||
values.put(AppTable.Cols.NAME, name);
|
values.put(AppTable.Cols.NAME, name);
|
||||||
|
|
||||||
// Required fields (NOT NULL in the database).
|
// Required fields (NOT NULL in the database).
|
||||||
@ -196,18 +198,38 @@ public class Assert {
|
|||||||
|
|
||||||
Uri uri = AppProvider.getContentUri();
|
Uri uri = AppProvider.getContentUri();
|
||||||
|
|
||||||
resolver.insert(uri, values);
|
context.getContentResolver().insert(uri, values);
|
||||||
|
return AppProvider.Helper.findByPackageName(context.getContentResolver(), packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri insertApk(ShadowContentResolver resolver, String id, int versionCode) {
|
private static App ensureApp(Context context, String packageName) {
|
||||||
return insertApk(resolver, id, versionCode, new ContentValues());
|
App app = AppProvider.Helper.findByPackageName(context.getContentResolver(), packageName);
|
||||||
|
if (app == null) {
|
||||||
|
insertApp(context, packageName, packageName);
|
||||||
|
app = AppProvider.Helper.findByPackageName(context.getContentResolver(), packageName);
|
||||||
|
}
|
||||||
|
assertNotNull(app);
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri insertApk(ShadowContentResolver resolver, String id, int versionCode, ContentValues additionalValues) {
|
public static Uri insertApk(Context context, String packageName, int versionCode) {
|
||||||
|
return insertApk(context, ensureApp(context, packageName), versionCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri insertApk(Context context, String packageName, int versionCode, ContentValues additionalValues) {
|
||||||
|
return insertApk(context, ensureApp(context, packageName), versionCode, additionalValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri insertApk(Context context, App app, int versionCode) {
|
||||||
|
return insertApk(context, app, versionCode, new ContentValues());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri insertApk(Context context, App app, int versionCode, ContentValues additionalValues) {
|
||||||
|
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
|
|
||||||
values.put(ApkTable.Cols.PACKAGE_NAME, id);
|
values.put(ApkTable.Cols.APP_ID, app.getId());
|
||||||
|
values.put(ApkTable.Cols.PACKAGE_NAME, app.packageName);
|
||||||
values.put(ApkTable.Cols.VERSION_CODE, versionCode);
|
values.put(ApkTable.Cols.VERSION_CODE, versionCode);
|
||||||
|
|
||||||
// Required fields (NOT NULL in the database).
|
// Required fields (NOT NULL in the database).
|
||||||
@ -222,7 +244,7 @@ public class Assert {
|
|||||||
|
|
||||||
Uri uri = ApkProvider.getContentUri();
|
Uri uri = ApkProvider.getContentUri();
|
||||||
|
|
||||||
return resolver.insert(uri, values);
|
return context.getContentResolver().insert(uri, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ import org.junit.Before;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
@ -33,9 +32,9 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest {
|
|||||||
protected static final String REPO_ARCHIVE = "Test F-Droid repo (Archive)";
|
protected static final String REPO_ARCHIVE = "Test F-Droid repo (Archive)";
|
||||||
protected static final String REPO_CONFLICTING = "Test F-Droid repo with different apps";
|
protected static final String REPO_CONFLICTING = "Test F-Droid repo with different apps";
|
||||||
|
|
||||||
protected RepoUpdater conflictingRepoUpdater;
|
protected static final String REPO_MAIN_URI = "https://f-droid.org/repo";
|
||||||
protected RepoUpdater mainRepoUpdater;
|
protected static final String REPO_ARCHIVE_URI = "https://f-droid.org/archive";
|
||||||
protected RepoUpdater archiveRepoUpdater;
|
protected static final String REPO_CONFLICTING_URI = "https://example.com/conflicting/fdroid/repo";
|
||||||
|
|
||||||
private static final String PUB_KEY =
|
private static final String PUB_KEY =
|
||||||
"3082050b308202f3a003020102020420d8f212300d06092a864886f70d01010b050030363110300e0603" +
|
"3082050b308202f3a003020102020420d8f212300d06092a864886f70d01010b050030363110300e0603" +
|
||||||
@ -79,10 +78,6 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest {
|
|||||||
RepoProvider.Helper.remove(context, 3);
|
RepoProvider.Helper.remove(context, 3);
|
||||||
RepoProvider.Helper.remove(context, 4);
|
RepoProvider.Helper.remove(context, 4);
|
||||||
|
|
||||||
conflictingRepoUpdater = createUpdater(REPO_CONFLICTING, context);
|
|
||||||
mainRepoUpdater = createUpdater(REPO_MAIN, context);
|
|
||||||
archiveRepoUpdater = createUpdater(REPO_ARCHIVE, context);
|
|
||||||
|
|
||||||
Preferences.setup(context);
|
Preferences.setup(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,10 +152,10 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private RepoUpdater createUpdater(String name, Context context) {
|
private RepoUpdater createUpdater(String name, String uri, Context context) {
|
||||||
Repo repo = new Repo();
|
Repo repo = new Repo();
|
||||||
repo.signingCertificate = PUB_KEY;
|
repo.signingCertificate = PUB_KEY;
|
||||||
repo.address = "https://fake.url/" + UUID.randomUUID().toString() + "/fdroid/repo";
|
repo.address = uri;
|
||||||
repo.name = name;
|
repo.name = name;
|
||||||
|
|
||||||
ContentValues values = new ContentValues(2);
|
ContentValues values = new ContentValues(2);
|
||||||
@ -176,15 +171,15 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected boolean updateConflicting() throws UpdateException {
|
protected boolean updateConflicting() throws UpdateException {
|
||||||
return updateRepo(conflictingRepoUpdater, "multiRepo.conflicting.jar");
|
return updateRepo(createUpdater(REPO_CONFLICTING, REPO_CONFLICTING_URI, context), "multiRepo.conflicting.jar");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean updateMain() throws UpdateException {
|
protected boolean updateMain() throws UpdateException {
|
||||||
return updateRepo(mainRepoUpdater, "multiRepo.normal.jar");
|
return updateRepo(createUpdater(REPO_MAIN, REPO_MAIN_URI, context), "multiRepo.normal.jar");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean updateArchive() throws UpdateException {
|
protected boolean updateArchive() throws UpdateException {
|
||||||
return updateRepo(archiveRepoUpdater, "multiRepo.archive.jar");
|
return updateRepo(createUpdater(REPO_ARCHIVE, REPO_ARCHIVE_URI, context), "multiRepo.archive.jar");
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean updateRepo(RepoUpdater updater, String indexJarPath) throws UpdateException {
|
private boolean updateRepo(RepoUpdater updater, String indexJarPath) throws UpdateException {
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContextWrapper;
|
||||||
|
|
||||||
|
import org.mockito.AdditionalAnswers;
|
||||||
|
import org.robolectric.RuntimeEnvironment;
|
||||||
|
import org.robolectric.shadows.ShadowContentResolver;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -8,6 +15,7 @@ import java.io.OutputStream;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
public class TestUtils {
|
public class TestUtils {
|
||||||
|
|
||||||
@ -37,4 +45,21 @@ public class TestUtils {
|
|||||||
return tempFile;
|
return tempFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The way that Robolectric has to implement shadows for Android classes such as {@link android.content.ContentProvider}
|
||||||
|
* is by using a special annotation that means the classes will implement the correct methods at runtime.
|
||||||
|
* However this means that the shadow of a content provider does not actually extend
|
||||||
|
* {@link android.content.ContentProvider}. As such, we need to do some special mocking using
|
||||||
|
* Mockito in order to provide a {@link ContextWrapper} which is able to return a proper
|
||||||
|
* content resolver that delegates to the Robolectric shadow object.
|
||||||
|
*/
|
||||||
|
public static ContextWrapper createContextWithContentResolver(ShadowContentResolver contentResolver) {
|
||||||
|
final ContentResolver resolver = mock(ContentResolver.class, AdditionalAnswers.delegatesTo(contentResolver));
|
||||||
|
return new ContextWrapper(RuntimeEnvironment.application.getApplicationContext()) {
|
||||||
|
@Override
|
||||||
|
public ContentResolver getContentResolver() {
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import java.util.List;
|
|||||||
import static org.fdroid.fdroid.Assert.assertCantDelete;
|
import static org.fdroid.fdroid.Assert.assertCantDelete;
|
||||||
import static org.fdroid.fdroid.Assert.assertContainsOnly;
|
import static org.fdroid.fdroid.Assert.assertContainsOnly;
|
||||||
import static org.fdroid.fdroid.Assert.assertResultCount;
|
import static org.fdroid.fdroid.Assert.assertResultCount;
|
||||||
|
import static org.fdroid.fdroid.Assert.insertApp;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
@ -38,9 +39,11 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAppApks() {
|
public void testAppApks() {
|
||||||
|
App fdroidApp = insertApp(context, "org.fdroid.fdroid", "F-Droid");
|
||||||
|
App exampleApp = insertApp(context, "com.example", "Example");
|
||||||
for (int i = 1; i <= 10; i++) {
|
for (int i = 1; i <= 10; i++) {
|
||||||
Assert.insertApk(contentResolver, "org.fdroid.fdroid", i);
|
Assert.insertApk(context, fdroidApp, i);
|
||||||
Assert.insertApk(contentResolver, "com.example", i);
|
Assert.insertApk(context, exampleApp, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertTotalApkCount(20);
|
assertTotalApkCount(20);
|
||||||
@ -188,7 +191,7 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
Apk apk = new MockApk("org.fdroid.fdroid", 13);
|
Apk apk = new MockApk("org.fdroid.fdroid", 13);
|
||||||
|
|
||||||
// Insert a new record...
|
// Insert a new record...
|
||||||
Uri newUri = Assert.insertApk(contentResolver, apk.packageName, apk.versionCode);
|
Uri newUri = Assert.insertApk(context, apk.packageName, apk.versionCode);
|
||||||
assertEquals(ApkProvider.getContentUri(apk).toString(), newUri.toString());
|
assertEquals(ApkProvider.getContentUri(apk).toString(), newUri.toString());
|
||||||
cursor = queryAllApks();
|
cursor = queryAllApks();
|
||||||
assertNotNull(cursor);
|
assertNotNull(cursor);
|
||||||
@ -206,7 +209,7 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class)
|
@Test(expected = IllegalArgumentException.class)
|
||||||
public void testCursorMustMoveToFirst() {
|
public void testCursorMustMoveToFirst() {
|
||||||
Assert.insertApk(contentResolver, "org.example.test", 12);
|
Assert.insertApk(context, "org.example.test", 12);
|
||||||
Cursor cursor = queryAllApks();
|
Cursor cursor = queryAllApks();
|
||||||
new Apk(cursor);
|
new Apk(cursor);
|
||||||
}
|
}
|
||||||
@ -216,7 +219,7 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
String[] projectionCount = new String[] {Cols._COUNT};
|
String[] projectionCount = new String[] {Cols._COUNT};
|
||||||
|
|
||||||
for (int i = 0; i < 13; i++) {
|
for (int i = 0; i < 13; i++) {
|
||||||
Assert.insertApk(contentResolver, "com.example", i);
|
Assert.insertApk(context, "com.example", i);
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri all = ApkProvider.getContentUri();
|
Uri all = ApkProvider.getContentUri();
|
||||||
@ -261,7 +264,7 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
public void assertInvalidExtraField(String field) {
|
public void assertInvalidExtraField(String field) {
|
||||||
ContentValues invalidRepo = new ContentValues();
|
ContentValues invalidRepo = new ContentValues();
|
||||||
invalidRepo.put(field, "Test data");
|
invalidRepo.put(field, "Test data");
|
||||||
Assert.insertApk(contentResolver, "org.fdroid.fdroid", 10, invalidRepo);
|
Assert.insertApk(context, "org.fdroid.fdroid", 10, invalidRepo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -271,10 +274,10 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
|
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(Cols.REPO_ID, 10);
|
values.put(Cols.REPO_ID, 10);
|
||||||
values.put(Cols.REPO_ADDRESS, "http://example.com");
|
values.put(Cols.Repo.ADDRESS, "http://example.com");
|
||||||
values.put(Cols.REPO_VERSION, 3);
|
values.put(Cols.Repo.VERSION, 3);
|
||||||
values.put(Cols.FEATURES, "Some features");
|
values.put(Cols.FEATURES, "Some features");
|
||||||
Uri uri = Assert.insertApk(contentResolver, "com.example.com", 1, values);
|
Uri uri = Assert.insertApk(context, "com.example.com", 1, values);
|
||||||
|
|
||||||
assertResultCount(1, queryAllApks());
|
assertResultCount(1, queryAllApks());
|
||||||
|
|
||||||
@ -301,18 +304,18 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
public void testKnownApks() {
|
public void testKnownApks() {
|
||||||
|
|
||||||
for (int i = 0; i < 7; i++) {
|
for (int i = 0; i < 7; i++) {
|
||||||
Assert.insertApk(contentResolver, "org.fdroid.fdroid", i);
|
Assert.insertApk(context, "org.fdroid.fdroid", i);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < 9; i++) {
|
for (int i = 0; i < 9; i++) {
|
||||||
Assert.insertApk(contentResolver, "org.example", i);
|
Assert.insertApk(context, "org.example", i);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
Assert.insertApk(contentResolver, "com.example", i);
|
Assert.insertApk(context, "com.example", i);
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.insertApk(contentResolver, "com.apk.thingo", 1);
|
Assert.insertApk(context, "com.apk.thingo", 1);
|
||||||
|
|
||||||
Apk[] known = {
|
Apk[] known = {
|
||||||
new MockApk("org.fdroid.fdroid", 1),
|
new MockApk("org.fdroid.fdroid", 1),
|
||||||
@ -359,18 +362,18 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
public void testFindByApp() {
|
public void testFindByApp() {
|
||||||
|
|
||||||
for (int i = 0; i < 7; i++) {
|
for (int i = 0; i < 7; i++) {
|
||||||
Assert.insertApk(contentResolver, "org.fdroid.fdroid", i);
|
Assert.insertApk(context, "org.fdroid.fdroid", i);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < 9; i++) {
|
for (int i = 0; i < 9; i++) {
|
||||||
Assert.insertApk(contentResolver, "org.example", i);
|
Assert.insertApk(context, "org.example", i);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
Assert.insertApk(contentResolver, "com.example", i);
|
Assert.insertApk(context, "com.example", i);
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.insertApk(contentResolver, "com.apk.thingo", 1);
|
Assert.insertApk(context, "com.apk.thingo", 1);
|
||||||
|
|
||||||
assertTotalApkCount(7 + 9 + 3 + 1);
|
assertTotalApkCount(7 + 9 + 3 + 1);
|
||||||
|
|
||||||
@ -394,7 +397,7 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testUpdate() {
|
public void testUpdate() {
|
||||||
|
|
||||||
Uri apkUri = Assert.insertApk(contentResolver, "com.example", 10);
|
Uri apkUri = Assert.insertApk(context, "com.example", 10);
|
||||||
|
|
||||||
String[] allFields = Cols.ALL;
|
String[] allFields = Cols.ALL;
|
||||||
Cursor cursor = contentResolver.query(apkUri, allFields, null, null, null);
|
Cursor cursor = contentResolver.query(apkUri, allFields, null, null, null);
|
||||||
@ -453,18 +456,20 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
// the Helper.find() method doesn't stumble upon the app we are interested
|
// the Helper.find() method doesn't stumble upon the app we are interested
|
||||||
// in by shear dumb luck...
|
// in by shear dumb luck...
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
Assert.insertApk(contentResolver, "org.fdroid.apk." + i, i);
|
Assert.insertApk(context, "org.fdroid.apk." + i, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
App exampleApp = insertApp(context, "com.example", "Example");
|
||||||
|
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(Cols.VERSION_NAME, "v1.1");
|
values.put(Cols.VERSION_NAME, "v1.1");
|
||||||
values.put(Cols.HASH, "xxxxyyyy");
|
values.put(Cols.HASH, "xxxxyyyy");
|
||||||
values.put(Cols.HASH_TYPE, "a hash type");
|
values.put(Cols.HASH_TYPE, "a hash type");
|
||||||
Assert.insertApk(contentResolver, "com.example", 11, values);
|
Assert.insertApk(context, exampleApp, 11, values);
|
||||||
|
|
||||||
// ...and a few more for good measure...
|
// ...and a few more for good measure...
|
||||||
for (int i = 15; i < 20; i++) {
|
for (int i = 15; i < 20; i++) {
|
||||||
Assert.insertApk(contentResolver, "com.other.thing." + i, i);
|
Assert.insertApk(context, "com.other.thing." + i, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
Apk apk = ApkProvider.Helper.find(context, "com.example", 11);
|
Apk apk = ApkProvider.Helper.find(context, "com.example", 11);
|
||||||
@ -540,7 +545,7 @@ public class ApkProviderTest extends FDroidProviderTest {
|
|||||||
protected Apk insertApkForRepo(String id, int versionCode, long repoId) {
|
protected Apk insertApkForRepo(String id, int versionCode, long repoId) {
|
||||||
ContentValues additionalValues = new ContentValues();
|
ContentValues additionalValues = new ContentValues();
|
||||||
additionalValues.put(Cols.REPO_ID, repoId);
|
additionalValues.put(Cols.REPO_ID, repoId);
|
||||||
Uri uri = Assert.insertApk(contentResolver, id, versionCode, additionalValues);
|
Uri uri = Assert.insertApk(context, id, versionCode, additionalValues);
|
||||||
return ApkProvider.Helper.get(context, uri);
|
return ApkProvider.Helper.get(context, uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
package org.fdroid.fdroid.data;
|
package org.fdroid.fdroid.data;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.ContextWrapper;
|
import android.content.ContextWrapper;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.TestUtils;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.mockito.AdditionalAnswers;
|
|
||||||
import org.robolectric.RuntimeEnvironment;
|
import org.robolectric.RuntimeEnvironment;
|
||||||
import org.robolectric.Shadows;
|
import org.robolectric.Shadows;
|
||||||
import org.robolectric.shadows.ShadowContentResolver;
|
import org.robolectric.shadows.ShadowContentResolver;
|
||||||
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
|
|
||||||
public abstract class FDroidProviderTest {
|
public abstract class FDroidProviderTest {
|
||||||
|
|
||||||
protected ShadowContentResolver contentResolver;
|
protected ShadowContentResolver contentResolver;
|
||||||
@ -20,13 +17,7 @@ public abstract class FDroidProviderTest {
|
|||||||
@Before
|
@Before
|
||||||
public final void setupBase() {
|
public final void setupBase() {
|
||||||
contentResolver = Shadows.shadowOf(RuntimeEnvironment.application.getContentResolver());
|
contentResolver = Shadows.shadowOf(RuntimeEnvironment.application.getContentResolver());
|
||||||
final ContentResolver resolver = mock(ContentResolver.class, AdditionalAnswers.delegatesTo(contentResolver));
|
context = TestUtils.createContextWithContentResolver(contentResolver);
|
||||||
context = new ContextWrapper(RuntimeEnvironment.application.getApplicationContext()) {
|
|
||||||
@Override
|
|
||||||
public ContentResolver getContentResolver() {
|
|
||||||
return resolver;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ShadowContentResolver.registerProvider(AppProvider.getAuthority(), new AppProvider());
|
ShadowContentResolver.registerProvider(AppProvider.getAuthority(), new AppProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.fdroid.fdroid.data;
|
package org.fdroid.fdroid.data;
|
||||||
|
|
||||||
import org.fdroid.fdroid.BuildConfig;
|
import org.fdroid.fdroid.BuildConfig;
|
||||||
|
import org.fdroid.fdroid.TestUtils;
|
||||||
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
|
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
|
||||||
import org.fdroid.fdroid.mock.MockApk;
|
import org.fdroid.fdroid.mock.MockApk;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
@ -93,14 +94,27 @@ public class ProviderUriTests {
|
|||||||
App app = new App();
|
App app = new App();
|
||||||
app.packageName = "org.fdroid.fdroid";
|
app.packageName = "org.fdroid.fdroid";
|
||||||
|
|
||||||
List<App> apps = new ArrayList<>(1);
|
|
||||||
apps.add(app);
|
|
||||||
|
|
||||||
assertValidUri(resolver, AppProvider.getContentUri(app), "content://org.fdroid.fdroid.data.AppProvider/org.fdroid.fdroid", projection);
|
assertValidUri(resolver, AppProvider.getContentUri(app), "content://org.fdroid.fdroid.data.AppProvider/org.fdroid.fdroid", projection);
|
||||||
assertValidUri(resolver, AppProvider.getContentUri(apps), "content://org.fdroid.fdroid.data.AppProvider/apps/org.fdroid.fdroid", projection);
|
|
||||||
assertValidUri(resolver, AppProvider.getContentUri("org.fdroid.fdroid"), "content://org.fdroid.fdroid.data.AppProvider/org.fdroid.fdroid", projection);
|
assertValidUri(resolver, AppProvider.getContentUri("org.fdroid.fdroid"), "content://org.fdroid.fdroid.data.AppProvider/org.fdroid.fdroid", projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validTempAppProviderUris() {
|
||||||
|
ShadowContentResolver.registerProvider(TempAppProvider.getAuthority(), new TempAppProvider());
|
||||||
|
String[] projection = new String[]{Schema.AppTable.Cols._ID};
|
||||||
|
|
||||||
|
// Required so that the `assertValidUri` calls below will indeed have a real temp_fdroid_app
|
||||||
|
// table to query.
|
||||||
|
TempAppProvider.Helper.init(TestUtils.createContextWithContentResolver(resolver));
|
||||||
|
|
||||||
|
List<String> packageNames = new ArrayList<>(2);
|
||||||
|
packageNames.add("org.fdroid.fdroid");
|
||||||
|
packageNames.add("com.example.com");
|
||||||
|
|
||||||
|
assertValidUri(resolver, TempAppProvider.getAppsUri(packageNames), "content://org.fdroid.fdroid.data.TempAppProvider/apps/org.fdroid.fdroid%2Ccom.example.com", projection);
|
||||||
|
assertValidUri(resolver, TempAppProvider.getContentUri(), "content://org.fdroid.fdroid.data.TempAppProvider", projection);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invalidApkProviderUris() {
|
public void invalidApkProviderUris() {
|
||||||
ShadowContentResolver.registerProvider(ApkProvider.getAuthority(), new ApkProvider());
|
ShadowContentResolver.registerProvider(ApkProvider.getAuthority(), new ApkProvider());
|
||||||
|
Loading…
x
Reference in New Issue
Block a user