put up a notification for each completed download

This makes background installs and updates a lot easier
This commit is contained in:
Hans-Christoph Steiner 2016-04-11 14:17:50 -04:00
parent d114f428e7
commit 74d1c9521d
10 changed files with 129 additions and 13 deletions

View File

@ -34,7 +34,7 @@ public class DownloaderServiceTest extends ServiceTestCase<DownloaderService> {
} }
}, new IntentFilter(Downloader.ACTION_PROGRESS)); }, new IntentFilter(Downloader.ACTION_PROGRESS));
for (String url : urls) { for (String url : urls) {
DownloaderService.queue(getContext(), url); DownloaderService.queue(getContext(), null, url);
} }
Thread.sleep(30000); Thread.sleep(30000);
} }

View File

@ -442,6 +442,9 @@
<service <service
android:name=".net.DownloaderService" android:name=".net.DownloaderService"
android:exported="false" /> android:exported="false" />
<service
android:name=".net.DownloadCompleteService"
android:exported="false" />
<service android:name=".net.WifiStateChangeService" /> <service android:name=".net.WifiStateChangeService" />
<service android:name=".localrepo.SwapService" /> <service android:name=".localrepo.SwapService" />
</application> </application>

View File

@ -511,7 +511,7 @@ public class AppDetails extends AppCompatActivity {
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
try { try {
installer.installPackage(localFile, app.packageName); installer.installPackage(localFile, app.packageName, intent.getDataString());
} catch (InstallFailedException e) { } catch (InstallFailedException e) {
Log.e(TAG, "Android not compatible with this Installer!", e); Log.e(TAG, "Android not compatible with this Installer!", e);
} }
@ -869,7 +869,7 @@ public class AppDetails extends AppCompatActivity {
activeDownloadUrlString = urlString; activeDownloadUrlString = urlString;
registerDownloaderReceivers(); registerDownloaderReceivers();
headerFragment.startProgress(); headerFragment.startProgress();
DownloaderService.queue(this, activeDownloadUrlString); DownloaderService.queue(this, apk.packageName, activeDownloadUrlString);
} }
public void removeApk(String packageName) { public void removeApk(String packageName) {

View File

@ -496,7 +496,7 @@ public class UpdateService extends IntentService implements ProgressListener {
ApkProvider.DataColumns.NAME, ApkProvider.DataColumns.NAME,
}); });
String urlString = Utils.getApkUrl(repoAddress, apk); String urlString = Utils.getApkUrl(repoAddress, apk);
DownloaderService.queue(this, urlString); DownloaderService.queue(this, app.packageName, urlString);
cursor.moveToNext(); cursor.moveToNext();
} }
} }

View File

@ -66,6 +66,7 @@ import java.util.Formatter;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.zip.Adler32;
public final class Utils { 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 * 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 * 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. * 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. * is fine since that does the right thing.
*/ */
public static SanitizedFile getApkCacheDir(Context context) { public static SanitizedFile getApkCacheDir(Context context) {
@ -412,6 +413,15 @@ public final class Utils {
return repoAddress + "/" + apk.apkName.replace(" ", "%20"); 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<String> { public static final class CommaSeparatedList implements Iterable<String> {
private final String value; private final String value;

View File

@ -20,6 +20,7 @@
package org.fdroid.fdroid.installer; package org.fdroid.fdroid.installer;
import android.app.Activity; import android.app.Activity;
import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; 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. * 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; SanitizedFile apkToInstall = null;
try { try {
Map<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath()); Map<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath());
@ -228,6 +230,9 @@ public abstract class Installer {
FileCompat.setReadable(apkToInstall, true, false); FileCompat.setReadable(apkToInstall, true, false);
installPackageInternal(apkToInstall); installPackageInternal(apkToInstall);
NotificationManager nm = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(Utils.getApkUrlNotificationId(urlString));
} catch (NumberFormatException | NoSuchAlgorithmException | IOException e) { } catch (NumberFormatException | NoSuchAlgorithmException | IOException e) {
throw new InstallFailedException(e); throw new InstallFailedException(e);
} catch (ClassCastException e) { } catch (ClassCastException e) {

View File

@ -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());
}
}
}

View File

@ -72,6 +72,8 @@ import java.util.HashMap;
public class DownloaderService extends Service { public class DownloaderService extends Service {
public static final String TAG = "DownloaderService"; 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_QUEUE = "org.fdroid.fdroid.net.DownloaderService.action.QUEUE";
private static final String ACTION_CANCEL = "org.fdroid.fdroid.net.DownloaderService.action.CANCEL"; private static final String ACTION_CANCEL = "org.fdroid.fdroid.net.DownloaderService.action.CANCEL";
@ -216,6 +218,8 @@ public class DownloaderService extends Service {
}); });
downloader.download(); downloader.download();
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile); sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile);
DownloadCompleteService.notify(this, intent.getStringExtra(EXTRA_PACKAGE_NAME),
intent.getDataString());
} catch (InterruptedException e) { } catch (InterruptedException e) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile); sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile);
} catch (IOException e) { } 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 * All notifications are sent as an {@link Intent} via local broadcasts to be received by
* *
* @param context * @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) * @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); Log.i(TAG, "queue " + urlString);
Intent intent = new Intent(context, DownloaderService.class); Intent intent = new Intent(context, DownloaderService.class);
intent.setAction(ACTION_QUEUE); intent.setAction(ACTION_QUEUE);
intent.setData(Uri.parse(urlString)); intent.setData(Uri.parse(urlString));
if (!TextUtils.isEmpty(EXTRA_PACKAGE_NAME)) {
intent.putExtra(EXTRA_PACKAGE_NAME, packageName);
}
context.startService(intent); context.startService(intent);
} }
@ -268,7 +276,7 @@ public class DownloaderService extends Service {
* *
* @param context * @param context
* @param urlString The URL to remove from the download queue * @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) { public static void cancel(Context context, String urlString) {
Log.i(TAG, "cancel " + urlString); Log.i(TAG, "cancel " + urlString);

View File

@ -788,15 +788,15 @@ public class SwapWorkflowActivity extends AppCompatActivity {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
String path = intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH); 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, localBroadcastManager.registerReceiver(downloadCompleteReceiver,
DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE)); 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 { try {
Installer.getActivityInstaller(this, new Installer.InstallerCallback() { Installer.getActivityInstaller(this, new Installer.InstallerCallback() {
@ -811,7 +811,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
public void onError(int operation, int errorCode) { public void onError(int operation, int errorCode) {
// TODO: Boo! // TODO: Boo!
} }
}).installPackage(apkFile, packageName); }).installPackage(apkFile, packageName, urlString);
localBroadcastManager.unregisterReceiver(downloadCompleteReceiver); localBroadcastManager.unregisterReceiver(downloadCompleteReceiver);
} catch (Installer.InstallFailedException e) { } catch (Installer.InstallFailedException e) {
// TODO: Handle exception properly // TODO: Handle exception properly

View File

@ -341,6 +341,8 @@
<string name="swap_not_enabled">Swapping not enabled</string> <string name="swap_not_enabled">Swapping not enabled</string>
<string name="swap_not_enabled_description">Before swapping, your device must be made visible.</string> <string name="swap_not_enabled_description">Before swapping, your device must be made visible.</string>
<string name="tap_to_install_format">Tap to install %s</string>
<string name="tap_to_update_format">Tap to update %s</string>
<string name="install_confirm">Do you want to install this application? <string name="install_confirm">Do you want to install this application?
It will get access to:</string> It will get access to:</string>
<string name="install_confirm_no_perms">Do you want to install this application? <string name="install_confirm_no_perms">Do you want to install this application?