diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ced498306..7b4b8cdd5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -447,6 +447,9 @@
android:exported="false" />
+
diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java
index f63ad894e..abcd19523 100644
--- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java
+++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java
@@ -37,7 +37,6 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.ListFragment;
import android.support.v4.app.NavUtils;
@@ -86,8 +85,8 @@ import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppProvider;
-import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
+import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.Installer.InstallFailedException;
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
@@ -868,7 +867,7 @@ public class AppDetails extends AppCompatActivity {
activeDownloadUrlString = apk.getUrl();
registerDownloaderReceivers();
headerFragment.startProgress();
- DownloaderService.queue(this, apk.packageName, activeDownloadUrlString);
+ InstallManagerService.queue(this, app, apk);
}
private void removeApk(String packageName) {
diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java
index 1be4092fd..0cb83a178 100644
--- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java
+++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java
@@ -48,6 +48,7 @@ import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
+import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
@@ -497,8 +498,7 @@ public class UpdateService extends IntentService implements ProgressListener {
for (int i = 0; i < cursor.getCount(); i++) {
App app = new App(cursor);
Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode);
- String urlString = apk.getUrl();
- DownloaderService.queue(this, app.packageName, urlString);
+ InstallManagerService.queue(this, app, apk);
cursor.moveToNext();
}
cursor.close();
diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java
index e19408a77..fb1178374 100644
--- a/app/src/main/java/org/fdroid/fdroid/Utils.java
+++ b/app/src/main/java/org/fdroid/fdroid/Utils.java
@@ -334,6 +334,17 @@ public final class Utils {
return apkCacheDir;
}
+ /**
+ * Get the full path for where an APK URL will be downloaded into.
+ */
+ public static SanitizedFile getApkDownloadPath(Context context, Uri uri) {
+ File dir = new File(Utils.getApkCacheDir(context), uri.getHost() + "-" + uri.getPort());
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ return new SanitizedFile(dir, uri.getLastPathSegment());
+ }
+
/**
* Recursively delete files in {@code dir} that were last modified
* {@code secondsAgo} seconds ago, e.g. when it was downloaded.
diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java
new file mode 100644
index 000000000..9e651b52b
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java
@@ -0,0 +1,115 @@
+package org.fdroid.fdroid.installer;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.IBinder;
+import android.support.v4.content.LocalBroadcastManager;
+
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.net.Downloader;
+import org.fdroid.fdroid.net.DownloaderService;
+
+import java.io.File;
+import java.util.HashMap;
+
+/**
+ * Manages the whole process when a background update triggers an install or the user
+ * requests an APK to be installed. It handles checking whether the APK is cached,
+ * downloading it, putting up and maintaining a {@link Notification}, and more.
+ *
+ * Data is sent via {@link Intent}s so that Android handles the message queuing
+ * and {@link Service} lifecycle for us, although it adds one layer of redirection
+ * between the static method to send the {@code Intent} and the method to
+ * actually process it.
+ *
+ * The full 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
+ * of {@link Intent#setData(Uri)}, where the core data of an {@code Intent} is
+ * a {@code Uri}. The full download URL is guaranteed to be unique since it
+ * points to files on a filesystem, where there cannot be multiple files with
+ * the same name. This provides a unique ID beyond just {@code packageName}
+ * and {@code versionCode} since there could be different copies of the same
+ * APK on different servers, signed by different keys, or even different builds.
+ *
+ * - for a {@link Uri} ID, use {@code Uri}, {@link Intent#getData()}
+ *
- for a {@code String} ID, use {@code urlString}, {@link Uri#toString()}, or
+ * {@link Intent#getDataString()}
+ *
- for an {@code int} ID, use {@link String#hashCode()}
+ *
+ */
+public class InstallManagerService extends Service {
+ public static final String TAG = "InstallManagerService";
+
+ private static final String ACTION_INSTALL = "org.fdroid.fdroid.InstallManagerService.action.INSTALL";
+
+ /**
+ * The collection of APKs that are actively going through this whole process.
+ */
+ private static final HashMap ACTIVE_APKS = new HashMap(3);
+
+ private LocalBroadcastManager localBroadcastManager;
+
+ /**
+ * This service does not use binding, so no need to implement this method
+ */
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Utils.debugLog(TAG, "creating Service");
+ localBroadcastManager = LocalBroadcastManager.getInstance(this);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Utils.debugLog(TAG, "onStartCommand " + intent);
+ String urlString = intent.getDataString();
+ Apk apk = ACTIVE_APKS.get(urlString);
+ File apkFilePath = Utils.getApkDownloadPath(this, intent.getData());
+ long apkFileSize = apkFilePath.length();
+ if (!apkFilePath.exists() || apkFileSize < apk.size) {
+ Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath);
+ DownloaderService.queue(this, apk.packageName, urlString);
+ } else if (apkFileSize == apk.size) {
+ Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath);
+ sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath);
+ sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath);
+ } else {
+ Utils.debugLog(TAG, " delete and download again " + urlString + " " + apkFilePath);
+ apkFilePath.delete();
+ DownloaderService.queue(this, apk.packageName, urlString);
+ }
+ return START_REDELIVER_INTENT; // if killed before completion, retry Intent
+ }
+
+ private void sendBroadcast(Uri uri, String action, File file) {
+ Intent intent = new Intent(action);
+ intent.setData(uri);
+ intent.putExtra(Downloader.EXTRA_DOWNLOAD_PATH, file.getAbsolutePath());
+ localBroadcastManager.sendBroadcast(intent);
+ }
+
+ /**
+ * 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
+ *
+ * @param context this app's {@link Context}
+ */
+ public static void queue(Context context, App app, Apk apk) {
+ String urlString = apk.getUrl();
+ Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + urlString);
+ ACTIVE_APKS.put(urlString, apk);
+ Intent intent = new Intent(context, InstallManagerService.class);
+ intent.setAction(ACTION_INSTALL);
+ intent.setData(Uri.parse(urlString));
+ context.startService(intent);
+ }
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
index 46205d2e7..e5685ac49 100644
--- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
+++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java
@@ -253,9 +253,7 @@ public class DownloaderService extends Service {
*/
protected void handleIntent(Intent intent) {
final Uri uri = intent.getData();
- File downloadDir = new File(Utils.getApkCacheDir(this), uri.getHost() + "-" + uri.getPort());
- downloadDir.mkdirs();
- final SanitizedFile localFile = new SanitizedFile(downloadDir, uri.getLastPathSegment());
+ final SanitizedFile localFile = Utils.getApkDownloadPath(this, uri);
final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile);
diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
index 2b063872f..6b0b46aaf 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
@@ -41,6 +41,7 @@ import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.NewRepoConfig;
+import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.localrepo.LocalRepoManager;
import org.fdroid.fdroid.localrepo.SwapService;
@@ -788,7 +789,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
};
localBroadcastManager.registerReceiver(downloadCompleteReceiver,
DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE));
- DownloaderService.queue(this, app.packageName, urlString);
+ InstallManagerService.queue(this, app, apk);
}
private void handleDownloadComplete(File apkFile, String packageName, String urlString) {