diff --git a/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java b/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java index ee18b006f..195fe0634 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java @@ -23,24 +23,17 @@ import android.app.Instrumentation; import android.net.Uri; import android.os.Build; import android.util.Log; -import androidx.annotation.NonNull; + import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.fdroid.fdroid.AssetUtils; -import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.compat.FileCompatTest; import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoXMLHandler; -import org.fdroid.fdroid.mock.RepoDetails; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -69,7 +62,6 @@ public class ApkVerifierTest { File sdk14Apk; File minMaxApk; private File extendedPermissionsApk; - private File extendedPermsXml; @Before public void setUp() { @@ -89,14 +81,14 @@ public class ApkVerifierTest { "org.fdroid.extendedpermissionstest.apk", dir ); - extendedPermsXml = AssetUtils.copyAssetToDir(instrumentation.getContext(), + File extendedPermsXml = AssetUtils.copyAssetToDir(instrumentation.getContext(), "extendedPerms.xml", dir ); assertTrue(sdk14Apk.exists()); assertTrue(minMaxApk.exists()); assertTrue(extendedPermissionsApk.exists()); - assertTrue(extendedPermsXml.exists()); + assertTrue(extendedPermsXml.exists()); // TODO remove file when test was ported } @Test @@ -108,50 +100,6 @@ public class ApkVerifierTest { assertFalse(ApkVerifier.requestedPermissionsEqual(null, perms)); } - @Test - public void testWithoutPrefix() { - Apk apk = new Apk(); - apk.packageName = "org.fdroid.permissions.sdk14"; - apk.targetSdkVersion = 14; - ArrayList noPrefixPermissionsList = new ArrayList<>(Arrays.asList( - "AUTHENTICATE_ACCOUNTS", - "MANAGE_ACCOUNTS", - "READ_PROFILE", - "WRITE_PROFILE", - "GET_ACCOUNTS", - "READ_CONTACTS", - "WRITE_CONTACTS", - "WRITE_EXTERNAL_STORAGE", - "READ_EXTERNAL_STORAGE", - "INTERNET", - "ACCESS_NETWORK_STATE", - "NFC", - "READ_SYNC_SETTINGS", - "WRITE_SYNC_SETTINGS", - "WRITE_CALL_LOG", // implied-permission! - "READ_CALL_LOG" // implied-permission! - )); - if (Build.VERSION.SDK_INT >= 29) { - noPrefixPermissionsList.add("android.permission.ACCESS_MEDIA_LOCATION"); - } - String[] noPrefixPermissions = noPrefixPermissionsList.toArray(new String[0]); - - for (int i = 0; i < noPrefixPermissions.length; i++) { - noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]); - } - apk.requestedPermissions = noPrefixPermissions; - - Uri uri = Uri.fromFile(sdk14Apk); - ApkVerifier apkVerifier = new ApkVerifier(instrumentation.getContext(), uri, apk); - - try { - apkVerifier.verifyApk(); - } catch (ApkVerifier.ApkVerificationException | ApkVerifier.ApkPermissionUnequalException e) { - e.printStackTrace(); - fail(e.getMessage()); - } - } - @Test(expected = ApkVerifier.ApkPermissionUnequalException.class) public void testWithMinMax() throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException { @@ -296,9 +244,8 @@ public class ApkVerifierTest { } @Test - public void testExtendedPerms() throws IOException, - ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException { - RepoDetails actualDetails = getFromFile(extendedPermsXml); + public void testExtendedPerms() + throws ApkVerifier.ApkPermissionUnequalException, ApkVerifier.ApkVerificationException { HashSet expectedSet = new HashSet<>(Arrays.asList( "android.permission.ACCESS_NETWORK_STATE", "android.permission.ACCESS_WIFI_STATE", @@ -327,7 +274,8 @@ public class ApkVerifierTest { expectedSet.add("android.permission.CALL_PHONE"); } } - Apk apk = actualDetails.apks.get(0); + // TODO get this from "extendedPerms.xml" and use setRequestedPermissions + Apk apk = new Apk(); HashSet actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions)); for (String permission : expectedSet) { if (!actualSet.contains(permission)) { @@ -355,8 +303,7 @@ public class ApkVerifierTest { } @Test - public void testImpliedPerms() throws IOException { - RepoDetails actualDetails = getFromFile(extendedPermsXml); + public void testImpliedPerms() { TreeSet expectedSet = new TreeSet<>(Arrays.asList( "android.permission.ACCESS_NETWORK_STATE", "android.permission.ACCESS_WIFI_STATE", @@ -383,7 +330,8 @@ public class ApkVerifierTest { if (Build.VERSION.SDK_INT >= 29) { expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION"); } - Apk apk = actualDetails.apks.get(1); + // TODO get this from "extendedPerms.xml" and use setRequestedPermissions + Apk apk = new Apk(); Log.i(TAG, "APK: " + apk.apkName); HashSet actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions)); for (String permission : expectedSet) { @@ -423,7 +371,8 @@ public class ApkVerifierTest { expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION"); } expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]); - apk = actualDetails.apks.get(2); + // TODO get this from "extendedPerms.xml" and use setRequestedPermissions + apk = new Apk(); Log.i(TAG, "APK: " + apk.apkName); actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions)); for (String permission : expectedSet) { @@ -441,14 +390,4 @@ public class ApkVerifierTest { assertTrue(ApkVerifier.requestedPermissionsEqual(expectedPermissions, apk.requestedPermissions)); } - @NonNull - private RepoDetails getFromFile(File indexFile) throws IOException { - InputStream inputStream = null; - try { - inputStream = new FileInputStream(indexFile); - return RepoDetails.getFromFile(inputStream, Repo.PUSH_REQUEST_IGNORE); - } finally { - Utils.closeQuietly(inputStream); - } - } } diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java index b8500278a..6ff8d2d74 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java @@ -40,21 +40,16 @@ import com.google.android.material.switchmaterial.SwitchMaterial; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; -import org.fdroid.download.Downloader; import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.NfcHelper; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.NewRepoConfig; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.nearby.peers.BluetoothPeer; import org.fdroid.fdroid.nearby.peers.Peer; import org.fdroid.fdroid.net.BluetoothDownloader; -import org.fdroid.fdroid.net.DownloaderService; import org.fdroid.fdroid.qr.CameraCharacteristicsChecker; import org.fdroid.fdroid.views.main.MainActivity; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be241e08e..00374ffae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -431,35 +431,6 @@ android:name=".installer.FileInstallerActivity" android:theme="@style/AppThemeTransparent" /> - - - - - - - - - - - diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 59ed5a307..15ae3b8ba 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -456,7 +456,8 @@ public class UpdateService extends JobIntentService { } else if ((manualUpdate || forcedUpdate) && fdroidPrefs.isOnDemandDownloadAllowed()) { Utils.debugLog(TAG, "manually requested or forced update"); if (forcedUpdate) { - DBHelper.resetTransient(this); + DBHelper.resetRepos(this); + // TODO check if we still need something like this: // InstalledAppProviderService.compareToPackageManager(this); } } else if (!fdroidPrefs.isBackgroundDownloadAllowed() && !fdroidPrefs.isOnDemandDownloadAllowed()) { diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 923cb9f01..ef5cf0f75 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -62,7 +62,6 @@ import org.fdroid.download.Mirror; import org.fdroid.fdroid.compat.FileCompat; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.SanitizedFile; -import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.net.TreeUriDownloader; import org.fdroid.index.v2.FileV2; import org.xml.sax.XMLReader; @@ -86,7 +85,6 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.Formatter; @@ -94,7 +92,6 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -1044,58 +1041,4 @@ public final class Utils { } } - /** - * Returns a list of unwanted anti-features from a list of acceptable anti-features - * Basically: all anti-features minus the ones that are okay. - */ - private static List unwantedAntifeatures(Context context, Set acceptableAntifeatures) { - List antiFeatures = new ArrayList<>( - Arrays.asList(context.getResources().getStringArray(R.array.antifeaturesValues)) - ); - - antiFeatures.removeAll(acceptableAntifeatures); - - return antiFeatures; - } - - /** - * Returns a SQL filter to use in Cursors to filter out everything with non-acceptable antifeatures - * - * @param context - * @return String - */ - public static String getAntifeatureSQLFilter(Context context) { - List unwantedAntifeatures = Utils.unwantedAntifeatures( - context, - Preferences.get().showAppsWithAntiFeatures() - ); - - StringBuilder antiFeatureFilter = new StringBuilder(Schema.AppMetadataTable.NAME) - .append(".") - .append(Schema.AppMetadataTable.Cols.ANTI_FEATURES) - .append(" IS NULL"); - - if (!unwantedAntifeatures.isEmpty()) { - antiFeatureFilter.append(" OR ("); - - for (int i = 0; i < unwantedAntifeatures.size(); i++) { - String unwantedAntifeature = unwantedAntifeatures.get(i); - - if (i > 0) { - antiFeatureFilter.append(" AND "); - } - - antiFeatureFilter.append(Schema.AppMetadataTable.NAME) - .append(".") - .append(Schema.AppMetadataTable.Cols.ANTI_FEATURES) - .append(" NOT LIKE '%") - .append(unwantedAntifeature) - .append("%'"); - } - - antiFeatureFilter.append(")"); - } - - return antiFeatureFilter.toString(); - } } diff --git a/app/src/main/java/org/fdroid/fdroid/data/Apk.java b/app/src/main/java/org/fdroid/fdroid/data/Apk.java index dc8292f42..f0dee9c1d 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java @@ -2,10 +2,8 @@ package org.fdroid.fdroid.data; import android.Manifest; import android.annotation.TargetApi; -import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageInfo; -import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Environment; @@ -25,7 +23,6 @@ import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.CompatibilityChecker; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Schema.ApkTable.Cols; import org.fdroid.fdroid.installer.ApkCache; import org.fdroid.fdroid.net.TreeUriDownloader; import org.fdroid.index.v2.PermissionV2; @@ -58,7 +55,7 @@ import androidx.annotation.Nullable; * @see fdroiddata * @see fdroidserver */ -public class Apk extends ValueObject implements Comparable, Parcelable { +public class Apk implements Comparable, Parcelable { // Using only byte-range keeps it only 8-bits in the SQLite database @JsonIgnore @@ -166,97 +163,6 @@ public class Apk extends ValueObject implements Comparable, Parcelable { repoId = 0; } - public Apk(Cursor cursor) { - - checkCursorPosition(cursor); - - for (int i = 0; i < cursor.getColumnCount(); i++) { - switch (cursor.getColumnName(i)) { - case Cols.APP_ID: - appId = cursor.getLong(i); - break; - case Cols.HASH: - hash = cursor.getString(i); - break; - case Cols.HASH_TYPE: - hashType = cursor.getString(i); - break; - case Cols.ADDED_DATE: - added = Utils.parseDate(cursor.getString(i), null); - break; - case Cols.FEATURES: - features = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.Package.PACKAGE_NAME: - packageName = cursor.getString(i); - break; - case Cols.IS_COMPATIBLE: - compatible = cursor.getInt(i) == 1; - break; - case Cols.MIN_SDK_VERSION: - minSdkVersion = cursor.getInt(i); - break; - case Cols.TARGET_SDK_VERSION: - targetSdkVersion = cursor.getInt(i); - break; - case Cols.MAX_SDK_VERSION: - maxSdkVersion = cursor.getInt(i); - break; - case Cols.OBB_MAIN_FILE: - obbMainFile = cursor.getString(i); - break; - case Cols.OBB_MAIN_FILE_SHA256: - obbMainFileSha256 = cursor.getString(i); - break; - case Cols.OBB_PATCH_FILE: - obbPatchFile = cursor.getString(i); - break; - case Cols.OBB_PATCH_FILE_SHA256: - obbPatchFileSha256 = cursor.getString(i); - break; - case Cols.NAME: - apkName = cursor.getString(i); - break; - case Cols.REQUESTED_PERMISSIONS: - requestedPermissions = convertToRequestedPermissions(cursor.getString(i)); - break; - case Cols.NATIVE_CODE: - nativecode = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.INCOMPATIBLE_REASONS: - incompatibleReasons = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.REPO_ID: - repoId = cursor.getInt(i); - break; - case Cols.SIGNATURE: - sig = cursor.getString(i); - break; - case Cols.SIZE: - size = cursor.getInt(i); - break; - case Cols.SOURCE_NAME: - srcname = cursor.getString(i); - break; - case Cols.VERSION_NAME: - versionName = cursor.getString(i); - break; - case Cols.VERSION_CODE: - versionCode = cursor.getInt(i); - break; - case Cols.Repo.VERSION: - repoVersion = cursor.getInt(i); - break; - case Cols.Repo.ADDRESS: - repoAddress = cursor.getString(i); - break; - case Cols.AntiFeatures.ANTI_FEATURES: - antiFeatures = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - } - } - } - public Apk(AppVersion v) { Repository repo = Objects.requireNonNull(FDroidApp.getRepo(v.getRepoId())); repoAddress = repo.getAddress(); @@ -420,40 +326,6 @@ public class Apk extends ValueObject implements Comparable, Parcelable { return new File(App.getObbDir(packageName), obbPatchFile); } - @Override - public String toString() { - return toContentValues().toString(); - } - - public ContentValues toContentValues() { - ContentValues values = new ContentValues(); - values.put(Cols.APP_ID, appId); - values.put(Cols.VERSION_NAME, versionName); - values.put(Cols.VERSION_CODE, versionCode); - values.put(Cols.REPO_ID, repoId); - values.put(Cols.HASH, hash); - values.put(Cols.HASH_TYPE, hashType); - values.put(Cols.SIGNATURE, sig); - values.put(Cols.SOURCE_NAME, srcname); - values.put(Cols.SIZE, size); - values.put(Cols.NAME, apkName); - values.put(Cols.MIN_SDK_VERSION, minSdkVersion); - values.put(Cols.TARGET_SDK_VERSION, targetSdkVersion); - values.put(Cols.MAX_SDK_VERSION, maxSdkVersion); - values.put(Cols.OBB_MAIN_FILE, obbMainFile); - values.put(Cols.OBB_MAIN_FILE_SHA256, obbMainFileSha256); - values.put(Cols.OBB_PATCH_FILE, obbPatchFile); - values.put(Cols.OBB_PATCH_FILE_SHA256, obbPatchFileSha256); - values.put(Cols.ADDED_DATE, Utils.formatDate(added, "")); - values.put(Cols.REQUESTED_PERMISSIONS, Utils.serializeCommaSeparatedString(requestedPermissions)); - values.put(Cols.FEATURES, Utils.serializeCommaSeparatedString(features)); - values.put(Cols.NATIVE_CODE, Utils.serializeCommaSeparatedString(nativecode)); - values.put(Cols.INCOMPATIBLE_REASONS, Utils.serializeCommaSeparatedString(incompatibleReasons)); - values.put(Cols.AntiFeatures.ANTI_FEATURES, Utils.serializeCommaSeparatedString(antiFeatures)); - values.put(Cols.IS_COMPATIBLE, compatible ? 1 : 0); - return values; - } - @Override @TargetApi(19) public int compareTo(@NonNull Apk apk) { @@ -541,18 +413,6 @@ public class Apk extends ValueObject implements Comparable, Parcelable { } }; - private String[] convertToRequestedPermissions(String permissionsFromDb) { - String[] array = Utils.parseCommaSeparatedString(permissionsFromDb); - if (array != null) { - HashSet requestedPermissionsSet = new HashSet<>(); - for (String permission : array) { - requestedPermissionsSet.add(RepoXMLHandler.fdroidToAndroidPermission(permission)); - } - return requestedPermissionsSet.toArray(new String[requestedPermissionsSet.size()]); - } - return null; - } - /** * Set the Package Name property while ensuring it is sanitized. */ diff --git a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java deleted file mode 100644 index 2bbd316cb..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java +++ /dev/null @@ -1,712 +0,0 @@ -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.util.Log; - -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Schema.AntiFeatureTable; -import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable; -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; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -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 (packageName 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. - */ - 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 = 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(Long.toString(apk.versionCode)) - .build(); - } - - public static List cursorToList(Cursor cursor) { - int knownApkCount = cursor != null ? cursor.getCount() : 0; - List 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); - } - - /** - * Find an app which is closest to the version code suggested by the server, with some caveats: - *
    - *
  • If installed, limit to apks signed by the same signer as the installed apk.
  • - *
  • Otherwise, limit to apks signed by the "preferred" signer (see {@link App#preferredSigner}).
  • - *
- * If all else fails, try to return some {@link Apk} that will install something, - * rather than returning a null and triggering a {@link NullPointerException}. - */ - @Nullable - @Deprecated - public static Apk findSuggestedApk(Context context, App app) { - String mostAppropriateSignature = app.getMostAppropriateSignature(); - Apk apk = findApkFromAnyRepo(context, app.packageName, app.autoInstallVersionCode, - mostAppropriateSignature); - if (apk == null && (mostAppropriateSignature == null || !app.isInstalled(context))) { - List apks = findByPackageName(context, app.packageName); - for (Apk availableApk : apks) { - if (availableApk.sig.equals(mostAppropriateSignature)) { - apk = availableApk; - break; - } - } - if (apk == null && apks.size() > 0) { - apk = apks.get(0); - } - } - return apk; - - } - - public static Apk findApkFromAnyRepo(Context context, String packageName, int versionCode) { - return findApkFromAnyRepo(context, packageName, versionCode, null); - } - - public static Apk findApkFromAnyRepo(Context context, String packageName, int versionCode, - String signature) { - final Uri uri = getApkFromAnyRepoUri(packageName, versionCode, signature); - return findByUri(context, uri, Cols.ALL); - } - - public static Apk findByUri(Context context, Uri uri, String[] projection) { - ContentResolver resolver = context.getContentResolver(); - 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 findByPackageName(Context context, String packageName) { - ContentResolver resolver = context.getContentResolver(); - final Uri uri = getAppUri(packageName); - final String sort = "apk." + Cols.VERSION_CODE + " DESC"; - Cursor cursor = resolver.query(uri, Cols.ALL, null, null, sort); - return cursorToList(cursor); - } - - public static List 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); - } - - @NonNull - public static List findAppVersionsByRepo(Context context, App app, Repo repo) { - ContentResolver resolver = context.getContentResolver(); - final Uri uri = getRepoUri(repo.getId(), app.packageName); - Cursor cursor = resolver.query(uri, Cols.ALL, null, null, null); - return cursorToList(cursor); - } - - private static Apk cursorToApk(Cursor cursor) { - Apk apk = null; - if (cursor != null) { - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - apk = new Apk(cursor); - } - cursor.close(); - } - return apk; - } - - public static Apk get(Context context, Uri uri) { - ContentResolver resolver = context.getContentResolver(); - Cursor cursor = resolver.query(uri, Cols.ALL, null, null, null); - return cursorToApk(cursor); - } - - @NonNull - public static List findApksByHash(Context context, String apkHash) { - if (apkHash == null) { - return Collections.emptyList(); - } - ContentResolver resolver = context.getContentResolver(); - final Uri uri = getContentUri(); - String selection = " apk." + Cols.HASH + " = ? "; - String[] selectionArgs = new String[]{apkHash}; - Cursor cursor = resolver.query(uri, Cols.ALL, selection, selectionArgs, null); - return cursorToList(cursor); - } - } - - private static final int CODE_PACKAGE = CODE_SINGLE + 1; - private static final int CODE_REPO = CODE_PACKAGE + 1; - private static final int CODE_APKS = CODE_REPO + 1; - private static final int CODE_APK_ROW_ID = CODE_APKS + 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 int CODE_REPO_APP = CODE_APK_FROM_REPO + 1; - - private static final String PROVIDER_NAME = "ApkProvider"; - protected static final String PATH_APK_FROM_ANY_REPO = "apk-any-repo"; - protected static final String PATH_APK_FROM_REPO = "apk-from-repo"; - protected static final String PATH_REPO_APP = "repo-app"; - private static final String PATH_APKS = "apks"; - private static final String PATH_APP = "app"; - private static final String PATH_REPO = "repo"; - private static final String PATH_APK_ROW_ID = "apk-rowId"; - - private static final UriMatcher MATCHER = new UriMatcher(-1); - - private static final Map REPO_FIELDS = new HashMap<>(); - private static final Map PACKAGE_FIELDS = new HashMap<>(); - - static { - REPO_FIELDS.put(Cols.Repo.VERSION, RepoTable.Cols.VERSION); - REPO_FIELDS.put(Cols.Repo.ADDRESS, RepoTable.Cols.ADDRESS); - PACKAGE_FIELDS.put(Cols.Package.PACKAGE_NAME, PackageTable.Cols.PACKAGE_NAME); - - MATCHER.addURI(getAuthority(), PATH_REPO + "/#", CODE_REPO); - MATCHER.addURI(getAuthority(), PATH_REPO_APP + "/#/*", CODE_REPO_APP); - MATCHER.addURI(getAuthority(), PATH_APK_FROM_ANY_REPO + "/#/*/*", CODE_APK_FROM_ANY_REPO); - 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_APK_ROW_ID + "/#", CODE_APK_ROW_ID); - MATCHER.addURI(getAuthority(), null, CODE_LIST); - } - - public static String getAuthority() { - return AUTHORITY + "." + PROVIDER_NAME; - } - - public static Uri getContentUri() { - return Uri.parse("content://" + getAuthority()); - } - - private Uri getApkUri(long apkRowId) { - return getContentUri().buildUpon() - .appendPath(PATH_APK_ROW_ID) - .appendPath(Long.toString(apkRowId)) - .build(); - } - - public static Uri getAppUri(String packageName) { - return getContentUri() - .buildUpon() - .appendPath(PATH_APP) - .appendPath(packageName) - .build(); - } - - public static Uri getRepoUri(long repoId) { - return getContentUri() - .buildUpon() - .appendPath(PATH_REPO) - .appendPath(Long.toString(repoId)) - .build(); - } - - public static Uri getRepoUri(long repoId, String packageName) { - return getContentUri() - .buildUpon() - .appendPath(PATH_REPO_APP) - .appendPath(Long.toString(repoId)) - .appendPath(packageName) - .build(); - } - - public static Uri getApkFromAnyRepoUri(Apk apk) { - return getApkFromAnyRepoUri(apk.packageName, apk.versionCode, null); - } - - public static Uri getApkFromAnyRepoUri(String packageName, long versionCode, @Nullable String signature) { - Uri.Builder builder = getContentUri() - .buildUpon() - .appendPath(PATH_APK_FROM_ANY_REPO) - .appendPath(Long.toString(versionCode)) - .appendPath(packageName); - - if (signature != null) { - builder.appendPath(signature); - } - - return builder.build(); - } - - @Override - protected String getTableName() { - return ApkTable.NAME; - } - - protected String getApkAntiFeatureJoinTableName() { - return ApkAntiFeatureJoinTable.NAME; - } - - protected String getAppTableName() { - return AppMetadataTable.NAME; - } - - @Override - protected String getProviderName() { - return PROVIDER_NAME; - } - - @Override - protected UriMatcher getMatcher() { - return MATCHER; - } - - private class Query extends QueryBuilder { - - private boolean repoTableRequired; - private boolean antiFeaturesRequested; - - /** - * If the query includes anti features, then we group by apk id. This - * is because joining onto the anti-features table will result in - * multiple result rows for each apk (potentially), so we will - * {@code GROUP_CONCAT} each of the anti-features into a single comma- - * separated list for each apk. If we are _not_ including anti- - * features, then don't group by apk, because when doing a COUNT(*) - * this will result in the wrong result. - */ - @Override - protected String groupBy() { - return antiFeaturesRequested ? "apk." + Cols.ROW_ID : null; - } - - @Override - 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 + ")" + // NOPMD NOCHECKSTYLE LineLength - " LEFT JOIN " + pkg + " AS pkg ON (pkg." + PackageTable.Cols.ROW_ID + " = app." + AppMetadataTable.Cols.PACKAGE_ID + ")"; // NOPMD NOCHECKSTYLE LineLength - } - - @Override - public void addField(String field) { - if (PACKAGE_FIELDS.containsKey(field)) { - addPackageField(PACKAGE_FIELDS.get(field), field); - } else if (REPO_FIELDS.containsKey(field)) { - addRepoField(REPO_FIELDS.get(field), field); - } else if (Cols.AntiFeatures.ANTI_FEATURES.equals(field)) { - antiFeaturesRequested = true; - addAntiFeatures(); - } else if (field.equals(Cols._ID)) { - appendField(Cols.ROW_ID, "apk", Cols._ID); - } else if (field.equals(Cols._COUNT)) { - appendField("COUNT(*) AS " + Cols._COUNT); - } else if (field.equals(Cols._COUNT_DISTINCT)) { - appendField("COUNT(DISTINCT apk." + Cols.APP_ID + ") AS " + Cols._COUNT_DISTINCT); - } else { - appendField(field, "apk"); - } - } - - private void addPackageField(String field, String alias) { - appendField(field, "pkg", alias); - } - - private void addRepoField(String field, String alias) { - if (!repoTableRequired) { - repoTableRequired = true; - leftJoin(RepoTable.NAME, "repo", "apk." + Cols.REPO_ID + " = repo." + RepoTable.Cols._ID); - } - appendField(field, "repo", alias); - } - - private void addAntiFeatures() { - String apkAntiFeature = "apkAntiFeatureJoin"; - String antiFeature = "antiFeature"; - - leftJoin(getApkAntiFeatureJoinTableName(), apkAntiFeature, - "apk." + Cols.ROW_ID + " = " + apkAntiFeature + "." + ApkAntiFeatureJoinTable.Cols.APK_ID); - - leftJoin(AntiFeatureTable.NAME, antiFeature, - apkAntiFeature + "." + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + " = " - + antiFeature + "." + AntiFeatureTable.Cols.ROW_ID); - - appendField("group_concat(" + antiFeature + "." + AntiFeatureTable.Cols.NAME + ") as " - + Cols.AntiFeatures.ANTI_FEATURES); - } - } - - private QuerySelection queryPackage(String packageName) { - final String selection = "pkg." + PackageTable.Cols.PACKAGE_NAME + " = ?"; - final String[] args = {packageName}; - return new QuerySelection(selection, args); - } - - private QuerySelection querySingleFromAnyRepo(Uri uri) { - return querySingleFromAnyRepo(uri, true); - } - - private QuerySelection querySingleFromAnyRepo(Uri uri, boolean includeAlias) { - String alias = includeAlias ? "apk." : ""; - - String selection = - alias + Cols.VERSION_CODE + " = ? AND " + - alias + Cols.APP_ID + " IN (" + getMetadataIdFromPackageNameQuery() + ")"; - - List pathSegments = uri.getPathSegments(); - List args = new ArrayList<>(3); - args.add(pathSegments.get(1)); // 0th path segment is the word "apk" and we are not interested in it. - args.add(pathSegments.get(2)); - - if (pathSegments.size() >= 4) { - selection += " AND " + alias + Cols.SIGNATURE + " = ? "; - args.add(pathSegments.get(3)); - } - - return new QuerySelection(selection, args); - } - - private QuerySelection querySingle(long apkRowId) { - return querySingle(apkRowId, true); - } - - private QuerySelection querySingle(long apkRowId, boolean includeAlias) { - String alias = includeAlias ? "apk." : ""; - final String selection = alias + Cols.ROW_ID + " = ?"; - final String[] args = {Long.toString(apkRowId)}; - 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 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); - } - - protected QuerySelection queryRepo(long repoId, boolean includeAlias) { - String alias = includeAlias ? "apk." : ""; - final String selection = alias + Cols.REPO_ID + " = ? "; - final String[] args = {Long.toString(repoId)}; - return new QuerySelection(selection, args); - } - - protected QuerySelection queryApks(String apkKeys) { - return queryApks(apkKeys, true); - } - - protected QuerySelection queryApks(String apkKeys, boolean includeAlias) { - final String[] apkDetails = apkKeys.split(","); - if (apkDetails.length > MAX_APKS_TO_QUERY) { - throw new IllegalArgumentException( - "Cannot query more than " + MAX_APKS_TO_QUERY + ". " + - "You tried to query " + apkDetails.length); - } - String alias = includeAlias ? "apk." : ""; - final String[] args = new String[apkDetails.length * 2]; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < apkDetails.length; i++) { - String[] parts = apkDetails[i].split(":"); - String appId = parts[0]; - String versionCode = parts[1]; - args[i * 2] = appId; - args[i * 2 + 1] = versionCode; - if (i != 0) { - sb.append(" OR "); - } - - sb.append(" ( ") - .append(Cols.APP_ID) - .append(" = ? ") - .append(" AND ") - .append(alias) - .append(Cols.VERSION_CODE) - .append(" = ? ) "); - } - - return new QuerySelection(sb.toString(), args); - } - - 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 - public Cursor query(@NonNull Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - - QuerySelection query = new QuerySelection(selection, selectionArgs); - - switch (MATCHER.match(uri)) { - case CODE_REPO_APP: - List uriSegments = uri.getPathSegments(); - Long repoId = Long.parseLong(uriSegments.get(1)); - String packageName = uriSegments.get(2); - query = query.add(queryRepo(repoId)).add(queryPackage(packageName)); - break; - - case CODE_LIST: - break; - - case CODE_APK_FROM_ANY_REPO: - query = query.add(querySingleFromAnyRepo(uri)); - break; - - case CODE_APK_ROW_ID: - query = query.add(querySingle(Long.parseLong(uri.getLastPathSegment()))); - break; - - case CODE_PACKAGE: - query = query.add(queryPackage(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); - queryBuilder.addOrderBy(sortOrder); - - Cursor cursor = LoggingQuery.rawQuery(db(), queryBuilder.toString(), queryBuilder.getArgs()); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - return cursor; - } - - private static void removeFieldsFromOtherTables(ContentValues values) { - for (Map.Entry repoField : REPO_FIELDS.entrySet()) { - final String field = repoField.getKey(); - if (values.containsKey(field)) { - values.remove(field); - } - } - - for (Map.Entry appField : PACKAGE_FIELDS.entrySet()) { - final String field = appField.getKey(); - if (values.containsKey(field)) { - values.remove(field); - } - } - } - - @Override - public Uri insert(@NonNull Uri uri, ContentValues values) { - boolean saveAntiFeatures = false; - String[] antiFeatures = null; - if (values.containsKey(Cols.AntiFeatures.ANTI_FEATURES)) { - saveAntiFeatures = true; - String antiFeaturesString = values.getAsString(Cols.AntiFeatures.ANTI_FEATURES); - antiFeatures = Utils.parseCommaSeparatedString(antiFeaturesString); - values.remove(Cols.AntiFeatures.ANTI_FEATURES); - } - - removeFieldsFromOtherTables(values); - validateFields(Cols.ALL, values); - long newId = db().insertOrThrow(getTableName(), null, values); - - if (saveAntiFeatures) { - ensureAntiFeatures(antiFeatures, newId); - } - - if (!isApplyingBatch()) { - getContext().getContentResolver().notifyChange(uri, null); - } - return getApkUri(newId); - } - - protected void ensureAntiFeatures(String[] antiFeatures, long apkId) { - db().delete(getApkAntiFeatureJoinTableName(), - ApkAntiFeatureJoinTable.Cols.APK_ID + " = ?", - new String[]{Long.toString(apkId)}); - if (antiFeatures != null) { - Set antiFeatureSet = new HashSet<>(); - for (String antiFeatureName : antiFeatures) { - - // 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 (antiFeatureSet.contains(antiFeatureName)) { - continue; - } - - antiFeatureSet.add(antiFeatureName); - - long antiFeatureId = ensureAntiFeature(antiFeatureName); - ContentValues categoryValues = new ContentValues(2); - categoryValues.put(ApkAntiFeatureJoinTable.Cols.APK_ID, apkId); - categoryValues.put(ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID, antiFeatureId); - db().insert(getApkAntiFeatureJoinTableName(), null, categoryValues); - } - } - } - - protected long ensureAntiFeature(String antiFeatureName) { - long antiFeatureId = 0; - Cursor cursor = db().query(AntiFeatureTable.NAME, new String[]{AntiFeatureTable.Cols.ROW_ID}, - AntiFeatureTable.Cols.NAME + " = ?", new String[]{antiFeatureName}, null, null, null); - if (cursor != null) { - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - antiFeatureId = cursor.getLong(0); - } - cursor.close(); - } - - if (antiFeatureId <= 0) { - ContentValues values = new ContentValues(1); - values.put(AntiFeatureTable.Cols.NAME, antiFeatureName); - antiFeatureId = db().insert(AntiFeatureTable.NAME, null, values); - } - - return antiFeatureId; - } - - @Override - public int delete(@NonNull 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()), false)); - break; - - case CODE_APKS: - query = query.add(queryApks(uri.getLastPathSegment(), false)); - break; - - default: - Log.e(TAG, "Invalid URI for apk content provider: " + uri); - throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); - } - - int rowsAffected = db().delete(getTableName(), query.getSelection(), query.getArgs()); - getContext().getContentResolver().notifyChange(uri, null); - return rowsAffected; - - } - - @Override - public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) { - if (MATCHER.match(uri) != CODE_APK_FROM_REPO) { - throw new UnsupportedOperationException("Cannot update anything other than a single apk."); - } - - boolean saveAntiFeatures = false; - String[] antiFeatures = null; - if (values.containsKey(Cols.AntiFeatures.ANTI_FEATURES)) { - saveAntiFeatures = true; - String antiFeaturesString = values.getAsString(Cols.AntiFeatures.ANTI_FEATURES); - antiFeatures = Utils.parseCommaSeparatedString(antiFeaturesString); - values.remove(Cols.AntiFeatures.ANTI_FEATURES); - } - - validateFields(Cols.ALL, values); - removeFieldsFromOtherTables(values); - - QuerySelection query = new QuerySelection(where, whereArgs); - query = query.add(querySingleWithAppId(uri)); - - int numRows = db().update(getTableName(), values, query.getSelection(), query.getArgs()); - - if (saveAntiFeatures) { - // Get the database ID of the row we just updated, so that we can join relevant anti features to it. - Cursor result = db().query(getTableName(), new String[]{Cols.ROW_ID}, - query.getSelection(), query.getArgs(), null, null, null); - if (result != null) { - result.moveToFirst(); - long apkId = result.getLong(0); - ensureAntiFeatures(antiFeatures, apkId); - result.close(); - } - } - - if (!isApplyingBatch()) { - getContext().getContentResolver().notifyChange(uri, null); - } - return numRows; - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index 18f2b06af..801357c8e 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -1,6 +1,5 @@ package org.fdroid.fdroid.data; -import android.content.ContentValues; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -8,12 +7,9 @@ import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.content.res.XmlResourceParser; -import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Build; import android.os.Environment; -import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -25,7 +21,6 @@ import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.commons.io.filefilter.RegexFileFilter; import org.fdroid.database.AppListItem; import org.fdroid.database.Repository; import org.fdroid.database.UpdatableApp; @@ -35,24 +30,18 @@ import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; import org.fdroid.fdroid.net.TreeUriDownloader; import org.fdroid.index.v2.FileV2; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.File; -import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.Set; import info.guardianproject.netcipher.NetCipher; @@ -75,7 +64,7 @@ import androidx.core.os.LocaleListCompat; * @see fdroiddata * @see fdroidserver */ -public class App extends ValueObject implements Comparable, Parcelable { +public class App implements Comparable, Parcelable { @JsonIgnore private static final String TAG = "App"; @@ -83,7 +72,7 @@ public class App extends ValueObject implements Comparable, Parcelable { /** * {@link LocaleListCompat} for finding the right app description material. * It is set globally static to a) cache this value, since there are thousands - * of {@link App} entries, and b) make it easy to test {@link #setLocalized(Map)} )} + * of {@link App} entries, and b) make it easy to test} */ @JsonIgnore public static LocaleListCompat systemLocaleList; @@ -210,9 +199,7 @@ public class App extends ValueObject implements Comparable, Parcelable { /** * Unlike other public fields, this is only accessible via a getter, to - * emphasise that setting it wont do anything. In order to change this, - * you need to change {@link #autoInstallVersionCode} to an APK which is - * in the {@link Schema.ApkTable} table. + * emphasise that setting it wont do anything. */ private String autoInstallVersionName; @@ -229,7 +216,7 @@ public class App extends ValueObject implements Comparable, 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}. + * an app is in any other part of F-Droid, use the database. */ public String[] categories; @@ -252,10 +239,6 @@ public class App extends ValueObject implements Comparable, Parcelable { */ public String iconUrl; - public static String getIconName(String packageName, long versionCode) { - return packageName + "_" + versionCode + ".png"; - } - @Override public int compareTo(@NonNull App app) { return name.compareToIgnoreCase(app.name); @@ -264,163 +247,6 @@ public class App extends ValueObject implements Comparable, Parcelable { public App() { } - public App(final Cursor cursor) { - - checkCursorPosition(cursor); - - final int cursorColumnCount = cursor.getColumnCount(); - for (int i = 0; i < cursorColumnCount; i++) { - final String n = cursor.getColumnName(i); - switch (n) { - 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.PACKAGE_NAME: - packageName = cursor.getString(i); - break; - case Cols.NAME: - name = cursor.getString(i); - break; - case Cols.SUMMARY: - summary = cursor.getString(i); - break; - case Cols.ICON: - iconFromApk = cursor.getString(i); - break; - case Cols.DESCRIPTION: - description = cursor.getString(i); - break; - case Cols.WHATSNEW: - whatsNew = cursor.getString(i); - break; - case Cols.LICENSE: - license = cursor.getString(i); - break; - case Cols.AUTHOR_NAME: - authorName = cursor.getString(i); - break; - case Cols.AUTHOR_EMAIL: - authorEmail = cursor.getString(i); - break; - case Cols.WEBSITE: - webSite = cursor.getString(i); - break; - case Cols.ISSUE_TRACKER: - issueTracker = cursor.getString(i); - break; - case Cols.SOURCE_CODE: - sourceCode = cursor.getString(i); - break; - case Cols.TRANSLATION: - translation = cursor.getString(i); - break; - case Cols.VIDEO: - video = cursor.getString(i); - break; - case Cols.CHANGELOG: - changelog = cursor.getString(i); - break; - case Cols.DONATE: - donate = cursor.getString(i); - break; - case Cols.BITCOIN: - bitcoin = cursor.getString(i); - break; - case Cols.LITECOIN: - litecoin = cursor.getString(i); - break; - case Cols.FLATTR_ID: - flattrID = cursor.getString(i); - break; - case Cols.LIBERAPAY: - liberapay = cursor.getString(i); - break; - case Cols.OPEN_COLLECTIVE: - openCollective = cursor.getString(i); - break; - case Cols.AutoInstallApk.VERSION_NAME: - autoInstallVersionName = cursor.getString(i); - break; - case Cols.PREFERRED_SIGNER: - preferredSigner = cursor.getString(i); - break; - case Cols.AUTO_INSTALL_VERSION_CODE: - autoInstallVersionCode = cursor.getInt(i); - break; - case Cols.SUGGESTED_VERSION_CODE: - suggestedVersionCode = cursor.getInt(i); - break; - case Cols.SUGGESTED_VERSION_NAME: - suggestedVersionName = cursor.getString(i); - break; - case Cols.ADDED: - added = Utils.parseDate(cursor.getString(i), null); - break; - case Cols.LAST_UPDATED: - lastUpdated = Utils.parseDate(cursor.getString(i), null); - break; - case Cols.ANTI_FEATURES: - antiFeatures = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.REQUIREMENTS: - requirements = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.ICON_URL: - iconUrl = cursor.getString(i); - break; - case Cols.FEATURE_GRAPHIC: - featureGraphic = cursor.getString(i); - break; - case Cols.PROMO_GRAPHIC: - promoGraphic = cursor.getString(i); - break; - case Cols.TV_BANNER: - tvBanner = cursor.getString(i); - break; - case Cols.PHONE_SCREENSHOTS: - phoneScreenshots = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.SEVEN_INCH_SCREENSHOTS: - sevenInchScreenshots = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.TEN_INCH_SCREENSHOTS: - tenInchScreenshots = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.TV_SCREENSHOTS: - tvScreenshots = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.WEAR_SCREENSHOTS: - wearScreenshots = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.IS_APK: - isApk = cursor.getInt(i) == 1; - break; - case Cols.IS_LOCALIZED: - isLocalized = cursor.getInt(i) == 1; - break; - case Cols.InstalledApp.VERSION_CODE: - installedVersionCode = cursor.getInt(i); - break; - case Cols.InstalledApp.VERSION_NAME: - installedVersionName = cursor.getString(i); - break; - case Cols.InstalledApp.SIGNATURE: - installedSig = cursor.getString(i); - break; - case "_id": - break; - default: - Log.e(TAG, "Unknown column name " + n); - } - } - } - public App(final UpdatableApp app) { id = 0; repoId = app.getUpdate().getRepoId(); @@ -573,255 +399,6 @@ public class App extends ValueObject implements Comparable, Parcelable { } } - /** - * {@link #liberapay} was originally included using a numeric ID, now it is a - * username. This should not override {@link #liberapay} if that is already set. - */ - @JsonProperty("liberapayID") - void setLiberapayID(String liberapayId) { // NOPMD - if (TextUtils.isEmpty(liberapayId) || !TextUtils.isEmpty(liberapay)) { - return; - } - try { - int id = Integer.parseInt(liberapayId); - if (id > 0) { - liberapay = "~" + liberapayId; - } - } catch (NumberFormatException e) { - // ignored - } - } - - /** - * Parses the {@code localized} block in the incoming index metadata, - * choosing the best match in terms of locale/language while filling as - * many fields as possible. It first sets up a locale list based on user - * preference and the locales available for this app, then picks the texts - * based on that list. One thing that makes this tricky is that any given - * locale block in the index might not have all the fields. So when filling - * out each value, it needs to go through the whole preference list each time, - * rather than just taking the whole block for a specific locale. This is to - * ensure that there is something to show, as often as possible. - *

- * It is still possible that the fields will be loaded directly by Jackson - * without any locale info. This comes from the old-style, inline app metadata - * fields that do not have locale info. They should not be used if the - * {@code localized} block is included in the index. Also, null strings in - * the {@code localized} block should not overwrite Name/Summary/Description - * strings with empty/null if they were set directly by Jackson. - *

    - *
  1. the country variant {@code de-AT} from the user locale list - *
  2. only the language {@code de} from the above locale - *
  3. next locale in the user's preference list ({@code >= android-24}) - *
  4. {@code en-US} since its the most common English for software - *
  5. the first available {@code en} locale - *
- *

- * The system-wide language preference list was added in {@code android-24}. - * - * @see Android language and locale resolution overview - */ - @JsonProperty("localized") - void setLocalized(Map> localized) { // NOPMD - if (systemLocaleList == null) { - systemLocaleList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()); - } - Set supportedLocales = localized.keySet(); - setIsLocalized(supportedLocales); - String value = getLocalizedEntry(localized, supportedLocales, "whatsNew"); - if (!TextUtils.isEmpty(value)) { - whatsNew = value; - } - - value = getLocalizedEntry(localized, supportedLocales, "video"); - if (!TextUtils.isEmpty(value)) { - video = value.trim(); - } - value = getLocalizedEntry(localized, supportedLocales, "name"); - if (!TextUtils.isEmpty(value)) { - name = value.trim(); - } - value = getLocalizedEntry(localized, supportedLocales, "summary"); - if (!TextUtils.isEmpty(value)) { - summary = value.trim(); - } - value = getLocalizedEntry(localized, supportedLocales, "description"); - if (!TextUtils.isEmpty(value)) { - description = formatDescription(value); - } - value = getLocalizedGraphicsEntry(localized, supportedLocales, "icon"); - if (!TextUtils.isEmpty(value)) { - iconUrl = value; - } - - featureGraphic = getLocalizedGraphicsEntry(localized, supportedLocales, "featureGraphic"); - promoGraphic = getLocalizedGraphicsEntry(localized, supportedLocales, "promoGraphic"); - tvBanner = getLocalizedGraphicsEntry(localized, supportedLocales, "tvBanner"); - - wearScreenshots = getLocalizedListEntry(localized, supportedLocales, "wearScreenshots"); - phoneScreenshots = getLocalizedListEntry(localized, supportedLocales, "phoneScreenshots"); - sevenInchScreenshots = getLocalizedListEntry(localized, supportedLocales, "sevenInchScreenshots"); - tenInchScreenshots = getLocalizedListEntry(localized, supportedLocales, "tenInchScreenshots"); - tvScreenshots = getLocalizedListEntry(localized, supportedLocales, "tvScreenshots"); - } - - /** - * Sets the boolean flag {@link #isLocalized} if this app entry has an localized - * entry in one of the user's current locales. - * - * @see org.fdroid.fdroid.views.main.WhatsNewViewBinder#onCreateLoader(int, android.os.Bundle) - */ - private void setIsLocalized(Set supportedLocales) { - isLocalized = false; - for (int i = 0; i < systemLocaleList.size(); i++) { - String language = systemLocaleList.get(i).getLanguage(); - for (String supportedLocale : supportedLocales) { - if (language.equals(supportedLocale.split("-")[0])) { - isLocalized = true; - return; - } - } - } - } - - /** - * Returns the right localized version of this entry, based on an imitation of - * the logic that Android uses. - * - * @see LocaleList - */ - private String getLocalizedEntry(Map> localized, - Set supportedLocales, @NonNull String key) { - Map localizedLocaleMap = getLocalizedLocaleMap(localized, supportedLocales, key); - if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) { - for (Object entry : localizedLocaleMap.values()) { - return (String) entry; // NOPMD - } - } - return null; - } - - private String getLocalizedGraphicsEntry(Map> localized, - Set supportedLocales, @NonNull String key) { - Map localizedLocaleMap = getLocalizedLocaleMap(localized, supportedLocales, key); - if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) { - for (String locale : localizedLocaleMap.keySet()) { - return locale + "/" + localizedLocaleMap.get(locale); // NOPMD - } - } - return null; - } - - private String[] getLocalizedListEntry(Map> localized, - Set supportedLocales, @NonNull String key) { - Map localizedLocaleMap = getLocalizedLocaleMap(localized, supportedLocales, key); - if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) { - for (String locale : localizedLocaleMap.keySet()) { - ArrayList entry = (ArrayList) localizedLocaleMap.get(locale); - if (entry != null && entry.size() > 0) { - String[] result = new String[entry.size()]; - int i = 0; - for (String e : entry) { - result[i] = locale + "/" + key + "/" + e; - i++; - } - return result; - } - } - } - return new String[0]; - } - - /** - * Return one matching entry from the {@code localized} block in the app entry - * in the index JSON. - */ - private Map getLocalizedLocaleMap(Map> localized, - Set supportedLocales, @NonNull String key) { - String[] localesToUse = getLocalesForKey(localized, supportedLocales, key); - if (localesToUse.length > 0) { - Locale firstMatch = systemLocaleList.getFirstMatch(localesToUse); - if (firstMatch != null) { - for (String languageTag : new String[]{toLanguageTag(firstMatch), null}) { - if (languageTag == null) { - languageTag = getFallbackLanguageTag(firstMatch, localesToUse); // NOPMD - } - Map localeEntry = localized.get(languageTag); - if (localeEntry != null && localeEntry.containsKey(key)) { - Object value = localeEntry.get(key); - if (value != null) { - Map localizedLocaleMap = new HashMap<>(); - localizedLocaleMap.put(languageTag, value); - return localizedLocaleMap; - } - } - } - } - } - return null; - } - - /** - * Replace with {@link Locale#toLanguageTag()} once - * {@link android.os.Build.VERSION_CODES#LOLLIPOP} is {@code minSdkVersion} - */ - private String toLanguageTag(Locale firstMatch) { - if (Build.VERSION.SDK_INT < 21) { - return firstMatch.toString().replace("_", "-"); - } else { - return firstMatch.toLanguageTag(); - } - } - - /** - * Get all locales that have an entry for {@code key}. - */ - private String[] getLocalesForKey(Map> localized, - Set supportedLocales, @NonNull String key) { - Set localesToUse = new HashSet<>(); - for (String locale : supportedLocales) { - Map localeEntry = localized.get(locale); - if (localeEntry != null && localeEntry.get(key) != null) { - localesToUse.add(locale); - } - } - return localesToUse.toArray(new String[0]); - } - - /** - * Look for the first language-country match for languages with multiple scripts. - * Then look for a language-only match, for when there is no exact - * {@link Locale} match. Then try a locale with the same language, but - * different country. If there are still no matches, return the {@code en-US} - * entry. If all else fails, try to return the first existing English locale. - */ - private String getFallbackLanguageTag(Locale firstMatch, String[] localesToUse) { - final String firstMatchLanguageCountry = firstMatch.getLanguage() + "-" + firstMatch.getCountry(); - for (String languageTag : localesToUse) { - if (languageTag.equals(firstMatchLanguageCountry)) { - return languageTag; - } - } - final String firstMatchLanguage = firstMatch.getLanguage(); - String englishLastResort = null; - for (String languageTag : localesToUse) { - if (languageTag.equals(firstMatchLanguage)) { - return languageTag; - } else if ("en-US".equals(languageTag)) { - englishLastResort = languageTag; - } - } - for (String languageTag : localesToUse) { - String languageToUse = languageTag.split("-")[0]; - if (firstMatchLanguage.equals(languageToUse)) { - return languageTag; - } else if (englishLastResort == null && "en".equals(languageToUse)) { - englishLastResort = languageTag; - } - } - return englishLastResort; - } - /** * Returns the app description text with all newlines replaced by {@code
} */ @@ -957,29 +534,6 @@ public class App extends ValueObject implements Comparable, Parcelable { + "/Android/obb/" + packageName); } - public static void initInstalledObbFiles(Apk apk) { - File obbdir = getObbDir(apk.packageName); - FileFilter filter = new RegexFileFilter("(main|patch)\\.[0-9-][0-9]*\\." + apk.packageName + "\\.obb"); - File[] files = obbdir.listFiles(filter); - if (files == null) { - return; - } - Arrays.sort(files); - for (File f : files) { - String filename = f.getName(); - String[] segments = filename.split("\\."); - if (Integer.parseInt(segments[1]) <= apk.versionCode) { - if ("main".equals(segments[0])) { - apk.obbMainFile = filename; - apk.obbMainFileSha256 = Utils.getFileHexDigest(f, apk.hashType); - } else if ("patch".equals(segments[0])) { - apk.obbPatchFile = filename; - apk.obbPatchFileSha256 = Utils.getFileHexDigest(f, apk.hashType); - } - } - } - } - /** * Attempts to find the installed {@link Apk} in the given list of APKs. If not found, will lookup the * the details of the installed app and use that to instantiate an {@link Apk} to be returned. @@ -1029,58 +583,6 @@ public class App extends ValueObject implements Comparable, Parcelable { } - public ContentValues toContentValues() { - - 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.PACKAGE_NAME, packageName); - values.put(Cols.NAME, name); - values.put(Cols.REPO_ID, repoId); - values.put(Cols.SUMMARY, summary); - values.put(Cols.ICON, iconFromApk); - values.put(Cols.ICON_URL, iconUrl); - values.put(Cols.DESCRIPTION, description); - values.put(Cols.WHATSNEW, whatsNew); - values.put(Cols.LICENSE, license); - values.put(Cols.AUTHOR_NAME, authorName); - values.put(Cols.AUTHOR_EMAIL, authorEmail); - values.put(Cols.WEBSITE, webSite); - values.put(Cols.ISSUE_TRACKER, issueTracker); - values.put(Cols.SOURCE_CODE, sourceCode); - values.put(Cols.TRANSLATION, translation); - values.put(Cols.VIDEO, video); - values.put(Cols.CHANGELOG, changelog); - values.put(Cols.DONATE, donate); - values.put(Cols.BITCOIN, bitcoin); - values.put(Cols.LITECOIN, litecoin); - values.put(Cols.FLATTR_ID, flattrID); - values.put(Cols.LIBERAPAY, liberapay); - values.put(Cols.OPEN_COLLECTIVE, openCollective); - values.put(Cols.ADDED, Utils.formatDate(added, "")); - values.put(Cols.LAST_UPDATED, Utils.formatDate(lastUpdated, "")); - values.put(Cols.PREFERRED_SIGNER, preferredSigner); - values.put(Cols.AUTO_INSTALL_VERSION_CODE, autoInstallVersionCode); - values.put(Cols.SUGGESTED_VERSION_NAME, suggestedVersionName); - values.put(Cols.SUGGESTED_VERSION_CODE, suggestedVersionCode); - 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.FEATURE_GRAPHIC, featureGraphic); - values.put(Cols.PROMO_GRAPHIC, promoGraphic); - values.put(Cols.TV_BANNER, tvBanner); - values.put(Cols.PHONE_SCREENSHOTS, Utils.serializeCommaSeparatedString(phoneScreenshots)); - values.put(Cols.SEVEN_INCH_SCREENSHOTS, Utils.serializeCommaSeparatedString(sevenInchScreenshots)); - values.put(Cols.TEN_INCH_SCREENSHOTS, Utils.serializeCommaSeparatedString(tenInchScreenshots)); - values.put(Cols.TV_SCREENSHOTS, Utils.serializeCommaSeparatedString(tvScreenshots)); - values.put(Cols.WEAR_SCREENSHOTS, Utils.serializeCommaSeparatedString(wearScreenshots)); - values.put(Cols.IS_COMPATIBLE, compatible ? 1 : 0); - values.put(Cols.IS_APK, isApk ? 1 : 0); - values.put(Cols.IS_LOCALIZED, isLocalized ? 1 : 0); - - return values; - } - public boolean isInstalled(Context context) { // First check isApk() before isMediaInstalled() because the latter is quite expensive, // hitting the database for each apk version, then the disk to check for installed media. @@ -1170,23 +672,6 @@ public class App extends ValueObject implements Comparable, Parcelable { return apk; } - @Deprecated - public AppPrefs getPrefs(Context context) { - return AppPrefs.createDefault(); - } - - /** - * True if there are new versions (apks) available and the user wants - * to be notified about them - */ - @Deprecated - public boolean canAndWantToUpdate(Context context) { - boolean canUpdate = hasUpdates(); - final org.fdroid.database.AppPrefs prefs = this.prefs; - boolean wantsUpdate = prefs == null || !prefs.shouldIgnoreUpdate(autoInstallVersionCode); - return canUpdate && wantsUpdate; - } - /** * True if there are new versions (apks) available and the user wants to be notified about them */ @@ -1468,9 +953,4 @@ public class App extends ValueObject implements Comparable, Parcelable { return null; } - @Override - public String toString() { - return toContentValues().toString(); - } - } diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppPrefs.java b/app/src/main/java/org/fdroid/fdroid/data/AppPrefs.java deleted file mode 100644 index fee69d192..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/AppPrefs.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.fdroid.fdroid.data; - -public class AppPrefs extends ValueObject { - - /** - * True if all updates for this app are to be ignored. - */ - public boolean ignoreAllUpdates; - - /** - * The version code of the app for which the update should be ignored. - */ - public int ignoreThisUpdate; - - /** - * Don't notify of vulnerabilities in this app. - */ - public boolean ignoreVulnerabilities; - - public AppPrefs(int ignoreThis, boolean ignoreAll, boolean ignoreVulns) { - ignoreThisUpdate = ignoreThis; - ignoreAllUpdates = ignoreAll; - ignoreVulnerabilities = ignoreVulns; - } - - public static AppPrefs createDefault() { - return new AppPrefs(0, false, false); - } - - @Override - public boolean equals(Object o) { - return o != null && o instanceof AppPrefs && - ((AppPrefs) o).ignoreAllUpdates == ignoreAllUpdates && - ((AppPrefs) o).ignoreThisUpdate == ignoreThisUpdate && - ((AppPrefs) o).ignoreVulnerabilities == ignoreVulnerabilities; - } - - @Override - public int hashCode() { - return (ignoreThisUpdate + "-" + ignoreAllUpdates + "-" + ignoreVulnerabilities).hashCode(); - } - - public AppPrefs createClone() { - return new AppPrefs(ignoreThisUpdate, ignoreAllUpdates, ignoreVulnerabilities); - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppPrefsProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppPrefsProvider.java deleted file mode 100644 index e8550c03f..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/AppPrefsProvider.java +++ /dev/null @@ -1,164 +0,0 @@ -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.AppPrefsTable; -import org.fdroid.fdroid.data.Schema.AppPrefsTable.Cols; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class AppPrefsProvider extends FDroidProvider { - - public static final class Helper { - private Helper() { - } - - public static void update(Context context, App app, AppPrefs prefs) { - ContentValues values = new ContentValues(3); - values.put(Cols.IGNORE_ALL_UPDATES, prefs.ignoreAllUpdates); - values.put(Cols.IGNORE_THIS_UPDATE, prefs.ignoreThisUpdate); - values.put(Cols.IGNORE_VULNERABILITIES, prefs.ignoreVulnerabilities); - - if (getPrefsOrNull(context, app) == null) { - values.put(Cols.PACKAGE_NAME, app.packageName); - context.getContentResolver().insert(getContentUri(), values); - } else { - context.getContentResolver().update(getAppUri(app.packageName), values, null, null); - } - } - - @NonNull - public static AppPrefs getPrefsOrDefault(Context context, App app) { - AppPrefs prefs = getPrefsOrNull(context, app); - return prefs == null ? AppPrefs.createDefault() : prefs; - } - - @Nullable - public static AppPrefs getPrefsOrNull(Context context, App app) { - Cursor cursor = context.getContentResolver().query(getAppUri(app.packageName), Cols.ALL, - null, null, null); - if (cursor == null) { - return null; - } - - try { - if (cursor.getCount() == 0) { - return null; - } - - cursor.moveToFirst(); - return new AppPrefs( - cursor.getInt(cursor.getColumnIndexOrThrow(Cols.IGNORE_THIS_UPDATE)), - cursor.getInt(cursor.getColumnIndexOrThrow(Cols.IGNORE_ALL_UPDATES)) > 0, - cursor.getInt(cursor.getColumnIndexOrThrow(Cols.IGNORE_VULNERABILITIES)) > 0); - } finally { - cursor.close(); - } - } - } - - private class Query extends QueryBuilder { - - @Override - protected String getRequiredTables() { - return AppPrefsTable.NAME; - } - - @Override - public void addField(String field) { - appendField(field, getTableName()); - } - } - - private static final String PROVIDER_NAME = "AppPrefsProvider"; - - private static final UriMatcher MATCHER = new UriMatcher(-1); - - private static final String PATH_PACKAGE_NAME = "packageName"; - - static { - MATCHER.addURI(getAuthority(), PATH_PACKAGE_NAME + "/*", CODE_SINGLE); - } - - private static Uri getContentUri() { - return Uri.parse("content://" + getAuthority()); - } - - public static Uri getAppUri(String packageName) { - return getContentUri().buildUpon().appendPath(PATH_PACKAGE_NAME).appendPath(packageName).build(); - } - - @Override - protected String getTableName() { - return AppPrefsTable.NAME; - } - - @Override - protected String getProviderName() { - return "AppPrefsProvider"; - } - - public static String getAuthority() { - return AUTHORITY + "." + PROVIDER_NAME; - } - - @Override - protected UriMatcher getMatcher() { - return MATCHER; - } - - protected QuerySelection querySingle(String packageName) { - final String selection = getTableName() + "." + Cols.PACKAGE_NAME + " = ?"; - final String[] args = {packageName}; - return new QuerySelection(selection, args); - } - - @Override - public Cursor query(@NonNull Uri uri, String[] projection, - String customSelection, String[] selectionArgs, String sortOrder) { - if (MATCHER.match(uri) != CODE_SINGLE) { - throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri); - } - - QuerySelection selection = new QuerySelection(customSelection, selectionArgs) - .add(querySingle(uri.getLastPathSegment())); - - Query query = new Query(); - query.addSelection(selection); - query.addFields(projection); - query.addOrderBy(sortOrder); - - Cursor cursor = LoggingQuery.rawQuery(db(), query.toString(), query.getArgs()); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - return cursor; - } - - @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) { - db().insertOrThrow(getTableName(), null, values); - getContext().getContentResolver().notifyChange(AppProvider.getCanUpdateUri(), null); - return getAppUri(values.getAsString(Cols.PACKAGE_NAME)); - } - - @Override - public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) { - if (MATCHER.match(uri) != CODE_SINGLE) { - throw new UnsupportedOperationException("Update not supported for " + uri + "."); - } - - QuerySelection query = new QuerySelection(where, whereArgs).add(querySingle(uri.getLastPathSegment())); - int count = db().update(getTableName(), values, query.getSelection(), query.getArgs()); - getContext().getContentResolver().notifyChange(AppProvider.getCanUpdateUri(), null); - return count; - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java deleted file mode 100644 index 4790ef0dc..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ /dev/null @@ -1,1200 +0,0 @@ -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.text.TextUtils; -import android.util.Log; - -import org.fdroid.fdroid.Preferences; -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable; -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.AppPrefsTable; -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.HashSet; -import java.util.List; -import java.util.Set; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * 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. - */ -@SuppressWarnings("LineLength") -public class AppProvider extends FDroidProvider { - - private static final String TAG = "AppProvider"; - - public static final class Helper { - - private Helper() { - } - - public static List all(ContentResolver resolver) { - return all(resolver, Cols.ALL); - } - - public static List all(ContentResolver resolver, String[] projection) { - final Uri uri = AppProvider.getContentUri(); - Cursor cursor = resolver.query(uri, projection, null, null, null); - return cursorToList(cursor); - } - - static List cursorToList(Cursor cursor) { - int knownAppCount = cursor != null ? cursor.getCount() : 0; - List apps = new ArrayList<>(knownAppCount); - if (cursor != null) { - if (knownAppCount > 0) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - apps.add(new App(cursor)); - cursor.moveToNext(); - } - } - cursor.close(); - } - return apps; - } - - public static App findHighestPriorityMetadata(ContentResolver resolver, String packageName, String[] cols) { - final Uri uri = getHighestPriorityMetadataUri(packageName); - return cursorToApp(resolver.query(uri, cols, null, null, null)); - } - - public static App findHighestPriorityMetadata(ContentResolver resolver, String packageName) { - return findHighestPriorityMetadata(resolver, packageName, Cols.ALL); - } - - /** - * 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) { - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - app = new App(cursor); - } - cursor.close(); - } - return app; - } - - public static void calcSuggestedApk(Context context, String packageName) { - Uri uri = Uri.withAppendedPath(calcSuggestedApksUri(), packageName); - context.getContentResolver().update(uri, null, null, null); - } - - public static void calcSuggestedApks(Context context) { - context.getContentResolver().update(calcSuggestedApksUri(), null, null, null); - } - - public static List 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); - } - - public static List findInstalledAppsWithKnownVulns(Context context) { - Uri uri = getInstalledWithKnownVulnsUri(); - Cursor cursor = context.getContentResolver().query(uri, Cols.ALL, null, null, null); - return cursorToList(cursor); - } - } - - /** - * A QuerySelection which is aware of the option/need to join onto the - * installed apps table. Note that the base classes - * {@link org.fdroid.fdroid.data.QuerySelection#add(QuerySelection)} and - * {@link org.fdroid.fdroid.data.QuerySelection#add(String, String[])} methods - * will only return the base class {@link org.fdroid.fdroid.data.QuerySelection} - * which is not aware of the installed app table. - * However, the - * {@link org.fdroid.fdroid.data.AppProvider.AppQuerySelection#add(org.fdroid.fdroid.data.AppProvider.AppQuerySelection)} - * method from this class will return an instance of this class, that is aware of - * the install apps table. - */ - protected static class AppQuerySelection extends QuerySelection { - - private boolean naturalJoinToInstalled; - private boolean naturalJoinApks; - private boolean naturalJoinAntiFeatures; - private boolean leftJoinPrefs; - - AppQuerySelection() { - // The same as no selection, because "1" will always resolve to true when executing the SQL query. - // e.g. "WHERE 1 AND ..." is the same as "WHERE ..." - super("1"); - } - - AppQuerySelection(String selection) { - super(selection); - } - - AppQuerySelection(String selection, String[] args) { - super(selection, args); - } - - public boolean naturalJoinToInstalled() { - return naturalJoinToInstalled; - } - - public boolean naturalJoinToApks() { - return naturalJoinApks; - } - - public boolean naturalJoinAntiFeatures() { - return naturalJoinAntiFeatures; - } - - /** - * Tells the query selection that it will need to join onto the installed apps table - * when used. This should be called when your query makes use of fields from that table - * (for example, list all installed, or list those which can be updated). - * - * @return A reference to this object, to allow method chaining, for example - * return new AppQuerySelection(selection).requiresInstalledTable()) - */ - public AppQuerySelection requireNaturalInstalledTable() { - naturalJoinToInstalled = true; - return this; - } - - /** - * Note that this has large performance implications, so should only be used if you are already limiting - * the result set based on other, more drastic conditions first. - * See https://gitlab.com/fdroid/fdroidclient/issues/1143 for the investigation which identified these - * performance implications. - */ - public AppQuerySelection requireNaturalJoinApks() { - naturalJoinApks = true; - return this; - } - - public AppQuerySelection requireNatrualJoinAntiFeatures() { - naturalJoinAntiFeatures = true; - return this; - } - - public boolean leftJoinToPrefs() { - return leftJoinPrefs; - } - - public AppQuerySelection requireLeftJoinPrefs() { - leftJoinPrefs = true; - return this; - } - - public AppQuerySelection add(AppQuerySelection query) { - QuerySelection both = super.add(query); - AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs()); - if (this.naturalJoinToInstalled() || query.naturalJoinToInstalled()) { - bothWithJoin.requireNaturalInstalledTable(); - } - - if (this.naturalJoinToApks() || query.naturalJoinToApks()) { - bothWithJoin.requireNaturalJoinApks(); - } - - if (this.leftJoinToPrefs() || query.leftJoinToPrefs()) { - bothWithJoin.requireLeftJoinPrefs(); - } - - if (this.naturalJoinAntiFeatures() || query.naturalJoinAntiFeatures()) { - bothWithJoin.requireNatrualJoinAntiFeatures(); - } - - return bothWithJoin; - } - - } - - protected class Query extends QueryBuilder { - - private boolean isSuggestedApkTableAdded; - private boolean requiresInstalledTable; - private boolean requiresApkTable; - private boolean requiresAntiFeatures; - private boolean requiresLeftJoinToPrefs; - private boolean countFieldAppended; - - @Override - protected String getRequiredTables() { - final String pkg = PackageTable.NAME; - final String app = getTableName(); - 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 + ") "; - } - - @Override - protected String groupBy() { - // 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; - } - - public void addSelection(AppQuerySelection selection) { - super.addSelection(selection); - if (selection.naturalJoinToInstalled()) { - naturalJoinToInstalledTable(); - } - if (selection.naturalJoinToApks()) { - naturalJoinToApkTable(); - } - if (selection.leftJoinToPrefs()) { - leftJoinToPrefs(); - } - if (selection.naturalJoinAntiFeatures()) { - naturalJoinAntiFeatures(); - } - } - - // TODO: What if the selection requires a natural join, but we first get a left join - // because something causes leftJoin to be caused first? Maybe throw an exception? - public void naturalJoinToInstalledTable() { - if (!requiresInstalledTable) { - join( - InstalledAppTable.NAME, - "installed", - "installed." + InstalledAppTable.Cols.PACKAGE_ID + " = " + PackageTable.NAME + "." + PackageTable.Cols.ROW_ID); - requiresInstalledTable = true; - } - } - - public void naturalJoinToApkTable() { - if (!requiresApkTable) { - join( - getApkTableName(), - getApkTableName(), - getApkTableName() + "." + ApkTable.Cols.APP_ID + " = " + getTableName() + "." + Cols.ROW_ID - ); - requiresApkTable = true; - } - } - - public void leftJoinToPrefs() { - if (!requiresLeftJoinToPrefs) { - leftJoin( - AppPrefsTable.NAME, - "prefs", - "prefs." + AppPrefsTable.Cols.PACKAGE_NAME + " = " + PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME); - requiresLeftJoinToPrefs = true; - } - } - - public void leftJoinToInstalledTable() { - if (!requiresInstalledTable) { - leftJoin( - InstalledAppTable.NAME, - "installed", - "installed." + InstalledAppTable.Cols.PACKAGE_ID + " = " + PackageTable.NAME + "." + PackageTable.Cols.ROW_ID); - requiresInstalledTable = true; - } - } - - public void naturalJoinAntiFeatures() { - if (!requiresAntiFeatures) { - join( - getApkAntiFeatureJoinTableName(), - "apkAntiFeature", - "apkAntiFeature." + ApkAntiFeatureJoinTable.Cols.APK_ID + " = " + getApkTableName() + "." + ApkTable.Cols.ROW_ID); - - join( - Schema.AntiFeatureTable.NAME, - "antiFeature", - "antiFeature." + Schema.AntiFeatureTable.Cols.ROW_ID + " = " + "apkAntiFeature." + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID); - - requiresAntiFeatures = true; - } - } - - @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.AutoInstallApk.VERSION_NAME: - addAutoInstallApkVersionField(); - break; - case Cols.InstalledApp.VERSION_NAME: - addInstalledAppVersionName(); - break; - case Cols.InstalledApp.VERSION_CODE: - addInstalledAppVersionCode(); - break; - case Cols.InstalledApp.SIGNATURE: - addInstalledSig(); - break; - case Cols._COUNT: - appendCountField(); - break; - default: - appendField(field, getTableName()); - break; - } - } - - private void appendCountField() { - countFieldAppended = true; - appendField("COUNT( DISTINCT " + getTableName() + "." + Cols.ROW_ID + " ) AS " + Cols._COUNT); - } - - private void addAutoInstallApkVersionField() { - addAutoInstallApkField( - ApkTable.Cols.VERSION_NAME, - Cols.AutoInstallApk.VERSION_NAME); - } - - /** - * @see suggestedApk name mismatch #1063 - */ - private void addAutoInstallApkField(String fieldName, String alias) { - if (!isSuggestedApkTableAdded) { - isSuggestedApkTableAdded = true; - leftJoin( - getApkTableName(), - "suggestedApk", - getTableName() + "." + Cols.AUTO_INSTALL_VERSION_CODE + " = suggestedApk." + ApkTable.Cols.VERSION_CODE + " AND " + getTableName() + "." + Cols.ROW_ID + " = suggestedApk." + ApkTable.Cols.APP_ID); - } - appendField(fieldName, "suggestedApk", alias); - } - - private void addInstalledAppVersionName() { - addInstalledAppField( - InstalledAppTable.Cols.VERSION_NAME, - Cols.InstalledApp.VERSION_NAME - ); - } - - private void addInstalledAppVersionCode() { - addInstalledAppField( - InstalledAppTable.Cols.VERSION_CODE, - Cols.InstalledApp.VERSION_CODE - ); - } - - private void addInstalledSig() { - addInstalledAppField( - InstalledAppTable.Cols.SIGNATURE, - Cols.InstalledApp.SIGNATURE - ); - } - - private void addInstalledAppField(String fieldName, String alias) { - leftJoinToInstalledTable(); - appendField(fieldName, "installed", alias); - } - } - - private static final String PROVIDER_NAME = "AppProvider"; - - private static final UriMatcher MATCHER = new UriMatcher(-1); - - private static final String PATH_INSTALLED = "installed"; - private static final String PATH_CAN_UPDATE = "canUpdate"; - private static final String PATH_SEARCH = "search"; - private static final String PATH_SEARCH_REPO = "searchRepo"; - protected static final String PATH_APPS = "apps"; - protected static final String PATH_SPECIFIC_APP = "app"; - private static final String PATH_LATEST_TAB = "recentlyUpdated"; - private static final String PATH_CATEGORY = "category"; - 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 String PATH_CALC_SUGGESTED_APKS = "calcNonRepoDetailsFromIndex"; - private static final String PATH_TOP_FROM_CATEGORY = "topFromCategory"; - private static final String PATH_INSTALLED_WITH_KNOWN_VULNS = "installedWithKnownVulns"; - - private static final int CAN_UPDATE = CODE_SINGLE + 1; - private static final int INSTALLED = CAN_UPDATE + 1; - private static final int SEARCH_TEXT = INSTALLED + 1; - private static final int SEARCH_TEXT_AND_CATEGORIES = SEARCH_TEXT + 1; - private static final int LATEST_TAB = SEARCH_TEXT_AND_CATEGORIES + 1; - private static final int CATEGORY = LATEST_TAB + 1; - private static final int CALC_SUGGESTED_APKS = CATEGORY + 1; - private static final int REPO = CALC_SUGGESTED_APKS + 1; - private static final int SEARCH_REPO = REPO + 1; - private static final int HIGHEST_PRIORITY = SEARCH_REPO + 1; - private static final int CALC_PREFERRED_METADATA = HIGHEST_PRIORITY + 1; - private static final int TOP_FROM_CATEGORY = CALC_PREFERRED_METADATA + 1; - private static final int INSTALLED_WITH_KNOWN_VULNS = TOP_FROM_CATEGORY + 1; - - static { - MATCHER.addURI(getAuthority(), null, CODE_LIST); - MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS, CALC_SUGGESTED_APKS); - MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS + "/*", CALC_SUGGESTED_APKS); - MATCHER.addURI(getAuthority(), PATH_LATEST_TAB, LATEST_TAB); - MATCHER.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY); - MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*/*", SEARCH_TEXT_AND_CATEGORIES); - MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH_TEXT); - MATCHER.addURI(getAuthority(), PATH_SEARCH_REPO + "/*/*", SEARCH_REPO); - MATCHER.addURI(getAuthority(), PATH_REPO + "/#", REPO); - MATCHER.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE); - MATCHER.addURI(getAuthority(), PATH_INSTALLED, INSTALLED); - 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); - MATCHER.addURI(getAuthority(), PATH_TOP_FROM_CATEGORY + "/#/*", TOP_FROM_CATEGORY); - MATCHER.addURI(getAuthority(), PATH_INSTALLED_WITH_KNOWN_VULNS, INSTALLED_WITH_KNOWN_VULNS); - } - - public static Uri getContentUri() { - return Uri.parse("content://" + getAuthority()); - } - - /** - * Get entries that are sorted by the {@link Schema.AppMetadataTable.Cols#LAST_UPDATED} - * date. - * - * @see #LATEST_TAB - */ - public static Uri getLatestTabUri() { - return Uri.withAppendedPath(getContentUri(), PATH_LATEST_TAB); - } - - private static Uri calcSuggestedApksUri() { - return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_APKS); - } - - /** - * Get all {@link App} entries in the given {@code category} - */ - public static Uri getCategoryUri(String category) { - return getContentUri().buildUpon() - .appendPath(PATH_CATEGORY) - .appendPath(category) - .build(); - } - - public static Uri getInstalledWithKnownVulnsUri() { - return getContentUri().buildUpon() - .appendPath(PATH_INSTALLED_WITH_KNOWN_VULNS) - .build(); - } - - /** - * Get the top {@link App} entries in the given {@code category} to display - * in the overview screen in {@link org.fdroid.fdroid.views.categories.CategoryController}. - * The number of entries is defined by {@code limit}. - * - * @see org.fdroid.fdroid.views.categories.CategoryController#onCreateLoader(int, android.os.Bundle) - */ - public static Uri getTopFromCategoryUri(String category, int limit) { - return getContentUri().buildUpon() - .appendPath(PATH_TOP_FROM_CATEGORY) - .appendPath(Integer.toString(limit)) - .appendPath(category) - .build(); - } - - public static Uri getInstalledUri() { - return Uri.withAppendedPath(getContentUri(), PATH_INSTALLED); - } - - public static Uri getCanUpdateUri() { - return Uri.withAppendedPath(getContentUri(), PATH_CAN_UPDATE); - } - - public static Uri getRepoUri(Repo repo) { - return getContentUri().buildUpon() - .appendPath(PATH_REPO) - .appendPath(String.valueOf(repo.id)) - .build(); - } - - /** - * @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 getSearchUri(String query, @Nullable String category) { - if (TextUtils.isEmpty(query) && TextUtils.isEmpty(category)) { - // Return all the things for an empty search. - return getContentUri(); - } else if (TextUtils.isEmpty(query)) { - return getCategoryUri(category); - } - - Uri.Builder builder = getContentUri().buildUpon() - .appendPath(PATH_SEARCH) - .appendPath(query); - - if (!TextUtils.isEmpty(category)) { - builder.appendPath(category); - } - - return builder.build(); - } - - public static Uri getSearchUri(Repo repo, String query) { - return getContentUri().buildUpon() - .appendPath(PATH_SEARCH_REPO) - .appendPath(String.valueOf(repo.id)) - .appendPath(query) - .build(); - } - - @Override - protected String getTableName() { - return AppMetadataTable.NAME; - } - - protected String getCatJoinTableName() { - return CatJoinTable.NAME; - } - - protected String getApkTableName() { - return ApkTable.NAME; - } - - protected String getApkAntiFeatureJoinTableName() { - return ApkAntiFeatureJoinTable.NAME; - } - - @Override - protected String getProviderName() { - return "AppProvider"; - } - - public static String getAuthority() { - return AUTHORITY + "." + PROVIDER_NAME; - } - - @Override - protected UriMatcher getMatcher() { - return MATCHER; - } - - private AppQuerySelection queryCanUpdate() { - final String app = getTableName(); - - // Need to use COALESCE because the prefs join may not resolve any rows, which means the - // ignore* fields will be NULL. In that case, we want to instead use a default value of 0. - final String ignoreCurrent = " COALESCE(prefs." + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ", 0) != " + app + "." + Cols.AUTO_INSTALL_VERSION_CODE; - final String ignoreAll = "COALESCE(prefs." + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + ", 0) != 1"; - - final String ignore = " (" + ignoreCurrent + " AND " + ignoreAll + ") "; - final String where = ignore + " AND " + app + "." + Cols.AUTO_INSTALL_VERSION_CODE + " > installed." + InstalledAppTable.Cols.VERSION_CODE; - - return new AppQuerySelection(where).requireNaturalInstalledTable().requireLeftJoinPrefs(); - } - - private AppQuerySelection queryRepo(long repoId) { - final String selection = getTableName() + "." + Cols.REPO_ID + " = ? "; - final String[] args = {String.valueOf(repoId)}; - return new AppQuerySelection(selection, args); - } - - private AppQuerySelection queryInstalled() { - return new AppQuerySelection().requireNaturalInstalledTable(); - } - - private AppQuerySelection querySearch(String query) { - // Put in a Set to remove duplicates - final Set keywordSet = new HashSet<>(Arrays.asList(query.split("\\s"))); - - if (keywordSet.isEmpty()) { - return new AppQuerySelection(); - } - - // Surround each keyword in % for wildcard searching - final String[] keywords = new String[keywordSet.size()]; - int iKeyword = 0; - for (final String keyword : keywordSet) { - keywords[iKeyword] = "%" + keyword + "%"; - iKeyword++; - } - - final String app = getTableName(); - final String[] columns = { - PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME, - app + "." + Cols.NAME, - app + "." + Cols.SUMMARY, - app + "." + Cols.DESCRIPTION, - }; - - // Build selection string and fill out keyword arguments - final StringBuilder selection = new StringBuilder(); - final String[] selectionKeywords = new String[columns.length * keywords.length]; - iKeyword = 0; - boolean firstColumn = true; - for (final String column : columns) { - if (firstColumn) { - firstColumn = false; - } else { - selection.append(" OR "); - } - selection.append('('); - boolean firstKeyword = true; - for (final String keyword : keywords) { - if (firstKeyword) { - firstKeyword = false; - } else { - selection.append(" AND "); - } - selection.append(column).append(" LIKE ?"); - selectionKeywords[iKeyword] = keyword; - iKeyword++; - } - selection.append(") "); - } - return new AppQuerySelection(selection.toString(), selectionKeywords); - } - - 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)); - } - - private AppQuerySelection queryExcludeSwap() { - // fdroid_repo will have null fields if the LEFT JOIN didn't resolve, e.g. due to there - // being no apks for the app in the result set. In that case, we can't tell if it is from - // a swap repo or not. - final String isSwap = RepoTable.NAME + "." + RepoTable.Cols.IS_SWAP; - final String selection = "COALESCE(" + isSwap + ", 0) = 0"; - return new AppQuerySelection(selection); - } - - /** - * 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 queryCategory(String category) { - if (TextUtils.isEmpty(category)) { - return new AppQuerySelection(); - } - - // Note, the COLLATE NOCASE only works for ASCII columns. The "ICU extension" for SQLite - // provides proper case management for Unicode characters, but is not something provided - // by Android. - final String selection = CategoryTable.NAME + "." + CategoryTable.Cols.NAME + " = ? COLLATE NOCASE "; - final String[] args = {category}; - return new AppQuerySelection(selection, args); - } - - private AppQuerySelection queryInstalledWithKnownVulns() { - String apk = getApkTableName(); - - // Include the hash in this check because otherwise apps with any vulnerable version will - // get returned, rather than just the installed version. - String compareHash = apk + "." + ApkTable.Cols.HASH + " = installed." + InstalledAppTable.Cols.HASH; - String knownVuln = " antiFeature." + Schema.AntiFeatureTable.Cols.NAME + " = 'KnownVuln' "; - String notIgnored = " COALESCE(prefs." + AppPrefsTable.Cols.IGNORE_VULNERABILITIES + ", 0) = 0 "; - - String selection = knownVuln + " AND " + compareHash + " AND " + notIgnored; - - return new AppQuerySelection(selection) - .requireNaturalInstalledTable() - .requireNaturalJoinApks() - .requireNatrualJoinAntiFeatures() - .requireLeftJoinPrefs(); - } - - static AppQuerySelection queryPackageNames(String packageNames, String packageNameField) { - String[] args = packageNames.split(","); - String selection = packageNameField + " IN (" + generateQuestionMarksForInClause(args.length) + ")"; - return new AppQuerySelection(selection, args); - } - - @Override - public Cursor query(@NonNull Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) { - AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs); - - // 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; - - int limit = 0; - - List pathSegments = uri.getPathSegments(); - switch (MATCHER.match(uri)) { - case CALC_PREFERRED_METADATA: - updatePreferredMetadata(); - return null; - - case CODE_LIST: - includeSwap = false; - break; - - case CODE_SINGLE: - long repoId = Long.parseLong(pathSegments.get(1)); - String packageName = pathSegments.get(2); - selection = selection.add(querySingle(packageName, repoId)); - repoIsKnown = true; - break; - - case CAN_UPDATE: - selection = selection.add(queryCanUpdate()); - includeSwap = false; - break; - - case REPO: - selection = selection.add(queryRepo(Long.parseLong(uri.getLastPathSegment()))); - repoIsKnown = true; - break; - - case INSTALLED: - selection = selection.add(queryInstalled()); - sortOrder = Cols.NAME; - includeSwap = false; - break; - - case SEARCH_TEXT: - selection = selection.add(querySearch(pathSegments.get(1))); - includeSwap = false; - break; - - case SEARCH_TEXT_AND_CATEGORIES: - selection = selection - .add(querySearch(pathSegments.get(1))) - .add(queryCategory(pathSegments.get(2))); - includeSwap = false; - break; - - case SEARCH_REPO: - selection = selection - .add(querySearch(pathSegments.get(2))) - .add(queryRepo(Long.parseLong(pathSegments.get(1)))); - repoIsKnown = true; - break; - - case CATEGORY: - selection = selection.add(queryCategory(uri.getLastPathSegment())); - includeSwap = false; - break; - - case TOP_FROM_CATEGORY: - selection = selection.add(queryCategory(pathSegments.get(2))); - limit = Integer.parseInt(pathSegments.get(1)); - includeSwap = false; - break; - - case INSTALLED_WITH_KNOWN_VULNS: - selection = selection.add(queryInstalledWithKnownVulns()); - includeSwap = false; - break; - - case LATEST_TAB: - // There seems no reason to limit the number of apps on the front page, but it helps - // if it loads quickly, as it is the default view shown every time F-Droid is opened. - // 200 is an arbitrary number which hopefully gives the user enough to scroll through - // if they are bored. - limit = 200; - - 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, limit); - } - - /** - * Helper method used by the genuine {@link AppProvider}. - *

- * Query the database table specified by {@code uri}, which is usually (always?) - * {@link AppMetadataTable} with specified {@code selection} and {@code sortOrder}. - * WARNING: This contains a hack if {@code sortOrder} is equal to {@link Cols#NAME}, - * i.e. not a complete table.column name, but just that single column name. In that case, - * a {@code sortOrder} is built out into a {@code sortOrder} that includes localized sorting. - */ - protected Cursor runQuery(Uri uri, AppQuerySelection selection, String[] projection, boolean includeSwap, String sortOrder, int limit) { - if (!includeSwap) { - selection = selection.add(queryExcludeSwap()); - } - - if (Cols.NAME.equals(sortOrder)) { - sortOrder = getTableName() + "." + sortOrder + " COLLATE LOCALIZED "; - } - - Query query = new Query(); - query.addSelection(selection); - query.addFields(projection); // TODO: Make the order of addFields/addSelection not dependent on each other... - query.addOrderBy(sortOrder); - query.addLimit(limit); - - Cursor cursor = LoggingQuery.rawQuery(db(), query.toString(), query.getArgs()); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - return cursor; - } - - @Override - public int delete(@NonNull Uri uri, String where, String[] whereArgs) { - if (MATCHER.match(uri) != REPO) { - throw new UnsupportedOperationException("Delete not supported for " + uri + "."); - } - - long repoId = Long.parseLong(uri.getLastPathSegment()); - - final String catJoin = getCatJoinTableName(); - final String app = getTableName(); - String query = "DELETE FROM " + catJoin + " WHERE " + CatJoinTable.Cols.APP_METADATA_ID + " IN " + - "(SELECT " + Cols.ROW_ID + " FROM " + app + " WHERE " + app + "." + Cols.REPO_ID + " = ?)"; - db().execSQL(query, new String[]{String.valueOf(repoId)}); - - AppQuerySelection selection = new AppQuerySelection(where, whereArgs).add(queryRepo(repoId)); - int result = db().delete(getTableName(), selection.getSelection(), selection.getArgs()); - - getContext().getContentResolver().notifyChange(ApkProvider.getContentUri(), null); - getContext().getContentResolver().notifyChange(AppProvider.getContentUri(), null); - getContext().getContentResolver().notifyChange(CategoryProvider.getContentUri(), null); - - return result; - } - - @Override - public Uri insert(@NonNull Uri uri, ContentValues values) { - long packageId = PackageIdProvider.Helper.ensureExists(getContext(), values.getAsString(Cols.Package.PACKAGE_NAME)); - values.remove(Cols.Package.PACKAGE_NAME); - values.put(Cols.PACKAGE_ID, packageId); - - if (!values.containsKey(Cols.DESCRIPTION) || values.getAsString(Cols.DESCRIPTION) == null) { - // the current structure assumes that description is always present and non-null - values.put(Cols.DESCRIPTION, ""); - } - - // Trim these to avoid unwanted newlines in the UI - values.put(Cols.SUMMARY, values.getAsString(Cols.SUMMARY).trim()); - values.put(Cols.NAME, values.getAsString(Cols.NAME).trim()); - - 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 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); - } - getContext().getContentResolver().notifyChange(CategoryProvider.getContentUri(), null); - } - } - - @Override - public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) { - if (MATCHER.match(uri) != CALC_SUGGESTED_APKS) { - throw new UnsupportedOperationException("Update not supported for " + uri + "."); - } - - List segments = uri.getPathSegments(); - if (segments.size() > 1) { - String packageName = segments.get(1); - updateSuggestedApk(packageName); - } else { - updateSuggestedApks(); - } - getContext().getContentResolver().notifyChange(getCanUpdateUri(), null); - return 0; - } - - protected void updateAllAppDetails() { - updatePreferredMetadata(); - updateCompatibleFlags(); - updateSuggestedFromUpstream(null); - } - - /** - * If the repo hasn't changed, then there are many things which we shouldn't waste time updating - * (compared to {@link AppProvider#updateAllAppDetails()}: - *

    - *
  • The "preferred metadata", as that is calculated based on repo with highest priority, and - * only takes into account the package name, not specific versions, when figuring this out.
  • - * - *
  • Compatible flags. These were calculated earlier, whether or not an app was suggested or not.
  • - * - *
  • Icon URLs. While technically these do change when the suggested version changes, it is not - * important enough to spend a significant amount of time to calculate. In the future maybe, - * but that effort should instead go into implementing an intent service.
  • - *
- * In the future, this problem of taking a long time should be fixed by implementing an - * {@link android.app.IntentService} as described in https://gitlab.com/fdroid/fdroidclient/issues/520. - */ - protected void updateSuggestedApks() { - updateSuggestedFromUpstream(null); - updateSuggestedFromLatest(null); - } - - protected void updateSuggestedApk(String packageName) { - updateSuggestedFromUpstream(packageName); - updateSuggestedFromLatest(packageName); - } - - private void updatePreferredMetadata() { - Utils.debugLog(TAG, "Deciding on which metadata should take priority for each package."); - - final String app = getTableName(); - - final String highestPriority = - "SELECT MAX(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. - */ - private void updateCompatibleFlags() { - Utils.debugLog(TAG, "Calculating whether apps are compatible, based on whether any of their apks are compatible"); - - final String apk = getApkTableName(); - final String app = getTableName(); - - String updateSql = - "UPDATE " + app + " SET " + Cols.IS_COMPATIBLE + " = ( " + - " SELECT TOTAL( " + apk + "." + ApkTable.Cols.IS_COMPATIBLE + ") > 0 " + - " FROM " + apk + - " WHERE " + apk + "." + ApkTable.Cols.APP_ID + " = " + app + "." + Cols.ROW_ID + " );"; - - db().execSQL(updateSql); - } - - /** - * Look at the upstream version of each app, our goal is to find the apk - * with the closest version code to that, without going over. - * If the app is not compatible at all (i.e. no versions were compatible) - * then we take the highest, otherwise we take the highest compatible version. - * If the app is installed, then all apks signed by a different certificate are - * ignored for the purpose of this calculation. - * - * @see #updateSuggestedFromLatest(String) - */ - private void updateSuggestedFromUpstream(@Nullable String packageName) { - // Utils.debugLog(TAG, "Calculating suggested versions for all NON-INSTALLED apps which specify an upstream version code."); - - final String apk = getApkTableName(); - final String app = getTableName(); - final String installed = InstalledAppTable.NAME; - - final boolean unstableUpdates = Preferences.get().getUnstableUpdates(); - String restrictToStable = unstableUpdates ? "" : (apk + "." + ApkTable.Cols.VERSION_CODE + " <= " + app + "." + Cols.SUGGESTED_VERSION_CODE + " AND "); - - String restrictToApp = ""; - String[] args = null; - - if (packageName != null) { - restrictToApp = " AND " + app + "." + Cols.PACKAGE_ID + " = (" + getPackageIdFromPackageNameQuery() + ") "; - args = new String[]{packageName}; - } - - // The join onto `appForThisApk` is to ensure that the MAX(apk.versionCode) is chosen from - // all apps regardless of repo. If we joined directly onto the outer `app` table we are - // in the process of updating, then it would be limited to only apks from the same repo. - // By adding the extra join, and then joining based on the packageId of this inner app table - // and the app table we are updating, we take into account all apks for this app. - - // The check apk.sig = COALESCE(installed.sig, apk.sig) would ideally be better written as: - // `installedSig IS NULL OR installedSig = apk.sig` - // however that would require a separate sub query for each `installedSig` which is more - // expensive. Using a COALESCE is a less expressive way to write the same thing with only - // a single subquery. - // Also note that the `installedSig IS NULL` is not because there is a `NULL` entry in the - // installed table (this is impossible), but rather because the subselect above returned - // zero rows. - String updateSql = - "UPDATE " + app + " SET " + Cols.AUTO_INSTALL_VERSION_CODE + " = ( " + - " SELECT MAX( " + apk + "." + ApkTable.Cols.VERSION_CODE + " ) " + - " FROM " + apk + - " JOIN " + app + " AS appForThisApk ON (appForThisApk." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + ") " + - " LEFT JOIN " + installed + " ON (" + installed + "." + InstalledAppTable.Cols.PACKAGE_ID + " = " + app + "." + Cols.PACKAGE_ID + ") " + - " WHERE " + - app + "." + Cols.PACKAGE_ID + " = appForThisApk." + Cols.PACKAGE_ID + " AND " + - apk + "." + ApkTable.Cols.SIGNATURE + " IS COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " + - restrictToStable + - " ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + Cols.IS_COMPATIBLE + " = 1 ) ) " + - " WHERE " + Cols.SUGGESTED_VERSION_CODE + " > 0 " + restrictToApp; - - LoggingQuery.execSQL(db(), updateSql, args); - } - - /** - * We set each app's suggested version to the latest available that is - * compatible, or the latest available if none are compatible. - *

- * If the suggested version is null, it means that we could not figure it - * out from the upstream vercode. In such a case, fall back to the simpler - * algorithm as if upstreamVercode was 0. - * - * @see #updateSuggestedFromUpstream(String) - */ - private void updateSuggestedFromLatest(@Nullable String packageName) { - // Utils.debugLog(TAG, "Calculating suggested versions for all apps which don't specify an upstream version code."); - - final String apk = getApkTableName(); - final String app = getTableName(); - final String installed = InstalledAppTable.NAME; - - final String restrictToApps; - final String[] args; - - if (packageName == null) { - restrictToApps = " COALESCE(" + Cols.SUGGESTED_VERSION_CODE + ", 0) = 0 OR " + Cols.AUTO_INSTALL_VERSION_CODE + " IS NULL "; - args = null; - } else { - // Don't update an app with an upstream version code, because that would have been updated - // by updateSuggestedFromUpdate(packageName). - restrictToApps = " COALESCE(" + Cols.SUGGESTED_VERSION_CODE + ", 0) = 0 AND " + app + "." + Cols.PACKAGE_ID + " = (" + getPackageIdFromPackageNameQuery() + ") "; - args = new String[]{packageName}; - } - - String updateSql = - "UPDATE " + app + " SET " + Cols.AUTO_INSTALL_VERSION_CODE + " = ( " + - " SELECT MAX( " + apk + "." + ApkTable.Cols.VERSION_CODE + " ) " + - " FROM " + apk + - " JOIN " + app + " AS appForThisApk ON (appForThisApk." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + ") " + - " LEFT JOIN " + installed + " ON (" + installed + "." + InstalledAppTable.Cols.PACKAGE_ID + " = " + app + "." + Cols.PACKAGE_ID + ") " + - " WHERE " + - app + "." + Cols.PACKAGE_ID + " = appForThisApk." + Cols.PACKAGE_ID + " AND " + - apk + "." + ApkTable.Cols.SIGNATURE + " IS COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " + - " ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + ApkTable.Cols.IS_COMPATIBLE + " = 1 ) ) " + - " WHERE " + restrictToApps; - - LoggingQuery.execSQL(db(), updateSql, args); - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java b/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java deleted file mode 100644 index b7a121ad3..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java +++ /dev/null @@ -1,250 +0,0 @@ -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.AppMetadataTable; -import org.fdroid.fdroid.data.Schema.CatJoinTable; -import org.fdroid.fdroid.data.Schema.CategoryTable; -import org.fdroid.fdroid.data.Schema.CategoryTable.Cols; -import org.fdroid.fdroid.data.Schema.PackageTable; - -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import androidx.annotation.NonNull; - -public class CategoryProvider extends FDroidProvider { - public static final String TAG = "CategoryProvider"; - - public static final class Helper { - private Helper() { - } - - /** - * During repo updates, each app needs to know the ID of each category it belongs to. - * This results in lots of database lookups, usually at least one for each app, sometimes more. - * To improve performance, this caches the association between categories and their database IDs. - *

- * It can stay around for the entire F-Droid process, even across multiple repo updates, as we - * don't actually remove data from the categories table. - */ - private static final Map KNOWN_CATEGORIES = new HashMap<>(); - - /** - * Used by tests to clear that the "Category -> ID" cache (used to prevent excessive disk reads). - */ - static void clearCategoryIdCache() { - KNOWN_CATEGORIES.clear(); - } - - public static long ensureExists(Context context, String category) { - // Check our in-memory cache to potentially prevent a trip to the database (and hence disk). - String lowerCaseCategory = category.toLowerCase(Locale.ENGLISH); - if (KNOWN_CATEGORIES.containsKey(lowerCaseCategory)) { - return KNOWN_CATEGORIES.get(lowerCaseCategory); - } - - 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()); - } - - KNOWN_CATEGORIES.put(lowerCaseCategory, id); - - return id; - } - - private 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(); - } - } - } - - private class Query extends QueryBuilder { - - @Override - protected String getRequiredTables() { - return CategoryTable.NAME + " LEFT JOIN " + 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() { - // Make sure that metadata from the preferred repository is used to determine if - // there is an app present or not. - join(AppMetadataTable.NAME, "app", "app." + AppMetadataTable.Cols.ROW_ID - + " = " + CatJoinTable.NAME + "." + CatJoinTable.Cols.APP_METADATA_ID); - join(PackageTable.NAME, "pkg", "pkg." + PackageTable.Cols.PREFERRED_METADATA - + " = " + "app." + AppMetadataTable.Cols.ROW_ID); - } - } - - 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); - } - - 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 + " = ? COLLATE NOCASE"; - 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); - - if (onlyCategoriesWithApps) { - query.setOnlyCategoriesWithApps(); - } - - Cursor cursor = LoggingQuery.rawQuery(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); - // Don't try and notify listeners here, because it will instead happen when the TempAppProvider - // is committed (when the AppProvider and ApkProviders notify their listeners). There is no - // other time where categories get added (at time of writing) so this should be okay. - 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 + "."); - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 9b4c3ab2d..9c582a492 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -23,12 +23,7 @@ package org.fdroid.fdroid.data; -import android.content.ContentValues; import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.text.TextUtils; import android.util.Log; import androidx.annotation.AnyThread; @@ -38,18 +33,8 @@ import androidx.annotation.WorkerThread; import org.fdroid.database.FDroidDatabase; import org.fdroid.database.FDroidDatabaseHolder; import org.fdroid.database.InitialRepository; -import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Schema.AntiFeatureTable; -import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable; -import org.fdroid.fdroid.data.Schema.ApkTable; -import org.fdroid.fdroid.data.Schema.AppMetadataTable; -import org.fdroid.fdroid.data.Schema.AppPrefsTable; -import org.fdroid.fdroid.data.Schema.CatJoinTable; -import org.fdroid.fdroid.data.Schema.InstalledAppTable; -import org.fdroid.fdroid.data.Schema.PackageTable; -import org.fdroid.fdroid.data.Schema.RepoTable; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -58,7 +43,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -70,13 +54,10 @@ import java.util.List; * used in test subclasses. */ @SuppressWarnings("LineLength") -public class DBHelper extends SQLiteOpenHelper { +public class DBHelper { private static final String TAG = "DBHelper"; - public static final int REPO_XML_ITEM_COUNT = 8; - - private static DBHelper instance; - private static final String DATABASE_NAME = "fdroid"; + static final int REPO_XML_ITEM_COUNT = 8; public static FDroidDatabase getDb(Context context) { return FDroidDatabaseHolder.getDb(context, "fdroid_db", db -> prePopulateDb(context, db)); @@ -109,222 +90,6 @@ public class DBHelper extends SQLiteOpenHelper { })); } - 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, " - + RepoTable.Cols.ADDRESS + " text not null, " - + RepoTable.Cols.NAME + " text, " - + RepoTable.Cols.DESCRIPTION + " text, " - + RepoTable.Cols.IN_USE + " integer not null, " - + RepoTable.Cols.PRIORITY + " integer not null, " - + RepoTable.Cols.SIGNING_CERT + " text, " - + RepoTable.Cols.FINGERPRINT + " text, " - + RepoTable.Cols.MAX_AGE + " integer not null default 0, " - + RepoTable.Cols.VERSION + " integer not null default 0, " - + RepoTable.Cols.LAST_ETAG + " text, " - + RepoTable.Cols.LAST_UPDATED + " string," - + RepoTable.Cols.IS_SWAP + " integer boolean default 0," - + RepoTable.Cols.USERNAME + " string, " - + RepoTable.Cols.PASSWORD + " string," - + RepoTable.Cols.TIMESTAMP + " integer not null default 0, " - + RepoTable.Cols.ICON + " string, " - + RepoTable.Cols.MIRRORS + " string, " - + RepoTable.Cols.USER_MIRRORS + " string, " - + RepoTable.Cols.DISABLED_MIRRORS + " string, " - + RepoTable.Cols.PUSH_REQUESTS + " integer not null default " + Repo.PUSH_REQUEST_IGNORE - + ");"; - - static final String CREATE_TABLE_APK = - "CREATE TABLE " + ApkTable.NAME + " ( " - + ApkTable.Cols.APP_ID + " integer not null, " - + ApkTable.Cols.VERSION_NAME + " text, " - + ApkTable.Cols.REPO_ID + " integer not null, " - + ApkTable.Cols.HASH + " text not null, " - + ApkTable.Cols.VERSION_CODE + " int not null," - + ApkTable.Cols.NAME + " text not null, " - + ApkTable.Cols.SIZE + " int not null, " - + ApkTable.Cols.SIGNATURE + " string, " - + ApkTable.Cols.SOURCE_NAME + " string, " - + ApkTable.Cols.MIN_SDK_VERSION + " integer, " - + ApkTable.Cols.TARGET_SDK_VERSION + " integer, " - + ApkTable.Cols.MAX_SDK_VERSION + " integer, " - + ApkTable.Cols.OBB_MAIN_FILE + " string, " - + ApkTable.Cols.OBB_MAIN_FILE_SHA256 + " string, " - + ApkTable.Cols.OBB_PATCH_FILE + " string, " - + ApkTable.Cols.OBB_PATCH_FILE_SHA256 + " string, " - + ApkTable.Cols.REQUESTED_PERMISSIONS + " string, " - + ApkTable.Cols.FEATURES + " string, " - + ApkTable.Cols.NATIVE_CODE + " string, " - + ApkTable.Cols.HASH_TYPE + " string, " - + ApkTable.Cols.ADDED_DATE + " string, " - + ApkTable.Cols.IS_COMPATIBLE + " int not null, " - + ApkTable.Cols.INCOMPATIBLE_REASONS + " text" - + ");"; - - static final String CREATE_TABLE_APP_METADATA = "CREATE TABLE " + AppMetadataTable.NAME - + " ( " - + 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, " - + AppMetadataTable.Cols.DESCRIPTION + " text not null, " - + AppMetadataTable.Cols.WHATSNEW + " text, " - + AppMetadataTable.Cols.LICENSE + " text not null, " - + AppMetadataTable.Cols.AUTHOR_NAME + " text, " - + AppMetadataTable.Cols.AUTHOR_EMAIL + " text, " - + AppMetadataTable.Cols.WEBSITE + " text, " - + AppMetadataTable.Cols.ISSUE_TRACKER + " text, " - + AppMetadataTable.Cols.SOURCE_CODE + " text, " - + AppMetadataTable.Cols.TRANSLATION + " text, " - + AppMetadataTable.Cols.VIDEO + " string, " - + AppMetadataTable.Cols.CHANGELOG + " text, " - + AppMetadataTable.Cols.PREFERRED_SIGNER + " text," - + AppMetadataTable.Cols.AUTO_INSTALL_VERSION_CODE + " text," - + AppMetadataTable.Cols.SUGGESTED_VERSION_NAME + " text," - + AppMetadataTable.Cols.SUGGESTED_VERSION_CODE + " integer," - + AppMetadataTable.Cols.ANTI_FEATURES + " string," - + AppMetadataTable.Cols.DONATE + " string," - + AppMetadataTable.Cols.BITCOIN + " string," - + AppMetadataTable.Cols.LITECOIN + " string," - + AppMetadataTable.Cols.FLATTR_ID + " string," - + AppMetadataTable.Cols.LIBERAPAY + " string," - + AppMetadataTable.Cols.OPEN_COLLECTIVE + " string," - + AppMetadataTable.Cols.REQUIREMENTS + " string," - + AppMetadataTable.Cols.ADDED + " string," - + AppMetadataTable.Cols.LAST_UPDATED + " string," - + AppMetadataTable.Cols.IS_COMPATIBLE + " int not null," - + AppMetadataTable.Cols.ICON_URL + " text, " - + AppMetadataTable.Cols.FEATURE_GRAPHIC + " string," - + AppMetadataTable.Cols.PROMO_GRAPHIC + " string," - + AppMetadataTable.Cols.TV_BANNER + " string," - + AppMetadataTable.Cols.PHONE_SCREENSHOTS + " string," - + AppMetadataTable.Cols.SEVEN_INCH_SCREENSHOTS + " string," - + AppMetadataTable.Cols.TEN_INCH_SCREENSHOTS + " string," - + AppMetadataTable.Cols.TV_SCREENSHOTS + " string," - + AppMetadataTable.Cols.WEAR_SCREENSHOTS + " string," - + AppMetadataTable.Cols.IS_APK + " boolean," - + AppMetadataTable.Cols.IS_LOCALIZED + " boolean," - + "primary key(" + AppMetadataTable.Cols.PACKAGE_ID + ", " + AppMetadataTable.Cols.REPO_ID + "));"; - - private static final String CREATE_TABLE_APP_PREFS = "CREATE TABLE " + AppPrefsTable.NAME - + " ( " - + AppPrefsTable.Cols.PACKAGE_NAME + " TEXT, " - + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + " INT NOT NULL, " - + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + " INT BOOLEAN NOT NULL, " - + AppPrefsTable.Cols.IGNORE_VULNERABILITIES + " INT BOOLEAN 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_ID + " INT NOT NULL UNIQUE, " - + InstalledAppTable.Cols.VERSION_CODE + " INT NOT NULL, " - + InstalledAppTable.Cols.VERSION_NAME + " TEXT NOT NULL, " - + InstalledAppTable.Cols.APPLICATION_LABEL + " TEXT NOT NULL, " - + InstalledAppTable.Cols.SIGNATURE + " TEXT NOT NULL, " - + InstalledAppTable.Cols.LAST_UPDATE_TIME + " INTEGER NOT NULL DEFAULT 0, " - + InstalledAppTable.Cols.HASH_TYPE + " TEXT NOT NULL, " - + InstalledAppTable.Cols.HASH + " TEXT NOT NULL" - + " );"; - - private static final String CREATE_TABLE_ANTI_FEATURE = "CREATE TABLE " + AntiFeatureTable.NAME - + " ( " - + AntiFeatureTable.Cols.NAME + " TEXT NOT NULL " - + " );"; - - static final String CREATE_TABLE_APK_ANTI_FEATURE_JOIN = "CREATE TABLE " + ApkAntiFeatureJoinTable.NAME - + " ( " - + ApkAntiFeatureJoinTable.Cols.APK_ID + " INT NOT NULL, " - + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + " INT NOT NULL, " - + "primary key(" + ApkAntiFeatureJoinTable.Cols.APK_ID + ", " + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + ") " - + " );"; - - protected static final int DB_VERSION = 85; - - private final Context context; - - DBHelper(Context context) { - super(context, DATABASE_NAME, null, DB_VERSION); - this.context = context.getApplicationContext(); - } - - /** - * Only used for testing. Not quite sure how to mock a singleton variable like this. - */ - public static void clearDbHelperSingleton() { - if (instance != null) { - instance.close(); - } - instance = null; - } - - static synchronized DBHelper getInstance(Context context) { - if (instance == null) { - Utils.debugLog(TAG, "First time accessing database, creating new helper"); - instance = new DBHelper(context); - } - return instance; - } - - @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_CATEGORY); - db.execSQL(CREATE_TABLE_CAT_JOIN); - db.execSQL(CREATE_TABLE_INSTALLED_APP); - db.execSQL(CREATE_TABLE_REPO); - db.execSQL(CREATE_TABLE_APP_PREFS); - db.execSQL(CREATE_TABLE_ANTI_FEATURE); - db.execSQL(CREATE_TABLE_APK_ANTI_FEATURE_JOIN); - ensureIndexes(db); - - List initialRepos = DBHelper.loadInitialRepos(context); - - for (int i = 0; i < initialRepos.size(); i += REPO_XML_ITEM_COUNT) { - insertRepo( - db, - initialRepos.get(i), // name - initialRepos.get(i + 1), // address - initialRepos.get(i + 2), // description - initialRepos.get(i + 3), // version - initialRepos.get(i + 4), // enabled - initialRepos.get(i + 5), // priority - initialRepos.get(i + 6), // pushRequests - initialRepos.get(i + 7) // pubkey - ); - } - } - /** * Load Additional Repos first, then Default Repos. This way, Default * Repos will be shown after the OEM-added ones on the Manage Repos @@ -338,7 +103,7 @@ public class DBHelper extends SQLiteOpenHelper { * This also cleans up the whitespace in the description item, since the * XML parsing will include the linefeeds and indenting in the description. */ - public static List loadInitialRepos(Context context) throws IllegalArgumentException { + static List loadInitialRepos(Context context) throws IllegalArgumentException { String packageName = context.getPackageName(); List initialRepos = DBHelper.loadAdditionalRepos(packageName); List defaultRepos = Arrays.asList(context.getResources().getStringArray(R.array.default_repos)); @@ -391,7 +156,7 @@ public class DBHelper extends SQLiteOpenHelper { * not allowed to set the priority since that would give it the power to * override {@code default_repos.xml}. */ - public static List parseAdditionalReposXml(File additionalReposFile) + static List parseAdditionalReposXml(File additionalReposFile) throws IOException, XmlPullParserException { List repoItems = new LinkedList<>(); InputStream xmlInputStream = new FileInputStream(additionalReposFile); @@ -437,1121 +202,4 @@ public class DBHelper extends SQLiteOpenHelper { return new LinkedList<>(); } - @Override - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - resetTransient(context); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - - Utils.debugLog(TAG, "Upgrading database from v" + oldVersion + " v" + newVersion); - - migrateRepoTable(db, oldVersion); - - // The other tables are transient and can just be reset. Do this after - // the repo table changes though, because it also clears the lastetag - // fields which didn't always exist. - resetTransientPre42(db, oldVersion); - - addNameAndDescriptionToRepo(db, oldVersion); - addFingerprintToRepo(db, oldVersion); - addMaxAgeToRepo(db, oldVersion); - addVersionToRepo(db, oldVersion); - addLastUpdatedToRepo(db, oldVersion); - renameRepoId(db, oldVersion); - populateRepoNames(db, oldVersion); - addIsSwapToRepo(db, oldVersion); - addChangelogToApp(db, oldVersion); - addCredentialsToRepo(db, oldVersion); - addAuthorToApp(db, oldVersion); - useMaxValueInMaxSdkVersion(db, oldVersion); - requireTimestampInRepos(db, oldVersion); - addTargetSdkVersionToApk(db, oldVersion); - migrateAppPrimaryKeyToRowId(db, oldVersion); - removeApkPackageNameColumn(db, oldVersion); - addAppPrefsTable(db, oldVersion); - lowerCaseApkHashes(db, oldVersion); - supportRepoPushRequests(db, oldVersion); - migrateToPackageTable(db, oldVersion); - addObbFiles(db, oldVersion); - addCategoryTables(db, oldVersion); - addIndexV1Fields(db, oldVersion); - addIndexV1AppFields(db, oldVersion); - recalculatePreferredMetadata(db, oldVersion); - addWhatsNewAndVideo(db, oldVersion); - dropApkPrimaryKey(db, oldVersion); - addIntegerPrimaryKeyToInstalledApps(db, oldVersion); - addPreferredSignerToApp(db, oldVersion); - updatePreferredSignerIfEmpty(db, oldVersion); - addIsAppToApp(db, oldVersion); - addApkAntiFeatures(db, oldVersion); - addIgnoreVulnPref(db, oldVersion); - addLiberapayID(db, oldVersion); - addUserMirrorsFields(db, oldVersion); - removeNotNullFromVersionName(db, oldVersion); - addDisabledMirrorsFields(db, oldVersion); - addIsLocalized(db, oldVersion); - addTranslation(db, oldVersion); - switchRepoArchivePriorities(db, oldVersion); - deleteOldIconUrls(db, oldVersion); - addOpenCollective(db, oldVersion); - } - - private void addOpenCollective(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 85) { - return; - } - - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.OPEN_COLLECTIVE)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.OPEN_COLLECTIVE + " field to " - + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " - + AppMetadataTable.Cols.OPEN_COLLECTIVE + " string;"); - } - } - - private void deleteOldIconUrls(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 84) { - return; - } - Utils.debugLog(TAG, "Clearing iconUrl field to enable localized icons on next update"); - db.execSQL("UPDATE " + AppMetadataTable.NAME + " SET " + AppMetadataTable.Cols.ICON_URL + "= NULL"); - } - - private void switchRepoArchivePriorities(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 83) { - return; - } - Utils.debugLog(TAG, "Switching default repo and archive priority."); - - db.execSQL("UPDATE " + RepoTable.NAME + " SET " + RepoTable.Cols.PRIORITY - + "= ( SELECT SUM(" + RepoTable.Cols.PRIORITY + ")" + " FROM " + RepoTable.NAME - + " WHERE " + RepoTable.Cols.ADDRESS + " IN ( 'https://f-droid.org/repo', 'https://f-droid.org/archive')" - + ") - " + RepoTable.Cols.PRIORITY - + " WHERE " + RepoTable.Cols.ADDRESS + " IN ( 'https://f-droid.org/repo', 'https://f-droid.org/archive')" - + " AND 'TRUE' IN (SELECT CASE WHEN a." + RepoTable.Cols.PRIORITY + " = b." - + RepoTable.Cols.PRIORITY + "-1" + " THEN 'TRUE' ELSE 'FASLE' END" - + " FROM " + RepoTable.NAME + " AS a INNER JOIN " + RepoTable.NAME - + " AS b ON a." + RepoTable.Cols.ADDRESS + "= 'https://f-droid.org/repo'" - + " AND b." + RepoTable.Cols.ADDRESS + "= 'https://f-droid.org/archive'" - + ")" - ); - - db.execSQL("UPDATE " + RepoTable.NAME + " SET " + RepoTable.Cols.PRIORITY - + "= ( SELECT SUM(" + RepoTable.Cols.PRIORITY + ")" + " FROM " + RepoTable.NAME - + " WHERE " + RepoTable.Cols.ADDRESS + " IN ( 'https://guardianproject.info/fdroid/repo', 'https://guardianproject.info/fdroid/archive')" - + ") - " + RepoTable.Cols.PRIORITY - + " WHERE " + RepoTable.Cols.ADDRESS + " IN ( 'https://guardianproject.info/fdroid/repo', 'https://guardianproject.info/fdroid/archive')" - + " AND 'TRUE' IN (SELECT CASE WHEN a." + RepoTable.Cols.PRIORITY + " = b." - + RepoTable.Cols.PRIORITY + "-1" + " THEN 'TRUE' ELSE 'FASLE' END" - + " FROM " + RepoTable.NAME + " AS a INNER JOIN " + RepoTable.NAME + " AS b ON a." - + RepoTable.Cols.ADDRESS + "= 'https://guardianproject.info/fdroid/repo'" - + " AND b." + RepoTable.Cols.ADDRESS + "= 'https://guardianproject.info/fdroid/archive'" - + ")" - ); - } - - private void addTranslation(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 82) { - return; - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.TRANSLATION)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.TRANSLATION + " field to " - + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " - + AppMetadataTable.Cols.TRANSLATION + " string;"); - } - } - - private void addIsLocalized(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 81) { - return; - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.IS_LOCALIZED)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.IS_LOCALIZED + " field to " - + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " - + AppMetadataTable.Cols.IS_LOCALIZED + " boolean;"); - } - } - - private void addDisabledMirrorsFields(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 80) { - return; - } - if (!columnExists(db, RepoTable.NAME, RepoTable.Cols.DISABLED_MIRRORS)) { - Utils.debugLog(TAG, "Adding " + RepoTable.Cols.DISABLED_MIRRORS + " field to " + RepoTable.NAME + " table in db."); - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.DISABLED_MIRRORS + " string;"); - } - } - - private void removeNotNullFromVersionName(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 79) { - return; - } - - Log.i(TAG, "Forcing repo refresh to remove NOT NULL from " + ApkTable.Cols.VERSION_NAME); - resetTransient(db); - } - - private void addUserMirrorsFields(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 78) { - return; - } - if (!columnExists(db, RepoTable.NAME, RepoTable.Cols.USER_MIRRORS)) { - Utils.debugLog(TAG, "Adding " + RepoTable.Cols.USER_MIRRORS + " field to " + RepoTable.NAME + " table in db."); - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.USER_MIRRORS + " string;"); - } - } - - private void addLiberapayID(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 77) { - return; - } - - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.LIBERAPAY)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.LIBERAPAY + " field to " - + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " - + AppMetadataTable.Cols.LIBERAPAY + " string;"); - } - } - - private void addIgnoreVulnPref(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 76) { - return; - } - - if (!columnExists(db, AppPrefsTable.NAME, AppPrefsTable.Cols.IGNORE_VULNERABILITIES)) { - Utils.debugLog(TAG, "Adding " + AppPrefsTable.Cols.IGNORE_VULNERABILITIES + " field to " + AppPrefsTable.NAME + " table in db."); - db.execSQL("alter table " + AppPrefsTable.NAME + " add column " + AppPrefsTable.Cols.IGNORE_VULNERABILITIES + " boolean;"); - } - } - - private void addApkAntiFeatures(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 76) { - return; - } - - Log.i(TAG, "Adding anti features on a per-apk basis."); - resetTransient(db); - } - - private void addIsAppToApp(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 74) { - return; - } - - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.IS_APK)) { - Log.i(TAG, "Figuring out whether each \"app\" is actually an app, or it represents other media."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.IS_APK + " boolean;"); - - // Find all apks for which their filename DOESN'T end in ".apk", and if there is more than one, the - // corresponding app is updated to be marked as media. - String apkName = ApkTable.Cols.NAME; - String query = "UPDATE " + AppMetadataTable.NAME + " SET " + AppMetadataTable.Cols.IS_APK + " = (" + - " SELECT COUNT(*) FROM " + ApkTable.NAME + " AS apk" + - " WHERE " + - " " + ApkTable.Cols.APP_ID + " = " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.ROW_ID + - " AND SUBSTR(" + apkName + ", LENGTH(" + apkName + ") - 3) != '.apk'" + - ") = 0;"; - Log.i(TAG, query); - db.execSQL(query); - } - } - - private void updatePreferredSignerIfEmpty(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 73) { - return; - } - - Log.i(TAG, "Forcing repo refresh to calculate preferred signer."); - resetTransient(db); - } - - private void addPreferredSignerToApp(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 72) { - return; - } - - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.PREFERRED_SIGNER)) { - Log.i(TAG, "Adding preferred signer to app table."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.PREFERRED_SIGNER + " text;"); - } - } - - private void addIntegerPrimaryKeyToInstalledApps(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 71) { - return; - } - - Log.i(TAG, "Replacing primary key on installed app table with integer for performance."); - - db.beginTransaction(); - try { - if (tableExists(db, Schema.InstalledAppTable.NAME)) { - db.execSQL("DROP TABLE " + Schema.InstalledAppTable.NAME); - } - - db.execSQL(CREATE_TABLE_INSTALLED_APP); - ensureIndexes(db); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - private void dropApkPrimaryKey(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 70) { - return; - } - - // versionCode + repo is no longer a valid primary key given a repo can have multiple apks - // with the same versionCode, signed by different certificates. - Log.i(TAG, "Dropping composite primary key on apk table in favour of sqlite's rowid"); - resetTransient(db); - } - - private void addWhatsNewAndVideo(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 69) { - return; - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.WHATSNEW)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.WHATSNEW + " field to " + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.WHATSNEW + " text;"); - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.VIDEO)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.VIDEO + " field to " + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.VIDEO + " string;"); - } - } - - private void recalculatePreferredMetadata(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 68) { - return; - } - - Log.i(TAG, "Previously, the repository metadata was being interpreted backwards. Need to force a repo refresh to fix this."); - resetTransient(db); - } - - private void addIndexV1AppFields(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 67) { - return; - } - // Strings - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.FEATURE_GRAPHIC)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.FEATURE_GRAPHIC + " field to " + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.FEATURE_GRAPHIC + " string;"); - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.PROMO_GRAPHIC)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.PROMO_GRAPHIC + " field to " + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.PROMO_GRAPHIC + " string;"); - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.TV_BANNER)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.TV_BANNER + " field to " + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.TV_BANNER + " string;"); - } - // String Arrays - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.PHONE_SCREENSHOTS)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.PHONE_SCREENSHOTS + " field to " + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.PHONE_SCREENSHOTS + " string;"); - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.SEVEN_INCH_SCREENSHOTS)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.SEVEN_INCH_SCREENSHOTS + " field to " + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.SEVEN_INCH_SCREENSHOTS + " string;"); - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.TEN_INCH_SCREENSHOTS)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.TEN_INCH_SCREENSHOTS + " field to " + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.TEN_INCH_SCREENSHOTS + " string;"); - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.TV_SCREENSHOTS)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.TV_SCREENSHOTS + " field to " + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.TV_SCREENSHOTS + " string;"); - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.WEAR_SCREENSHOTS)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.WEAR_SCREENSHOTS + " field to " + AppMetadataTable.NAME + " table in db."); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.WEAR_SCREENSHOTS + " string;"); - } - } - - private void addIndexV1Fields(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 66) { - return; - } - if (!columnExists(db, Schema.RepoTable.NAME, RepoTable.Cols.ICON)) { - Utils.debugLog(TAG, "Adding " + RepoTable.Cols.ICON + " field to " + RepoTable.NAME + " table in db."); - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.ICON + " string;"); - } - - if (!columnExists(db, RepoTable.NAME, RepoTable.Cols.MIRRORS)) { - Utils.debugLog(TAG, "Adding " + RepoTable.Cols.MIRRORS + " field to " + RepoTable.NAME + " table in db."); - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.MIRRORS + " string;"); - } - } - - /** - * 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) { - if (oldVersion >= 64) { - return; - } - - Utils.debugLog(TAG, "Ensuring " + ApkTable.Cols.OBB_MAIN_FILE + ", " + - ApkTable.Cols.OBB_PATCH_FILE + ", and hash columns exist on " + ApkTable.NAME); - - if (!columnExists(db, ApkTable.NAME, ApkTable.Cols.OBB_MAIN_FILE)) { - db.execSQL("alter table " + ApkTable.NAME + " add column " - + ApkTable.Cols.OBB_MAIN_FILE + " string"); - } - - if (!columnExists(db, ApkTable.NAME, ApkTable.Cols.OBB_MAIN_FILE_SHA256)) { - db.execSQL("alter table " + ApkTable.NAME + " add column " - + ApkTable.Cols.OBB_MAIN_FILE_SHA256 + " string"); - } - - if (!columnExists(db, ApkTable.NAME, ApkTable.Cols.OBB_PATCH_FILE)) { - db.execSQL("alter table " + ApkTable.NAME + " add column " - + ApkTable.Cols.OBB_PATCH_FILE + " string"); - } - - if (!columnExists(db, ApkTable.NAME, ApkTable.Cols.OBB_PATCH_FILE_SHA256)) { - db.execSQL("alter table " + ApkTable.NAME + " add column " - + ApkTable.Cols.OBB_PATCH_FILE_SHA256 + " string"); - } - } - - 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_ITEM_COUNT + 1]; - String gpPubKey = defaultRepos[REPO_XML_ITEM_COUNT * 2 + 7]; - String gpAddress = defaultRepos[REPO_XML_ITEM_COUNT * 2 + 1]; - String gpArchiveAddress = defaultRepos[REPO_XML_ITEM_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.getColumnIndexOrThrow(RepoTable.Cols.SIGNING_CERT)); - String address = cursor.getString(cursor.getColumnIndexOrThrow(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) { - if (oldVersion >= 61) { - return; - } - Utils.debugLog(TAG, "Lowercasing all APK hashes"); - db.execSQL("UPDATE " + InstalledAppTable.NAME + " SET " + InstalledAppTable.Cols.HASH - + " = lower(" + InstalledAppTable.Cols.HASH + ")"); - } - - private void addAppPrefsTable(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 60) { - return; - } - - Utils.debugLog(TAG, "Creating app preferences table"); - db.execSQL(CREATE_TABLE_APP_PREFS); - - Utils.debugLog(TAG, "Migrating app preferences to separate table"); - db.execSQL( - "INSERT INTO " + AppPrefsTable.NAME + " (" - + AppPrefsTable.Cols.PACKAGE_NAME + ", " - + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ", " - + AppPrefsTable.Cols.IGNORE_ALL_UPDATES - + ") SELECT " - + "id, " - + "ignoreThisUpdate, " - + "ignoreAllUpdates " - + "FROM " + AppMetadataTable.NAME + " " - + "WHERE ignoreThisUpdate > 0 OR ignoreAllUpdates > 0" - ); - - resetTransient(db); - } - - /** - * Ordinarily, if a column is no longer used, we'd err on the side of just leaving it in the - * database but stop referring to it in Java. However because it forms part of the primary - * key of this table, we need to change the primary key to something which _is_ used. Thus, - * this function will rename the old table, create the new table, and then insert all of the - * data from the old into the new with the new primary key. - */ - private void removeApkPackageNameColumn(SQLiteDatabase db, int oldVersion) { - if (oldVersion < 59) { - - Utils.debugLog(TAG, "Changing primary key of " + ApkTable.NAME + " from package + vercode to app + vercode + repo"); - db.beginTransaction(); - - try { - // http://stackoverflow.com/questions/805363/how-do-i-rename-a-column-in-a-sqlite-database-table#805508 - String tempTableName = ApkTable.NAME + "__temp__"; - db.execSQL("ALTER TABLE " + ApkTable.NAME + " RENAME TO " + tempTableName + ";"); - - String createTableDdl = "CREATE TABLE " + ApkTable.NAME + " ( " - + ApkTable.Cols.APP_ID + " integer not null, " - + ApkTable.Cols.VERSION_NAME + " text not null, " - + ApkTable.Cols.REPO_ID + " integer not null, " - + ApkTable.Cols.HASH + " text not null, " - + ApkTable.Cols.VERSION_CODE + " int not null," - + ApkTable.Cols.NAME + " text not null, " - + ApkTable.Cols.SIZE + " int not null, " - + ApkTable.Cols.SIGNATURE + " string, " - + ApkTable.Cols.SOURCE_NAME + " string, " - + ApkTable.Cols.MIN_SDK_VERSION + " integer, " - + ApkTable.Cols.TARGET_SDK_VERSION + " integer, " - + ApkTable.Cols.MAX_SDK_VERSION + " integer, " - + ApkTable.Cols.REQUESTED_PERMISSIONS + " string, " - + ApkTable.Cols.FEATURES + " string, " - + ApkTable.Cols.NATIVE_CODE + " string, " - + ApkTable.Cols.HASH_TYPE + " string, " - + ApkTable.Cols.ADDED_DATE + " string, " - + ApkTable.Cols.IS_COMPATIBLE + " int not null, " - + ApkTable.Cols.INCOMPATIBLE_REASONS + " text, " - + "PRIMARY KEY (" + ApkTable.Cols.APP_ID + ", " + ApkTable.Cols.VERSION_CODE + ", " + ApkTable.Cols.REPO_ID + ")" - + ");"; - - db.execSQL(createTableDdl); - - String nonPackageNameFields = TextUtils.join(", ", new String[]{ - ApkTable.Cols.APP_ID, - ApkTable.Cols.VERSION_NAME, - ApkTable.Cols.REPO_ID, - ApkTable.Cols.HASH, - ApkTable.Cols.VERSION_CODE, - ApkTable.Cols.NAME, - ApkTable.Cols.SIZE, - ApkTable.Cols.SIGNATURE, - ApkTable.Cols.SOURCE_NAME, - ApkTable.Cols.MIN_SDK_VERSION, - ApkTable.Cols.TARGET_SDK_VERSION, - ApkTable.Cols.MAX_SDK_VERSION, - ApkTable.Cols.REQUESTED_PERMISSIONS, - ApkTable.Cols.FEATURES, - ApkTable.Cols.NATIVE_CODE, - ApkTable.Cols.HASH_TYPE, - ApkTable.Cols.ADDED_DATE, - ApkTable.Cols.IS_COMPATIBLE, - ApkTable.Cols.INCOMPATIBLE_REASONS, - }); - - String insertSql = "INSERT INTO " + ApkTable.NAME + - "(" + nonPackageNameFields + " ) " + - "SELECT " + nonPackageNameFields + " FROM " + tempTableName + ";"; - - db.execSQL(insertSql); - db.execSQL("DROP TABLE " + tempTableName + ";"); - - // Now that the old table has been dropped, we can create indexes again. - // Attempting this before dropping the old table will not work, because the - // indexes exist on the _old_ table, and so are unable to be added (with the - // same name) to the _new_ table. - ensureIndexes(db); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - } - - private void migrateAppPrimaryKeyToRowId(SQLiteDatabase db, int oldVersion) { - if (oldVersion < 58 && !columnExists(db, ApkTable.NAME, ApkTable.Cols.APP_ID)) { - db.beginTransaction(); - try { - final String alter = "ALTER TABLE " + ApkTable.NAME + " ADD COLUMN " + ApkTable.Cols.APP_ID + " NUMERIC"; - Log.i(TAG, "Adding appId foreign key to " + ApkTable.NAME); - Utils.debugLog(TAG, alter); - db.execSQL(alter); - - // Hard coded the string literal ".id" as ApkTable.Cols.PACKAGE_NAME was removed in - // the subsequent migration (DB_VERSION 59) - 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.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); - ensureIndexes(db); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - } - - - /** - * Migrate repo list to new structure. (No way to change primary - * key in sqlite - table must be recreated). - */ - private void migrateRepoTable(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 20) { - return; - } - List oldrepos = new ArrayList<>(); - Cursor cursor = db.query(RepoTable.NAME, - new String[]{RepoTable.Cols.ADDRESS, RepoTable.Cols.IN_USE, RepoTable.Cols.SIGNING_CERT}, - null, null, null, null, null); - if (cursor != null) { - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - Repo repo = new Repo(); - repo.address = cursor.getString(0); - repo.inuse = cursor.getInt(1) == 1; - repo.signingCertificate = cursor.getString(2); - oldrepos.add(repo); - cursor.moveToNext(); - } - } - cursor.close(); - } - db.execSQL("drop table " + RepoTable.NAME); - db.execSQL(CREATE_TABLE_REPO); - for (final Repo repo : oldrepos) { - ContentValues values = new ContentValues(); - values.put(RepoTable.Cols.ADDRESS, repo.address); - values.put(RepoTable.Cols.IN_USE, repo.inuse); - values.put(RepoTable.Cols.PRIORITY, 10); - values.put(RepoTable.Cols.SIGNING_CERT, repo.signingCertificate); - values.put(RepoTable.Cols.LAST_ETAG, (String) null); - db.insert(RepoTable.NAME, null, values); - } - } - - private void insertNameAndDescription(SQLiteDatabase db, - String name, String address, String description) { - ContentValues values = new ContentValues(); - values.clear(); - values.put(RepoTable.Cols.NAME, name); - values.put(RepoTable.Cols.DESCRIPTION, description); - db.update(RepoTable.NAME, values, RepoTable.Cols.ADDRESS + " = ?", new String[]{ - address, - }); - } - - /** - * Add a name and description to the repo table, and updates the two - * default repos with values from strings.xml. - */ - private void addNameAndDescriptionToRepo(SQLiteDatabase db, int oldVersion) { - boolean nameExists = columnExists(db, RepoTable.NAME, RepoTable.Cols.NAME); - boolean descriptionExists = columnExists(db, RepoTable.NAME, RepoTable.Cols.DESCRIPTION); - if (oldVersion >= 21 || (nameExists && descriptionExists)) { - return; - } - if (!nameExists) { - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.NAME + " text"); - } - if (!descriptionExists) { - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.DESCRIPTION + " text"); - } - - String[] defaultRepos = context.getResources().getStringArray(R.array.default_repos); - for (int i = 0; i < defaultRepos.length / REPO_XML_ITEM_COUNT; i++) { - int offset = i * REPO_XML_ITEM_COUNT; - insertNameAndDescription(db, - defaultRepos[offset], // name - defaultRepos[offset + 1], // address - defaultRepos[offset + 2] // description - ); - } - } - - /** - * Add a fingerprint field to repos. For any field with a public key, - * calculate its fingerprint and save it to the database. - */ - private void addFingerprintToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 44) { - return; - } - if (!columnExists(db, RepoTable.NAME, RepoTable.Cols.FINGERPRINT)) { - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.FINGERPRINT + " text"); - } - List oldrepos = new ArrayList<>(); - Cursor cursor = db.query(RepoTable.NAME, - new String[]{RepoTable.Cols.ADDRESS, RepoTable.Cols.SIGNING_CERT}, - null, null, null, null, null); - if (cursor != null) { - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - Repo repo = new Repo(); - repo.address = cursor.getString(0); - repo.signingCertificate = cursor.getString(1); - oldrepos.add(repo); - cursor.moveToNext(); - } - } - cursor.close(); - } - for (final Repo repo : oldrepos) { - ContentValues values = new ContentValues(); - values.put(RepoTable.Cols.FINGERPRINT, Utils.calcFingerprint(repo.signingCertificate)); - db.update(RepoTable.NAME, values, RepoTable.Cols.ADDRESS + " = ?", new String[]{repo.address}); - } - } - - private void addMaxAgeToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 30 || columnExists(db, RepoTable.NAME, RepoTable.Cols.MAX_AGE)) { - return; - } - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.MAX_AGE + " integer not null default 0"); - } - - private void addVersionToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 33 || columnExists(db, RepoTable.NAME, RepoTable.Cols.VERSION)) { - return; - } - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.VERSION + " integer not null default 0"); - } - - private void addLastUpdatedToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 35 || columnExists(db, RepoTable.NAME, RepoTable.Cols.LAST_UPDATED)) { - return; - } - Utils.debugLog(TAG, "Adding " + RepoTable.Cols.LAST_UPDATED + " column to " + RepoTable.NAME); - db.execSQL("Alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.LAST_UPDATED + " string"); - } - - private void populateRepoNames(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 37) { - return; - } - Utils.debugLog(TAG, "Populating repo names from the url"); - final String[] columns = {RepoTable.Cols.ADDRESS, RepoTable.Cols._ID}; - Cursor cursor = db.query(RepoTable.NAME, columns, - RepoTable.Cols.NAME + " IS NULL OR " + RepoTable.Cols.NAME + " = ''", null, null, null, null); - if (cursor != null) { - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - String address = cursor.getString(0); - long id = cursor.getInt(1); - ContentValues values = new ContentValues(1); - String name = Repo.addressToName(address); - values.put(RepoTable.Cols.NAME, name); - final String[] args = {Long.toString(id)}; - Utils.debugLog(TAG, "Setting repo name to '" + name + "' for repo " + address); - db.update(RepoTable.NAME, values, RepoTable.Cols._ID + " = ?", args); - cursor.moveToNext(); - } - } - cursor.close(); - } - } - - private void renameRepoId(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 36 || columnExists(db, RepoTable.NAME, RepoTable.Cols._ID)) { - return; - } - - Utils.debugLog(TAG, "Renaming " + RepoTable.NAME + ".id to " + RepoTable.Cols._ID); - db.beginTransaction(); - - try { - // http://stackoverflow.com/questions/805363/how-do-i-rename-a-column-in-a-sqlite-database-table#805508 - String tempTableName = RepoTable.NAME + "__temp__"; - db.execSQL("ALTER TABLE " + RepoTable.NAME + " RENAME TO " + tempTableName + ";"); - - // I realise this is available in the CREATE_TABLE_REPO above, - // however I have a feeling that it will need to be the same as the - // current structure of the table as of DBVersion 36, or else we may - // get into strife. For example, if there was a field that - // got removed, then it will break the "insert select" - // statement. Therefore, I've put a copy of CREATE_TABLE_REPO - // here that is the same as it was at DBVersion 36. - String createTableDdl = "create table " + RepoTable.NAME + " (" - + RepoTable.Cols._ID + " integer not null primary key, " - + RepoTable.Cols.ADDRESS + " text not null, " - + RepoTable.Cols.NAME + " text, " - + RepoTable.Cols.DESCRIPTION + " text, " - + RepoTable.Cols.IN_USE + " integer not null, " - + RepoTable.Cols.PRIORITY + " integer not null, " - + RepoTable.Cols.SIGNING_CERT + " text, " - + RepoTable.Cols.FINGERPRINT + " text, " - + RepoTable.Cols.MAX_AGE + " integer not null default 0, " - + RepoTable.Cols.VERSION + " integer not null default 0, " - + RepoTable.Cols.LAST_ETAG + " text, " - + RepoTable.Cols.LAST_UPDATED + " string);"; - - db.execSQL(createTableDdl); - - String nonIdFields = TextUtils.join(", ", new String[]{ - RepoTable.Cols.ADDRESS, - RepoTable.Cols.NAME, - RepoTable.Cols.DESCRIPTION, - RepoTable.Cols.IN_USE, - RepoTable.Cols.PRIORITY, - RepoTable.Cols.SIGNING_CERT, - RepoTable.Cols.FINGERPRINT, - RepoTable.Cols.MAX_AGE, - RepoTable.Cols.VERSION, - RepoTable.Cols.LAST_ETAG, - RepoTable.Cols.LAST_UPDATED, - }); - - String insertSql = "INSERT INTO " + RepoTable.NAME + - "(" + RepoTable.Cols._ID + ", " + nonIdFields + " ) " + - "SELECT id, " + nonIdFields + " FROM " + tempTableName + ";"; - - db.execSQL(insertSql); - db.execSQL("DROP TABLE " + tempTableName + ";"); - db.setTransactionSuccessful(); - } catch (Exception e) { - Log.e(TAG, "Error renaming id to " + RepoTable.Cols._ID, e); - } - db.endTransaction(); - } - - private void addIsSwapToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 47 || columnExists(db, RepoTable.NAME, RepoTable.Cols.IS_SWAP)) { - return; - } - Utils.debugLog(TAG, "Adding " + RepoTable.Cols.IS_SWAP + " field to " + RepoTable.NAME + " table in db."); - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.IS_SWAP + " boolean default 0;"); - } - - private void addCredentialsToRepo(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 52) { - return; - } - if (!columnExists(db, Schema.RepoTable.NAME, RepoTable.Cols.USERNAME)) { - Utils.debugLog(TAG, "Adding " + RepoTable.Cols.USERNAME + " field to " + RepoTable.NAME + " table in db."); - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.USERNAME + " string;"); - } - - if (!columnExists(db, RepoTable.NAME, RepoTable.Cols.PASSWORD)) { - Utils.debugLog(TAG, "Adding " + RepoTable.Cols.PASSWORD + " field to " + RepoTable.NAME + " table in db."); - db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.PASSWORD + " string;"); - } - } - - private void addChangelogToApp(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 48 || columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.CHANGELOG)) { - return; - } - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.CHANGELOG + " column to " + AppMetadataTable.NAME); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.CHANGELOG + " text"); - } - - private void addAuthorToApp(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 53) { - return; - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.AUTHOR_NAME)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.AUTHOR_NAME + " column to " + AppMetadataTable.NAME); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.AUTHOR_NAME + " text"); - } - if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.AUTHOR_EMAIL)) { - Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.AUTHOR_EMAIL + " column to " + AppMetadataTable.NAME); - db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.AUTHOR_EMAIL + " text"); - } - } - - private void useMaxValueInMaxSdkVersion(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 54) { - return; - } - Utils.debugLog(TAG, "Converting " + ApkTable.Cols.MAX_SDK_VERSION + " value 0 to " + Byte.MAX_VALUE); - ContentValues values = new ContentValues(); - values.put(ApkTable.Cols.MAX_SDK_VERSION, Byte.MAX_VALUE); - db.update(ApkTable.NAME, values, ApkTable.Cols.MAX_SDK_VERSION + " < 1", null); - } - - /** - * The {@code } value was in the metadata for a long time, - * but it was not being used in the client until now. - */ - private void requireTimestampInRepos(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 55) { - return; - } - if (!columnExists(db, RepoTable.NAME, RepoTable.Cols.TIMESTAMP)) { - Utils.debugLog(TAG, "Adding " + RepoTable.Cols.TIMESTAMP + " column to " + RepoTable.NAME); - db.execSQL("alter table " + RepoTable.NAME + " add column " - + RepoTable.Cols.TIMESTAMP + " integer not null default 0"); - } - } - - /** - * By clearing the etags stored in the repo table, it means that next time the user updates - * their repos (either manually or on a scheduled task), they will update regardless of whether - * they have changed since last update or not. - */ - private static void clearRepoEtags(SQLiteDatabase db) { - Utils.debugLog(TAG, "Clearing repo etags, so next update will not be skipped with \"Repos up to date\"."); - db.execSQL("update " + RepoTable.NAME + " set " + RepoTable.Cols.LAST_ETAG + " = NULL"); - } - - /** - * Resets all database tables that are generated from the index files downloaded - * from the active repositories. This will trigger the index file(s) to be - * downloaded processed on the next update. - */ - public static void resetTransient(Context context) { - resetTransient(getInstance(context).getWritableDatabase()); - } - - private static void resetTransient(SQLiteDatabase db) { - Utils.debugLog(TAG, "Removing all index tables, they will be recreated next time F-Droid updates."); - - Preferences.get().resetLastUpdateCheck(); - CategoryProvider.Helper.clearCategoryIdCache(); - - 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); - } - - if (tableExists(db, AntiFeatureTable.NAME)) { - db.execSQL("DROP TABLE " + AntiFeatureTable.NAME); - } - - if (tableExists(db, ApkAntiFeatureJoinTable.NAME)) { - db.execSQL("DROP TABLE " + ApkAntiFeatureJoinTable.NAME); - } - - if (tableExists(db, InstalledAppTable.NAME)) { - db.execSQL("DROP TABLE " + InstalledAppTable.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); - db.execSQL(CREATE_TABLE_ANTI_FEATURE); - db.execSQL(CREATE_TABLE_APK_ANTI_FEATURE_JOIN); - db.execSQL(CREATE_TABLE_INSTALLED_APP); - clearRepoEtags(db); - ensureIndexes(db); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - private void resetTransientPre42(SQLiteDatabase db, int oldVersion) { - // Before version 42, only transient info was stored in here. As of some time - // just before 42 (F-Droid 0.60ish) it now has "ignore this version" info which - // was is specified by the user. We don't want to weely-neely nuke that data. - // and the new way to deal with changes to the table structure is to add a - // if (oldVersion < x && !columnExists(...) and then alter the table as required. - if (oldVersion >= 42) { - return; - } - - Preferences.get().resetLastUpdateCheck(); - - db.execSQL("drop table " + AppMetadataTable.NAME); - db.execSQL("drop table " + ApkTable.NAME); - clearRepoEtags(db); - db.execSQL(CREATE_TABLE_APP_METADATA); - db.execSQL(CREATE_TABLE_APK); - ensureIndexes(db); - } - - 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 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 + ");"); - db.execSQL("CREATE INDEX IF NOT EXISTS repoId ON " + ApkTable.NAME + " (" + ApkTable.Cols.REPO_ID + ");"); - - if (tableExists(db, AppPrefsTable.NAME)) { - Utils.debugLog(TAG, "Ensuring indexes exist for " + AppPrefsTable.NAME); - db.execSQL("CREATE INDEX IF NOT EXISTS appPrefs_packageName on " + AppPrefsTable.NAME + " (" + AppPrefsTable.Cols.PACKAGE_NAME + ");"); - db.execSQL("CREATE INDEX IF NOT EXISTS appPrefs_packageName_ignoreAll_ignoreThis on " + AppPrefsTable.NAME + " (" + - AppPrefsTable.Cols.PACKAGE_NAME + ", " + - AppPrefsTable.Cols.IGNORE_ALL_UPDATES + ", " + - AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ");"); - } - - if (columnExists(db, InstalledAppTable.NAME, InstalledAppTable.Cols.PACKAGE_ID)) { - Utils.debugLog(TAG, "Ensuring indexes exist for " + InstalledAppTable.NAME); - db.execSQL("CREATE INDEX IF NOT EXISTS installedApp_packageId_vercode on " + InstalledAppTable.NAME + " (" + - InstalledAppTable.Cols.PACKAGE_ID + ", " + InstalledAppTable.Cols.VERSION_CODE + ");"); - } - - Utils.debugLog(TAG, "Ensuring indexes exist for " + RepoTable.NAME); - db.execSQL("CREATE INDEX IF NOT EXISTS repo_id_isSwap on " + RepoTable.NAME + " (" + - RepoTable.Cols._ID + ", " + RepoTable.Cols.IS_SWAP + ");"); - } - - private void addTargetSdkVersionToApk(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 57) { - return; - } - Utils.debugLog(TAG, "Adding " + ApkTable.Cols.TARGET_SDK_VERSION - + " columns to " + ApkTable.NAME); - db.execSQL("alter table " + ApkTable.NAME + " add column " - + ApkTable.Cols.TARGET_SDK_VERSION + " integer"); - } - - private void supportRepoPushRequests(SQLiteDatabase db, int oldVersion) { - if (oldVersion >= 62) { - return; - } - Utils.debugLog(TAG, "Adding " + RepoTable.Cols.PUSH_REQUESTS - + " columns to " + RepoTable.NAME); - db.execSQL("alter table " + RepoTable.NAME + " add column " - + RepoTable.Cols.PUSH_REQUESTS + " integer not null default " - + Repo.PUSH_REQUEST_IGNORE); - } - - private static boolean columnExists(SQLiteDatabase db, String table, String field) { - boolean found = false; - Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null); - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - String name = cursor.getString(cursor.getColumnIndexOrThrow("name")); - if (name.equalsIgnoreCase(field)) { - found = true; - break; - } - cursor.moveToNext(); - } - cursor.close(); - return found; - } - - private static boolean tableExists(SQLiteDatabase db, String table) { - Cursor cursor = db.query("sqlite_master", new String[]{"name"}, - "type = 'table' AND name = ?", new String[]{table}, null, null, null); - - boolean exists = cursor.getCount() > 0; - cursor.close(); - return exists; - } - - /** - * Insert a new repo into the database. This also initializes the list of - * "mirror" URLs. There should always be at least one URL there. - */ - private void insertRepo(SQLiteDatabase db, String name, String address, - String description, String version, String enabled, - String priority, String pushRequests, String pubKey) { - ContentValues values = new ContentValues(); - values.put(RepoTable.Cols.ADDRESS, address); - values.put(RepoTable.Cols.NAME, name); - values.put(RepoTable.Cols.DESCRIPTION, description); - values.put(RepoTable.Cols.SIGNING_CERT, pubKey); - values.put(RepoTable.Cols.FINGERPRINT, Utils.calcFingerprint(pubKey)); - values.put(RepoTable.Cols.MAX_AGE, 0); - values.put(RepoTable.Cols.VERSION, Utils.parseInt(version, 0)); - values.put(RepoTable.Cols.IN_USE, Utils.parseInt(enabled, 0)); - values.put(RepoTable.Cols.PRIORITY, Utils.parseInt(priority, Integer.MAX_VALUE)); - values.put(RepoTable.Cols.LAST_ETAG, (String) null); - values.put(RepoTable.Cols.TIMESTAMP, 0); - - String[] initializeMirrors = {address}; - values.put(Schema.RepoTable.Cols.MIRRORS, Utils.serializeCommaSeparatedString(initializeMirrors)); - - switch (pushRequests) { - case "ignore": - values.put(RepoTable.Cols.PUSH_REQUESTS, Repo.PUSH_REQUEST_IGNORE); - break; - case "prompt": - values.put(RepoTable.Cols.PUSH_REQUESTS, Repo.PUSH_REQUEST_PROMPT); - break; - case "always": - values.put(RepoTable.Cols.PUSH_REQUESTS, Repo.PUSH_REQUEST_ACCEPT_ALWAYS); - break; - default: - throw new IllegalArgumentException(pushRequests + " is not a supported option!"); - } - - Utils.debugLog(TAG, "Adding repository " + name + " with push requests as " + pushRequests); - db.insert(RepoTable.NAME, null, values); - } - } diff --git a/app/src/main/java/org/fdroid/fdroid/data/FDroidProvider.java b/app/src/main/java/org/fdroid/fdroid/data/FDroidProvider.java deleted file mode 100644 index 1a579d41b..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/FDroidProvider.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.fdroid.fdroid.data; - -import android.content.ContentProvider; -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentValues; -import android.content.OperationApplicationException; -import android.content.UriMatcher; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; - -import org.fdroid.fdroid.BuildConfig; - -import java.util.ArrayList; - -import androidx.annotation.NonNull; - -public abstract class FDroidProvider extends ContentProvider { - - public static final String TAG = "FDroidProvider"; - - static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".data"; - - static final int CODE_LIST = 1; - static final int CODE_SINGLE = 2; - - private boolean applyingBatch; - - protected abstract String getTableName(); - - protected abstract String getProviderName(); - - /** - * Should always be the same as the provider:name in the AndroidManifest - */ - public final String getName() { - return AUTHORITY + "." + getProviderName(); - } - - /** - * Tells us if we are in the middle of a batch of operations. Allows us to - * decide not to notify the content resolver of changes, - * every single time we do something during many operations. - * Based on http://stackoverflow.com/a/15886915. - */ - protected final boolean isApplyingBatch() { - return this.applyingBatch; - } - - @NonNull - @Override - public ContentProviderResult[] applyBatch(@NonNull ArrayList operations) - throws OperationApplicationException { - ContentProviderResult[] result = null; - applyingBatch = true; - final SQLiteDatabase db = db(); - db.beginTransaction(); - try { - result = super.applyBatch(operations); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - applyingBatch = false; - } - return result; - } - - @Override - public boolean onCreate() { - return true; - } - - protected final synchronized SQLiteDatabase db() { - return DBHelper.getInstance(getContext()).getWritableDatabase(); - } - - @Override - public String getType(@NonNull Uri uri) { - String type; - switch (getMatcher().match(uri)) { - case CODE_LIST: - type = "dir"; - break; - case CODE_SINGLE: - default: - type = "item"; - break; - } - return "vnd.android.cursor." + type + "/vnd." + AUTHORITY + "." + getProviderName(); - } - - protected abstract UriMatcher getMatcher(); - - protected static String generateQuestionMarksForInClause(int num) { - StringBuilder sb = new StringBuilder(num * 2); - for (int i = 0; i < num; i++) { - if (i != 0) { - sb.append(','); - } - sb.append('?'); - } - return sb.toString(); - } - - protected void validateFields(String[] validFields, ContentValues values) - throws IllegalArgumentException { - for (final String key : values.keySet()) { - boolean isValid = false; - for (final String validKey : validFields) { - if (validKey.equals(key)) { - isValid = true; - break; - } - } - - if (!isValid) { - throw new IllegalArgumentException( - "Cannot save field '" + key + "' to provider " + getProviderName()); - } - } - } - - /** - * 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 + " = ?"; - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/LoggingQuery.java b/app/src/main/java/org/fdroid/fdroid/data/LoggingQuery.java deleted file mode 100644 index 881d1c707..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/LoggingQuery.java +++ /dev/null @@ -1,171 +0,0 @@ -package org.fdroid.fdroid.data; - -import android.database.Cursor; -import android.database.CursorWrapper; -import android.database.sqlite.SQLiteDatabase; - -import org.fdroid.fdroid.BuildConfig; -import org.fdroid.fdroid.Utils; - -/** - * Helper class to log slow queries to logcat when in debug mode. When not in debug mode, it - * runs the queries without any logging. - * - * Here is an example of what would be output to logcat for a query that takes too long (except the - * query would not be formatted as nicely): - * - * Query [155ms]: - * SELECT fdroid_app.rowid as _id, ... - * FROM fdroid_app - * LEFT JOIN fdroid_apk ON (fdroid_apk.appId = fdroid_app.rowid) - * LEFT JOIN fdroid_repo ON (fdroid_apk.repo = fdroid_repo._id) - * LEFT JOIN fdroid_installedApp AS installed ON (installed.appId = fdroid_app.id) - * LEFT JOIN fdroid_apk AS suggestedApk ON (fdroid_app.suggestedVercode = suggestedApk.vercode - * AND fdroid_app.rowid = suggestedApk.appId) - * WHERE - * fdroid_repo.isSwap = 0 OR fdroid_repo.isSwap IS NULL - * GROUP BY fdroid_app.rowid - * ORDER BY fdroid_app.name COLLATE LOCALIZED - * Explain: - * SCAN TABLE fdroid_app - * SEARCH TABLE fdroid_apk USING COVERING INDEX sqlite_autoindex_fdroid_apk_1 (appId=?) - * SEARCH TABLE fdroid_repo USING INTEGER PRIMARY KEY (rowid=?) - * SEARCH TABLE fdroid_installedApp AS installed USING INDEX sqlite_autoindex_fdroid_installedApp_1 (appId=?) - * SEARCH TABLE fdroid_apk AS suggestedApk USING INDEX sqlite_autoindex_fdroid_apk_1 (appId=? AND vercode=?) - * USE TEMP B-TREE FOR ORDER BY - */ -final class LoggingQuery { - - private static final long SLOW_QUERY_DURATION = 100; - private static final String TAG = "Slow Query"; - - private final SQLiteDatabase db; - private final String query; - private final String[] queryArgs; - - private LoggingQuery(SQLiteDatabase db, String query, String[] queryArgs) { - this.db = db; - this.query = query; - this.queryArgs = queryArgs; - } - - /** - * When running a debug build, this will log details (including query plans) for any query which - * takes longer than {@link LoggingQuery#SLOW_QUERY_DURATION}. - */ - private Cursor rawQuery() { - if (BuildConfig.DEBUG) { - long startTime = System.currentTimeMillis(); - Cursor cursor = db.rawQuery(query, queryArgs); - long queryDuration = System.currentTimeMillis() - startTime; - - if (queryDuration >= SLOW_QUERY_DURATION) { - logSlowQuery(queryDuration); - } - - return new LogGetCountCursorWrapper(cursor); - } - return db.rawQuery(query, queryArgs); - } - - /** - * Sometimes the query will not actually be run when invoking "query()". - * Under such circumstances, it falls to the {@link android.content.ContentProvider#query} - * method to manually invoke the {@link Cursor#getCount()} method to force query execution. - * It does so with a comment saying "Force query execution". When this happens, the call to - * query() takes 1ms, whereas the call go getCount() is the bit which takes time. - * As such, we will also track that method duration in order to potentially log slow queries. - */ - private final class LogGetCountCursorWrapper extends CursorWrapper { - private LogGetCountCursorWrapper(Cursor cursor) { - super(cursor); - } - - @Override - public int getCount() { - long startTime = System.currentTimeMillis(); - int count = super.getCount(); - long queryDuration = System.currentTimeMillis() - startTime; - if (queryDuration >= SLOW_QUERY_DURATION) { - logSlowQuery(queryDuration); - } - return count; - } - } - - private void execSQLInternal() { - if (BuildConfig.DEBUG) { - long startTime = System.currentTimeMillis(); - long queryDuration = System.currentTimeMillis() - startTime; - executeSQLInternal(); - if (queryDuration >= SLOW_QUERY_DURATION) { - logSlowQuery(queryDuration); - } - } else { - executeSQLInternal(); - } - } - - private void executeSQLInternal() { - if (queryArgs == null || queryArgs.length == 0) { - db.execSQL(query); - } else { - db.execSQL(query, queryArgs); - } - } - - /** - * Log the query and its duration to the console. In addition, execute an "EXPLAIN QUERY PLAN" - * for the query in question so that the query can be diagnosed (https://sqlite.org/eqp.html) - */ - private void logSlowQuery(long queryDuration) { - StringBuilder sb = new StringBuilder(); - sb.append("Query [") - .append(queryDuration) - .append("ms]: ") - .append(query); - - try { - StringBuilder sbExplain = new StringBuilder("\nExplain:\n"); - for (String plan : getExplainQueryPlan()) { - sbExplain.append(" ").append(plan).append("\n"); - } - sb.append(sbExplain); - } catch (Exception e) { - // Ignore exception, we caught this because the SQLite docs say explain query plan can - // change between versions. We do our best in getExplainQueryPlan() to mitigate this, - // but it may still break with newer releases. In that case, at least we will still be - // logging the slow query to logcat which is helpful. - } - - Utils.debugLog(TAG, sb.toString()); - } - - private String[] getExplainQueryPlan() { - Cursor cursor = db.rawQuery("EXPLAIN QUERY PLAN " + query, queryArgs); - String[] plan = new String[cursor.getCount()]; - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - // The docs at https://sqlite.org/eqp.html talk about how the output format of - // EXPLAIN QUERY PLAN can change between SQLite versions. This has been observed - // between the sqlite versions on Android 2.3.3 and Android 5.0. However, it seems - // that the last column is always the one with the interesting details that we wish - // to log. If this fails for some reason, then hey, it is only for debug builds, right? - if (cursor.getColumnCount() > 0) { - int index = cursor.getColumnCount() - 1; - plan[cursor.getPosition()] = cursor.getString(index); - } - cursor.moveToNext(); - } - cursor.close(); - return plan; - } - - public static Cursor rawQuery(SQLiteDatabase db, String query, String[] queryBuilderArgs) { - return new LoggingQuery(db, query, queryBuilderArgs).rawQuery(); - } - - public static void execSQL(SQLiteDatabase db, String sql, String[] queryArgs) { - new LoggingQuery(db, sql, queryArgs).execSQLInternal(); - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/ObbUrlActivity.java b/app/src/main/java/org/fdroid/fdroid/data/ObbUrlActivity.java index e18b53dee..924e0a3f8 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/ObbUrlActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/data/ObbUrlActivity.java @@ -2,7 +2,6 @@ package org.fdroid.fdroid.data; import android.content.ComponentName; import android.content.Intent; -import android.content.pm.PackageInfo; import android.net.Uri; import android.os.Bundle; @@ -38,11 +37,6 @@ public class ObbUrlActivity extends AppCompatActivity { String packageName = componentName.getPackageName(); Apk apk = null; - PackageInfo packageInfo = Utils.getPackageInfo(this, packageName); - if (packageInfo != null) { - apk = ApkProvider.Helper.findApkFromAnyRepo(this, packageName, packageInfo.versionCode); - } - if (apk == null) { Utils.debugLog(TAG, "got null APK for " + packageName); } else if (ACTION_GET_OBB_MAIN_URL.equals(action)) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/OrderClause.java b/app/src/main/java/org/fdroid/fdroid/data/OrderClause.java deleted file mode 100644 index c5e415914..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/OrderClause.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.fdroid.fdroid.data; - -class OrderClause { - - private final String expression; - private String[] args; - - OrderClause(String expression) { - this.expression = expression; - } - - OrderClause(String field, String[] args, boolean isAscending) { - this.expression = field + " " + (isAscending ? "ASC" : "DESC"); - this.args = args; - } - - @Override - public String toString() { - return expression; - } - - public String[] getArgs() { - return args; - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/PackageIdProvider.java b/app/src/main/java/org/fdroid/fdroid/data/PackageIdProvider.java deleted file mode 100644 index b1a06208a..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/PackageIdProvider.java +++ /dev/null @@ -1,175 +0,0 @@ -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; - -import androidx.annotation.NonNull; - -public class PackageIdProvider 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 = "PackageIdProvider"; - - 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 PROVIDER_NAME; - } - - 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(@NonNull 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.rawQuery(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(@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 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(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) { - throw new UnsupportedOperationException("Update not supported for " + uri + "."); - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/QueryBuilder.java b/app/src/main/java/org/fdroid/fdroid/data/QueryBuilder.java deleted file mode 100644 index 9669a83ed..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/QueryBuilder.java +++ /dev/null @@ -1,169 +0,0 @@ -package org.fdroid.fdroid.data; - -import android.text.TextUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import androidx.annotation.Nullable; - -abstract class QueryBuilder { - - private final List fields = new ArrayList<>(); - private final StringBuilder tables = new StringBuilder(getRequiredTables()); - private String selection; - private String[] selectionArgs; - private final List orderBys = new ArrayList<>(); - private int limit = 0; - - protected abstract String getRequiredTables(); - - public abstract void addField(String field); - - public void addFields(String[] fields) { - for (final String field : fields) { - addField(field); - } - } - - protected boolean isDistinct() { - return false; - } - - protected String groupBy() { - return null; - } - - protected void appendField(String field) { - appendField(field, null, null); - } - - protected void appendField(String field, String tableAlias) { - appendField(field, tableAlias, null); - } - - protected final void appendField(String field, String tableAlias, - String fieldAlias) { - - StringBuilder fieldBuilder = new StringBuilder(); - - if (tableAlias != null) { - fieldBuilder.append(tableAlias).append('.'); - } - - fieldBuilder.append(field); - - if (fieldAlias != null) { - fieldBuilder.append(" AS ").append(fieldAlias); - } - - fields.add(fieldBuilder.toString()); - } - - public void addSelection(@Nullable QuerySelection selection) { - if (selection == null) { - this.selection = null; - this.selectionArgs = null; - } else { - this.selection = selection.getSelection(); - this.selectionArgs = selection.getArgs(); - } - } - - /** - * Add an order by, which includes an expression and optionally ASC or DESC afterward. - */ - public void addOrderBy(String orderBy) { - if (orderBy != null) { - orderBys.add(new OrderClause(orderBy)); - } - } - - public void addOrderBy(@Nullable OrderClause orderClause) { - if (orderClause != null) { - orderBys.add(orderClause); - } - } - - public void addLimit(int limit) { - this.limit = limit; - } - - public String[] getArgs() { - List args = new ArrayList<>(); - - if (selectionArgs != null) { - Collections.addAll(args, selectionArgs); - } - - for (OrderClause orderBy : orderBys) { - if (orderBy.getArgs() != null) { - Collections.addAll(args, orderBy.getArgs()); - } - } - - String[] strings = new String[args.size()]; - args.toArray(strings); - return strings; - } - - protected final void leftJoin(String table, String alias, String condition) { - joinWithType("LEFT", table, alias, condition); - } - - protected final void join(String table, String alias, String condition) { - joinWithType("", table, alias, condition); - } - - private void joinWithType(String type, String table, String alias, String condition) { - tables.append(' ') - .append(type) - .append(" JOIN ") - .append(table); - - if (alias != null) { - tables.append(" AS ").append(alias); - } - - tables.append(" ON (") - .append(condition) - .append(')'); - } - - private String distinctSql() { - return isDistinct() ? " DISTINCT " : ""; - } - - private String fieldsSql() { - return TextUtils.join(", ", fields); - } - - private String whereSql() { - return selection != null ? " WHERE " + selection : ""; - } - - private String orderBySql() { - if (orderBys.isEmpty()) { - return ""; - } - return " ORDER BY " + TextUtils.join(", ", orderBys); - } - - private String groupBySql() { - return groupBy() != null ? " GROUP BY " + groupBy() : ""; - } - - private String tablesSql() { - return tables.toString(); - } - - private String limitSql() { - return limit > 0 ? " LIMIT " + limit : ""; - } - - public String toString() { - return "SELECT " + distinctSql() + fieldsSql() + " FROM " + tablesSql() - + whereSql() + groupBySql() + orderBySql() + limitSql(); - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java b/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java deleted file mode 100644 index 5c32395a7..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/QuerySelection.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.fdroid.fdroid.data; - -import android.text.TextUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Helper class used by sublasses of ContentProvider to make the constraints - * required for a given content URI (e.g. all apps that belong to a repo) - * easily appendable to the constraints which are passed into, e.g. the query() - * method in the content provider. - */ -public class QuerySelection { - - private final String[] args; - private final String selection; - - public QuerySelection(String selection) { - this.selection = selection; - this.args = new String[] {}; - } - - public QuerySelection(String selection, String[] args) { - this.args = args; - this.selection = selection; - } - - public QuerySelection(String selection, List args) { - this.args = new String[ args.size() ]; - args.toArray(this.args); - this.selection = selection; - } - - public String[] getArgs() { - return args; - } - - public String getSelection() { - return selection; - } - - private boolean hasSelection() { - return !TextUtils.isEmpty(selection); - } - - private boolean hasArgs() { - return args != null && args.length > 0; - } - - public QuerySelection add(String selection, String[] args) { - return add(new QuerySelection(selection, args)); - } - - public QuerySelection add(QuerySelection query) { - String s = null; - if (this.hasSelection() && query.hasSelection()) { - s = " (" + this.selection + ") AND (" + query.getSelection() + ") "; - } else if (this.hasSelection()) { - s = this.selection; - } else if (query.hasSelection()) { - s = query.selection; - } - - int thisNumArgs = this.hasArgs() ? this.args.length : 0; - int queryNumArgs = query.hasArgs() ? query.args.length : 0; - List a = new ArrayList<>(thisNumArgs + queryNumArgs); - - if (this.hasArgs()) { - Collections.addAll(a, this.args); - } - - if (query.hasArgs()) { - Collections.addAll(a, query.getArgs()); - } - - return new QuerySelection(s, a); - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/Repo.java b/app/src/main/java/org/fdroid/fdroid/data/Repo.java index 23f394a5e..3c5f77aca 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Repo.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Repo.java @@ -23,32 +23,13 @@ package org.fdroid.fdroid.data; -import android.content.ContentValues; -import android.database.Cursor; import android.net.Uri; -import android.text.TextUtils; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import org.fdroid.download.DownloadRequest; -import org.fdroid.download.Mirror; -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Schema.RepoTable.Cols; import org.fdroid.fdroid.net.TreeUriDownloader; -import java.net.MalformedURLException; -import java.net.Proxy; -import java.net.URL; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.Date; -import java.util.HashSet; import java.util.List; -import info.guardianproject.netcipher.NetCipher; - - /** * Represents a the descriptive info and metadata about a given repo, as provided * by the repo index. This also keeps track of the state of the repo. @@ -61,250 +42,21 @@ import info.guardianproject.netcipher.NetCipher; * @see fdroiddata * @see fdroidserver */ -public class Repo extends ValueObject { - - public static final int VERSION_DENSITY_SPECIFIC_ICONS = 11; +public class Repo { public static final int PUSH_REQUEST_IGNORE = 0; - public static final int PUSH_REQUEST_PROMPT = 1; - public static final int PUSH_REQUEST_ACCEPT_ALWAYS = 2; - public static final int INT_UNSET_VALUE = -1; - // these are never set by the Apk/package index metadata - @JsonIgnore - protected long id; - @JsonIgnore - public boolean inuse; - @JsonIgnore - public int priority; - @JsonIgnore - public Date lastUpdated; - @JsonIgnore - public boolean isSwap; - /** - * last etag we updated from, null forces update - */ - @JsonIgnore - public String lastetag; - /** - * How to treat push requests included in this repo's index XML. This comes - * from {@code default_repo.xml} or perhaps user input. It should never be - * settable from the server-side. - */ - @JsonIgnore - public int pushRequests = PUSH_REQUEST_IGNORE; - - /** - * The canonical URL of the repo. - */ public String address; - public String name; - public String description; - public String icon; - /** - * index version, i.e. what fdroidserver built it - 0 if not specified - */ - public int version; - /** - * The signing certificate, {@code null} for a newly added repo - */ - public String signingCertificate; - /** - * The SHA1 fingerprint of {@link #signingCertificate}, set to {@code null} when a - * newly added repo did not include fingerprint. It should never be an - * empty {@link String}, i.e. {@code ""} - */ - public String fingerprint; - /** - * maximum age of index that will be accepted - 0 for any - */ - public int maxage; - - public String username; - public String password; - - /** - * When the signed repo index was generated, used to protect against replay attacks - */ - public long timestamp; - - /** - * Official mirrors of this repo, considered automatically interchangeable - */ - public String[] mirrors; - - /** - * Mirrors added by the user, either by UI input or by attaching removeable storage - */ - @JsonIgnore - public String[] userMirrors; - - /** - * Mirrors that have been manually disabled by the user. - */ - @JsonIgnore - public String[] disabledMirrors; - - public Repo() { - } - - public Repo(String address) { - this.address = address; - } - - public Repo(Cursor cursor) { - - checkCursorPosition(cursor); - - for (int i = 0; i < cursor.getColumnCount(); i++) { - switch (cursor.getColumnName(i)) { - case Cols._ID: - id = cursor.getInt(i); - break; - case Cols.LAST_ETAG: - lastetag = cursor.getString(i); - break; - case Cols.ADDRESS: - address = cursor.getString(i); - break; - case Cols.DESCRIPTION: - description = cursor.getString(i); - break; - case Cols.FINGERPRINT: - fingerprint = cursor.getString(i); - break; - case Cols.IN_USE: - inuse = cursor.getInt(i) == 1; - break; - case Cols.LAST_UPDATED: - String dateString = cursor.getString(i); - lastUpdated = Utils.parseTime(dateString, Utils.parseDate(dateString, null)); - break; - case Cols.MAX_AGE: - maxage = cursor.getInt(i); - break; - case Cols.VERSION: - version = cursor.getInt(i); - break; - case Cols.NAME: - name = cursor.getString(i); - break; - case Cols.SIGNING_CERT: - signingCertificate = cursor.getString(i); - break; - case Cols.PRIORITY: - priority = cursor.getInt(i); - break; - case Cols.IS_SWAP: - isSwap = cursor.getInt(i) == 1; - break; - case Cols.USERNAME: - username = cursor.getString(i); - break; - case Cols.PASSWORD: - password = cursor.getString(i); - break; - case Cols.TIMESTAMP: - timestamp = cursor.getLong(i); - break; - case Cols.ICON: - icon = cursor.getString(i); - break; - case Cols.MIRRORS: - mirrors = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.USER_MIRRORS: - userMirrors = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.DISABLED_MIRRORS: - disabledMirrors = Utils.parseCommaSeparatedString(cursor.getString(i)); - break; - case Cols.PUSH_REQUESTS: - pushRequests = cursor.getInt(i); - break; - } - } - } - - /** - * @return the database ID to find this repo in the database - */ - public long getId() { - return id; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return address; - } - - public boolean isSigned() { - return !TextUtils.isEmpty(this.signingCertificate); - } - - /** - * This happens when a repo is configed with a fingerprint, but the client - * has not connected to it yet to download its signing certificate - */ - public boolean isSignedButUnverified() { - return TextUtils.isEmpty(this.signingCertificate) && !TextUtils.isEmpty(this.fingerprint); - } - - public boolean hasBeenUpdated() { - return this.lastetag != null; - } - - /** - * If we haven't run an update for this repo yet, then the name - * will be unknown, in which case we will just take a guess at an - * appropriate name based on the url (e.g. "f-droid.org/archive") - */ - public static String addressToName(String address) { - String tempName; - try { - URL url = new URL(address); - tempName = url.getHost() + url.getPath(); - } catch (MalformedURLException e) { - tempName = address; - } - return tempName; - } - - /** - * Gets the path relative to the repo root. - * Can be used to create URLs for use with mirrors. - * Attention: This does NOT encode for use in URLs. - */ - public static String getPath(String... pathElements) { - /* Each String in pathElements might contain a /, should keep these as path elements */ - ArrayList elements = new ArrayList<>(); - for (String element : pathElements) { - Collections.addAll(elements, element.split("/")); - } - // build up path WITHOUT encoding the segments, this will happen later when turned into URL - StringBuilder sb = new StringBuilder(); - for (String element : elements) { - sb.append(element).append("/"); - } - sb.deleteCharAt(sb.length() - 1); // remove trailing slash - return sb.toString(); - } @Deprecated // not taking mirrors into account public String getFileUrl(String... pathElements) { /* Each String in pathElements might contain a /, should keep these as path elements */ - List elements = new ArrayList(); + List elements = new ArrayList<>(); for (String element : pathElements) { - for (String elementPart : element.split("/")) { - elements.add(elementPart); - } + Collections.addAll(elements, element.split("/")); } - /** + /* * Storage Access Framework URLs have this wacky URL-encoded path within the URL path. * * i.e. @@ -328,123 +80,4 @@ public class Repo extends ValueObject { return result.build().toString(); } } - - public DownloadRequest getDownloadRequest(String path) { - List mirrors = Mirror.fromStrings(getMirrorList()); - Proxy proxy = NetCipher.getProxy(); - return new DownloadRequest(path, mirrors, proxy, username, password); - } - - private static int toInt(Integer value) { - if (value == null) { - return 0; - } - return value; - } - - public void setValues(ContentValues values) { - - if (values.containsKey(Cols._ID)) { - id = toInt(values.getAsInteger(Cols._ID)); - } - - if (values.containsKey(Cols.LAST_ETAG)) { - lastetag = values.getAsString(Cols.LAST_ETAG); - } - - if (values.containsKey(Cols.ADDRESS)) { - address = values.getAsString(Cols.ADDRESS); - } - - if (values.containsKey(Cols.DESCRIPTION)) { - description = values.getAsString(Cols.DESCRIPTION); - } - - if (values.containsKey(Cols.FINGERPRINT)) { - fingerprint = values.getAsString(Cols.FINGERPRINT); - } - - if (values.containsKey(Cols.IN_USE)) { - inuse = toInt(values.getAsInteger(Cols.IN_USE)) == 1; - } - - if (values.containsKey(Cols.LAST_UPDATED)) { - final String dateString = values.getAsString(Cols.LAST_UPDATED); - lastUpdated = Utils.parseTime(dateString, Utils.parseDate(dateString, null)); - } - - if (values.containsKey(Cols.MAX_AGE)) { - maxage = toInt(values.getAsInteger(Cols.MAX_AGE)); - } - - if (values.containsKey(Cols.VERSION)) { - version = toInt(values.getAsInteger(Cols.VERSION)); - } - - if (values.containsKey(Cols.NAME)) { - name = values.getAsString(Cols.NAME); - } - - if (values.containsKey(Cols.SIGNING_CERT)) { - signingCertificate = values.getAsString(Cols.SIGNING_CERT); - } - - if (values.containsKey(Cols.PRIORITY)) { - priority = toInt(values.getAsInteger(Cols.PRIORITY)); - } - - if (values.containsKey(Cols.IS_SWAP)) { - isSwap = toInt(values.getAsInteger(Cols.IS_SWAP)) == 1; - } - - if (values.containsKey(Cols.USERNAME)) { - username = values.getAsString(Cols.USERNAME); - } - - if (values.containsKey(Cols.PASSWORD)) { - password = values.getAsString(Cols.PASSWORD); - } - - if (values.containsKey(Cols.TIMESTAMP)) { - timestamp = toInt(values.getAsInteger(Cols.TIMESTAMP)); - } - - if (values.containsKey(Cols.ICON)) { - icon = values.getAsString(Cols.ICON); - } - - if (values.containsKey(Cols.MIRRORS)) { - mirrors = Utils.parseCommaSeparatedString(values.getAsString(Cols.MIRRORS)); - } - - if (values.containsKey(Cols.USER_MIRRORS)) { - userMirrors = Utils.parseCommaSeparatedString(values.getAsString(Cols.USER_MIRRORS)); - } - - if (values.containsKey(Cols.DISABLED_MIRRORS)) { - disabledMirrors = Utils.parseCommaSeparatedString(values.getAsString(Cols.DISABLED_MIRRORS)); - } - - if (values.containsKey(Cols.PUSH_REQUESTS)) { - pushRequests = toInt(values.getAsInteger(Cols.PUSH_REQUESTS)); - } - } - - /** - * @return {@link List} of valid URLs to reach this repo, including the canonical URL - */ - public List getMirrorList() { - final HashSet allMirrors = new HashSet<>(); - if (userMirrors != null) { - allMirrors.addAll(Arrays.asList(userMirrors)); - } - if (mirrors != null) { - allMirrors.addAll(Arrays.asList(mirrors)); - } - allMirrors.add(address); - if (disabledMirrors != null) { - allMirrors.removeAll(Arrays.asList(disabledMirrors)); - } - return new ArrayList<>(allMirrors); - } } diff --git a/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java b/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java deleted file mode 100644 index 5b13c2fb9..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java +++ /dev/null @@ -1,511 +0,0 @@ -package org.fdroid.fdroid.data; - -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.UriMatcher; -import android.database.Cursor; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.fdroid.fdroid.AppUpdateStatusManager; -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Schema.RepoTable; -import org.fdroid.fdroid.data.Schema.RepoTable.Cols; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -public class RepoProvider extends FDroidProvider { - - private static final String TAG = "RepoProvider"; - - public static final class Helper { - - private static final String TAG = "RepoProvider.Helper"; - - private Helper() { - } - - /** - * Find by the content URI of a repo ({@link RepoProvider#getContentUri(long)}). - */ - public static Repo get(Context context, Uri uri) { - ContentResolver resolver = context.getContentResolver(); - Cursor cursor = resolver.query(uri, Cols.ALL, null, null, null); - return cursorToRepo(cursor); - } - - public static Repo findById(Context context, long repoId) { - return findById(context, repoId, Cols.ALL); - } - - public static Repo findById(Context context, long repoId, - String[] projection) { - ContentResolver resolver = context.getContentResolver(); - Uri uri = RepoProvider.getContentUri(repoId); - Cursor cursor = resolver.query(uri, projection, null, null, null); - return cursorToRepo(cursor); - } - - /** - * This method decides what repo a URL belongs to by iteratively removing path fragments and - * checking if it belongs to a repo or not. It will match the most specific repository which - * could serve the file at the given URL. - *

- * For any given HTTP resource requested by F-Droid, it should belong to a repository. - * Whether that resource is an index.jar, an icon, or a .apk file, they all belong to a - * repository. Therefore, that repository must exist in the database. The way to find out - * which repository a particular URL came from requires some consideration: - *

  • Repositories can exist at particular paths on a server (e.g. /fdroid/repo) - *
  • Individual files can exist at a more specific path on the repo (e.g. - * /fdroid/repo/icons/org.fdroid.fdroid.png)
  • - *

    - * So for a given URL "/fdroid/repo/icons/org.fdroid.fdroid.png" we don't actually know - * whether it is for the file "org.fdroid.fdroid.png" at repository "/fdroid/repo/icons" or - * the file "icons/org.fdroid.fdroid.png" at the repository at "/fdroid/repo". - */ - @Nullable - public static Repo findByUrl(Context context, Uri uri, String[] projection) { - Uri withoutQuery = uri.buildUpon().query(null).build(); - Repo repo = findByAddress(context, withoutQuery.toString(), projection); - - // Take a copy of this, because the result of getPathSegments() is an AbstractList - // which doesn't support the remove() operation. - List pathSegments = new ArrayList<>(withoutQuery.getPathSegments()); - - boolean haveTriedWithoutPath = false; - while (repo == null && !haveTriedWithoutPath) { - if (pathSegments.isEmpty()) { - haveTriedWithoutPath = true; - } else { - pathSegments.remove(pathSegments.size() - 1); - withoutQuery = withoutQuery.buildUpon().path(TextUtils.join("/", pathSegments)).build(); - } - repo = findByAddress(context, withoutQuery.toString(), projection); - } - return repo; - } - - public static Repo findByAddress(Context context, String address) { - return findByAddress(context, address, Cols.ALL); - } - - public static Repo findByAddress(Context context, - String address, String[] projection) { - List repos = findBy( - context, Cols.ADDRESS, address, projection); - if (repos.isEmpty()) { - return null; - } else { - return repos.get(0); - } - } - - public static List all(Context context) { - return all(context, Cols.ALL); - } - - public static List all(Context context, String[] projection) { - ContentResolver resolver = context.getContentResolver(); - Uri uri = RepoProvider.getContentUri(); - Cursor cursor = resolver.query(uri, projection, null, null, null); - return cursorToList(cursor); - } - - private static List findBy(Context context, - String fieldName, - String fieldValue, - String[] projection) { - ContentResolver resolver = context.getContentResolver(); - Uri uri = RepoProvider.getContentUri(); - final String[] args = {fieldValue}; - Cursor cursor = resolver.query( - uri, projection, fieldName + " = ?", args, null); - return cursorToList(cursor); - } - - private static List cursorToList(Cursor cursor) { - int knownRepoCount = cursor != null ? cursor.getCount() : 0; - List repos = new ArrayList<>(knownRepoCount); - if (cursor != null) { - if (knownRepoCount > 0) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - repos.add(new Repo(cursor)); - cursor.moveToNext(); - } - } - cursor.close(); - } - return repos; - } - - private static Repo cursorToRepo(Cursor cursor) { - Repo repo = null; - if (cursor != null) { - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - repo = new Repo(cursor); - } - cursor.close(); - } - return repo; - } - - /** - * Updates the repo metadata in the database. All data comes from the - * index file except {@link Repo#id}, which is generated by the database. - * That makes for an two cycle workflow, where first this must be called - * to fetch the {@code Repo.id} from the database, then it is called a - * second time to actually set the repo metadata. - */ - public static void update(Context context, Repo repo, ContentValues values) { - ContentResolver resolver = context.getContentResolver(); - - // Change the name to the new address. Next time we update the repo - // index file, it will populate the name field with the proper - // name, but the best we can do is guess right now. - if (values.containsKey(Cols.ADDRESS) && - !values.containsKey(Cols.NAME)) { - String name = Repo.addressToName(values.getAsString(Cols.ADDRESS)); - values.put(Cols.NAME, name); - } - - /* - * If the repo is signed and has a public key, then guarantee that - * the fingerprint is also set. The stored fingerprint is checked - * when a repo URI is received by FDroid to prevent bad actors from - * overriding repo configs with other keys. So if the fingerprint is - * not stored yet, calculate it and store it. If the fingerprint is - * stored, then check it against the calculated fingerprint just to - * make sure it is correct. If the fingerprint is empty, then store - * the calculated one. - */ - if (values.containsKey(Cols.SIGNING_CERT)) { - String publicKey = values.getAsString(Cols.SIGNING_CERT); - String calcedFingerprint = Utils.calcFingerprint(publicKey); - if (values.containsKey(Cols.FINGERPRINT)) { - String fingerprint = values.getAsString(Cols.FINGERPRINT); - if (!TextUtils.isEmpty(publicKey)) { - if (TextUtils.isEmpty(fingerprint)) { - values.put(Cols.FINGERPRINT, calcedFingerprint); - } else if (!fingerprint.equals(calcedFingerprint)) { - // TODO the UI should represent this error! - Log.e(TAG, "The stored and calculated fingerprints do not match!"); - Log.e(TAG, "Stored: " + fingerprint); - Log.e(TAG, "Calculated: " + calcedFingerprint); - } - } - } else if (!TextUtils.isEmpty(publicKey)) { - // no fingerprint in 'values', so put one there - values.put(Cols.FINGERPRINT, calcedFingerprint); - } - } - - if (values.containsKey(Cols.IN_USE)) { - Integer inUse = values.getAsInteger(Cols.IN_USE); - if (inUse != null && inUse == 0) { - values.put(Cols.LAST_ETAG, (String) null); - } - } - - final Uri uri = getContentUri(repo.getId()); - final String[] args = {Long.toString(repo.getId())}; - resolver.update(uri, values, Cols._ID + " = ?", args); - repo.setValues(values); - } - - /** - * This doesn't do anything other than call "insert" on the content - * resolver, but I thought I'd put it here in the interests of having - * each of the CRUD methods available in the helper class. - */ - public static Uri insert(Context context, - ContentValues values) { - ContentResolver resolver = context.getContentResolver(); - Uri uri = RepoProvider.getContentUri(); - return resolver.insert(uri, values); - } - - public static void remove(Context context, long repoId) { - purgeApps(context, findById(context, repoId)); - ContentResolver resolver = context.getContentResolver(); - Uri uri = RepoProvider.getContentUri(repoId); - resolver.delete(uri, null, null); - } - - public static void purgeApps(Context context, Repo repo) { - Uri apkUri = ApkProvider.getRepoUri(repo.getId()); - ContentResolver resolver = context.getContentResolver(); - int apkCount = resolver.delete(apkUri, null, null); - Utils.debugLog(TAG, "Removed " + apkCount + " apks from repo " + repo.name); - - Uri appUri = AppProvider.getRepoUri(repo); - int appCount = resolver.delete(appUri, null, null); - Utils.debugLog(TAG, "Removed " + appCount + " apps from repo " + repo.address + "."); - - AppUpdateStatusManager.getInstance(context).removeAllByRepo(repo.id); - - AppProvider.Helper.recalculatePreferredMetadata(context); - } - - public static int countAppsForRepo(Context context, long repoId) { - ContentResolver resolver = context.getContentResolver(); - final String[] projection = {Schema.ApkTable.Cols._COUNT_DISTINCT}; - Uri apkUri = ApkProvider.getRepoUri(repoId); - Cursor cursor = resolver.query(apkUri, projection, null, null, null); - int count = 0; - if (cursor != null) { - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - count = cursor.getInt(0); - } - cursor.close(); - } - return count; - } - - @Nullable - public static Date lastUpdate(Context context) { - ContentResolver resolver = context.getContentResolver(); - final String[] projection = {Cols.LAST_UPDATED}; - final String selection = Cols.IN_USE + " = 1"; - Cursor cursor = resolver.query(getContentUri(), projection, - selection, null, Cols.LAST_UPDATED + " DESC"); - - Date lastUpdate = null; - if (cursor != null) { - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - String dateString = cursor.getString(0); - lastUpdate = Utils.parseTime(dateString, Utils.parseDate(dateString, null)); - } - cursor.close(); - } - - return lastUpdate; - } - - public static int countEnabledRepos(Context context) { - ContentResolver resolver = context.getContentResolver(); - final String[] projection = {Cols._ID}; - final String selection = Cols.IN_USE + " = 1"; - Cursor cursor = resolver.query(getContentUri(), projection, selection, null, null); - - int count = 0; - if (cursor != null) { - count = cursor.getCount(); - cursor.close(); - } - - return count; - } - - /** - * Helper method to ensure that next time the user asks for a repository update, we will - * fetch the metadata and update regardless of whether the metadata has changed or not. - * This is useful for when we change languages, because we need to ask the user to fetch - * the metadata again, so that we can extract the correctly-localized metadata. - */ - public static void clearEtags(Context context) { - ContentValues values = new ContentValues(1); - values.put(Cols.LAST_ETAG, (String) null); - context.getContentResolver().update(getContentUri(), values, null, null); - } - } - - private static final String PROVIDER_NAME = "RepoProvider"; - private static final String PATH_ALL_EXCEPT_SWAP = "allExceptSwap"; - - private static final int CODE_ALL_EXCEPT_SWAP = CODE_SINGLE + 1; - - private static final UriMatcher MATCHER = new UriMatcher(-1); - - static { - MATCHER.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST); - MATCHER.addURI(AUTHORITY + "." + PROVIDER_NAME, PATH_ALL_EXCEPT_SWAP, CODE_ALL_EXCEPT_SWAP); - MATCHER.addURI(AUTHORITY + "." + PROVIDER_NAME, "#", CODE_SINGLE); - } - - public static String getAuthority() { - return AUTHORITY + "." + PROVIDER_NAME; - } - - public static Uri getContentUri() { - return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME); - } - - public static Uri getContentUri(long repoId) { - return ContentUris.withAppendedId(getContentUri(), repoId); - } - - public static Uri allExceptSwapUri() { - return getContentUri().buildUpon() - .appendPath(PATH_ALL_EXCEPT_SWAP) - .build(); - } - - @Override - protected String getTableName() { - return RepoTable.NAME; - } - - @Override - protected String getProviderName() { - return "RepoProvider"; - } - - @Override - protected UriMatcher getMatcher() { - return MATCHER; - } - - @Override - public Cursor query(@NonNull Uri uri, String[] projection, - String selection, String[] selectionArgs, String sortOrder) { - - if (TextUtils.isEmpty(sortOrder)) { - sortOrder = Cols.PRIORITY + " ASC"; - } - - switch (MATCHER.match(uri)) { - case CODE_LIST: - // Do nothing (don't restrict query) - break; - - case CODE_SINGLE: - selection = (selection == null ? "" : selection + " AND ") + - Cols._ID + " = " + uri.getLastPathSegment(); - break; - - case CODE_ALL_EXCEPT_SWAP: - selection = "COALESCE(" + Cols.IS_SWAP + ", 0) = 0 "; - break; - - default: - Log.e(TAG, "Invalid URI for repo content provider: " + uri); - throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri); - } - - Cursor cursor = db().query(getTableName(), projection, - selection, selectionArgs, null, null, sortOrder); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - return cursor; - } - - @Override - public Uri insert(@NonNull Uri uri, ContentValues values) { - - // Don't let people specify arbitrary priorities. Instead, we are responsible - // for making sure that newly created repositories by default have the highest priority. - values.put(Cols.PRIORITY, getMaxPriority() + 1); - - if (!values.containsKey(Cols.ADDRESS)) { - throw new UnsupportedOperationException("Cannot add repo without an address."); - } - - // The following fields have NOT NULL constraints in the DB, so need - // to be present. - - if (!values.containsKey(Cols.IN_USE)) { - values.put(Cols.IN_USE, 1); - } - - if (!values.containsKey(Cols.MAX_AGE)) { - values.put(Cols.MAX_AGE, 0); - } - - if (!values.containsKey(Cols.VERSION)) { - values.put(Cols.VERSION, 0); - } - - if (!values.containsKey(Cols.NAME) || values.get(Cols.NAME) == null) { - final String address = values.getAsString(Cols.ADDRESS); - values.put(Cols.NAME, Repo.addressToName(address)); - } - - long id = db().insertOrThrow(getTableName(), null, values); - Utils.debugLog(TAG, "Inserted repo. Notifying provider change: '" + uri + "'."); - getContext().getContentResolver().notifyChange(uri, null); - return getContentUri(id); - } - - private int getMaxPriority() { - Cursor cursor = db().query(RepoTable.NAME, new String[]{"MAX(" + Cols.PRIORITY + ")"}, - "COALESCE(" + Cols.IS_SWAP + ", 0) = 0", null, null, null, null); - cursor.moveToFirst(); - int max = cursor.getInt(0); - cursor.close(); - return max; - } - - @Override - public int delete(@NonNull Uri uri, String where, String[] whereArgs) { - - QuerySelection selection = new QuerySelection(where, whereArgs); - switch (MATCHER.match(uri)) { - case CODE_LIST: - // Don't support deleting of multiple repos. - return 0; - - case CODE_SINGLE: - selection = selection.add(Cols._ID + " = ?", new String[]{uri.getLastPathSegment()}); - break; - - default: - Log.e(TAG, "Invalid URI for repo content provider: " + uri); - throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri); - } - - int rowsAffected = db().delete(getTableName(), selection.getSelection(), selection.getArgs()); - Utils.debugLog(TAG, "Deleted repo. Notifying provider change: '" + uri + "'."); - getContext().getContentResolver().notifyChange(uri, null); - return rowsAffected; - } - - @Override - public int update(@NonNull 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.getColumnIndexOrThrow(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; - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/RepoPushRequest.java b/app/src/main/java/org/fdroid/fdroid/data/RepoPushRequest.java deleted file mode 100644 index 246cd45d4..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/RepoPushRequest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2016 Blue Jay Wireless - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, - * MA 02110-1301, USA. - */ - -package org.fdroid.fdroid.data; - -import org.fdroid.fdroid.Utils; - -import java.util.Arrays; -import java.util.List; - -import androidx.annotation.Nullable; - -/** - * Represents action requests embedded in the index XML received from a repo. - * When {@link #versionCode} is {@code null}, that means that the - * {@code versionCode} was not specified by the server, and F-Droid should - * install the best available version. - */ -public class RepoPushRequest { - public static final String TAG = "RepoPushRequest"; - - public static final String INSTALL = "install"; - public static final String UNINSTALL = "uninstall"; - public static final List VALID_REQUESTS = Arrays.asList(INSTALL, UNINSTALL); - - public final String request; - public final String packageName; - @Nullable - public final Integer versionCode; - - /** - * Create a new instance. {@code request} is validated against the list of - * valid install requests. {@code packageName} has a safety validation to - * make sure that only valid Android/Java Package Name characters are included. - */ - public RepoPushRequest(String request, String packageName, @Nullable String versionCode) { - if (VALID_REQUESTS.contains(request)) { - this.request = request; - } else { - this.request = null; - } - - if (Utils.isSafePackageName(packageName)) { - this.packageName = packageName; - } else { - this.packageName = null; - } - - Integer i; - try { - i = Integer.parseInt(versionCode); - } catch (NumberFormatException e) { - i = null; - } - this.versionCode = i; - } - - @Override - public String toString() { - return request + " " + packageName + " " + versionCode; - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/RepoXMLHandler.java b/app/src/main/java/org/fdroid/fdroid/data/RepoXMLHandler.java deleted file mode 100644 index c39cf2f39..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/RepoXMLHandler.java +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com - * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -package org.fdroid.fdroid.data; - -import android.Manifest; -import android.os.Build; - -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Schema.ApkTable; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.regex.Pattern; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Parses the index.xml into Java data structures. - */ -public class RepoXMLHandler extends DefaultHandler { - - // The repo we're processing. - private final Repo repo; - - private List apksList = new ArrayList<>(); - - private App curapp; - private Apk curapk; - - private String currentApkHashType; - - // After processing the XML, these will be -1 if the index didn't specify - // them - otherwise it will be the value specified. - private int repoMaxAge = -1; - private int repoVersion; - private long repoTimestamp; - private String repoDescription; - private String repoName; - private String repoIcon; - private final ArrayList repoMirrors = new ArrayList<>(); - - /** - * Set of requested permissions per package/APK - */ - private final HashSet requestedPermissionsSet = new HashSet<>(); - - /** - * the X.509 signing certificate stored in the header of index.xml - */ - private String repoSigningCert; - - private final StringBuilder curchars = new StringBuilder(); - - public interface IndexReceiver { - void receiveRepo(String name, String description, String signingCert, int maxage, int version, - long timestamp, String icon, String[] mirrors); - - void receiveApp(App app, List packages); - - void receiveRepoPushRequest(RepoPushRequest repoPushRequest); - } - - private final IndexReceiver receiver; - - public RepoXMLHandler(Repo repo, @NonNull IndexReceiver receiver) { - this.repo = repo; - this.receiver = receiver; - } - - @Override - public void characters(char[] ch, int start, int length) { - curchars.append(ch, start, length); - } - - @Override - public void endElement(String uri, String localName, String qName) - throws SAXException { - - if ("application".equals(localName) && curapp != null) { - onApplicationParsed(); - } else if ("package".equals(localName) && curapk != null && curapp != null) { - if (Build.VERSION.SDK_INT >= 16 && - requestedPermissionsSet.contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - requestedPermissionsSet.add(Manifest.permission.READ_EXTERNAL_STORAGE); - } - if (Build.VERSION.SDK_INT >= 29) { - if (requestedPermissionsSet.contains(Manifest.permission.ACCESS_FINE_LOCATION)) { - requestedPermissionsSet.add(Manifest.permission.ACCESS_COARSE_LOCATION); - } - if (curapk.targetSdkVersion >= 29) { - // Do nothing. The targetSdk for the below split-permissions is set to 29, - // so we don't make any changes for apps targetting 29 or above - } else { - // TODO: Change the strings below to Manifest.permission once we target SDK 29. - if (requestedPermissionsSet.contains(Manifest.permission.ACCESS_FINE_LOCATION)) { - requestedPermissionsSet.add("android.permission.ACCESS_BACKGROUND_LOCATION"); - } - if (requestedPermissionsSet.contains(Manifest.permission.ACCESS_COARSE_LOCATION)) { - requestedPermissionsSet.add("android.permission.ACCESS_BACKGROUND_LOCATION"); - } - if (requestedPermissionsSet.contains(Manifest.permission.READ_EXTERNAL_STORAGE)) { - requestedPermissionsSet.add("android.permission.ACCESS_MEDIA_LOCATION"); - } - } - } - if (Build.VERSION.SDK_INT >= 31) { - if (curapk.targetSdkVersion >= 31) { - // Do nothing. The targetSdk for the below split-permissions is set to 31, - // so we don't make any changes for apps targetting 31 or above - } else { - // TODO: Change the strings below to Manifest.permission once we target SDK 31. - if (requestedPermissionsSet.contains(Manifest.permission.BLUETOOTH) || - requestedPermissionsSet.contains(Manifest.permission.BLUETOOTH_ADMIN)) { - requestedPermissionsSet.add("android.permission.BLUETOOTH_SCAN"); - requestedPermissionsSet.add("android.permission.BLUETOOTH_CONNECT"); - requestedPermissionsSet.add("android.permission.BLUETOOTH_ADVERTISE"); - } - } - } - if (Build.VERSION.SDK_INT >= 33) { - if (curapk.targetSdkVersion >= 33) { - // Do nothing. The targetSdk for the below split-permissions is set to 33, - // so we don't make any changes for apps targetting 33 or above - } else { - // TODO: Change the strings below to Manifest.permission once we target SDK 33. - if (requestedPermissionsSet.contains(Manifest.permission.BODY_SENSORS)) { - requestedPermissionsSet.add("android.permission.BODY_SENSORS_BACKGROUND"); - } - if (requestedPermissionsSet.contains(Manifest.permission.READ_EXTERNAL_STORAGE) || - requestedPermissionsSet.contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - requestedPermissionsSet.add("android.permission.READ_MEDIA_AUDIO"); - requestedPermissionsSet.add("android.permission.READ_MEDIA_VIDEO"); - requestedPermissionsSet.add("android.permission.READ_MEDIA_IMAGES"); - } - } - } - int size = requestedPermissionsSet.size(); - curapk.requestedPermissions = requestedPermissionsSet.toArray(new String[size]); - requestedPermissionsSet.clear(); - apksList.add(curapk); - curapk = null; - } else if ("repo".equals(localName)) { - onRepoParsed(); - } else if (curchars.length() == 0) { - // All options below require non-empty content - return; - } - final String str = curchars.toString().trim(); - if (curapk != null) { - switch (localName) { - case ApkTable.Cols.VERSION_NAME: - curapk.versionName = str; - break; - case "versioncode": // ApkTable.Cols.VERSION_CODE - curapk.versionCode = Utils.parseInt(str, -1); - break; - case ApkTable.Cols.SIZE: - curapk.size = Utils.parseInt(str, 0); - break; - case ApkTable.Cols.HASH: - if (currentApkHashType == null || "md5".equals(currentApkHashType)) { - if (curapk.hash == null) { - curapk.hash = str; - curapk.hashType = "sha256"; - } - } else if ("sha256".equals(currentApkHashType)) { - curapk.hash = str; - curapk.hashType = "sha256"; - } - break; - case ApkTable.Cols.SIGNATURE: - curapk.sig = str; - // the first APK in the list provides the preferred signature - if (curapp.preferredSigner == null) { - curapp.preferredSigner = str; - } - break; - case ApkTable.Cols.SOURCE_NAME: - curapk.srcname = str; - break; - case "apkname": // ApkTable.Cols.NAME - curapk.apkName = str; - break; - case "sdkver": // ApkTable.Cols.MIN_SDK_VERSION - curapk.minSdkVersion = Utils.parseInt(str, Apk.SDK_VERSION_MIN_VALUE); - break; - case ApkTable.Cols.TARGET_SDK_VERSION: - curapk.targetSdkVersion = Utils.parseInt(str, Apk.SDK_VERSION_MIN_VALUE); - break; - case "maxsdkver": // ApkTable.Cols.MAX_SDK_VERSION - curapk.maxSdkVersion = Utils.parseInt(str, Apk.SDK_VERSION_MAX_VALUE); - if (curapk.maxSdkVersion == 0) { - // before fc0df0dcf4dd0d5f13de82d7cd9254b2b48cb62d, this could be 0 - curapk.maxSdkVersion = Apk.SDK_VERSION_MAX_VALUE; - } - break; - case ApkTable.Cols.OBB_MAIN_FILE: - curapk.obbMainFile = str; - break; - case ApkTable.Cols.OBB_MAIN_FILE_SHA256: - curapk.obbMainFileSha256 = str; - break; - case ApkTable.Cols.OBB_PATCH_FILE: - curapk.obbPatchFile = str; - break; - case ApkTable.Cols.OBB_PATCH_FILE_SHA256: - curapk.obbPatchFileSha256 = str; - break; - case ApkTable.Cols.ADDED_DATE: - curapk.added = Utils.parseDate(str, null); - break; - case "permissions": // together with "; - break; - case "desc": - // New-style description. - curapp.description = App.formatDescription(str); - break; - case "summary": - curapp.summary = str; - break; - case "license": - curapp.license = str; - break; - case "author": - curapp.authorName = str; - break; - case "email": - curapp.authorEmail = str; - break; - case "source": - curapp.sourceCode = str; - break; - case "changelog": - curapp.changelog = str; - break; - case "donate": - curapp.donate = str; - break; - case "bitcoin": - curapp.bitcoin = str; - break; - case "litecoin": - curapp.litecoin = str; - break; - case "flattr": - curapp.flattrID = str; - break; - case "liberapay": - curapp.liberapay = str; - break; - case "web": - curapp.webSite = str; - break; - case "tracker": - curapp.issueTracker = str; - break; - case "added": - curapp.added = Utils.parseDate(str, null); - break; - case "lastupdated": - curapp.lastUpdated = Utils.parseDate(str, null); - break; - case "marketversion": - curapp.suggestedVersionName = str; - break; - case "marketvercode": - curapp.suggestedVersionCode = Utils.parseInt(str, -1); - break; - case "categories": - curapp.categories = Utils.parseCommaSeparatedString(str); - break; - case "antifeatures": - curapp.antiFeatures = Utils.parseCommaSeparatedString(str); - break; - case "requirements": - curapp.requirements = Utils.parseCommaSeparatedString(str); - break; - } - } else if ("description".equals(localName)) { - repoDescription = cleanWhiteSpace(str); - } else if ("mirror".equals(localName)) { - repoMirrors.add(str); - } - } - - private static final Pattern OLD_FDROID_PERMISSION = Pattern.compile("[A-Z_]+"); - - /** - * It appears that the default Android permissions in android.Manifest.permissions - * are prefixed with "android.permission." and then the constant name. - * FDroid just includes the constant name in the apk list, so we prefix it - * with "android.permission." - * - * @see - * More info into index - size, permissions, features, sdk version - */ - public static String fdroidToAndroidPermission(String permission) { - if (OLD_FDROID_PERMISSION.matcher(permission).matches()) { - return "android.permission." + permission; - } - - return permission; - } - - private void addRequestedPermission(String permission) { - requestedPermissionsSet.add(permission); - } - - private void addCommaSeparatedPermissions(String permissions) { - String[] array = Utils.parseCommaSeparatedString(permissions); - if (array != null) { - for (String permission : array) { - requestedPermissionsSet.add(fdroidToAndroidPermission(permission)); - } - } - } - - private void removeRequestedPermission(String permission) { - requestedPermissionsSet.remove(permission); - } - - private void onApplicationParsed() { - receiver.receiveApp(curapp, apksList); - curapp = null; - apksList = new ArrayList<>(); - // If the app packageName is already present in this apps list, then it - // means the same index file has a duplicate app, which should - // not be allowed. - // However, I'm thinking that it should be undefined behaviour, - // because it is probably a bug in the fdroid server that made it - // happen, and I don't *think* it will crash the client, because - // the first app will insert, the second one will update the newly - // inserted one. - } - - private void onRepoParsed() { - receiver.receiveRepo(repoName, repoDescription, repoSigningCert, repoMaxAge, repoVersion, - repoTimestamp, repoIcon, repoMirrors.toArray(new String[repoMirrors.size()])); - } - - private void onRepoPushRequestParsed(RepoPushRequest repoPushRequest) { - receiver.receiveRepoPushRequest(repoPushRequest); - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) - throws SAXException { - super.startElement(uri, localName, qName, attributes); - - if ("repo".equals(localName)) { - repoSigningCert = attributes.getValue("", "pubkey"); - repoMaxAge = Utils.parseInt(attributes.getValue("", "maxage"), -1); - repoVersion = Utils.parseInt(attributes.getValue("", "version"), -1); - repoName = cleanWhiteSpace(attributes.getValue("", "name")); - repoDescription = cleanWhiteSpace(attributes.getValue("", "description")); - repoTimestamp = parseLong(attributes.getValue("", "timestamp"), 0); - repoIcon = attributes.getValue("", "icon"); - } else if (RepoPushRequest.VALID_REQUESTS.contains(localName)) { - if (repo.pushRequests == Repo.PUSH_REQUEST_ACCEPT_ALWAYS) { - RepoPushRequest r = new RepoPushRequest( - localName, - attributes.getValue("packageName"), - attributes.getValue("versionCode")); - onRepoPushRequestParsed(r); - } - } else if ("application".equals(localName) && curapp == null) { - curapp = new App(); - curapp.repoId = repo.getId(); - try { - curapp.setPackageName(attributes.getValue("", "id")); - } catch (IllegalArgumentException e) { - throw new SAXException(e); - } - - // To appease the NON NULL constraint in the DB. Usually there is a description, and it - // is quite difficult to get an app to _not_ have a description when using fdroidserver. - // However, it shouldn't crash the client when this happens. - curapp.description = ""; - } else if ("package".equals(localName) && curapp != null && curapk == null) { - curapk = new Apk(); - curapk.packageName = curapp.packageName; - curapk.repoId = repo.getId(); - currentApkHashType = null; - - } else if ("hash".equals(localName) && curapk != null) { - currentApkHashType = attributes.getValue("", "type"); - } else if ("uses-permission".equals(localName) && curapk != null) { - String maxSdkVersion = attributes.getValue("maxSdkVersion"); - if (maxSdkVersion == null || Build.VERSION.SDK_INT <= Integer.valueOf(maxSdkVersion)) { - addRequestedPermission(attributes.getValue("name")); - } else { - removeRequestedPermission(attributes.getValue("name")); - } - } else if ("uses-permission-sdk-23".equals(localName) && curapk != null) { - String maxSdkVersion = attributes.getValue("maxSdkVersion"); - if (Build.VERSION.SDK_INT >= 23 && - (maxSdkVersion == null || Build.VERSION.SDK_INT <= Integer.valueOf(maxSdkVersion))) { - addRequestedPermission(attributes.getValue("name")); - } else { - removeRequestedPermission(attributes.getValue("name")); - } - } - curchars.setLength(0); - } - - private static String cleanWhiteSpace(@Nullable String str) { - return str == null ? null : str.replaceAll("\\s", " "); - } - - private static long parseLong(String str, long fallback) { - if (str == null || str.length() == 0) { - return fallback; - } - long result; - try { - result = Long.parseLong(str); - } catch (NumberFormatException e) { - result = fallback; - } - return result; - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/Schema.java b/app/src/main/java/org/fdroid/fdroid/data/Schema.java deleted file mode 100644 index 9deef9dd0..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java +++ /dev/null @@ -1,422 +0,0 @@ -package org.fdroid.fdroid.data; - -import android.provider.BaseColumns; - -/** - * The authoritative reference to each table/column which should exist in the database. - * Constants from this interface should be used in preference to string literals when referring to - * the tables/columns in the database. - */ -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"; - - 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"; - - interface Cols extends BaseColumns { - // Join onto app table via packageName, not appId. The corresponding app row could - // be deleted and then re-added in the future with the same metadata but a different - // rowid. This should not cause us to forget the preferences specified by a user. - String PACKAGE_NAME = "packageName"; - - String IGNORE_ALL_UPDATES = "ignoreAllUpdates"; - String IGNORE_THIS_UPDATE = "ignoreThisUpdate"; - String IGNORE_VULNERABILITIES = "ignoreVulnerabilities"; - - String[] ALL = {PACKAGE_NAME, IGNORE_ALL_UPDATES, IGNORE_THIS_UPDATE, IGNORE_VULNERABILITIES}; - } - } - - 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 { - String ROW_ID = "rowid"; - - /** - * 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 = {ROW_ID, APP_METADATA_ID, CATEGORY_ID}; - } - } - - interface AntiFeatureTable { - - String NAME = "fdroid_antiFeature"; - - interface Cols { - String ROW_ID = "rowid"; - String NAME = "name"; - - String[] ALL = {ROW_ID, NAME}; - } - } - - /** - * An entry in this table signifies that an apk has a particular anti feature. - * - * @see AntiFeatureTable - * @see ApkTable - */ - interface ApkAntiFeatureJoinTable { - - String NAME = "fdroid_apkAntiFeatureJoin"; - - interface Cols { - /** - * Foreign key to {@link ApkTable}. - * - * @see ApkTable - */ - String APK_ID = "apkId"; - - /** - * Foreign key to {@link AntiFeatureTable}. - * - * @see AntiFeatureTable - */ - String ANTI_FEATURE_ID = "antiFeatureId"; - - /** - * @see AppMetadataTable.Cols#ALL_COLS - */ - String[] ALL_COLS = {APK_ID, ANTI_FEATURE_ID}; - } - } - - interface AppMetadataTable { - - String NAME = "fdroid_app"; - - interface Cols { - /** - * Same as the primary key {@link Cols#ROW_ID}, except aliased as "_id" instead - * of "rowid". Required for {@link android.content.CursorLoader}s. - */ - String _ID = "rowid as _id"; - String ROW_ID = "rowid"; - String _COUNT = "_count"; - String IS_COMPATIBLE = "compatible"; - String PACKAGE_ID = "packageId"; - String REPO_ID = "repoId"; - String NAME = "name"; - String SUMMARY = "summary"; - String ICON = "icon"; - String DESCRIPTION = "description"; - String WHATSNEW = "whatsNew"; - String LICENSE = "license"; - String AUTHOR_NAME = "author"; - String AUTHOR_EMAIL = "email"; - String WEBSITE = "webURL"; - String ISSUE_TRACKER = "trackerURL"; - String SOURCE_CODE = "sourceURL"; - String TRANSLATION = "translation"; - String VIDEO = "video"; - String CHANGELOG = "changelogURL"; - String DONATE = "donateURL"; - String BITCOIN = "bitcoinAddr"; - String LITECOIN = "litecoinAddr"; - String FLATTR_ID = "flattrID"; - String LIBERAPAY = "liberapayID"; - String OPEN_COLLECTIVE = "openCollective"; - String PREFERRED_SIGNER = "preferredSigner"; - String AUTO_INSTALL_VERSION_CODE = "suggestedVercode"; // name mismatch from issue #1063 - String SUGGESTED_VERSION_NAME = "upstreamVersion"; // name mismatch from issue #1063 - String SUGGESTED_VERSION_CODE = "upstreamVercode"; // name mismatch from issue #1063 - String ADDED = "added"; - String LAST_UPDATED = "lastUpdated"; - String ANTI_FEATURES = "antiFeatures"; - String REQUIREMENTS = "requirements"; - String ICON_URL = "iconUrl"; - String FEATURE_GRAPHIC = "featureGraphic"; - String PROMO_GRAPHIC = "promoGraphic"; - String TV_BANNER = "tvBanner"; - String PHONE_SCREENSHOTS = "phoneScreenshots"; - String SEVEN_INCH_SCREENSHOTS = "sevenInchScreenshots"; - String TEN_INCH_SCREENSHOTS = "tenInchScreenshots"; - String TV_SCREENSHOTS = "tvScreenshots"; - String WEAR_SCREENSHOTS = "wearScreenshots"; - String IS_APK = "isApk"; - - /** - * Has this {@code App} been localized into one of the user's current locales. - * - * @see App#setIsLocalized(java.util.Set) - * @see org.fdroid.fdroid.views.main.WhatsNewViewBinder#onCreateLoader(int, android.os.Bundle) - */ - String IS_LOCALIZED = "isLocalized"; - - interface AutoInstallApk { - String VERSION_NAME = "suggestedApkVersion"; - } - - interface InstalledApp { - String VERSION_CODE = "installedVersionCode"; - String VERSION_NAME = "installedVersionName"; - String SIGNATURE = "installedSig"; - } - - interface Package { - 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} - * or which are from other related tables (e.g. {@link AutoInstallApk#VERSION_NAME}). - */ - String[] ALL_COLS = { - ROW_ID, PACKAGE_ID, REPO_ID, IS_COMPATIBLE, NAME, SUMMARY, ICON, DESCRIPTION, - WHATSNEW, LICENSE, AUTHOR_NAME, AUTHOR_EMAIL, WEBSITE, ISSUE_TRACKER, SOURCE_CODE, - TRANSLATION, VIDEO, CHANGELOG, DONATE, BITCOIN, LITECOIN, FLATTR_ID, LIBERAPAY, - OPEN_COLLECTIVE, SUGGESTED_VERSION_NAME, SUGGESTED_VERSION_CODE, ADDED, LAST_UPDATED, - ANTI_FEATURES, REQUIREMENTS, ICON_URL, - FEATURE_GRAPHIC, PROMO_GRAPHIC, TV_BANNER, PHONE_SCREENSHOTS, - SEVEN_INCH_SCREENSHOTS, TEN_INCH_SCREENSHOTS, TV_SCREENSHOTS, WEAR_SCREENSHOTS, - PREFERRED_SIGNER, AUTO_INSTALL_VERSION_CODE, IS_APK, IS_LOCALIZED, - }; - - /** - * Superset of {@link Cols#ALL_COLS} including fields from other tables and also an alias - * to satisfy the Android requirement for an "_ID" field. - * - * @see Cols#ALL_COLS - */ - String[] ALL = { - _ID, ROW_ID, REPO_ID, IS_COMPATIBLE, NAME, SUMMARY, ICON, DESCRIPTION, - WHATSNEW, LICENSE, AUTHOR_NAME, AUTHOR_EMAIL, WEBSITE, ISSUE_TRACKER, SOURCE_CODE, - TRANSLATION, VIDEO, CHANGELOG, DONATE, BITCOIN, LITECOIN, FLATTR_ID, LIBERAPAY, - OPEN_COLLECTIVE, SUGGESTED_VERSION_NAME, SUGGESTED_VERSION_CODE, ADDED, LAST_UPDATED, - ANTI_FEATURES, REQUIREMENTS, ICON_URL, - FEATURE_GRAPHIC, PROMO_GRAPHIC, TV_BANNER, PHONE_SCREENSHOTS, - SEVEN_INCH_SCREENSHOTS, TEN_INCH_SCREENSHOTS, TV_SCREENSHOTS, WEAR_SCREENSHOTS, - PREFERRED_SIGNER, AUTO_INSTALL_VERSION_CODE, IS_APK, IS_LOCALIZED, AutoInstallApk.VERSION_NAME, - InstalledApp.VERSION_CODE, InstalledApp.VERSION_NAME, - InstalledApp.SIGNATURE, Package.PACKAGE_NAME, - }; - } - } - - /** - * This table stores details of all the application versions we - * know about. Each relates directly back to an entry in TABLE_APP. - * This information is retrieved from the repositories. - */ - interface ApkTable { - - String NAME = "fdroid_apk"; - - interface Cols extends BaseColumns { - String _COUNT_DISTINCT = "countDistinct"; - - /** - * Foreign key to the {@link AppMetadataTable}. - */ - String APP_ID = "appId"; - String ROW_ID = "rowid"; - String VERSION_NAME = "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 TARGET_SDK_VERSION = "targetSdkVersion"; - String MAX_SDK_VERSION = "maxSdkVersion"; - String OBB_MAIN_FILE = "obbMainFile"; - String OBB_MAIN_FILE_SHA256 = "obbMainFileSha256"; - String OBB_PATCH_FILE = "obbPatchFile"; - String OBB_PATCH_FILE_SHA256 = "obbPatchFileSha256"; - String REQUESTED_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"; - - interface Repo { - String VERSION = "repoVersion"; - String ADDRESS = "repoAddress"; - } - - interface Package { - String PACKAGE_NAME = "package_packageName"; - } - - interface AntiFeatures { - String ANTI_FEATURES = "antiFeatures_commaSeparated"; - } - - /** - * @see AppMetadataTable.Cols#ALL_COLS - */ - String[] ALL_COLS = { - APP_ID, VERSION_NAME, REPO_ID, HASH, VERSION_CODE, NAME, - SIZE, SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, TARGET_SDK_VERSION, MAX_SDK_VERSION, - OBB_MAIN_FILE, OBB_MAIN_FILE_SHA256, OBB_PATCH_FILE, OBB_PATCH_FILE_SHA256, - REQUESTED_PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE, - IS_COMPATIBLE, INCOMPATIBLE_REASONS, - }; - - /** - * @see AppMetadataTable.Cols#ALL - */ - String[] ALL = { - _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, - OBB_MAIN_FILE, OBB_MAIN_FILE_SHA256, OBB_PATCH_FILE, OBB_PATCH_FILE_SHA256, - REQUESTED_PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE, - IS_COMPATIBLE, Repo.VERSION, Repo.ADDRESS, INCOMPATIBLE_REASONS, - AntiFeatures.ANTI_FEATURES, - }; - } - } - - interface RepoTable { - - String NAME = "fdroid_repo"; - - interface Cols extends BaseColumns { - - String ADDRESS = "address"; - String NAME = "name"; - String DESCRIPTION = "description"; - String IN_USE = "inuse"; - String PRIORITY = "priority"; - String SIGNING_CERT = "pubkey"; - String FINGERPRINT = "fingerprint"; - String MAX_AGE = "maxage"; - String LAST_ETAG = "lastetag"; - String LAST_UPDATED = "lastUpdated"; - String VERSION = "version"; - String IS_SWAP = "isSwap"; - String USERNAME = "username"; - String PASSWORD = "password"; - String TIMESTAMP = "timestamp"; - String ICON = "icon"; - String MIRRORS = "mirrors"; - String USER_MIRRORS = "userMirrors"; - String DISABLED_MIRRORS = "disabledMirrors"; - String PUSH_REQUESTS = "pushRequests"; - - String[] ALL = { - _ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, SIGNING_CERT, - FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION, IS_SWAP, - USERNAME, PASSWORD, TIMESTAMP, ICON, MIRRORS, USER_MIRRORS, DISABLED_MIRRORS, PUSH_REQUESTS, - }; - } - } - - interface InstalledAppTable { - - String NAME = "fdroid_installedApp"; - - interface Cols { - String _ID = "rowid as _id"; // Required for CursorLoaders - String PACKAGE_ID = "packageId"; - String VERSION_CODE = "versionCode"; - String VERSION_NAME = "versionName"; - String APPLICATION_LABEL = "applicationLabel"; - String SIGNATURE = "sig"; - String LAST_UPDATE_TIME = "lastUpdateTime"; - String HASH_TYPE = "hashType"; - String HASH = "hash"; - - interface Package { - String NAME = "packageName"; - } - - String[] ALL = { - _ID, PACKAGE_ID, Package.NAME, VERSION_CODE, VERSION_NAME, APPLICATION_LABEL, - SIGNATURE, LAST_UPDATE_TIME, HASH_TYPE, HASH, - }; - } - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/ValueObject.java b/app/src/main/java/org/fdroid/fdroid/data/ValueObject.java deleted file mode 100644 index dca9e0a40..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/ValueObject.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.fdroid.fdroid.data; - -import android.database.Cursor; - -class ValueObject { - - void checkCursorPosition(Cursor cursor) throws IllegalArgumentException { - if (cursor.getPosition() == -1) { - throw new IllegalArgumentException( - "Cursor position is -1. " + - "Did you forget to moveToFirst() or move() before passing to the value object?"); - } - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java index 24fff30b4..ae4218a5f 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java @@ -27,8 +27,6 @@ import android.net.Uri; import org.apache.commons.io.FileUtils; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.SanitizedFile; import java.io.File; @@ -57,11 +55,9 @@ public class ApkCache { * out during the install process. Most likely, apkFile was just downloaded, * so it should still be in the RAM disk cache. */ - public static SanitizedFile copyApkFromCacheToFiles(Context context, File apkFile, Apk expectedApk) + static SanitizedFile copyApkFromCacheToFiles(Context context, File apkFile, Apk expectedApk) throws IOException { - App app = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), - expectedApk.packageName); - String name = app == null ? expectedApk.packageName : app.name; + String name = expectedApk.packageName; String apkFileName = name + "-" + expectedApk.versionName + ".apk"; return copyApkToFiles(context, apkFile, apkFileName, true, expectedApk.hash, expectedApk.hashType); } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java index 2343ba8bb..6cd6888f3 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkVerifier.java @@ -57,7 +57,7 @@ class ApkVerifier { this.pm = context.getPackageManager(); } - public void verifyApk() throws ApkVerificationException, ApkPermissionUnequalException { + void verifyApk() throws ApkVerificationException, ApkPermissionUnequalException { Utils.debugLog(TAG, "localApkUri.getPath: " + localApkUri.getPath()); // parse downloaded apk file locally @@ -105,7 +105,7 @@ class ApkVerifier { * data format is {@link String} arrays but they are in effect sets. This is the * same data format as {@link android.content.pm.PackageInfo#requestedPermissions} */ - public static boolean requestedPermissionsEqual(@Nullable String[] expected, @Nullable String[] actual) { + static boolean requestedPermissionsEqual(@Nullable String[] expected, @Nullable String[] actual) { Utils.debugLog(TAG, "Checking permissions"); Utils.debugLog(TAG, "Expected:\n " + (expected == null ? "None" : TextUtils.join("\n ", expected))); Utils.debugLog(TAG, "Actual:\n " + (actual == null ? "None" : TextUtils.join("\n ", actual))); @@ -124,26 +124,18 @@ class ApkVerifier { return expectedSet.equals(actualSet); } - public static class ApkVerificationException extends Exception { + static class ApkVerificationException extends Exception { ApkVerificationException(String message) { super(message); } - - ApkVerificationException(Throwable cause) { - super(cause); - } } - public static class ApkPermissionUnequalException extends Exception { + static class ApkPermissionUnequalException extends Exception { ApkPermissionUnequalException(String message) { super(message); } - - ApkPermissionUnequalException(Throwable cause) { - super(cause); - } } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java index b183e5b26..30857c0e5 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java @@ -44,7 +44,6 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.DBHelper; -import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; import org.fdroid.fdroid.views.main.MainActivity; import androidx.annotation.NonNull; @@ -87,8 +86,8 @@ public class AppListActivity extends AppCompatActivity implements CategoryTextWa private LiveData> itemsLiveData; private interface SortClause { - String WORDS = Cols.NAME; - String LAST_UPDATED = Cols.LAST_UPDATED; + String WORDS = "Name"; + String LAST_UPDATED = "LastUpdated"; } @Override diff --git a/app/src/test/java/org/fdroid/fdroid/UtilsTest.java b/app/src/test/java/org/fdroid/fdroid/UtilsTest.java index bf53563fb..56c3d6072 100644 --- a/app/src/test/java/org/fdroid/fdroid/UtilsTest.java +++ b/app/src/test/java/org/fdroid/fdroid/UtilsTest.java @@ -4,8 +4,6 @@ package org.fdroid.fdroid; import android.content.Context; import android.database.Cursor; -import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter; import org.junit.After; import org.junit.Before; @@ -18,7 +16,6 @@ import java.io.IOException; import java.util.Date; import java.util.TimeZone; -import androidx.loader.content.CursorLoader; import androidx.test.core.app.ApplicationProvider; import static org.junit.Assert.assertEquals; @@ -287,27 +284,4 @@ public class UtilsTest { } } } - - @Test - public void testGetAntifeatureSQLFilterWithNone() { - Context context = ApplicationProvider.getApplicationContext(); - Preferences.setupForTests(context); - assertEquals( - "fdroid_app.antiFeatures IS NULL OR (fdroid_app.antiFeatures NOT LIKE '%_anti_others_%')", - Utils.getAntifeatureSQLFilter(context) - ); - } - - @Test - public void testGetAntifeatureSQLFilter() { - CursorLoader cursorLoader = new CursorLoader( - context, - AppProvider.getLatestTabUri(), - Schema.AppMetadataTable.Cols.ALL, - Utils.getAntifeatureSQLFilter(context), - null, - null); - cursor = cursorLoader.loadInBackground(); - assertNotNull(cursor); - } } diff --git a/app/src/test/java/org/fdroid/fdroid/data/DBHelperTest.java b/app/src/test/java/org/fdroid/fdroid/data/DBHelperTest.java index 3466af1fa..8dd1bd818 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/DBHelperTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/DBHelperTest.java @@ -330,11 +330,8 @@ public class DBHelperTest { assertEquals(shouldBeRepos.get(i), initialRepos.get(i)); } } finally { - for (Repo repo : RepoProvider.Helper.all(context, new String[]{Schema.RepoTable.Cols._ID})) { - RepoProvider.Helper.remove(context, repo.getId()); - } + //noinspection ResultOfMethodCallIgnored additionalReposXmlFile.delete(); - DBHelper.clearDbHelperSingleton(); } } } diff --git a/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java b/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java index d4e8c36a0..d76dc72fe 100644 --- a/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java +++ b/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java @@ -6,7 +6,7 @@ import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @Implements(App.class) -public class ShadowApp extends ValueObject { +public class ShadowApp { @Implementation protected static int[] getMinTargetMaxSdkVersions(Context context, String packageName) { diff --git a/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java b/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java index dd05a478e..8f8d90fa6 100644 --- a/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java +++ b/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java @@ -1,7 +1,6 @@ package org.fdroid.fdroid.updater; import android.content.ContentResolver; -import android.content.ContentValues; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; @@ -16,8 +15,6 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.data.ShadowApp; import org.fdroid.fdroid.nearby.LocalHTTPD; import org.fdroid.fdroid.nearby.LocalRepoKeyStore; @@ -77,11 +74,6 @@ public class SwapRepoTest { Preferences.setupForTests(context); } - @After - public final void tearDownBase() { - DBHelper.clearDbHelperSingleton(); - } - /** * @see WifiStateChangeService.WifiInfoThread#run() */ @@ -143,15 +135,15 @@ public class SwapRepoTest { //updater.processDownloadedFile(indexJarFile); boolean foundRepo = false; - for (Repo repoFromDb : RepoProvider.Helper.all(context)) { - if (TextUtils.equals(repo.address, repoFromDb.address)) { - foundRepo = true; - repo = repoFromDb; - } - } + //for (Repo repoFromDb : RepoProvider.Helper.all(context)) { + // if (TextUtils.equals(repo.address, repoFromDb.address)) { + // foundRepo = true; + // repo = repoFromDb; + // } + //} assertTrue(foundRepo); - assertNotEquals(-1, repo.getId()); + //assertNotEquals(-1, repo.getId()); //List apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL); //assertEquals(1, apks.size()); //for (Apk apk : apks) { @@ -171,11 +163,9 @@ public class SwapRepoTest { * that ensures it includes the primary key from the database. */ static Repo createRepo(String name, String uri, Context context, String signingCert) { - ContentValues values = new ContentValues(3); - values.put(Schema.RepoTable.Cols.SIGNING_CERT, signingCert); - values.put(Schema.RepoTable.Cols.ADDRESS, uri); - values.put(Schema.RepoTable.Cols.NAME, name); - RepoProvider.Helper.insert(context, values); - return RepoProvider.Helper.findByAddress(context, uri); + //values.put(Schema.RepoTable.Cols.SIGNING_CERT, signingCert); + //values.put(Schema.RepoTable.Cols.ADDRESS, uri); + //values.put(Schema.RepoTable.Cols.NAME, name); + return new Repo(); } } \ No newline at end of file diff --git a/app/src/testShared/java/org/fdroid/fdroid/mock/MockRepo.java b/app/src/testShared/java/org/fdroid/fdroid/mock/MockRepo.java deleted file mode 100644 index 5926c7b88..000000000 --- a/app/src/testShared/java/org/fdroid/fdroid/mock/MockRepo.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.fdroid.fdroid.mock; - -import org.fdroid.fdroid.data.Repo; - -public class MockRepo extends Repo { - - public MockRepo(long id) { - this.id = id; - } - - public MockRepo(long id, int pushRequests) { - this.id = id; - this.pushRequests = pushRequests; - } - -} diff --git a/app/src/testShared/java/org/fdroid/fdroid/mock/RepoDetails.java b/app/src/testShared/java/org/fdroid/fdroid/mock/RepoDetails.java deleted file mode 100644 index 6614300d1..000000000 --- a/app/src/testShared/java/org/fdroid/fdroid/mock/RepoDetails.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.fdroid.fdroid.mock; - -import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.RepoPushRequest; -import org.fdroid.fdroid.data.RepoXMLHandler; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -import androidx.annotation.NonNull; - -import static org.junit.Assert.fail; - -public class RepoDetails implements RepoXMLHandler.IndexReceiver { - public static final String TAG = "RepoDetails"; - - public String name; - public String description; - public String signingCert; - public int maxAge; - public int version; - public long timestamp; - public String icon; - public String[] mirrors; - - public final List apks = new ArrayList<>(); - public final List apps = new ArrayList<>(); - public final List repoPushRequestList = new ArrayList<>(); - - @Override - public void receiveRepo(String name, String description, String signingCert, int maxage, - int version, long timestamp, String icon, String[] mirrors) { - this.name = name; - this.description = description; - this.signingCert = signingCert; - this.maxAge = maxage; - this.version = version; - this.timestamp = timestamp; - this.icon = icon; - this.mirrors = mirrors; - } - - @Override - public void receiveApp(App app, List packages) { - apks.addAll(packages); - apps.add(app); - } - - @Override - public void receiveRepoPushRequest(RepoPushRequest repoPushRequest) { - repoPushRequestList.add(repoPushRequest); - } - - @NonNull - public static RepoDetails getFromFile(InputStream inputStream, int pushRequests) { - try { - SAXParserFactory factory = SAXParserFactory.newInstance(); - factory.setNamespaceAware(true); - SAXParser parser = factory.newSAXParser(); - XMLReader reader = parser.getXMLReader(); - RepoDetails repoDetails = new RepoDetails(); - MockRepo mockRepo = new MockRepo(100, pushRequests); - RepoXMLHandler handler = new RepoXMLHandler(mockRepo, repoDetails); - reader.setContentHandler(handler); - InputSource is = new InputSource(new BufferedInputStream(inputStream)); - reader.parse(is); - return repoDetails; - } catch (ParserConfigurationException | SAXException | IOException e) { - e.printStackTrace(); - fail(); - - // Satisfies the compiler, but fail() will always throw a runtime exception so we never - // reach this return statement. - return null; - } - } - -} -