, Parcelable {
public Apk() {
}
- /**
- * If you need an {@link Apk} but it is no longer in the database any more (e.g. because the
- * version you have installed is no longer in the repository metadata) then you can instantiate
- * an {@link Apk} via an {@link InstalledApp} instance.
- *
- * Note: Many of the fields on this instance will not be known in this circumstance. Currently
- * the only things that are known are:
- *
- *
- * - {@link Apk#packageName}
- *
- {@link Apk#versionName}
- *
- {@link Apk#versionCode}
- *
- {@link Apk#hash}
- *
- {@link Apk#hashType}
- *
- *
- * This could instead be implemented by accepting a {@link PackageInfo} and it would get much
- * the same information, but it wouldn't have the hash of the package. Seeing as we've already
- * done the hard work to calculate that hash and stored it in the database, we may as well use
- * that.
- */
- public Apk(@NonNull InstalledApp app) {
- packageName = app.getPackageName();
- versionName = app.getVersionName();
- versionCode = app.getVersionCode();
- hash = app.getHash(); // checksum of the APK, in lowercase hex
- hashType = app.getHashType();
-
- // zero for "we don't know". If we require this in the future, then we could look up the
- // file on disk if required.
- size = 0;
-
- // Same as size. We could look this up if required but not needed at time of writing.
- installedFile = null;
-
- // If we are being created from an InstalledApp, it is because we couldn't load it from the
- // apk table in the database, indicating it is not available in any of our repos.
- repoId = 0;
- }
-
/**
* Creates a dummy APK from what is currently installed.
*/
diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java
index cafa3ba04..58755d480 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/App.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/App.java
@@ -564,39 +564,6 @@ public class App extends ValueObject implements Comparable, Parcelable {
isApk = apk.isApk();
}
- /**
- * Instantiate from a locally installed package.
- *
- * Initializes an {@link App} instances from an APK file. Since the file
- * could in the cache, and files can disappear from the cache at any time,
- * this needs to be quite defensive ensuring that {@code apkFile} still
- * exists.
- */
- @Nullable
- public static App getInstance(Context context, PackageManager pm, InstalledApp installedApp, String packageName)
- throws CertificateEncodingException, IOException, PackageManager.NameNotFoundException {
- App app = new App();
- PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
- SanitizedFile apkFile = SanitizedFile.knownSanitized(packageInfo.applicationInfo.publicSourceDir);
- app.installedApk = new Apk();
- if (installedApp != null) {
- app.installedApk.hashType = installedApp.getHashType();
- app.installedApk.hash = installedApp.getHash();
- } else if (apkFile.canRead()) {
- String hashType = "sha256";
- String hash = Utils.getFileHexDigest(apkFile, hashType);
- if (TextUtils.isEmpty(hash)) {
- return null;
- }
- app.installedApk.hashType = hashType;
- app.installedApk.hash = hash;
- }
-
- app.setFromPackageInfo(pm, packageInfo);
- app.initInstalledApk(context, app.installedApk, packageInfo, apkFile);
- return app;
- }
-
/**
* In order to format all in coming descriptions before they are written
* out to the database and used elsewhere, this is needed to intercept
diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java
index 877006028..6d7d9c7c4 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java
@@ -467,7 +467,6 @@ public class DBHelper extends SQLiteOpenHelper {
addAuthorToApp(db, oldVersion);
useMaxValueInMaxSdkVersion(db, oldVersion);
requireTimestampInRepos(db, oldVersion);
- recreateInstalledAppTable(db, oldVersion);
addTargetSdkVersionToApk(db, oldVersion);
migrateAppPrimaryKeyToRowId(db, oldVersion);
removeApkPackageNameColumn(db, oldVersion);
@@ -1468,24 +1467,6 @@ public class DBHelper extends SQLiteOpenHelper {
RepoTable.Cols._ID + ", " + RepoTable.Cols.IS_SWAP + ");");
}
- /**
- * If any column was added or removed, just drop the table, create it again
- * and let the cache be filled from scratch by {@link InstalledAppProviderService}
- * For DB versions older than 43, this will create the {@link InstalledAppProvider}
- * table for the first time.
- */
- private void recreateInstalledAppTable(SQLiteDatabase db, int oldVersion) {
- if (oldVersion >= 56) {
- return;
- }
- Utils.debugLog(TAG, "(re)creating 'installed app' database table.");
- if (tableExists(db, "fdroid_installedApp")) {
- db.execSQL("DROP TABLE fdroid_installedApp;");
- }
-
- db.execSQL(CREATE_TABLE_INSTALLED_APP);
- }
-
private void addTargetSdkVersionToApk(SQLiteDatabase db, int oldVersion) {
if (oldVersion >= 57) {
return;
diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java
deleted file mode 100644
index 20edcd367..000000000
--- a/app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package org.fdroid.fdroid.data;
-
-import android.database.Cursor;
-
-public class InstalledApp extends ValueObject {
-
- private long id;
- private String packageName;
- private int versionCode;
- private String versionName;
- private String applicationLabel;
- private String signature;
- private long lastUpdateTime;
- private String hashType;
- private String hash;
-
- public InstalledApp(Cursor cursor) {
-
- checkCursorPosition(cursor);
-
- for (int i = 0; i < cursor.getColumnCount(); i++) {
- String n = cursor.getColumnName(i);
- switch (n) {
- case Schema.InstalledAppTable.Cols._ID:
- id = cursor.getLong(i);
- break;
- case Schema.InstalledAppTable.Cols.Package.NAME:
- packageName = cursor.getString(i);
- break;
- case Schema.InstalledAppTable.Cols.VERSION_CODE:
- versionCode = cursor.getInt(i);
- break;
- case Schema.InstalledAppTable.Cols.VERSION_NAME:
- versionName = cursor.getString(i);
- break;
- case Schema.InstalledAppTable.Cols.APPLICATION_LABEL:
- applicationLabel = cursor.getString(i);
- break;
- case Schema.InstalledAppTable.Cols.SIGNATURE:
- signature = cursor.getString(i);
- break;
- case Schema.InstalledAppTable.Cols.LAST_UPDATE_TIME:
- lastUpdateTime = cursor.getLong(i);
- break;
- case Schema.InstalledAppTable.Cols.HASH_TYPE:
- hashType = cursor.getString(i);
- break;
- case Schema.InstalledAppTable.Cols.HASH:
- hash = cursor.getString(i);
- break;
- }
- }
- }
-
- public long getId() {
- return id;
- }
-
- public String getPackageName() {
- return packageName;
- }
-
- public int getVersionCode() {
- return versionCode;
- }
-
- public String getVersionName() {
- return versionName;
- }
-
- public String getApplicationLabel() {
- return applicationLabel;
- }
-
- public String getSignature() {
- return signature;
- }
-
- public long getLastUpdateTime() {
- return lastUpdateTime;
- }
-
- public String getHashType() {
- return hashType;
- }
-
- public String getHash() {
- return hash;
- }
-}
diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java
deleted file mode 100644
index ec808dc38..000000000
--- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java
+++ /dev/null
@@ -1,380 +0,0 @@
-package org.fdroid.fdroid.data;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.UriMatcher;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.data.Schema.AppMetadataTable;
-import org.fdroid.fdroid.data.Schema.InstalledAppTable;
-import org.fdroid.fdroid.data.Schema.InstalledAppTable.Cols;
-import org.fdroid.fdroid.data.Schema.PackageTable;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-public class InstalledAppProvider extends FDroidProvider {
-
- private static final String TAG = "InstalledAppProvider";
-
- public static class Helper {
-
- public static App[] all(Context context) {
- ArrayList appList = new ArrayList<>();
- Cursor cursor = context.getContentResolver().query(InstalledAppProvider.getAllAppsUri(),
- null, null, null, null);
- if (cursor != null) {
- if (cursor.getCount() > 0) {
- cursor.moveToFirst();
- while (!cursor.isAfterLast()) {
- appList.add(new App(cursor));
- cursor.moveToNext();
- }
- }
- cursor.close();
- }
- return appList.toArray(new App[0]);
- }
-
- /**
- * @return The keys are the package names, and their corresponding values are
- * the {@link PackageInfo#lastUpdateTime last update time} in milliseconds.
- */
- public static Map lastUpdateTimes(Context context) {
-
- Map cachedInfo = new HashMap<>();
-
- final Uri uri = InstalledAppProvider.getContentUri();
- Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
- if (cursor != null) {
- if (cursor.getCount() > 0) {
- cursor.moveToFirst();
- while (!cursor.isAfterLast()) {
- cachedInfo.put(
- cursor.getString(cursor.getColumnIndexOrThrow(Cols.Package.NAME)),
- cursor.getLong(cursor.getColumnIndexOrThrow(Cols.LAST_UPDATE_TIME))
- );
- cursor.moveToNext();
- }
- }
- cursor.close();
- }
-
- return cachedInfo;
- }
-
- @Nullable
- public static InstalledApp findByPackageName(Context context, String packageName) {
- Cursor cursor = context.getContentResolver().query(getAppUri(packageName), null, null, null, null);
- if (cursor == null) {
- return null;
- }
-
- try {
- if (cursor.getCount() == 0) {
- return null;
- }
-
- cursor.moveToFirst();
- return new InstalledApp(cursor);
- } finally {
- cursor.close();
- }
- }
- }
-
- private static final String PROVIDER_NAME = "InstalledAppProvider";
-
- private static final String PATH_SEARCH = "search";
- private static final int CODE_SEARCH = CODE_SINGLE + 1;
- private static final String PATH_ALL_APPS = "allApps";
- private static final int CODE_ALL_APPS = CODE_SEARCH + 1;
-
- private static final UriMatcher MATCHER = new UriMatcher(-1);
-
- /**
- * Built-in apps that are signed by the various Android ROM keys.
- *
- * @see Certificates and private keys
- */
- private static final String[] SYSTEM_PACKAGES = {
- "android", // platform key
- "com.android.email", // test/release key
- "com.android.contacts", // shared key
- "com.android.providers.downloads", // media key
- };
-
- private static String[] systemSignatures;
-
- static {
- MATCHER.addURI(getAuthority(), null, CODE_LIST);
- MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", CODE_SEARCH);
- MATCHER.addURI(getAuthority(), PATH_ALL_APPS, CODE_ALL_APPS);
- MATCHER.addURI(getAuthority(), "*", CODE_SINGLE);
- }
-
- public static Uri getContentUri() {
- return Uri.parse("content://" + getAuthority());
- }
-
- public static Uri getAllAppsUri() {
- return getContentUri().buildUpon().appendPath(PATH_ALL_APPS).build();
- }
-
- /**
- * @return the {@link Uri} that points to a specific installed app
- */
- public static Uri getAppUri(String packageName) {
- return Uri.withAppendedPath(getContentUri(), packageName);
- }
-
- public static Uri getSearchUri(String keywords) {
- return getContentUri().buildUpon()
- .appendPath(PATH_SEARCH)
- .appendPath(keywords)
- .build();
- }
-
- public static String getApplicationLabel(Context context, String packageName) {
- PackageManager pm = context.getPackageManager();
- ApplicationInfo appInfo;
- try {
- appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
- return appInfo.loadLabel(pm).toString();
- } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
- Utils.debugLog(TAG, "Could not get application label: " + e.getMessage());
- }
- return packageName; // all else fails, return packageName
- }
-
- /**
- * Add SQL selection statement to exclude {@link InstalledApp}s that were
- * signed by the platform/shared/media/testkey keys.
- *
- * @see Certificates and private keys
- */
- private QuerySelection selectNotSystemSignature(QuerySelection selection) {
- if (systemSignatures == null) {
- Log.i(TAG, "selectNotSystemSignature: systemSignature == null, querying for it");
- HashSet signatures = new HashSet<>();
- for (String packageName : SYSTEM_PACKAGES) {
- Cursor cursor = query(InstalledAppProvider.getAppUri(packageName), new String[]{Cols.SIGNATURE},
- null, null, null);
- if (cursor != null) {
- if (cursor.moveToFirst()) {
- signatures.add(cursor.getString(cursor.getColumnIndexOrThrow(Cols.SIGNATURE)));
- }
- cursor.close();
- }
- }
- systemSignatures = signatures.toArray(new String[signatures.size()]);
- }
-
- Log.i(TAG, "excluding InstalledApps signed by system signatures");
- for (String systemSignature : systemSignatures) {
- selection = selection.add("NOT " + Cols.SIGNATURE + " IN (?)", new String[]{systemSignature});
- }
- return selection;
- }
-
- @Override
- protected String getTableName() {
- return InstalledAppTable.NAME;
- }
-
- @Override
- protected String getProviderName() {
- return "InstalledAppProvider";
- }
-
- public static String getAuthority() {
- return AUTHORITY + "." + PROVIDER_NAME;
- }
-
- @Override
- protected UriMatcher getMatcher() {
- return MATCHER;
- }
-
- private QuerySelection queryApp(String packageName) {
- return new QuerySelection(Cols.Package.NAME + " = ?", new String[]{packageName});
- }
-
- private QuerySelection queryAppSubQuery(String packageName) {
- String pkg = Schema.PackageTable.NAME;
- String subQuery = "(" +
- " SELECT " + pkg + "." + Schema.PackageTable.Cols.ROW_ID +
- " FROM " + pkg +
- " WHERE " + pkg + "." + Schema.PackageTable.Cols.PACKAGE_NAME + " = ?)";
- String query = Cols.PACKAGE_ID + " = " + subQuery;
- return new QuerySelection(query, new String[]{packageName});
- }
-
- private QuerySelection querySearch(String query) {
- return new QuerySelection(Cols.APPLICATION_LABEL + " LIKE ?",
- new String[]{"%" + query + "%"});
- }
-
- private static class QueryBuilder extends org.fdroid.fdroid.data.QueryBuilder {
- @Override
- protected String getRequiredTables() {
- String pkg = Schema.PackageTable.NAME;
- String installed = InstalledAppTable.NAME;
- return installed + " JOIN " + pkg +
- " ON (" + pkg + "." + Schema.PackageTable.Cols.ROW_ID + " = " +
- installed + "." + Cols.PACKAGE_ID + ")";
- }
-
- @Override
- public void addField(String field) {
- if (TextUtils.equals(field, Cols.Package.NAME)) {
- appendField(Schema.PackageTable.Cols.PACKAGE_NAME, Schema.PackageTable.NAME, field);
- } else {
- appendField(field, InstalledAppTable.NAME);
- }
- }
- }
-
- @Override
- public Cursor query(@NonNull Uri uri, String[] projection,
- String customSelection, String[] selectionArgs, String sortOrder) {
- if (sortOrder == null) {
- sortOrder = Cols.APPLICATION_LABEL;
- }
-
- QuerySelection selection = new QuerySelection(customSelection, selectionArgs);
- QueryBuilder query = null;
- switch (MATCHER.match(uri)) {
- case CODE_LIST:
- selection = selectNotSystemSignature(selection);
- break;
-
- case CODE_SINGLE:
- selection = selection.add(queryApp(uri.getLastPathSegment()));
- break;
-
- case CODE_SEARCH:
- selection = selection.add(querySearch(uri.getLastPathSegment()));
- break;
-
- case CODE_ALL_APPS:
- selection = selectNotSystemSignature(selection);
- query = new QueryBuilder();
- query.addField(Cols._ID);
- query.appendField(Cols.APPLICATION_LABEL, null, Schema.AppMetadataTable.Cols.NAME);
- query.appendField(Cols.VERSION_CODE, null, AppMetadataTable.Cols.SUGGESTED_VERSION_CODE);
- query.appendField(Cols.VERSION_NAME, null, AppMetadataTable.Cols.SUGGESTED_VERSION_NAME);
- query.appendField(PackageTable.Cols.PACKAGE_NAME, PackageTable.NAME,
- AppMetadataTable.Cols.Package.PACKAGE_NAME);
- break;
-
- default:
- String message = "Invalid URI for installed app content provider: " + uri;
- Log.e(TAG, message);
- throw new UnsupportedOperationException(message);
- }
-
- if (query != null) { // NOPMD
- // the fields are already setup above
- } else if (projection == null || projection.length == 0) {
- query = new QueryBuilder();
- query.addFields(Cols.ALL);
- } else {
- query = new QueryBuilder();
- query.addFields(projection);
- }
- query.addSelection(selection);
- query.addOrderBy(sortOrder);
-
- Cursor cursor = db().rawQuery(query.toString(), selection.getArgs());
- cursor.setNotificationUri(getContext().getContentResolver(), uri);
- return cursor;
- }
-
- @Override
- public int delete(@NonNull Uri uri, String where, String[] whereArgs) {
-
- if (MATCHER.match(uri) != CODE_SINGLE) {
- throw new UnsupportedOperationException("Delete not supported for " + uri + ".");
- }
-
- String packageName = uri.getLastPathSegment();
- QuerySelection query = new QuerySelection(where, whereArgs);
- query = query.add(queryAppSubQuery(packageName));
-
- int count = db().delete(getTableName(), query.getSelection(), query.getArgs());
-
- AppProvider.Helper.calcSuggestedApk(getContext(), packageName);
-
- return count;
- }
-
- /**
- * {@link Cols.Package#NAME} is not included in the database here, because
- * it is included only in the {@link PackageTable}, since there are large
- * cross-table queries needed to handle the complexity of multiple repos
- * potentially serving the same apps.
- */
- @Override
- public Uri insert(@NonNull Uri uri, ContentValues values) {
-
- if (MATCHER.match(uri) != CODE_LIST) {
- throw new UnsupportedOperationException("Insert not supported for " + uri + ".");
- }
-
- if (!values.containsKey(Cols.Package.NAME)) {
- throw new IllegalStateException("Package name not provided to InstalledAppProvider");
- }
-
- String packageName = values.getAsString(Cols.Package.NAME);
- long packageId = PackageIdProvider.Helper.ensureExists(getContext(), packageName);
- values.remove(Cols.Package.NAME);
- values.put(Cols.PACKAGE_ID, packageId);
-
- verifyVersionNameNotNull(values);
-
- db().replaceOrThrow(getTableName(), null, values);
-
- AppProvider.Helper.calcSuggestedApk(getContext(), packageName);
-
- return getAppUri(values.getAsString(Cols.Package.NAME));
- }
-
- /**
- * Update is not supported for {@code InstalledAppProvider}. Instead, use
- * {@link #insert(Uri, ContentValues)}, and it will overwrite the relevant
- * row, if one exists. This just throws {@link UnsupportedOperationException}
- */
- @Override
- public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) {
- throw new UnsupportedOperationException("\"Update' not supported for installed appp provider."
- + " Instead, you should insert, and it will overwrite the relevant rows if one exists.");
- }
-
- /**
- * During development, I stumbled across one (out of over 300) installed apps which had a versionName
- * of null. As such, I figured we may as well store it as "Unknown". The alternative is to allow the
- * column to accept NULL values in the database, and then deal with the potential of a null everywhere
- * "versionName" is used.
- */
- private void verifyVersionNameNotNull(ContentValues values) {
- if (values.containsKey(Cols.VERSION_NAME) && values.getAsString(Cols.VERSION_NAME) == null) {
- values.put(Cols.VERSION_NAME, getContext().getString(R.string.unknown));
- }
- }
-
-}
diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java
deleted file mode 100644
index c61f8cedb..000000000
--- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java
+++ /dev/null
@@ -1,382 +0,0 @@
-package org.fdroid.fdroid.data;
-
-import android.content.ComponentName;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.IBinder;
-import android.os.Process;
-import android.os.RemoteException;
-import android.util.Log;
-
-import org.acra.ACRA;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.data.Schema.InstalledAppTable;
-import org.fdroid.fdroid.installer.PrivilegedInstaller;
-import org.fdroid.fdroid.privileged.IPrivilegedService;
-
-import java.io.File;
-import java.io.FilenameFilter;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeSet;
-import java.util.concurrent.TimeUnit;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.JobIntentService;
-import io.reactivex.rxjava3.disposables.CompositeDisposable;
-import io.reactivex.rxjava3.schedulers.Schedulers;
-import io.reactivex.rxjava3.subjects.PublishSubject;
-
-/**
- * Handles all updates to {@link InstalledAppProvider}, whether checking the contents
- * versus what Android says is installed, or processing {@link Intent}s that come
- * from {@link android.content.BroadcastReceiver}s for {@link Intent#ACTION_PACKAGE_ADDED}
- * and {@link Intent#ACTION_PACKAGE_REMOVED}
- *
- * Since {@link android.content.ContentProvider#insert(Uri, ContentValues)} does not check
- * for duplicate records, it is entirely the job of this service to ensure that it is not
- * inserting duplicate versions of the same installed APK. On that note,
- * {@link #insertAppIntoDb(Context, PackageInfo, String, String)} and
- * {@link #deleteAppFromDb(Context, String)} are both static methods to enable easy testing
- * of this stuff.
- *
- * This also updates the {@link org.fdroid.fdroid.AppUpdateStatusManager.Status status} of any
- * package installs that are still in progress. Most importantly, this
- * provides the final {@link org.fdroid.fdroid.AppUpdateStatusManager.Status#Installed status update}
- * to mark the end of the installation process. It also errors out installation
- * processes where some outside factor uninstalled the package while the F-Droid
- * process was underway, e.g. uninstalling via {@code adb}, updates via Google
- * Play, Yalp, etc.
- */
-public class InstalledAppProviderService extends JobIntentService {
- private static final String TAG = "InstalledAppProviderSer";
-
- private static final String ACTION_INSERT = "org.fdroid.fdroid.data.action.INSERT";
- private static final String ACTION_DELETE = "org.fdroid.fdroid.data.action.DELETE";
-
- private static final String EXTRA_PACKAGE_INFO = "org.fdroid.fdroid.data.extra.PACKAGE_INFO";
-
- /**
- * This is for notifying the users of this {@link android.content.ContentProvider}
- * that the contents have changed. Since {@link Intent}s can come in slow
- * or fast, and this can trigger a lot of UI updates, the actual
- * notifications are rate limited to one per second.
- */
- private PublishSubject packageChangeNotifier;
-
- private final CompositeDisposable compositeDisposable = new CompositeDisposable();
-
- @Override
- public void onCreate() {
- super.onCreate();
- packageChangeNotifier = PublishSubject.create();
-
- // This "debounced" event will queue up any number of invocations within one second, and
- // only emit an event to the subscriber after it has not received any new events for one second.
- // This ensures that we don't constantly ask our lists of apps to update as we iterate over
- // the list of installed apps and insert them to the database...
- compositeDisposable.add(
- packageChangeNotifier
- .subscribeOn(Schedulers.newThread())
- .debounce(3, TimeUnit.SECONDS)
- .subscribe(packageName -> {
- Utils.debugLog(TAG, "Notifying content providers to update relevant views.");
- getContentResolver().notifyChange(AppProvider.getContentUri(), null);
- getContentResolver().notifyChange(ApkProvider.getContentUri(), null);
- })
- );
-
- // ...alternatively, this non-debounced version will instantly emit an event about the
- // particular package being updated. This is required so that our AppDetails view can update
- // itself immediately in response to an app being installed/upgraded/removed.
- // It does this _without_ triggering the main lists to update themselves, because they listen
- // only for changes to specific URIs in the AppProvider. These are triggered when a more
- // general notification (e.g. to AppProvider.getContentUri()) is fired, but not when a
- // sibling such as AppProvider.getHighestPriorityMetadataUri() is fired.
- compositeDisposable.add(
- packageChangeNotifier
- .subscribeOn(Schedulers.newThread())
- .subscribe(packageName -> getContentResolver()
- .notifyChange(AppProvider.getHighestPriorityMetadataUri(packageName), null))
- );
- }
-
- @Override
- public void onDestroy() {
- compositeDisposable.dispose();
- super.onDestroy();
- }
-
- /**
- * Inserts an app into {@link InstalledAppProvider} based on a {@code package:} {@link Uri}.
- * This has no checks for whether it is inserting an exact duplicate, whatever is provided
- * will be inserted.
- */
- public static void insert(Context context, PackageInfo packageInfo) {
- insert(context, Utils.getPackageUri(packageInfo.packageName), packageInfo);
- }
-
- /**
- * Inserts an app into {@link InstalledAppProvider} based on a {@code package:} {@link Uri}.
- * This has no checks for whether it is inserting an exact duplicate, whatever is provided
- * will be inserted.
- */
- public static void insert(Context context, Uri uri) {
- insert(context, uri, null);
- }
-
- private static void insert(Context context, Uri uri, PackageInfo packageInfo) {
- Intent intent = new Intent(context, InstalledAppProviderService.class);
- intent.setAction(ACTION_INSERT);
- intent.setData(uri);
- intent.putExtra(EXTRA_PACKAGE_INFO, packageInfo);
- enqueueWork(context, intent);
- }
-
- /**
- * Deletes an app from {@link InstalledAppProvider} based on a {@code package:} {@link Uri}
- */
- public static void delete(Context context, String packageName) {
- delete(context, Utils.getPackageUri(packageName));
- }
-
- /**
- * Deletes an app from {@link InstalledAppProvider} based on a {@code package:} {@link Uri}
- */
- public static void delete(Context context, Uri uri) {
- Intent intent = new Intent(context, InstalledAppProviderService.class);
- intent.setAction(ACTION_DELETE);
- intent.setData(uri);
- enqueueWork(context, intent);
- }
-
- private static void enqueueWork(Context context, Intent intent) {
- enqueueWork(context, InstalledAppProviderService.class, 0x192834, intent);
- }
-
- /**
- * Make sure that {@link InstalledAppProvider}, our database of installed apps,
- * is in sync with what the {@link PackageManager} tells us is installed. Once
- * completed, the relevant {@link android.content.ContentProvider}s will be
- * notified of any changes to installed statuses. The packages are processed
- * in alphabetically order so that "{@code android}" is processed first. That
- * is always present and signed by the system key, so it is the source of the
- * system key for comparing all packages.
- *
- * The installed app cache could get out of sync, e.g. if F-Droid crashed/ or
- * ran out of battery half way through responding to {@link Intent#ACTION_PACKAGE_ADDED}.
- * This method returns immediately, and will continue to work in an
- * {@link JobIntentService}. It doesn't really matter where we put this in the
- * bootstrap process, because it runs in its own thread, at the lowest priority:
- * {@link Process#THREAD_PRIORITY_LOWEST}.
- *
- * APKs installed in {@code /system} will often have zeroed out timestamps, like
- * 2008-01-01 (ziptime) or 2009-01-01. So instead anything older than 2010 every
- * time since we have no way to know whether an APK wasn't changed as part of an
- * OTA update. An OTA update could change the APK without changing the
- * {@link PackageInfo#versionCode} or {@link PackageInfo#lastUpdateTime}.
- *
- * @see = 29 &&
- PrivilegedInstaller.isExtensionInstalledCorrectly(context) ==
- PrivilegedInstaller.IS_EXTENSION_INSTALLED_YES) {
- ServiceConnection mServiceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
- List packageInfoList = null;
- try {
- packageInfoList = privService.getInstalledPackages(PackageManager.GET_SIGNATURES);
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- compareToPackageManager(context, packageInfoList);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName componentName) {
- // Nothing to tear down from onServiceConnected
- }
- };
-
- Intent serviceIntent = new Intent(PrivilegedInstaller.PRIVILEGED_EXTENSION_SERVICE_INTENT);
- serviceIntent.setPackage(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME);
- context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
- Context.BIND_AUTO_CREATE);
- } else {
- compareToPackageManager(context, null);
- }
- }
-
- private static class PackageInfoComparator implements Comparator {
- @Override
- public int compare(PackageInfo o1, PackageInfo o2) {
- // There are two trichrome library entries in the list,
- // one for each version. We only want the newest here.
- String[] duplicateList = new String[]{"org.chromium.trichromelibrary"};
- for (String dup : duplicateList) {
- if (o1.packageName.contentEquals(dup)
- && o2.packageName.contentEquals(dup)) {
- return Integer.compare(o1.versionCode, o2.versionCode);
- }
- }
- return o1.packageName.compareTo(o2.packageName);
- }
- }
-
- private static void compareToPackageManager(Context context, List packageInfoList) {
- if (packageInfoList == null || packageInfoList.isEmpty()) {
- packageInfoList = context.getPackageManager().getInstalledPackages(PackageManager.GET_SIGNATURES);
- }
- Map cachedInfo = InstalledAppProvider.Helper.lastUpdateTimes(context);
- TreeSet packageInfoSet = new TreeSet<>(new PackageInfoComparator());
- packageInfoSet.addAll(packageInfoList);
- for (PackageInfo packageInfo : packageInfoSet) {
- if (cachedInfo.containsKey(packageInfo.packageName)) {
- if (packageInfo.lastUpdateTime < 1262300400000L // 2010-01-01 00:00
- || packageInfo.lastUpdateTime > cachedInfo.get(packageInfo.packageName)) {
- insert(context, packageInfo);
- }
- cachedInfo.remove(packageInfo.packageName);
- } else {
- insert(context, packageInfo);
- }
- }
-
- for (String packageName : cachedInfo.keySet()) {
- delete(context, packageName);
- }
- }
-
- @Nullable
- public static File getPathToInstalledApk(PackageInfo packageInfo) {
- File apk = new File(packageInfo.applicationInfo.publicSourceDir);
- if (apk.isDirectory()) {
- FilenameFilter filter = (dir, name) -> name.endsWith(".apk");
- File[] files = apk.listFiles(filter);
- if (files == null) {
- String msg = packageInfo.packageName + " sourceDir has no APKs: " + apk.getAbsolutePath();
- Utils.debugLog(TAG, msg);
- ACRA.getErrorReporter().handleException(new IllegalArgumentException(msg), false);
- return null;
- }
- apk = files[0];
- }
-
- return apk;
- }
-
- @Override
- protected void onHandleWork(@NonNull Intent intent) {
- Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
-
- //AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this);
- String packageName = intent.getData().getSchemeSpecificPart();
- final String action = intent.getAction();
- if (ACTION_INSERT.equals(action)) {
- PackageInfo packageInfo = getPackageInfo(intent, packageName);
- if (packageInfo != null) {
- //for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) {
- // these cause duplicate events, do we really need this?
- // ausm.updateApk(status.getCanonicalUrl(), AppUpdateStatusManager.Status.Installed, null);
- //}
- File apk = getPathToInstalledApk(packageInfo);
- if (apk == null) {
- return;
- }
- if (apk.exists() && apk.canRead()) {
- try {
- String hashType = "sha256";
- String hash = Utils.getFileHexDigest(apk, hashType);
- insertAppIntoDb(this, packageInfo, hashType, hash);
- } catch (IllegalArgumentException e) {
- Utils.debugLog(TAG, e.getMessage());
- ACRA.getErrorReporter().handleException(e, false);
- return;
- }
- }
- }
- } else if (ACTION_DELETE.equals(action)) {
- deleteAppFromDb(this, packageName);
- //for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) {
- // these cause duplicate events, do we really need this?
- // ausm.updateApk(status.getCanonicalUrl(), AppUpdateStatusManager.Status.InstallError, null);
- //}
- }
- packageChangeNotifier.onNext(packageName);
- }
-
- /**
- * This class will either have received an intent from the {@link InstalledAppProviderService}
- * itself, while iterating over installed apps, or from a {@link Intent#ACTION_PACKAGE_ADDED}
- * broadcast. In the first case, it will already have a {@link PackageInfo} for us. However if
- * it is from the later case, we'll need to query the {@link PackageManager} ourselves to get
- * this info.
- *
- * Can still return null, as there is potentially race conditions to do with uninstalling apps
- * such that querying the {@link PackageManager} for a given package may throw an exception.
- *
- * The {@code PackageManagerGetSignatures} lint check is not relevant here since this is doing
- * nothing related to verifying the signature. The APK signatures are just processed to
- * produce the unique ID of the signer to determine compatibility. This {@code Service} does
- * nothing related to checking valid APK signatures.
- */
- @SuppressWarnings("PackageManagerGetSignatures")
- @Nullable
- private PackageInfo getPackageInfo(Intent intent, String packageName) {
- PackageInfo packageInfo = intent.getParcelableExtra(EXTRA_PACKAGE_INFO);
- if (packageInfo != null) {
- return packageInfo;
- }
-
- try {
- return getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- return null;
- }
- }
-
- /**
- * @param hash Although the has could be calculated within this function, it is helpful to inject
- * the hash so as to be able to use this method during testing. Otherwise, the
- * hashing method will try to hash a non-existent .apk file and try to insert NULL
- * into the database when under test.
- */
- static void insertAppIntoDb(Context context, PackageInfo packageInfo, String hashType, String hash) {
- if (true) return;
- Log.d(TAG, "insertAppIntoDb " + packageInfo.packageName);
- Uri uri = InstalledAppProvider.getContentUri();
- ContentValues contentValues = new ContentValues();
- contentValues.put(InstalledAppTable.Cols.Package.NAME, packageInfo.packageName);
- contentValues.put(InstalledAppTable.Cols.VERSION_CODE, packageInfo.versionCode);
- contentValues.put(InstalledAppTable.Cols.VERSION_NAME, packageInfo.versionName);
- contentValues.put(InstalledAppTable.Cols.APPLICATION_LABEL,
- InstalledAppProvider.getApplicationLabel(context, packageInfo.packageName));
- contentValues.put(InstalledAppTable.Cols.SIGNATURE, Utils.getPackageSigner(packageInfo));
- contentValues.put(InstalledAppTable.Cols.LAST_UPDATE_TIME, packageInfo.lastUpdateTime);
-
- contentValues.put(InstalledAppTable.Cols.HASH_TYPE, hashType);
- contentValues.put(InstalledAppTable.Cols.HASH, hash);
-
- context.getContentResolver().insert(uri, contentValues);
- }
-
- static void deleteAppFromDb(Context context, String packageName) {
- Log.d(TAG, "deleteAppFromDb " + packageName);
- Uri uri = InstalledAppProvider.getAppUri(packageName);
- context.getContentResolver().delete(uri, null, null);
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java
deleted file mode 100644
index 9f6797c9a..000000000
--- a/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.fdroid.fdroid.receiver;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.fdroid.fdroid.data.InstalledAppProviderService;
-import org.fdroid.fdroid.installer.PrivilegedInstaller;
-
-/**
- * Receive {@link Intent#ACTION_PACKAGE_ADDED} and {@link Intent#ACTION_PACKAGE_REMOVED}
- * events from {@link android.content.pm.PackageManager} to keep
- * {@link org.fdroid.fdroid.data.InstalledAppProvider} updated. This ignores
- * {@link Intent#EXTRA_REPLACING} and instead handles updates by just deleting then
- * inserting the app being updated in direct response to the {@code Intent}s from
- * the system. This is also necessary because there are no other checks to prevent
- * multiple copies of the same app being inserted into {@link InstalledAppProviderService}.
- */
-public class PackageManagerReceiver extends BroadcastReceiver {
- private static final String TAG = "PackageManagerReceiver";
-
- @Override
- public void onReceive(Context context, Intent intent) {
- // TODO might not be needed anymore
- if (true) return;
- if (intent != null) {
- String action = intent.getAction();
- if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
- InstalledAppProviderService.insert(context, intent.getData());
- } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
- if (TextUtils.equals(context.getPackageName(), intent.getData().getSchemeSpecificPart())) {
- Log.i(TAG, "Ignoring request to remove ourselves from cache.");
- } else {
- InstalledAppProviderService.delete(context, intent.getData());
- }
- } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action) && Build.VERSION.SDK_INT >= 29 &&
- PrivilegedInstaller.isExtensionInstalledCorrectly(context) ==
- PrivilegedInstaller.IS_EXTENSION_INSTALLED_YES) {
- String[] allowList = new String[]{"org.chromium.chrome"};
- for (String allowed : allowList) {
- if (allowed.equals(intent.getData().getSchemeSpecificPart())) {
- InstalledAppProviderService.compareToPackageManager(context);
- }
- }
- } else {
- Log.i(TAG, "unsupported action: " + action + " " + intent);
- }
- }
- }
-}