BobStore/F-Droid/src/org/fdroid/fdroid/data/ApkProvider.java

510 lines
18 KiB
Java

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.provider.BaseColumns;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ApkProvider extends FDroidProvider {
private static final String TAG = "ApkProvider";
/**
* SQLite has a maximum of 999 parameters in a query. Each apk we add
* requires two (id and vercode) so we can only query half of that. Then,
* we may want to add additional constraints, so we give our self some
* room by saying only 450 apks can be queried at once.
*/
protected static final int MAX_APKS_TO_QUERY = 450;
public static final class Helper {
private Helper() {}
public static void update(Context context, Apk apk) {
ContentResolver resolver = context.getContentResolver();
Uri uri = getContentUri(apk.id, apk.vercode);
resolver.update(uri, apk.toContentValues(), null, null);
}
public static List<Apk> cursorToList(Cursor cursor) {
int knownApkCount = cursor != null ? cursor.getCount() : 0;
List<Apk> apks = new ArrayList<>(knownApkCount);
if (cursor != null) {
if (knownApkCount > 0) {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
apks.add(new Apk(cursor));
cursor.moveToNext();
}
}
cursor.close();
}
return apks;
}
public static int deleteApksByRepo(Context context, Repo repo) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getRepoUri(repo.getId());
return resolver.delete(uri, null, null);
}
public static void deleteApksByApp(Context context, App app) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getAppUri(app.id);
resolver.delete(uri, null, null);
}
public static void deleteApks(final Context context, final List<Apk> apks) {
if (apks.size() > ApkProvider.MAX_APKS_TO_QUERY) {
int middle = apks.size() / 2;
List<Apk> apks1 = apks.subList(0, middle);
List<Apk> apks2 = apks.subList(middle, apks.size());
deleteApks(context, apks1);
deleteApks(context, apks2);
} else {
deleteApksSafely(context, apks);
}
}
private static void deleteApksSafely(final Context context, final List<Apk> apks) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getContentUri(apks);
resolver.delete(uri, null, null);
}
public static Apk find(Context context, String id, int versionCode) {
return find(context, id, versionCode, DataColumns.ALL);
}
public static Apk find(Context context, String id, int versionCode, String[] projection) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getContentUri(id, versionCode);
Cursor cursor = resolver.query(uri, projection, null, null, null);
Apk apk = null;
if (cursor != null) {
if (cursor.getCount() > 0) {
cursor.moveToFirst();
apk = new Apk(cursor);
}
cursor.close();
}
return apk;
}
public static List<Apk> findByApp(Context context, String appId) {
return findByApp(context, appId, ApkProvider.DataColumns.ALL);
}
public static List<Apk> findByApp(Context context,
String appId, String[] projection) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getAppUri(appId);
final String sort = ApkProvider.DataColumns.VERSION_CODE + " DESC";
Cursor cursor = resolver.query(uri, projection, null, null, sort);
return cursorToList(cursor);
}
/**
* Returns apks in the database, which have the same id and version as
* one of the apks in the "apks" argument.
*/
public static List<Apk> knownApks(Context context, List<Apk> apks, String[] fields) {
if (apks.size() == 0) {
return new ArrayList<>();
}
List<Apk> knownApks = new ArrayList<>();
if (apks.size() > ApkProvider.MAX_APKS_TO_QUERY) {
int middle = apks.size() / 2;
List<Apk> apks1 = apks.subList(0, middle);
List<Apk> apks2 = apks.subList(middle, apks.size());
knownApks.addAll(knownApks(context, apks1, fields));
knownApks.addAll(knownApks(context,apks2, fields));
} else {
knownApks.addAll(knownApksSafe(context,apks, fields));
}
return knownApks;
}
private static List<Apk> knownApksSafe(final Context context, final List<Apk> apks, final String[] fields) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getContentUri(apks);
Cursor cursor = resolver.query(uri, fields, null, null, null);
return cursorToList(cursor);
}
public static List<Apk> findByRepo(Context context, Repo repo, String[] fields) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getRepoUri(repo.getId());
Cursor cursor = resolver.query(uri, fields, null, null, null);
return cursorToList(cursor);
}
public static Apk get(Context context, Uri uri) {
return get(context, uri, DataColumns.ALL);
}
public static Apk get(Context context, Uri uri, String[] fields) {
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(uri, fields, null, null, null);
Apk apk = null;
if (cursor != null) {
if (cursor.getCount() > 0) {
cursor.moveToFirst();
apk = new Apk(cursor);
}
cursor.close();
}
return apk;
}
}
public interface DataColumns extends BaseColumns {
String _COUNT_DISTINCT_ID = "countDistinct";
String APK_ID = "id";
String VERSION = "version";
String REPO_ID = "repo";
String HASH = "hash";
String VERSION_CODE = "vercode";
String NAME = "apkName";
String SIZE = "size";
String SIGNATURE = "sig";
String SOURCE_NAME = "srcname";
String MIN_SDK_VERSION = "minSdkVersion";
String MAX_SDK_VERSION = "maxSdkVersion";
String PERMISSIONS = "permissions";
String FEATURES = "features";
String NATIVE_CODE = "nativecode";
String HASH_TYPE = "hashType";
String ADDED_DATE = "added";
String IS_COMPATIBLE = "compatible";
String INCOMPATIBLE_REASONS = "incompatibleReasons";
String REPO_VERSION = "repoVersion";
String REPO_ADDRESS = "repoAddress";
String[] ALL = {
_ID, APK_ID, VERSION, REPO_ID, HASH, VERSION_CODE, NAME, SIZE,
SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, MAX_SDK_VERSION,
PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE,
IS_COMPATIBLE, REPO_VERSION, REPO_ADDRESS, INCOMPATIBLE_REASONS
};
}
private static final int CODE_APP = CODE_SINGLE + 1;
private static final int CODE_REPO = CODE_APP + 1;
private static final int CODE_APKS = CODE_REPO + 1;
private static final String PROVIDER_NAME = "ApkProvider";
private static final String PATH_APK = "apk";
private static final String PATH_APKS = "apks";
private static final String PATH_APP = "app";
private static final String PATH_REPO = "repo";
private static final UriMatcher matcher = new UriMatcher(-1);
public static final Map<String,String> REPO_FIELDS = new HashMap<>();
static {
REPO_FIELDS.put(DataColumns.REPO_VERSION, RepoProvider.DataColumns.VERSION);
REPO_FIELDS.put(DataColumns.REPO_ADDRESS, RepoProvider.DataColumns.ADDRESS);
matcher.addURI(getAuthority(), PATH_REPO + "/#", CODE_REPO);
matcher.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE);
matcher.addURI(getAuthority(), PATH_APKS + "/*", CODE_APKS);
matcher.addURI(getAuthority(), PATH_APP + "/*", CODE_APP);
matcher.addURI(getAuthority(), null, CODE_LIST);
}
public static String getAuthority() {
return AUTHORITY + "." + PROVIDER_NAME;
}
public static Uri getContentUri() {
return Uri.parse("content://" + getAuthority());
}
public static Uri getAppUri(String appId) {
return getContentUri()
.buildUpon()
.appendPath(PATH_APP)
.appendPath(appId)
.build();
}
public static Uri getRepoUri(long repoId) {
return getContentUri()
.buildUpon()
.appendPath(PATH_REPO)
.appendPath(Long.toString(repoId))
.build();
}
public static Uri getContentUri(Apk apk) {
return getContentUri(apk.id, apk.vercode);
}
public static Uri getContentUri(String id, int versionCode) {
return getContentUri()
.buildUpon()
.appendPath(PATH_APK)
.appendPath(Integer.toString(versionCode))
.appendPath(id)
.build();
}
/**
* Intentionally left protected because it will break if apks is larger than
* {@link org.fdroid.fdroid.data.ApkProvider#MAX_APKS_TO_QUERY}. Instead of using
* this directly, think about using
* {@link org.fdroid.fdroid.data.ApkProvider.Helper#knownApks(android.content.Context, java.util.List, String[])}
*/
protected static Uri getContentUri(List<Apk> apks) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < apks.size(); i++) {
if (i != 0) {
builder.append(',');
}
final Apk a = apks.get(i);
builder.append(a.id).append(':').append(a.vercode);
}
return getContentUri().buildUpon()
.appendPath(PATH_APKS)
.appendPath(builder.toString())
.build();
}
@Override
protected String getTableName() {
return DBHelper.TABLE_APK;
}
@Override
protected String getProviderName() {
return PROVIDER_NAME;
}
@Override
protected UriMatcher getMatcher() {
return matcher;
}
private static class Query extends QueryBuilder {
private boolean repoTableRequired = false;
@Override
protected String getRequiredTables() {
return DBHelper.TABLE_APK + " AS apk";
}
@Override
public void addField(String field) {
if (REPO_FIELDS.containsKey(field)) {
addRepoField(REPO_FIELDS.get(field), field);
} else if (field.equals(DataColumns._ID)) {
appendField("rowid", "apk", "_id");
} else if (field.equals(DataColumns._COUNT)) {
appendField("COUNT(*) AS " + DataColumns._COUNT);
} else if (field.equals(DataColumns._COUNT_DISTINCT_ID)) {
appendField("COUNT(DISTINCT apk.id) AS " + DataColumns._COUNT_DISTINCT_ID);
} else {
appendField(field, "apk");
}
}
private void addRepoField(String field, String alias) {
if (!repoTableRequired) {
repoTableRequired = true;
leftJoin(DBHelper.TABLE_REPO, "repo", "apk.repo = repo._id");
}
appendField(field, "repo", alias);
}
}
private QuerySelection queryApp(String appId) {
final String selection = DataColumns.APK_ID + " = ? ";
final String[] args = { appId };
return new QuerySelection(selection, args);
}
private QuerySelection querySingle(Uri uri) {
final String selection = " vercode = ? and id = ? ";
final String[] args = {
// First (0th) path segment is the word "apk",
// and we are not interested in it.
uri.getPathSegments().get(1),
uri.getPathSegments().get(2)
};
return new QuerySelection(selection, args);
}
private QuerySelection queryRepo(long repoId) {
final String selection = DataColumns.REPO_ID + " = ? ";
final String[] args = { Long.toString(repoId) };
return new QuerySelection(selection, args);
}
private QuerySelection queryApks(String apkKeys) {
final String[] apkDetails = apkKeys.split(",");
final String[] args = new String[apkDetails.length * 2];
StringBuilder sb = new StringBuilder();
if (apkDetails.length > MAX_APKS_TO_QUERY) {
throw new IllegalArgumentException(
"Cannot query more than " + MAX_APKS_TO_QUERY + ". " +
"You tried to query " + apkDetails.length);
}
for (int i = 0; i < apkDetails.length; i++) {
String[] parts = apkDetails[i].split(":");
String id = parts[0];
String verCode = parts[1];
args[i * 2] = id;
args[i * 2 + 1] = verCode;
if (i != 0) {
sb.append(" OR ");
}
sb.append(" ( id = ? AND vercode = ? ) ");
}
return new QuerySelection(sb.toString(), args);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
QuerySelection query = new QuerySelection(selection, selectionArgs);
switch (matcher.match(uri)) {
case CODE_LIST:
break;
case CODE_SINGLE:
query = query.add(querySingle(uri));
break;
case CODE_APP:
query = query.add(queryApp(uri.getLastPathSegment()));
break;
case CODE_APKS:
query = query.add(queryApks(uri.getLastPathSegment()));
break;
case CODE_REPO:
query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment())));
break;
default:
Log.e(TAG, "Invalid URI for apk content provider: " + uri);
throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri);
}
Query queryBuilder = new Query();
for (final String field : projection) {
queryBuilder.addField(field);
}
queryBuilder.addSelection(query.getSelection());
queryBuilder.addOrderBy(sortOrder);
Cursor cursor = read().rawQuery(queryBuilder.toString(), query.getArgs());
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
private static void removeRepoFields(ContentValues values) {
for (Map.Entry<String,String> repoField : REPO_FIELDS.entrySet()) {
final String field = repoField.getKey();
if (values.containsKey(field)) {
Log.i(TAG, "Cannot insert/update '" + field + "' field " +
"on apk table, as it belongs to the repo table. " +
"This field will be ignored.");
values.remove(field);
}
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
removeRepoFields(values);
validateFields(DataColumns.ALL, values);
write().insertOrThrow(getTableName(), null, values);
if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null);
}
return getContentUri(
values.getAsString(DataColumns.APK_ID),
values.getAsInteger(DataColumns.VERSION_CODE));
}
@Override
public int delete(Uri uri, String where, String[] whereArgs) {
QuerySelection query = new QuerySelection(where, whereArgs);
switch (matcher.match(uri)) {
case CODE_REPO:
query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment())));
break;
case CODE_APP:
query = query.add(queryApp(uri.getLastPathSegment()));
break;
case CODE_APKS:
query = query.add(queryApks(uri.getLastPathSegment()));
break;
case CODE_LIST:
throw new UnsupportedOperationException("Can't delete all apks.");
case CODE_SINGLE:
throw new UnsupportedOperationException("Can't delete individual apks.");
default:
Log.e(TAG, "Invalid URI for apk content provider: " + uri);
throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri);
}
int rowsAffected = write().delete(getTableName(), query.getSelection(), query.getArgs());
getContext().getContentResolver().notifyChange(uri, null);
return rowsAffected;
}
@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
if (matcher.match(uri) != CODE_SINGLE) {
throw new UnsupportedOperationException("Cannot update anything other than a single apk.");
}
validateFields(DataColumns.ALL, values);
removeRepoFields(values);
QuerySelection query = new QuerySelection(where, whereArgs);
query = query.add(querySingle(uri));
int numRows = write().update(getTableName(), values, query.getSelection(), query.getArgs());
if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null);
}
return numRows;
}
}