From 74d1c9521d75e6157fc9f60e5deffda777755975 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 11 Apr 2016 14:17:50 -0400 Subject: [PATCH] put up a notification for each completed download This makes background installs and updates a lot easier --- .../fdroid/net/DownloaderServiceTest.java | 2 +- app/src/main/AndroidManifest.xml | 3 + .../java/org/fdroid/fdroid/AppDetails.java | 4 +- .../java/org/fdroid/fdroid/UpdateService.java | 2 +- .../main/java/org/fdroid/fdroid/Utils.java | 12 ++- .../fdroid/fdroid/installer/Installer.java | 7 +- .../fdroid/net/DownloadCompleteService.java | 88 +++++++++++++++++++ .../fdroid/fdroid/net/DownloaderService.java | 14 ++- .../views/swap/SwapWorkflowActivity.java | 8 +- app/src/main/res/values/strings.xml | 2 + 10 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/net/DownloadCompleteService.java diff --git a/app/src/androidTest/java/org/fdroid/fdroid/net/DownloaderServiceTest.java b/app/src/androidTest/java/org/fdroid/fdroid/net/DownloaderServiceTest.java index 7803ad070..d859d7c0d 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/net/DownloaderServiceTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/net/DownloaderServiceTest.java @@ -34,7 +34,7 @@ public class DownloaderServiceTest extends ServiceTestCase { } }, new IntentFilter(Downloader.ACTION_PROGRESS)); for (String url : urls) { - DownloaderService.queue(getContext(), url); + DownloaderService.queue(getContext(), null, url); } Thread.sleep(30000); } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3610320ea..0f2e2e03e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -442,6 +442,9 @@ + diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index 9312efa85..a9e57447b 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -511,7 +511,7 @@ public class AppDetails extends AppCompatActivity { public void onReceive(Context context, Intent intent) { File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); try { - installer.installPackage(localFile, app.packageName); + installer.installPackage(localFile, app.packageName, intent.getDataString()); } catch (InstallFailedException e) { Log.e(TAG, "Android not compatible with this Installer!", e); } @@ -869,7 +869,7 @@ public class AppDetails extends AppCompatActivity { activeDownloadUrlString = urlString; registerDownloaderReceivers(); headerFragment.startProgress(); - DownloaderService.queue(this, activeDownloadUrlString); + DownloaderService.queue(this, apk.packageName, activeDownloadUrlString); } public 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 613436fa1..f27dcb3aa 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -496,7 +496,7 @@ public class UpdateService extends IntentService implements ProgressListener { ApkProvider.DataColumns.NAME, }); String urlString = Utils.getApkUrl(repoAddress, apk); - DownloaderService.queue(this, urlString); + DownloaderService.queue(this, app.packageName, urlString); cursor.moveToNext(); } } diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 94ae4ed64..3b58681dc 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -66,6 +66,7 @@ import java.util.Formatter; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.zip.Adler32; public final class Utils { @@ -321,7 +322,7 @@ public final class Utils { * This location is only for caching, do not install directly from this location * because if the file is on the External Storage, any other app could swap out * the APK while the install was in process, allowing malware to install things. - * Using {@link org.fdroid.fdroid.installer.Installer#installPackage(File, String)} + * Using {@link org.fdroid.fdroid.installer.Installer#installPackage(File, String, String)} * is fine since that does the right thing. */ public static SanitizedFile getApkCacheDir(Context context) { @@ -412,6 +413,15 @@ public final class Utils { return repoAddress + "/" + apk.apkName.replace(" ", "%20"); } + /** + * This generates a unique, reproducible ID for notifications related to {@code urlString} + */ + public static int getApkUrlNotificationId(String urlString) { + Adler32 checksum = new Adler32(); + checksum.update(urlString.getBytes()); + return (int) checksum.getValue(); + } + public static final class CommaSeparatedList implements Iterable { private final String value; 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 092f62e41..bd85b71f0 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java @@ -20,6 +20,7 @@ package org.fdroid.fdroid.installer; import android.app.Activity; +import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -170,7 +171,8 @@ public abstract class Installer { /** * This is the safe, single point of entry for submitting an APK file to be installed. */ - public void installPackage(File apkFile, String packageName) throws InstallFailedException { + public void installPackage(File apkFile, String packageName, String urlString) + throws InstallFailedException { SanitizedFile apkToInstall = null; try { Map attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath()); @@ -228,6 +230,9 @@ public abstract class Installer { FileCompat.setReadable(apkToInstall, true, false); installPackageInternal(apkToInstall); + NotificationManager nm = (NotificationManager) + mContext.getSystemService(Context.NOTIFICATION_SERVICE); + nm.cancel(Utils.getApkUrlNotificationId(urlString)); } catch (NumberFormatException | NoSuchAlgorithmException | IOException e) { throw new InstallFailedException(e); } catch (ClassCastException e) { diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloadCompleteService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloadCompleteService.java new file mode 100644 index 000000000..6574a76fd --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloadCompleteService.java @@ -0,0 +1,88 @@ +package org.fdroid.fdroid.net; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Process; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; +import android.text.TextUtils; + +import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; + +public class DownloadCompleteService extends IntentService { + private static final String TAG = "DownloadCompleteService"; + + private static final String ACTION_NOTIFY = "org.fdroid.fdroid.net.action.NOTIFY"; + private static final String EXTRA_PACKAGE_NAME = "org.fdroid.fdroid.net.extra.PACKAGE_NAME"; + + public DownloadCompleteService() { + super("DownloadCompleteService"); + } + + public static void notify(Context context, String packageName, String urlString) { + Intent intent = new Intent(context, DownloadCompleteService.class); + intent.setAction(ACTION_NOTIFY); + intent.setData(Uri.parse(urlString)); + intent.putExtra(EXTRA_PACKAGE_NAME, packageName); + context.startService(intent); + } + + @Override + protected void onHandleIntent(Intent intent) { + Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); + if (intent != null) { + final String action = intent.getAction(); + if (!ACTION_NOTIFY.equals(action)) { + Utils.debugLog(TAG, "intent action is not ACTION_NOTIFY"); + return; + } + String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + if (TextUtils.isEmpty(packageName)) { + Utils.debugLog(TAG, "intent is missing EXTRA_PACKAGE_NAME"); + return; + } + + String title; + try { + PackageManager pm = getPackageManager(); + title = String.format(getString(R.string.tap_to_update_format), + pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0))); + } catch (PackageManager.NameNotFoundException e) { + App app = AppProvider.Helper.findByPackageName(getContentResolver(), packageName, + new String[]{ + AppProvider.DataColumns.NAME, + }); + title = String.format(getString(R.string.tap_to_install_format), app.name); + } + + Intent notifyIntent = new Intent(this, AppDetails.class); + notifyIntent.putExtra(AppDetails.EXTRA_APPID, packageName); + TaskStackBuilder stackBuilder = TaskStackBuilder + .create(this) + .addParentStack(AppDetails.class) + .addNextIntent(notifyIntent); + int requestCode = Utils.getApkUrlNotificationId(intent.getDataString()); + PendingIntent pendingIntent = stackBuilder.getPendingIntent(requestCode, + PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Builder builder = + new NotificationCompat.Builder(this) + .setAutoCancel(true) + .setContentTitle(title) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setContentIntent(pendingIntent) + .setContentText(getString(R.string.tap_to_install)); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.notify(Utils.getApkUrlNotificationId(intent.getDataString()), builder.build()); + } + } +} 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 f83874d49..25d6aaed8 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java @@ -72,6 +72,8 @@ import java.util.HashMap; public class DownloaderService extends Service { public static final String TAG = "DownloaderService"; + static final String EXTRA_PACKAGE_NAME = "org.fdroid.fdroid.net.DownloaderService.extra.PACKAGE_NAME"; + private static final String ACTION_QUEUE = "org.fdroid.fdroid.net.DownloaderService.action.QUEUE"; private static final String ACTION_CANCEL = "org.fdroid.fdroid.net.DownloaderService.action.CANCEL"; @@ -216,6 +218,8 @@ public class DownloaderService extends Service { }); downloader.download(); sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile); + DownloadCompleteService.notify(this, intent.getStringExtra(EXTRA_PACKAGE_NAME), + intent.getDataString()); } catch (InterruptedException e) { sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile); } catch (IOException e) { @@ -250,14 +254,18 @@ public class DownloaderService extends Service { * All notifications are sent as an {@link Intent} via local broadcasts to be received by * * @param context - * @param urlString The URL to add to the download queue + * @param packageName The packageName of the app being downloaded + * @param urlString The URL to add to the download queue * @see #cancel(Context, String) */ - public static void queue(Context context, String urlString) { + public static void queue(Context context, String packageName, String urlString) { Log.i(TAG, "queue " + urlString); Intent intent = new Intent(context, DownloaderService.class); intent.setAction(ACTION_QUEUE); intent.setData(Uri.parse(urlString)); + if (!TextUtils.isEmpty(EXTRA_PACKAGE_NAME)) { + intent.putExtra(EXTRA_PACKAGE_NAME, packageName); + } context.startService(intent); } @@ -268,7 +276,7 @@ public class DownloaderService extends Service { * * @param context * @param urlString The URL to remove from the download queue - * @see #queue(Context, String) + * @see #queue(Context, String, String) */ public static void cancel(Context context, String urlString) { Log.i(TAG, "cancel " + urlString); 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 219f4c0b4..2586224be 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 @@ -788,15 +788,15 @@ public class SwapWorkflowActivity extends AppCompatActivity { @Override public void onReceive(Context context, Intent intent) { String path = intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH); - handleDownloadComplete(new File(path), app.packageName); + handleDownloadComplete(new File(path), app.packageName, intent.getDataString()); } }; localBroadcastManager.registerReceiver(downloadCompleteReceiver, DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE)); - DownloaderService.queue(this, urlString); + DownloaderService.queue(this, app.packageName, urlString); } - private void handleDownloadComplete(File apkFile, String packageName) { + private void handleDownloadComplete(File apkFile, String packageName, String urlString) { try { Installer.getActivityInstaller(this, new Installer.InstallerCallback() { @@ -811,7 +811,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { public void onError(int operation, int errorCode) { // TODO: Boo! } - }).installPackage(apkFile, packageName); + }).installPackage(apkFile, packageName, urlString); localBroadcastManager.unregisterReceiver(downloadCompleteReceiver); } catch (Installer.InstallFailedException e) { // TODO: Handle exception properly diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d99f819e6..074fd531d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -341,6 +341,8 @@ Swapping not enabled Before swapping, your device must be made visible. + Tap to install %s + Tap to update %s Do you want to install this application? It will get access to: Do you want to install this application?