Merge branch 'download-service-pending-intent' into 'master'

Misc fixes/improvements to apk downloading

This started as a fix to #625, but quickly turned into a mini rampage to fix and/or improve a few different things in the `DownloaderService`. Hopefully the commits are self explanatory as to what they fix, but of course feel free to ask questions if you have any.

See merge request !265
This commit is contained in:
Daniel Martí 2016-04-22 16:58:57 +00:00
commit c240283ad4
4 changed files with 157 additions and 60 deletions

View File

@ -426,8 +426,8 @@ public class AppDetails extends AppCompatActivity {
@Override @Override
protected void onResumeFragments() { protected void onResumeFragments() {
super.onResumeFragments(); super.onResumeFragments();
headerFragment = (AppDetailsHeaderFragment) getSupportFragmentManager().findFragmentById(R.id.header);
refreshApkList(); refreshApkList();
refreshHeader();
supportInvalidateOptionsMenu(); supportInvalidateOptionsMenu();
if (DownloaderService.isQueuedOrActive(activeDownloadUrlString)) { if (DownloaderService.isQueuedOrActive(activeDownloadUrlString)) {
registerDownloaderReceivers(); registerDownloaderReceivers();
@ -564,7 +564,7 @@ public class AppDetails extends AppCompatActivity {
@Override @Override
protected void onDestroy() { protected void onDestroy() {
cleanUpFinishedDownload(); unregisterDownloaderReceivers();
super.onDestroy(); super.onDestroy();
} }
@ -615,8 +615,6 @@ public class AppDetails extends AppCompatActivity {
} }
private void refreshHeader() { private void refreshHeader() {
headerFragment = (AppDetailsHeaderFragment)
getSupportFragmentManager().findFragmentById(R.id.header);
if (headerFragment != null) { if (headerFragment != null) {
headerFragment.updateViews(); headerFragment.updateViews();
} }
@ -1416,17 +1414,44 @@ public class AppDetails extends AppCompatActivity {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
updateViews(); updateViews();
restoreProgressBarOnResume();
}
/**
* After resuming the fragment, decide whether or not we need to show the progress bar.
* Also, put an appropriate message depending on whether or not the download is active or
* just queued.
*
* NOTE: this can't be done in the `updateViews` method as it currently stands. The reason
* is because that method gets called all the time, for all sorts of reasons. The progress
* bar is updated with actual progress values in response to async broadcasts. If we always
* tried to force the progress bar in `updateViews`, it would override the values that were
* set by the async progress broadcasts.
*/
private void restoreProgressBarOnResume() {
if (appDetails.activeDownloadUrlString != null) {
// We don't actually know what the current progress is, so this will show an indeterminate
// progress bar until the first progress/complete event we receive.
final String message = DownloaderService.isQueued(appDetails.activeDownloadUrlString)
? getString(R.string.download_pending)
: "";
showIndeterminateProgress(message);
}
} }
/** /**
* Displays empty, indeterminate progress bar and related views. * Displays empty, indeterminate progress bar and related views.
*/ */
public void startProgress() { public void startProgress() {
showIndeterminateProgress(getString(R.string.download_pending));
updateViews();
}
private void showIndeterminateProgress(String message) {
setProgressVisible(true); setProgressVisible(true);
progressBar.setIndeterminate(true); progressBar.setIndeterminate(true);
progressSize.setText(""); progressSize.setText(message);
progressPercent.setText(""); progressPercent.setText("");
updateViews();
} }
/** /**

View File

@ -2,17 +2,14 @@ package org.fdroid.fdroid.net;
import android.app.IntentService; import android.app.IntentService;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent;
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;
import android.net.Uri; import android.net.Uri;
import android.os.Process; import android.os.Process;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.App;
@ -42,12 +39,12 @@ public class DownloadCompleteService extends IntentService {
if (intent != null) { if (intent != null) {
final String action = intent.getAction(); final String action = intent.getAction();
if (!ACTION_NOTIFY.equals(action)) { if (!ACTION_NOTIFY.equals(action)) {
Utils.debugLog(TAG, "intent action is not ACTION_NOTIFY"); Utils.debugLog(TAG, "Intent action is not ACTION_NOTIFY");
return; return;
} }
String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
if (TextUtils.isEmpty(packageName)) { if (TextUtils.isEmpty(packageName)) {
Utils.debugLog(TAG, "intent is missing EXTRA_PACKAGE_NAME"); Utils.debugLog(TAG, "Intent is missing EXTRA_PACKAGE_NAME");
return; return;
} }
@ -64,22 +61,13 @@ public class DownloadCompleteService extends IntentService {
title = String.format(getString(R.string.tap_to_install_format), app.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()); int requestCode = Utils.getApkUrlNotificationId(intent.getDataString());
PendingIntent pendingIntent = stackBuilder.getPendingIntent(requestCode,
PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = NotificationCompat.Builder builder =
new NotificationCompat.Builder(this) new NotificationCompat.Builder(this)
.setAutoCancel(true) .setAutoCancel(true)
.setContentTitle(title) .setContentTitle(title)
.setSmallIcon(android.R.drawable.stat_sys_download_done) .setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentIntent(pendingIntent) .setContentIntent(DownloaderService.createAppDetailsIntent(this, requestCode, packageName))
.setContentText(getString(R.string.tap_to_install)); .setContentText(getString(R.string.tap_to_install));
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.notify(Utils.getApkUrlNotificationId(intent.getDataString()), builder.build()); nm.notify(Utils.getApkUrlNotificationId(intent.getDataString()), builder.build());

View File

@ -19,6 +19,7 @@ package org.fdroid.fdroid.net;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -31,14 +32,21 @@ import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.os.PatternMatcher; import android.os.PatternMatcher;
import android.os.Process; import android.os.Process;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.data.SanitizedFile;
import java.io.File; import java.io.File;
@ -104,7 +112,7 @@ public class DownloaderService extends Service {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
Log.i(TAG, "onCreate"); Utils.debugLog(TAG, "Creating downloader service.");
HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
thread.start(); thread.start();
@ -117,60 +125,102 @@ public class DownloaderService extends Service {
@Override @Override
public void onStart(Intent intent, int startId) { public void onStart(Intent intent, int startId) {
super.onStart(intent, startId); super.onStart(intent, startId);
Log.i(TAG, "onStart " + startId + " " + intent); Utils.debugLog(TAG, "Received Intent for downloading: " + intent + " (with a startId of " + startId + ")");
String uriString = intent.getDataString(); String uriString = intent.getDataString();
if (uriString == null) { if (uriString == null) {
Log.e(TAG, "Received Intent with no URI: " + intent); Log.e(TAG, "Received Intent with no URI: " + intent);
return; return;
} }
if (ACTION_CANCEL.equals(intent.getAction())) { if (ACTION_CANCEL.equals(intent.getAction())) {
Log.i(TAG, "Removed " + intent); Utils.debugLog(TAG, "Cancelling download of " + uriString);
Integer what = QUEUE_WHATS.remove(uriString); if (isQueued(uriString)) {
if (what != null && serviceHandler.hasMessages(what)) {
// the URL is in the queue, remove it
serviceHandler.removeMessages(what); serviceHandler.removeMessages(what);
} else if (downloader != null && TextUtils.equals(uriString, downloader.sourceUrl.toString())) { } else if (isActive(uriString)) {
// the URL is being downloaded, cancel it
downloader.cancelDownload(); downloader.cancelDownload();
} else { } else {
Log.e(TAG, "CANCEL called on something not queued or running: " + startId + " " + intent); Log.e(TAG, "ACTION_CANCEL called on something not queued or running");
}
QUEUE_WHATS.remove(uriString);
if (isQueueEmpty()) {
stopForeground(true);
} }
} else if (ACTION_QUEUE.equals(intent.getAction())) { } else if (ACTION_QUEUE.equals(intent.getAction())) {
if (Preferences.get().isUpdateNotificationEnabled()) {
startForeground(NOTIFY_DOWNLOADING, createNotification(intent.getDataString()).build());
}
Log.i(TAG, "Queued " + intent);
Message msg = serviceHandler.obtainMessage(); Message msg = serviceHandler.obtainMessage();
msg.arg1 = startId; msg.arg1 = startId;
msg.obj = intent; msg.obj = intent;
msg.what = what++; msg.what = what++;
serviceHandler.sendMessage(msg); serviceHandler.sendMessage(msg);
Log.i(TAG, "QUEUE_WHATS.put(" + uriString + ", " + msg.what);
QUEUE_WHATS.put(uriString, msg.what); QUEUE_WHATS.put(uriString, msg.what);
Utils.debugLog(TAG, "Queued download of " + uriString + ". Now " + QUEUE_WHATS.size() + " downloads in the queue");
} else { } else {
Log.e(TAG, "Received Intent with unknown action: " + intent); Log.e(TAG, "Received Intent with unknown action: " + intent);
} }
} }
private NotificationCompat.Builder createNotification(String urlString) { @Nullable
private static String getPackageNameFromIntent(@NonNull Intent intent) {
return intent.hasExtra(EXTRA_PACKAGE_NAME) ? intent.getStringExtra(EXTRA_PACKAGE_NAME) : null;
}
private NotificationCompat.Builder createNotification(String urlString, @Nullable String packageName) {
return new NotificationCompat.Builder(this) return new NotificationCompat.Builder(this)
.setAutoCancel(true) .setAutoCancel(true)
.setContentTitle(getString(R.string.downloading)) .setContentIntent(createAppDetailsIntent(this, 0, packageName))
.setContentTitle(getNotificationTitle(packageName))
.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
.setContentText(urlString) .setContentText(urlString)
.setProgress(100, 0, true); .setProgress(100, 0, true);
} }
/**
* If downloading an apk (i.e. <code>packageName != null</code>) then the title will indicate
* the name of the app which the apk belongs to. Otherwise, it will be a generic "Downloading..."
* message.
*/
private String getNotificationTitle(@Nullable String packageName) {
String title;
if (packageName != null) {
App app = AppProvider.Helper.findByPackageName(
getContentResolver(), packageName, new String[] {AppProvider.DataColumns.NAME});
title = getString(R.string.downloading_apk, app.name);
} else {
title = getString(R.string.downloading);
}
return title;
}
public static PendingIntent createAppDetailsIntent(@NonNull Context context, int requestCode, @Nullable String packageName) {
TaskStackBuilder stackBuilder;
if (packageName != null) {
Intent notifyIntent = new Intent(context.getApplicationContext(), AppDetails.class)
.putExtra(AppDetails.EXTRA_APPID, packageName);
stackBuilder = TaskStackBuilder
.create(context.getApplicationContext())
.addParentStack(AppDetails.class)
.addNextIntent(notifyIntent);
} else {
Intent notifyIntent = new Intent(context.getApplicationContext(), FDroid.class);
stackBuilder = TaskStackBuilder
.create(context.getApplicationContext())
.addParentStack(FDroid.class)
.addNextIntent(notifyIntent);
}
return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT);
}
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId); onStart(intent, startId);
Log.i(TAG, "onStartCommand " + intent); Utils.debugLog(TAG, "onStartCommand " + intent);
return START_REDELIVER_INTENT; // if killed before completion, retry Intent return START_REDELIVER_INTENT; // if killed before completion, retry Intent
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
Log.i(TAG, "onDestroy"); Utils.debugLog(TAG, "Destroying downloader service. Will move to background and stop our Looper.");
stopForeground(true); stopForeground(true);
serviceLooper.quit(); //NOPMD - this is copied from IntentService, no super call needed serviceLooper.quit(); //NOPMD - this is copied from IntentService, no super call needed
} }
@ -204,29 +254,39 @@ public class DownloaderService extends Service {
File downloadDir = new File(Utils.getApkCacheDir(this), uri.getHost() + "-" + uri.getPort()); File downloadDir = new File(Utils.getApkCacheDir(this), uri.getHost() + "-" + uri.getPort());
downloadDir.mkdirs(); downloadDir.mkdirs();
final SanitizedFile localFile = new SanitizedFile(downloadDir, uri.getLastPathSegment()); final SanitizedFile localFile = new SanitizedFile(downloadDir, uri.getLastPathSegment());
final String packageName = getPackageNameFromIntent(intent);
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile); sendBroadcast(uri, Downloader.ACTION_STARTED, localFile);
if (Preferences.get().isUpdateNotificationEnabled()) {
Notification notification = createNotification(intent.getDataString(), getPackageNameFromIntent(intent)).build();
startForeground(NOTIFY_DOWNLOADING, notification);
}
try { try {
downloader = DownloaderFactory.create(this, uri, localFile); downloader = DownloaderFactory.create(this, uri, localFile);
downloader.setListener(new Downloader.DownloaderProgressListener() { downloader.setListener(new Downloader.DownloaderProgressListener() {
@Override @Override
public void sendProgress(URL sourceUrl, int bytesRead, int totalBytes) { public void sendProgress(URL sourceUrl, int bytesRead, int totalBytes) {
if (isActive(uri.toString())) {
Intent intent = new Intent(Downloader.ACTION_PROGRESS); Intent intent = new Intent(Downloader.ACTION_PROGRESS);
intent.setData(uri); intent.setData(uri);
intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead); intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead);
intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes); intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes);
localBroadcastManager.sendBroadcast(intent); localBroadcastManager.sendBroadcast(intent);
if (Preferences.get().isUpdateNotificationEnabled()) {
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification notification = createNotification(uri.toString()) Notification notification = createNotification(uri.toString(), packageName)
.setProgress(totalBytes, bytesRead, false) .setProgress(totalBytes, bytesRead, false)
.build(); .build();
nm.notify(NOTIFY_DOWNLOADING, notification); nm.notify(NOTIFY_DOWNLOADING, notification);
} }
}
}
}); });
downloader.download(); downloader.download();
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile); sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile);
DownloadCompleteService.notify(this, intent.getStringExtra(EXTRA_PACKAGE_NAME), DownloadCompleteService.notify(this, packageName, intent.getDataString());
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) {
@ -237,6 +297,10 @@ public class DownloaderService extends Service {
if (downloader != null) { if (downloader != null) {
downloader.close(); downloader.close();
} }
// May have already been removed in response to a cancel intent, but that wont cause
// problems if we ask to remove it again.
QUEUE_WHATS.remove(uri.toString());
stopForeground(true);
} }
downloader = null; downloader = null;
} }
@ -266,11 +330,11 @@ public class DownloaderService extends Service {
* @see #cancel(Context, String) * @see #cancel(Context, String)
*/ */
public static void queue(Context context, String packageName, String urlString) { public static void queue(Context context, String packageName, String urlString) {
Log.i(TAG, "queue " + urlString); Utils.debugLog(TAG, "Preparing " + urlString + " to go into the download queue");
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)) { if (!TextUtils.isEmpty(packageName)) {
intent.putExtra(EXTRA_PACKAGE_NAME, packageName); intent.putExtra(EXTRA_PACKAGE_NAME, packageName);
} }
context.startService(intent); context.startService(intent);
@ -286,7 +350,7 @@ public class DownloaderService extends Service {
* @see #queue(Context, String, 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); Utils.debugLog(TAG, "Preparing cancellation of " + urlString + " download");
Intent intent = new Intent(context, DownloaderService.class); Intent intent = new Intent(context, DownloaderService.class);
intent.setAction(ACTION_CANCEL); intent.setAction(ACTION_CANCEL);
intent.setData(Uri.parse(urlString)); intent.setData(Uri.parse(urlString));
@ -294,18 +358,36 @@ public class DownloaderService extends Service {
} }
/** /**
* Check if a URL is waiting in the queue for downloading or if actively * Check if a URL is waiting in the queue for downloading or if actively being downloaded.
* being downloaded. This is useful for checking whether to re-register * This is useful for checking whether to re-register {@link android.content.BroadcastReceiver}s
* {@link android.content.BroadcastReceiver}s in * in {@link android.app.Activity#onResume()}.
* {@link android.app.Activity#onResume()} * @see DownloaderService#isQueued(String)
* @see DownloaderService#isActive(String)
*/ */
public static boolean isQueuedOrActive(String urlString) { public static boolean isQueuedOrActive(String urlString) {
return isQueued(urlString) || isActive(urlString);
}
public static boolean isQueueEmpty() {
return QUEUE_WHATS.isEmpty();
}
/**
* Check if a URL is waiting in the queue for downloading.
*/
public static boolean isQueued(String urlString) {
if (TextUtils.isEmpty(urlString)) { if (TextUtils.isEmpty(urlString)) {
return false; return false;
} }
Integer what = QUEUE_WHATS.get(urlString); Integer what = QUEUE_WHATS.get(urlString);
return (what != null && serviceHandler.hasMessages(what)) return what != null && serviceHandler.hasMessages(what);
|| (downloader != null && TextUtils.equals(urlString, downloader.sourceUrl.toString())); }
/**
* Check if a URL is actively being downloaded.
*/
public static boolean isActive(String urlString) {
return downloader != null && QUEUE_WHATS.containsKey(urlString) && TextUtils.equals(urlString, downloader.sourceUrl.toString());
} }
/** /**

View File

@ -367,10 +367,12 @@
<string name="uninstall_confirm">Do you want to uninstall this app?</string> <string name="uninstall_confirm">Do you want to uninstall this app?</string>
<string name="tap_to_install">Download completed, tap to install</string> <string name="tap_to_install">Download completed, tap to install</string>
<string name="download_error">Download unsuccessful</string> <string name="download_error">Download unsuccessful</string>
<string name="download_pending">Waiting to start download…</string>
<string name="perms_new_perm_prefix">New: </string> <string name="perms_new_perm_prefix">New: </string>
<string name="perms_description_app">Provided by %1$s.</string> <string name="perms_description_app">Provided by %1$s.</string>
<string name="downloading">Downloading…</string> <string name="downloading">Downloading…</string>
<string name="downloading_apk">Downloading %1$s</string>
<string name="interval_never">Never</string> <string name="interval_never">Never</string>
<string name="interval_1h">Hourly</string> <string name="interval_1h">Hourly</string>