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) {