mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-02-15 01:23:56 -05:00
[app] remove old ContentProviders from old database implementation
This commit is contained in:
committed by
Hans-Christoph Steiner
parent
a783d3cb94
commit
7a1d288792
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 + ".");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 + " = ?";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 + ".");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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?");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user