From 67e66a7b0cb79f14917e78edfe9b8b53756d3652 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 6 May 2016 12:27:47 +0200 Subject: [PATCH] InstallManagerService skeleton which checks cache before installing DownloaderService is structured to be as simple as possible, and as tightly matched to the downloading lifecycle as possible, with a single queue for all requests to avoid downloads competing for bandwidth. This does not represent the possibilities of the whole install process. For example, downloading can happen in parallel with checking the cache, and if an APK is fully cached, there is no need for it to go through the DownloaderService queue. This also lays the groundwork towards simplifying DownloaderService even more, by moving the Notification handling to InstallManagerService. That will provide a single place to manage all aspects of the Notifications that has a lifecycle that is longer than the Notifications, unlike an Activity or DownloaderService. --- app/src/main/AndroidManifest.xml | 3 + .../java/org/fdroid/fdroid/AppDetails.java | 5 +- .../java/org/fdroid/fdroid/UpdateService.java | 4 +- .../main/java/org/fdroid/fdroid/Utils.java | 11 ++ .../installer/InstallManagerService.java | 115 ++++++++++++++++++ .../fdroid/fdroid/net/DownloaderService.java | 4 +- .../views/swap/SwapWorkflowActivity.java | 3 +- 7 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java 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) {