- * 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
* 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.
+ *
+ * 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 APK Expansion Files
*/
@@ -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,9 +403,8 @@ 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);
+ Apk apkComplete = appUpdateStatusManager.getApk(downloadUrl);
if (apkComplete != null && apkComplete.isApk()) {
try {
@@ -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,
+ *
+ * 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);
}
}
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java
index 38268e983..817f1fa53 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java
@@ -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);
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java
index db214122b..ee47f5050 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java
@@ -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);
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java
index fee625af3..653f32068 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java
@@ -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);
diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java
index a07982285..84bdbf378 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java
@@ -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,38 +366,39 @@ public class AppDetailsRecyclerViewAdapter
});
}
- public void setProgress(long bytesDownloaded, long totalBytes, int resIdString) {
- if (bytesDownloaded == 0 && totalBytes == 0) {
- // Remove progress bar
- 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) {
- progressLabel.setText(resIdString);
- progressLabel.setContentDescription(context.getString(R.string.downloading));
- progressPercent.setText("");
- } else if (totalBytes > 0 && bytesDownloaded >= 0) {
- int percent = Utils.getPercent(bytesDownloaded, totalBytes);
- progressLabel.setText(Utils.getFriendlySize(bytesDownloaded)
- + " / " + Utils.getFriendlySize(totalBytes));
- progressLabel.setContentDescription(context.getString(R.string.app__tts__downloading_progress,
- percent));
- progressPercent.setText(String.format(Locale.ENGLISH, "%d%%", percent));
- } else if (bytesDownloaded >= 0) {
- progressLabel.setText(Utils.getFriendlySize(bytesDownloaded));
- progressLabel.setContentDescription(context.getString(R.string.downloading));
- progressPercent.setText("");
- }
+ public void clearProgress() {
+ progressLayout.setVisibility(View.GONE);
+ buttonLayout.setVisibility(View.VISIBLE);
+ }
- // Make sure it's visible
- if (progressLayout.getVisibility() != View.VISIBLE) {
- progressLayout.setVisibility(View.VISIBLE);
- buttonLayout.setVisibility(View.GONE);
- }
+ 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("");
+ }
+
+ 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));
+ progressLabel.setContentDescription(context.getString(R.string.app__tts__downloading_progress,
+ percent));
+ progressPercent.setText(String.format(Locale.ENGLISH, "%d%%", percent));
+ } else if (bytesDownloaded >= 0) {
+ progressLabel.setText(Utils.getFriendlySize(bytesDownloaded));
+ progressLabel.setContentDescription(context.getString(R.string.downloading));
+ progressPercent.setText("");
}
}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java
index 7350e5c80..fcf90e1c2 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java
@@ -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)
diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java
index 981409eff..4bf93f9e0 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java
@@ -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);