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));
for (String url : urls) {
DownloaderService.queue(getContext(), url);
DownloaderService.queue(getContext(), null, url);
}
Thread.sleep(30000);
}

View File

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

View File

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

View File

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

View File

@ -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<String> {
private final String value;

View File

@ -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<String, Object> 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) {

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

View File

@ -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

View File

@ -341,6 +341,8 @@
<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="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?
It will get access to:</string>
<string name="install_confirm_no_perms">Do you want to install this application?