Merge branch 'fix-511--database-package-table' into 'master'
Properly support multiple repositories with the same app Fixes #511. Provides proper support for multiple repos in the database + updater (despite not being able to reorder them in the UI yet). # Info Previously, each app had only one row in the app table. This caused problems when its metadata was updated, because it would get overriden if two repositories provided the same app. This change: * Creates a new `package` table which acts as the table that includes one row for each app (like the `app` table did previously) * Re-purposes the existing `app` table as an `app_metadata` table, where each package can have multiple rows (one for each repository that provides that package) This results in some queries which are slightly more complex, but the overall performance should not change too much. In the long term, it should have a net positive impact on performance, because we no longer need to join between tables based on a package name. Now we almost exclusively use integer IDs to join between tables. There are also appropriate indexes which make the queries avoid full table scans in all cases I'm aware of. I realise this is a big MR, but it is as small as I could make it without submitting half finished stuff which breaks `master`. I've done my best to merge smaller MRs before hand, but I was unable to identify anything else to pull out of this MR as a separate thing. ## Migration All app/apk metadata is dropped, and then the repos are asked to update themselves again next time F-Droid starts. Additionally, existing repositories have their `priority` changed to something more meaningful. On current master, if you add two custom repositories in addition to the four default ones, you will get the following: |rowid|address|priority| |---------|-------|-------| |1|https://f-droid.org/repo|10| |2|https://f-droid.org/archive|20| |3|https://guardianproject.info/fdroid/repo|10| |4|https://guardianproject.info/fdroid/archive|20| |5|http://10.0.0.6:8888/normal-repo/repo|10| |6|http://10.0.0.6:8888/conflicting-repo/repo|10| Note how the priority defaults to 10 for each additional repository which is added. This MR should change the priorities so they look like so: |rowid|address|priority| |-------|-------|--------| |1|https://f-droid.org/repo|1| |2|https://f-droid.org/archive|2| |3|https://guardianproject.info/fdroid/repo|3| |4|https://guardianproject.info/fdroid/archive|4| |5|http://10.0.0.6:8888/normal-repo/repo|5| |6|http://10.0.0.6:8888/conflicting-repo/repo|6| Here is a snipped from the logcat from `DBHelper` during the migration: ``` Setting priority of repo https://f-droid.org/repo to 1 Setting priority of repo https://f-droid.org/archive to 2 Setting priority of repo https://guardianproject.info/fdroid/repo to 3 Setting priority of repo https://guardianproject.info/fdroid/archive to 4 Setting priority of repo http://10.0.0.6:8888/normal-repo/repo to 5 Setting priority of repo http://10.0.0.6:8888/conflicting-repo/repo to 6 ``` All newly added repositories on this branch will get the next highest available priority. # Future work One thing which is not yet supported fully: Suggested version code. Two repositories can end up with the same exact .apk file. If that .apk is the "suggested version", then we should eliminate the idea of "suggested version code" and instead have a "suggested apk" (which implicitly includes the repository it comes from, so we choose the one with the better priority). Right now, we kind of assume that it doesn't matter which repo provides the suggested apk, as long as one of them has an .apk with th ecorect version code and signing key. I guess it doesn't _particularly_ matter from a security perspective, because a malicious repo wont be able to trick a user into installing an apk with a different signing key, but it would be good to iron this out. See merge request !375
This commit is contained in:
commit
6eeaf8662a
@ -100,6 +100,11 @@
|
||||
android:name="org.fdroid.fdroid.data.AppPrefsProvider"
|
||||
android:exported="false"/>
|
||||
|
||||
<provider
|
||||
android:authorities="org.fdroid.fdroid.data.PackageProvider"
|
||||
android:name="org.fdroid.fdroid.data.PackageProvider"
|
||||
android:exported="false"/>
|
||||
|
||||
<provider
|
||||
android:name="org.fdroid.fdroid.installer.ApkFileProvider"
|
||||
android:authorities="org.fdroid.fdroid.installer.ApkFileProvider"
|
||||
|
@ -425,7 +425,7 @@ public class AppDetails extends AppCompatActivity {
|
||||
// register observer to know when install status changes
|
||||
myAppObserver = new AppObserver(new Handler());
|
||||
getContentResolver().registerContentObserver(
|
||||
AppProvider.getContentUri(app.packageName),
|
||||
AppProvider.getHighestPriorityMetadataUri(app.packageName),
|
||||
true,
|
||||
myAppObserver);
|
||||
}
|
||||
@ -667,7 +667,7 @@ public class AppDetails extends AppCompatActivity {
|
||||
calcActiveDownloadUrlString(packageName);
|
||||
|
||||
if (!TextUtils.isEmpty(packageName)) {
|
||||
newApp = AppProvider.Helper.findByPackageName(getContentResolver(), packageName);
|
||||
newApp = AppProvider.Helper.findHighestPriorityMetadata(getContentResolver(), packageName);
|
||||
}
|
||||
|
||||
setApp(newApp);
|
||||
|
@ -458,7 +458,12 @@ public class RepoUpdater {
|
||||
}
|
||||
if (RepoPushRequest.INSTALL.equals(repoPushRequest.request)) {
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
App app = AppProvider.Helper.findByPackageName(cr, packageName);
|
||||
|
||||
// TODO: In the future, this needs to be able to specify which repository to get
|
||||
// the package from. Better yet, we should be able to specify the hash of a package
|
||||
// to install (especially when we move to using hashes more as identifiers than we
|
||||
// do righ tnow).
|
||||
App app = AppProvider.Helper.findHighestPriorityMetadata(cr, packageName);
|
||||
if (app == null) {
|
||||
Utils.debugLog(TAG, packageName + " not in local database, ignoring request to"
|
||||
+ repoPushRequest.request);
|
||||
|
@ -61,7 +61,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
|
||||
private final StringBuilder curchars = new StringBuilder();
|
||||
|
||||
interface IndexReceiver {
|
||||
public interface IndexReceiver {
|
||||
void receiveRepo(String name, String description, String signingCert, int maxage, int version, long timestamp);
|
||||
|
||||
void receiveApp(App app, List<Apk> packages);
|
||||
@ -280,6 +280,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
}
|
||||
} else if ("application".equals(localName) && curapp == null) {
|
||||
curapp = new App();
|
||||
curapp.repoId = repo.getId();
|
||||
curapp.packageName = attributes.getValue("", "id");
|
||||
|
||||
// To appease the NON NULL constraint in the DB. Usually there is a description, and it
|
||||
|
@ -63,7 +63,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
|
||||
public String[] incompatibleReasons;
|
||||
|
||||
/**
|
||||
* The numeric primary key of the App table, which is used to join apks.
|
||||
* The numeric primary key of the Metadata table, which is used to join apks.
|
||||
*/
|
||||
public long appId;
|
||||
|
||||
@ -91,7 +91,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
|
||||
case Cols.FEATURES:
|
||||
features = Utils.parseCommaSeparatedString(cursor.getString(i));
|
||||
break;
|
||||
case Cols.App.PACKAGE_NAME:
|
||||
case Cols.Package.PACKAGE_NAME:
|
||||
packageName = cursor.getString(i);
|
||||
break;
|
||||
case Cols.IS_COMPATIBLE:
|
||||
|
@ -11,6 +11,7 @@ import android.util.Log;
|
||||
import org.fdroid.fdroid.data.Schema.ApkTable;
|
||||
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
||||
import org.fdroid.fdroid.data.Schema.PackageTable;
|
||||
import org.fdroid.fdroid.data.Schema.RepoTable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -36,10 +37,19 @@ public class ApkProvider extends FDroidProvider {
|
||||
|
||||
public static void update(Context context, Apk apk) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri uri = getApkFromAnyRepoUri(apk.packageName, apk.versionCode);
|
||||
Uri uri = getApkFromRepoUri(apk);
|
||||
resolver.update(uri, apk.toContentValues(), null, null);
|
||||
}
|
||||
|
||||
public static Uri getApkFromRepoUri(Apk apk) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_APK_FROM_REPO)
|
||||
.appendPath(Long.toString(apk.appId))
|
||||
.appendPath(Integer.toString(apk.versionCode))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static List<Apk> cursorToList(Cursor cursor) {
|
||||
int knownApkCount = cursor != null ? cursor.getCount() : 0;
|
||||
List<Apk> apks = new ArrayList<>(knownApkCount);
|
||||
@ -70,7 +80,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
* Find all apks for a particular app, but limit it to those originating from the
|
||||
* specified repo.
|
||||
*/
|
||||
public static List<Apk> find(Context context, Repo repo, List<App> apps, String[] projection) {
|
||||
public static List<Apk> findByUri(Context context, Repo repo, List<App> apps, String[] projection) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
final Uri uri = getContentUriForApps(repo, apps);
|
||||
Cursor cursor = resolver.query(uri, projection, null, null, null);
|
||||
@ -171,9 +181,12 @@ public class ApkProvider extends FDroidProvider {
|
||||
private static final int CODE_REPO_APPS = CODE_APKS + 1;
|
||||
protected static final int CODE_REPO_APK = CODE_REPO_APPS + 1;
|
||||
private static final int CODE_APK_ROW_ID = CODE_REPO_APK + 1;
|
||||
static final int CODE_APK_FROM_ANY_REPO = CODE_APK_ROW_ID + 1;
|
||||
static final int CODE_APK_FROM_REPO = CODE_APK_FROM_ANY_REPO + 1;
|
||||
|
||||
private static final String PROVIDER_NAME = "ApkProvider";
|
||||
protected static final String PATH_APK = "apk";
|
||||
protected static final String PATH_APK_FROM_ANY_REPO = "apk-any-repo";
|
||||
protected static final String PATH_APK_FROM_REPO = "apk-from-repo";
|
||||
private static final String PATH_APKS = "apks";
|
||||
private static final String PATH_APP = "app";
|
||||
private static final String PATH_REPO = "repo";
|
||||
@ -189,10 +202,11 @@ public class ApkProvider extends FDroidProvider {
|
||||
static {
|
||||
REPO_FIELDS.put(Cols.Repo.VERSION, RepoTable.Cols.VERSION);
|
||||
REPO_FIELDS.put(Cols.Repo.ADDRESS, RepoTable.Cols.ADDRESS);
|
||||
PACKAGE_FIELDS.put(Cols.App.PACKAGE_NAME, AppMetadataTable.Cols.PACKAGE_NAME);
|
||||
PACKAGE_FIELDS.put(Cols.Package.PACKAGE_NAME, PackageTable.Cols.PACKAGE_NAME);
|
||||
|
||||
MATCHER.addURI(getAuthority(), PATH_REPO + "/#", CODE_REPO);
|
||||
MATCHER.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE);
|
||||
MATCHER.addURI(getAuthority(), PATH_APK_FROM_ANY_REPO + "/#/*", CODE_APK_FROM_ANY_REPO);
|
||||
MATCHER.addURI(getAuthority(), PATH_APK_FROM_REPO + "/#/#", CODE_APK_FROM_REPO);
|
||||
MATCHER.addURI(getAuthority(), PATH_APKS + "/*", CODE_APKS);
|
||||
MATCHER.addURI(getAuthority(), PATH_APP + "/*", CODE_PACKAGE);
|
||||
MATCHER.addURI(getAuthority(), PATH_REPO_APPS + "/#/*", CODE_REPO_APPS);
|
||||
@ -239,7 +253,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
public static Uri getApkFromAnyRepoUri(String packageName, int versionCode) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_APK)
|
||||
.appendPath(PATH_APK_FROM_ANY_REPO)
|
||||
.appendPath(Integer.toString(versionCode))
|
||||
.appendPath(packageName)
|
||||
.build();
|
||||
@ -274,7 +288,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
builder.append(',');
|
||||
}
|
||||
final Apk apk = apks.get(i);
|
||||
builder.append(apk.packageName).append(':').append(apk.versionCode);
|
||||
builder.append(apk.appId).append(':').append(apk.versionCode);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
@ -317,9 +331,11 @@ public class ApkProvider extends FDroidProvider {
|
||||
protected String getRequiredTables() {
|
||||
final String apk = getTableName();
|
||||
final String app = getAppTableName();
|
||||
final String pkg = PackageTable.NAME;
|
||||
|
||||
return apk + " AS apk " +
|
||||
" LEFT JOIN " + app + " AS app ON (app." + AppMetadataTable.Cols.ROW_ID + " = apk." + Cols.APP_ID + ")";
|
||||
" LEFT JOIN " + app + " AS app ON (app." + AppMetadataTable.Cols.ROW_ID + " = apk." + Cols.APP_ID + ")" +
|
||||
" LEFT JOIN " + pkg + " AS pkg ON (pkg." + PackageTable.Cols.ROW_ID + " = app." + AppMetadataTable.Cols.PACKAGE_ID + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -340,7 +356,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
}
|
||||
|
||||
private void addPackageField(String field, String alias) {
|
||||
appendField(field, "app", alias);
|
||||
appendField(field, "pkg", alias);
|
||||
}
|
||||
|
||||
private void addRepoField(String field, String alias) {
|
||||
@ -354,12 +370,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
}
|
||||
|
||||
private QuerySelection queryPackage(String packageName) {
|
||||
return queryPackage(packageName, true);
|
||||
}
|
||||
|
||||
private QuerySelection queryPackage(String packageName, boolean includeTableAlias) {
|
||||
String alias = includeTableAlias ? "apk." : "";
|
||||
final String selection = alias + Cols.APP_ID + " = (" + getAppIdFromPackageNameQuery() + ")";
|
||||
final String selection = "pkg." + PackageTable.Cols.PACKAGE_NAME + " = ?";
|
||||
final String[] args = {packageName};
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
@ -370,7 +381,15 @@ public class ApkProvider extends FDroidProvider {
|
||||
|
||||
private QuerySelection querySingleFromAnyRepo(Uri uri, boolean includeAlias) {
|
||||
String alias = includeAlias ? "apk." : "";
|
||||
final String selection = alias + Cols.VERSION_CODE + " = ? and " + alias + Cols.APP_ID + " = (" + getAppIdFromPackageNameQuery() + ")";
|
||||
|
||||
// TODO: Technically multiple repositories can provide the apk with this version code.
|
||||
// Therefore, in the very near future we'll need to change from calculating a
|
||||
// "suggested version code" to a "suggested apk" and join directly onto the apk table.
|
||||
// This way, we can take into account both repo priorities and signing keys of any
|
||||
// already installed apks to ensure that the best version is suggested to the user.
|
||||
// At this point, we may pull back the "wrong" apk in weird edge cases, but the user
|
||||
// wont be tricked into installing it, as it will (likely) have a different signing key.
|
||||
final String selection = alias + Cols.VERSION_CODE + " = ? and " + alias + Cols.APP_ID + " IN (" + getMetadataIdFromPackageNameQuery() + ")";
|
||||
final String[] args = {
|
||||
// First (0th) path segment is the word "apk",
|
||||
// and we are not interested in it.
|
||||
@ -391,6 +410,21 @@ public class ApkProvider extends FDroidProvider {
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Doesn't prefix column names with table alias. This is so that it can be used in UPDATE
|
||||
* queries. Note that this lack of table alias prefixes means this can't be used for general
|
||||
* constraints in a regular select query within {@link ApkProvider} as the queries specify
|
||||
* aliases for the apk table.
|
||||
*/
|
||||
private QuerySelection querySingleWithAppId(Uri uri) {
|
||||
List<String> path = uri.getPathSegments();
|
||||
String appId = path.get(1);
|
||||
String versionCode = path.get(2);
|
||||
final String selection = Cols.APP_ID + " = ? AND " + Cols.VERSION_CODE + " = ? ";
|
||||
final String[] args = {appId, versionCode};
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
protected QuerySelection queryRepo(long repoId) {
|
||||
return queryRepo(repoId, true);
|
||||
}
|
||||
@ -403,7 +437,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
}
|
||||
|
||||
private QuerySelection queryRepoApps(long repoId, String packageNames) {
|
||||
return queryRepo(repoId).add(AppProvider.queryPackageNames(packageNames, "app." + AppMetadataTable.Cols.PACKAGE_NAME));
|
||||
return queryRepo(repoId).add(AppProvider.queryPackageNames(packageNames, "pkg." + PackageTable.Cols.PACKAGE_NAME));
|
||||
}
|
||||
|
||||
protected QuerySelection queryApks(String apkKeys) {
|
||||
@ -422,28 +456,32 @@ public class ApkProvider extends FDroidProvider {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < apkDetails.length; i++) {
|
||||
String[] parts = apkDetails[i].split(":");
|
||||
String packageName = parts[0];
|
||||
String appId = parts[0];
|
||||
String versionCode = parts[1];
|
||||
args[i * 2] = packageName;
|
||||
args[i * 2] = appId;
|
||||
args[i * 2 + 1] = versionCode;
|
||||
if (i != 0) {
|
||||
sb.append(" OR ");
|
||||
}
|
||||
|
||||
sb.append(" ( ")
|
||||
.append(alias)
|
||||
.append(Cols.APP_ID)
|
||||
.append(" = (")
|
||||
.append(getAppIdFromPackageNameQuery())
|
||||
.append(") AND ")
|
||||
.append(" = ? ")
|
||||
.append(" AND ")
|
||||
.append(alias)
|
||||
.append(Cols.VERSION_CODE)
|
||||
.append(" = ? ) ");
|
||||
}
|
||||
|
||||
return new QuerySelection(sb.toString(), args);
|
||||
}
|
||||
|
||||
private String getAppIdFromPackageNameQuery() {
|
||||
return "SELECT " + AppMetadataTable.Cols.ROW_ID + " FROM " + getAppTableName() + " WHERE " + AppMetadataTable.Cols.PACKAGE_NAME + " = ?";
|
||||
private String getMetadataIdFromPackageNameQuery() {
|
||||
return "SELECT m." + AppMetadataTable.Cols.ROW_ID + " " +
|
||||
"FROM " + AppMetadataTable.NAME + " AS m " +
|
||||
"JOIN " + PackageTable.NAME + " AS p ON ( " +
|
||||
" m." + AppMetadataTable.Cols.PACKAGE_ID + " = p." + PackageTable.Cols.ROW_ID + " ) " +
|
||||
"WHERE p." + PackageTable.Cols.PACKAGE_NAME + " = ?";
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -455,7 +493,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
case CODE_LIST:
|
||||
break;
|
||||
|
||||
case CODE_SINGLE:
|
||||
case CODE_APK_FROM_ANY_REPO:
|
||||
query = query.add(querySingleFromAnyRepo(uri));
|
||||
break;
|
||||
|
||||
@ -535,10 +573,6 @@ public class ApkProvider extends FDroidProvider {
|
||||
query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment()), false));
|
||||
break;
|
||||
|
||||
case CODE_PACKAGE:
|
||||
query = query.add(queryPackage(uri.getLastPathSegment(), false));
|
||||
break;
|
||||
|
||||
case CODE_APKS:
|
||||
query = query.add(queryApks(uri.getLastPathSegment(), false));
|
||||
break;
|
||||
@ -562,7 +596,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||
if (MATCHER.match(uri) != CODE_SINGLE) {
|
||||
if (MATCHER.match(uri) != CODE_APK_FROM_REPO) {
|
||||
throw new UnsupportedOperationException("Cannot update anything other than a single apk.");
|
||||
}
|
||||
return performUpdateUnchecked(uri, values, where, whereArgs);
|
||||
@ -573,7 +607,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
removeFieldsFromOtherTables(values);
|
||||
|
||||
QuerySelection query = new QuerySelection(where, whereArgs);
|
||||
query = query.add(querySingleFromAnyRepo(uri, false));
|
||||
query = query.add(querySingleWithAppId(uri));
|
||||
|
||||
int numRows = db().update(getTableName(), values, query.getSelection(), query.getArgs());
|
||||
if (!isApplyingBatch()) {
|
||||
|
@ -46,6 +46,14 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
|
||||
public String packageName = "unknown";
|
||||
public String name = "Unknown";
|
||||
|
||||
/**
|
||||
* This is primarily for the purpose of saving app metadata when parsing an index.xml file.
|
||||
* At most other times, we don't particularly care which repo an {@link App} object came from.
|
||||
* It is pretty much transparent, because the metadata will be populated from the repo with
|
||||
* the highest priority. The UI doesn't care normally _which_ repo provided the metadata.
|
||||
*/
|
||||
public long repoId;
|
||||
public String summary = "Unknown application";
|
||||
public String icon;
|
||||
|
||||
@ -147,10 +155,13 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
case Cols.ROW_ID:
|
||||
id = cursor.getLong(i);
|
||||
break;
|
||||
case Cols.REPO_ID:
|
||||
repoId = cursor.getLong(i);
|
||||
break;
|
||||
case Cols.IS_COMPATIBLE:
|
||||
compatible = cursor.getInt(i) == 1;
|
||||
break;
|
||||
case Cols.PACKAGE_NAME:
|
||||
case Cols.Package.PACKAGE_NAME:
|
||||
packageName = cursor.getString(i);
|
||||
break;
|
||||
case Cols.NAME:
|
||||
@ -430,8 +441,9 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
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.PACKAGE_NAME, packageName);
|
||||
values.put(Cols.NAME, name);
|
||||
values.put(Cols.REPO_ID, repoId);
|
||||
values.put(Cols.SUMMARY, summary);
|
||||
values.put(Cols.ICON, icon);
|
||||
values.put(Cols.ICON_URL, iconUrl);
|
||||
@ -560,6 +572,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
dest.writeByte(this.compatible ? (byte) 1 : (byte) 0);
|
||||
dest.writeString(this.packageName);
|
||||
dest.writeString(this.name);
|
||||
dest.writeLong(this.repoId);
|
||||
dest.writeString(this.summary);
|
||||
dest.writeString(this.icon);
|
||||
dest.writeString(this.description);
|
||||
@ -596,6 +609,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
this.compatible = in.readByte() != 0;
|
||||
this.packageName = in.readString();
|
||||
this.name = in.readString();
|
||||
this.repoId = in.readLong();
|
||||
this.summary = in.readString();
|
||||
this.icon = in.readString();
|
||||
this.description = in.readString();
|
||||
|
@ -18,6 +18,7 @@ import org.fdroid.fdroid.data.Schema.AppPrefsTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
|
||||
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
|
||||
import org.fdroid.fdroid.data.Schema.PackageTable;
|
||||
import org.fdroid.fdroid.data.Schema.RepoTable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -27,6 +28,23 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Each app has a bunch of metadata that it associates with a package name (such as org.fdroid.fdroid).
|
||||
* Multiple repositories can host the same package, and provide different metadata for that app.
|
||||
*
|
||||
* As such, it is usually the case that you are interested in an {@link App} which has its metadata
|
||||
* provided by "the repo with the best priority", rather than "specific repo X". This is important
|
||||
* when asking for an apk, whereby the preferable way is likely using:
|
||||
*
|
||||
* * {@link AppProvider.Helper#findHighestPriorityMetadata(ContentResolver, String)}
|
||||
*
|
||||
* rather than:
|
||||
*
|
||||
* * {@link AppProvider.Helper#findSpecificApp(ContentResolver, String, long, String[])}
|
||||
*
|
||||
* The same can be said of retrieving a list of {@link App} objects, where the metadata for each app
|
||||
* in the result set should be populated from the repository with the best priority.
|
||||
*/
|
||||
public class AppProvider extends FDroidProvider {
|
||||
|
||||
private static final String TAG = "AppProvider";
|
||||
@ -120,16 +138,27 @@ public class AppProvider extends FDroidProvider {
|
||||
return categories;
|
||||
}
|
||||
|
||||
public static App findByPackageName(ContentResolver resolver, String packageName) {
|
||||
return findByPackageName(resolver, packageName, Cols.ALL);
|
||||
public static App findHighestPriorityMetadata(ContentResolver resolver, String packageName) {
|
||||
final Uri uri = getHighestPriorityMetadataUri(packageName);
|
||||
return cursorToApp(resolver.query(uri, Cols.ALL, null, null, null));
|
||||
}
|
||||
|
||||
public static App findByPackageName(ContentResolver resolver, String packageName,
|
||||
String[] projection) {
|
||||
final Uri uri = getContentUri(packageName);
|
||||
/**
|
||||
* Returns an {@link App} with metadata provided by a specific {@code repoId}. Keep in mind
|
||||
* that most of the time we don't care which repo provides the metadata for a particular app,
|
||||
* as long as it is the repo with the best priority. In those cases, you should instead use
|
||||
* {@link AppProvider.Helper#findHighestPriorityMetadata(ContentResolver, String)}.
|
||||
*/
|
||||
public static App findSpecificApp(ContentResolver resolver, String packageName, long repoId,
|
||||
String[] projection) {
|
||||
final Uri uri = getSpecificAppUri(packageName, repoId);
|
||||
return cursorToApp(resolver.query(uri, projection, null, null, null));
|
||||
}
|
||||
|
||||
public static App findSpecificApp(ContentResolver resolver, String packageName, long repoId) {
|
||||
return findSpecificApp(resolver, packageName, repoId, Cols.ALL);
|
||||
}
|
||||
|
||||
private static App cursorToApp(Cursor cursor) {
|
||||
App app = null;
|
||||
if (cursor != null) {
|
||||
@ -155,6 +184,11 @@ public class AppProvider extends FDroidProvider {
|
||||
public static List<App> findCanUpdate(Context context, String[] projection) {
|
||||
return cursorToList(context.getContentResolver().query(AppProvider.getCanUpdateUri(), projection, null, null, null));
|
||||
}
|
||||
|
||||
public static void recalculatePreferredMetadata(Context context) {
|
||||
Uri uri = Uri.withAppendedPath(AppProvider.getContentUri(), PATH_CALC_PREFERRED_METADATA);
|
||||
context.getContentResolver().query(uri, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -254,13 +288,15 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
@Override
|
||||
protected String getRequiredTables() {
|
||||
final String pkg = PackageTable.NAME;
|
||||
final String app = getTableName();
|
||||
final String apk = getApkTableName();
|
||||
final String repo = RepoTable.NAME;
|
||||
|
||||
return app +
|
||||
" 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 + ") ";
|
||||
return pkg +
|
||||
" JOIN " + app + " ON (" + app + "." + Cols.PACKAGE_ID + " = " + pkg + "." + PackageTable.Cols.ROW_ID + ") " +
|
||||
" JOIN " + repo + " ON (" + app + "." + Cols.REPO_ID + " = " + repo + "." + RepoTable.Cols._ID + ") " +
|
||||
" LEFT JOIN " + apk + " ON (" + apk + "." + ApkTable.Cols.APP_ID + " = " + app + "." + Cols.ROW_ID + ") ";
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -291,7 +327,7 @@ public class AppProvider extends FDroidProvider {
|
||||
join(
|
||||
InstalledAppTable.NAME,
|
||||
"installed",
|
||||
"installed." + InstalledAppTable.Cols.PACKAGE_NAME + " = " + getTableName() + "." + Cols.PACKAGE_NAME);
|
||||
"installed." + InstalledAppTable.Cols.PACKAGE_NAME + " = " + PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME);
|
||||
requiresInstalledTable = true;
|
||||
}
|
||||
}
|
||||
@ -301,7 +337,7 @@ public class AppProvider extends FDroidProvider {
|
||||
leftJoin(
|
||||
AppPrefsTable.NAME,
|
||||
"prefs",
|
||||
"prefs." + AppPrefsTable.Cols.PACKAGE_NAME + " = " + getTableName() + "." + Cols.PACKAGE_NAME);
|
||||
"prefs." + AppPrefsTable.Cols.PACKAGE_NAME + " = " + PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME);
|
||||
requiresLeftJoinToPrefs = true;
|
||||
}
|
||||
}
|
||||
@ -311,7 +347,7 @@ public class AppProvider extends FDroidProvider {
|
||||
leftJoin(
|
||||
InstalledAppTable.NAME,
|
||||
"installed",
|
||||
"installed." + InstalledAppTable.Cols.PACKAGE_NAME + " = " + getTableName() + "." + Cols.PACKAGE_NAME);
|
||||
"installed." + InstalledAppTable.Cols.PACKAGE_NAME + " = " + PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME);
|
||||
requiresInstalledTable = true;
|
||||
}
|
||||
}
|
||||
@ -319,6 +355,9 @@ public class AppProvider extends FDroidProvider {
|
||||
@Override
|
||||
public void addField(String field) {
|
||||
switch (field) {
|
||||
case Cols.Package.PACKAGE_NAME:
|
||||
appendField(PackageTable.Cols.PACKAGE_NAME, PackageTable.NAME, Cols.Package.PACKAGE_NAME);
|
||||
break;
|
||||
case Cols.SuggestedApk.VERSION_NAME:
|
||||
addSuggestedApkVersionField();
|
||||
break;
|
||||
@ -404,11 +443,14 @@ public class AppProvider extends FDroidProvider {
|
||||
private static final String PATH_SEARCH_REPO = "searchRepo";
|
||||
private static final String PATH_NO_APKS = "noApks";
|
||||
protected static final String PATH_APPS = "apps";
|
||||
protected static final String PATH_SPECIFIC_APP = "app";
|
||||
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_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex";
|
||||
private static final String PATH_REPO = "repo";
|
||||
private static final String PATH_HIGHEST_PRIORITY = "highestPriority";
|
||||
private static final String PATH_CALC_PREFERRED_METADATA = "calcPreferredMetadata";
|
||||
|
||||
private static final int CAN_UPDATE = CODE_SINGLE + 1;
|
||||
private static final int INSTALLED = CAN_UPDATE + 1;
|
||||
@ -422,6 +464,8 @@ public class AppProvider extends FDroidProvider {
|
||||
private static final int SEARCH_REPO = REPO + 1;
|
||||
private static final int SEARCH_INSTALLED = SEARCH_REPO + 1;
|
||||
private static final int SEARCH_CAN_UPDATE = SEARCH_INSTALLED + 1;
|
||||
private static final int HIGHEST_PRIORITY = SEARCH_CAN_UPDATE + 1;
|
||||
private static final int CALC_PREFERRED_METADATA = HIGHEST_PRIORITY + 1;
|
||||
|
||||
static {
|
||||
MATCHER.addURI(getAuthority(), null, CODE_LIST);
|
||||
@ -437,7 +481,9 @@ public class AppProvider extends FDroidProvider {
|
||||
MATCHER.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE);
|
||||
MATCHER.addURI(getAuthority(), PATH_INSTALLED, INSTALLED);
|
||||
MATCHER.addURI(getAuthority(), PATH_NO_APKS, NO_APKS);
|
||||
MATCHER.addURI(getAuthority(), "*", CODE_SINGLE);
|
||||
MATCHER.addURI(getAuthority(), PATH_HIGHEST_PRIORITY + "/*", HIGHEST_PRIORITY);
|
||||
MATCHER.addURI(getAuthority(), PATH_SPECIFIC_APP + "/#/*", CODE_SINGLE);
|
||||
MATCHER.addURI(getAuthority(), PATH_CALC_PREFERRED_METADATA, CALC_PREFERRED_METADATA);
|
||||
}
|
||||
|
||||
public static Uri getContentUri() {
|
||||
@ -486,6 +532,27 @@ public class AppProvider extends FDroidProvider {
|
||||
return getContentUri(app.packageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see AppProvider.Helper#findSpecificApp(ContentResolver, String, long, String[]) for details
|
||||
* of why you should usually prefer {@link AppProvider#getHighestPriorityMetadataUri(String)} to
|
||||
* this method.
|
||||
*/
|
||||
public static Uri getSpecificAppUri(String packageName, long repoId) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_SPECIFIC_APP)
|
||||
.appendPath(Long.toString(repoId))
|
||||
.appendPath(packageName)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri getHighestPriorityMetadataUri(String packageName) {
|
||||
return getContentUri().buildUpon()
|
||||
.appendPath(PATH_HIGHEST_PRIORITY)
|
||||
.appendPath(packageName)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri getContentUri(String packageName) {
|
||||
return Uri.withAppendedPath(getContentUri(), packageName);
|
||||
}
|
||||
@ -590,7 +657,7 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
final String app = getTableName();
|
||||
final String[] columns = {
|
||||
app + "." + Cols.PACKAGE_NAME,
|
||||
PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME,
|
||||
app + "." + Cols.NAME,
|
||||
app + "." + Cols.SUMMARY,
|
||||
app + "." + Cols.DESCRIPTION,
|
||||
@ -624,9 +691,22 @@ public class AppProvider extends FDroidProvider {
|
||||
return new AppQuerySelection(selection.toString(), selectionKeywords);
|
||||
}
|
||||
|
||||
protected AppQuerySelection querySingle(String packageName) {
|
||||
final String selection = getTableName() + "." + Cols.PACKAGE_NAME + " = ?";
|
||||
final String[] args = {packageName};
|
||||
protected AppQuerySelection querySingle(String packageName, long repoId) {
|
||||
final String selection = getTableName() + "." + Cols.REPO_ID + " = ? ";
|
||||
final String[] args = {Long.toString(repoId)};
|
||||
return new AppQuerySelection(selection, args).add(queryPackageName(packageName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link AppProvider#querySingle(String, long)} except it is used for the purpose
|
||||
* of an UPDATE query rather than a SELECT query. This means that it must use a subquery to get
|
||||
* the {@link Cols.Package#PACKAGE_ID} rather than the join which is already in place for that
|
||||
* table. The reason is because UPDATE queries cannot include joins in SQLite.
|
||||
*/
|
||||
protected AppQuerySelection querySingleForUpdate(String packageName, long repoId) {
|
||||
final String selection = Cols.PACKAGE_ID + " = (" + getPackageIdFromPackageNameQuery() +
|
||||
") AND " + Cols.REPO_ID + " = ? ";
|
||||
final String[] args = {packageName, Long.toString(repoId)};
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
@ -645,6 +725,22 @@ public class AppProvider extends FDroidProvider {
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that for each app metadata row with the same package name, only the one from the repo
|
||||
* with the best priority is represented in the result set. While possible to calculate this
|
||||
* dynamically each time the query is run, we precalculate it during repo updates for performance.
|
||||
*/
|
||||
private AppQuerySelection queryHighestPriority() {
|
||||
final String selection = PackageTable.NAME + "." + PackageTable.Cols.PREFERRED_METADATA + " = " + getTableName() + "." + Cols.ROW_ID;
|
||||
return new AppQuerySelection(selection);
|
||||
}
|
||||
|
||||
private AppQuerySelection queryPackageName(String packageName) {
|
||||
final String selection = PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME + " = ? ";
|
||||
final String[] args = {packageName};
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private AppQuerySelection queryRecentlyUpdated() {
|
||||
final String app = getTableName();
|
||||
final String lastUpdated = app + "." + Cols.LAST_UPDATED;
|
||||
@ -691,13 +787,29 @@ public class AppProvider extends FDroidProvider {
|
||||
// Queries which are for the main list of apps should not include swap apps.
|
||||
boolean includeSwap = true;
|
||||
|
||||
// It is usually the case that we ask for app(s) for which we don't care what repo is
|
||||
// responsible for providing them. In that case, we need to populate the metadata with
|
||||
// that form the repo with the highest priority.
|
||||
// Whenever we know which repo it is coming from, then it is important that we don't
|
||||
// delegate to the repo with the highest priority, but rather the specific repo we are
|
||||
// querying from.
|
||||
boolean repoIsKnown = false;
|
||||
|
||||
switch (MATCHER.match(uri)) {
|
||||
case CALC_PREFERRED_METADATA:
|
||||
updatePreferredMetadata();
|
||||
return null;
|
||||
|
||||
case CODE_LIST:
|
||||
includeSwap = false;
|
||||
break;
|
||||
|
||||
case CODE_SINGLE:
|
||||
selection = selection.add(querySingle(uri.getLastPathSegment()));
|
||||
List<String> pathParts = uri.getPathSegments();
|
||||
long repoId = Long.parseLong(pathParts.get(1));
|
||||
String packageName = pathParts.get(2);
|
||||
selection = selection.add(querySingle(packageName, repoId));
|
||||
repoIsKnown = true;
|
||||
break;
|
||||
|
||||
case CAN_UPDATE:
|
||||
@ -707,6 +819,7 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
case REPO:
|
||||
selection = selection.add(queryRepo(Long.parseLong(uri.getLastPathSegment())));
|
||||
repoIsKnown = true;
|
||||
break;
|
||||
|
||||
case INSTALLED:
|
||||
@ -733,6 +846,7 @@ public class AppProvider extends FDroidProvider {
|
||||
selection = selection
|
||||
.add(querySearch(uri.getPathSegments().get(2)))
|
||||
.add(queryRepo(Long.parseLong(uri.getPathSegments().get(1))));
|
||||
repoIsKnown = true;
|
||||
break;
|
||||
|
||||
case NO_APKS:
|
||||
@ -756,11 +870,20 @@ public class AppProvider extends FDroidProvider {
|
||||
includeSwap = false;
|
||||
break;
|
||||
|
||||
case HIGHEST_PRIORITY:
|
||||
selection = selection.add(queryPackageName(uri.getLastPathSegment()));
|
||||
includeSwap = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Invalid URI for app content provider: " + uri);
|
||||
throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri);
|
||||
}
|
||||
|
||||
if (!repoIsKnown) {
|
||||
selection = selection.add(queryHighestPriority());
|
||||
}
|
||||
|
||||
return runQuery(uri, selection, projection, includeSwap, sortOrder);
|
||||
}
|
||||
|
||||
@ -799,44 +922,59 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
long packageId = PackageProvider.Helper.ensureExists(getContext(), values.getAsString(Cols.Package.PACKAGE_NAME));
|
||||
values.remove(Cols.Package.PACKAGE_NAME);
|
||||
values.put(Cols.PACKAGE_ID, packageId);
|
||||
|
||||
db().insertOrThrow(getTableName(), null, values);
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
return getContentUri(values.getAsString(Cols.PACKAGE_NAME));
|
||||
return getSpecificAppUri(values.getAsString(PackageTable.Cols.PACKAGE_NAME), values.getAsLong(Cols.REPO_ID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||
QuerySelection query = new QuerySelection(where, whereArgs);
|
||||
switch (MATCHER.match(uri)) {
|
||||
|
||||
case CALC_APP_DETAILS_FROM_INDEX:
|
||||
updateAppDetails();
|
||||
return 0;
|
||||
|
||||
case CODE_SINGLE:
|
||||
query = query.add(querySingle(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Update not supported for " + uri + ".");
|
||||
|
||||
if (MATCHER.match(uri) != CALC_APP_DETAILS_FROM_INDEX) {
|
||||
throw new UnsupportedOperationException("Update not supported for " + uri + ".");
|
||||
}
|
||||
int count = db().update(getTableName(), values, query.getSelection(), query.getArgs());
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
return count;
|
||||
|
||||
updateAppDetails();
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected void updateAppDetails() {
|
||||
updatePreferredMetadata();
|
||||
updateCompatibleFlags();
|
||||
updateSuggestedFromUpstream();
|
||||
updateSuggestedFromLatest();
|
||||
updateIconUrls(getContext(), db(), getTableName(), getApkTableName());
|
||||
}
|
||||
|
||||
private void updatePreferredMetadata() {
|
||||
Utils.debugLog(TAG, "Deciding on which metadata should take priority for each package.");
|
||||
|
||||
final String app = getTableName();
|
||||
|
||||
final String highestPriority =
|
||||
"SELECT MIN(r." + RepoTable.Cols.PRIORITY + ") " +
|
||||
"FROM " + RepoTable.NAME + " AS r " +
|
||||
"JOIN " + getTableName() + " AS m ON (m." + Cols.REPO_ID + " = r." + RepoTable.Cols._ID + ") " +
|
||||
"WHERE m." + Cols.PACKAGE_ID + " = " + "metadata." + Cols.PACKAGE_ID;
|
||||
|
||||
String updateSql =
|
||||
"UPDATE " + PackageTable.NAME + " " +
|
||||
"SET " + PackageTable.Cols.PREFERRED_METADATA + " = ( " +
|
||||
" SELECT metadata." + Cols.ROW_ID +
|
||||
" FROM " + app + " AS metadata " +
|
||||
" JOIN " + RepoTable.NAME + " AS repo ON (metadata." + Cols.REPO_ID + " = repo." + RepoTable.Cols._ID + ") " +
|
||||
" WHERE metadata." + Cols.PACKAGE_ID + " = " + PackageTable.NAME + "." + PackageTable.Cols.ROW_ID +
|
||||
" AND repo." + RepoTable.Cols.PRIORITY + " = (" + highestPriority + ")" +
|
||||
");";
|
||||
|
||||
db().execSQL(updateSql);
|
||||
}
|
||||
|
||||
/**
|
||||
* For each app, we want to set the isCompatible flag to 1 if any of the apks we know
|
||||
* about are compatible, and 0 otherwise.
|
||||
|
@ -34,6 +34,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.PackageTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppPrefsTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
||||
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
|
||||
@ -50,6 +51,12 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String DATABASE_NAME = "fdroid";
|
||||
|
||||
private static final String CREATE_TABLE_PACKAGE = "CREATE TABLE " + PackageTable.NAME
|
||||
+ " ( "
|
||||
+ PackageTable.Cols.PACKAGE_NAME + " text not null, "
|
||||
+ PackageTable.Cols.PREFERRED_METADATA + " integer"
|
||||
+ ");";
|
||||
|
||||
private static final String CREATE_TABLE_REPO = "create table "
|
||||
+ RepoTable.NAME + " ("
|
||||
+ RepoTable.Cols._ID + " integer primary key, "
|
||||
@ -97,7 +104,8 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
static final String CREATE_TABLE_APP_METADATA = "CREATE TABLE " + AppMetadataTable.NAME
|
||||
+ " ( "
|
||||
+ AppMetadataTable.Cols.PACKAGE_NAME + " text not null, "
|
||||
+ AppMetadataTable.Cols.PACKAGE_ID + " integer not null, "
|
||||
+ AppMetadataTable.Cols.REPO_ID + " integer not null, "
|
||||
+ AppMetadataTable.Cols.NAME + " text not null, "
|
||||
+ AppMetadataTable.Cols.SUMMARY + " text not null, "
|
||||
+ AppMetadataTable.Cols.ICON + " text, "
|
||||
@ -124,7 +132,7 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
+ AppMetadataTable.Cols.IS_COMPATIBLE + " int not null,"
|
||||
+ AppMetadataTable.Cols.ICON_URL + " text, "
|
||||
+ AppMetadataTable.Cols.ICON_URL_LARGE + " text, "
|
||||
+ "primary key(" + AppMetadataTable.Cols.PACKAGE_NAME + "));";
|
||||
+ "primary key(" + AppMetadataTable.Cols.PACKAGE_ID + ", " + AppMetadataTable.Cols.REPO_ID + "));";
|
||||
|
||||
private static final String CREATE_TABLE_APP_PREFS = "CREATE TABLE " + AppPrefsTable.NAME
|
||||
+ " ( "
|
||||
@ -146,7 +154,7 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
+ " );";
|
||||
private static final String DROP_TABLE_INSTALLED_APP = "DROP TABLE " + InstalledAppTable.NAME + ";";
|
||||
|
||||
private static final int DB_VERSION = 62;
|
||||
private static final int DB_VERSION = 63;
|
||||
|
||||
private final Context context;
|
||||
|
||||
@ -248,6 +256,7 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
|
||||
db.execSQL(CREATE_TABLE_PACKAGE);
|
||||
db.execSQL(CREATE_TABLE_APP_METADATA);
|
||||
db.execSQL(CREATE_TABLE_APK);
|
||||
db.execSQL(CREATE_TABLE_INSTALLED_APP);
|
||||
@ -344,6 +353,63 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
addAppPrefsTable(db, oldVersion);
|
||||
lowerCaseApkHashes(db, oldVersion);
|
||||
supportRepoPushRequests(db, oldVersion);
|
||||
migrateToPackageTable(db, oldVersion);
|
||||
}
|
||||
|
||||
private void migrateToPackageTable(SQLiteDatabase db, int oldVersion) {
|
||||
if (oldVersion >= 63) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetTransient(db);
|
||||
|
||||
// By pushing _ALL_ repositories to a priority of 10, it makes it slightly easier
|
||||
// to query for the non-default repositories later on in this method.
|
||||
ContentValues highPriority = new ContentValues(1);
|
||||
highPriority.put(RepoTable.Cols.PRIORITY, 10);
|
||||
db.update(RepoTable.NAME, highPriority, null, null);
|
||||
|
||||
String[] defaultRepos = context.getResources().getStringArray(R.array.default_repos);
|
||||
String fdroidPubKey = defaultRepos[7];
|
||||
String fdroidAddress = defaultRepos[1];
|
||||
String fdroidArchiveAddress = defaultRepos[REPO_XML_ARG_COUNT + 1];
|
||||
String gpPubKey = defaultRepos[REPO_XML_ARG_COUNT * 2 + 7];
|
||||
String gpAddress = defaultRepos[REPO_XML_ARG_COUNT * 2 + 1];
|
||||
String gpArchiveAddress = defaultRepos[REPO_XML_ARG_COUNT * 3 + 1];
|
||||
|
||||
updateRepoPriority(db, fdroidPubKey, fdroidAddress, 1);
|
||||
updateRepoPriority(db, fdroidPubKey, fdroidArchiveAddress, 2);
|
||||
updateRepoPriority(db, gpPubKey, gpAddress, 3);
|
||||
updateRepoPriority(db, gpPubKey, gpArchiveAddress, 4);
|
||||
|
||||
int priority = 5;
|
||||
String[] projection = new String[] {RepoTable.Cols.SIGNING_CERT, RepoTable.Cols.ADDRESS};
|
||||
|
||||
// Order by ID, because that is a good analogy for the order in which they were added.
|
||||
// The order in which they were added is likely the order they present in the ManageRepos activity.
|
||||
Cursor cursor = db.query(RepoTable.NAME, projection, RepoTable.Cols.PRIORITY + " > 4", null, null, null, RepoTable.Cols._ID);
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
String signingCert = cursor.getString(cursor.getColumnIndex(RepoTable.Cols.SIGNING_CERT));
|
||||
String address = cursor.getString(cursor.getColumnIndex(RepoTable.Cols.ADDRESS));
|
||||
updateRepoPriority(db, signingCert, address, priority);
|
||||
cursor.moveToNext();
|
||||
priority++;
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
private void updateRepoPriority(SQLiteDatabase db, String signingCert, String address, int priority) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(RepoTable.Cols.PRIORITY, Integer.toString(priority));
|
||||
|
||||
Utils.debugLog(TAG, "Setting priority of repo " + address + " to " + priority);
|
||||
db.update(
|
||||
RepoTable.NAME,
|
||||
values,
|
||||
RepoTable.Cols.SIGNING_CERT + " = ? AND " + RepoTable.Cols.ADDRESS + " = ?",
|
||||
new String[] {signingCert, address}
|
||||
);
|
||||
}
|
||||
|
||||
private void lowerCaseApkHashes(SQLiteDatabase db, int oldVersion) {
|
||||
@ -370,7 +436,7 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
+ AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ", "
|
||||
+ AppPrefsTable.Cols.IGNORE_ALL_UPDATES
|
||||
+ ") SELECT "
|
||||
+ AppMetadataTable.Cols.PACKAGE_NAME + ", "
|
||||
+ "id, "
|
||||
+ "ignoreThisUpdate, "
|
||||
+ "ignoreAllUpdates "
|
||||
+ "FROM " + AppMetadataTable.NAME + " "
|
||||
@ -479,7 +545,7 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
final String update = "UPDATE " + ApkTable.NAME + " SET " + ApkTable.Cols.APP_ID + " = ( " +
|
||||
"SELECT app." + AppMetadataTable.Cols.ROW_ID + " " +
|
||||
"FROM " + AppMetadataTable.NAME + " AS app " +
|
||||
"WHERE " + ApkTable.NAME + ".id = app." + AppMetadataTable.Cols.PACKAGE_NAME + ")";
|
||||
"WHERE " + ApkTable.NAME + ".id = app.id)";
|
||||
Log.i(TAG, "Updating foreign key from " + ApkTable.NAME + " to " + AppMetadataTable.NAME + " to use numeric foreign key.");
|
||||
Utils.debugLog(TAG, update);
|
||||
db.execSQL(update);
|
||||
@ -733,8 +799,13 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (tableExists(db, PackageTable.NAME)) {
|
||||
db.execSQL("DROP TABLE " + PackageTable.NAME);
|
||||
}
|
||||
|
||||
db.execSQL("DROP TABLE " + AppMetadataTable.NAME);
|
||||
db.execSQL("DROP TABLE " + ApkTable.NAME);
|
||||
db.execSQL(CREATE_TABLE_PACKAGE);
|
||||
db.execSQL(CREATE_TABLE_APP_METADATA);
|
||||
db.execSQL(CREATE_TABLE_APK);
|
||||
clearRepoEtags(db);
|
||||
@ -765,11 +836,24 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
|
||||
private static void ensureIndexes(SQLiteDatabase db) {
|
||||
if (tableExists(db, PackageTable.NAME)) {
|
||||
Utils.debugLog(TAG, "Ensuring indexes exist for " + PackageTable.NAME);
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS package_packageName on " + PackageTable.NAME + " (" + PackageTable.Cols.PACKAGE_NAME + ");");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS package_preferredMetadata on " + PackageTable.NAME + " (" + PackageTable.Cols.PREFERRED_METADATA + ");");
|
||||
}
|
||||
|
||||
Utils.debugLog(TAG, "Ensuring indexes exist for " + AppMetadataTable.NAME);
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS app_id on " + AppMetadataTable.NAME + " (" + AppMetadataTable.Cols.PACKAGE_NAME + ");");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS name on " + AppMetadataTable.NAME + " (" + AppMetadataTable.Cols.NAME + ");"); // Used for sorting most lists
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS added on " + AppMetadataTable.NAME + " (" + AppMetadataTable.Cols.ADDED + ");"); // Used for sorting "newly added"
|
||||
|
||||
if (columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.PACKAGE_ID)) {
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS metadata_packageId ON " + AppMetadataTable.NAME + " (" + AppMetadataTable.Cols.PACKAGE_ID + ");");
|
||||
}
|
||||
|
||||
if (columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.REPO_ID)) {
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS metadata_repoId ON " + AppMetadataTable.NAME + " (" + AppMetadataTable.Cols.REPO_ID + ");");
|
||||
}
|
||||
|
||||
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 + ");");
|
||||
|
@ -157,4 +157,12 @@ public abstract class FDroidProvider extends ContentProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to be used when you need to know the primary key from the package table
|
||||
* when all you have is the package name.
|
||||
*/
|
||||
protected static String getPackageIdFromPackageNameQuery() {
|
||||
return "SELECT " + Schema.PackageTable.Cols.ROW_ID + " FROM " + Schema.PackageTable.NAME + " WHERE " + Schema.PackageTable.Cols.PACKAGE_NAME + " = ?";
|
||||
}
|
||||
}
|
||||
|
170
app/src/main/java/org/fdroid/fdroid/data/PackageProvider.java
Normal file
170
app/src/main/java/org/fdroid/fdroid/data/PackageProvider.java
Normal file
@ -0,0 +1,170 @@
|
||||
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 org.fdroid.fdroid.data.Schema.PackageTable;
|
||||
import org.fdroid.fdroid.data.Schema.PackageTable.Cols;
|
||||
|
||||
public class PackageProvider extends FDroidProvider {
|
||||
|
||||
public static final class Helper {
|
||||
private Helper() { }
|
||||
|
||||
public static long ensureExists(Context context, String packageName) {
|
||||
long id = getPackageId(context, packageName);
|
||||
if (id <= 0) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(Cols.PACKAGE_NAME, packageName);
|
||||
Uri uri = context.getContentResolver().insert(getContentUri(), values);
|
||||
id = Long.parseLong(uri.getLastPathSegment());
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
public static long getPackageId(Context context, String packageName) {
|
||||
String[] projection = new String[] {Cols.ROW_ID};
|
||||
Cursor cursor = context.getContentResolver().query(getPackageUri(packageName), projection, null, null, null);
|
||||
if (cursor == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
if (cursor.getCount() == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
cursor.moveToFirst();
|
||||
return cursor.getLong(cursor.getColumnIndexOrThrow(Cols.ROW_ID));
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Query extends QueryBuilder {
|
||||
|
||||
@Override
|
||||
protected String getRequiredTables() {
|
||||
return PackageTable.NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addField(String field) {
|
||||
appendField(field, getTableName());
|
||||
}
|
||||
}
|
||||
|
||||
private static final String PROVIDER_NAME = "PackageProvider";
|
||||
|
||||
private static final UriMatcher MATCHER = new UriMatcher(-1);
|
||||
|
||||
private static final String PATH_PACKAGE_NAME = "packageName";
|
||||
private static final String PATH_PACKAGE_ID = "packageId";
|
||||
|
||||
static {
|
||||
MATCHER.addURI(getAuthority(), PATH_PACKAGE_NAME + "/*", CODE_SINGLE);
|
||||
}
|
||||
|
||||
private static Uri getContentUri() {
|
||||
return Uri.parse("content://" + getAuthority());
|
||||
}
|
||||
|
||||
public static Uri getPackageUri(String packageName) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_PACKAGE_NAME)
|
||||
.appendPath(packageName)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Not actually used as part of the external API to this content provider.
|
||||
* Rather, used as a mechanism for returning the ID of a newly inserted row after calling
|
||||
* {@link android.content.ContentProvider#insert(Uri, ContentValues)}, as that is only able
|
||||
* to return a {@link Uri}. The {@link Uri#getLastPathSegment()} of this URI contains a
|
||||
* {@link Long} which is the {@link PackageTable.Cols#ROW_ID} of the newly inserted row.
|
||||
*/
|
||||
private static Uri getPackageIdUri(long packageId) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_PACKAGE_ID)
|
||||
.appendPath(Long.toString(packageId))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTableName() {
|
||||
return PackageTable.NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getProviderName() {
|
||||
return "PackageProvider";
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleting of packages is not required.
|
||||
* It doesn't matter if we have a package name in the database after the package is no longer
|
||||
* present in the repo any more. They wont take up much space, and it is the presence of rows
|
||||
* in the {@link Schema.AppMetadataTable} which decides whether something is available in the
|
||||
* F-Droid client or not.
|
||||
*/
|
||||
@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) {
|
||||
long rowId = db().insertOrThrow(getTableName(), null, values);
|
||||
getContext().getContentResolver().notifyChange(AppProvider.getCanUpdateUri(), null);
|
||||
return getPackageIdUri(rowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Package names never change. If a package name has changed, then that means that it is a
|
||||
* new app all together as far as Android is concerned.
|
||||
*/
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||
throw new UnsupportedOperationException("Update not supported for " + uri + ".");
|
||||
}
|
||||
}
|
@ -143,8 +143,8 @@ public class RepoPersister {
|
||||
for (App app : apps) {
|
||||
packageNames.add(app.packageName);
|
||||
}
|
||||
String[] projection = {Schema.AppMetadataTable.Cols.ROW_ID, Schema.AppMetadataTable.Cols.PACKAGE_NAME};
|
||||
List<App> fromDb = TempAppProvider.Helper.findByPackageNames(context, packageNames, projection);
|
||||
String[] projection = {Schema.AppMetadataTable.Cols.ROW_ID, Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME};
|
||||
List<App> fromDb = TempAppProvider.Helper.findByPackageNames(context, packageNames, repo.id, projection);
|
||||
|
||||
Map<String, Long> ids = new HashMap<>(fromDb.size());
|
||||
for (App app : fromDb) {
|
||||
@ -175,15 +175,17 @@ public class RepoPersister {
|
||||
*/
|
||||
private ArrayList<ContentProviderOperation> insertOrUpdateApks(List<Apk> packages) {
|
||||
String[] projection = new String[]{
|
||||
Schema.ApkTable.Cols.App.PACKAGE_NAME,
|
||||
Schema.ApkTable.Cols.Package.PACKAGE_NAME,
|
||||
Schema.ApkTable.Cols.VERSION_CODE,
|
||||
Schema.ApkTable.Cols.REPO_ID,
|
||||
Schema.ApkTable.Cols.APP_ID,
|
||||
};
|
||||
List<Apk> existingApks = ApkProvider.Helper.knownApks(context, packages, projection);
|
||||
ArrayList<ContentProviderOperation> operations = new ArrayList<>(packages.size());
|
||||
for (Apk apk : packages) {
|
||||
boolean exists = false;
|
||||
for (Apk existing : existingApks) {
|
||||
if (existing.packageName.equals(apk.packageName) && existing.versionCode == apk.versionCode) {
|
||||
if (existing.repo == apk.repo && existing.packageName.equals(apk.packageName) && existing.versionCode == apk.versionCode) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
@ -204,7 +206,7 @@ public class RepoPersister {
|
||||
* <strong>Does not do any checks to see if the app already exists or not.</strong>
|
||||
*/
|
||||
private ContentProviderOperation updateExistingApp(App app) {
|
||||
Uri uri = TempAppProvider.getAppUri(app);
|
||||
Uri uri = TempAppProvider.getSpecificTempAppUri(app.packageName, app.repoId);
|
||||
return ContentProviderOperation.newUpdate(uri).withValues(app.toContentValues()).build();
|
||||
}
|
||||
|
||||
@ -224,8 +226,8 @@ public class RepoPersister {
|
||||
* array.
|
||||
*/
|
||||
private boolean isAppInDatabase(App app) {
|
||||
String[] fields = {Schema.AppMetadataTable.Cols.PACKAGE_NAME};
|
||||
App found = AppProvider.Helper.findByPackageName(context.getContentResolver(), app.packageName, fields);
|
||||
String[] fields = {Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME};
|
||||
App found = AppProvider.Helper.findSpecificApp(context.getContentResolver(), app.packageName, repo.id, fields);
|
||||
return found != null;
|
||||
}
|
||||
|
||||
@ -255,8 +257,8 @@ public class RepoPersister {
|
||||
*/
|
||||
@Nullable
|
||||
private ContentProviderOperation deleteOrphanedApks(List<App> apps, Map<String, List<Apk>> packages) {
|
||||
String[] projection = new String[]{Schema.ApkTable.Cols.App.PACKAGE_NAME, Schema.ApkTable.Cols.VERSION_CODE};
|
||||
List<Apk> existing = ApkProvider.Helper.find(context, repo, apps, projection);
|
||||
String[] projection = new String[]{Schema.ApkTable.Cols.Package.PACKAGE_NAME, Schema.ApkTable.Cols.VERSION_CODE};
|
||||
List<Apk> existing = ApkProvider.Helper.findByUri(context, repo, apps, projection);
|
||||
List<Apk> toDelete = new ArrayList<>();
|
||||
|
||||
for (Apk existingApk : existing) {
|
||||
|
@ -232,6 +232,8 @@ public class RepoProvider extends FDroidProvider {
|
||||
Uri appUri = AppProvider.getNoApksUri();
|
||||
int appCount = resolver.delete(appUri, null, null);
|
||||
Utils.debugLog(TAG, "Removed " + appCount + " apps with no apks.");
|
||||
|
||||
AppProvider.Helper.recalculatePreferredMetadata(context);
|
||||
}
|
||||
|
||||
public static int countAppsForRepo(Context context, long repoId) {
|
||||
@ -301,7 +303,7 @@ public class RepoProvider extends FDroidProvider {
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
sortOrder = "_ID ASC";
|
||||
sortOrder = Cols.PRIORITY + " ASC";
|
||||
}
|
||||
|
||||
switch (MATCHER.match(uri)) {
|
||||
@ -399,7 +401,37 @@ public class RepoProvider extends FDroidProvider {
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||
|
||||
// When the priority of a repo changes, we need to update the "preferred metadata" foreign
|
||||
// key in the package table to point to the best possible record in the app metadata table.
|
||||
// The full list of times when we need to recalculate the preferred metadata includes:
|
||||
// * After the priority of a repo changes
|
||||
// * After a repo is disabled
|
||||
// * After a repo is enabled
|
||||
// * After an update is performed
|
||||
// This code only checks for the priority changing. All other occasions we can't do the
|
||||
// recalculation right now, because we likely haven't added/removed the relevant apps
|
||||
// from the metadata table yet. Usually the repo details are updated, then a request is
|
||||
// made to do the heavier work (e.g. a repo update to get new list of apps from server).
|
||||
// After the heavier work is complete, then that process can request the preferred metadata
|
||||
// to be recalculated.
|
||||
boolean priorityChanged = false;
|
||||
if (values.containsKey(Cols.PRIORITY)) {
|
||||
Cursor priorityCursor = db().query(getTableName(), new String[]{Cols.PRIORITY}, where, whereArgs, null, null, null);
|
||||
if (priorityCursor.getCount() > 0) {
|
||||
priorityCursor.moveToFirst();
|
||||
int oldPriority = priorityCursor.getInt(priorityCursor.getColumnIndex(Cols.PRIORITY));
|
||||
priorityChanged = oldPriority != values.getAsInteger(Cols.PRIORITY);
|
||||
}
|
||||
priorityCursor.close();
|
||||
}
|
||||
|
||||
int numRows = db().update(getTableName(), values, where, whereArgs);
|
||||
|
||||
if (priorityChanged) {
|
||||
AppProvider.Helper.recalculatePreferredMetadata(getContext());
|
||||
}
|
||||
|
||||
Utils.debugLog(TAG, "Updated repo. Notifying provider change: '" + uri + "'.");
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return numRows;
|
||||
|
@ -9,6 +9,30 @@ import android.provider.BaseColumns;
|
||||
*/
|
||||
public interface Schema {
|
||||
|
||||
interface PackageTable {
|
||||
|
||||
String NAME = "fdroid_package";
|
||||
|
||||
interface Cols {
|
||||
String ROW_ID = "rowid";
|
||||
String PACKAGE_NAME = "packageName";
|
||||
|
||||
/**
|
||||
* Metadata about a package (e.g. description, icon, etc) can come from multiple
|
||||
* different repos. This is a foreign key to the row in {@link AppMetadataTable} for
|
||||
* this package that comes from the repo with the best priority. Although it can be
|
||||
* calculated at runtime using an SQL query, it is more efficient to figure out the
|
||||
* preferred metadata once, after a repo update, rather than every time we need to know
|
||||
* about a package.
|
||||
*/
|
||||
String PREFERRED_METADATA = "preferredMetadata";
|
||||
|
||||
String[] ALL = {
|
||||
ROW_ID, PACKAGE_NAME, PREFERRED_METADATA,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface AppPrefsTable {
|
||||
|
||||
String NAME = "fdroid_appPrefs";
|
||||
@ -39,7 +63,8 @@ public interface Schema {
|
||||
String ROW_ID = "rowid";
|
||||
String _COUNT = "_count";
|
||||
String IS_COMPATIBLE = "compatible";
|
||||
String PACKAGE_NAME = "id";
|
||||
String PACKAGE_ID = "packageId";
|
||||
String REPO_ID = "repoId";
|
||||
String NAME = "name";
|
||||
String SUMMARY = "summary";
|
||||
String ICON = "icon";
|
||||
@ -76,13 +101,17 @@ public interface Schema {
|
||||
String SIGNATURE = "installedSig";
|
||||
}
|
||||
|
||||
interface Package {
|
||||
String PACKAGE_NAME = "package_packageName";
|
||||
}
|
||||
|
||||
/**
|
||||
* Each of the physical columns in the sqlite table. Differs from {@link Cols#ALL} in
|
||||
* that it doesn't include fields which are aliases of other fields (e.g. {@link Cols#_ID}
|
||||
* or which are from other related tables (e.g. {@link Cols.SuggestedApk#VERSION_NAME}).
|
||||
*/
|
||||
String[] ALL_COLS = {
|
||||
ROW_ID, IS_COMPATIBLE, PACKAGE_NAME, NAME, SUMMARY, ICON, DESCRIPTION,
|
||||
ROW_ID, PACKAGE_ID, REPO_ID, IS_COMPATIBLE, NAME, SUMMARY, ICON, DESCRIPTION,
|
||||
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,
|
||||
@ -96,14 +125,14 @@ public interface Schema {
|
||||
* @see Cols#ALL_COLS
|
||||
*/
|
||||
String[] ALL = {
|
||||
_ID, ROW_ID, IS_COMPATIBLE, PACKAGE_NAME, NAME, SUMMARY, ICON, DESCRIPTION,
|
||||
_ID, ROW_ID, REPO_ID, IS_COMPATIBLE, NAME, SUMMARY, ICON, DESCRIPTION,
|
||||
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, ICON_URL, ICON_URL_LARGE,
|
||||
SUGGESTED_VERSION_CODE, SuggestedApk.VERSION_NAME,
|
||||
InstalledApp.VERSION_CODE, InstalledApp.VERSION_NAME,
|
||||
InstalledApp.SIGNATURE,
|
||||
InstalledApp.SIGNATURE, Package.PACKAGE_NAME,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -149,8 +178,8 @@ public interface Schema {
|
||||
String ADDRESS = "repoAddress";
|
||||
}
|
||||
|
||||
interface App {
|
||||
String PACKAGE_NAME = "appPackageName";
|
||||
interface Package {
|
||||
String PACKAGE_NAME = "package_packageName";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -167,7 +196,7 @@ public interface Schema {
|
||||
* @see AppMetadataTable.Cols#ALL
|
||||
*/
|
||||
String[] ALL = {
|
||||
_ID, APP_ID, App.PACKAGE_NAME, VERSION_NAME, REPO_ID, HASH, VERSION_CODE, NAME,
|
||||
_ID, APP_ID, Package.PACKAGE_NAME, VERSION_NAME, REPO_ID, HASH, VERSION_CODE, NAME,
|
||||
SIZE, SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, TARGET_SDK_VERSION, MAX_SDK_VERSION,
|
||||
PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE,
|
||||
IS_COMPATIBLE, Repo.VERSION, Repo.ADDRESS, INCOMPATIBLE_REASONS,
|
||||
|
@ -27,7 +27,8 @@ public class TempApkProvider extends ApkProvider {
|
||||
|
||||
static {
|
||||
MATCHER.addURI(getAuthority(), PATH_INIT, CODE_INIT);
|
||||
MATCHER.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE);
|
||||
MATCHER.addURI(getAuthority(), PATH_APK_FROM_ANY_REPO + "/#/*", CODE_APK_FROM_ANY_REPO);
|
||||
MATCHER.addURI(getAuthority(), PATH_APK_FROM_REPO + "/#/#", CODE_APK_FROM_REPO);
|
||||
MATCHER.addURI(getAuthority(), PATH_REPO_APK + "/#/*", CODE_REPO_APK);
|
||||
}
|
||||
|
||||
@ -52,9 +53,9 @@ public class TempApkProvider extends ApkProvider {
|
||||
public static Uri getApkUri(Apk apk) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_APK)
|
||||
.appendPath(PATH_APK_FROM_REPO)
|
||||
.appendPath(Long.toString(apk.appId))
|
||||
.appendPath(Integer.toString(apk.versionCode))
|
||||
.appendPath(apk.packageName)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -96,7 +97,7 @@ public class TempApkProvider extends ApkProvider {
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||
if (MATCHER.match(uri) != CODE_SINGLE) {
|
||||
if (MATCHER.match(uri) != CODE_APK_FROM_REPO) {
|
||||
throw new UnsupportedOperationException("Cannot update anything other than a single apk.");
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,8 @@ import java.util.List;
|
||||
|
||||
import org.fdroid.fdroid.data.Schema.ApkTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
|
||||
import org.fdroid.fdroid.data.Schema.PackageTable;
|
||||
|
||||
/**
|
||||
* This class does all of its operations in a temporary sqlite table.
|
||||
@ -40,8 +42,8 @@ public class TempAppProvider extends AppProvider {
|
||||
static {
|
||||
MATCHER.addURI(getAuthority(), PATH_INIT, CODE_INIT);
|
||||
MATCHER.addURI(getAuthority(), PATH_COMMIT, CODE_COMMIT);
|
||||
MATCHER.addURI(getAuthority(), PATH_APPS + "/*", APPS);
|
||||
MATCHER.addURI(getAuthority(), "*", CODE_SINGLE);
|
||||
MATCHER.addURI(getAuthority(), PATH_APPS + "/#/*", APPS);
|
||||
MATCHER.addURI(getAuthority(), PATH_SPECIFIC_APP + "/#/*", CODE_SINGLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -57,19 +59,36 @@ public class TempAppProvider extends AppProvider {
|
||||
return Uri.parse("content://" + getAuthority());
|
||||
}
|
||||
|
||||
public static Uri getAppUri(App app) {
|
||||
return Uri.withAppendedPath(getContentUri(), app.packageName);
|
||||
/**
|
||||
* Same as {@link AppProvider#getSpecificAppUri(String, long)}, except loads data from the temp
|
||||
* table being used during a repo update rather than the persistent table.
|
||||
*/
|
||||
public static Uri getSpecificTempAppUri(String packageName, long repoId) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_SPECIFIC_APP)
|
||||
.appendPath(Long.toString(repoId))
|
||||
.appendPath(packageName)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri getAppsUri(List<String> apps) {
|
||||
public static Uri getAppsUri(List<String> apps, long repoId) {
|
||||
return getContentUri().buildUpon()
|
||||
.appendPath(PATH_APPS)
|
||||
.appendPath(Long.toString(repoId))
|
||||
.appendPath(TextUtils.join(",", apps))
|
||||
.build();
|
||||
}
|
||||
|
||||
private AppQuerySelection queryApps(String packageNames) {
|
||||
return queryPackageNames(packageNames, getTableName() + "." + AppMetadataTable.Cols.PACKAGE_NAME);
|
||||
private AppQuerySelection queryRepoApps(long repoId, String packageNames) {
|
||||
return queryPackageNames(packageNames, PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME)
|
||||
.add(queryRepo(repoId));
|
||||
}
|
||||
|
||||
private AppQuerySelection queryRepo(long repoId) {
|
||||
String[] args = new String[] {Long.toString(repoId)};
|
||||
String selection = getTableName() + "." + Cols.REPO_ID + " = ? ";
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
public static class Helper {
|
||||
@ -84,8 +103,8 @@ public class TempAppProvider extends AppProvider {
|
||||
TempApkProvider.Helper.init(context);
|
||||
}
|
||||
|
||||
public static List<App> findByPackageNames(Context context, List<String> packageNames, String[] projection) {
|
||||
Uri uri = getAppsUri(packageNames);
|
||||
public static List<App> findByPackageNames(Context context, List<String> packageNames, long repoId, String[] projection) {
|
||||
Uri uri = getAppsUri(packageNames, repoId);
|
||||
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
|
||||
return AppProvider.Helper.cursorToList(cursor);
|
||||
}
|
||||
@ -126,11 +145,17 @@ public class TempAppProvider extends AppProvider {
|
||||
throw new UnsupportedOperationException("Update not supported for " + uri + ".");
|
||||
}
|
||||
|
||||
QuerySelection query = new QuerySelection(where, whereArgs).add(querySingle(uri.getLastPathSegment()));
|
||||
List<String> pathParts = uri.getPathSegments();
|
||||
String packageName = pathParts.get(2);
|
||||
long repoId = Long.parseLong(pathParts.get(1));
|
||||
QuerySelection query = new QuerySelection(where, whereArgs).add(querySingleForUpdate(packageName, repoId));
|
||||
|
||||
// Package names for apps cannot change...
|
||||
values.remove(Cols.Package.PACKAGE_NAME);
|
||||
|
||||
int count = db().update(getTableName(), values, query.getSelection(), query.getArgs());
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
getContext().getContentResolver().notifyChange(getHighestPriorityMetadataUri(packageName), null);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@ -140,7 +165,8 @@ public class TempAppProvider extends AppProvider {
|
||||
AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs);
|
||||
switch (MATCHER.match(uri)) {
|
||||
case APPS:
|
||||
selection = selection.add(queryApps(uri.getLastPathSegment()));
|
||||
List<String> segments = uri.getPathSegments();
|
||||
selection = selection.add(queryRepoApps(Long.parseLong(segments.get(1)), segments.get(2)));
|
||||
break;
|
||||
}
|
||||
|
||||
@ -163,7 +189,7 @@ public class TempAppProvider extends AppProvider {
|
||||
db.execSQL("ATTACH DATABASE ':memory:' AS " + DB);
|
||||
db.execSQL(DBHelper.CREATE_TABLE_APP_METADATA.replaceFirst(AppMetadataTable.NAME, DB + "." + getTableName()));
|
||||
db.execSQL(copyData(AppMetadataTable.Cols.ALL_COLS, AppMetadataTable.NAME, DB + "." + getTableName()));
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_id ON " + getTableName() + " (" + AppMetadataTable.Cols.PACKAGE_NAME + ");");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_id ON " + getTableName() + " (" + AppMetadataTable.Cols.PACKAGE_ID + ");");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_upstreamVercode ON " + getTableName() + " (" + AppMetadataTable.Cols.UPSTREAM_VERSION_CODE + ");");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_compatible ON " + getTableName() + " (" + AppMetadataTable.Cols.IS_COMPATIBLE + ");");
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import org.fdroid.fdroid.compat.PackageManagerCompat;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
|
||||
@ -259,7 +260,7 @@ public class InstallManagerService extends Service {
|
||||
App app = getAppFromActive(downloadUrl);
|
||||
if (app == null) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
app = AppProvider.Helper.findByPackageName(resolver, apk.packageName);
|
||||
app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo);
|
||||
}
|
||||
// show notification if app details is not visible
|
||||
if (app != null && AppDetails.isAppVisible(app.packageName)) {
|
||||
@ -346,7 +347,8 @@ public class InstallManagerService extends Service {
|
||||
String name = getAppName(apk);
|
||||
if (TextUtils.isEmpty(name) || name.equals(new App().name)) {
|
||||
ContentResolver resolver = getContentResolver();
|
||||
App app = AppProvider.Helper.findByPackageName(resolver, apk.packageName);
|
||||
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo,
|
||||
new String[] {Schema.AppMetadataTable.Cols.NAME});
|
||||
if (app == null || TextUtils.isEmpty(app.name)) {
|
||||
return; // do not have a name to display, so leave notification as is
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ public class InstallConfirmActivity extends FragmentActivity implements OnCancel
|
||||
intent = getIntent();
|
||||
Uri uri = intent.getData();
|
||||
Apk apk = ApkProvider.Helper.findByUri(this, uri, Schema.ApkTable.Cols.ALL);
|
||||
app = AppProvider.Helper.findByPackageName(getContentResolver(), apk.packageName);
|
||||
app = AppProvider.Helper.findSpecificApp(getContentResolver(), apk.packageName, apk.repo, Schema.AppMetadataTable.Cols.ALL);
|
||||
|
||||
appDiff = new AppDiff(getPackageManager(), apk);
|
||||
|
||||
|
@ -39,7 +39,7 @@ public abstract class AppListFragment extends ListFragment implements
|
||||
|
||||
private static final String[] APP_PROJECTION = {
|
||||
AppMetadataTable.Cols._ID, // Required for cursor loader to work.
|
||||
AppMetadataTable.Cols.PACKAGE_NAME,
|
||||
AppMetadataTable.Cols.Package.PACKAGE_NAME,
|
||||
AppMetadataTable.Cols.NAME,
|
||||
AppMetadataTable.Cols.SUMMARY,
|
||||
AppMetadataTable.Cols.IS_COMPATIBLE,
|
||||
|
@ -289,7 +289,8 @@ public class SwapAppsView extends ListView implements
|
||||
public void onChange(boolean selfChange) {
|
||||
Activity activity = getActivity();
|
||||
if (activity != null) {
|
||||
app = AppProvider.Helper.findByPackageName(getActivity().getContentResolver(), app.packageName);
|
||||
app = AppProvider.Helper.findSpecificApp(getActivity().getContentResolver(),
|
||||
app.packageName, app.repoId, AppMetadataTable.Cols.ALL);
|
||||
resetView();
|
||||
}
|
||||
}
|
||||
@ -318,7 +319,7 @@ public class SwapAppsView extends ListView implements
|
||||
// implemented on API-16, so leaving like this for now.
|
||||
getActivity().getContentResolver().unregisterContentObserver(appObserver);
|
||||
getActivity().getContentResolver().registerContentObserver(
|
||||
AppProvider.getContentUri(this.app.packageName), true, appObserver);
|
||||
AppProvider.getSpecificAppUri(this.app.packageName, this.app.repoId), true, appObserver);
|
||||
}
|
||||
resetView();
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
<!-- enabled -->
|
||||
<item>1</item>
|
||||
<!-- priority -->
|
||||
<item>10</item>
|
||||
<item>1</item>
|
||||
<!-- push requests -->
|
||||
<item>ignore</item>
|
||||
<!-- pubkey -->
|
||||
@ -38,7 +38,7 @@
|
||||
<!-- enabled -->
|
||||
<item>0</item>
|
||||
<!-- priority -->
|
||||
<item>20</item>
|
||||
<item>2</item>
|
||||
<!-- push requests -->
|
||||
<item>ignore</item>
|
||||
<!-- pubkey -->
|
||||
@ -60,7 +60,7 @@
|
||||
<!-- enabled -->
|
||||
<item>0</item>
|
||||
<!-- priority -->
|
||||
<item>10</item>
|
||||
<item>3</item>
|
||||
<!-- push requests -->
|
||||
<item>ignore</item>
|
||||
<!-- pubkey -->
|
||||
@ -81,7 +81,7 @@
|
||||
<!-- enabled -->
|
||||
<item>0</item>
|
||||
<!-- priority -->
|
||||
<item>20</item>
|
||||
<item>4</item>
|
||||
<!-- push requests -->
|
||||
<item>ignore</item>
|
||||
<!-- pubkey -->
|
||||
|
@ -183,7 +183,8 @@ public class Assert {
|
||||
public static App insertApp(Context context, String packageName, String name, ContentValues additionalValues) {
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(AppMetadataTable.Cols.PACKAGE_NAME, packageName);
|
||||
values.put(AppMetadataTable.Cols.REPO_ID, 1);
|
||||
values.put(AppMetadataTable.Cols.Package.PACKAGE_NAME, packageName);
|
||||
values.put(AppMetadataTable.Cols.NAME, name);
|
||||
|
||||
// Required fields (NOT NULL in the database).
|
||||
@ -197,14 +198,14 @@ public class Assert {
|
||||
Uri uri = AppProvider.getContentUri();
|
||||
|
||||
context.getContentResolver().insert(uri, values);
|
||||
return AppProvider.Helper.findByPackageName(context.getContentResolver(), packageName);
|
||||
return AppProvider.Helper.findSpecificApp(context.getContentResolver(), packageName, 1, AppMetadataTable.Cols.ALL);
|
||||
}
|
||||
|
||||
private static App ensureApp(Context context, String packageName) {
|
||||
App app = AppProvider.Helper.findByPackageName(context.getContentResolver(), packageName);
|
||||
public static App ensureApp(Context context, String packageName) {
|
||||
App app = AppProvider.Helper.findSpecificApp(context.getContentResolver(), packageName, 1, AppMetadataTable.Cols.ALL);
|
||||
if (app == null) {
|
||||
insertApp(context, packageName, packageName);
|
||||
app = AppProvider.Helper.findByPackageName(context.getContentResolver(), packageName);
|
||||
app = AppProvider.Helper.findSpecificApp(context.getContentResolver(), packageName, 1, AppMetadataTable.Cols.ALL);
|
||||
}
|
||||
assertNotNull(app);
|
||||
return app;
|
||||
|
@ -1,155 +0,0 @@
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/*
|
||||
At time fo writing, the following tests did not pass. This is because the multi-repo support
|
||||
in F-Droid was not sufficient. When working on proper multi repo support than this should be
|
||||
uncommented and all these tests will be required to pass:
|
||||
|
||||
@Config(constants = BuildConfig.class)
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ProperMultiRepoUpdaterTest extends MultiRepoUpdaterTest {
|
||||
private static final String TAG = "ProperMultiRepoSupport";
|
||||
|
||||
/*@Test
|
||||
public void testCorrectConflictingThenMainThenArchive() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateConflicting() && updateMain() && updateArchive()) {
|
||||
assertExpected();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectConflictingThenArchiveThenMain() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateConflicting() && updateArchive() && updateMain()) {
|
||||
assertExpected();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectArchiveThenMainThenConflicting() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateArchive() && updateMain() && updateConflicting()) {
|
||||
assertExpected();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectArchiveThenConflictingThenMain() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateArchive() && updateConflicting() && updateMain()) {
|
||||
assertExpected();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectMainThenArchiveThenConflicting() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateMain() && updateArchive() && updateConflicting()) {
|
||||
assertExpected();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectMainThenConflictingThenArchive() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateMain() && updateConflicting() && updateArchive()) {
|
||||
assertExpected();
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Check that all of the expected apps and apk versions are available in the database. This
|
||||
* check will take into account the repository the apks came from, to ensure that each
|
||||
* repository indeed contains the apks that it said it would provide.
|
||||
*/
|
||||
private void assertExpected() {
|
||||
Log.i(TAG, "Asserting all versions of each .apk are in index.");
|
||||
List<Repo> repos = RepoProvider.Helper.all(context);
|
||||
assertEquals("Repos", 3, repos.size());
|
||||
|
||||
assertMainRepo(repos);
|
||||
assertMainArchiveRepo(repos);
|
||||
assertConflictingRepo(repos);
|
||||
}
|
||||
|
||||
/**
|
||||
* + 2048 (com.uberspot.a2048)
|
||||
* - Version 1.96 (19)
|
||||
* - Version 1.95 (18)
|
||||
* + AdAway (org.adaway)
|
||||
* - Version 3.0.2 (54)
|
||||
* - Version 3.0.1 (53)
|
||||
* - Version 3.0 (52)
|
||||
* + adbWireless (siir.es.adbWireless)
|
||||
* - Version 1.5.4 (12)
|
||||
*/
|
||||
private void assertMainRepo(List<Repo> allRepos) {
|
||||
Repo repo = findRepo(REPO_MAIN, allRepos);
|
||||
|
||||
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
|
||||
assertEquals("Apks for main repo", apks.size(), 6);
|
||||
assertApksExist(apks, "com.uberspot.a2048", new int[]{18, 19});
|
||||
assertApksExist(apks, "org.adaway", new int[]{52, 53, 54});
|
||||
assertApksExist(apks, "siir.es.adbWireless", new int[]{12});
|
||||
}
|
||||
|
||||
/**
|
||||
* + AdAway (org.adaway)
|
||||
* - Version 2.9.2 (51)
|
||||
* - Version 2.9.1 (50)
|
||||
* - Version 2.9 (49)
|
||||
* - Version 2.8.1 (48)
|
||||
* - Version 2.8 (47)
|
||||
* - Version 2.7 (46)
|
||||
* - Version 2.6 (45)
|
||||
* - Version 2.3 (42)
|
||||
* - Version 2.1 (40)
|
||||
* - Version 1.37 (38)
|
||||
* - Version 1.36 (37)
|
||||
* - Version 1.35 (36)
|
||||
* - Version 1.34 (35)
|
||||
*/
|
||||
private void assertMainArchiveRepo(List<Repo> allRepos) {
|
||||
Repo repo = findRepo(REPO_ARCHIVE, allRepos);
|
||||
|
||||
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
|
||||
assertEquals("Apks for main archive repo", 13, apks.size());
|
||||
assertApksExist(apks, "org.adaway", new int[]{35, 36, 37, 38, 40, 42, 45, 46, 47, 48, 49, 50, 51});
|
||||
}
|
||||
|
||||
/**
|
||||
* + AdAway (org.adaway)
|
||||
* - Version 3.0.1 (53) *
|
||||
* - Version 3.0 (52) *
|
||||
* - Version 2.9.2 (51) *
|
||||
* - Version 2.2.1 (50) *
|
||||
* + Add to calendar (org.dgtale.icsimport)
|
||||
* - Version 1.2 (3)
|
||||
* - Version 1.1 (2)
|
||||
*/
|
||||
private void assertConflictingRepo(List<Repo> allRepos) {
|
||||
Repo repo = findRepo(REPO_CONFLICTING, allRepos);
|
||||
|
||||
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
|
||||
assertEquals("Apks for main repo", 6, apks.size());
|
||||
assertApksExist(apks, "org.adaway", new int[]{50, 51, 52, 53});
|
||||
assertApksExist(apks, "org.dgtale.icsimport", new int[]{2, 3});
|
||||
}
|
||||
|
||||
}
|
@ -242,41 +242,45 @@ public class ApkProviderTest extends FDroidProviderTest {
|
||||
@Test
|
||||
public void testKnownApks() {
|
||||
|
||||
App fdroid = Assert.ensureApp(context, "org.fdroid.fdroid");
|
||||
for (int i = 0; i < 7; i++) {
|
||||
Assert.insertApk(context, "org.fdroid.fdroid", i);
|
||||
Assert.insertApk(context, fdroid, i);
|
||||
}
|
||||
|
||||
App exampleOrg = Assert.ensureApp(context, "org.example");
|
||||
for (int i = 0; i < 9; i++) {
|
||||
Assert.insertApk(context, "org.example", i);
|
||||
Assert.insertApk(context, exampleOrg, i);
|
||||
}
|
||||
|
||||
App exampleCom = Assert.ensureApp(context, "com.example");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
Assert.insertApk(context, "com.example", i);
|
||||
Assert.insertApk(context, exampleCom, i);
|
||||
}
|
||||
|
||||
Assert.insertApk(context, "com.apk.thingo", 1);
|
||||
App thingo = Assert.ensureApp(context, "com.apk.thingo");
|
||||
Assert.insertApk(context, thingo, 1);
|
||||
|
||||
Apk[] known = {
|
||||
new MockApk("org.fdroid.fdroid", 1),
|
||||
new MockApk("org.fdroid.fdroid", 3),
|
||||
new MockApk("org.fdroid.fdroid", 5),
|
||||
new MockApk(fdroid, 1),
|
||||
new MockApk(fdroid, 3),
|
||||
new MockApk(fdroid, 5),
|
||||
|
||||
new MockApk("com.example", 1),
|
||||
new MockApk("com.example", 2),
|
||||
new MockApk(exampleCom, 1),
|
||||
new MockApk(exampleCom, 2),
|
||||
};
|
||||
|
||||
Apk[] unknown = {
|
||||
new MockApk("org.fdroid.fdroid", 7),
|
||||
new MockApk("org.fdroid.fdroid", 9),
|
||||
new MockApk("org.fdroid.fdroid", 11),
|
||||
new MockApk("org.fdroid.fdroid", 13),
|
||||
new MockApk(fdroid, 7),
|
||||
new MockApk(fdroid, 9),
|
||||
new MockApk(fdroid, 11),
|
||||
new MockApk(fdroid, 13),
|
||||
|
||||
new MockApk("com.example", 3),
|
||||
new MockApk("com.example", 4),
|
||||
new MockApk("com.example", 5),
|
||||
new MockApk(exampleCom, 3),
|
||||
new MockApk(exampleCom, 4),
|
||||
new MockApk(exampleCom, 5),
|
||||
|
||||
new MockApk("info.example", 1),
|
||||
new MockApk("info.example", 2),
|
||||
new MockApk(-10, 1),
|
||||
new MockApk(-10, 2),
|
||||
};
|
||||
|
||||
List<Apk> apksToCheck = new ArrayList<>(known.length + unknown.length);
|
||||
@ -284,7 +288,8 @@ public class ApkProviderTest extends FDroidProviderTest {
|
||||
Collections.addAll(apksToCheck, unknown);
|
||||
|
||||
String[] projection = {
|
||||
Cols.App.PACKAGE_NAME,
|
||||
Cols.Package.PACKAGE_NAME,
|
||||
Cols.APP_ID,
|
||||
Cols.VERSION_CODE,
|
||||
};
|
||||
|
||||
@ -424,7 +429,7 @@ public class ApkProviderTest extends FDroidProviderTest {
|
||||
assertEquals("a hash type", apk.hashType);
|
||||
|
||||
String[] projection = {
|
||||
Cols.App.PACKAGE_NAME,
|
||||
Cols.Package.PACKAGE_NAME,
|
||||
Cols.HASH,
|
||||
};
|
||||
|
||||
|
@ -68,7 +68,7 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
|
||||
@Test
|
||||
public void testCantFindApp() {
|
||||
assertNull(AppProvider.Helper.findByPackageName(context.getContentResolver(), "com.example.doesnt-exist"));
|
||||
assertNull(AppProvider.Helper.findSpecificApp(context.getContentResolver(), "com.example.doesnt-exist", 1, Cols.ALL));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -111,14 +111,14 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
ContentResolver r = context.getContentResolver();
|
||||
|
||||
// Can't "update", although can "install"...
|
||||
App notInstalled = AppProvider.Helper.findByPackageName(r, "not installed");
|
||||
App notInstalled = AppProvider.Helper.findSpecificApp(r, "not installed", 1, Cols.ALL);
|
||||
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");
|
||||
App installedAlreadyLatestIgnoreAll = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore all");
|
||||
App installedAlreadyLatestIgnoreLatest = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore latest");
|
||||
App installedAlreadyLatestIgnoreOld = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore old");
|
||||
App installedOnlyOneVersionAvailable = AppProvider.Helper.findSpecificApp(r, "installed, only one version available", 1, Cols.ALL);
|
||||
App installedAlreadyLatestNoIgnore = AppProvider.Helper.findSpecificApp(r, "installed, already latest, no ignore", 1, Cols.ALL);
|
||||
App installedAlreadyLatestIgnoreAll = AppProvider.Helper.findSpecificApp(r, "installed, already latest, ignore all", 1, Cols.ALL);
|
||||
App installedAlreadyLatestIgnoreLatest = AppProvider.Helper.findSpecificApp(r, "installed, already latest, ignore latest", 1, Cols.ALL);
|
||||
App installedAlreadyLatestIgnoreOld = AppProvider.Helper.findSpecificApp(r, "installed, already latest, ignore old", 1, Cols.ALL);
|
||||
|
||||
assertFalse(installedOnlyOneVersionAvailable.canAndWantToUpdate(context));
|
||||
assertFalse(installedAlreadyLatestNoIgnore.canAndWantToUpdate(context));
|
||||
@ -126,10 +126,10 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
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");
|
||||
App installedOldNoIgnore = AppProvider.Helper.findSpecificApp(r, "installed, old version, no ignore", 1, Cols.ALL);
|
||||
App installedOldIgnoreAll = AppProvider.Helper.findSpecificApp(r, "installed, old version, ignore all", 1, Cols.ALL);
|
||||
App installedOldIgnoreLatest = AppProvider.Helper.findSpecificApp(r, "installed, old version, ignore latest", 1, Cols.ALL);
|
||||
App installedOldIgnoreNewerNotLatest = AppProvider.Helper.findSpecificApp(r, "installed, old version, ignore newer, but not latest", 1, Cols.ALL);
|
||||
|
||||
assertTrue(installedOldNoIgnore.canAndWantToUpdate(context));
|
||||
assertFalse(installedOldIgnoreAll.canAndWantToUpdate(context));
|
||||
@ -169,7 +169,7 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
|
||||
assertResultCount(contentResolver, 10, AppProvider.getContentUri(), PROJ);
|
||||
|
||||
String[] projection = {Cols.PACKAGE_NAME};
|
||||
String[] projection = {Cols.Package.PACKAGE_NAME};
|
||||
List<App> canUpdateApps = AppProvider.Helper.findCanUpdate(context, projection);
|
||||
|
||||
String[] expectedCanUpdate = {
|
||||
@ -239,7 +239,7 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
assertEquals("org.fdroid.fdroid", app.packageName);
|
||||
assertEquals("F-Droid", app.name);
|
||||
|
||||
App otherApp = AppProvider.Helper.findByPackageName(context.getContentResolver(), "org.fdroid.fdroid");
|
||||
App otherApp = AppProvider.Helper.findSpecificApp(context.getContentResolver(), "org.fdroid.fdroid", 1, Cols.ALL);
|
||||
assertNotNull(otherApp);
|
||||
assertEquals("org.fdroid.fdroid", otherApp.packageName);
|
||||
assertEquals("F-Droid", otherApp.name);
|
||||
@ -260,7 +260,7 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
String[] projection = new String[] {
|
||||
Cols._ID,
|
||||
Cols.NAME,
|
||||
Cols.PACKAGE_NAME,
|
||||
Cols.Package.PACKAGE_NAME,
|
||||
};
|
||||
return contentResolver.query(AppProvider.getContentUri(), projection, null, null, null);
|
||||
}
|
||||
@ -356,7 +356,8 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
public App insertApp(String id, String name, ContentValues additionalValues) {
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Cols.PACKAGE_NAME, id);
|
||||
values.put(Cols.Package.PACKAGE_NAME, id);
|
||||
values.put(Cols.REPO_ID, 1);
|
||||
values.put(Cols.NAME, name);
|
||||
|
||||
// Required fields (NOT NULL in the database).
|
||||
@ -370,6 +371,9 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
Uri uri = AppProvider.getContentUri();
|
||||
|
||||
contentResolver.insert(uri, values);
|
||||
return AppProvider.Helper.findByPackageName(context.getContentResolver(), id);
|
||||
|
||||
AppProvider.Helper.recalculatePreferredMetadata(context);
|
||||
|
||||
return AppProvider.Helper.findSpecificApp(context.getContentResolver(), id, 1, Cols.ALL);
|
||||
}
|
||||
}
|
||||
|
@ -93,10 +93,10 @@ public class ProviderUriTests {
|
||||
assertValidUri(resolver, AppProvider.getCanUpdateUri(), "content://org.fdroid.fdroid.data.AppProvider/canUpdate", projection);
|
||||
|
||||
App app = new App();
|
||||
app.repoId = 1;
|
||||
app.packageName = "org.fdroid.fdroid";
|
||||
|
||||
assertValidUri(resolver, AppProvider.getContentUri(app), "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);
|
||||
assertValidUri(resolver, AppProvider.getSpecificAppUri(app.packageName, app.repoId), "content://org.fdroid.fdroid.data.AppProvider/app/1/org.fdroid.fdroid", projection);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -112,7 +112,7 @@ public class ProviderUriTests {
|
||||
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.getAppsUri(packageNames, 1), "content://org.fdroid.fdroid.data.TempAppProvider/apps/1/org.fdroid.fdroid%2Ccom.example.com", projection);
|
||||
assertValidUri(resolver, TempAppProvider.getContentUri(), "content://org.fdroid.fdroid.data.TempAppProvider", projection);
|
||||
}
|
||||
|
||||
@ -135,9 +135,9 @@ public class ProviderUriTests {
|
||||
|
||||
assertValidUri(resolver, ApkProvider.getContentUri(), "content://org.fdroid.fdroid.data.ApkProvider", projection);
|
||||
assertValidUri(resolver, ApkProvider.getAppUri("org.fdroid.fdroid"), "content://org.fdroid.fdroid.data.ApkProvider/app/org.fdroid.fdroid", projection);
|
||||
assertValidUri(resolver, ApkProvider.getApkFromAnyRepoUri(new MockApk("org.fdroid.fdroid", 100)), "content://org.fdroid.fdroid.data.ApkProvider/apk/100/org.fdroid.fdroid", projection);
|
||||
assertValidUri(resolver, ApkProvider.getApkFromAnyRepoUri(new MockApk("org.fdroid.fdroid", 100)), "content://org.fdroid.fdroid.data.ApkProvider/apk-any-repo/100/org.fdroid.fdroid", projection);
|
||||
assertValidUri(resolver, ApkProvider.getContentUri(apks), projection);
|
||||
assertValidUri(resolver, ApkProvider.getApkFromAnyRepoUri("org.fdroid.fdroid", 100), "content://org.fdroid.fdroid.data.ApkProvider/apk/100/org.fdroid.fdroid", projection);
|
||||
assertValidUri(resolver, ApkProvider.getApkFromAnyRepoUri("org.fdroid.fdroid", 100), "content://org.fdroid.fdroid.data.ApkProvider/apk-any-repo/100/org.fdroid.fdroid", projection);
|
||||
assertValidUri(resolver, ApkProvider.getRepoUri(1000), "content://org.fdroid.fdroid.data.ApkProvider/repo/1000", projection);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.fdroid.fdroid.mock;
|
||||
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
|
||||
public class MockApk extends Apk {
|
||||
|
||||
@ -9,4 +10,14 @@ public class MockApk extends Apk {
|
||||
this.versionCode = versionCode;
|
||||
}
|
||||
|
||||
public MockApk(App app, int versionCode) {
|
||||
this.appId = app.getId();
|
||||
this.versionCode = versionCode;
|
||||
}
|
||||
|
||||
public MockApk(long appId, int versionCode) {
|
||||
this.appId = appId;
|
||||
this.versionCode = versionCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
package org.fdroid.fdroid.updater;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.RepoUpdater.UpdateException;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
import org.fdroid.fdroid.data.Schema.RepoTable.Cols;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
@ -39,49 +40,67 @@ public class AcceptableMultiRepoUpdaterTest extends MultiRepoUpdaterTest {
|
||||
@Test
|
||||
public void testAcceptableConflictingThenMainThenArchive() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateConflicting() && updateMain() && updateArchive()) {
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
updateConflicting();
|
||||
updateMain();
|
||||
updateArchive();
|
||||
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptableConflictingThenArchiveThenMain() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateConflicting() && updateArchive() && updateMain()) {
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
updateConflicting();
|
||||
updateArchive();
|
||||
updateMain();
|
||||
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptableArchiveThenMainThenConflicting() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateArchive() && updateMain() && updateConflicting()) {
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
updateArchive();
|
||||
updateMain();
|
||||
updateConflicting();
|
||||
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptableArchiveThenConflictingThenMain() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateArchive() && updateConflicting() && updateMain()) {
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
updateArchive();
|
||||
updateConflicting();
|
||||
updateMain();
|
||||
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptableMainThenArchiveThenConflicting() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateMain() && updateArchive() && updateConflicting()) {
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
updateMain();
|
||||
updateArchive();
|
||||
updateConflicting();
|
||||
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptableMainThenConflictingThenArchive() throws UpdateException {
|
||||
assertEmpty();
|
||||
if (updateMain() && updateConflicting() && updateArchive()) {
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
updateMain();
|
||||
updateConflicting();
|
||||
updateArchive();
|
||||
|
||||
assertSomewhatAcceptable();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -128,7 +147,7 @@ public class AcceptableMultiRepoUpdaterTest extends MultiRepoUpdaterTest {
|
||||
|
||||
private void disableRepo(Repo repo) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(Schema.RepoTable.Cols.IN_USE, 0);
|
||||
values.put(Cols.IN_USE, 0);
|
||||
RepoProvider.Helper.update(context, repo, values);
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
package org.fdroid.fdroid;
|
||||
package org.fdroid.fdroid.updater;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.RepoUpdater;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.Repo;
|
@ -1,12 +1,15 @@
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
package org.fdroid.fdroid.updater;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.RepoUpdater;
|
||||
import org.fdroid.fdroid.RepoUpdater.UpdateException;
|
||||
import org.fdroid.fdroid.TestUtils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
@ -162,7 +165,7 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest {
|
||||
repo.address = uri;
|
||||
repo.name = name;
|
||||
|
||||
ContentValues values = new ContentValues(2);
|
||||
ContentValues values = new ContentValues(3);
|
||||
values.put(Schema.RepoTable.Cols.SIGNING_CERT, repo.signingCertificate);
|
||||
values.put(Schema.RepoTable.Cols.ADDRESS, repo.address);
|
||||
values.put(Schema.RepoTable.Cols.NAME, repo.name);
|
||||
@ -178,19 +181,19 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest {
|
||||
return new RepoUpdater(context, createRepo(name, uri, context));
|
||||
}
|
||||
|
||||
protected boolean updateConflicting() throws UpdateException {
|
||||
return updateRepo(createUpdater(REPO_CONFLICTING, REPO_CONFLICTING_URI, context), "multiRepo.conflicting.jar");
|
||||
protected void updateConflicting() throws UpdateException {
|
||||
updateRepo(createUpdater(REPO_CONFLICTING, REPO_CONFLICTING_URI, context), "multiRepo.conflicting.jar");
|
||||
}
|
||||
|
||||
protected boolean updateMain() throws UpdateException {
|
||||
return updateRepo(createUpdater(REPO_MAIN, REPO_MAIN_URI, context), "multiRepo.normal.jar");
|
||||
protected void updateMain() throws UpdateException {
|
||||
updateRepo(createUpdater(REPO_MAIN, REPO_MAIN_URI, context), "multiRepo.normal.jar");
|
||||
}
|
||||
|
||||
protected boolean updateArchive() throws UpdateException {
|
||||
return updateRepo(createUpdater(REPO_ARCHIVE, REPO_ARCHIVE_URI, context), "multiRepo.archive.jar");
|
||||
protected void updateArchive() throws UpdateException {
|
||||
updateRepo(createUpdater(REPO_ARCHIVE, REPO_ARCHIVE_URI, context), "multiRepo.archive.jar");
|
||||
}
|
||||
|
||||
protected boolean updateRepo(RepoUpdater updater, String indexJarPath) throws UpdateException {
|
||||
protected void updateRepo(RepoUpdater updater, String indexJarPath) throws UpdateException {
|
||||
File indexJar = TestUtils.copyResourceToTempFile(indexJarPath);
|
||||
try {
|
||||
updater.processDownloadedFile(indexJar);
|
||||
@ -199,7 +202,6 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest {
|
||||
indexJar.delete();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,390 @@
|
||||
|
||||
package org.fdroid.fdroid.updater;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.support.annotation.StringDef;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.RepoUpdater;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
||||
import org.fdroid.fdroid.data.Schema.RepoTable.Cols;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
// TODO: Use sdk=24 when Robolectric supports this
|
||||
@Config(constants = BuildConfig.class, sdk = 23)
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
public class ProperMultiRepoUpdaterTest extends MultiRepoUpdaterTest {
|
||||
private static final String TAG = "ProperMultiRepoSupport";
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@StringDef({"Conflicting", "Normal"})
|
||||
public @interface RepoIdentifier { }
|
||||
|
||||
/*
|
||||
*This test fails due to issue #568 (https://gitlab.com/fdroid/fdroidclient/issues/568).
|
||||
@Test
|
||||
public void appsRemovedFromRepo() throws RepoUpdater.UpdateException {
|
||||
assertEquals(0, AppProvider.Helper.all(context.getContentResolver()).size());
|
||||
|
||||
updateMain();
|
||||
Repo repo = RepoProvider.Helper.findByAddress(context, REPO_MAIN_URI);
|
||||
|
||||
assertEquals(3, AppProvider.Helper.all(context.getContentResolver()).size());
|
||||
assertEquals(6, ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL).size());
|
||||
assertEquals(3, ApkProvider.Helper.findByPackageName(context, "org.adaway").size());
|
||||
assertEquals(2, ApkProvider.Helper.findByPackageName(context, "com.uberspot.a2048").size());
|
||||
assertEquals(1, ApkProvider.Helper.findByPackageName(context, "siir.es.adbWireless").size());
|
||||
|
||||
RepoUpdater updater = new RepoUpdater(context, RepoProvider.Helper.findByAddress(context, repo.address));
|
||||
updateRepo(updater, "multiRepo.conflicting.jar");
|
||||
|
||||
assertEquals(2, AppProvider.Helper.all(context.getContentResolver()).size());
|
||||
assertEquals(6, ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL).size());
|
||||
assertEquals(4, ApkProvider.Helper.findByPackageName(context, "org.adaway").size());
|
||||
assertEquals(2, ApkProvider.Helper.findByPackageName(context, "org.dgtale.icsimport").size());
|
||||
}*/
|
||||
|
||||
@Test
|
||||
public void mainRepo() throws RepoUpdater.UpdateException {
|
||||
assertEmpty();
|
||||
updateMain();
|
||||
assertMainRepo();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void archiveRepo() throws RepoUpdater.UpdateException {
|
||||
assertEmpty();
|
||||
updateArchive();
|
||||
assertMainArchiveRepoMetadata();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void conflictingRepo() throws RepoUpdater.UpdateException {
|
||||
assertEmpty();
|
||||
updateConflicting();
|
||||
assertConflictingRepo();
|
||||
}
|
||||
|
||||
private Map<String, App> allApps() {
|
||||
List<App> apps = AppProvider.Helper.all(context.getContentResolver());
|
||||
Map<String, App> appsIndexedByPackageName = new HashMap<>(apps.size());
|
||||
for (App app : apps) {
|
||||
appsIndexedByPackageName.put(app.packageName, app);
|
||||
}
|
||||
return appsIndexedByPackageName;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void metadataWithRepoPriority() throws RepoUpdater.UpdateException {
|
||||
updateConflicting();
|
||||
updateMain();
|
||||
updateArchive();
|
||||
|
||||
Repo conflictingRepo = RepoProvider.Helper.findByAddress(context, REPO_CONFLICTING_URI);
|
||||
|
||||
assertEquals(1, conflictingRepo.priority);
|
||||
assertEquals(2, RepoProvider.Helper.findByAddress(context, REPO_MAIN_URI).priority);
|
||||
assertEquals(3, RepoProvider.Helper.findByAddress(context, REPO_ARCHIVE_URI).priority);
|
||||
|
||||
assertMainRepo();
|
||||
assertMainArchiveRepoMetadata();
|
||||
assertConflictingRepo();
|
||||
|
||||
assertRepoTakesPriority("Conflicting");
|
||||
|
||||
// Make the conflicting repo less important than the main repo.
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(Cols.PRIORITY, 5);
|
||||
RepoProvider.Helper.update(context, conflictingRepo, values);
|
||||
Repo updatedConflictingRepo = RepoProvider.Helper.findByAddress(context, REPO_CONFLICTING_URI);
|
||||
assertEquals(5, updatedConflictingRepo.priority);
|
||||
|
||||
assertRepoTakesPriority("Normal");
|
||||
}
|
||||
|
||||
private void assertRepoTakesPriority(@RepoIdentifier String higherPriority) {
|
||||
Map<String, App> allApps = allApps();
|
||||
|
||||
// Provided by both the "Main" and "Conflicting" repo, so need to fetch metdata from the
|
||||
// repo with the higher "Conflicting" repo has a higher priority.
|
||||
App adAway = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), "org.adaway");
|
||||
assertAdAwayMetadata(adAway, higherPriority);
|
||||
assertAdAwayMetadata(allApps.get("org.adaway"), higherPriority);
|
||||
|
||||
|
||||
// This is only provided by the "Main" or "Archive" repo. Both the main and archive repo both
|
||||
// pull their metadata from the same build recipe in fdroidserver. The only difference is that
|
||||
// the archive repository contains .apks from further back, but their metadata is the same.
|
||||
App a2048 = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), "com.uberspot.a2048");
|
||||
assert2048Metadata(a2048, "Normal");
|
||||
assert2048Metadata(allApps.get("com.uberspot.a2048"), "Normal");
|
||||
|
||||
// This is only provided by the "Conflicting" repo.
|
||||
App calendar = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), "org.dgtale.icsimport");
|
||||
assertCalendarMetadata(calendar, "Conflicting");
|
||||
assertCalendarMetadata(allApps.get("org.dgtale.icsimport"), "Conflicting");
|
||||
|
||||
// This is only provided by the "Main" repo.
|
||||
App adb = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), "siir.es.adbWireless");
|
||||
assertAdbMetadata(adb, "Normal");
|
||||
assertAdbMetadata(allApps.get("siir.es.adbWireless"), "Normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectConflictingThenMainThenArchive() throws RepoUpdater.UpdateException {
|
||||
assertEmpty();
|
||||
|
||||
updateConflicting();
|
||||
updateMain();
|
||||
updateArchive();
|
||||
|
||||
assertExpected();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectConflictingThenArchiveThenMain() throws RepoUpdater.UpdateException {
|
||||
assertEmpty();
|
||||
|
||||
updateConflicting();
|
||||
updateArchive();
|
||||
updateMain();
|
||||
|
||||
assertExpected();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectArchiveThenMainThenConflicting() throws RepoUpdater.UpdateException {
|
||||
assertEmpty();
|
||||
|
||||
updateArchive();
|
||||
updateMain();
|
||||
updateConflicting();
|
||||
|
||||
assertExpected();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectArchiveThenConflictingThenMain() throws RepoUpdater.UpdateException {
|
||||
assertEmpty();
|
||||
|
||||
updateArchive();
|
||||
updateConflicting();
|
||||
updateMain();
|
||||
|
||||
assertExpected();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectMainThenArchiveThenConflicting() throws RepoUpdater.UpdateException {
|
||||
assertEmpty();
|
||||
|
||||
updateMain();
|
||||
updateArchive();
|
||||
updateConflicting();
|
||||
|
||||
assertExpected();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectMainThenConflictingThenArchive() throws RepoUpdater.UpdateException {
|
||||
assertEmpty();
|
||||
|
||||
updateMain();
|
||||
updateConflicting();
|
||||
updateArchive();
|
||||
|
||||
assertExpected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that all of the expected apps and apk versions are available in the database. This
|
||||
* check will take into account the repository the apks came from, to ensure that each
|
||||
* repository indeed contains the apks that it said it would provide.
|
||||
*/
|
||||
private void assertExpected() {
|
||||
Log.i(TAG, "Asserting all versions of each .apk are in index.");
|
||||
List<Repo> repos = RepoProvider.Helper.all(context);
|
||||
assertEquals("Repos", 3, repos.size());
|
||||
|
||||
assertMainRepo(repos);
|
||||
assertMainArchiveRepoMetadata(repos);
|
||||
assertConflictingRepo(repos);
|
||||
}
|
||||
|
||||
private void assertMainRepo() {
|
||||
assertMainRepo(RepoProvider.Helper.all(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* + 2048 (com.uberspot.a2048)
|
||||
* - Version 1.96 (19)
|
||||
* - Version 1.95 (18)
|
||||
* + AdAway (org.adaway)
|
||||
* - Version 3.0.2 (54)
|
||||
* - Version 3.0.1 (53)
|
||||
* - Version 3.0 (52)
|
||||
* + adbWireless (siir.es.adbWireless)
|
||||
* - Version 1.5.4 (12)
|
||||
*/
|
||||
private void assertMainRepo(List<Repo> allRepos) {
|
||||
Repo repo = findRepo(REPO_MAIN, allRepos);
|
||||
|
||||
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
|
||||
assertEquals("Apks for main repo", apks.size(), 6);
|
||||
assertApksExist(apks, "com.uberspot.a2048", new int[]{18, 19});
|
||||
assertApksExist(apks, "org.adaway", new int[]{52, 53, 54});
|
||||
assertApksExist(apks, "siir.es.adbWireless", new int[]{12});
|
||||
|
||||
assert2048Metadata(repo, "Normal");
|
||||
assertAdAwayMetadata(repo, "Normal");
|
||||
assertAdbMetadata(repo, "Normal");
|
||||
}
|
||||
|
||||
private void assert2048Metadata(Repo repo, @RepoIdentifier String id) {
|
||||
App a2048 = AppProvider.Helper.findSpecificApp(context.getContentResolver(), "com.uberspot.a2048", repo.getId(), AppMetadataTable.Cols.ALL);
|
||||
assert2048Metadata(a2048, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id An identifier that we've put in the metadata for each repo to ensure that
|
||||
* we can identify the metadata is coming from the correct repo.
|
||||
*/
|
||||
private void assert2048Metadata(App a2048, @RepoIdentifier String id) {
|
||||
assertNotNull(a2048);
|
||||
assertEquals("2048", a2048.name);
|
||||
assertEquals(String.format("<p>2048 from %s repo.</p>", id), a2048.description);
|
||||
assertEquals(String.format("Puzzle game (%s)", id), a2048.summary);
|
||||
assertEquals(String.format("https://github.com/uberspot/2048-android?%s", id), a2048.webURL);
|
||||
assertEquals(String.format("https://github.com/uberspot/2048-android?code&%s", id), a2048.sourceURL);
|
||||
assertEquals(String.format("https://github.com/uberspot/2048-android/issues?%s", id), a2048.trackerURL);
|
||||
}
|
||||
|
||||
private void assertAdAwayMetadata(Repo repo, @RepoIdentifier String id) {
|
||||
App adaway = AppProvider.Helper.findSpecificApp(context.getContentResolver(), "org.adaway", repo.getId(), AppMetadataTable.Cols.ALL);
|
||||
assertAdAwayMetadata(adaway, id);
|
||||
}
|
||||
|
||||
/** @see ProperMultiRepoUpdaterTest#assert2048Metadata(Repo, String) */
|
||||
private void assertAdAwayMetadata(App adaway, @RepoIdentifier String id) {
|
||||
assertNotNull(adaway);
|
||||
assertEquals(String.format("AdAway", id), adaway.name);
|
||||
assertEquals(String.format("<p>AdAway from %s repo.</p>", id), adaway.description);
|
||||
assertEquals(String.format("Block advertisements (%s)", id), adaway.summary);
|
||||
assertEquals(String.format("http://sufficientlysecure.org/index.php/adaway?%s", id), adaway.webURL);
|
||||
assertEquals(String.format("https://github.com/dschuermann/ad-away?%s", id), adaway.sourceURL);
|
||||
assertEquals(String.format("https://github.com/dschuermann/ad-away/issues?%s", id), adaway.trackerURL);
|
||||
assertEquals(String.format("https://github.com/dschuermann/ad-away/raw/HEAD/CHANGELOG?%s", id), adaway.changelogURL);
|
||||
assertEquals(String.format("http://sufficientlysecure.org/index.php/adaway?%s", id), adaway.donateURL);
|
||||
assertEquals(String.format("369138", id), adaway.flattrID);
|
||||
}
|
||||
|
||||
private void assertAdbMetadata(Repo repo, @RepoIdentifier String id) {
|
||||
App adb = AppProvider.Helper.findSpecificApp(context.getContentResolver(), "siir.es.adbWireless", repo.getId(), AppMetadataTable.Cols.ALL);
|
||||
assertAdbMetadata(adb, id);
|
||||
}
|
||||
|
||||
/** @see ProperMultiRepoUpdaterTest#assert2048Metadata(Repo, String) */
|
||||
private void assertAdbMetadata(App adb, @RepoIdentifier String id) {
|
||||
assertNotNull(adb);
|
||||
assertEquals("adbWireless", adb.name);
|
||||
assertEquals(String.format("<p>adbWireless from %s repo.</p>", id), adb.description);
|
||||
assertEquals(String.format("Wireless adb (%s)", id), adb.summary);
|
||||
assertEquals(String.format("https://adbwireless.example.com?%s", id), adb.webURL);
|
||||
assertEquals(String.format("https://adbwireless.example.com/source?%s", id), adb.sourceURL);
|
||||
assertEquals(String.format("https://adbwireless.example.com/issues?%s", id), adb.trackerURL);
|
||||
}
|
||||
|
||||
private void assertCalendarMetadata(Repo repo, @RepoIdentifier String id) {
|
||||
App calendar = AppProvider.Helper.findSpecificApp(context.getContentResolver(), "org.dgtale.icsimport", repo.getId(), AppMetadataTable.Cols.ALL);
|
||||
assertCalendarMetadata(calendar, id);
|
||||
}
|
||||
|
||||
/** @see ProperMultiRepoUpdaterTest#assert2048Metadata(Repo, String) */
|
||||
private void assertCalendarMetadata(App calendar, @RepoIdentifier String id) {
|
||||
assertNotNull(calendar);
|
||||
assertEquals("Add to calendar", calendar.name);
|
||||
assertEquals(String.format("<p>Add to calendar from %s repo.</p>", id), calendar.description);
|
||||
assertEquals(String.format("Import .ics files into calendar (%s)", id), calendar.summary);
|
||||
assertEquals(String.format("https://github.com/danielegobbetti/ICSImport/blob/HEAD/README.md?%s", id), calendar.webURL);
|
||||
assertEquals(String.format("https://github.com/danielegobbetti/ICSImport?%s", id), calendar.sourceURL);
|
||||
assertEquals(String.format("https://github.com/danielegobbetti/ICSImport/issues?%s", id), calendar.trackerURL);
|
||||
assertEquals("2225390", calendar.flattrID);
|
||||
}
|
||||
|
||||
private void assertMainArchiveRepoMetadata() {
|
||||
assertMainArchiveRepoMetadata(RepoProvider.Helper.all(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* + AdAway (org.adaway)
|
||||
* - Version 2.9.2 (51)
|
||||
* - Version 2.9.1 (50)
|
||||
* - Version 2.9 (49)
|
||||
* - Version 2.8.1 (48)
|
||||
* - Version 2.8 (47)
|
||||
* - Version 2.7 (46)
|
||||
* - Version 2.6 (45)
|
||||
* - Version 2.3 (42)
|
||||
* - Version 2.1 (40)
|
||||
* - Version 1.37 (38)
|
||||
* - Version 1.36 (37)
|
||||
* - Version 1.35 (36)
|
||||
* - Version 1.34 (35)
|
||||
*/
|
||||
private void assertMainArchiveRepoMetadata(List<Repo> allRepos) {
|
||||
Repo repo = findRepo(REPO_ARCHIVE, allRepos);
|
||||
|
||||
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
|
||||
assertEquals("Apks for main archive repo", 13, apks.size());
|
||||
assertApksExist(apks, "org.adaway", new int[]{35, 36, 37, 38, 40, 42, 45, 46, 47, 48, 49, 50, 51});
|
||||
|
||||
assertAdAwayMetadata(repo, "Normal");
|
||||
}
|
||||
|
||||
private void assertConflictingRepo() {
|
||||
assertConflictingRepo(RepoProvider.Helper.all(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* + AdAway (org.adaway)
|
||||
* - Version 3.0.1 (53) *
|
||||
* - Version 3.0 (52) *
|
||||
* - Version 2.9.2 (51) *
|
||||
* - Version 2.2.1 (50) *
|
||||
* + Add to calendar (org.dgtale.icsimport)
|
||||
* - Version 1.2 (3)
|
||||
* - Version 1.1 (2)
|
||||
*/
|
||||
private void assertConflictingRepo(List<Repo> allRepos) {
|
||||
Repo repo = findRepo(REPO_CONFLICTING, allRepos);
|
||||
|
||||
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
|
||||
assertEquals("Apks for conflicting repo", 6, apks.size());
|
||||
assertApksExist(apks, "org.adaway", new int[]{50, 51, 52, 53});
|
||||
assertApksExist(apks, "org.dgtale.icsimport", new int[]{2, 3});
|
||||
|
||||
assertAdAwayMetadata(repo, "Conflicting");
|
||||
assertCalendarMetadata(repo, "Conflicting");
|
||||
}
|
||||
|
||||
}
|
@ -20,12 +20,14 @@
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
package org.fdroid.fdroid.updater;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.RepoXMLHandler;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.Repo;
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user