From dbd4c467f874c0f866e01682c31a5601eb045eca Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Thu, 3 Sep 2015 17:55:58 +0200 Subject: [PATCH] added asyndownloader to use DownloadManager when possible --- .../org/fdroid/fdroid/net/ApkDownloader.java | 26 +++- .../fdroid/fdroid/net/AsyncDownloader.java | 134 ++++++++++++++++++ .../fdroid/fdroid/net/DownloaderFactory.java | 2 +- 3 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java index 8fc64b83e..8bc058d2b 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -21,8 +21,8 @@ package org.fdroid.fdroid.net; import android.content.Context; +import android.os.Build; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.content.LocalBroadcastManager; @@ -38,6 +38,7 @@ import org.fdroid.fdroid.data.SanitizedFile; import java.io.File; import java.io.IOException; +import java.net.URL; import java.security.NoSuchAlgorithmException; /** @@ -193,11 +194,17 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { Utils.DebugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile); try { - Downloader downloader = DownloaderFactory.create(context, remoteAddress, localFile); - dlWrapper = new AsyncDownloadWrapper(downloader, this); + if (canUseDownloadManager(new URL(remoteAddress))) { + // If we can use Android's DownloadManager, let's use it, because + // of better OS integration, reliability, and async ability + dlWrapper = new AsyncDownloader(context, this, curApk.apkName, remoteAddress, localFile); + } else { + Downloader downloader = DownloaderFactory.create(context, remoteAddress, localFile); + dlWrapper = new AsyncDownloadWrapper(downloader, this); + } + dlWrapper.download(); return true; - } catch (IOException e) { onErrorDownloading(e.getLocalizedMessage()); } @@ -205,6 +212,17 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { return false; } + /** + * Tests to see if we can use Android's DownloadManager to download the APK, instead of + * a downloader returned from DownloadFactory. + * @param url + * @return + */ + private boolean canUseDownloadManager(URL url) { + return Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO + && !DownloaderFactory.isOnionAddress(url); + } + private void sendMessage(String type) { sendProgressEvent(new ProgressListener.Event(type)); } diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java new file mode 100644 index 000000000..301e8b347 --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java @@ -0,0 +1,134 @@ +package org.fdroid.fdroid.net; + +import android.annotation.TargetApi; +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Build; +import android.os.ParcelFileDescriptor; + +import org.fdroid.fdroid.data.SanitizedFile; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A downloader that uses Android's DownloadManager to perform a download. + */ +@TargetApi(Build.VERSION_CODES.GINGERBREAD) +public class AsyncDownloader extends AsyncDownloadWrapper { + private final Context context; + private final DownloadManager dm; + private SanitizedFile localFile; + private String remoteAddress; + private String appName; + private Listener listener; + + private long downloadId = -1; + + /** + * Normally the listener would be provided using a setListener method. + * However for the purposes of this async downloader, it doesn't make + * sense to have an async task without any way to notify the outside + * world about completion. Therefore, we require the listener as a + * parameter to the constructor. + * + * @param listener + */ + public AsyncDownloader(Context context, Listener listener, String appName, String remoteAddress, SanitizedFile localFile) { + super(null, listener); + this.context = context; + this.appName = appName; + this.remoteAddress = remoteAddress; + this.listener = listener; + this.localFile = localFile; + + if (appName == null || appName.trim().length() == 0) { + this.appName = remoteAddress; + } + + dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + } + + @Override + public void download() { + // set up download request + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress)); + request.setTitle(appName); + + if (listener != null) { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + intentFilter.addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED); + context.registerReceiver(downloadReceiver, intentFilter); + } + + downloadId = dm.enqueue(request); + } + + @Override + public int getBytesRead() { + return 0; + } + + @Override + public int getTotalBytes() { + return 0; + } + + @Override + public void attemptCancel() { + if (downloadId >= 0) dm.remove(downloadId); + } + + // Broadcast receiver to receive broadcasts from the download manager + private BroadcastReceiver downloadReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (listener == null) return; // no point if no-one is listening + + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { + long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); + if (id == downloadId) { + context.unregisterReceiver(this); + + // clear the notification + dm.remove(id); + + try { + // write the downloaded file to the expected location + ParcelFileDescriptor fd = dm.openDownloadedFile(id); + InputStream is = new FileInputStream(fd.getFileDescriptor()); + OutputStream os = new FileOutputStream(localFile); + byte[] buffer = new byte[1024]; + int count = 0; + while ((count = is.read(buffer, 0, buffer.length)) > 0) { + os.write(buffer, 0, count); + } + os.close(); + + listener.onDownloadComplete(); + } catch (IOException e) { + listener.onErrorDownloading(e.getLocalizedMessage()); + return; + } + } + } + + if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(intent.getAction())) { + long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); + if (id == downloadId) { + // TODO - display app details screen for this app + } + } + } + }; +} diff --git a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java index 5b476dd1c..757df7f20 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java @@ -51,7 +51,7 @@ public class DownloaderFactory { return "bluetooth".equalsIgnoreCase(url.getProtocol()); } - private static boolean isOnionAddress(URL url) { + static boolean isOnionAddress(URL url) { return url.getHost().endsWith(".onion"); } }