mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-20 06:47:06 -04:00
[app] kill InstalledAppProvider and related code
We don't need to cache installed apps locally as they are available via the PackageManager as well. This avoids an entire class of bugs where our cache gets out of sync with reality. Also, it simplifies the code the database. We no longer need to listen to broadcast about which packages get installed and removed which is more tricker when targetting newer Android SDKs.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -455,11 +455,6 @@
|
||||
android:authorities="${applicationId}.data.TempAppProvider"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="org.fdroid.fdroid.data.InstalledAppProvider"
|
||||
android:authorities="${applicationId}.data.InstalledAppProvider"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="org.fdroid.fdroid.data.AppPrefsProvider"
|
||||
android:authorities="${applicationId}.data.AppPrefsProvider"
|
||||
@@ -512,15 +507,6 @@
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".receiver.PackageManagerReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_ADDED" />
|
||||
<action android:name="android.intent.action.PACKAGE_CHANGED" />
|
||||
<action android:name="android.intent.action.PACKAGE_REMOVED" />
|
||||
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".NotificationBroadcastReceiver"
|
||||
android:exported="false">
|
||||
@@ -568,11 +554,6 @@
|
||||
<service
|
||||
android:name=".installer.ObfInstallerService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.InstalledAppProviderService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service
|
||||
android:name=".AddRepoIntentService"
|
||||
android:exported="false" />
|
||||
|
||||
@@ -151,46 +151,6 @@ public class Apk extends ValueObject implements Comparable<Apk>, 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.
|
||||
* <p>
|
||||
* Note: Many of the fields on this instance will not be known in this circumstance. Currently
|
||||
* the only things that are known are:
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>{@link Apk#packageName}
|
||||
* <li>{@link Apk#versionName}
|
||||
* <li>{@link Apk#versionCode}
|
||||
* <li>{@link Apk#hash}
|
||||
* <li>{@link Apk#hashType}
|
||||
* </ul>
|
||||
* <p>
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -564,39 +564,6 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
isApk = apk.isApk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate from a locally installed package.
|
||||
* <p>
|
||||
* 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<App> 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<String, Long> lastUpdateTimes(Context context) {
|
||||
|
||||
Map<String, Long> 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 <a href="https://source.android.com/devices/tech/ota/sign_builds#certificates-keys">Certificates and private keys</a>
|
||||
*/
|
||||
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 <a href="https://source.android.com/devices/tech/ota/sign_builds#certificates-keys">Certificates and private keys</a>
|
||||
*/
|
||||
private QuerySelection selectNotSystemSignature(QuerySelection selection) {
|
||||
if (systemSignatures == null) {
|
||||
Log.i(TAG, "selectNotSystemSignature: systemSignature == null, querying for it");
|
||||
HashSet<String> 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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<String> 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.
|
||||
* <p>
|
||||
* 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}.
|
||||
* <p>
|
||||
* 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 <a href="https://gitlab.com/fdroid/fdroidclient/issues/819>issue #819</a>
|
||||
*/
|
||||
public static void compareToPackageManager(final Context context) {
|
||||
Utils.debugLog(TAG, "Comparing package manager to our installed app cache.");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 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<PackageInfo> 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<PackageInfo> {
|
||||
@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<PackageInfo> packageInfoList) {
|
||||
if (packageInfoList == null || packageInfoList.isEmpty()) {
|
||||
packageInfoList = context.getPackageManager().getInstalledPackages(PackageManager.GET_SIGNATURES);
|
||||
}
|
||||
Map<String, Long> cachedInfo = InstalledAppProvider.Helper.lastUpdateTimes(context);
|
||||
TreeSet<PackageInfo> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user