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/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/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java
index ee03cb40c..a5c7e02a4 100644
--- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java
+++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java
@@ -39,6 +39,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;
@@ -91,6 +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.AsyncDownloaderFromAndroid;
import org.fdroid.fdroid.net.Downloader;
import java.io.File;
@@ -364,6 +366,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
Log.e(TAG, "No application ID found in the intent!");
return null;
}
+
return i.getStringExtra(EXTRA_APPID);
}
@@ -428,6 +431,18 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
}
localBroadcastManager = LocalBroadcastManager.getInstance(this);
+
+ // Check if a download is running for this app
+ 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
+ refreshHeader();
+ refreshApkList();
+ final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode);
+ install(apkToInstall);
+ }
+
}
// The signature of the installed version.
@@ -451,6 +466,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
refreshApkList();
refreshHeader();
supportInvalidateOptionsMenu();
+
if (downloadHandler != null) {
if (downloadHandler.isComplete()) {
downloadCompleteInstallApk();
@@ -554,7 +570,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
protected void onDestroy() {
if (downloadHandler != null) {
if (!inProcessOfChangingConfiguration) {
- downloadHandler.cancel();
+ downloadHandler.cancel(false);
cleanUpFinishedDownload();
}
}
@@ -811,12 +827,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);
@@ -858,8 +870,18 @@ 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;
+ }
+ return repo.address;
+ }
+
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);
@@ -1517,7 +1539,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/Utils.java b/F-Droid/src/org/fdroid/fdroid/Utils.java
index e2b2ec3ff..13615b909 100644
--- a/F-Droid/src/org/fdroid/fdroid/Utils.java
+++ b/F-Droid/src/org/fdroid/fdroid/Utils.java
@@ -142,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);
}
/**
@@ -161,17 +161,20 @@ 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 {
- 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/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 8fc64b83e..070db2e6e 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java
@@ -22,7 +22,6 @@ package org.fdroid.fdroid.net;
import android.content.Context;
import android.content.Intent;
-import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
@@ -34,6 +33,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;
@@ -45,7 +45,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";
@@ -68,6 +68,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;
@@ -75,7 +76,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;
@@ -88,8 +89,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);
@@ -184,7 +186,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
// Can we use the cached version?
if (verifyOrDelete(potentiallyCachedFile)) {
delete(localFile);
- Utils.copy(potentiallyCachedFile, localFile);
+ Utils.copyQuietly(potentiallyCachedFile, localFile);
prepareApkFileAndSendCompleteMessage();
return false;
}
@@ -193,11 +195,9 @@ 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);
+ dlWrapper = DownloaderFactory.createAsync(context, remoteAddress, localFile, app.name + " " + curApk.version, curApk.id, this);
dlWrapper.download();
return true;
-
} catch (IOException e) {
onErrorDownloading(e.getLocalizedMessage());
}
@@ -241,7 +241,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.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);
}
}
@@ -271,11 +271,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..36021300b 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java
@@ -5,19 +5,9 @@ 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 {
+class AsyncDownloadWrapper extends Handler implements AsyncDownloader {
private static final String TAG = "AsyncDownloadWrapper";
@@ -27,9 +17,10 @@ public class AsyncDownloadWrapper extends Handler {
private static final String MSG_DATA = "data";
private final Downloader downloader;
- private final Listener listener;
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
@@ -39,37 +30,7 @@ public class AsyncDownloadWrapper extends Handler {
*/
public AsyncDownloadWrapper(Downloader downloader, Listener listener) {
this.downloader = downloader;
- this.listener = listener;
- }
-
- public void download() {
- downloadThread = new DownloadThread();
- downloadThread.start();
- }
-
- public void attemptCancel() {
- 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;
- }
+ this.listener = listener;
}
public int getBytesRead() {
@@ -80,10 +41,33 @@ public class AsyncDownloadWrapper extends Handler {
return downloader.getTotalBytes();
}
- public interface Listener extends ProgressListener {
- void onErrorDownloading(String localisedExceptionDetails);
- void onDownloadComplete();
- void onDownloadCancelled();
+ 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 {
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..9612ce975
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java
@@ -0,0 +1,25 @@
+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;
+
+public interface AsyncDownloader {
+
+ interface Listener extends ProgressListener {
+ void onErrorDownloading(String localisedExceptionDetails);
+ void onDownloadComplete();
+ void onDownloadCancelled();
+ }
+
+ int getBytesRead();
+ int getTotalBytes();
+ void download();
+ void attemptCancel(boolean userRequested);
+
+}
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..c3523e131
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java
@@ -0,0 +1,311 @@
+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.text.TextUtils;
+import android.util.Log;
+
+import org.fdroid.fdroid.Utils;
+
+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 implements AsyncDownloader {
+ private final Context context;
+ private final DownloadManager dm;
+ private File localFile;
+ private String remoteAddress;
+ private String downloadTitle;
+ private String uniqueDownloadId;
+ private Listener listener;
+
+ private long downloadManagerId = -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.
+ */
+ public AsyncDownloaderFromAndroid(Context context, Listener listener, String downloadTitle, String downloadId, String remoteAddress, File localFile) {
+ this.context = context;
+ this.downloadTitle = downloadTitle;
+ this.uniqueDownloadId = downloadId;
+ this.remoteAddress = remoteAddress;
+ this.listener = listener;
+ this.localFile = localFile;
+
+ if (TextUtils.isEmpty(downloadTitle)) {
+ this.downloadTitle = remoteAddress;
+ }
+
+ dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+ }
+
+ @Override
+ public void download() {
+ // Check if the download is complete
+ if ((downloadManagerId = isDownloadComplete(context, uniqueDownloadId)) > 0) {
+ // clear the notification
+ dm.remove(downloadManagerId);
+
+ try {
+ // write the downloaded file to the expected location
+ ParcelFileDescriptor fd = dm.openDownloadedFile(downloadManagerId);
+ copyFile(fd.getFileDescriptor(), localFile);
+ listener.onDownloadComplete();
+ } catch (IOException e) {
+ listener.onErrorDownloading(e.getLocalizedMessage());
+ }
+ return;
+ }
+
+ // Check if the download is still in progress
+ if (downloadManagerId < 0) {
+ downloadManagerId = isDownloading(context, uniqueDownloadId);
+ }
+
+ // Start a new download
+ if (downloadManagerId < 0) {
+ // set up download request
+ DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress));
+ request.setTitle(downloadTitle);
+ request.setDescription(uniqueDownloadId); // we will retrieve this later from the description field
+ this.downloadManagerId = dm.enqueue(request);
+ }
+
+ context.registerReceiver(receiver,
+ new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
+ }
+
+ /**
+ * Copy input file to output file
+ * @throws IOException
+ */
+ private void copyFile(FileDescriptor inputFile, File outputFile) throws IOException {
+ InputStream input = null;
+ OutputStream output = null;
+ try {
+ input = new FileInputStream(inputFile);
+ output = new FileOutputStream(outputFile);
+ Utils.copy(input, output);
+ } finally {
+ Utils.closeQuietly(output);
+ Utils.closeQuietly(input);
+ }
+ }
+
+ @Override
+ public int getBytesRead() {
+ if (downloadManagerId < 0) return 0;
+
+ DownloadManager.Query query = new DownloadManager.Query();
+ query.setFilterById(downloadManagerId);
+ Cursor c = dm.query(query);
+
+ try {
+ if (c.moveToFirst()) {
+ // 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);
+ }
+ } finally {
+ c.close();
+ }
+
+ return 0;
+ }
+
+ @Override
+ public int getTotalBytes() {
+ if (downloadManagerId < 0) return 0;
+
+ DownloadManager.Query query = new DownloadManager.Query();
+ query.setFilterById(downloadManagerId);
+ Cursor c = dm.query(query);
+
+ try {
+ if (c.moveToFirst()) {
+ // 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);
+ }
+ } 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 && downloadManagerId >= 0) {
+ dm.remove(downloadManagerId);
+ }
+ }
+
+ /**
+ * Extract the uniqueDownloadId from a given download id.
+ * @return - uniqueDownloadId or null if not found
+ */
+ 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);
+ Cursor c = dm.query(query);
+
+ try {
+ if (c.moveToFirst()) {
+ // we use the description column to store the unique id for this download
+ 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.
+ * @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()) {
+ int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TITLE);
+ return c.getString(columnIndex);
+ }
+ } finally {
+ c.close();
+ }
+
+ return null;
+ }
+
+ /**
+ * 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 unique id for that download
+ 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 specified id
+ * @return -1 if not downloading, else the id from the Android download manager
+ */
+ 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 columnUniqueDownloadId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
+ int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID);
+
+ try {
+ while (c.moveToNext()) {
+ if (uniqueDownloadId.equals(c.getString(columnUniqueDownloadId))) {
+ return c.getLong(columnId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ return -1;
+ }
+
+ /**
+ * 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 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 columnUniqueDownloadId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
+ int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID);
+
+ try {
+ while (c.moveToNext()) {
+ if (uniqueDownloadId.equals(c.getString(columnUniqueDownloadId))) {
+ 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 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 {
+ 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 5b476dd1c..672071a82 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java
@@ -1,6 +1,7 @@
package org.fdroid.fdroid.net;
import android.content.Context;
+import android.os.Build;
import java.io.File;
import java.io.IOException;
@@ -51,7 +52,29 @@ public class DownloaderFactory {
return "bluetooth".equalsIgnoreCase(url.getProtocol());
}
- private static boolean isOnionAddress(URL url) {
+ 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 AsyncDownloadWrapper(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
new file mode 100644
index 000000000..3c50edf00
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java
@@ -0,0 +1,65 @@
+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.AsyncDownloaderFromAndroid;
+
+/**
+ * Receive notifications from the Android DownloadManager and pass them onto the
+ * AppDetails activity
+ */
+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 = AsyncDownloaderFromAndroid.getDownloadId(intent);
+ String appId = AsyncDownloaderFromAndroid.getDownloadId(context, downloadId);
+
+ 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 = AsyncDownloaderFromAndroid.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);
+ }
+ }
+}
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) {