From a7a52fbfba3f7c89a249ceb5c3c42d587e37d3f9 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 10 Nov 2016 11:16:29 +1100 Subject: [PATCH] Fall back to InstalledAppProvider when trying to identify the apk to uninstall. Extracted `getInstalledApk()` method so that it could be better documented, and makes the `uninstallApk()` more consise. It will now throw an `IllegalStateException` if no apk is found, because as issue #800 shows we will end up with a NPE otherwise. Fixes issue #800. --- .../java/org/fdroid/fdroid/AppDetails.java | 54 ++++++++--- .../main/java/org/fdroid/fdroid/data/Apk.java | 39 ++++++++ .../org/fdroid/fdroid/data/InstalledApp.java | 90 +++++++++++++++++++ .../fdroid/data/InstalledAppProvider.java | 19 ++++ 4 files changed, 190 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index da54273ea..b4cf3a148 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -84,6 +84,7 @@ import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppPrefs; import org.fdroid.fdroid.data.AppPrefsProvider; import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.InstalledApp; import org.fdroid.fdroid.data.InstalledAppProvider; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.Schema; @@ -961,23 +962,52 @@ public class AppDetails extends AppCompatActivity { } /** - * Queue for uninstall based on the instance variable {@link #app} + * Attempts to find the installed {@link Apk} from the database. If not found, will lookup the + * {@link InstalledAppProvider} to find the details of the installed app and use that to + * instantiate an {@link Apk} to be returned. + * + * Cases where an {@link Apk} will not be found in the database and for which we fall back to + * the {@link InstalledAppProvider} include: + * + System apps which are provided by a repository, but for which the version code bundled + * with the system is not included in the repository. + * + Regular apps from a repository, where the installed version is old enough that it is no + * longer available in the repository. + * + * @throws IllegalStateException If neither the {@link PackageManager} or the + * {@link InstalledAppProvider} can't find a reference to the installed apk. + */ + @NonNull + private Apk getInstalledApk() { + try { + PackageInfo pi = packageManager.getPackageInfo(app.packageName, 0); + + Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, pi.packageName, pi.versionCode); + if (apk == null) { + InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(context, pi.packageName); + if (installedApp == null) { + throw new IllegalStateException("No installed app found when trying to uninstall"); + } + + apk = new Apk(installedApp); + } + return apk; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + throw new IllegalStateException("Couldn't find app while installing"); + } + } + + /** + * Queue for uninstall based on the instance variable {@link #app}. */ private void uninstallApk() { - Apk apk = app.installedApk; - if (apk == null) { + if (app.installedApk == null) { // TODO ideally, app would be refreshed immediately after install, then this // workaround would be unnecessary - try { - PackageInfo pi = packageManager.getPackageInfo(app.packageName, 0); - apk = ApkProvider.Helper.findApkFromAnyRepo(this, pi.packageName, pi.versionCode); - app.installedApk = apk; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - return; // not installed - } + app.installedApk = getInstalledApk(); } - Installer installer = InstallerFactory.create(this, apk); + + Installer installer = InstallerFactory.create(this, app.installedApk); Intent intent = installer.getUninstallScreen(); if (intent != null) { // uninstall screen required 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 f43b0eeb0..d00250966 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java @@ -6,6 +6,7 @@ import android.database.Cursor; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.NonNull; import org.fdroid.fdroid.RepoXMLHandler; import org.fdroid.fdroid.Utils; @@ -80,6 +81,44 @@ 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. + repo = 0; + } + public Apk(Cursor cursor) { checkCursorPosition(cursor); diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java new file mode 100644 index 000000000..ec9aec9fd --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java @@ -0,0 +1,90 @@ +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 index 4e777a27f..13088dcbf 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -9,6 +9,7 @@ import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; +import android.support.annotation.Nullable; import android.util.Log; import org.fdroid.fdroid.R; @@ -53,6 +54,24 @@ public class InstalledAppProvider extends FDroidProvider { return cachedInfo; } + @Nullable + public static InstalledApp findByPackageName(Context context, String packageName) { + Cursor cursor = context.getContentResolver().query(getAppUri(packageName), Cols.ALL, 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";