Merge branch 'downloaderservice-fixes' into 'master'
DownloaderService fixes This aims to simplify the `DownloaderService` a bunch to make it easier to understand. It also fixes some bugs with the download queue and related progress reports. This is groundwork for the upcoming `InstallManagerService` which will manage the whole process of installing an app, from checking the cache, to downloading, to running the install process. See merge request !273
This commit is contained in:
commit
169346ce76
@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid.net;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
|
||||||
import android.test.ServiceTestCase;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
@SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics
|
|
||||||
public class DownloaderServiceTest extends ServiceTestCase<DownloaderService> {
|
|
||||||
public static final String TAG = "DownloaderServiceTest";
|
|
||||||
|
|
||||||
String[] urls = {
|
|
||||||
"https://en.wikipedia.org/wiki/Index.html",
|
|
||||||
"https://mirrors.kernel.org/debian/dists/stable/Release",
|
|
||||||
"https://f-droid.org/archive/de.we.acaldav_5.apk",
|
|
||||||
// sites that use SNI for HTTPS
|
|
||||||
"https://guardianproject.info/fdroid/repo/index.jar",
|
|
||||||
};
|
|
||||||
|
|
||||||
public DownloaderServiceTest() {
|
|
||||||
super(DownloaderService.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testQueueingDownload() throws InterruptedException {
|
|
||||||
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getContext());
|
|
||||||
localBroadcastManager.registerReceiver(new BroadcastReceiver() {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
Log.i(TAG, "onReceive " + intent);
|
|
||||||
}
|
|
||||||
}, new IntentFilter(Downloader.ACTION_PROGRESS));
|
|
||||||
for (String url : urls) {
|
|
||||||
DownloaderService.queue(getContext(), null, url);
|
|
||||||
}
|
|
||||||
Thread.sleep(30000);
|
|
||||||
}
|
|
||||||
}
|
|
@ -442,9 +442,6 @@
|
|||||||
<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
|
<service
|
||||||
android:name=".CleanCacheService"
|
android:name=".CleanCacheService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
@ -1443,10 +1443,11 @@ public class AppDetails extends AppCompatActivity {
|
|||||||
if (appDetails.activeDownloadUrlString != null) {
|
if (appDetails.activeDownloadUrlString != null) {
|
||||||
// We don't actually know what the current progress is, so this will show an indeterminate
|
// 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.
|
// progress bar until the first progress/complete event we receive.
|
||||||
final String message = DownloaderService.isQueued(appDetails.activeDownloadUrlString)
|
if (DownloaderService.isQueuedOrActive(appDetails.activeDownloadUrlString)) {
|
||||||
? getString(R.string.download_pending)
|
showIndeterminateProgress(getString(R.string.download_pending));
|
||||||
: "";
|
} else {
|
||||||
showIndeterminateProgress(message);
|
showIndeterminateProgress("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +67,6 @@ 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 {
|
||||||
|
|
||||||
@ -439,15 +438,6 @@ 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;
|
||||||
|
|
||||||
|
@ -227,7 +227,7 @@ public abstract class Installer {
|
|||||||
|
|
||||||
NotificationManager nm = (NotificationManager)
|
NotificationManager nm = (NotificationManager)
|
||||||
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
nm.cancel(Utils.getApkUrlNotificationId(urlString));
|
nm.cancel(urlString.hashCode());
|
||||||
} catch (NumberFormatException | NoSuchAlgorithmException | IOException e) {
|
} catch (NumberFormatException | NoSuchAlgorithmException | IOException e) {
|
||||||
throw new InstallFailedException(e);
|
throw new InstallFailedException(e);
|
||||||
} catch (ClassCastException e) {
|
} catch (ClassCastException e) {
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
package org.fdroid.fdroid.net;
|
|
||||||
|
|
||||||
import android.app.IntentService;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
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.text.TextUtils;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
int requestCode = Utils.getApkUrlNotificationId(intent.getDataString());
|
|
||||||
NotificationCompat.Builder builder =
|
|
||||||
new NotificationCompat.Builder(this)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
.setContentIntent(DownloaderService.createAppDetailsIntent(this, requestCode, packageName))
|
|
||||||
.setContentText(getString(R.string.tap_to_install));
|
|
||||||
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
||||||
nm.notify(Utils.getApkUrlNotificationId(intent.getDataString()), builder.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -30,7 +30,6 @@ public abstract class Downloader {
|
|||||||
private volatile boolean cancelled = false;
|
private volatile boolean cancelled = false;
|
||||||
private volatile int bytesRead;
|
private volatile int bytesRead;
|
||||||
private volatile int totalBytes;
|
private volatile int totalBytes;
|
||||||
private Timer timer;
|
|
||||||
|
|
||||||
public final File outputFile;
|
public final File outputFile;
|
||||||
|
|
||||||
@ -49,7 +48,7 @@ public abstract class Downloader {
|
|||||||
/**
|
/**
|
||||||
* For sending download progress, should only be called in {@link #progressTask}
|
* For sending download progress, should only be called in {@link #progressTask}
|
||||||
*/
|
*/
|
||||||
private DownloaderProgressListener downloaderProgressListener;
|
private volatile DownloaderProgressListener downloaderProgressListener;
|
||||||
|
|
||||||
protected abstract InputStream getDownloadersInputStream() throws IOException;
|
protected abstract InputStream getDownloadersInputStream() throws IOException;
|
||||||
|
|
||||||
@ -131,9 +130,6 @@ public abstract class Downloader {
|
|||||||
private void throwExceptionIfInterrupted() throws InterruptedException {
|
private void throwExceptionIfInterrupted() throws InterruptedException {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
Utils.debugLog(TAG, "Received interrupt, cancelling download");
|
Utils.debugLog(TAG, "Received interrupt, cancelling download");
|
||||||
if (timer != null) {
|
|
||||||
timer.cancel();
|
|
||||||
}
|
|
||||||
throw new InterruptedException();
|
throw new InterruptedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,40 +147,44 @@ public abstract class Downloader {
|
|||||||
* progress counter.
|
* progress counter.
|
||||||
*/
|
*/
|
||||||
private void copyInputToOutputStream(InputStream input, int bufferSize, OutputStream output) throws IOException, InterruptedException {
|
private void copyInputToOutputStream(InputStream input, int bufferSize, OutputStream output) throws IOException, InterruptedException {
|
||||||
bytesRead = 0;
|
Timer timer = new Timer();
|
||||||
totalBytes = totalDownloadSize();
|
try {
|
||||||
byte[] buffer = new byte[bufferSize];
|
bytesRead = 0;
|
||||||
|
totalBytes = totalDownloadSize();
|
||||||
|
byte[] buffer = new byte[bufferSize];
|
||||||
|
|
||||||
timer = new Timer();
|
timer.scheduleAtFixedRate(progressTask, 0, 100);
|
||||||
timer.scheduleAtFixedRate(progressTask, 0, 100);
|
|
||||||
|
|
||||||
// Getting the total download size could potentially take time, depending on how
|
|
||||||
// it is implemented, so we may as well check this before we proceed.
|
|
||||||
throwExceptionIfInterrupted();
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
|
|
||||||
int count;
|
|
||||||
if (input.available() > 0) {
|
|
||||||
int readLength = Math.min(input.available(), buffer.length);
|
|
||||||
count = input.read(buffer, 0, readLength);
|
|
||||||
} else {
|
|
||||||
count = input.read(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Getting the total download size could potentially take time, depending on how
|
||||||
|
// it is implemented, so we may as well check this before we proceed.
|
||||||
throwExceptionIfInterrupted();
|
throwExceptionIfInterrupted();
|
||||||
|
|
||||||
if (count == -1) {
|
while (true) {
|
||||||
Utils.debugLog(TAG, "Finished downloading from stream");
|
|
||||||
break;
|
int count;
|
||||||
|
if (input.available() > 0) {
|
||||||
|
int readLength = Math.min(input.available(), buffer.length);
|
||||||
|
count = input.read(buffer, 0, readLength);
|
||||||
|
} else {
|
||||||
|
count = input.read(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
throwExceptionIfInterrupted();
|
||||||
|
|
||||||
|
if (count == -1) {
|
||||||
|
Utils.debugLog(TAG, "Finished downloading from stream");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bytesRead += count;
|
||||||
|
output.write(buffer, 0, count);
|
||||||
}
|
}
|
||||||
bytesRead += count;
|
} finally {
|
||||||
output.write(buffer, 0, count);
|
downloaderProgressListener = null;
|
||||||
|
timer.cancel();
|
||||||
|
timer.purge();
|
||||||
|
output.flush();
|
||||||
|
output.close();
|
||||||
}
|
}
|
||||||
timer.cancel();
|
|
||||||
timer.purge();
|
|
||||||
output.flush();
|
|
||||||
output.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +24,7 @@ import android.app.Service;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
@ -52,7 +53,6 @@ import org.fdroid.fdroid.data.SanitizedFile;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DownloaderService is a service that handles asynchronous download requests
|
* DownloaderService is a service that handles asynchronous download requests
|
||||||
@ -60,24 +60,28 @@ import java.util.HashMap;
|
|||||||
* through {@link android.content.Context#startService(Intent)} calls; the
|
* through {@link android.content.Context#startService(Intent)} calls; the
|
||||||
* service is started as needed, handles each Intent in turn using a worker
|
* service is started as needed, handles each Intent in turn using a worker
|
||||||
* thread, and stops itself when it runs out of work.
|
* thread, and stops itself when it runs out of work.
|
||||||
* <p/>
|
* <p>
|
||||||
* <p>This "work queue processor" pattern is commonly used to offload tasks
|
* This "work queue processor" pattern is commonly used to offload tasks
|
||||||
* from an application's main thread. The DownloaderService class exists to
|
* from an application's main thread. The DownloaderService class exists to
|
||||||
* simplify this pattern and take care of the mechanics. DownloaderService
|
* simplify this pattern and take care of the mechanics. DownloaderService
|
||||||
* will receive the Intents, launch a worker thread, and stop the service as
|
* will receive the Intents, launch a worker thread, and stop the service as
|
||||||
* appropriate.
|
* appropriate.
|
||||||
* <p/>
|
* <p>
|
||||||
* <p>All requests are handled on a single worker thread -- they may take as
|
* All requests are handled on a single worker thread -- they may take as
|
||||||
* long as necessary (and will not block the application's main loop), but
|
* long as necessary (and will not block the application's main loop), but
|
||||||
* only one request will be processed at a time.
|
* only one request will be processed at a time.
|
||||||
* <p/>
|
* <p>
|
||||||
* <div class="special reference">
|
* The full URL for the file to download is also used as the unique ID to
|
||||||
* <h3>Developer Guides</h3>
|
* represent the download itself throughout F-Droid. This follows the model
|
||||||
* <p>For a detailed discussion about how to create services, read the
|
* of {@link Intent#setData(Uri)}, where the core data of an {@code Intent} is
|
||||||
* <a href="{@docRoot}guide/topics/fundamentals/services.html">Services</a> developer guide.</p>
|
* a {@code Uri}. For places that need an {@code int} ID,
|
||||||
* </div>
|
* {@link String#hashCode()} should be used to get a reproducible, unique {@code int}
|
||||||
|
* from any {@code urlString}. The full URL is guaranteed to be unique since
|
||||||
|
* it points to a file on a filesystem. This is more important with media files
|
||||||
|
* than with APKs since there is not reliable standard for a unique ID for
|
||||||
|
* media files, unlike APKs with {@code packageName} and {@code versionCode}.
|
||||||
*
|
*
|
||||||
* @see android.os.AsyncTask
|
* @see android.app.IntentService
|
||||||
*/
|
*/
|
||||||
public class DownloaderService extends Service {
|
public class DownloaderService extends Service {
|
||||||
private static final String TAG = "DownloaderService";
|
private static final String TAG = "DownloaderService";
|
||||||
@ -94,9 +98,6 @@ public class DownloaderService extends Service {
|
|||||||
private static volatile Downloader downloader;
|
private static volatile Downloader downloader;
|
||||||
private LocalBroadcastManager localBroadcastManager;
|
private LocalBroadcastManager localBroadcastManager;
|
||||||
|
|
||||||
private static final HashMap<String, Integer> QUEUE_WHATS = new HashMap<>();
|
|
||||||
private int what;
|
|
||||||
|
|
||||||
private final class ServiceHandler extends Handler {
|
private final class ServiceHandler extends Handler {
|
||||||
ServiceHandler(Looper looper) {
|
ServiceHandler(Looper looper) {
|
||||||
super(looper);
|
super(looper);
|
||||||
@ -133,40 +134,30 @@ public class DownloaderService extends Service {
|
|||||||
}
|
}
|
||||||
if (ACTION_CANCEL.equals(intent.getAction())) {
|
if (ACTION_CANCEL.equals(intent.getAction())) {
|
||||||
Utils.debugLog(TAG, "Cancelling download of " + uriString);
|
Utils.debugLog(TAG, "Cancelling download of " + uriString);
|
||||||
if (isQueued(uriString)) {
|
Integer whatToRemove = uriString.hashCode();
|
||||||
serviceHandler.removeMessages(what);
|
if (serviceHandler.hasMessages(whatToRemove)) {
|
||||||
|
serviceHandler.removeMessages(whatToRemove);
|
||||||
} else if (isActive(uriString)) {
|
} else if (isActive(uriString)) {
|
||||||
downloader.cancelDownload();
|
downloader.cancelDownload();
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "ACTION_CANCEL called on something not queued or running");
|
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())) {
|
||||||
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 = uriString.hashCode();
|
||||||
serviceHandler.sendMessage(msg);
|
serviceHandler.sendMessage(msg);
|
||||||
QUEUE_WHATS.put(uriString, msg.what);
|
Utils.debugLog(TAG, "Queued download of " + uriString);
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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) {
|
private NotificationCompat.Builder createNotification(String urlString, @Nullable String packageName) {
|
||||||
return new NotificationCompat.Builder(this)
|
return new NotificationCompat.Builder(this)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setContentIntent(createAppDetailsIntent(this, 0, packageName))
|
.setContentIntent(createAppDetailsIntent(0, packageName))
|
||||||
.setContentTitle(getNotificationTitle(packageName))
|
.setContentTitle(getNotificationTitle(packageName))
|
||||||
.addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel),
|
.addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel),
|
||||||
createCancelDownloadIntent(this, 0, urlString))
|
createCancelDownloadIntent(this, 0, urlString))
|
||||||
@ -191,20 +182,20 @@ public class DownloaderService extends Service {
|
|||||||
return getString(R.string.downloading);
|
return getString(R.string.downloading);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PendingIntent createAppDetailsIntent(@NonNull Context context, int requestCode, @Nullable String packageName) {
|
private PendingIntent createAppDetailsIntent(int requestCode, String packageName) {
|
||||||
TaskStackBuilder stackBuilder;
|
TaskStackBuilder stackBuilder;
|
||||||
if (packageName != null) {
|
if (packageName != null) {
|
||||||
Intent notifyIntent = new Intent(context.getApplicationContext(), AppDetails.class)
|
Intent notifyIntent = new Intent(getApplicationContext(), AppDetails.class)
|
||||||
.putExtra(AppDetails.EXTRA_APPID, packageName);
|
.putExtra(AppDetails.EXTRA_APPID, packageName);
|
||||||
|
|
||||||
stackBuilder = TaskStackBuilder
|
stackBuilder = TaskStackBuilder
|
||||||
.create(context.getApplicationContext())
|
.create(getApplicationContext())
|
||||||
.addParentStack(AppDetails.class)
|
.addParentStack(AppDetails.class)
|
||||||
.addNextIntent(notifyIntent);
|
.addNextIntent(notifyIntent);
|
||||||
} else {
|
} else {
|
||||||
Intent notifyIntent = new Intent(context.getApplicationContext(), FDroid.class);
|
Intent notifyIntent = new Intent(getApplicationContext(), FDroid.class);
|
||||||
stackBuilder = TaskStackBuilder
|
stackBuilder = TaskStackBuilder
|
||||||
.create(context.getApplicationContext())
|
.create(getApplicationContext())
|
||||||
.addParentStack(FDroid.class)
|
.addParentStack(FDroid.class)
|
||||||
.addNextIntent(notifyIntent);
|
.addNextIntent(notifyIntent);
|
||||||
}
|
}
|
||||||
@ -266,11 +257,11 @@ 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);
|
final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
|
||||||
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile);
|
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile);
|
||||||
|
|
||||||
if (Preferences.get().isUpdateNotificationEnabled()) {
|
if (Preferences.get().isUpdateNotificationEnabled()) {
|
||||||
Notification notification = createNotification(intent.getDataString(), getPackageNameFromIntent(intent)).build();
|
Notification notification = createNotification(intent.getDataString(), intent.getStringExtra(EXTRA_PACKAGE_NAME)).build();
|
||||||
startForeground(NOTIFY_DOWNLOADING, notification);
|
startForeground(NOTIFY_DOWNLOADING, notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,26 +270,24 @@ public class DownloaderService extends Service {
|
|||||||
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()) {
|
if (Preferences.get().isUpdateNotificationEnabled()) {
|
||||||
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||||
Notification notification = createNotification(uri.toString(), packageName)
|
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, packageName, intent.getDataString());
|
notifyDownloadComplete(packageName, 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) {
|
||||||
@ -309,14 +298,40 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post a notification about a completed download. {@code packageName} must be a valid
|
||||||
|
* and currently in the app index database.
|
||||||
|
*/
|
||||||
|
private void notifyDownloadComplete(String packageName, String urlString) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
int downloadUrlId = urlString.hashCode();
|
||||||
|
NotificationCompat.Builder builder =
|
||||||
|
new NotificationCompat.Builder(this)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
.setContentIntent(createAppDetailsIntent(downloadUrlId, packageName))
|
||||||
|
.setContentText(getString(R.string.tap_to_install));
|
||||||
|
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||||
|
nm.notify(downloadUrlId, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
private void sendBroadcast(Uri uri, String action, File file) {
|
private void sendBroadcast(Uri uri, String action, File file) {
|
||||||
sendBroadcast(uri, action, file, null);
|
sendBroadcast(uri, action, file, null);
|
||||||
}
|
}
|
||||||
@ -336,7 +351,7 @@ public class DownloaderService extends Service {
|
|||||||
* <p/>
|
* <p/>
|
||||||
* 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 this app's {@link Context}
|
||||||
* @param packageName The packageName of the app being downloaded
|
* @param packageName The packageName of the app being downloaded
|
||||||
* @param urlString The URL to add to the download queue
|
* @param urlString The URL to add to the download queue
|
||||||
* @see #cancel(Context, String)
|
* @see #cancel(Context, String)
|
||||||
@ -357,7 +372,7 @@ public class DownloaderService extends Service {
|
|||||||
* <p/>
|
* <p/>
|
||||||
* 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 this app's {@link 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, String)
|
* @see #queue(Context, String, String)
|
||||||
*/
|
*/
|
||||||
@ -373,34 +388,19 @@ public class DownloaderService extends Service {
|
|||||||
* Check if a URL is waiting in the queue for downloading or if actively being downloaded.
|
* Check if a URL is waiting in the queue for downloading or if actively being downloaded.
|
||||||
* This is useful for checking whether to re-register {@link android.content.BroadcastReceiver}s
|
* This is useful for checking whether to re-register {@link android.content.BroadcastReceiver}s
|
||||||
* in {@link android.app.Activity#onResume()}.
|
* in {@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);
|
if (TextUtils.isEmpty(urlString)) { //NOPMD - suggests unreadable format
|
||||||
}
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Integer what = QUEUE_WHATS.get(urlString);
|
return serviceHandler.hasMessages(urlString.hashCode()) || isActive(urlString);
|
||||||
return what != null && serviceHandler.hasMessages(what);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a URL is actively being downloaded.
|
* Check if a URL is actively being downloaded.
|
||||||
*/
|
*/
|
||||||
public static boolean isActive(String urlString) {
|
public static boolean isActive(String urlString) {
|
||||||
return downloader != null && QUEUE_WHATS.containsKey(urlString) && TextUtils.equals(urlString, downloader.sourceUrl.toString());
|
return downloader != null && TextUtils.equals(urlString, downloader.sourceUrl.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -409,7 +409,6 @@ public class DownloaderService extends Service {
|
|||||||
* @param urlString The full file URL to match.
|
* @param urlString The full file URL to match.
|
||||||
* @param action {@link Downloader#ACTION_STARTED}, {@link Downloader#ACTION_PROGRESS},
|
* @param action {@link Downloader#ACTION_STARTED}, {@link Downloader#ACTION_PROGRESS},
|
||||||
* {@link Downloader#ACTION_INTERRUPTED}, or {@link Downloader#ACTION_COMPLETE},
|
* {@link Downloader#ACTION_INTERRUPTED}, or {@link Downloader#ACTION_COMPLETE},
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public static IntentFilter getIntentFilter(String urlString, String action) {
|
public static IntentFilter getIntentFilter(String urlString, String action) {
|
||||||
Uri uri = Uri.parse(urlString);
|
Uri uri = Uri.parse(urlString);
|
||||||
|
@ -120,7 +120,7 @@ public class HttpDownloader extends Downloader {
|
|||||||
// workaround until NetCipher supports HTTPS SNI
|
// workaround until NetCipher supports HTTPS SNI
|
||||||
// https://gitlab.com/fdroid/fdroidclient/issues/431
|
// https://gitlab.com/fdroid/fdroidclient/issues/431
|
||||||
if (connection instanceof HttpsURLConnection
|
if (connection instanceof HttpsURLConnection
|
||||||
&& "f-droid.org".equals(sourceUrl.getHost())) {
|
&& !"f-droid.org".equals(sourceUrl.getHost())) {
|
||||||
((HttpsURLConnection) connection).setSSLSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
|
((HttpsURLConnection) connection).setSSLSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user