From dbd4c467f874c0f866e01682c31a5601eb045eca Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Thu, 3 Sep 2015 17:55:58 +0200 Subject: [PATCH 01/20] 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"); } } From 5f989739bba53c5783f7a9bb0d10ac532bfbfc96 Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Fri, 4 Sep 2015 09:20:21 +0200 Subject: [PATCH 02/20] wip: running download manager outside fdroid --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 4 ++-- F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java | 8 +++++--- .../src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java | 2 +- F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java | 8 ++++++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index ee03cb40c..83f903849 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -554,7 +554,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A protected void onDestroy() { if (downloadHandler != null) { if (!inProcessOfChangingConfiguration) { - downloadHandler.cancel(); + downloadHandler.cancel(false); cleanUpFinishedDownload(); } } @@ -1517,7 +1517,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A if (activity == null || activity.downloadHandler == null) return; - activity.downloadHandler.cancel(); + activity.downloadHandler.cancel(true); activity.cleanUpFinishedDownload(); setProgressVisible(false); updateViews(); diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java index 8bc058d2b..0c22313f8 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -289,11 +289,13 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { /** * Attempts to cancel the download (if in progress) and also removes the progress - * listener (to prevent + * listener + * + * @param userRequested - true if the user requested the cancel (via button click), otherwise false. */ - public void cancel() { + public void cancel(boolean userRequested) { if (dlWrapper != null) { - dlWrapper.attemptCancel(); + dlWrapper.attemptCancel(userRequested); } } diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java index ef0996ad4..789a8ae18 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java @@ -47,7 +47,7 @@ public class AsyncDownloadWrapper extends Handler { downloadThread.start(); } - public void attemptCancel() { + public void attemptCancel(boolean userRequested) { if (downloadThread != null) { downloadThread.interrupt(); } diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java index 301e8b347..72c4d2181 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java @@ -85,8 +85,12 @@ public class AsyncDownloader extends AsyncDownloadWrapper { } @Override - public void attemptCancel() { - if (downloadId >= 0) dm.remove(downloadId); + public void attemptCancel(boolean userRequested) { + if (userRequested && downloadId >= 0) { + dm.remove(downloadId); + } + + context.unregisterReceiver(downloadReceiver); } // Broadcast receiver to receive broadcasts from the download manager From f9fee5beb0f3c2f59df9174288e9a5f9b8a9b875 Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Fri, 4 Sep 2015 11:27:17 +0200 Subject: [PATCH 03/20] wip: when a download is completed, app is woken up and app details screen displayed --- F-Droid/AndroidManifest.xml | 8 ++++++ F-Droid/src/org/fdroid/fdroid/AppDetails.java | 11 ++++++++ .../org/fdroid/fdroid/net/ApkDownloader.java | 2 +- .../fdroid/fdroid/net/AsyncDownloader.java | 26 +++++++++++++++++-- .../receiver/DownloadManagerReceiver.java | 22 ++++++++++++++++ 5 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml index 779fba7e1..f2683b0a6 100644 --- a/F-Droid/AndroidManifest.xml +++ b/F-Droid/AndroidManifest.xml @@ -450,6 +450,14 @@ + + + + + + + + diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index 83f903849..7b53b151d 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -22,6 +22,7 @@ package org.fdroid.fdroid; import android.app.Activity; +import android.app.DownloadManager; import android.bluetooth.BluetoothAdapter; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; @@ -34,6 +35,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; import android.database.ContentObserver; +import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; @@ -91,6 +93,7 @@ import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException; import org.fdroid.fdroid.installer.Installer.InstallerCallback; import org.fdroid.fdroid.net.ApkDownloader; +import org.fdroid.fdroid.net.AsyncDownloader; import org.fdroid.fdroid.net.Downloader; import java.io.File; @@ -361,9 +364,16 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A private String getAppIdFromIntent() { Intent i = getIntent(); if (!i.hasExtra(EXTRA_APPID)) { + if (i.hasExtra(DownloadManager.EXTRA_DOWNLOAD_ID)) { + // we have been passed a DownloadManager download id, so get the app id for it + long downloadId = i.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); + return AsyncDownloader.getAppId(this, downloadId); + } + Log.e(TAG, "No application ID found in the intent!"); return null; } + return i.getStringExtra(EXTRA_APPID); } @@ -451,6 +461,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A refreshApkList(); refreshHeader(); supportInvalidateOptionsMenu(); + if (downloadHandler != null) { if (downloadHandler.isComplete()) { downloadCompleteInstallApk(); diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java index 0c22313f8..f8de9a802 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -197,7 +197,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { 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); + dlWrapper = new AsyncDownloader(context, this, curApk.apkName, curApk.id, remoteAddress, localFile); } else { Downloader downloader = DownloaderFactory.create(context, remoteAddress, localFile); dlWrapper = new AsyncDownloadWrapper(downloader, this); diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java index 72c4d2181..3f9f7cfd2 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java @@ -6,10 +6,12 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.ParcelFileDescriptor; +import org.fdroid.fdroid.AppDetails; import org.fdroid.fdroid.data.SanitizedFile; import java.io.ByteArrayOutputStream; @@ -30,6 +32,7 @@ public class AsyncDownloader extends AsyncDownloadWrapper { private SanitizedFile localFile; private String remoteAddress; private String appName; + private String appId; private Listener listener; private long downloadId = -1; @@ -43,10 +46,11 @@ public class AsyncDownloader extends AsyncDownloadWrapper { * * @param listener */ - public AsyncDownloader(Context context, Listener listener, String appName, String remoteAddress, SanitizedFile localFile) { + public AsyncDownloader(Context context, Listener listener, String appName, String appId, String remoteAddress, SanitizedFile localFile) { super(null, listener); this.context = context; this.appName = appName; + this.appId = appId; this.remoteAddress = remoteAddress; this.listener = listener; this.localFile = localFile; @@ -63,6 +67,7 @@ public class AsyncDownloader extends AsyncDownloadWrapper { // set up download request DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress)); request.setTitle(appName); + request.setDescription(appId); // we will retrieve this later from the description field if (listener != null) { IntentFilter intentFilter = new IntentFilter(); @@ -97,7 +102,10 @@ public class AsyncDownloader extends AsyncDownloadWrapper { 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 (listener == null) { + // without a listener, install UI won't come up, so ignore this + return; + } if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); @@ -135,4 +143,18 @@ public class AsyncDownloader extends AsyncDownloadWrapper { } } }; + + public static String getAppId(Context context, long downloadId) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadId); + Cursor c = dm.query(query); + if (c.moveToFirst()) { + // we use the description column to store the app id + int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); + return c.getString(columnIndex); + } + + return null; + } } diff --git a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java new file mode 100644 index 000000000..6f08e2323 --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java @@ -0,0 +1,22 @@ +package org.fdroid.fdroid.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.fdroid.fdroid.AppDetails; + +/** + * Receive notifications from the Android DownloadManager + */ +public class DownloadManagerReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + // pass the download manager broadcast onto the AppDetails screen and let it handle it + Intent appDetails = new Intent(context, AppDetails.class); + appDetails.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + appDetails.setAction(intent.getAction()); + appDetails.putExtras(intent.getExtras()); + context.startActivity(appDetails); + } +} From 11caf22dc6f24cb59ead4a0ebf75a17f0a583ecc Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Fri, 4 Sep 2015 13:43:47 +0200 Subject: [PATCH 04/20] wip: app details now reloads details of running downloads --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 31 +++- .../fdroid/fdroid/net/AsyncDownloader.java | 171 ++++++++++++------ 2 files changed, 136 insertions(+), 66 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index 7b53b151d..a5149501c 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -41,6 +41,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.ListFragment; import android.support.v4.app.NavUtils; @@ -438,6 +439,17 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A } localBroadcastManager = LocalBroadcastManager.getInstance(this); + + // Check if a download is running for this app + if (AsyncDownloader.isDownloading(this, app.id) >= 0) { + // call install to re-setup the listeners and downloaders + // the AsyncDownloader will not restart the download since the download is running + refreshHeader(); + refreshApkList(); + final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode); + install(apkToInstall); + } + } // The signature of the installed version. @@ -822,12 +834,8 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A if (downloadHandler != null && !downloadHandler.isComplete()) return; - final String[] projection = { RepoProvider.DataColumns.ADDRESS }; - Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection); - if (repo == null || repo.address == null) { - return; - } - final String repoaddress = repo.address; + final String repoaddress = getRepoAddress(apk); + if (repoaddress == null) return; if (!apk.compatible) { AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); @@ -869,6 +877,17 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A startDownload(apk, repoaddress); } + @Nullable + private String getRepoAddress(Apk apk) { + final String[] projection = { RepoProvider.DataColumns.ADDRESS }; + Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection); + if (repo == null || repo.address == null) { + return null; + } + final String repoaddress = repo.address; + return repoaddress; + } + private void startDownload(Apk apk, String repoAddress) { downloadHandler = new ApkDownloader(getBaseContext(), apk, repoAddress); localBroadcastManager.registerReceiver(downloaderProgressReceiver, diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java index 3f9f7cfd2..852406eae 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java @@ -12,6 +12,8 @@ import android.os.Build; import android.os.ParcelFileDescriptor; import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.SanitizedFile; import java.io.ByteArrayOutputStream; @@ -64,19 +66,39 @@ public class AsyncDownloader extends AsyncDownloadWrapper { @Override public void download() { + if ((downloadId = isDownloadComplete(context, appId)) > 0) { + // clear the notification + dm.remove(downloadId); + + try { + // write the downloaded file to the expected location + ParcelFileDescriptor fd = dm.openDownloadedFile(downloadId); + 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; + } + + downloadId = isDownloading(context, appId); + if (downloadId >= 0) return; + // set up download request DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress)); request.setTitle(appName); request.setDescription(appId); // we will retrieve this later from the description field - 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); + this.downloadId = dm.enqueue(request); } @Override @@ -94,67 +116,96 @@ public class AsyncDownloader extends AsyncDownloadWrapper { if (userRequested && downloadId >= 0) { dm.remove(downloadId); } - - context.unregisterReceiver(downloadReceiver); } - // 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) { - // without a listener, install UI won't come up, so ignore this - return; - } - - 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 - } - } - } - }; - + /** + * Extract the appId from a given download id. + * @param context + * @param downloadId + * @return - appId or null if not found + */ public static String getAppId(Context context, long downloadId) { DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); DownloadManager.Query query = new DownloadManager.Query(); query.setFilterById(downloadId); Cursor c = dm.query(query); - if (c.moveToFirst()) { - // we use the description column to store the app id - int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); - return c.getString(columnIndex); + + try { + if (c.moveToFirst()) { + // we use the description column to store the app id + int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); + return c.getString(columnIndex); + } + } finally { + c.close(); } return null; } + + /** + * Get the downloadId from an Intent sent by the DownloadManagerReceiver + * @param intent + * @return + */ + public static long getDownloadId(Intent intent) { + if (intent != null) { + return intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); + } else { + return -1; + } + } + + /** + * Check if a download is running for the app + * @param context + * @param appId + * @return -1 if not downloading, else the downloadId + */ + public static long isDownloading(Context context, String appId) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Query query = new DownloadManager.Query(); + Cursor c = dm.query(query); + int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); + int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID); + + try { + while (c.moveToNext()) { + if (appId.equals(c.getString(columnAppId))) { + return c.getLong(columnId); + } + } + } finally { + c.close(); + } + + return -1; + } + + /** + * Check if a download for an app is complete. + * @param context + * @param appId + * @return -1 if download is not complete, otherwise the download id + */ + public static long isDownloadComplete(Context context, String appId) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL); + Cursor c = dm.query(query); + int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); + int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID); + + try { + while (c.moveToNext()) { + if (appId.equals(c.getString(columnAppId))) { + return c.getLong(columnId); + } + } + } finally { + c.close(); + } + + return -1; + } } From 8a7feba9ccff246ca7d7de3caee04f61b1791387 Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Fri, 4 Sep 2015 13:46:43 +0200 Subject: [PATCH 05/20] drastically speed up debug build by not minifying and compressing resources. Release builds unaffected. --- F-Droid/build.gradle | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/F-Droid/build.gradle b/F-Droid/build.gradle index 1be738406..266b54d22 100644 --- a/F-Droid/build.gradle +++ b/F-Droid/build.gradle @@ -141,13 +141,17 @@ android { buildTypes { all { - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + minifyEnabled false + shrinkResources false } debug { debuggable true } + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } } compileOptions { From e827be1b5b389da7259fd5abff470cc0ac990a39 Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Fri, 4 Sep 2015 13:57:48 +0200 Subject: [PATCH 06/20] when user clicks on notification, the app details screen now comes up. If there are multiple downloads, the first one is shown. --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index a5149501c..90d400a66 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -371,6 +371,14 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A return AsyncDownloader.getAppId(this, downloadId); } + if (i.hasExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS)) { + // we have been passed a DownloadManager download id, so get the app id for it + long[] downloadIds = i.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS); + if (downloadIds != null && downloadIds.length > 0) { + return AsyncDownloader.getAppId(this, downloadIds[0]); + } + } + Log.e(TAG, "No application ID found in the intent!"); return null; } @@ -442,8 +450,9 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A // Check if a download is running for this app if (AsyncDownloader.isDownloading(this, app.id) >= 0) { - // call install to re-setup the listeners and downloaders - // the AsyncDownloader will not restart the download since the download is running + // call install() to re-setup the listeners and downloaders + // the AsyncDownloader will not restart the download since the download is running, + // and thus the version we pass to install() is not important refreshHeader(); refreshApkList(); final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode); From efd4ebeadf24a130f46a46d84dbf193db5cd7543 Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Fri, 4 Sep 2015 14:44:14 +0200 Subject: [PATCH 07/20] cleaned up code, prevented multiple app details screens popping up, fixed "no such app" toast after install --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 15 --------------- .../org/fdroid/fdroid/net/AsyncDownloader.java | 17 ++++++++++++++--- .../receiver/DownloadManagerReceiver.java | 8 +++++++- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index 90d400a66..981e01411 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -365,21 +365,6 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A private String getAppIdFromIntent() { Intent i = getIntent(); if (!i.hasExtra(EXTRA_APPID)) { - if (i.hasExtra(DownloadManager.EXTRA_DOWNLOAD_ID)) { - // we have been passed a DownloadManager download id, so get the app id for it - long downloadId = i.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); - return AsyncDownloader.getAppId(this, downloadId); - } - - if (i.hasExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS)) { - // we have been passed a DownloadManager download id, so get the app id for it - long[] downloadIds = i.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS); - if (downloadIds != null && downloadIds.length > 0) { - return AsyncDownloader.getAppId(this, downloadIds[0]); - } - } - - Log.e(TAG, "No application ID found in the intent!"); return null; } diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java index 852406eae..f6e63735f 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java @@ -150,10 +150,21 @@ public class AsyncDownloader extends AsyncDownloadWrapper { */ public static long getDownloadId(Intent intent) { if (intent != null) { - return intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); - } else { - return -1; + if (intent.hasExtra(DownloadManager.EXTRA_DOWNLOAD_ID)) { + // we have been passed a DownloadManager download id, so get the app id for it + return intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); + } + + if (intent.hasExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS)) { + // we have been passed a DownloadManager download id, so get the app id for it + long[] downloadIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS); + if (downloadIds != null && downloadIds.length > 0) { + return downloadIds[0]; + } + } } + + return -1; } /** diff --git a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java index 6f08e2323..913d1c4df 100644 --- a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java +++ b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.net.AsyncDownloader; /** * Receive notifications from the Android DownloadManager @@ -12,11 +13,16 @@ import org.fdroid.fdroid.AppDetails; public class DownloadManagerReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { + // work out the app Id to send to the AppDetails Screen + long downloadId = AsyncDownloader.getDownloadId(intent); + String appId = AsyncDownloader.getAppId(context, downloadId); + // pass the download manager broadcast onto the AppDetails screen and let it handle it Intent appDetails = new Intent(context, AppDetails.class); - appDetails.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + appDetails.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); appDetails.setAction(intent.getAction()); appDetails.putExtras(intent.getExtras()); + appDetails.putExtra(AppDetails.EXTRA_APPID, appId); context.startActivity(appDetails); } } From f3ef78d29204f7afe8059807b1d9ea0a01cbd21d Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Fri, 4 Sep 2015 16:16:49 +0200 Subject: [PATCH 08/20] reverted to previous version, for merge request. --- F-Droid/build.gradle | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/F-Droid/build.gradle b/F-Droid/build.gradle index 266b54d22..1be738406 100644 --- a/F-Droid/build.gradle +++ b/F-Droid/build.gradle @@ -141,17 +141,13 @@ android { buildTypes { all { - minifyEnabled false - shrinkResources false - } - debug { - debuggable true - } - release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } + debug { + debuggable true + } } compileOptions { From d10a56ed448905077d9ba62daa42b906d8084c92 Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Fri, 4 Sep 2015 16:54:54 +0200 Subject: [PATCH 09/20] re-inserted log statement mistakenly removed --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 1 + 1 file changed, 1 insertion(+) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index 981e01411..ed42c392a 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -365,6 +365,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A private String getAppIdFromIntent() { Intent i = getIntent(); if (!i.hasExtra(EXTRA_APPID)) { + Log.e(TAG, "No application ID found in the intent!"); return null; } From 6f8b49c974add78075f1e09359646946cf378f3f Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Sat, 5 Sep 2015 09:54:25 +0200 Subject: [PATCH 10/20] proper handling of file resources, implemented progress stats for async downloader --- .../fdroid/fdroid/net/AsyncDownloader.java | 68 ++++++++++++++++--- .../receiver/DownloadManagerReceiver.java | 1 + 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java index f6e63735f..57046e0c1 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java @@ -12,12 +12,14 @@ import android.os.Build; import android.os.ParcelFileDescriptor; import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.SanitizedFile; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -73,15 +75,7 @@ public class AsyncDownloader extends AsyncDownloadWrapper { try { // write the downloaded file to the expected location ParcelFileDescriptor fd = dm.openDownloadedFile(downloadId); - 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(); - + copyFile(fd.getFileDescriptor(), localFile); listener.onDownloadComplete(); } catch (IOException e) { listener.onErrorDownloading(e.getLocalizedMessage()); @@ -101,13 +95,67 @@ public class AsyncDownloader extends AsyncDownloadWrapper { this.downloadId = dm.enqueue(request); } + /** + * Copy input file to output file + * @param inputFile + * @param outputFile + * @throws IOException + */ + private void copyFile(FileDescriptor inputFile, SanitizedFile outputFile) throws IOException { + InputStream is = new FileInputStream(inputFile); + OutputStream os = new FileOutputStream(outputFile); + byte[] buffer = new byte[1024]; + int count = 0; + + try { + while ((count = is.read(buffer, 0, buffer.length)) > 0) { + os.write(buffer, 0, count); + } + } finally { + os.close(); + is.close(); + } + } + @Override public int getBytesRead() { + if (downloadId < 0) return 0; + + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadId); + Cursor c = dm.query(query); + + try { + if (c.moveToFirst()) { + // we use the description column to store the app id + int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); + return c.getInt(columnIndex); + } + } finally { + c.close(); + } + return 0; } @Override public int getTotalBytes() { + if (downloadId < 0) return 0; + + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadId); + Cursor c = dm.query(query); + + try { + if (c.moveToFirst()) { + // we use the description column to store the app id + int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); + return c.getInt(columnIndex); + } + } finally { + c.close(); + } + return 0; } @@ -156,7 +204,7 @@ public class AsyncDownloader extends AsyncDownloadWrapper { } if (intent.hasExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS)) { - // we have been passed a DownloadManager download id, so get the app id for it + // we have been passed multiple download id's - just return the first one long[] downloadIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS); if (downloadIds != null && downloadIds.length > 0) { return downloadIds[0]; diff --git a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java index 913d1c4df..0a689b40d 100644 --- a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java +++ b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.receiver; +import android.app.DownloadManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; From 13e54ced07b0a0ca0654adcf35b9e40ece741104 Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Sat, 5 Sep 2015 10:02:42 +0200 Subject: [PATCH 11/20] Display of app name and version in the download title --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 2 +- F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index ed42c392a..945fbbd14 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -884,7 +884,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A } private void startDownload(Apk apk, String repoAddress) { - downloadHandler = new ApkDownloader(getBaseContext(), apk, repoAddress); + downloadHandler = new ApkDownloader(getBaseContext(), app, apk, repoAddress); localBroadcastManager.registerReceiver(downloaderProgressReceiver, new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS)); downloadHandler.setProgressListener(this); diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java index f8de9a802..7b1192497 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -34,6 +34,7 @@ import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.compat.FileCompat; import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.SanitizedFile; import java.io.File; @@ -69,6 +70,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { */ public static final String EVENT_DATA_ERROR_TYPE = "apkDownloadErrorType"; + @NonNull private final App app; @NonNull private final Apk curApk; @NonNull private final Context context; @NonNull private final String repoAddress; @@ -89,8 +91,9 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { setProgressListener(null); } - public ApkDownloader(@NonNull final Context context, @NonNull final Apk apk, @NonNull final String repoAddress) { + public ApkDownloader(@NonNull final Context context, @NonNull final App app, @NonNull final Apk apk, @NonNull final String repoAddress) { this.context = context; + this.app = app; curApk = apk; this.repoAddress = repoAddress; localFile = new SanitizedFile(Utils.getApkDownloadDir(context), apk.apkName); @@ -197,7 +200,9 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { 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, curApk.id, remoteAddress, localFile); + dlWrapper = new AsyncDownloader(context, this, + app.name + " " + curApk.version, curApk.id, + remoteAddress, localFile); } else { Downloader downloader = DownloaderFactory.create(context, remoteAddress, localFile); dlWrapper = new AsyncDownloadWrapper(downloader, this); From ef40b5f3db3b00200754fba1d5167a82297ea4d1 Mon Sep 17 00:00:00 2001 From: Toby Kurien Date: Sat, 5 Sep 2015 11:54:17 +0200 Subject: [PATCH 12/20] Now displays a notification when download is complete, unless use is already on the app details screen, in which case it pops up the installer immediately --- F-Droid/res/values/strings.xml | 1 + .../fdroid/fdroid/net/AsyncDownloader.java | 81 +++++++++++++++++-- .../receiver/DownloadManagerReceiver.java | 52 ++++++++++-- 3 files changed, 118 insertions(+), 16 deletions(-) diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml index 737cbdc04..5326ce92c 100644 --- a/F-Droid/res/values/strings.xml +++ b/F-Droid/res/values/strings.xml @@ -389,6 +389,7 @@ this may cost you money Do you want to replace this app with the factory version? Do you want to uninstall this app? + Download completed, tap to install NEW: Provided by %1$s. diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java index 57046e0c1..0f49c3e1d 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java @@ -10,6 +10,8 @@ import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.ParcelFileDescriptor; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; import org.fdroid.fdroid.AppDetails; import org.fdroid.fdroid.ProgressListener; @@ -68,6 +70,7 @@ public class AsyncDownloader extends AsyncDownloadWrapper { @Override public void download() { + // Check if the download is complete if ((downloadId = isDownloadComplete(context, appId)) > 0) { // clear the notification dm.remove(downloadId); @@ -80,19 +83,25 @@ public class AsyncDownloader extends AsyncDownloadWrapper { } catch (IOException e) { listener.onErrorDownloading(e.getLocalizedMessage()); } - return; } - downloadId = isDownloading(context, appId); - if (downloadId >= 0) return; + // Check if the download is still in progress + if (downloadId < 0) { + downloadId = isDownloading(context, appId); + } - // set up download request - DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress)); - request.setTitle(appName); - request.setDescription(appId); // we will retrieve this later from the description field + // Start a new download + if (downloadId < 0) { + // set up download request + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress)); + request.setTitle(appName); + request.setDescription(appId); // we will retrieve this later from the description field + this.downloadId = dm.enqueue(request); + } - this.downloadId = dm.enqueue(request); + context.registerReceiver(receiver, + new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } /** @@ -161,6 +170,12 @@ public class AsyncDownloader extends AsyncDownloadWrapper { @Override public void attemptCancel(boolean userRequested) { + try { + context.unregisterReceiver(receiver); + } catch (Exception e) { + // ignore if receiver already unregistered + } + if (userRequested && downloadId >= 0) { dm.remove(downloadId); } @@ -191,6 +206,31 @@ public class AsyncDownloader extends AsyncDownloadWrapper { return null; } + /** + * Extract the download title from a given download id. + * @param context + * @param downloadId + * @return - title or null if not found + */ + public static String getDownloadTitle(Context context, long downloadId) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadId); + Cursor c = dm.query(query); + + try { + if (c.moveToFirst()) { + // we use the description column to store the app id + int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TITLE); + return c.getString(columnIndex); + } + } finally { + c.close(); + } + + return null; + } + /** * Get the downloadId from an Intent sent by the DownloadManagerReceiver * @param intent @@ -267,4 +307,29 @@ public class AsyncDownloader extends AsyncDownloadWrapper { return -1; } + + /** + * Broadcast receiver to listen for ACTION_DOWNLOAD_COMPLETE broadcasts + */ + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { + long dId = getDownloadId(intent); + String appId = getAppId(context, dId); + if (listener != null && dId == downloadId && appId != null) { + // our current download has just completed, so let's throw up install dialog + // immediately + try { + context.unregisterReceiver(receiver); + } catch (Exception e) { + // ignore if receiver already unregistered + } + + // call download() to copy the file and start the installer + download(); + } + } + } + }; } diff --git a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java index 0a689b40d..0194a0aaf 100644 --- a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java +++ b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java @@ -1,15 +1,21 @@ package org.fdroid.fdroid.receiver; import android.app.DownloadManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.support.v4.app.NotificationCompat; import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.R; import org.fdroid.fdroid.net.AsyncDownloader; /** - * Receive notifications from the Android DownloadManager + * Receive notifications from the Android DownloadManager and pass them onto the + * AppDetails activity */ public class DownloadManagerReceiver extends BroadcastReceiver { @Override @@ -18,12 +24,42 @@ public class DownloadManagerReceiver extends BroadcastReceiver { long downloadId = AsyncDownloader.getDownloadId(intent); String appId = AsyncDownloader.getAppId(context, downloadId); - // pass the download manager broadcast onto the AppDetails screen and let it handle it - Intent appDetails = new Intent(context, AppDetails.class); - appDetails.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - appDetails.setAction(intent.getAction()); - appDetails.putExtras(intent.getExtras()); - appDetails.putExtra(AppDetails.EXTRA_APPID, appId); - context.startActivity(appDetails); + if (appId == null) { + // bogus broadcast (e.g. download cancelled, but system sent a DOWNLOAD_COMPLETE) + return; + } + + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { + // show a notification the user can click to install the app + Intent appDetails = new Intent(context, AppDetails.class); + appDetails.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + appDetails.setAction(intent.getAction()); + appDetails.putExtras(intent.getExtras()); + appDetails.putExtra(AppDetails.EXTRA_APPID, appId); + + PendingIntent pi = PendingIntent.getActivity( + context, 1, appDetails, PendingIntent.FLAG_ONE_SHOT); + + // launch LocalRepoActivity if the user selects this notification + String downloadTitle = AsyncDownloader.getDownloadTitle(context, downloadId); + Notification notif = new NotificationCompat.Builder(context) + .setContentTitle(downloadTitle) + .setContentText(context.getString(R.string.tap_to_install)) + .setSmallIcon(R.drawable.ic_stat_notify) + .setContentIntent(pi) + .setAutoCancel(true) + .build(); + + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify((int)downloadId, notif); + } else if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(intent.getAction())) { + // pass the notification click onto the AppDetails screen and let it handle it + Intent appDetails = new Intent(context, AppDetails.class); + appDetails.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + appDetails.setAction(intent.getAction()); + appDetails.putExtras(intent.getExtras()); + appDetails.putExtra(AppDetails.EXTRA_APPID, appId); + context.startActivity(appDetails); + } } } From 9b7c4c7b4afff861fd3154e0b0393133a11f2ddc Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 9 Sep 2015 07:32:06 +1000 Subject: [PATCH 13/20] WIP: CR. --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index 945fbbd14..7fa611d87 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -879,8 +879,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A if (repo == null || repo.address == null) { return null; } - final String repoaddress = repo.address; - return repoaddress; + return repo.address; } private void startDownload(Apk apk, String repoAddress) { From 69ecaf023fb922ef37ff72168c04248165144c73 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 9 Sep 2015 08:31:21 +1000 Subject: [PATCH 14/20] Refactored AsyncDownloader to only ever be constructed by DownloadFactory. --- F-Droid/src/org/fdroid/fdroid/AppDetails.java | 6 +- .../org/fdroid/fdroid/net/ApkDownloader.java | 27 +- .../fdroid/net/AsyncDownloadWrapper.java | 114 ------ .../fdroid/fdroid/net/AsyncDownloader.java | 359 ++++-------------- .../net/AsyncDownloaderFromAndroid.java | 326 ++++++++++++++++ .../fdroid/fdroid/net/DownloaderFactory.java | 24 ++ .../receiver/DownloadManagerReceiver.java | 8 +- 7 files changed, 428 insertions(+), 436 deletions(-) delete mode 100644 F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java create mode 100644 F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java index 7fa611d87..a5c7e02a4 100644 --- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java +++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java @@ -22,7 +22,6 @@ package org.fdroid.fdroid; import android.app.Activity; -import android.app.DownloadManager; import android.bluetooth.BluetoothAdapter; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; @@ -35,7 +34,6 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; import android.database.ContentObserver; -import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; @@ -94,7 +92,7 @@ import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException; import org.fdroid.fdroid.installer.Installer.InstallerCallback; import org.fdroid.fdroid.net.ApkDownloader; -import org.fdroid.fdroid.net.AsyncDownloader; +import org.fdroid.fdroid.net.AsyncDownloaderFromAndroid; import org.fdroid.fdroid.net.Downloader; import java.io.File; @@ -435,7 +433,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A localBroadcastManager = LocalBroadcastManager.getInstance(this); // Check if a download is running for this app - if (AsyncDownloader.isDownloading(this, app.id) >= 0) { + if (AsyncDownloaderFromAndroid.isDownloading(this, app.id) >= 0) { // call install() to re-setup the listeners and downloaders // the AsyncDownloader will not restart the download since the download is running, // and thus the version we pass to install() is not important diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java index 7b1192497..c743ebd3e 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -47,7 +47,7 @@ import java.security.NoSuchAlgorithmException; * If the file has previously been downloaded, it will make use of that * instead, without going to the network to download a new one. */ -public class ApkDownloader implements AsyncDownloadWrapper.Listener { +public class ApkDownloader implements AsyncDownloader.Listener { private static final String TAG = "ApkDownloader"; @@ -78,7 +78,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { @NonNull private final SanitizedFile potentiallyCachedFile; private ProgressListener listener; - private AsyncDownloadWrapper dlWrapper = null; + private AsyncDownloader dlWrapper = null; private boolean isComplete = false; private final long id = ++downloadIdCounter; @@ -197,17 +197,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener { Utils.DebugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile); try { - 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, - app.name + " " + curApk.version, curApk.id, - remoteAddress, localFile); - } else { - Downloader downloader = DownloaderFactory.create(context, remoteAddress, localFile); - dlWrapper = new AsyncDownloadWrapper(downloader, this); - } - + dlWrapper = DownloaderFactory.createAsync(context, remoteAddress, localFile, app.name + " " + curApk.version, curApk.id, this); dlWrapper.download(); return true; } catch (IOException e) { @@ -217,17 +207,6 @@ 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/AsyncDownloadWrapper.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java deleted file mode 100644 index 789a8ae18..000000000 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.fdroid.fdroid.net; - -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.util.Log; - -import org.fdroid.fdroid.ProgressListener; - -import java.io.IOException; - -/** - * Given a {@link org.fdroid.fdroid.net.Downloader}, this wrapper will conduct the download operation on a - * separate thread. All progress/status/error/etc events will be forwarded from that thread to the thread - * that {@link AsyncDownloadWrapper#download()} was invoked on. If you want to respond with UI feedback - * to these events, it is important that you execute the download method of this class from the UI thread. - * That way, all forwarded events will be handled on that thread. - */ -@SuppressWarnings("serial") -public class AsyncDownloadWrapper extends Handler { - - private static final String TAG = "AsyncDownloadWrapper"; - - private static final int MSG_DOWNLOAD_COMPLETE = 2; - private static final int MSG_DOWNLOAD_CANCELLED = 3; - private static final int MSG_ERROR = 4; - private static final String MSG_DATA = "data"; - - private final Downloader downloader; - private final Listener listener; - private DownloadThread downloadThread = null; - - /** - * 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. - */ - public AsyncDownloadWrapper(Downloader downloader, Listener listener) { - this.downloader = downloader; - this.listener = listener; - } - - public void download() { - downloadThread = new DownloadThread(); - downloadThread.start(); - } - - public void attemptCancel(boolean userRequested) { - if (downloadThread != null) { - downloadThread.interrupt(); - } - } - - /** - * Receives "messages" from the download thread, and passes them onto the - * relevant {@link org.fdroid.fdroid.net.AsyncDownloadWrapper.Listener} - * @param message - */ - public void handleMessage(Message message) { - switch (message.arg1) { - case MSG_DOWNLOAD_COMPLETE: - listener.onDownloadComplete(); - break; - case MSG_DOWNLOAD_CANCELLED: - listener.onDownloadCancelled(); - break; - case MSG_ERROR: - listener.onErrorDownloading(message.getData().getString(MSG_DATA)); - break; - } - } - - public int getBytesRead() { - return downloader.getBytesRead(); - } - - public int getTotalBytes() { - return downloader.getTotalBytes(); - } - - public interface Listener extends ProgressListener { - void onErrorDownloading(String localisedExceptionDetails); - void onDownloadComplete(); - void onDownloadCancelled(); - } - - private class DownloadThread extends Thread { - - public void run() { - try { - downloader.download(); - sendMessage(MSG_DOWNLOAD_COMPLETE); - } catch (InterruptedException e) { - sendMessage(MSG_DOWNLOAD_CANCELLED); - } catch (IOException e) { - Log.e(TAG, "I/O exception in download thread", e); - Bundle data = new Bundle(1); - data.putString(MSG_DATA, e.getLocalizedMessage()); - Message message = new Message(); - message.arg1 = MSG_ERROR; - message.setData(data); - AsyncDownloadWrapper.this.sendMessage(message); - } - } - - private void sendMessage(int messageType) { - Message message = new Message(); - message.arg1 = messageType; - AsyncDownloadWrapper.this.sendMessage(message); - } - } -} diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java index 0f49c3e1d..2589ad709 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java @@ -1,47 +1,34 @@ 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.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.support.v4.content.LocalBroadcastManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; import android.util.Log; -import org.fdroid.fdroid.AppDetails; import org.fdroid.fdroid.ProgressListener; -import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.SanitizedFile; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileDescriptor; -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. + * Given a {@link org.fdroid.fdroid.net.Downloader}, this wrapper will conduct the download operation on a + * separate thread. All progress/status/error/etc events will be forwarded from that thread to the thread + * that {@link AsyncDownloader#download()} was invoked on. If you want to respond with UI feedback + * to these events, it is important that you execute the download method of this class from the UI thread. + * That way, all forwarded events will be handled on that thread. */ -@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 String appId; - private Listener listener; +@SuppressWarnings("serial") +public class AsyncDownloader extends Handler { - private long downloadId = -1; + private static final String TAG = "AsyncDownloadWrapper"; + + private static final int MSG_DOWNLOAD_COMPLETE = 2; + private static final int MSG_DOWNLOAD_CANCELLED = 3; + private static final int MSG_ERROR = 4; + private static final String MSG_DATA = "data"; + + private final Downloader downloader; + private final Listener listener; + private DownloadThread downloadThread = null; /** * Normally the listener would be provided using a setListener method. @@ -49,287 +36,79 @@ public class AsyncDownloader extends AsyncDownloadWrapper { * 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 appId, String remoteAddress, SanitizedFile localFile) { - super(null, listener); - this.context = context; - this.appName = appName; - this.appId = appId; - 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); + public AsyncDownloader(Downloader downloader, Listener listener) { + this.downloader = downloader; + this.listener = listener; } - @Override public void download() { - // Check if the download is complete - if ((downloadId = isDownloadComplete(context, appId)) > 0) { - // clear the notification - dm.remove(downloadId); - - try { - // write the downloaded file to the expected location - ParcelFileDescriptor fd = dm.openDownloadedFile(downloadId); - copyFile(fd.getFileDescriptor(), localFile); - listener.onDownloadComplete(); - } catch (IOException e) { - listener.onErrorDownloading(e.getLocalizedMessage()); - } - return; - } - - // Check if the download is still in progress - if (downloadId < 0) { - downloadId = isDownloading(context, appId); - } - - // Start a new download - if (downloadId < 0) { - // set up download request - DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress)); - request.setTitle(appName); - request.setDescription(appId); // we will retrieve this later from the description field - this.downloadId = dm.enqueue(request); - } - - context.registerReceiver(receiver, - new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + downloadThread = new DownloadThread(); + downloadThread.start(); } - /** - * Copy input file to output file - * @param inputFile - * @param outputFile - * @throws IOException - */ - private void copyFile(FileDescriptor inputFile, SanitizedFile outputFile) throws IOException { - InputStream is = new FileInputStream(inputFile); - OutputStream os = new FileOutputStream(outputFile); - byte[] buffer = new byte[1024]; - int count = 0; - - try { - while ((count = is.read(buffer, 0, buffer.length)) > 0) { - os.write(buffer, 0, count); - } - } finally { - os.close(); - is.close(); - } - } - - @Override - public int getBytesRead() { - if (downloadId < 0) return 0; - - DownloadManager.Query query = new DownloadManager.Query(); - query.setFilterById(downloadId); - Cursor c = dm.query(query); - - try { - if (c.moveToFirst()) { - // we use the description column to store the app id - int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); - return c.getInt(columnIndex); - } - } finally { - c.close(); - } - - return 0; - } - - @Override - public int getTotalBytes() { - if (downloadId < 0) return 0; - - DownloadManager.Query query = new DownloadManager.Query(); - query.setFilterById(downloadId); - Cursor c = dm.query(query); - - try { - if (c.moveToFirst()) { - // we use the description column to store the app id - int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); - return c.getInt(columnIndex); - } - } finally { - c.close(); - } - - return 0; - } - - @Override public void attemptCancel(boolean userRequested) { - try { - context.unregisterReceiver(receiver); - } catch (Exception e) { - // ignore if receiver already unregistered - } - - if (userRequested && downloadId >= 0) { - dm.remove(downloadId); + if (downloadThread != null) { + downloadThread.interrupt(); } } /** - * Extract the appId from a given download id. - * @param context - * @param downloadId - * @return - appId or null if not found + * Receives "messages" from the download thread, and passes them onto the + * relevant {@link AsyncDownloader.Listener} + * @param message */ - public static String getAppId(Context context, long downloadId) { - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - DownloadManager.Query query = new DownloadManager.Query(); - query.setFilterById(downloadId); - Cursor c = dm.query(query); - - try { - if (c.moveToFirst()) { - // we use the description column to store the app id - int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); - return c.getString(columnIndex); - } - } finally { - c.close(); + public void handleMessage(Message message) { + switch (message.arg1) { + case MSG_DOWNLOAD_COMPLETE: + listener.onDownloadComplete(); + break; + case MSG_DOWNLOAD_CANCELLED: + listener.onDownloadCancelled(); + break; + case MSG_ERROR: + listener.onErrorDownloading(message.getData().getString(MSG_DATA)); + break; } - - return null; } - /** - * Extract the download title from a given download id. - * @param context - * @param downloadId - * @return - title or null if not found - */ - public static String getDownloadTitle(Context context, long downloadId) { - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - DownloadManager.Query query = new DownloadManager.Query(); - query.setFilterById(downloadId); - Cursor c = dm.query(query); - - try { - if (c.moveToFirst()) { - // we use the description column to store the app id - int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TITLE); - return c.getString(columnIndex); - } - } finally { - c.close(); - } - - return null; + public int getBytesRead() { + return downloader.getBytesRead(); } - /** - * Get the downloadId from an Intent sent by the DownloadManagerReceiver - * @param intent - * @return - */ - public static long getDownloadId(Intent intent) { - if (intent != null) { - if (intent.hasExtra(DownloadManager.EXTRA_DOWNLOAD_ID)) { - // we have been passed a DownloadManager download id, so get the app id for it - return intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); - } - - if (intent.hasExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS)) { - // we have been passed multiple download id's - just return the first one - long[] downloadIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS); - if (downloadIds != null && downloadIds.length > 0) { - return downloadIds[0]; - } - } - } - - return -1; + public int getTotalBytes() { + return downloader.getTotalBytes(); } - /** - * Check if a download is running for the app - * @param context - * @param appId - * @return -1 if not downloading, else the downloadId - */ - public static long isDownloading(Context context, String appId) { - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - DownloadManager.Query query = new DownloadManager.Query(); - Cursor c = dm.query(query); - int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); - int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID); - - try { - while (c.moveToNext()) { - if (appId.equals(c.getString(columnAppId))) { - return c.getLong(columnId); - } - } - } finally { - c.close(); - } - - return -1; + public interface Listener extends ProgressListener { + void onErrorDownloading(String localisedExceptionDetails); + void onDownloadComplete(); + void onDownloadCancelled(); } - /** - * Check if a download for an app is complete. - * @param context - * @param appId - * @return -1 if download is not complete, otherwise the download id - */ - public static long isDownloadComplete(Context context, String appId) { - DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - DownloadManager.Query query = new DownloadManager.Query(); - query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL); - Cursor c = dm.query(query); - int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); - int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID); + private class DownloadThread extends Thread { - try { - while (c.moveToNext()) { - if (appId.equals(c.getString(columnAppId))) { - return c.getLong(columnId); - } + public void run() { + try { + downloader.download(); + sendMessage(MSG_DOWNLOAD_COMPLETE); + } catch (InterruptedException e) { + sendMessage(MSG_DOWNLOAD_CANCELLED); + } catch (IOException e) { + Log.e(TAG, "I/O exception in download thread", e); + Bundle data = new Bundle(1); + data.putString(MSG_DATA, e.getLocalizedMessage()); + Message message = new Message(); + message.arg1 = MSG_ERROR; + message.setData(data); + AsyncDownloader.this.sendMessage(message); } - } finally { - c.close(); } - return -1; + private void sendMessage(int messageType) { + Message message = new Message(); + message.arg1 = messageType; + AsyncDownloader.this.sendMessage(message); + } } - - /** - * Broadcast receiver to listen for ACTION_DOWNLOAD_COMPLETE broadcasts - */ - BroadcastReceiver receiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { - long dId = getDownloadId(intent); - String appId = getAppId(context, dId); - if (listener != null && dId == downloadId && appId != null) { - // our current download has just completed, so let's throw up install dialog - // immediately - try { - context.unregisterReceiver(receiver); - } catch (Exception e) { - // ignore if receiver already unregistered - } - - // call download() to copy the file and start the installer - download(); - } - } - } - }; } diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java new file mode 100644 index 000000000..d015c5033 --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java @@ -0,0 +1,326 @@ +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.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.ParcelFileDescriptor; + +import java.io.File; +import java.io.FileDescriptor; +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 AsyncDownloaderFromAndroid extends AsyncDownloader { + private final Context context; + private final DownloadManager dm; + private File localFile; + private String remoteAddress; + private String appName; + private String appId; + 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 AsyncDownloaderFromAndroid(Context context, Listener listener, String appName, String appId, String remoteAddress, File localFile) { + super(null, listener); + this.context = context; + this.appName = appName; + this.appId = appId; + 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() { + // Check if the download is complete + if ((downloadId = isDownloadComplete(context, appId)) > 0) { + // clear the notification + dm.remove(downloadId); + + try { + // write the downloaded file to the expected location + ParcelFileDescriptor fd = dm.openDownloadedFile(downloadId); + copyFile(fd.getFileDescriptor(), localFile); + listener.onDownloadComplete(); + } catch (IOException e) { + listener.onErrorDownloading(e.getLocalizedMessage()); + } + return; + } + + // Check if the download is still in progress + if (downloadId < 0) { + downloadId = isDownloading(context, appId); + } + + // Start a new download + if (downloadId < 0) { + // set up download request + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress)); + request.setTitle(appName); + request.setDescription(appId); // we will retrieve this later from the description field + this.downloadId = dm.enqueue(request); + } + + context.registerReceiver(receiver, + new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + } + + /** + * Copy input file to output file + * @param inputFile + * @param outputFile + * @throws IOException + */ + private void copyFile(FileDescriptor inputFile, File outputFile) throws IOException { + InputStream is = new FileInputStream(inputFile); + OutputStream os = new FileOutputStream(outputFile); + byte[] buffer = new byte[1024]; + int count = 0; + + try { + while ((count = is.read(buffer, 0, buffer.length)) > 0) { + os.write(buffer, 0, count); + } + } finally { + os.close(); + is.close(); + } + } + + @Override + public int getBytesRead() { + if (downloadId < 0) return 0; + + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadId); + Cursor c = dm.query(query); + + try { + if (c.moveToFirst()) { + // we use the description column to store the app id + int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); + return c.getInt(columnIndex); + } + } finally { + c.close(); + } + + return 0; + } + + @Override + public int getTotalBytes() { + if (downloadId < 0) return 0; + + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadId); + Cursor c = dm.query(query); + + try { + if (c.moveToFirst()) { + // we use the description column to store the app id + int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); + return c.getInt(columnIndex); + } + } finally { + c.close(); + } + + return 0; + } + + @Override + public void attemptCancel(boolean userRequested) { + try { + context.unregisterReceiver(receiver); + } catch (Exception e) { + // ignore if receiver already unregistered + } + + if (userRequested && downloadId >= 0) { + dm.remove(downloadId); + } + } + + /** + * Extract the appId from a given download id. + * @param context + * @param downloadId + * @return - appId or null if not found + */ + public static String getAppId(Context context, long downloadId) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadId); + Cursor c = dm.query(query); + + try { + if (c.moveToFirst()) { + // we use the description column to store the app id + int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); + return c.getString(columnIndex); + } + } finally { + c.close(); + } + + return null; + } + + /** + * Extract the download title from a given download id. + * @param context + * @param downloadId + * @return - title or null if not found + */ + public static String getDownloadTitle(Context context, long downloadId) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadId); + Cursor c = dm.query(query); + + try { + if (c.moveToFirst()) { + // we use the description column to store the app id + int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TITLE); + return c.getString(columnIndex); + } + } finally { + c.close(); + } + + return null; + } + + /** + * Get the downloadId from an Intent sent by the DownloadManagerReceiver + * @param intent + * @return + */ + public static long getDownloadId(Intent intent) { + if (intent != null) { + if (intent.hasExtra(DownloadManager.EXTRA_DOWNLOAD_ID)) { + // we have been passed a DownloadManager download id, so get the app id for it + return intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); + } + + if (intent.hasExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS)) { + // we have been passed multiple download id's - just return the first one + long[] downloadIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS); + if (downloadIds != null && downloadIds.length > 0) { + return downloadIds[0]; + } + } + } + + return -1; + } + + /** + * Check if a download is running for the app + * @param context + * @param appId + * @return -1 if not downloading, else the downloadId + */ + public static long isDownloading(Context context, String appId) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Query query = new DownloadManager.Query(); + Cursor c = dm.query(query); + int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); + int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID); + + try { + while (c.moveToNext()) { + if (appId.equals(c.getString(columnAppId))) { + return c.getLong(columnId); + } + } + } finally { + c.close(); + } + + return -1; + } + + /** + * Check if a download for an app is complete. + * @param context + * @param appId + * @return -1 if download is not complete, otherwise the download id + */ + public static long isDownloadComplete(Context context, String appId) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL); + Cursor c = dm.query(query); + int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); + int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID); + + try { + while (c.moveToNext()) { + if (appId.equals(c.getString(columnAppId))) { + return c.getLong(columnId); + } + } + } finally { + c.close(); + } + + return -1; + } + + /** + * Broadcast receiver to listen for ACTION_DOWNLOAD_COMPLETE broadcasts + */ + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { + long dId = getDownloadId(intent); + String appId = getAppId(context, dId); + if (listener != null && dId == downloadId && appId != null) { + // our current download has just completed, so let's throw up install dialog + // immediately + try { + context.unregisterReceiver(receiver); + } catch (Exception e) { + // ignore if receiver already unregistered + } + + // call download() to copy the file and start the installer + download(); + } + } + } + }; +} diff --git a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java index 757df7f20..575e141af 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java @@ -1,9 +1,11 @@ package org.fdroid.fdroid.net; import android.content.Context; +import android.os.Build; import java.io.File; import java.io.IOException; +import java.net.MalformedURLException; import java.net.URL; public class DownloaderFactory { @@ -51,7 +53,29 @@ public class DownloaderFactory { return "bluetooth".equalsIgnoreCase(url.getProtocol()); } + public static AsyncDownloader createAsync(Context context, String urlString, File destFile, String title, String id, AsyncDownloader.Listener listener) throws IOException { + return createAsync(context, new URL(urlString), destFile, title, id, listener); + } + + public static AsyncDownloader createAsync(Context context, URL url, File destFile, String title, String id, AsyncDownloader.Listener listener) + throws IOException { + if (canUseDownloadManager(url)) { + return new AsyncDownloaderFromAndroid(context, listener, title, id, url.toString(), destFile); + } else { + return new AsyncDownloader(create(context, url, destFile), listener); + } + } + static boolean isOnionAddress(URL url) { return url.getHost().endsWith(".onion"); } + + /** + * Tests to see if we can use Android's DownloadManager to download the APK, instead of + * a downloader returned from DownloadFactory. + */ + private static boolean canUseDownloadManager(URL url) { + return Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO && !isOnionAddress(url); + } + } diff --git a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java index 0194a0aaf..3ba5e6d34 100644 --- a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java +++ b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java @@ -11,7 +11,7 @@ import android.support.v4.app.NotificationCompat; import org.fdroid.fdroid.AppDetails; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.net.AsyncDownloader; +import org.fdroid.fdroid.net.AsyncDownloaderFromAndroid; /** * Receive notifications from the Android DownloadManager and pass them onto the @@ -21,8 +21,8 @@ public class DownloadManagerReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // work out the app Id to send to the AppDetails Screen - long downloadId = AsyncDownloader.getDownloadId(intent); - String appId = AsyncDownloader.getAppId(context, downloadId); + long downloadId = AsyncDownloaderFromAndroid.getDownloadId(intent); + String appId = AsyncDownloaderFromAndroid.getAppId(context, downloadId); if (appId == null) { // bogus broadcast (e.g. download cancelled, but system sent a DOWNLOAD_COMPLETE) @@ -41,7 +41,7 @@ public class DownloadManagerReceiver extends BroadcastReceiver { context, 1, appDetails, PendingIntent.FLAG_ONE_SHOT); // launch LocalRepoActivity if the user selects this notification - String downloadTitle = AsyncDownloader.getDownloadTitle(context, downloadId); + String downloadTitle = AsyncDownloaderFromAndroid.getDownloadTitle(context, downloadId); Notification notif = new NotificationCompat.Builder(context) .setContentTitle(downloadTitle) .setContentText(context.getString(R.string.tap_to_install)) From d0d287f668b317673ff8c29a90bc95314bb4853e Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 9 Sep 2015 16:57:01 +1000 Subject: [PATCH 15/20] Remove undocumented params from JavaDoc. Android Studio by default warns about undocumented params, which makes it harder to identify more problematic warnings to do with actual code problems. This warning could be toned down in the IDE so that it doesn't complain, but equally, the params are not neccesary in JavaDoc if they are undocumented, and don't end up adding any more than the parameters themselves. --- .../fdroid/net/AsyncDownloaderFromAndroid.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java index d015c5033..d75574a8a 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java @@ -40,8 +40,6 @@ public class AsyncDownloaderFromAndroid extends AsyncDownloader { * 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 AsyncDownloaderFromAndroid(Context context, Listener listener, String appName, String appId, String remoteAddress, File localFile) { super(null, listener); @@ -97,8 +95,6 @@ public class AsyncDownloaderFromAndroid extends AsyncDownloader { /** * Copy input file to output file - * @param inputFile - * @param outputFile * @throws IOException */ private void copyFile(FileDescriptor inputFile, File outputFile) throws IOException { @@ -174,8 +170,6 @@ public class AsyncDownloaderFromAndroid extends AsyncDownloader { /** * Extract the appId from a given download id. - * @param context - * @param downloadId * @return - appId or null if not found */ public static String getAppId(Context context, long downloadId) { @@ -199,8 +193,6 @@ public class AsyncDownloaderFromAndroid extends AsyncDownloader { /** * Extract the download title from a given download id. - * @param context - * @param downloadId * @return - title or null if not found */ public static String getDownloadTitle(Context context, long downloadId) { @@ -224,8 +216,6 @@ public class AsyncDownloaderFromAndroid extends AsyncDownloader { /** * Get the downloadId from an Intent sent by the DownloadManagerReceiver - * @param intent - * @return */ public static long getDownloadId(Intent intent) { if (intent != null) { @@ -248,8 +238,6 @@ public class AsyncDownloaderFromAndroid extends AsyncDownloader { /** * Check if a download is running for the app - * @param context - * @param appId * @return -1 if not downloading, else the downloadId */ public static long isDownloading(Context context, String appId) { @@ -274,8 +262,6 @@ public class AsyncDownloaderFromAndroid extends AsyncDownloader { /** * Check if a download for an app is complete. - * @param context - * @param appId * @return -1 if download is not complete, otherwise the download id */ public static long isDownloadComplete(Context context, String appId) { From 0a9941d93d5757867851cac636d0f3b24f988414 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 9 Sep 2015 17:13:30 +1000 Subject: [PATCH 16/20] Refactor AsyncDownloader to be an interface. The interface is used by both AsyncDownloadWrapper and AsyncDownloaderFromAndroid. --- .../fdroid/net/AsyncDownloadWrapper.java | 98 +++++++++++++++++ .../fdroid/fdroid/net/AsyncDownloader.java | 101 ++---------------- .../net/AsyncDownloaderFromAndroid.java | 3 +- .../fdroid/fdroid/net/DownloaderFactory.java | 3 +- 4 files changed, 106 insertions(+), 99 deletions(-) create mode 100644 F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java new file mode 100644 index 000000000..36021300b --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java @@ -0,0 +1,98 @@ +package org.fdroid.fdroid.net; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; + +class AsyncDownloadWrapper extends Handler implements AsyncDownloader { + + private static final String TAG = "AsyncDownloadWrapper"; + + private static final int MSG_DOWNLOAD_COMPLETE = 2; + private static final int MSG_DOWNLOAD_CANCELLED = 3; + private static final int MSG_ERROR = 4; + private static final String MSG_DATA = "data"; + + private final Downloader downloader; + private DownloadThread downloadThread = null; + + private final Listener listener; + + /** + * 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. + */ + public AsyncDownloadWrapper(Downloader downloader, Listener listener) { + this.downloader = downloader; + this.listener = listener; + } + + public int getBytesRead() { + return downloader.getBytesRead(); + } + + public int getTotalBytes() { + return downloader.getTotalBytes(); + } + + public void download() { + downloadThread = new DownloadThread(); + downloadThread.start(); + } + + public void attemptCancel(boolean userRequested) { + if (downloadThread != null) { + downloadThread.interrupt(); + } + } + + /** + * Receives "messages" from the download thread, and passes them onto the + * relevant {@link AsyncDownloader.Listener} + */ + public void handleMessage(Message message) { + switch (message.arg1) { + case MSG_DOWNLOAD_COMPLETE: + listener.onDownloadComplete(); + break; + case MSG_DOWNLOAD_CANCELLED: + listener.onDownloadCancelled(); + break; + case MSG_ERROR: + listener.onErrorDownloading(message.getData().getString(MSG_DATA)); + break; + } + } + + private class DownloadThread extends Thread { + + public void run() { + try { + downloader.download(); + sendMessage(MSG_DOWNLOAD_COMPLETE); + } catch (InterruptedException e) { + sendMessage(MSG_DOWNLOAD_CANCELLED); + } catch (IOException e) { + Log.e(TAG, "I/O exception in download thread", e); + Bundle data = new Bundle(1); + data.putString(MSG_DATA, e.getLocalizedMessage()); + Message message = new Message(); + message.arg1 = MSG_ERROR; + message.setData(data); + AsyncDownloadWrapper.this.sendMessage(message); + } + } + + private void sendMessage(int messageType) { + Message message = new Message(); + message.arg1 = messageType; + AsyncDownloadWrapper.this.sendMessage(message); + } + } +} diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java index 2589ad709..9612ce975 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java @@ -9,106 +9,17 @@ import org.fdroid.fdroid.ProgressListener; import java.io.IOException; -/** - * Given a {@link org.fdroid.fdroid.net.Downloader}, this wrapper will conduct the download operation on a - * separate thread. All progress/status/error/etc events will be forwarded from that thread to the thread - * that {@link AsyncDownloader#download()} was invoked on. If you want to respond with UI feedback - * to these events, it is important that you execute the download method of this class from the UI thread. - * That way, all forwarded events will be handled on that thread. - */ -@SuppressWarnings("serial") -public class AsyncDownloader extends Handler { +public interface AsyncDownloader { - private static final String TAG = "AsyncDownloadWrapper"; - - private static final int MSG_DOWNLOAD_COMPLETE = 2; - private static final int MSG_DOWNLOAD_CANCELLED = 3; - private static final int MSG_ERROR = 4; - private static final String MSG_DATA = "data"; - - private final Downloader downloader; - private final Listener listener; - private DownloadThread downloadThread = null; - - /** - * 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. - */ - public AsyncDownloader(Downloader downloader, Listener listener) { - this.downloader = downloader; - this.listener = listener; - } - - public void download() { - downloadThread = new DownloadThread(); - downloadThread.start(); - } - - public void attemptCancel(boolean userRequested) { - if (downloadThread != null) { - downloadThread.interrupt(); - } - } - - /** - * Receives "messages" from the download thread, and passes them onto the - * relevant {@link AsyncDownloader.Listener} - * @param message - */ - public void handleMessage(Message message) { - switch (message.arg1) { - case MSG_DOWNLOAD_COMPLETE: - listener.onDownloadComplete(); - break; - case MSG_DOWNLOAD_CANCELLED: - listener.onDownloadCancelled(); - break; - case MSG_ERROR: - listener.onErrorDownloading(message.getData().getString(MSG_DATA)); - break; - } - } - - public int getBytesRead() { - return downloader.getBytesRead(); - } - - public int getTotalBytes() { - return downloader.getTotalBytes(); - } - - public interface Listener extends ProgressListener { + interface Listener extends ProgressListener { void onErrorDownloading(String localisedExceptionDetails); void onDownloadComplete(); void onDownloadCancelled(); } - private class DownloadThread extends Thread { + int getBytesRead(); + int getTotalBytes(); + void download(); + void attemptCancel(boolean userRequested); - public void run() { - try { - downloader.download(); - sendMessage(MSG_DOWNLOAD_COMPLETE); - } catch (InterruptedException e) { - sendMessage(MSG_DOWNLOAD_CANCELLED); - } catch (IOException e) { - Log.e(TAG, "I/O exception in download thread", e); - Bundle data = new Bundle(1); - data.putString(MSG_DATA, e.getLocalizedMessage()); - Message message = new Message(); - message.arg1 = MSG_ERROR; - message.setData(data); - AsyncDownloader.this.sendMessage(message); - } - } - - private void sendMessage(int messageType) { - Message message = new Message(); - message.arg1 = messageType; - AsyncDownloader.this.sendMessage(message); - } - } } diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java index d75574a8a..dc839ea53 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java @@ -23,7 +23,7 @@ import java.io.OutputStream; * A downloader that uses Android's DownloadManager to perform a download. */ @TargetApi(Build.VERSION_CODES.GINGERBREAD) -public class AsyncDownloaderFromAndroid extends AsyncDownloader { +public class AsyncDownloaderFromAndroid implements AsyncDownloader { private final Context context; private final DownloadManager dm; private File localFile; @@ -42,7 +42,6 @@ public class AsyncDownloaderFromAndroid extends AsyncDownloader { * parameter to the constructor. */ public AsyncDownloaderFromAndroid(Context context, Listener listener, String appName, String appId, String remoteAddress, File localFile) { - super(null, listener); this.context = context; this.appName = appName; this.appId = appId; diff --git a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java index 575e141af..672071a82 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java @@ -5,7 +5,6 @@ import android.os.Build; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; public class DownloaderFactory { @@ -62,7 +61,7 @@ public class DownloaderFactory { if (canUseDownloadManager(url)) { return new AsyncDownloaderFromAndroid(context, listener, title, id, url.toString(), destFile); } else { - return new AsyncDownloader(create(context, url, destFile), listener); + return new AsyncDownloadWrapper(create(context, url, destFile), listener); } } From 7b773f94f93322caacc2b4129d0afbbac164db0b Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 9 Sep 2015 17:21:17 +1000 Subject: [PATCH 17/20] Make DownloadManager code file-agnostic, needn't be specific to "Apps" This is mainly cosmetic, changing the names of variables so that the downloader can seemingly be used for any type of download into the future. --- .../net/AsyncDownloaderFromAndroid.java | 85 +++++++++---------- .../receiver/DownloadManagerReceiver.java | 2 +- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java index dc839ea53..f9afe99e8 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java @@ -28,11 +28,11 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { private final DownloadManager dm; private File localFile; private String remoteAddress; - private String appName; - private String appId; + private String downloadTitle; + private String uniqueDownloadId; private Listener listener; - private long downloadId = -1; + private long downloadManagerId = -1; /** * Normally the listener would be provided using a setListener method. @@ -41,16 +41,16 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { * world about completion. Therefore, we require the listener as a * parameter to the constructor. */ - public AsyncDownloaderFromAndroid(Context context, Listener listener, String appName, String appId, String remoteAddress, File localFile) { + public AsyncDownloaderFromAndroid(Context context, Listener listener, String downloadTitle, String downloadId, String remoteAddress, File localFile) { this.context = context; - this.appName = appName; - this.appId = appId; + this.downloadTitle = downloadTitle; + this.uniqueDownloadId = downloadId; this.remoteAddress = remoteAddress; this.listener = listener; this.localFile = localFile; - if (appName == null || appName.trim().length() == 0) { - this.appName = remoteAddress; + if (downloadTitle == null || downloadTitle.trim().length() == 0) { + this.downloadTitle = remoteAddress; } dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); @@ -59,13 +59,13 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { @Override public void download() { // Check if the download is complete - if ((downloadId = isDownloadComplete(context, appId)) > 0) { + if ((downloadManagerId = isDownloadComplete(context, uniqueDownloadId)) > 0) { // clear the notification - dm.remove(downloadId); + dm.remove(downloadManagerId); try { // write the downloaded file to the expected location - ParcelFileDescriptor fd = dm.openDownloadedFile(downloadId); + ParcelFileDescriptor fd = dm.openDownloadedFile(downloadManagerId); copyFile(fd.getFileDescriptor(), localFile); listener.onDownloadComplete(); } catch (IOException e) { @@ -75,17 +75,17 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { } // Check if the download is still in progress - if (downloadId < 0) { - downloadId = isDownloading(context, appId); + if (downloadManagerId < 0) { + downloadManagerId = isDownloading(context, uniqueDownloadId); } // Start a new download - if (downloadId < 0) { + if (downloadManagerId < 0) { // set up download request DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress)); - request.setTitle(appName); - request.setDescription(appId); // we will retrieve this later from the description field - this.downloadId = dm.enqueue(request); + request.setTitle(downloadTitle); + request.setDescription(uniqueDownloadId); // we will retrieve this later from the description field + this.downloadManagerId = dm.enqueue(request); } context.registerReceiver(receiver, @@ -114,15 +114,15 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { @Override public int getBytesRead() { - if (downloadId < 0) return 0; + if (downloadManagerId < 0) return 0; DownloadManager.Query query = new DownloadManager.Query(); - query.setFilterById(downloadId); + query.setFilterById(downloadManagerId); Cursor c = dm.query(query); try { if (c.moveToFirst()) { - // we use the description column to store the app id + // we use the description column to store the unique id of this download int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); return c.getInt(columnIndex); } @@ -135,15 +135,15 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { @Override public int getTotalBytes() { - if (downloadId < 0) return 0; + if (downloadManagerId < 0) return 0; DownloadManager.Query query = new DownloadManager.Query(); - query.setFilterById(downloadId); + query.setFilterById(downloadManagerId); Cursor c = dm.query(query); try { if (c.moveToFirst()) { - // we use the description column to store the app id + // we use the description column to store the unique id for this download int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); return c.getInt(columnIndex); } @@ -162,16 +162,16 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { // ignore if receiver already unregistered } - if (userRequested && downloadId >= 0) { - dm.remove(downloadId); + if (userRequested && downloadManagerId >= 0) { + dm.remove(downloadManagerId); } } /** - * Extract the appId from a given download id. - * @return - appId or null if not found + * Extract the uniqueDownloadId from a given download id. + * @return - uniqueDownloadId or null if not found */ - public static String getAppId(Context context, long downloadId) { + public static String getDownloadId(Context context, long downloadId) { DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); DownloadManager.Query query = new DownloadManager.Query(); query.setFilterById(downloadId); @@ -179,7 +179,7 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { try { if (c.moveToFirst()) { - // we use the description column to store the app id + // we use the description column to store the unique id for this download int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); return c.getString(columnIndex); } @@ -202,7 +202,6 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { try { if (c.moveToFirst()) { - // we use the description column to store the app id int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TITLE); return c.getString(columnIndex); } @@ -214,12 +213,12 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { } /** - * Get the downloadId from an Intent sent by the DownloadManagerReceiver + * Get the downloadManagerId from an Intent sent by the DownloadManagerReceiver */ public static long getDownloadId(Intent intent) { if (intent != null) { if (intent.hasExtra(DownloadManager.EXTRA_DOWNLOAD_ID)) { - // we have been passed a DownloadManager download id, so get the app id for it + // we have been passed a DownloadManager download id, so get the unique id for that download return intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); } @@ -236,19 +235,19 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { } /** - * Check if a download is running for the app - * @return -1 if not downloading, else the downloadId + * Check if a download is running for the specified id + * @return -1 if not downloading, else the id from the Android download manager */ - public static long isDownloading(Context context, String appId) { + public static long isDownloading(Context context, String uniqueDownloadId) { DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); DownloadManager.Query query = new DownloadManager.Query(); Cursor c = dm.query(query); - int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); + int columnUniqueDownloadId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID); try { while (c.moveToNext()) { - if (appId.equals(c.getString(columnAppId))) { + if (uniqueDownloadId.equals(c.getString(columnUniqueDownloadId))) { return c.getLong(columnId); } } @@ -260,20 +259,20 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { } /** - * Check if a download for an app is complete. + * Check if a specific download is complete. * @return -1 if download is not complete, otherwise the download id */ - public static long isDownloadComplete(Context context, String appId) { + public static long isDownloadComplete(Context context, String uniqueDownloadId) { DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); DownloadManager.Query query = new DownloadManager.Query(); query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL); Cursor c = dm.query(query); - int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); + int columnUniqueDownloadId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION); int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID); try { while (c.moveToNext()) { - if (appId.equals(c.getString(columnAppId))) { + if (uniqueDownloadId.equals(c.getString(columnUniqueDownloadId))) { return c.getLong(columnId); } } @@ -292,8 +291,8 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { public void onReceive(Context context, Intent intent) { if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { long dId = getDownloadId(intent); - String appId = getAppId(context, dId); - if (listener != null && dId == downloadId && appId != null) { + String downloadId = getDownloadId(context, dId); + if (listener != null && dId == AsyncDownloaderFromAndroid.this.downloadManagerId && downloadId != null) { // our current download has just completed, so let's throw up install dialog // immediately try { diff --git a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java index 3ba5e6d34..3c50edf00 100644 --- a/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java +++ b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java @@ -22,7 +22,7 @@ public class DownloadManagerReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { // work out the app Id to send to the AppDetails Screen long downloadId = AsyncDownloaderFromAndroid.getDownloadId(intent); - String appId = AsyncDownloaderFromAndroid.getAppId(context, downloadId); + String appId = AsyncDownloaderFromAndroid.getDownloadId(context, downloadId); if (appId == null) { // bogus broadcast (e.g. download cancelled, but system sent a DOWNLOAD_COMPLETE) From a09587c7e287d9c834054ee2b8e38cfa4904eb4a Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 9 Sep 2015 17:26:22 +1000 Subject: [PATCH 18/20] Use helper functions where appropriate. --- F-Droid/src/org/fdroid/fdroid/Utils.java | 12 ++++++---- .../net/AsyncDownloaderFromAndroid.java | 23 ++++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/Utils.java b/F-Droid/src/org/fdroid/fdroid/Utils.java index e2b2ec3ff..0ea85c4ea 100644 --- a/F-Droid/src/org/fdroid/fdroid/Utils.java +++ b/F-Droid/src/org/fdroid/fdroid/Utils.java @@ -48,6 +48,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.BufferedInputStream; import java.io.Closeable; import java.io.File; +import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -162,16 +163,19 @@ public final class Utils { } public static boolean copy(File inFile, File outFile) { + InputStream input = null; + OutputStream output = null; try { - InputStream input = new FileInputStream(inFile); - OutputStream output = new FileOutputStream(outFile); + input = new FileInputStream(inFile); + output = new FileOutputStream(outFile); Utils.copy(input, output); - output.close(); - input.close(); return true; } catch (IOException e) { Log.e(TAG, "I/O error when copying a file", e); return false; + } finally { + closeQuietly(output); + closeQuietly(input); } } diff --git a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java index f9afe99e8..c3523e131 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java +++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java @@ -10,6 +10,10 @@ import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.ParcelFileDescriptor; +import android.text.TextUtils; +import android.util.Log; + +import org.fdroid.fdroid.Utils; import java.io.File; import java.io.FileDescriptor; @@ -49,7 +53,7 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { this.listener = listener; this.localFile = localFile; - if (downloadTitle == null || downloadTitle.trim().length() == 0) { + if (TextUtils.isEmpty(downloadTitle)) { this.downloadTitle = remoteAddress; } @@ -97,18 +101,15 @@ public class AsyncDownloaderFromAndroid implements AsyncDownloader { * @throws IOException */ private void copyFile(FileDescriptor inputFile, File outputFile) throws IOException { - InputStream is = new FileInputStream(inputFile); - OutputStream os = new FileOutputStream(outputFile); - byte[] buffer = new byte[1024]; - int count = 0; - + InputStream input = null; + OutputStream output = null; try { - while ((count = is.read(buffer, 0, buffer.length)) > 0) { - os.write(buffer, 0, count); - } + input = new FileInputStream(inputFile); + output = new FileOutputStream(outputFile); + Utils.copy(input, output); } finally { - os.close(); - is.close(); + Utils.closeQuietly(output); + Utils.closeQuietly(input); } } From 645f9fc5e3921286f3a1f3b765f0a46b94ff589c Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 10 Sep 2015 06:13:28 +1000 Subject: [PATCH 19/20] Rename Utils.copy and Utils.symlinkOrCopyFile to indicate exception clobbering Renamed to Utils.copyQuietly() and Utils.symlinkOrCopyFileQuietly(). The copy(File, File) method gobbles up IOExceptions, whereas the other copy(InputStream, OupputStream) method doesn't. This could cause confusion, whereas developers using one may not realise it is is gobblign their exceptions. --- F-Droid/src/org/fdroid/fdroid/Utils.java | 7 +++---- .../src/org/fdroid/fdroid/localrepo/LocalRepoManager.java | 6 +++--- F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java | 6 ++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/Utils.java b/F-Droid/src/org/fdroid/fdroid/Utils.java index 0ea85c4ea..13615b909 100644 --- a/F-Droid/src/org/fdroid/fdroid/Utils.java +++ b/F-Droid/src/org/fdroid/fdroid/Utils.java @@ -48,7 +48,6 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.BufferedInputStream; import java.io.Closeable; import java.io.File; -import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -143,8 +142,8 @@ public final class Utils { /** * Attempt to symlink, but if that fails, it will make a copy of the file. */ - public static boolean symlinkOrCopyFile(SanitizedFile inFile, SanitizedFile outFile) { - return FileCompat.symlink(inFile, outFile) || copy(inFile, outFile); + public static boolean symlinkOrCopyFileQuietly(SanitizedFile inFile, SanitizedFile outFile) { + return FileCompat.symlink(inFile, outFile) || copyQuietly(inFile, outFile); } /** @@ -162,7 +161,7 @@ public final class Utils { } } - public static boolean copy(File inFile, File outFile) { + public static boolean copyQuietly(File inFile, File outFile) { InputStream input = null; OutputStream output = null; try { diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java index e87705b18..e404db88d 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -140,7 +140,7 @@ public class LocalRepoManager { SanitizedFile apkFile = SanitizedFile.knownSanitized(appInfo.publicSourceDir); SanitizedFile fdroidApkLink = new SanitizedFile(webRoot, "fdroid.client.apk"); attemptToDelete(fdroidApkLink); - if (Utils.symlinkOrCopyFile(apkFile, fdroidApkLink)) + if (Utils.symlinkOrCopyFileQuietly(apkFile, fdroidApkLink)) fdroidClientURL = "/" + fdroidApkLink.getName(); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Could not set up F-Droid apk in the webroot", e); @@ -220,7 +220,7 @@ public class LocalRepoManager { private void symlinkFileElsewhere(String fileName, String symlinkPrefix, File directory) { SanitizedFile index = new SanitizedFile(directory, fileName); attemptToDelete(index); - Utils.symlinkOrCopyFile(new SanitizedFile(new File(directory, symlinkPrefix), fileName), index); + Utils.symlinkOrCopyFileQuietly(new SanitizedFile(new File(directory, symlinkPrefix), fileName), index); } private void deleteContents(File path) { @@ -249,7 +249,7 @@ public class LocalRepoManager { if (app.installedApk != null) { SanitizedFile outFile = new SanitizedFile(repoDir, app.installedApk.apkName); - if (Utils.symlinkOrCopyFile(app.installedApk.installedFile, outFile)) + if (Utils.symlinkOrCopyFileQuietly(app.installedApk.installedFile, outFile)) continue; } // if we got here, something went wrong diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java index c743ebd3e..070db2e6e 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -21,7 +21,6 @@ package org.fdroid.fdroid.net; import android.content.Context; -import android.os.Build; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; @@ -39,7 +38,6 @@ import org.fdroid.fdroid.data.SanitizedFile; import java.io.File; import java.io.IOException; -import java.net.URL; import java.security.NoSuchAlgorithmException; /** @@ -188,7 +186,7 @@ public class ApkDownloader implements AsyncDownloader.Listener { // Can we use the cached version? if (verifyOrDelete(potentiallyCachedFile)) { delete(localFile); - Utils.copy(potentiallyCachedFile, localFile); + Utils.copyQuietly(potentiallyCachedFile, localFile); prepareApkFileAndSendCompleteMessage(); return false; } @@ -243,7 +241,7 @@ public class ApkDownloader implements AsyncDownloader.Listener { private void cacheIfRequired() { if (Preferences.get().shouldCacheApks()) { Utils.DebugLog(TAG, "Copying .apk file to cache at " + potentiallyCachedFile.getAbsolutePath()); - Utils.copy(localFile, potentiallyCachedFile); + Utils.copyQuietly(localFile, potentiallyCachedFile); } } From 9848816df4a361b0d1c55ce36f086e71b3862de4 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 10 Sep 2015 07:56:05 +1000 Subject: [PATCH 20/20] Fix bug introduced when resolving conflicts during rebase. ApkDownloader now requires an `App` to be passed in. --- .../src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index a9528bf9c..cb8e5ee11 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -785,7 +785,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { public void install(@NonNull final App app) { final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode); - final ApkDownloader downloader = new ApkDownloader(this, apkToInstall, apkToInstall.repoAddress); + final ApkDownloader downloader = new ApkDownloader(this, app, apkToInstall, apkToInstall.repoAddress); downloader.setProgressListener(new ProgressListener() { @Override public void onProgress(Event event) {