Merge branch 'sort-out-install-uninstall-events' into 'master'
Sort out install uninstall events Closes #1311, #1031, #1271, #1435, and #1145 See merge request fdroid/fdroidclient!710
This commit is contained in:
commit
42424ad902
@ -280,9 +280,6 @@
|
||||
android:name=".data.InstalledAppProviderService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name=".AppUpdateStatusService"
|
||||
android:exported="false"/>
|
||||
|
||||
|
||||
<!-- Warning: Please add all new services to HidingManager -->
|
||||
|
@ -488,15 +488,15 @@ public class AppDetails2 extends AppCompatActivity
|
||||
case Downloading:
|
||||
if (newStatus.progressMax == 0) {
|
||||
// The first progress notification we get telling us our status is "Downloading"
|
||||
adapter.setProgress(-1, -1, R.string.download_pending);
|
||||
adapter.setIndeterminateProgress(R.string.download_pending);
|
||||
} else {
|
||||
adapter.setProgress(newStatus.progressCurrent, newStatus.progressMax, 0);
|
||||
adapter.setProgress(newStatus.progressCurrent, newStatus.progressMax);
|
||||
}
|
||||
break;
|
||||
|
||||
case ReadyToInstall:
|
||||
if (justReceived) {
|
||||
adapter.clearProgress();
|
||||
adapter.setIndeterminateProgress(R.string.installing);
|
||||
localBroadcastManager.registerReceiver(installReceiver,
|
||||
Installer.getInstallIntentFilter(Uri.parse(newStatus.getUniqueKey())));
|
||||
}
|
||||
@ -517,6 +517,9 @@ public class AppDetails2 extends AppCompatActivity
|
||||
break;
|
||||
|
||||
case Installing:
|
||||
adapter.setIndeterminateProgress(R.string.installing);
|
||||
break;
|
||||
|
||||
case Installed:
|
||||
case UpdateAvailable:
|
||||
case InstallError:
|
||||
@ -553,7 +556,7 @@ public class AppDetails2 extends AppCompatActivity
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case Installer.ACTION_INSTALL_STARTED:
|
||||
adapter.setProgress(-1, -1, R.string.installing);
|
||||
adapter.setIndeterminateProgress(R.string.installing);
|
||||
break;
|
||||
case Installer.ACTION_INSTALL_COMPLETE:
|
||||
adapter.clearProgress();
|
||||
@ -625,7 +628,7 @@ public class AppDetails2 extends AppCompatActivity
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case Installer.ACTION_UNINSTALL_STARTED:
|
||||
adapter.setProgress(-1, -1, R.string.uninstalling);
|
||||
adapter.setIndeterminateProgress(R.string.uninstalling);
|
||||
break;
|
||||
case Installer.ACTION_UNINSTALL_COMPLETE:
|
||||
adapter.clearProgress();
|
||||
@ -762,18 +765,29 @@ public class AppDetails2 extends AppCompatActivity
|
||||
installApk(apkToInstall);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall the app from the current screen. Since there are many ways
|
||||
* to uninstall an app, including from Google Play, {@code adb uninstall},
|
||||
* or Settings -> Apps, this method cannot ever be sure that the app isn't
|
||||
* already being uninstalled. So it needs to check that we can actually
|
||||
* get info on the installed app, otherwise, just call it interrupted and
|
||||
* quit.
|
||||
*
|
||||
* @see <a href="https://gitlab.com/fdroid/fdroidclient/issues/1435">issue #1435</a>
|
||||
*/
|
||||
@Override
|
||||
public void uninstallApk() {
|
||||
Apk apk = app.installedApk;
|
||||
if (apk == null) {
|
||||
// TODO ideally, app would be refreshed immediately after install, then this
|
||||
// workaround would be unnecessary - unless it is a media file
|
||||
apk = app.getMediaApkifInstalled(getApplicationContext());
|
||||
if (apk == null) {
|
||||
// When the app isn't a media file - the above workaround refers to this.
|
||||
apk = app.getInstalledApk(this);
|
||||
if (apk == null) {
|
||||
throw new IllegalStateException("Couldn't find installed apk for " + app.packageName);
|
||||
Log.d(TAG, "Couldn't find installed apk for " + app.packageName);
|
||||
Toast.makeText(this, R.string.uninstall_error_unknown, Toast.LENGTH_SHORT).show();
|
||||
uninstallReceiver.onReceive(this, new Intent(Installer.ACTION_UNINSTALL_INTERRUPTED));
|
||||
return;
|
||||
}
|
||||
}
|
||||
app.installedApk = apk;
|
||||
|
@ -18,6 +18,7 @@ import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.installer.ErrorDialogActivity;
|
||||
import org.fdroid.fdroid.installer.InstallManagerService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@ -41,7 +42,7 @@ import java.util.Map;
|
||||
*/
|
||||
public final class AppUpdateStatusManager {
|
||||
|
||||
private static final String TAG = "AppUpdateStatusManager";
|
||||
public static final String TAG = "AppUpdateStatusManager";
|
||||
|
||||
/**
|
||||
* Broadcast when:
|
||||
@ -90,7 +91,6 @@ public final class AppUpdateStatusManager {
|
||||
private static final String LOGTAG = "AppUpdateStatusManager";
|
||||
|
||||
public enum Status {
|
||||
PendingDownload,
|
||||
DownloadInterrupted,
|
||||
UpdateAvailable,
|
||||
Downloading,
|
||||
@ -125,6 +125,11 @@ public final class AppUpdateStatusManager {
|
||||
this.intent = intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the unique ID used to represent this specific package's install process
|
||||
* also known as {@code urlString}.
|
||||
* @see org.fdroid.fdroid.installer.InstallManagerService
|
||||
*/
|
||||
public String getUniqueKey() {
|
||||
return apk.getUrl();
|
||||
}
|
||||
@ -196,15 +201,9 @@ public final class AppUpdateStatusManager {
|
||||
private final HashMap<String, AppUpdateStatus> appMapping = new HashMap<>();
|
||||
private boolean isBatchUpdating;
|
||||
|
||||
/**
|
||||
* @see #isPendingInstall(String)
|
||||
*/
|
||||
private final SharedPreferences apksPendingInstall;
|
||||
|
||||
private AppUpdateStatusManager(Context context) {
|
||||
this.context = context;
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext());
|
||||
apksPendingInstall = context.getSharedPreferences("apks-pending-install", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public void removeAllByRepo(Repo repo) {
|
||||
@ -261,6 +260,10 @@ public final class AppUpdateStatusManager {
|
||||
entry.intent = intent;
|
||||
setEntryContentIntentIfEmpty(entry);
|
||||
notifyChange(entry, isStatusUpdate);
|
||||
|
||||
if (status == Status.Installed) {
|
||||
InstallManagerService.removePendingInstall(context, entry.getUniqueKey());
|
||||
}
|
||||
}
|
||||
|
||||
private void addApkInternal(@NonNull Apk apk, @NonNull Status status, PendingIntent intent) {
|
||||
@ -269,6 +272,10 @@ public final class AppUpdateStatusManager {
|
||||
setEntryContentIntentIfEmpty(entry);
|
||||
appMapping.put(entry.getUniqueKey(), entry);
|
||||
notifyAdd(entry);
|
||||
|
||||
if (status == Status.Installed) {
|
||||
InstallManagerService.removePendingInstall(context, entry.getUniqueKey());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyChange(String reason) {
|
||||
@ -370,8 +377,15 @@ public final class AppUpdateStatusManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an APK from being tracked, since it is now considered {@link Status#Installed}
|
||||
*
|
||||
* @param key the unique ID for the install process, also called {@code urlString}
|
||||
* @see org.fdroid.fdroid.installer.InstallManagerService
|
||||
*/
|
||||
public void removeApk(String key) {
|
||||
synchronized (appMapping) {
|
||||
InstallManagerService.removePendingInstall(context, key);
|
||||
AppUpdateStatus entry = appMapping.remove(key);
|
||||
if (entry != null) {
|
||||
Utils.debugLog(LOGTAG, "Remove APK " + entry.apk.apkName);
|
||||
@ -426,6 +440,8 @@ public final class AppUpdateStatusManager {
|
||||
entry.errorText = errorText;
|
||||
entry.intent = getAppErrorIntent(entry);
|
||||
notifyChange(entry, false);
|
||||
|
||||
InstallManagerService.removePendingInstall(context, entry.getUniqueKey());
|
||||
}
|
||||
}
|
||||
|
||||
@ -541,55 +557,4 @@ public final class AppUpdateStatusManager {
|
||||
errorDialogIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that this could technically be made private and automatically invoked when
|
||||
* {@link #addApk(Apk, Status, PendingIntent)} is called, but that would greatly reduce
|
||||
* the maintainability of this class. Right now it is used by two clients: the notification
|
||||
* manager, and the Updates tab. They have different requirements, with the Updates information
|
||||
* being more permanent than the notification info. As such, the different clients should be
|
||||
* aware of their requirements when invoking general-sounding methods like "addApk()", rather
|
||||
* than this class trying to second-guess why they added an apk.
|
||||
*
|
||||
* @see #isPendingInstall(String)
|
||||
*/
|
||||
public void markAsPendingInstall(String urlString) {
|
||||
AppUpdateStatus entry = get(urlString);
|
||||
if (entry != null) {
|
||||
Utils.debugLog(TAG, "Marking " + entry.apk.packageName + " as pending install.");
|
||||
apksPendingInstall.edit().putBoolean(entry.apk.hash, true).apply();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #markAsNoLongerPendingInstall(AppUpdateStatus)
|
||||
* @see #isPendingInstall(String)
|
||||
*/
|
||||
public void markAsNoLongerPendingInstall(String urlString) {
|
||||
AppUpdateStatus entry = get(urlString);
|
||||
if (entry != null) {
|
||||
markAsNoLongerPendingInstall(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #markAsNoLongerPendingInstall(AppUpdateStatus)
|
||||
* @see #isPendingInstall(String)
|
||||
*/
|
||||
public void markAsNoLongerPendingInstall(@NonNull AppUpdateStatus entry) {
|
||||
Utils.debugLog(TAG, "Marking " + entry.apk.packageName + " as NO LONGER pending install.");
|
||||
apksPendingInstall.edit().remove(entry.apk.hash).apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep track of the list of apks for which an install was initiated (i.e. a download + install).
|
||||
* This is used when F-Droid starts, so that it can look through the cached apks and decide whether
|
||||
* the presence of a .apk file means we should tell the user to press "Install" to complete the
|
||||
* process, or whether it is purely there because it was installed some time ago and is no longer
|
||||
* needed.
|
||||
*/
|
||||
public boolean isPendingInstall(String apkHash) {
|
||||
return apksPendingInstall.contains(apkHash);
|
||||
}
|
||||
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.InstalledAppProviderService;
|
||||
import org.fdroid.fdroid.installer.ApkCache;
|
||||
import org.fdroid.fdroid.installer.InstallManagerService;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Scans the list of downloaded .apk files in the cache. This info is used to
|
||||
* determine if an APK is ready to install. When a valid .apk file is found,
|
||||
* this checks whether that APK is already installed, and whether the user's
|
||||
* install request is still active. If all those are true, then this tells the
|
||||
* {@link AppUpdateStatusManager} that the APK is
|
||||
* {@link AppUpdateStatusManager.Status#ReadyToInstall}. This is an
|
||||
* {@link IntentService} so as to run on a background thread, as it hits the
|
||||
* disk a bit to figure out the hash of each downloaded file.
|
||||
*/
|
||||
public class AppUpdateStatusService extends IntentService {
|
||||
|
||||
private static final String TAG = "AppUpdateStatusService";
|
||||
|
||||
/**
|
||||
* Queue up a background scan of all downloaded apk files to see if we should notify the user
|
||||
* that they are ready to install.
|
||||
*/
|
||||
public static void scanDownloadedApks(Context context) {
|
||||
context.startService(new Intent(context, AppUpdateStatusService.class));
|
||||
}
|
||||
|
||||
public AppUpdateStatusService() {
|
||||
super("AppUpdateStatusService");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(@Nullable Intent intent) {
|
||||
Utils.debugLog(TAG, "Scanning apk cache to see if we need to prompt the user to install any apks.");
|
||||
File cacheDir = ApkCache.getApkCacheDir(this);
|
||||
if (cacheDir == null) {
|
||||
return;
|
||||
}
|
||||
String[] cacheDirList = cacheDir.list();
|
||||
if (cacheDirList == null) {
|
||||
return;
|
||||
}
|
||||
List<Apk> apksReadyToInstall = new ArrayList<>();
|
||||
for (String repoDirName : cacheDirList) {
|
||||
File repoDir = new File(cacheDir, repoDirName);
|
||||
String[] apks = repoDir.list();
|
||||
if (apks == null) {
|
||||
continue;
|
||||
}
|
||||
for (String apkFileName : apks) {
|
||||
Apk apk = processDownloadedApk(new File(repoDir, apkFileName));
|
||||
if (apk != null) {
|
||||
PackageInfo packageInfo = Utils.getPackageInfo(this, apk.packageName);
|
||||
if (packageInfo == null || packageInfo.versionCode != apk.versionCode) {
|
||||
Utils.debugLog(TAG, "Marking downloaded apk " + apk.apkName + " as ReadyToInstall");
|
||||
apksReadyToInstall.add(apk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (apksReadyToInstall.size() > 0) {
|
||||
AppUpdateStatusManager.getInstance(this).addApks(apksReadyToInstall,
|
||||
AppUpdateStatusManager.Status.ReadyToInstall);
|
||||
InstallManagerService.managePreviouslyDownloadedApks(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that {@param apkPath} is a valid apk which the user intends to install.
|
||||
* If it is corrupted to the point where {@link PackageManager} can't read it, doesn't match the hash of any apk
|
||||
* we know about in our database, is not pending install, or is already installed, then it will return null.
|
||||
*/
|
||||
@Nullable
|
||||
private Apk processDownloadedApk(File apkPath) {
|
||||
Utils.debugLog(TAG, "Checking " + apkPath);
|
||||
|
||||
// Overly defensive checking for existence. One would think that the file exists at this point,
|
||||
// because we got it from the result of File#list() earlier. However, this has proven to not be
|
||||
// sufficient, and by the time we get here we are often hitting a non-existent file.
|
||||
// This may be due to the fact that the loop checking each file in the cache takes a long time to execute.
|
||||
// If the number of apps in the cache is large, it can take 10s of seconds to complete. In such
|
||||
// cases, it is possible that Android has cleared up some files in the cache to make space in
|
||||
// the meantime.
|
||||
//
|
||||
// This is all just a hypothesis about what may have caused
|
||||
// https://gitlab.com/fdroid/fdroidclient/issues/1172
|
||||
if (!apkPath.exists()) {
|
||||
Log.i(TAG, "Was going to check " + apkPath + ", but it has since been removed from the cache.");
|
||||
return null;
|
||||
}
|
||||
|
||||
PackageInfo downloadedInfo = getPackageManager().getPackageArchiveInfo(apkPath.getAbsolutePath(),
|
||||
PackageManager.GET_GIDS);
|
||||
if (downloadedInfo == null) {
|
||||
Log.i(TAG, "Skipping " + apkPath + " because PackageManager was unable to read it.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Utils.debugLog(TAG, "Found package for " + downloadedInfo.packageName + ':' + downloadedInfo.versionCode
|
||||
+ ", checking its hash to see if it downloaded correctly.");
|
||||
Apk downloadedApk = findApkMatchingHash(apkPath);
|
||||
if (downloadedApk == null) {
|
||||
Log.i(TAG, "Either the apk wasn't downloaded fully, or the repo it came from has been disabled. "
|
||||
+ "Either way, not notifying the user about it.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!AppUpdateStatusManager.getInstance(this).isPendingInstall(downloadedApk.hash)) {
|
||||
Log.i(TAG, downloadedApk.packageName + ':' + downloadedApk.versionCode
|
||||
+ " is NOT pending install, probably just left over from a previous install.");
|
||||
return null;
|
||||
}
|
||||
|
||||
PackageInfo info = Utils.getPackageInfo(this, downloadedApk.packageName);
|
||||
if (info != null) {
|
||||
File pathToInstalled = InstalledAppProviderService.getPathToInstalledApk(info);
|
||||
if (pathToInstalled != null && pathToInstalled.canRead() &&
|
||||
pathToInstalled.length() == downloadedApk.size && // Check size before hash for performance.
|
||||
TextUtils.equals(Utils.getBinaryHash(pathToInstalled, "sha256"), downloadedApk.hash)) {
|
||||
Log.i(TAG, downloadedApk.packageName
|
||||
+ " is pending install, but we already have the correct version installed.");
|
||||
AppUpdateStatusManager.getInstance(this).markAsNoLongerPendingInstall(downloadedApk.getUrl());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Utils.debugLog(TAG, downloadedApk.packageName + ':' + downloadedApk.versionCode
|
||||
+ " is pending install, so we need to notify the user about installing it.");
|
||||
return downloadedApk;
|
||||
}
|
||||
|
||||
/**
|
||||
* There could be multiple apks with the same hash, provided by different repositories.
|
||||
* This method looks for all matching records in the database. It then asks each of these
|
||||
* {@link Apk} instances where they expect to be downloaded. If they expect to be downloaded
|
||||
* to {@param apkPath} then that instance is returned.
|
||||
* <p>
|
||||
* If no files have a matching hash, or only those which don't belong to the correct repo, then
|
||||
* this will return null. This method needs to do its own check whether the file exists,
|
||||
* since files can be deleted from the cache at any time without warning.
|
||||
*/
|
||||
@Nullable
|
||||
private Apk findApkMatchingHash(File apkPath) {
|
||||
if (!apkPath.canRead()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// NOTE: This presumes SHA256 is the only supported hash. It seems like that is an assumption
|
||||
// in more than one place in the F-Droid client. If this becomes a problem in the future, we
|
||||
// can query the Apk table for `SELECT DISTINCT hashType FROM fdroid_apk` and then we can just
|
||||
// try each of the hash types that have been specified in the metadata. Seems a bit overkill
|
||||
// at the time of writing though.
|
||||
String hash = Utils.getBinaryHash(apkPath, "sha256");
|
||||
|
||||
List<Apk> apksMatchingHash = ApkProvider.Helper.findApksByHash(this, hash);
|
||||
Utils.debugLog(TAG, "Found " + apksMatchingHash.size() + " apk(s) matching the hash " + hash);
|
||||
|
||||
for (Apk apk : apksMatchingHash) {
|
||||
if (apkPath.equals(ApkCache.getApkDownloadPath(this, Uri.parse(apk.getUrl())))) {
|
||||
return apk;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -372,7 +372,6 @@ public class FDroidApp extends Application {
|
||||
}
|
||||
|
||||
InstalledAppProviderService.compareToPackageManager(this);
|
||||
AppUpdateStatusService.scanDownloadedApks(this);
|
||||
|
||||
// If the user changes the preference to do with filtering rooted apps,
|
||||
// it is easier to just notify a change in the app provider,
|
||||
|
@ -45,6 +45,12 @@ class NotificationHelper {
|
||||
private static final int MAX_UPDATES_TO_SHOW = 5;
|
||||
private static final int MAX_INSTALLED_TO_SHOW = 10;
|
||||
|
||||
/**
|
||||
* Unique ID used to represent this specific package's install process,
|
||||
* including {@link Notification}s, also known as {@code urlString}.
|
||||
*
|
||||
* @see org.fdroid.fdroid.installer.InstallManagerService
|
||||
*/
|
||||
static final String EXTRA_NOTIFICATION_KEY = "key";
|
||||
private static final String GROUP_UPDATES = "updates";
|
||||
private static final String GROUP_INSTALLED = "installed";
|
||||
|
@ -12,7 +12,6 @@ import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.JobIntentService;
|
||||
import android.util.Log;
|
||||
import org.acra.ACRA;
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
|
||||
import rx.functions.Action1;
|
||||
@ -235,22 +234,6 @@ public class InstalledAppProviderService extends JobIntentService {
|
||||
try {
|
||||
String hashType = "sha256";
|
||||
String hash = Utils.getBinaryHash(apk, hashType);
|
||||
|
||||
// Ensure that we no longer notify the user that this apk successfully
|
||||
// downloaded and is now ready to be installed. Used to be handled only
|
||||
// by InstallManagerService after receiving ACTION_INSTALL_COMPLETE, but
|
||||
// that doesn't work for F-Droid itself, which never receives that action.
|
||||
for (Apk apkInRepo : ApkProvider.Helper.findApksByHash(this, hash)) {
|
||||
|
||||
Utils.debugLog(TAG, "Noticed that " + apkInRepo.apkName +
|
||||
" version " + apkInRepo.versionName + " was installed," +
|
||||
" so marking as no longer pending install");
|
||||
|
||||
AppUpdateStatusManager.getInstance(this)
|
||||
.markAsNoLongerPendingInstall(apkInRepo.getUrl());
|
||||
|
||||
}
|
||||
|
||||
insertAppIntoDb(this, packageInfo, hashType, hash);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Utils.debugLog(TAG, e.getMessage());
|
||||
|
@ -63,8 +63,6 @@ public class DefaultInstaller extends Installer {
|
||||
|
||||
@Override
|
||||
protected void uninstallPackage() {
|
||||
sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED);
|
||||
|
||||
Intent uninstallIntent = new Intent(context, DefaultInstallerActivity.class);
|
||||
uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE);
|
||||
uninstallIntent.putExtra(Installer.EXTRA_APK, apk);
|
||||
|
@ -78,8 +78,6 @@ public class ExtensionInstaller extends Installer {
|
||||
|
||||
@Override
|
||||
protected void uninstallPackage() {
|
||||
sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED);
|
||||
|
||||
Intent uninstallIntent = new Intent(context, InstallExtensionDialogActivity.class);
|
||||
uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL);
|
||||
|
||||
|
@ -68,8 +68,6 @@ public class FileInstaller extends Installer {
|
||||
|
||||
@Override
|
||||
protected void uninstallPackage() {
|
||||
sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED);
|
||||
|
||||
Intent uninstallIntent = new Intent(context, FileInstallerActivity.class);
|
||||
uninstallIntent.setAction(FileInstallerActivity.ACTION_UNINSTALL_FILE);
|
||||
uninstallIntent.putExtra(Installer.EXTRA_APK, apk);
|
||||
|
@ -7,9 +7,11 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
@ -40,6 +42,10 @@ import java.io.IOException;
|
||||
* For example, if this {@code InstallManagerService} gets killed, Android will cache
|
||||
* and then redeliver the {@link Intent} for us, which includes all of the data needed
|
||||
* for {@code InstallManagerService} to do its job for the whole lifecycle of an install.
|
||||
* This {@code Service} never stops itself after completing the action, e.g.
|
||||
* {@code {@link #stopSelf(int)}}, so {@code Intent}s are sometimes redelivered even
|
||||
* though they are no longer valid. {@link #onStartCommand(Intent, int, int)} checks
|
||||
* first that the incoming {@code Intent} is not an invalid, redelivered {@code Intent}.
|
||||
* <p>
|
||||
* The canonical URL for the APK file to download is also used as the unique ID to
|
||||
* represent the download itself throughout F-Droid. This follows the model
|
||||
@ -61,6 +67,11 @@ import java.io.IOException;
|
||||
* This also handles downloading OBB "APK Extension" files for any APK that has one
|
||||
* assigned to it. OBB files are queued up for download before the APK so that they
|
||||
* are hopefully in place before the APK starts. That is not guaranteed though.
|
||||
* <p>
|
||||
* There may be multiple, available APK files with the same hash. Although it
|
||||
* is not a security issue to install one or the other, they may have different
|
||||
* metadata to display in the client. Thus, it may result in weirdness if one
|
||||
* has a different name/description/summary, etc).
|
||||
*
|
||||
* @see <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a>
|
||||
*/
|
||||
@ -71,19 +82,11 @@ public class InstallManagerService extends Service {
|
||||
private static final String ACTION_INSTALL = "org.fdroid.fdroid.installer.action.INSTALL";
|
||||
private static final String ACTION_CANCEL = "org.fdroid.fdroid.installer.action.CANCEL";
|
||||
|
||||
/**
|
||||
* The install manager service needs to monitor downloaded apks so that it can wait for a user to
|
||||
* install them and respond accordingly. Usually the thing which starts listening for such events
|
||||
* does so directly after a download is complete. This works great, except when the user then
|
||||
* subsequently closes F-Droid and opens it at a later date. Under these circumstances, a background
|
||||
* service will scan all downloaded apks and notify the user about them. When it does so, the
|
||||
* install manager service needs to add listeners for if the apks get installed.
|
||||
*/
|
||||
private static final String ACTION_MANAGE_DOWNLOADED_APKS = "org.fdroid.fdroid.installer.action.ACTION_MANAGE_DOWNLOADED_APKS";
|
||||
|
||||
private static final String EXTRA_APP = "org.fdroid.fdroid.installer.extra.APP";
|
||||
private static final String EXTRA_APK = "org.fdroid.fdroid.installer.extra.APK";
|
||||
|
||||
private static SharedPreferences pendingInstalls;
|
||||
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
private AppUpdateStatusManager appUpdateStatusManager;
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
@ -119,8 +122,16 @@ public class InstallManagerService extends Service {
|
||||
intentFilter.addDataScheme("package");
|
||||
registerReceiver(broadcastReceiver, intentFilter);
|
||||
running = true;
|
||||
pendingInstalls = getPendingInstalls(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this {@link Service} is stopped, then all of the various
|
||||
* {@link BroadcastReceiver}s need to unregister themselves if they get
|
||||
* called. There can be multiple {@code BroadcastReceiver}s registered,
|
||||
* so it can't be done with a simple call here. So {@link #running} is the
|
||||
* signal to all the existing {@code BroadcastReceiver}s to unregister.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
running = false;
|
||||
@ -145,19 +156,14 @@ public class InstallManagerService extends Service {
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Utils.debugLog(TAG, "onStartCommand " + intent);
|
||||
|
||||
String action = intent.getAction();
|
||||
|
||||
if (ACTION_MANAGE_DOWNLOADED_APKS.equals(action)) {
|
||||
registerInstallerReceiversForDownlaodedApks();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
String urlString = intent.getDataString();
|
||||
if (TextUtils.isEmpty(urlString)) {
|
||||
Utils.debugLog(TAG, "empty urlString, nothing to do");
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
|
||||
if (ACTION_CANCEL.equals(action)) {
|
||||
DownloaderService.cancel(this, urlString);
|
||||
Apk apk = appUpdateStatusManager.getApk(urlString);
|
||||
@ -165,10 +171,14 @@ public class InstallManagerService extends Service {
|
||||
DownloaderService.cancel(this, apk.getPatchObbUrl());
|
||||
DownloaderService.cancel(this, apk.getMainObbUrl());
|
||||
}
|
||||
appUpdateStatusManager.markAsNoLongerPendingInstall(urlString);
|
||||
appUpdateStatusManager.removeApk(urlString);
|
||||
return START_NOT_STICKY;
|
||||
} else if (!ACTION_INSTALL.equals(action)) {
|
||||
} else if (ACTION_INSTALL.equals(action)) {
|
||||
if (!isPendingInstall(urlString)) {
|
||||
Log.i(TAG, "Ignoring INSTALL that is not Pending Install: " + intent);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Ignoring unknown intent action: " + intent);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
@ -204,9 +214,8 @@ public class InstallManagerService extends Service {
|
||||
DownloaderService.setTimeout(FDroidApp.getTimeout());
|
||||
|
||||
appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Downloading, null);
|
||||
appUpdateStatusManager.markAsPendingInstall(urlString);
|
||||
|
||||
registerApkDownloaderReceivers(urlString);
|
||||
registerPackageDownloaderReceivers(urlString);
|
||||
getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256);
|
||||
getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256);
|
||||
|
||||
@ -304,7 +313,11 @@ public class InstallManagerService extends Service {
|
||||
DownloaderService.getIntentFilter(obbUrlString));
|
||||
}
|
||||
|
||||
private void registerApkDownloaderReceivers(String urlString) {
|
||||
/**
|
||||
* Register a {@link BroadcastReceiver} for tracking download progress for a
|
||||
* give {@code urlString}. There can be multiple of these registered at a time.
|
||||
*/
|
||||
private void registerPackageDownloaderReceivers(String urlString) {
|
||||
|
||||
BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
@ -340,7 +353,7 @@ public class InstallManagerService extends Service {
|
||||
appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.ReadyToInstall, null);
|
||||
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
registerInstallerReceivers(downloadUri);
|
||||
registerInstallReceiver(downloadUri);
|
||||
|
||||
Apk apk = appUpdateStatusManager.getApk(urlString);
|
||||
if (apk != null) {
|
||||
@ -348,7 +361,6 @@ public class InstallManagerService extends Service {
|
||||
}
|
||||
break;
|
||||
case Downloader.ACTION_INTERRUPTED:
|
||||
appUpdateStatusManager.markAsNoLongerPendingInstall(urlString);
|
||||
appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE));
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
break;
|
||||
@ -357,7 +369,6 @@ public class InstallManagerService extends Service {
|
||||
DownloaderService.queue(context, FDroidApp.getMirror(mirrorUrlString, repoId), repoId, urlString);
|
||||
DownloaderService.setTimeout(FDroidApp.getTimeout());
|
||||
} catch (IOException e) {
|
||||
appUpdateStatusManager.markAsNoLongerPendingInstall(urlString);
|
||||
appUpdateStatusManager.setDownloadError(urlString, intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE));
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
}
|
||||
@ -373,20 +384,10 @@ public class InstallManagerService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* For each app in the {@link AppUpdateStatusManager.Status#ReadyToInstall} state, setup listeners
|
||||
* so that if the user installs it then we can respond accordingly. This makes sure that whether
|
||||
* the user just finished downloading it, or whether they downloaded it a day ago but have not yet
|
||||
* installed it, we get the same experience upon completing an install.
|
||||
* Register a {@link BroadcastReceiver} for tracking install progress for a
|
||||
* give {@link Uri}. There can be multiple of these registered at a time.
|
||||
*/
|
||||
private void registerInstallerReceiversForDownlaodedApks() {
|
||||
for (AppUpdateStatusManager.AppUpdateStatus appStatus : AppUpdateStatusManager.getInstance(this).getAll()) {
|
||||
if (appStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
|
||||
registerInstallerReceivers(Uri.parse(appStatus.getUniqueKey()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void registerInstallerReceivers(Uri downloadUri) {
|
||||
private void registerInstallReceiver(Uri downloadUri) {
|
||||
|
||||
BroadcastReceiver installReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
@ -402,7 +403,6 @@ public class InstallManagerService extends Service {
|
||||
appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installing, null);
|
||||
break;
|
||||
case Installer.ACTION_INSTALL_COMPLETE:
|
||||
appUpdateStatusManager.markAsNoLongerPendingInstall(downloadUrl);
|
||||
appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installed, null);
|
||||
Apk apkComplete = appUpdateStatusManager.getApk(downloadUrl);
|
||||
|
||||
@ -419,7 +419,6 @@ public class InstallManagerService extends Service {
|
||||
apk = intent.getParcelableExtra(Installer.EXTRA_APK);
|
||||
String errorMessage =
|
||||
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
|
||||
appUpdateStatusManager.markAsNoLongerPendingInstall(downloadUrl);
|
||||
if (!TextUtils.isEmpty(errorMessage)) {
|
||||
appUpdateStatusManager.setApkError(apk, errorMessage);
|
||||
} else {
|
||||
@ -443,13 +442,20 @@ public class InstallManagerService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Install an APK, checking the cache and downloading if necessary before starting the process.
|
||||
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
|
||||
* Install an APK, checking the cache and downloading if necessary before
|
||||
* starting the process. All notifications are sent as an {@link Intent}
|
||||
* via local broadcasts to be received by {@link BroadcastReceiver}s per
|
||||
* {@code urlString}. This also marks a given APK as in the process of
|
||||
* being installed, with the {@code urlString} of the download used as the
|
||||
* unique ID,
|
||||
* <p>
|
||||
* and the file hash used to verify that things are the same.
|
||||
*
|
||||
* @param context this app's {@link Context}
|
||||
*/
|
||||
public static void queue(Context context, App app, Apk apk) {
|
||||
public static void queue(Context context, App app, @NonNull Apk apk) {
|
||||
String urlString = apk.getUrl();
|
||||
putPendingInstall(context, urlString, apk.packageName);
|
||||
Uri downloadUri = Uri.parse(urlString);
|
||||
Installer.sendBroadcastInstall(context, downloadUri, Installer.ACTION_INSTALL_STARTED, apk,
|
||||
null, null);
|
||||
@ -463,15 +469,46 @@ public class InstallManagerService extends Service {
|
||||
}
|
||||
|
||||
public static void cancel(Context context, String urlString) {
|
||||
removePendingInstall(context, urlString);
|
||||
Intent intent = new Intent(context, InstallManagerService.class);
|
||||
intent.setAction(ACTION_CANCEL);
|
||||
intent.setData(Uri.parse(urlString));
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
public static void managePreviouslyDownloadedApks(Context context) {
|
||||
Intent intent = new Intent(context, InstallManagerService.class);
|
||||
intent.setAction(ACTION_MANAGE_DOWNLOADED_APKS);
|
||||
context.startService(intent);
|
||||
/**
|
||||
* Is the APK that matches the provided {@code hash} still waiting to be
|
||||
* installed? This restarts the install process for this APK if it was
|
||||
* interrupted somehow, like if F-Droid was killed before the download
|
||||
* completed, or the device lost power in the middle of the install
|
||||
* process.
|
||||
*/
|
||||
public boolean isPendingInstall(String urlString) {
|
||||
return pendingInstalls.contains(urlString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a given APK as in the process of being installed, with
|
||||
* the {@code urlString} of the download used as the unique ID,
|
||||
* and the file hash used to verify that things are the same.
|
||||
*
|
||||
* @see #isPendingInstall(String)
|
||||
*/
|
||||
public static void putPendingInstall(Context context, String urlString, String packageName) {
|
||||
if (pendingInstalls == null) {
|
||||
pendingInstalls = getPendingInstalls(context);
|
||||
}
|
||||
pendingInstalls.edit().putString(urlString, packageName).apply();
|
||||
}
|
||||
|
||||
public static void removePendingInstall(Context context, String urlString) {
|
||||
if (pendingInstalls == null) {
|
||||
pendingInstalls = getPendingInstalls(context);
|
||||
}
|
||||
pendingInstalls.edit().remove(urlString).apply();
|
||||
}
|
||||
|
||||
private static SharedPreferences getPendingInstalls(Context context) {
|
||||
return context.getSharedPreferences("pending-installs", Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
||||
|
@ -200,6 +200,15 @@ public abstract class Installer {
|
||||
}
|
||||
|
||||
private void sendBroadcastUninstall(String action, PendingIntent pendingIntent, String errorMessage) {
|
||||
sendBroadcastUninstall(context, apk, action, pendingIntent, errorMessage);
|
||||
}
|
||||
|
||||
static void sendBroadcastUninstall(Context context, Apk apk, String action) {
|
||||
sendBroadcastUninstall(context, apk, action, null, null);
|
||||
}
|
||||
|
||||
private static void sendBroadcastUninstall(Context context, Apk apk, String action,
|
||||
PendingIntent pendingIntent, String errorMessage) {
|
||||
Uri uri = Uri.fromParts("package", apk.packageName, null);
|
||||
|
||||
Intent intent = new Intent(action);
|
||||
|
@ -142,6 +142,9 @@ public class InstallerService extends JobIntentService {
|
||||
*/
|
||||
public static void uninstall(Context context, @NonNull Apk apk) {
|
||||
Objects.requireNonNull(apk);
|
||||
|
||||
Installer.sendBroadcastUninstall(context, apk, Installer.ACTION_UNINSTALL_STARTED);
|
||||
|
||||
Intent intent = new Intent(context, InstallerService.class);
|
||||
intent.setAction(ACTION_UNINSTALL);
|
||||
intent.putExtra(Installer.EXTRA_APK, apk);
|
||||
|
@ -355,8 +355,6 @@ public class PrivilegedInstaller extends Installer {
|
||||
|
||||
@Override
|
||||
protected void uninstallPackage() {
|
||||
sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED);
|
||||
|
||||
ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
|
||||
|
@ -34,9 +34,7 @@ import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
@ -204,12 +202,20 @@ public class AppDetailsRecyclerViewAdapter
|
||||
}
|
||||
|
||||
public void clearProgress() {
|
||||
setProgress(0, 0, 0);
|
||||
if (headerView != null) {
|
||||
headerView.clearProgress();
|
||||
}
|
||||
}
|
||||
|
||||
public void setProgress(long bytesDownloaded, long totalBytes, int resIdString) {
|
||||
public void setIndeterminateProgress(int resIdString) {
|
||||
if (headerView != null) {
|
||||
headerView.setProgress(bytesDownloaded, totalBytes, resIdString);
|
||||
headerView.setIndeterminateProgress(resIdString);
|
||||
}
|
||||
}
|
||||
|
||||
public void setProgress(long bytesDownloaded, long totalBytes) {
|
||||
if (headerView != null) {
|
||||
headerView.setProgress(bytesDownloaded, totalBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,21 +366,29 @@ public class AppDetailsRecyclerViewAdapter
|
||||
});
|
||||
}
|
||||
|
||||
public void setProgress(long bytesDownloaded, long totalBytes, int resIdString) {
|
||||
if (bytesDownloaded == 0 && totalBytes == 0) {
|
||||
// Remove progress bar
|
||||
public void clearProgress() {
|
||||
progressLayout.setVisibility(View.GONE);
|
||||
buttonLayout.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
progressBar.setMax(Utils.bytesToKb(totalBytes));
|
||||
progressBar.setProgress(Utils.bytesToKb(bytesDownloaded));
|
||||
progressBar.setIndeterminate(totalBytes == -1);
|
||||
progressLabel.setContentDescription("");
|
||||
if (resIdString != 0) {
|
||||
}
|
||||
|
||||
public void setIndeterminateProgress(int resIdString) {
|
||||
progressLayout.setVisibility(View.VISIBLE);
|
||||
buttonLayout.setVisibility(View.GONE);
|
||||
progressBar.setIndeterminate(true);
|
||||
progressLabel.setText(resIdString);
|
||||
progressLabel.setContentDescription(context.getString(R.string.downloading));
|
||||
progressPercent.setText("");
|
||||
} else if (totalBytes > 0 && bytesDownloaded >= 0) {
|
||||
}
|
||||
|
||||
public void setProgress(long bytesDownloaded, long totalBytes) {
|
||||
progressLayout.setVisibility(View.VISIBLE);
|
||||
buttonLayout.setVisibility(View.GONE);
|
||||
|
||||
progressBar.setMax(Utils.bytesToKb(totalBytes));
|
||||
progressBar.setProgress(Utils.bytesToKb(bytesDownloaded));
|
||||
progressBar.setIndeterminate(totalBytes <= 0);
|
||||
progressLabel.setContentDescription("");
|
||||
if (totalBytes > 0 && bytesDownloaded >= 0) {
|
||||
int percent = Utils.getPercent(bytesDownloaded, totalBytes);
|
||||
progressLabel.setText(Utils.getFriendlySize(bytesDownloaded)
|
||||
+ " / " + Utils.getFriendlySize(totalBytes));
|
||||
@ -386,13 +400,6 @@ public class AppDetailsRecyclerViewAdapter
|
||||
progressLabel.setContentDescription(context.getString(R.string.downloading));
|
||||
progressPercent.setText("");
|
||||
}
|
||||
|
||||
// Make sure it's visible
|
||||
if (progressLayout.getVisibility() != View.VISIBLE) {
|
||||
progressLayout.setVisibility(View.VISIBLE);
|
||||
buttonLayout.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void bindModel() {
|
||||
|
@ -395,7 +395,6 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
|
||||
* There are a bunch of reasons why we would get notified about app statuses.
|
||||
* The ones we are interested in are those which would result in the "items requiring user interaction"
|
||||
* to increase or decrease:
|
||||
* * Bulk updates of ready-to-install-apps (relating to {@link org.fdroid.fdroid.AppUpdateStatusService}.
|
||||
* * Change in status to:
|
||||
* * {@link AppUpdateStatusManager.Status#ReadyToInstall} (Causes the count to go UP by one)
|
||||
* * {@link AppUpdateStatusManager.Status#Installed} (Causes the count to go DOWN by one)
|
||||
|
@ -61,12 +61,6 @@ public class AppStatusListItemController extends AppListItemController {
|
||||
AppUpdateStatusManager manager = AppUpdateStatusManager.getInstance(activity);
|
||||
manager.removeApk(status.getUniqueKey());
|
||||
switch (status.status) {
|
||||
case ReadyToInstall:
|
||||
manager.markAsNoLongerPendingInstall(status);
|
||||
// Do this silently, because it should be pretty obvious based on the context
|
||||
// of a "Ready to install" app being dismissed.
|
||||
break;
|
||||
|
||||
case Downloading:
|
||||
cancelDownload();
|
||||
message = activity.getString(R.string.app_list__dismiss_downloading_app);
|
||||
|
Loading…
x
Reference in New Issue
Block a user