[app] remove old ContentProviders from old database implementation

This commit is contained in:
Torsten Grote
2022-06-15 14:57:26 -03:00
committed by Hans-Christoph Steiner
parent a783d3cb94
commit 7a1d288792
36 changed files with 49 additions and 7360 deletions

View File

@@ -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<String> 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<String> 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<String> 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<String> 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<String> 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);
}
}
}

View File

@@ -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;

View File

@@ -431,35 +431,6 @@
android:name=".installer.FileInstallerActivity"
android:theme="@style/AppThemeTransparent" />
<provider
android:name="org.fdroid.fdroid.data.AppProvider"
android:authorities="${applicationId}.data.AppProvider"
android:exported="false" />
<provider
android:name="org.fdroid.fdroid.data.RepoProvider"
android:authorities="${applicationId}.data.RepoProvider"
android:exported="false" />
<!-- Note: AppThemeTransparent, this activity shows dialogs only -->
<provider
android:name="org.fdroid.fdroid.data.ApkProvider"
android:authorities="${applicationId}.data.ApkProvider"
android:exported="false" />
<provider
android:name="org.fdroid.fdroid.data.AppPrefsProvider"
android:authorities="${applicationId}.data.AppPrefsProvider"
android:exported="false" />
<provider
android:name="org.fdroid.fdroid.data.PackageIdProvider"
android:authorities="${applicationId}.data.PackageIdProvider"
android:exported="false" />
<provider
android:name="org.fdroid.fdroid.data.CategoryProvider"
android:authorities="${applicationId}.data.CategoryProvider"
android:exported="false" />
<provider
android:name="org.fdroid.fdroid.installer.ApkFileProvider"
android:authorities="${applicationId}.installer.ApkFileProvider"

View File

@@ -38,8 +38,7 @@ import io.reactivex.rxjava3.disposables.Disposable;
/**
* Manages the state of APKs that are being installed or that have updates available.
* This also ensures the state is saved across F-Droid restarts, and repopulates
* based on {@link org.fdroid.fdroid.data.Schema.InstalledAppTable} data, APKs that
* This also ensures the state is saved across F-Droid restarts, APKs that
* are present in the cache, and the {@code apks-pending-install}
* {@link SharedPreferences} instance.
* <p>

View File

@@ -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()) {

View File

@@ -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<String> unwantedAntifeatures(Context context, Set<String> acceptableAntifeatures) {
List<String> 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<String> 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();
}
}

View File

@@ -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 <a href="https://gitlab.com/fdroid/fdroiddata">fdroiddata</a>
* @see <a href="https://gitlab.com/fdroid/fdroidserver">fdroidserver</a>
*/
public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
public class Apk implements Comparable<Apk>, 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<Apk>, 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<Apk>, 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<Apk>, Parcelable {
}
};
private String[] convertToRequestedPermissions(String permissionsFromDb) {
String[] array = Utils.parseCommaSeparatedString(permissionsFromDb);
if (array != null) {
HashSet<String> 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.
*/

View File

@@ -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<Apk> cursorToList(Cursor cursor) {
int knownApkCount = cursor != null ? cursor.getCount() : 0;
List<Apk> apks = new ArrayList<>(knownApkCount);
if (cursor != null) {
if (knownApkCount > 0) {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
apks.add(new Apk(cursor));
cursor.moveToNext();
}
}
cursor.close();
}
return apks;
}
public static int deleteApksByRepo(Context context, Repo repo) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getRepoUri(repo.getId());
return resolver.delete(uri, null, null);
}
/**
* Find an app which is closest to the version code suggested by the server, with some caveats:
* <ul>
* <li>If installed, limit to apks signed by the same signer as the installed apk.</li>
* <li>Otherwise, limit to apks signed by the "preferred" signer (see {@link App#preferredSigner}).</li>
* </ul>
* 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<Apk> 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<Apk> 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<Apk> findByRepo(Context context, Repo repo, String[] fields) {
ContentResolver resolver = context.getContentResolver();
final Uri uri = getRepoUri(repo.getId());
Cursor cursor = resolver.query(uri, fields, null, null, null);
return cursorToList(cursor);
}
@NonNull
public static List<Apk> 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<Apk> 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<String, String> REPO_FIELDS = new HashMap<>();
private static final Map<String, String> 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<String> pathSegments = uri.getPathSegments();
List<String> 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<String> path = uri.getPathSegments();
String appId = path.get(1);
String versionCode = path.get(2);
final String selection = Cols.APP_ID + " = ? AND " + Cols.VERSION_CODE + " = ? ";
final String[] args = {appId, versionCode};
return new QuerySelection(selection, args);
}
protected QuerySelection queryRepo(long repoId) {
return queryRepo(repoId, true);
}
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<String> 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<String, String> repoField : REPO_FIELDS.entrySet()) {
final String field = repoField.getKey();
if (values.containsKey(field)) {
values.remove(field);
}
}
for (Map.Entry<String, String> 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<String> 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;
}
}

View File

@@ -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 <a href="https://gitlab.com/fdroid/fdroiddata">fdroiddata</a>
* @see <a href="https://gitlab.com/fdroid/fdroidserver">fdroidserver</a>
*/
public class App extends ValueObject implements Comparable<App>, Parcelable {
public class App implements Comparable<App>, Parcelable {
@JsonIgnore
private static final String TAG = "App";
@@ -83,7 +72,7 @@ public class App extends ValueObject implements Comparable<App>, 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<App>, 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<App>, Parcelable {
/**
* List of categories (as defined in the metadata documentation) or null if there aren't any.
* This is only populated when parsing a repository. If you need to know about the categories
* an app is in any other part of F-Droid, use the {@link CategoryProvider}.
* 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<App>, 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<App>, 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<App>, 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.
* <p>
* 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.
* <ol>
* <li>the country variant {@code de-AT} from the user locale list
* <li>only the language {@code de} from the above locale
* <li>next locale in the user's preference list ({@code >= android-24})
* <li>{@code en-US} since its the most common English for software
* <li>the first available {@code en} locale
* </ol>
* <p>
* The system-wide language preference list was added in {@code android-24}.
*
* @see <a href="https://developer.android.com/guide/topics/resources/multilingual-support">Android language and locale resolution overview</a>
*/
@JsonProperty("localized")
void setLocalized(Map<String, Map<String, Object>> localized) { // NOPMD
if (systemLocaleList == null) {
systemLocaleList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration());
}
Set<String> 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<String> 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<String, Map<String, Object>> localized,
Set<String> supportedLocales, @NonNull String key) {
Map<String, Object> 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<String, Map<String, Object>> localized,
Set<String> supportedLocales, @NonNull String key) {
Map<String, Object> 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<String, Map<String, Object>> localized,
Set<String> supportedLocales, @NonNull String key) {
Map<String, Object> localizedLocaleMap = getLocalizedLocaleMap(localized, supportedLocales, key);
if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) {
for (String locale : localizedLocaleMap.keySet()) {
ArrayList<String> entry = (ArrayList<String>) 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<String, Object> getLocalizedLocaleMap(Map<String, Map<String, Object>> localized,
Set<String> 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<String, Object> localeEntry = localized.get(languageTag);
if (localeEntry != null && localeEntry.containsKey(key)) {
Object value = localeEntry.get(key);
if (value != null) {
Map<String, Object> 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<String, Map<String, Object>> localized,
Set<String> supportedLocales, @NonNull String key) {
Set<String> localesToUse = new HashSet<>();
for (String locale : supportedLocales) {
Map<String, Object> 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 <br>}
*/
@@ -957,29 +534,6 @@ public class App extends ValueObject implements Comparable<App>, 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<App>, 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<App>, 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<App>, Parcelable {
return null;
}
@Override
public String toString() {
return toContentValues().toString();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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.
* <p>
* 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<String, Long> 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 + ".");
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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<ContentProviderOperation> 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 + " = ?";
}
}

View File

@@ -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();
}
}

View File

@@ -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)) {

View File

@@ -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;
}
}

View File

@@ -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 + ".");
}
}

View File

@@ -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<String> fields = new ArrayList<>();
private final StringBuilder tables = new StringBuilder(getRequiredTables());
private String selection;
private String[] selectionArgs;
private final List<OrderClause> 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<String> 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();
}
}

View File

@@ -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<String> 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<String> 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);
}
}

View File

@@ -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 <a href="https://gitlab.com/fdroid/fdroiddata">fdroiddata</a>
* @see <a href="https://gitlab.com/fdroid/fdroidserver">fdroidserver</a>
*/
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<String> 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<String> elements = new ArrayList();
List<String> 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<Mirror> 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<String> getMirrorList() {
final HashSet<String> 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);
}
}

View File

@@ -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.
* <p>
* 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:
* <li>Repositories can exist at particular paths on a server (e.g. /fdroid/repo)
* <li>Individual files can exist at a more specific path on the repo (e.g.
* /fdroid/repo/icons/org.fdroid.fdroid.png)</li>
* <p>
* 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<String> 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<Repo> repos = findBy(
context, Cols.ADDRESS, address, projection);
if (repos.isEmpty()) {
return null;
} else {
return repos.get(0);
}
}
public static List<Repo> all(Context context) {
return all(context, Cols.ALL);
}
public static List<Repo> 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<Repo> 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<Repo> cursorToList(Cursor cursor) {
int knownRepoCount = cursor != null ? cursor.getCount() : 0;
List<Repo> 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;
}
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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<Apk> 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<String> repoMirrors = new ArrayList<>();
/**
* Set of requested permissions per package/APK
*/
private final HashSet<String> 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<Apk> 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 <uses-permissions* makes ApkTable.Cols.REQUESTED_PERMISSIONS
addCommaSeparatedPermissions(str);
break;
case ApkTable.Cols.FEATURES:
curapk.features = Utils.parseCommaSeparatedString(str);
break;
case ApkTable.Cols.NATIVE_CODE:
curapk.nativecode = Utils.parseCommaSeparatedString(str);
break;
}
} else if (curapp != null) {
switch (localName) {
case "name":
curapp.name = str;
break;
case "icon":
curapp.iconFromApk = str;
break;
case "description":
// This is the old-style description. We'll read it
// if present, to support old repos, but in newer
// repos it will get overwritten straight away!
curapp.description = "<p>" + str + "</p>";
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 <a href="https://gitlab.com/fdroid/fdroidserver/blob/1afa8cfc/update.py#L91">
* More info into index - size, permissions, features, sdk version</a>
*/
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;
}
}

View File

@@ -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:
* <ul>
* <li>{@link ApkTable} because they are specific builds of a particular package. Many different
* builds of the same package can exist.</li>
* <li>{@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.</li>
* </ul>
*/
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,
};
}
}
}

View File

@@ -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?");
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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<List<AppListItem>> itemsLiveData;
private interface SortClause {
String WORDS = Cols.NAME;
String LAST_UPDATED = Cols.LAST_UPDATED;
String WORDS = "Name";
String LAST_UPDATED = "LastUpdated";
}
@Override

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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) {

View File

@@ -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<Apk> 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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<Apk> apks = new ArrayList<>();
public final List<App> apps = new ArrayList<>();
public final List<RepoPushRequest> 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<Apk> 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;
}
}
}