diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SelectAppsView.java b/app/src/full/java/org/fdroid/fdroid/nearby/SelectAppsView.java index 62dee5a91..08e0ade1d 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SelectAppsView.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SelectAppsView.java @@ -27,7 +27,6 @@ import androidx.core.content.ContextCompat; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.InstalledAppProvider; import java.util.ArrayList; import java.util.List; diff --git a/app/src/full/java/org/fdroid/fdroid/panic/PanicResponderActivity.java b/app/src/full/java/org/fdroid/fdroid/panic/PanicResponderActivity.java index 164f16b7e..7a16b7235 100644 --- a/app/src/full/java/org/fdroid/fdroid/panic/PanicResponderActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/panic/PanicResponderActivity.java @@ -1,31 +1,21 @@ package org.fdroid.fdroid.panic; import android.content.BroadcastReceiver; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.util.Log; -import org.fdroid.database.FDroidDatabase; import org.fdroid.fdroid.Preferences; -import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.DBHelper; -import org.fdroid.fdroid.data.InstalledApp; -import org.fdroid.fdroid.data.InstalledAppProvider; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.InstallerService; import org.fdroid.fdroid.installer.PrivilegedInstaller; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; -import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d6775db5..a6ce51322 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -455,11 +455,6 @@ android:authorities="${applicationId}.data.TempAppProvider" android:exported="false" /> - - - - - - - - - - - @@ -568,11 +554,6 @@ - - diff --git a/app/src/main/java/org/fdroid/fdroid/data/Apk.java b/app/src/main/java/org/fdroid/fdroid/data/Apk.java index 602bcd831..c9d93ae7f 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java @@ -151,46 +151,6 @@ public class Apk extends ValueObject implements Comparable, 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); - } - } - } -}