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) {