Merge branch 'category-table' into 'master'
Store categories in separate category table Currently, the category that an app is in is recorded in the database via the `fdroid_app.categories` column, containing a comma separated list of strings. This makes it hard to query. The existing code to get a list of categories was pretty bad as a result. This moves to a different data model whereby categories are stored in a separate table. Each repo is free to specify that an app is in arbitray caregories (as with before). This is represented by a join table between categories and app metadata. The end result is that categories are much more a first class citizen than before, and they will be able to be queried easier - which is important for the new UI. Note that the categories table need never be emptied, it can keep being appended to. The reason is that if there are no apps in a particular category (represented by no corresponding rows in the join table) then the category will not be shown to the user. See merge request !409
This commit is contained in:
commit
b852c0dca0
@ -104,6 +104,11 @@
|
||||
android:name="org.fdroid.fdroid.data.PackageProvider"
|
||||
android:exported="false"/>
|
||||
|
||||
<provider
|
||||
android:authorities="org.fdroid.fdroid.data.CategoryProvider"
|
||||
android:name="org.fdroid.fdroid.data.CategoryProvider"
|
||||
android:exported="false"/>
|
||||
|
||||
<provider
|
||||
android:name="org.fdroid.fdroid.installer.ApkFileProvider"
|
||||
android:authorities="org.fdroid.fdroid.installer.ApkFileProvider"
|
||||
|
@ -1337,14 +1337,6 @@ public class AppDetails extends AppCompatActivity {
|
||||
tv.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Categories TextView
|
||||
final TextView categories = (TextView) view.findViewById(R.id.categories);
|
||||
if (prefs.expertMode() && app.categories != null) {
|
||||
categories.setText(TextUtils.join(", ", app.categories));
|
||||
} else {
|
||||
categories.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
Apk curApk = null;
|
||||
for (int i = 0; i < appDetails.getApks().getCount(); i++) {
|
||||
final Apk apk = appDetails.getApks().getItem(i);
|
||||
|
@ -101,6 +101,8 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
|
||||
/**
|
||||
* List of categories (as defined in the metadata documentation) or null if there aren't any.
|
||||
* This is only populated when parsing a repository. If you need to know about the categories
|
||||
* an app is in any other part of F-Droid, use the {@link CategoryProvider}.
|
||||
*/
|
||||
public String[] categories;
|
||||
|
||||
@ -230,9 +232,6 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
case Cols.LAST_UPDATED:
|
||||
lastUpdated = Utils.parseDate(cursor.getString(i), null);
|
||||
break;
|
||||
case Cols.CATEGORIES:
|
||||
categories = Utils.parseCommaSeparatedString(cursor.getString(i));
|
||||
break;
|
||||
case Cols.ANTI_FEATURES:
|
||||
antiFeatures = Utils.parseCommaSeparatedString(cursor.getString(i));
|
||||
break;
|
||||
@ -504,7 +503,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
values.put(Cols.SUGGESTED_VERSION_CODE, suggestedVersionCode);
|
||||
values.put(Cols.UPSTREAM_VERSION_NAME, upstreamVersionName);
|
||||
values.put(Cols.UPSTREAM_VERSION_CODE, upstreamVersionCode);
|
||||
values.put(Cols.CATEGORIES, Utils.serializeCommaSeparatedString(categories));
|
||||
values.put(Cols.ForWriting.Categories.CATEGORIES, Utils.serializeCommaSeparatedString(categories));
|
||||
values.put(Cols.ANTI_FEATURES, Utils.serializeCommaSeparatedString(antiFeatures));
|
||||
values.put(Cols.REQUIREMENTS, Utils.serializeCommaSeparatedString(requirements));
|
||||
values.put(Cols.IS_COMPATIBLE, compatible ? 1 : 0);
|
||||
|
@ -10,19 +10,19 @@ import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Schema.ApkTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppPrefsTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
|
||||
import org.fdroid.fdroid.data.Schema.CatJoinTable;
|
||||
import org.fdroid.fdroid.data.Schema.CategoryTable;
|
||||
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;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -92,51 +92,6 @@ public class AppProvider extends FDroidProvider {
|
||||
return apps;
|
||||
}
|
||||
|
||||
public static String getCategoryAll(Context context) {
|
||||
return context.getString(R.string.category_All);
|
||||
}
|
||||
|
||||
public static String getCategoryWhatsNew(Context context) {
|
||||
return context.getString(R.string.category_Whats_New);
|
||||
}
|
||||
|
||||
public static String getCategoryRecentlyUpdated(Context context) {
|
||||
return context.getString(R.string.category_Recently_Updated);
|
||||
}
|
||||
|
||||
public static List<String> categories(Context context) {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final Uri uri = getContentUri();
|
||||
final String[] projection = {Cols.CATEGORIES};
|
||||
final Cursor cursor = resolver.query(uri, projection, null, null, null);
|
||||
final Set<String> categorySet = new HashSet<>();
|
||||
if (cursor != null) {
|
||||
if (cursor.getCount() > 0) {
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
final String categoriesString = cursor.getString(0);
|
||||
String[] categoriesList = Utils.parseCommaSeparatedString(categoriesString);
|
||||
if (categoriesList != null) {
|
||||
Collections.addAll(categorySet, categoriesList);
|
||||
}
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
final List<String> categories = new ArrayList<>(categorySet);
|
||||
Collections.sort(categories);
|
||||
|
||||
// Populate the category list with the real categories, and the
|
||||
// locally generated meta-categories for "What's New", "Recently
|
||||
// Updated" and "All"...
|
||||
categories.add(0, getCategoryAll(context));
|
||||
categories.add(0, getCategoryRecentlyUpdated(context));
|
||||
categories.add(0, getCategoryWhatsNew(context));
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
public static App findHighestPriorityMetadata(ContentResolver resolver, String packageName) {
|
||||
final Uri uri = getHighestPriorityMetadataUri(packageName);
|
||||
return cursorToApp(resolver.query(uri, Cols.ALL, null, null, null));
|
||||
@ -255,12 +210,11 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
}
|
||||
|
||||
private class Query extends QueryBuilder {
|
||||
protected class Query extends QueryBuilder {
|
||||
|
||||
private boolean isSuggestedApkTableAdded;
|
||||
private boolean requiresInstalledTable;
|
||||
private boolean requiresLeftJoinToPrefs;
|
||||
private boolean categoryFieldAdded;
|
||||
private boolean countFieldAppended;
|
||||
|
||||
@Override
|
||||
@ -269,21 +223,21 @@ public class AppProvider extends FDroidProvider {
|
||||
final String app = getTableName();
|
||||
final String apk = getApkTableName();
|
||||
final String repo = RepoTable.NAME;
|
||||
final String cat = CategoryTable.NAME;
|
||||
final String catJoin = getCatJoinTableName();
|
||||
|
||||
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 " + catJoin + " ON (" + app + "." + Cols.ROW_ID + " = " + catJoin + "." + CatJoinTable.Cols.APP_METADATA_ID + ") " +
|
||||
" LEFT JOIN " + cat + " ON (" + cat + "." + CategoryTable.Cols.ROW_ID + " = " + catJoin + "." + CatJoinTable.Cols.CATEGORY_ID + ") " +
|
||||
" LEFT JOIN " + apk + " ON (" + apk + "." + ApkTable.Cols.APP_ID + " = " + app + "." + Cols.ROW_ID + ") ";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isDistinct() {
|
||||
return fieldCount() == 1 && categoryFieldAdded;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String groupBy() {
|
||||
// If the count field has been requested, then we want to group all rows together.
|
||||
// If the count field has been requested, then we want to group all rows together. Otherwise
|
||||
// we will only group all the rows belonging to a single app together.
|
||||
return countFieldAppended ? null : getTableName() + "." + Cols.ROW_ID;
|
||||
}
|
||||
|
||||
@ -351,9 +305,6 @@ public class AppProvider extends FDroidProvider {
|
||||
appendCountField();
|
||||
break;
|
||||
default:
|
||||
if (field.equals(Cols.CATEGORIES)) {
|
||||
categoryFieldAdded = true;
|
||||
}
|
||||
appendField(field, getTableName());
|
||||
break;
|
||||
}
|
||||
@ -574,6 +525,10 @@ public class AppProvider extends FDroidProvider {
|
||||
return AppMetadataTable.NAME;
|
||||
}
|
||||
|
||||
protected String getCatJoinTableName() {
|
||||
return CatJoinTable.NAME;
|
||||
}
|
||||
|
||||
protected String getApkTableName() {
|
||||
return ApkTable.NAME;
|
||||
}
|
||||
@ -635,6 +590,7 @@ public class AppProvider extends FDroidProvider {
|
||||
final String app = getTableName();
|
||||
final String[] columns = {
|
||||
PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME,
|
||||
CategoryTable.NAME + "." + CategoryTable.Cols.NAME,
|
||||
app + "." + Cols.NAME,
|
||||
app + "." + Cols.SUMMARY,
|
||||
app + "." + Cols.DESCRIPTION,
|
||||
@ -727,20 +683,8 @@ public class AppProvider extends FDroidProvider {
|
||||
}
|
||||
|
||||
private AppQuerySelection queryCategory(String category) {
|
||||
// TODO: In the future, add a new table for categories,
|
||||
// so we can join onto it.
|
||||
final String app = getTableName();
|
||||
final String selection =
|
||||
app + "." + Cols.CATEGORIES + " = ? OR " + // Only category e.g. "internet"
|
||||
app + "." + Cols.CATEGORIES + " LIKE ? OR " + // First category e.g. "internet,%"
|
||||
app + "." + Cols.CATEGORIES + " LIKE ? OR " + // Last category e.g. "%,internet"
|
||||
app + "." + Cols.CATEGORIES + " LIKE ? "; // One of many categories e.g. "%,internet,%"
|
||||
final String[] args = {
|
||||
category,
|
||||
category + ",%",
|
||||
"%," + category,
|
||||
"%," + category + ",%",
|
||||
};
|
||||
final String selection = CategoryTable.NAME + "." + CategoryTable.Cols.NAME + " = ? ";
|
||||
final String[] args = {category};
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
@ -903,13 +847,51 @@ public class AppProvider extends FDroidProvider {
|
||||
values.remove(Cols.Package.PACKAGE_NAME);
|
||||
values.put(Cols.PACKAGE_ID, packageId);
|
||||
|
||||
db().insertOrThrow(getTableName(), null, values);
|
||||
String[] categories = null;
|
||||
boolean saveCategories = false;
|
||||
if (values.containsKey(Cols.ForWriting.Categories.CATEGORIES)) {
|
||||
// Hold onto these categories, so that after we have an ID to reference the newly inserted
|
||||
// app metadata we can then specify its categories.
|
||||
saveCategories = true;
|
||||
categories = Utils.parseCommaSeparatedString(values.getAsString(Cols.ForWriting.Categories.CATEGORIES));
|
||||
values.remove(Cols.ForWriting.Categories.CATEGORIES);
|
||||
}
|
||||
|
||||
long appMetadataId = db().insertOrThrow(getTableName(), null, values);
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
|
||||
if (saveCategories) {
|
||||
ensureCategories(categories, appMetadataId);
|
||||
}
|
||||
|
||||
return getSpecificAppUri(values.getAsString(PackageTable.Cols.PACKAGE_NAME), values.getAsLong(Cols.REPO_ID));
|
||||
}
|
||||
|
||||
protected void ensureCategories(String[] categories, long appMetadataId) {
|
||||
db().delete(getCatJoinTableName(), CatJoinTable.Cols.APP_METADATA_ID + " = ?", new String[] {Long.toString(appMetadataId)});
|
||||
if (categories != null) {
|
||||
Set<String> categoriesSet = new HashSet<>();
|
||||
for (String categoryName : categories) {
|
||||
|
||||
// There is nothing stopping a server repeating a category name in the metadata of
|
||||
// an app. In order to prevent unique constraint violations, only insert once into
|
||||
// the join table.
|
||||
if (categoriesSet.contains(categoryName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
categoriesSet.add(categoryName);
|
||||
long categoryId = CategoryProvider.Helper.ensureExists(getContext(), categoryName);
|
||||
ContentValues categoryValues = new ContentValues(2);
|
||||
categoryValues.put(CatJoinTable.Cols.APP_METADATA_ID, appMetadataId);
|
||||
categoryValues.put(CatJoinTable.Cols.CATEGORY_ID, categoryId);
|
||||
db().insert(getCatJoinTableName(), null, categoryValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||
if (MATCHER.match(uri) != CALC_SUGGESTED_APKS) {
|
||||
|
254
app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java
Normal file
254
app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java
Normal file
@ -0,0 +1,254 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.data.Schema.CatJoinTable;
|
||||
import org.fdroid.fdroid.data.Schema.CategoryTable;
|
||||
import org.fdroid.fdroid.data.Schema.CategoryTable.Cols;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class CategoryProvider extends FDroidProvider {
|
||||
|
||||
public static final class Helper {
|
||||
private Helper() { }
|
||||
|
||||
public static long ensureExists(Context context, String category) {
|
||||
long id = getCategoryId(context, category);
|
||||
if (id <= 0) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(Cols.NAME, category);
|
||||
Uri uri = context.getContentResolver().insert(getContentUri(), values);
|
||||
id = Long.parseLong(uri.getLastPathSegment());
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
public static long getCategoryId(Context context, String category) {
|
||||
String[] projection = new String[] {Cols.ROW_ID};
|
||||
Cursor cursor = context.getContentResolver().query(getCategoryUri(category), 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();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCategoryAll(Context context) {
|
||||
return context.getString(R.string.category_All);
|
||||
}
|
||||
|
||||
public static String getCategoryWhatsNew(Context context) {
|
||||
return context.getString(R.string.category_Whats_New);
|
||||
}
|
||||
|
||||
public static String getCategoryRecentlyUpdated(Context context) {
|
||||
return context.getString(R.string.category_Recently_Updated);
|
||||
}
|
||||
|
||||
public static List<String> categories(Context context) {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final Uri uri = CategoryProvider.getAllCategories();
|
||||
final String[] projection = {Cols.NAME};
|
||||
final Cursor cursor = resolver.query(uri, projection, null, null, null);
|
||||
List<String> categories = new ArrayList<>(30);
|
||||
if (cursor != null) {
|
||||
if (cursor.getCount() > 0) {
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
final String name = cursor.getString(0);
|
||||
categories.add(name);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
Collections.sort(categories);
|
||||
|
||||
// Populate the category list with the real categories, and the
|
||||
// locally generated meta-categories for "What's New", "Recently
|
||||
// Updated" and "All"...
|
||||
categories.add(0, getCategoryAll(context));
|
||||
categories.add(0, getCategoryRecentlyUpdated(context));
|
||||
categories.add(0, getCategoryWhatsNew(context));
|
||||
|
||||
return categories;
|
||||
}
|
||||
}
|
||||
|
||||
private class Query extends QueryBuilder {
|
||||
|
||||
private boolean onlyCategoriesWithApps;
|
||||
|
||||
@Override
|
||||
protected String getRequiredTables() {
|
||||
String joinType = onlyCategoriesWithApps ? " JOIN " : " LEFT JOIN ";
|
||||
|
||||
return CategoryTable.NAME + joinType + CatJoinTable.NAME + " ON (" +
|
||||
CatJoinTable.Cols.CATEGORY_ID + " = " + CategoryTable.NAME + "." + Cols.ROW_ID + ") ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addField(String field) {
|
||||
appendField(field, getTableName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String groupBy() {
|
||||
return CategoryTable.NAME + "." + Cols.ROW_ID;
|
||||
}
|
||||
|
||||
public void setOnlyCategoriesWithApps(boolean onlyCategoriesWithApps) {
|
||||
this.onlyCategoriesWithApps = onlyCategoriesWithApps;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String PROVIDER_NAME = "CategoryProvider";
|
||||
|
||||
private static final UriMatcher MATCHER = new UriMatcher(-1);
|
||||
|
||||
private static final String PATH_CATEGORY_NAME = "categoryName";
|
||||
private static final String PATH_ALL_CATEGORIES = "all";
|
||||
private static final String PATH_CATEGORY_ID = "categoryId";
|
||||
|
||||
static {
|
||||
MATCHER.addURI(getAuthority(), PATH_CATEGORY_NAME + "/*", CODE_SINGLE);
|
||||
MATCHER.addURI(getAuthority(), PATH_ALL_CATEGORIES, CODE_LIST);
|
||||
}
|
||||
|
||||
private static Uri getContentUri() {
|
||||
return Uri.parse("content://" + getAuthority());
|
||||
}
|
||||
|
||||
public static Uri getAllCategories() {
|
||||
return Uri.withAppendedPath(getContentUri(), PATH_ALL_CATEGORIES);
|
||||
}
|
||||
|
||||
public static Uri getCategoryUri(String categoryName) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_CATEGORY_NAME)
|
||||
.appendPath(categoryName)
|
||||
.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 Cols#ROW_ID} of the newly inserted row.
|
||||
*/
|
||||
private static Uri getCategoryIdUri(long categoryId) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_CATEGORY_ID)
|
||||
.appendPath(Long.toString(categoryId))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTableName() {
|
||||
return CategoryTable.NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getProviderName() {
|
||||
return "CategoryProvider";
|
||||
}
|
||||
|
||||
public static String getAuthority() {
|
||||
return AUTHORITY + "." + PROVIDER_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UriMatcher getMatcher() {
|
||||
return MATCHER;
|
||||
}
|
||||
|
||||
protected QuerySelection querySingle(String categoryName) {
|
||||
final String selection = getTableName() + "." + Cols.NAME + " = ?";
|
||||
final String[] args = {categoryName};
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
protected QuerySelection queryAllInUse() {
|
||||
final String selection = CatJoinTable.NAME + "." + CatJoinTable.Cols.APP_METADATA_ID + " IS NOT NULL ";
|
||||
final String[] args = {};
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
|
||||
QuerySelection selection = new QuerySelection(customSelection, selectionArgs);
|
||||
boolean onlyCategoriesWithApps = false;
|
||||
switch (MATCHER.match(uri)) {
|
||||
case CODE_SINGLE:
|
||||
selection = selection.add(querySingle(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case CODE_LIST:
|
||||
selection = selection.add(queryAllInUse());
|
||||
onlyCategoriesWithApps = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Invalid URI for content provider: " + uri);
|
||||
}
|
||||
|
||||
Query query = new Query();
|
||||
query.addSelection(selection);
|
||||
query.addFields(projection);
|
||||
query.addOrderBy(sortOrder);
|
||||
query.setOnlyCategoriesWithApps(onlyCategoriesWithApps);
|
||||
|
||||
Cursor cursor = LoggingQuery.query(db(), query.toString(), query.getArgs());
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleting of categories is not required.
|
||||
* It doesn't matter if we have a category in the database when no apps are in that category.
|
||||
* They wont take up much space, and it is the presence of rows in the
|
||||
* {@link CatJoinTable} which decides whether a category is displayed in F-Droid or not.
|
||||
*/
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String where, String[] whereArgs) {
|
||||
throw new UnsupportedOperationException("Delete not supported for " + uri + ".");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||
long rowId = db().insertOrThrow(getTableName(), null, values);
|
||||
getContext().getContentResolver().notifyChange(AppProvider.getCanUpdateUri(), null);
|
||||
return getCategoryIdUri(rowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Category names never change. If an app originally is in category "Games" and then in a
|
||||
* future repo update is now in "Games & Stuff", then both categories can exist quite happily.
|
||||
*/
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||
throw new UnsupportedOperationException("Update not supported for " + uri + ".");
|
||||
}
|
||||
}
|
@ -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.CatJoinTable;
|
||||
import org.fdroid.fdroid.data.Schema.PackageTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppPrefsTable;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
||||
@ -130,7 +131,6 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
+ AppMetadataTable.Cols.LITECOIN_ADDR + " string,"
|
||||
+ AppMetadataTable.Cols.FLATTR_ID + " string,"
|
||||
+ AppMetadataTable.Cols.REQUIREMENTS + " string,"
|
||||
+ AppMetadataTable.Cols.CATEGORIES + " string,"
|
||||
+ AppMetadataTable.Cols.ADDED + " string,"
|
||||
+ AppMetadataTable.Cols.LAST_UPDATED + " string,"
|
||||
+ AppMetadataTable.Cols.IS_COMPATIBLE + " int not null,"
|
||||
@ -145,6 +145,27 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
+ AppPrefsTable.Cols.IGNORE_ALL_UPDATES + " INT NOT NULL "
|
||||
+ " );";
|
||||
|
||||
private static final String CREATE_TABLE_CATEGORY = "CREATE TABLE " + Schema.CategoryTable.NAME
|
||||
+ " ( "
|
||||
+ Schema.CategoryTable.Cols.NAME + " TEXT NOT NULL "
|
||||
+ " );";
|
||||
|
||||
/**
|
||||
* The order of the two columns in the primary key matters for this table. The index that is
|
||||
* built for sqlite to quickly search the primary key will be sorted by app metadata id first,
|
||||
* and category id second. This means that we don't need a separate individual index on the
|
||||
* app metadata id, because it can instead look through the primary key index. This can be
|
||||
* observed by flipping the order of the primary key columns, and noting the resulting sqlite
|
||||
* logs along the lines of:
|
||||
* E/SQLiteLog(14164): (284) automatic index on fdroid_categoryAppMetadataJoin(appMetadataId)
|
||||
*/
|
||||
static final String CREATE_TABLE_CAT_JOIN = "CREATE TABLE " + CatJoinTable.NAME
|
||||
+ " ( "
|
||||
+ CatJoinTable.Cols.APP_METADATA_ID + " INT NOT NULL, "
|
||||
+ CatJoinTable.Cols.CATEGORY_ID + " INT NOT NULL, "
|
||||
+ "primary key(" + CatJoinTable.Cols.APP_METADATA_ID + ", " + CatJoinTable.Cols.CATEGORY_ID + ") "
|
||||
+ " );";
|
||||
|
||||
private static final String CREATE_TABLE_INSTALLED_APP = "CREATE TABLE " + InstalledAppTable.NAME
|
||||
+ " ( "
|
||||
+ InstalledAppTable.Cols.PACKAGE_NAME + " TEXT NOT NULL PRIMARY KEY, "
|
||||
@ -157,7 +178,7 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
+ InstalledAppTable.Cols.HASH + " TEXT NOT NULL"
|
||||
+ " );";
|
||||
|
||||
protected static final int DB_VERSION = 64;
|
||||
protected static final int DB_VERSION = 65;
|
||||
|
||||
private final Context context;
|
||||
|
||||
@ -172,6 +193,8 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(CREATE_TABLE_PACKAGE);
|
||||
db.execSQL(CREATE_TABLE_APP_METADATA);
|
||||
db.execSQL(CREATE_TABLE_APK);
|
||||
db.execSQL(CREATE_TABLE_CATEGORY);
|
||||
db.execSQL(CREATE_TABLE_CAT_JOIN);
|
||||
db.execSQL(CREATE_TABLE_INSTALLED_APP);
|
||||
db.execSQL(CREATE_TABLE_REPO);
|
||||
db.execSQL(CREATE_TABLE_APP_PREFS);
|
||||
@ -234,6 +257,23 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
supportRepoPushRequests(db, oldVersion);
|
||||
migrateToPackageTable(db, oldVersion);
|
||||
addObbFiles(db, oldVersion);
|
||||
addCategoryTables(db, oldVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* It is possible to correctly migrate categories from the previous `categories` column in
|
||||
* app metadata to the new join table without destroying any data and requiring a repo update.
|
||||
* However, in practice other code since the previous stable has already reset the transient
|
||||
* tables and forced a repo update, so it is much easier to do the same here. It wont have any
|
||||
* negative impact on those upgrading from the previous stable. If there was a number of solid
|
||||
* alpha releases before this, then a proper migration would've be in order.
|
||||
*/
|
||||
private void addCategoryTables(SQLiteDatabase db, int oldVersion) {
|
||||
if (oldVersion >= 65) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetTransient(db);
|
||||
}
|
||||
|
||||
private void addObbFiles(SQLiteDatabase db, int oldVersion) {
|
||||
@ -823,15 +863,26 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
if (tableExists(db, Schema.CategoryTable.NAME)) {
|
||||
db.execSQL("DROP TABLE " + Schema.CategoryTable.NAME);
|
||||
}
|
||||
|
||||
if (tableExists(db, CatJoinTable.NAME)) {
|
||||
db.execSQL("DROP TABLE " + CatJoinTable.NAME);
|
||||
}
|
||||
|
||||
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);
|
||||
db.execSQL(CREATE_TABLE_CATEGORY);
|
||||
db.execSQL(CREATE_TABLE_CAT_JOIN);
|
||||
clearRepoEtags(db);
|
||||
ensureIndexes(db);
|
||||
db.setTransactionSuccessful();
|
||||
|
@ -9,6 +9,15 @@ import android.provider.BaseColumns;
|
||||
*/
|
||||
public interface Schema {
|
||||
|
||||
/**
|
||||
* A package is essentially the app that a developer builds and wants you to install on your
|
||||
* device. It differs from entries in:
|
||||
* * {@link ApkTable} because they are specific builds of a particular package. Many different
|
||||
* builds of the same package can exist.
|
||||
* * {@link AppMetadataTable} because this is metdata about a package which is specified by a
|
||||
* given repo. Different repos can provide the same package with different descriptions,
|
||||
* categories, etc.
|
||||
*/
|
||||
interface PackageTable {
|
||||
|
||||
String NAME = "fdroid_package";
|
||||
@ -50,6 +59,51 @@ public interface Schema {
|
||||
}
|
||||
}
|
||||
|
||||
interface CategoryTable {
|
||||
|
||||
String NAME = "fdroid_category";
|
||||
|
||||
interface Cols {
|
||||
String ROW_ID = "rowid";
|
||||
String NAME = "name";
|
||||
|
||||
String[] ALL = {
|
||||
ROW_ID, NAME,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An entry in this table signifies that an app is in a particular category. Each repo can
|
||||
* classify its apps in separate categories, and so the same record in {@link PackageTable}
|
||||
* can be in the same category multiple times, if multiple repos think that is the case.
|
||||
* @see CategoryTable
|
||||
* @see AppMetadataTable
|
||||
*/
|
||||
interface CatJoinTable {
|
||||
|
||||
String NAME = "fdroid_categoryAppMetadataJoin";
|
||||
|
||||
interface Cols {
|
||||
/**
|
||||
* Foreign key to {@link AppMetadataTable}.
|
||||
* @see AppMetadataTable
|
||||
*/
|
||||
String APP_METADATA_ID = "appMetadataId";
|
||||
|
||||
/**
|
||||
* Foreign key to {@link CategoryTable}.
|
||||
* @see CategoryTable
|
||||
*/
|
||||
String CATEGORY_ID = "categoryId";
|
||||
|
||||
/**
|
||||
* @see AppMetadataTable.Cols#ALL_COLS
|
||||
*/
|
||||
String[] ALL_COLS = {APP_METADATA_ID, CATEGORY_ID};
|
||||
}
|
||||
}
|
||||
|
||||
interface AppMetadataTable {
|
||||
|
||||
String NAME = "fdroid_app";
|
||||
@ -85,7 +139,6 @@ public interface Schema {
|
||||
String UPSTREAM_VERSION_CODE = "upstreamVercode";
|
||||
String ADDED = "added";
|
||||
String LAST_UPDATED = "lastUpdated";
|
||||
String CATEGORIES = "categories";
|
||||
String ANTI_FEATURES = "antiFeatures";
|
||||
String REQUIREMENTS = "requirements";
|
||||
String ICON_URL = "iconUrl";
|
||||
@ -105,6 +158,17 @@ public interface Schema {
|
||||
String PACKAGE_NAME = "package_packageName";
|
||||
}
|
||||
|
||||
/**
|
||||
* This is to make it explicit that you cannot request the {@link Categories#CATEGORIES}
|
||||
* field when selecting app metadata from the database. It is only here for the purpose
|
||||
* of inserting/updating apps.
|
||||
*/
|
||||
interface ForWriting {
|
||||
interface Categories {
|
||||
String CATEGORIES = "categories_commaSeparatedCateogryNames";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
@ -115,7 +179,7 @@ public interface Schema {
|
||||
LICENSE, AUTHOR, EMAIL, WEB_URL, TRACKER_URL, SOURCE_URL,
|
||||
CHANGELOG_URL, DONATE_URL, BITCOIN_ADDR, LITECOIN_ADDR, FLATTR_ID,
|
||||
UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,
|
||||
CATEGORIES, ANTI_FEATURES, REQUIREMENTS, ICON_URL, ICON_URL_LARGE,
|
||||
ANTI_FEATURES, REQUIREMENTS, ICON_URL, ICON_URL_LARGE,
|
||||
SUGGESTED_VERSION_CODE,
|
||||
};
|
||||
|
||||
@ -129,7 +193,7 @@ public interface Schema {
|
||||
LICENSE, AUTHOR, EMAIL, WEB_URL, TRACKER_URL, SOURCE_URL,
|
||||
CHANGELOG_URL, DONATE_URL, BITCOIN_ADDR, LITECOIN_ADDR, FLATTR_ID,
|
||||
UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,
|
||||
CATEGORIES, ANTI_FEATURES, REQUIREMENTS, ICON_URL, ICON_URL_LARGE,
|
||||
ANTI_FEATURES, REQUIREMENTS, ICON_URL, ICON_URL_LARGE,
|
||||
SUGGESTED_VERSION_CODE, SuggestedApk.VERSION_NAME,
|
||||
InstalledApp.VERSION_CODE, InstalledApp.VERSION_NAME,
|
||||
InstalledApp.SIGNATURE, Package.PACKAGE_NAME,
|
||||
|
@ -11,9 +11,11 @@ import android.text.TextUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.fdroid.fdroid.Utils;
|
||||
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.CatJoinTable;
|
||||
import org.fdroid.fdroid.data.Schema.PackageTable;
|
||||
|
||||
/**
|
||||
@ -29,6 +31,7 @@ public class TempAppProvider extends AppProvider {
|
||||
private static final String PROVIDER_NAME = "TempAppProvider";
|
||||
|
||||
static final String TABLE_TEMP_APP = "temp_" + AppMetadataTable.NAME;
|
||||
static final String TABLE_TEMP_CAT_JOIN = "temp_" + CatJoinTable.NAME;
|
||||
|
||||
private static final String PATH_INIT = "init";
|
||||
private static final String PATH_COMMIT = "commit";
|
||||
@ -51,6 +54,11 @@ public class TempAppProvider extends AppProvider {
|
||||
return TABLE_TEMP_APP;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCatJoinTableName() {
|
||||
return TABLE_TEMP_CAT_JOIN;
|
||||
}
|
||||
|
||||
public static String getAuthority() {
|
||||
return AUTHORITY + "." + PROVIDER_NAME;
|
||||
}
|
||||
@ -153,6 +161,12 @@ public class TempAppProvider extends AppProvider {
|
||||
// Package names for apps cannot change...
|
||||
values.remove(Cols.Package.PACKAGE_NAME);
|
||||
|
||||
if (values.containsKey(Cols.ForWriting.Categories.CATEGORIES)) {
|
||||
String[] categories = Utils.parseCommaSeparatedString(values.getAsString(Cols.ForWriting.Categories.CATEGORIES));
|
||||
ensureCategories(categories, packageName, repoId);
|
||||
values.remove(Cols.ForWriting.Categories.CATEGORIES);
|
||||
}
|
||||
|
||||
int count = db().update(getTableName(), values, query.getSelection(), query.getArgs());
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(getHighestPriorityMetadataUri(packageName), null);
|
||||
@ -160,6 +174,18 @@ public class TempAppProvider extends AppProvider {
|
||||
return count;
|
||||
}
|
||||
|
||||
private void ensureCategories(String[] categories, String packageName, long repoId) {
|
||||
Query query = new AppProvider.Query();
|
||||
query.addField(Cols.ROW_ID);
|
||||
query.addSelection(querySingle(packageName, repoId));
|
||||
Cursor cursor = db().rawQuery(query.toString(), query.getArgs());
|
||||
cursor.moveToFirst();
|
||||
long appMetadataId = cursor.getLong(0);
|
||||
cursor.close();
|
||||
|
||||
ensureCategories(categories, appMetadataId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
|
||||
AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs);
|
||||
@ -188,7 +214,9 @@ public class TempAppProvider extends AppProvider {
|
||||
ensureTempTableDetached(db);
|
||||
db.execSQL("ATTACH DATABASE ':memory:' AS " + DB);
|
||||
db.execSQL(DBHelper.CREATE_TABLE_APP_METADATA.replaceFirst(AppMetadataTable.NAME, DB + "." + getTableName()));
|
||||
db.execSQL(DBHelper.CREATE_TABLE_CAT_JOIN.replaceFirst(CatJoinTable.NAME, DB + "." + getCatJoinTableName()));
|
||||
db.execSQL(copyData(AppMetadataTable.Cols.ALL_COLS, AppMetadataTable.NAME, DB + "." + getTableName()));
|
||||
db.execSQL(copyData(CatJoinTable.Cols.ALL_COLS, CatJoinTable.NAME, DB + "." + getCatJoinTableName()));
|
||||
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 + ");");
|
||||
@ -208,8 +236,9 @@ public class TempAppProvider extends AppProvider {
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
final String tempApp = DB + "." + TempAppProvider.TABLE_TEMP_APP;
|
||||
final String tempApp = DB + "." + TABLE_TEMP_APP;
|
||||
final String tempApk = DB + "." + TempApkProvider.TABLE_TEMP_APK;
|
||||
final String tempCatJoin = DB + "." + TABLE_TEMP_CAT_JOIN;
|
||||
|
||||
db.execSQL("DELETE FROM " + AppMetadataTable.NAME + " WHERE 1");
|
||||
db.execSQL(copyData(AppMetadataTable.Cols.ALL_COLS, tempApp, AppMetadataTable.NAME));
|
||||
@ -217,6 +246,9 @@ public class TempAppProvider extends AppProvider {
|
||||
db.execSQL("DELETE FROM " + ApkTable.NAME + " WHERE 1");
|
||||
db.execSQL(copyData(ApkTable.Cols.ALL_COLS, tempApk, ApkTable.NAME));
|
||||
|
||||
db.execSQL("DELETE FROM " + CatJoinTable.NAME + " WHERE 1");
|
||||
db.execSQL(copyData(CatJoinTable.Cols.ALL_COLS, tempCatJoin, CatJoinTable.NAME));
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
||||
getContext().getContentResolver().notifyChange(AppProvider.getContentUri(), null);
|
||||
|
@ -26,6 +26,7 @@ import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.compat.ArrayAdapterCompat;
|
||||
import org.fdroid.fdroid.compat.CursorAdapterCompat;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.CategoryProvider;
|
||||
import org.fdroid.fdroid.views.AppListAdapter;
|
||||
import org.fdroid.fdroid.views.AvailableAppListAdapter;
|
||||
|
||||
@ -94,7 +95,7 @@ public class AvailableAppsFragment extends AppListFragment implements
|
||||
new AsyncTask<Void, Void, List<String>>() {
|
||||
@Override
|
||||
protected List<String> doInBackground(Void... params) {
|
||||
return AppProvider.Helper.categories(activity);
|
||||
return CategoryProvider.Helper.categories(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -132,7 +133,7 @@ public class AvailableAppsFragment extends AppListFragment implements
|
||||
categorySpinner = spinner;
|
||||
categorySpinner.setId(R.id.category_spinner);
|
||||
|
||||
categories = AppProvider.Helper.categories(getActivity());
|
||||
categories = CategoryProvider.Helper.categories(getActivity());
|
||||
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(
|
||||
getActivity(), android.R.layout.simple_spinner_item, translateCategories(getActivity(), categories));
|
||||
@ -163,20 +164,20 @@ public class AvailableAppsFragment extends AppListFragment implements
|
||||
|
||||
categoryWrapper = view.findViewById(R.id.category_wrapper);
|
||||
setupCategorySpinner((Spinner) view.findViewById(R.id.category_spinner));
|
||||
defaultCategory = AppProvider.Helper.getCategoryWhatsNew(getActivity());
|
||||
defaultCategory = CategoryProvider.Helper.getCategoryWhatsNew(getActivity());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Uri getDataUri() {
|
||||
if (currentCategory == null || currentCategory.equals(AppProvider.Helper.getCategoryAll(getActivity()))) {
|
||||
if (currentCategory == null || currentCategory.equals(CategoryProvider.Helper.getCategoryAll(getActivity()))) {
|
||||
return AppProvider.getContentUri();
|
||||
}
|
||||
if (currentCategory.equals(AppProvider.Helper.getCategoryRecentlyUpdated(getActivity()))) {
|
||||
if (currentCategory.equals(CategoryProvider.Helper.getCategoryRecentlyUpdated(getActivity()))) {
|
||||
return AppProvider.getRecentlyUpdatedUri();
|
||||
}
|
||||
if (currentCategory.equals(AppProvider.Helper.getCategoryWhatsNew(getActivity()))) {
|
||||
if (currentCategory.equals(CategoryProvider.Helper.getCategoryWhatsNew(getActivity()))) {
|
||||
return AppProvider.getNewlyAddedUri();
|
||||
}
|
||||
return AppProvider.getCategoryUri(currentCategory);
|
||||
|
@ -48,15 +48,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<!-- android:visibility="gone" because not needed in app details imho (but maybe will get used in another place soon) -->
|
||||
<TextView
|
||||
android:id="@+id/categories"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
tools:text="System" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/antifeatures"
|
||||
android:layout_width="fill_parent"
|
||||
|
@ -93,7 +93,7 @@ public class CategoryProviderTest extends FDroidProviderTest {
|
||||
insertAppWithCategory("com.rock", "Rock", "Mineral");
|
||||
insertAppWithCategory("com.banana", "Banana", "Vegetable");
|
||||
|
||||
List<String> categories = AppProvider.Helper.categories(context);
|
||||
List<String> categories = CategoryProvider.Helper.categories(context);
|
||||
String[] expected = new String[] {
|
||||
context.getResources().getString(R.string.category_Whats_New),
|
||||
context.getResources().getString(R.string.category_Recently_Updated),
|
||||
@ -111,7 +111,7 @@ public class CategoryProviderTest extends FDroidProviderTest {
|
||||
insertAppWithCategory("com.dog.rock.apple", "Dog-Rock-Apple", "Animal,Mineral,Vegetable");
|
||||
insertAppWithCategory("com.banana.apple", "Banana", "Vegetable,Vegetable");
|
||||
|
||||
List<String> categories = AppProvider.Helper.categories(context);
|
||||
List<String> categories = CategoryProvider.Helper.categories(context);
|
||||
String[] expected = new String[] {
|
||||
context.getResources().getString(R.string.category_Whats_New),
|
||||
context.getResources().getString(R.string.category_Recently_Updated),
|
||||
@ -127,7 +127,7 @@ public class CategoryProviderTest extends FDroidProviderTest {
|
||||
"Running,Shooting,Jumping,Bleh,Sneh,Pleh,Blah,Test category," +
|
||||
"The quick brown fox jumps over the lazy dog,With apostrophe's");
|
||||
|
||||
List<String> categoriesLonger = AppProvider.Helper.categories(context);
|
||||
List<String> categoriesLonger = CategoryProvider.Helper.categories(context);
|
||||
String[] expectedLonger = new String[] {
|
||||
context.getResources().getString(R.string.category_Whats_New),
|
||||
context.getResources().getString(R.string.category_Recently_Updated),
|
||||
@ -154,7 +154,7 @@ public class CategoryProviderTest extends FDroidProviderTest {
|
||||
|
||||
private void insertAppWithCategory(String id, String name, String categories) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(Cols.CATEGORIES, categories);
|
||||
values.put(Cols.ForWriting.Categories.CATEGORIES, categories);
|
||||
AppProviderTest.insertApp(contentResolver, context, id, name, values);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
|
||||
package org.fdroid.fdroid.updater;
|
||||
|
||||
import org.fdroid.fdroid.BuildConfig;
|
||||
import org.fdroid.fdroid.RepoUpdater;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/**
|
||||
* Tests two versions of the official main F-Droid metadata, from 10 days apart. This is here
|
||||
* because there is so much metadata to parse in the main repo, covering many different aspects
|
||||
* of the available metadata. Some apps will be added, others updated, and it should all just work.
|
||||
*/
|
||||
// TODO: Use sdk=24 when Robolectric supports this
|
||||
@Config(constants = BuildConfig.class, sdk = 23)
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
public class FDroidRepoUpdateTest extends MultiRepoUpdaterTest {
|
||||
|
||||
private static final String TAG = "FDroidRepoUpdateTest";
|
||||
|
||||
private static final String REPO_FDROID = "F-Droid";
|
||||
private static final String REPO_FDROID_URI = "https://f-droid.org/repo";
|
||||
private static final String REPO_FDROID_PUB_KEY = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef";
|
||||
|
||||
@Test
|
||||
public void doesntCrash() throws RepoUpdater.UpdateException {
|
||||
assertEmpty();
|
||||
updateEarlier();
|
||||
updateLater();
|
||||
}
|
||||
|
||||
protected void updateEarlier() throws RepoUpdater.UpdateException {
|
||||
Utils.debugLog(TAG, "Updating earlier version of F-Droid repo");
|
||||
updateRepo(createUpdater(REPO_FDROID, REPO_FDROID_URI, context, REPO_FDROID_PUB_KEY), "index.fdroid.2016-10-30.jar");
|
||||
}
|
||||
|
||||
protected void updateLater() throws RepoUpdater.UpdateException {
|
||||
Utils.debugLog(TAG, "Updating later version of F-Droid repo");
|
||||
updateRepo(createUpdater(REPO_FDROID, REPO_FDROID_URI, context, REPO_FDROID_PUB_KEY), "index.fdroid.2016-11-10.jar");
|
||||
}
|
||||
|
||||
}
|
@ -181,6 +181,10 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest {
|
||||
return new RepoUpdater(context, createRepo(name, uri, context));
|
||||
}
|
||||
|
||||
protected RepoUpdater createUpdater(String name, String uri, Context context, String signingCert) {
|
||||
return new RepoUpdater(context, createRepo(name, uri, context, signingCert));
|
||||
}
|
||||
|
||||
protected void updateConflicting() throws UpdateException {
|
||||
updateRepo(createUpdater(REPO_CONFLICTING, REPO_CONFLICTING_URI, context), "multiRepo.conflicting.jar");
|
||||
}
|
||||
|
BIN
app/src/test/resources/index.fdroid.2016-10-30.jar
Normal file
BIN
app/src/test/resources/index.fdroid.2016-10-30.jar
Normal file
Binary file not shown.
BIN
app/src/test/resources/index.fdroid.2016-11-10.jar
Normal file
BIN
app/src/test/resources/index.fdroid.2016-11-10.jar
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user