Merge branch 'fix-800--npe-uninstalling' into 'master'

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.

See merge request !413
This commit is contained in:
Hans-Christoph Steiner 2016-11-10 08:36:00 +00:00
commit b2b108c39e
4 changed files with 190 additions and 12 deletions

View File

@ -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.
*/
private void uninstallApk() {
Apk apk = app.installedApk;
if (apk == null) {
// TODO ideally, app would be refreshed immediately after install, then this
// workaround would be unnecessary
@NonNull
private Apk getInstalledApk() {
try {
PackageInfo pi = packageManager.getPackageInfo(app.packageName, 0);
apk = ApkProvider.Helper.findApkFromAnyRepo(this, pi.packageName, pi.versionCode);
app.installedApk = apk;
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();
return; // not installed
throw new IllegalStateException("Couldn't find app while installing");
}
}
Installer installer = InstallerFactory.create(this, apk);
/**
* Queue for uninstall based on the instance variable {@link #app}.
*/
private void uninstallApk() {
if (app.installedApk == null) {
// TODO ideally, app would be refreshed immediately after install, then this
// workaround would be unnecessary
app.installedApk = getInstalledApk();
}
Installer installer = InstallerFactory.create(this, app.installedApk);
Intent intent = installer.getUninstallScreen();
if (intent != null) {
// uninstall screen required

View File

@ -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<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.
*
* 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);

View File

@ -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;
}
}

View File

@ -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";