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.
This commit is contained in:
parent
37ba565f5b
commit
67e66a7b0c
@ -447,6 +447,9 @@
|
||||
android:exported="false" />
|
||||
<service android:name=".net.WifiStateChangeService" />
|
||||
<service android:name=".localrepo.SwapService" />
|
||||
<service
|
||||
android:name=".installer.InstallManagerService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p><ul>
|
||||
* <li>for a {@link Uri} ID, use {@code Uri}, {@link Intent#getData()}
|
||||
* <li>for a {@code String} ID, use {@code urlString}, {@link Uri#toString()}, or
|
||||
* {@link Intent#getDataString()}
|
||||
* <li>for an {@code int} ID, use {@link String#hashCode()}
|
||||
* </ul></p>
|
||||
*/
|
||||
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<String, Apk> ACTIVE_APKS = new HashMap<String, Apk>(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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user