Merge branch 'cr-of-download-manager' into 'master'

Resumeable, simultaneous APK downloads in the background using Android's DownloadManager (post code-review)

This is the CR'ed version of !132, ready for merging. Created a MR instead of direct merging because of CI goodness. Will merge when CI passes. Thanks for your work Toby.

See merge request !133
This commit is contained in:
Peter Serwylo 2015-09-09 23:21:07 +00:00
commit da8811eadf
12 changed files with 524 additions and 80 deletions

View File

@ -450,6 +450,14 @@
<action android:name="android.net.wifi.STATE_CHANGE" /> <action android:name="android.net.wifi.STATE_CHANGE" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".receiver.DownloadManagerReceiver" >
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED" />
</intent-filter>
</receiver>
<service android:name=".UpdateService" /> <service android:name=".UpdateService" />
<service android:name=".net.WifiStateChangeService" /> <service android:name=".net.WifiStateChangeService" />

View File

@ -389,6 +389,7 @@
<string name="perm_costs_money">this may cost you money</string> <string name="perm_costs_money">this may cost you money</string>
<string name="uninstall_update_confirm">Do you want to replace this app with the factory version?</string> <string name="uninstall_update_confirm">Do you want to replace this app with the factory version?</string>
<string name="uninstall_confirm">Do you want to uninstall this app?</string> <string name="uninstall_confirm">Do you want to uninstall this app?</string>
<string name="tap_to_install">Download completed, tap to install</string>
<string name="perms_new_perm_prefix">NEW: </string> <string name="perms_new_perm_prefix">NEW: </string>
<string name="perms_description_app">Provided by %1$s.</string> <string name="perms_description_app">Provided by %1$s.</string>

View File

@ -39,6 +39,7 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.ListFragment; import android.support.v4.app.ListFragment;
import android.support.v4.app.NavUtils; 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.AndroidNotCompatibleException;
import org.fdroid.fdroid.installer.Installer.InstallerCallback; import org.fdroid.fdroid.installer.Installer.InstallerCallback;
import org.fdroid.fdroid.net.ApkDownloader; import org.fdroid.fdroid.net.ApkDownloader;
import org.fdroid.fdroid.net.AsyncDownloaderFromAndroid;
import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.Downloader;
import java.io.File; 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!"); Log.e(TAG, "No application ID found in the intent!");
return null; return null;
} }
return i.getStringExtra(EXTRA_APPID); return i.getStringExtra(EXTRA_APPID);
} }
@ -428,6 +431,18 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
} }
localBroadcastManager = LocalBroadcastManager.getInstance(this); 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. // The signature of the installed version.
@ -451,6 +466,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
refreshApkList(); refreshApkList();
refreshHeader(); refreshHeader();
supportInvalidateOptionsMenu(); supportInvalidateOptionsMenu();
if (downloadHandler != null) { if (downloadHandler != null) {
if (downloadHandler.isComplete()) { if (downloadHandler.isComplete()) {
downloadCompleteInstallApk(); downloadCompleteInstallApk();
@ -554,7 +570,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
protected void onDestroy() { protected void onDestroy() {
if (downloadHandler != null) { if (downloadHandler != null) {
if (!inProcessOfChangingConfiguration) { if (!inProcessOfChangingConfiguration) {
downloadHandler.cancel(); downloadHandler.cancel(false);
cleanUpFinishedDownload(); cleanUpFinishedDownload();
} }
} }
@ -811,12 +827,8 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
if (downloadHandler != null && !downloadHandler.isComplete()) if (downloadHandler != null && !downloadHandler.isComplete())
return; return;
final String[] projection = { RepoProvider.DataColumns.ADDRESS }; final String repoaddress = getRepoAddress(apk);
Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection); if (repoaddress == null) return;
if (repo == null || repo.address == null) {
return;
}
final String repoaddress = repo.address;
if (!apk.compatible) { if (!apk.compatible) {
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
@ -858,8 +870,18 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
startDownload(apk, repoaddress); 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) { private void startDownload(Apk apk, String repoAddress) {
downloadHandler = new ApkDownloader(getBaseContext(), apk, repoAddress); downloadHandler = new ApkDownloader(getBaseContext(), app, apk, repoAddress);
localBroadcastManager.registerReceiver(downloaderProgressReceiver, localBroadcastManager.registerReceiver(downloaderProgressReceiver,
new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS)); new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
downloadHandler.setProgressListener(this); downloadHandler.setProgressListener(this);
@ -1517,7 +1539,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
if (activity == null || activity.downloadHandler == null) if (activity == null || activity.downloadHandler == null)
return; return;
activity.downloadHandler.cancel(); activity.downloadHandler.cancel(true);
activity.cleanUpFinishedDownload(); activity.cleanUpFinishedDownload();
setProgressVisible(false); setProgressVisible(false);
updateViews(); updateViews();

View File

@ -142,8 +142,8 @@ public final class Utils {
/** /**
* Attempt to symlink, but if that fails, it will make a copy of the file. * Attempt to symlink, but if that fails, it will make a copy of the file.
*/ */
public static boolean symlinkOrCopyFile(SanitizedFile inFile, SanitizedFile outFile) { public static boolean symlinkOrCopyFileQuietly(SanitizedFile inFile, SanitizedFile outFile) {
return FileCompat.symlink(inFile, outFile) || copy(inFile, 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 { try {
InputStream input = new FileInputStream(inFile); input = new FileInputStream(inFile);
OutputStream output = new FileOutputStream(outFile); output = new FileOutputStream(outFile);
Utils.copy(input, output); Utils.copy(input, output);
output.close();
input.close();
return true; return true;
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "I/O error when copying a file", e); Log.e(TAG, "I/O error when copying a file", e);
return false; return false;
} finally {
closeQuietly(output);
closeQuietly(input);
} }
} }

View File

@ -140,7 +140,7 @@ public class LocalRepoManager {
SanitizedFile apkFile = SanitizedFile.knownSanitized(appInfo.publicSourceDir); SanitizedFile apkFile = SanitizedFile.knownSanitized(appInfo.publicSourceDir);
SanitizedFile fdroidApkLink = new SanitizedFile(webRoot, "fdroid.client.apk"); SanitizedFile fdroidApkLink = new SanitizedFile(webRoot, "fdroid.client.apk");
attemptToDelete(fdroidApkLink); attemptToDelete(fdroidApkLink);
if (Utils.symlinkOrCopyFile(apkFile, fdroidApkLink)) if (Utils.symlinkOrCopyFileQuietly(apkFile, fdroidApkLink))
fdroidClientURL = "/" + fdroidApkLink.getName(); fdroidClientURL = "/" + fdroidApkLink.getName();
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Could not set up F-Droid apk in the webroot", 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) { private void symlinkFileElsewhere(String fileName, String symlinkPrefix, File directory) {
SanitizedFile index = new SanitizedFile(directory, fileName); SanitizedFile index = new SanitizedFile(directory, fileName);
attemptToDelete(index); 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) { private void deleteContents(File path) {
@ -249,7 +249,7 @@ public class LocalRepoManager {
if (app.installedApk != null) { if (app.installedApk != null) {
SanitizedFile outFile = new SanitizedFile(repoDir, app.installedApk.apkName); SanitizedFile outFile = new SanitizedFile(repoDir, app.installedApk.apkName);
if (Utils.symlinkOrCopyFile(app.installedApk.installedFile, outFile)) if (Utils.symlinkOrCopyFileQuietly(app.installedApk.installedFile, outFile))
continue; continue;
} }
// if we got here, something went wrong // if we got here, something went wrong

View File

@ -22,7 +22,6 @@ package org.fdroid.fdroid.net;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
@ -34,6 +33,7 @@ import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.FileCompat; import org.fdroid.fdroid.compat.FileCompat;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.data.SanitizedFile;
import java.io.File; 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 * If the file has previously been downloaded, it will make use of that
* instead, without going to the network to download a new one. * 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"; 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"; public static final String EVENT_DATA_ERROR_TYPE = "apkDownloadErrorType";
@NonNull private final App app;
@NonNull private final Apk curApk; @NonNull private final Apk curApk;
@NonNull private final Context context; @NonNull private final Context context;
@NonNull private final String repoAddress; @NonNull private final String repoAddress;
@ -75,7 +76,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
@NonNull private final SanitizedFile potentiallyCachedFile; @NonNull private final SanitizedFile potentiallyCachedFile;
private ProgressListener listener; private ProgressListener listener;
private AsyncDownloadWrapper dlWrapper = null; private AsyncDownloader dlWrapper = null;
private boolean isComplete = false; private boolean isComplete = false;
private final long id = ++downloadIdCounter; private final long id = ++downloadIdCounter;
@ -88,8 +89,9 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
setProgressListener(null); 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.context = context;
this.app = app;
curApk = apk; curApk = apk;
this.repoAddress = repoAddress; this.repoAddress = repoAddress;
localFile = new SanitizedFile(Utils.getApkDownloadDir(context), apk.apkName); localFile = new SanitizedFile(Utils.getApkDownloadDir(context), apk.apkName);
@ -184,7 +186,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
// Can we use the cached version? // Can we use the cached version?
if (verifyOrDelete(potentiallyCachedFile)) { if (verifyOrDelete(potentiallyCachedFile)) {
delete(localFile); delete(localFile);
Utils.copy(potentiallyCachedFile, localFile); Utils.copyQuietly(potentiallyCachedFile, localFile);
prepareApkFileAndSendCompleteMessage(); prepareApkFileAndSendCompleteMessage();
return false; return false;
} }
@ -193,11 +195,9 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
Utils.DebugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile); Utils.DebugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile);
try { try {
Downloader downloader = DownloaderFactory.create(context, remoteAddress, localFile); dlWrapper = DownloaderFactory.createAsync(context, remoteAddress, localFile, app.name + " " + curApk.version, curApk.id, this);
dlWrapper = new AsyncDownloadWrapper(downloader, this);
dlWrapper.download(); dlWrapper.download();
return true; return true;
} catch (IOException e) { } catch (IOException e) {
onErrorDownloading(e.getLocalizedMessage()); onErrorDownloading(e.getLocalizedMessage());
} }
@ -241,7 +241,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
private void cacheIfRequired() { private void cacheIfRequired() {
if (Preferences.get().shouldCacheApks()) { if (Preferences.get().shouldCacheApks()) {
Utils.DebugLog(TAG, "Copying .apk file to cache at " + potentiallyCachedFile.getAbsolutePath()); 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 * 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) { if (dlWrapper != null) {
dlWrapper.attemptCancel(); dlWrapper.attemptCancel(userRequested);
} }
} }

View File

@ -5,19 +5,9 @@ import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.util.Log; import android.util.Log;
import org.fdroid.fdroid.ProgressListener;
import java.io.IOException; import java.io.IOException;
/** class AsyncDownloadWrapper extends Handler implements AsyncDownloader {
* 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 String TAG = "AsyncDownloadWrapper";
@ -27,9 +17,10 @@ public class AsyncDownloadWrapper extends Handler {
private static final String MSG_DATA = "data"; private static final String MSG_DATA = "data";
private final Downloader downloader; private final Downloader downloader;
private final Listener listener;
private DownloadThread downloadThread = null; private DownloadThread downloadThread = null;
private final Listener listener;
/** /**
* Normally the listener would be provided using a setListener method. * Normally the listener would be provided using a setListener method.
* However for the purposes of this async downloader, it doesn't make * 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) { public AsyncDownloadWrapper(Downloader downloader, Listener listener) {
this.downloader = downloader; this.downloader = downloader;
this.listener = listener; 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;
}
} }
public int getBytesRead() { public int getBytesRead() {
@ -80,10 +41,33 @@ public class AsyncDownloadWrapper extends Handler {
return downloader.getTotalBytes(); return downloader.getTotalBytes();
} }
public interface Listener extends ProgressListener { public void download() {
void onErrorDownloading(String localisedExceptionDetails); downloadThread = new DownloadThread();
void onDownloadComplete(); downloadThread.start();
void onDownloadCancelled(); }
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 { private class DownloadThread extends Thread {

View File

@ -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);
}

View File

@ -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();
}
}
}
};
}

View File

@ -1,6 +1,7 @@
package org.fdroid.fdroid.net; package org.fdroid.fdroid.net;
import android.content.Context; import android.content.Context;
import android.os.Build;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -51,7 +52,29 @@ public class DownloaderFactory {
return "bluetooth".equalsIgnoreCase(url.getProtocol()); 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"); 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);
}
} }

View File

@ -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);
}
}
}

View File

@ -785,7 +785,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
public void install(@NonNull final App app) { public void install(@NonNull final App app) {
final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode); 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() { downloader.setProgressListener(new ProgressListener() {
@Override @Override
public void onProgress(Event event) { public void onProgress(Event event) {