Refactored AsyncDownloader to only ever be constructed by DownloadFactory.
This commit is contained in:
parent
9b7c4c7b4a
commit
69ecaf023f
@ -22,7 +22,6 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.DownloadManager;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.BroadcastReceiver;
|
||||
@ -35,7 +34,6 @@ import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@ -94,7 +92,7 @@ import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException;
|
||||
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
|
||||
import org.fdroid.fdroid.net.ApkDownloader;
|
||||
import org.fdroid.fdroid.net.AsyncDownloader;
|
||||
import org.fdroid.fdroid.net.AsyncDownloaderFromAndroid;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
|
||||
import java.io.File;
|
||||
@ -435,7 +433,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
|
||||
// Check if a download is running for this app
|
||||
if (AsyncDownloader.isDownloading(this, app.id) >= 0) {
|
||||
if (AsyncDownloaderFromAndroid.isDownloading(this, app.id) >= 0) {
|
||||
// call install() to re-setup the listeners and downloaders
|
||||
// the AsyncDownloader will not restart the download since the download is running,
|
||||
// and thus the version we pass to install() is not important
|
||||
|
@ -47,7 +47,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
* If the file has previously been downloaded, it will make use of that
|
||||
* instead, without going to the network to download a new one.
|
||||
*/
|
||||
public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
||||
public class ApkDownloader implements AsyncDownloader.Listener {
|
||||
|
||||
private static final String TAG = "ApkDownloader";
|
||||
|
||||
@ -78,7 +78,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
||||
@NonNull private final SanitizedFile potentiallyCachedFile;
|
||||
|
||||
private ProgressListener listener;
|
||||
private AsyncDownloadWrapper dlWrapper = null;
|
||||
private AsyncDownloader dlWrapper = null;
|
||||
private boolean isComplete = false;
|
||||
|
||||
private final long id = ++downloadIdCounter;
|
||||
@ -197,17 +197,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
||||
Utils.DebugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile);
|
||||
|
||||
try {
|
||||
if (canUseDownloadManager(new URL(remoteAddress))) {
|
||||
// If we can use Android's DownloadManager, let's use it, because
|
||||
// of better OS integration, reliability, and async ability
|
||||
dlWrapper = new AsyncDownloader(context, this,
|
||||
app.name + " " + curApk.version, curApk.id,
|
||||
remoteAddress, localFile);
|
||||
} else {
|
||||
Downloader downloader = DownloaderFactory.create(context, remoteAddress, localFile);
|
||||
dlWrapper = new AsyncDownloadWrapper(downloader, this);
|
||||
}
|
||||
|
||||
dlWrapper = DownloaderFactory.createAsync(context, remoteAddress, localFile, app.name + " " + curApk.version, curApk.id, this);
|
||||
dlWrapper.download();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
@ -217,17 +207,6 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests to see if we can use Android's DownloadManager to download the APK, instead of
|
||||
* a downloader returned from DownloadFactory.
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
private boolean canUseDownloadManager(URL url) {
|
||||
return Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO
|
||||
&& !DownloaderFactory.isOnionAddress(url);
|
||||
}
|
||||
|
||||
private void sendMessage(String type) {
|
||||
sendProgressEvent(new ProgressListener.Event(type));
|
||||
}
|
||||
|
@ -1,114 +0,0 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Given a {@link org.fdroid.fdroid.net.Downloader}, this wrapper will conduct the download operation on a
|
||||
* separate thread. All progress/status/error/etc events will be forwarded from that thread to the thread
|
||||
* that {@link AsyncDownloadWrapper#download()} was invoked on. If you want to respond with UI feedback
|
||||
* to these events, it is important that you execute the download method of this class from the UI thread.
|
||||
* That way, all forwarded events will be handled on that thread.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class AsyncDownloadWrapper extends Handler {
|
||||
|
||||
private static final String TAG = "AsyncDownloadWrapper";
|
||||
|
||||
private static final int MSG_DOWNLOAD_COMPLETE = 2;
|
||||
private static final int MSG_DOWNLOAD_CANCELLED = 3;
|
||||
private static final int MSG_ERROR = 4;
|
||||
private static final String MSG_DATA = "data";
|
||||
|
||||
private final Downloader downloader;
|
||||
private final Listener listener;
|
||||
private DownloadThread downloadThread = null;
|
||||
|
||||
/**
|
||||
* Normally the listener would be provided using a setListener method.
|
||||
* However for the purposes of this async downloader, it doesn't make
|
||||
* sense to have an async task without any way to notify the outside
|
||||
* world about completion. Therefore, we require the listener as a
|
||||
* parameter to the constructor.
|
||||
*/
|
||||
public AsyncDownloadWrapper(Downloader downloader, Listener listener) {
|
||||
this.downloader = downloader;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void download() {
|
||||
downloadThread = new DownloadThread();
|
||||
downloadThread.start();
|
||||
}
|
||||
|
||||
public void attemptCancel(boolean userRequested) {
|
||||
if (downloadThread != null) {
|
||||
downloadThread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives "messages" from the download thread, and passes them onto the
|
||||
* relevant {@link org.fdroid.fdroid.net.AsyncDownloadWrapper.Listener}
|
||||
* @param message
|
||||
*/
|
||||
public void handleMessage(Message message) {
|
||||
switch (message.arg1) {
|
||||
case MSG_DOWNLOAD_COMPLETE:
|
||||
listener.onDownloadComplete();
|
||||
break;
|
||||
case MSG_DOWNLOAD_CANCELLED:
|
||||
listener.onDownloadCancelled();
|
||||
break;
|
||||
case MSG_ERROR:
|
||||
listener.onErrorDownloading(message.getData().getString(MSG_DATA));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public int getBytesRead() {
|
||||
return downloader.getBytesRead();
|
||||
}
|
||||
|
||||
public int getTotalBytes() {
|
||||
return downloader.getTotalBytes();
|
||||
}
|
||||
|
||||
public interface Listener extends ProgressListener {
|
||||
void onErrorDownloading(String localisedExceptionDetails);
|
||||
void onDownloadComplete();
|
||||
void onDownloadCancelled();
|
||||
}
|
||||
|
||||
private class DownloadThread extends Thread {
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
downloader.download();
|
||||
sendMessage(MSG_DOWNLOAD_COMPLETE);
|
||||
} catch (InterruptedException e) {
|
||||
sendMessage(MSG_DOWNLOAD_CANCELLED);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "I/O exception in download thread", e);
|
||||
Bundle data = new Bundle(1);
|
||||
data.putString(MSG_DATA, e.getLocalizedMessage());
|
||||
Message message = new Message();
|
||||
message.arg1 = MSG_ERROR;
|
||||
message.setData(data);
|
||||
AsyncDownloadWrapper.this.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(int messageType) {
|
||||
Message message = new Message();
|
||||
message.arg1 = messageType;
|
||||
AsyncDownloadWrapper.this.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,47 +1,34 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.AppDetails;
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.SanitizedFile;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* A downloader that uses Android's DownloadManager to perform a download.
|
||||
* Given a {@link org.fdroid.fdroid.net.Downloader}, this wrapper will conduct the download operation on a
|
||||
* separate thread. All progress/status/error/etc events will be forwarded from that thread to the thread
|
||||
* that {@link AsyncDownloader#download()} was invoked on. If you want to respond with UI feedback
|
||||
* to these events, it is important that you execute the download method of this class from the UI thread.
|
||||
* That way, all forwarded events will be handled on that thread.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
|
||||
public class AsyncDownloader extends AsyncDownloadWrapper {
|
||||
private final Context context;
|
||||
private final DownloadManager dm;
|
||||
private SanitizedFile localFile;
|
||||
private String remoteAddress;
|
||||
private String appName;
|
||||
private String appId;
|
||||
private Listener listener;
|
||||
@SuppressWarnings("serial")
|
||||
public class AsyncDownloader extends Handler {
|
||||
|
||||
private long downloadId = -1;
|
||||
private static final String TAG = "AsyncDownloadWrapper";
|
||||
|
||||
private static final int MSG_DOWNLOAD_COMPLETE = 2;
|
||||
private static final int MSG_DOWNLOAD_CANCELLED = 3;
|
||||
private static final int MSG_ERROR = 4;
|
||||
private static final String MSG_DATA = "data";
|
||||
|
||||
private final Downloader downloader;
|
||||
private final Listener listener;
|
||||
private DownloadThread downloadThread = null;
|
||||
|
||||
/**
|
||||
* Normally the listener would be provided using a setListener method.
|
||||
@ -49,287 +36,79 @@ public class AsyncDownloader extends AsyncDownloadWrapper {
|
||||
* sense to have an async task without any way to notify the outside
|
||||
* world about completion. Therefore, we require the listener as a
|
||||
* parameter to the constructor.
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public AsyncDownloader(Context context, Listener listener, String appName, String appId, String remoteAddress, SanitizedFile localFile) {
|
||||
super(null, listener);
|
||||
this.context = context;
|
||||
this.appName = appName;
|
||||
this.appId = appId;
|
||||
this.remoteAddress = remoteAddress;
|
||||
this.listener = listener;
|
||||
this.localFile = localFile;
|
||||
|
||||
if (appName == null || appName.trim().length() == 0) {
|
||||
this.appName = remoteAddress;
|
||||
}
|
||||
|
||||
dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
public AsyncDownloader(Downloader downloader, Listener listener) {
|
||||
this.downloader = downloader;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download() {
|
||||
// Check if the download is complete
|
||||
if ((downloadId = isDownloadComplete(context, appId)) > 0) {
|
||||
// clear the notification
|
||||
dm.remove(downloadId);
|
||||
|
||||
try {
|
||||
// write the downloaded file to the expected location
|
||||
ParcelFileDescriptor fd = dm.openDownloadedFile(downloadId);
|
||||
copyFile(fd.getFileDescriptor(), localFile);
|
||||
listener.onDownloadComplete();
|
||||
} catch (IOException e) {
|
||||
listener.onErrorDownloading(e.getLocalizedMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the download is still in progress
|
||||
if (downloadId < 0) {
|
||||
downloadId = isDownloading(context, appId);
|
||||
}
|
||||
|
||||
// Start a new download
|
||||
if (downloadId < 0) {
|
||||
// set up download request
|
||||
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress));
|
||||
request.setTitle(appName);
|
||||
request.setDescription(appId); // we will retrieve this later from the description field
|
||||
this.downloadId = dm.enqueue(request);
|
||||
}
|
||||
|
||||
context.registerReceiver(receiver,
|
||||
new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||
downloadThread = new DownloadThread();
|
||||
downloadThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy input file to output file
|
||||
* @param inputFile
|
||||
* @param outputFile
|
||||
* @throws IOException
|
||||
*/
|
||||
private void copyFile(FileDescriptor inputFile, SanitizedFile outputFile) throws IOException {
|
||||
InputStream is = new FileInputStream(inputFile);
|
||||
OutputStream os = new FileOutputStream(outputFile);
|
||||
byte[] buffer = new byte[1024];
|
||||
int count = 0;
|
||||
|
||||
try {
|
||||
while ((count = is.read(buffer, 0, buffer.length)) > 0) {
|
||||
os.write(buffer, 0, count);
|
||||
}
|
||||
} finally {
|
||||
os.close();
|
||||
is.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBytesRead() {
|
||||
if (downloadId < 0) return 0;
|
||||
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterById(downloadId);
|
||||
Cursor c = dm.query(query);
|
||||
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
// we use the description column to store the app id
|
||||
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
|
||||
return c.getInt(columnIndex);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalBytes() {
|
||||
if (downloadId < 0) return 0;
|
||||
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterById(downloadId);
|
||||
Cursor c = dm.query(query);
|
||||
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
// we use the description column to store the app id
|
||||
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
|
||||
return c.getInt(columnIndex);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attemptCancel(boolean userRequested) {
|
||||
try {
|
||||
context.unregisterReceiver(receiver);
|
||||
} catch (Exception e) {
|
||||
// ignore if receiver already unregistered
|
||||
}
|
||||
|
||||
if (userRequested && downloadId >= 0) {
|
||||
dm.remove(downloadId);
|
||||
if (downloadThread != null) {
|
||||
downloadThread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the appId from a given download id.
|
||||
* @param context
|
||||
* @param downloadId
|
||||
* @return - appId or null if not found
|
||||
* Receives "messages" from the download thread, and passes them onto the
|
||||
* relevant {@link AsyncDownloader.Listener}
|
||||
* @param message
|
||||
*/
|
||||
public static String getAppId(Context context, long downloadId) {
|
||||
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterById(downloadId);
|
||||
Cursor c = dm.query(query);
|
||||
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
// we use the description column to store the app id
|
||||
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
|
||||
return c.getString(columnIndex);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
public void handleMessage(Message message) {
|
||||
switch (message.arg1) {
|
||||
case MSG_DOWNLOAD_COMPLETE:
|
||||
listener.onDownloadComplete();
|
||||
break;
|
||||
case MSG_DOWNLOAD_CANCELLED:
|
||||
listener.onDownloadCancelled();
|
||||
break;
|
||||
case MSG_ERROR:
|
||||
listener.onErrorDownloading(message.getData().getString(MSG_DATA));
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the download title from a given download id.
|
||||
* @param context
|
||||
* @param downloadId
|
||||
* @return - title or null if not found
|
||||
*/
|
||||
public static String getDownloadTitle(Context context, long downloadId) {
|
||||
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterById(downloadId);
|
||||
Cursor c = dm.query(query);
|
||||
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
// we use the description column to store the app id
|
||||
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TITLE);
|
||||
return c.getString(columnIndex);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
public int getBytesRead() {
|
||||
return downloader.getBytesRead();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the downloadId from an Intent sent by the DownloadManagerReceiver
|
||||
* @param intent
|
||||
* @return
|
||||
*/
|
||||
public static long getDownloadId(Intent intent) {
|
||||
if (intent != null) {
|
||||
if (intent.hasExtra(DownloadManager.EXTRA_DOWNLOAD_ID)) {
|
||||
// we have been passed a DownloadManager download id, so get the app id for it
|
||||
return intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
|
||||
}
|
||||
|
||||
if (intent.hasExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS)) {
|
||||
// we have been passed multiple download id's - just return the first one
|
||||
long[] downloadIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
|
||||
if (downloadIds != null && downloadIds.length > 0) {
|
||||
return downloadIds[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
public int getTotalBytes() {
|
||||
return downloader.getTotalBytes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a download is running for the app
|
||||
* @param context
|
||||
* @param appId
|
||||
* @return -1 if not downloading, else the downloadId
|
||||
*/
|
||||
public static long isDownloading(Context context, String appId) {
|
||||
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
Cursor c = dm.query(query);
|
||||
int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
|
||||
int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID);
|
||||
|
||||
try {
|
||||
while (c.moveToNext()) {
|
||||
if (appId.equals(c.getString(columnAppId))) {
|
||||
return c.getLong(columnId);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return -1;
|
||||
public interface Listener extends ProgressListener {
|
||||
void onErrorDownloading(String localisedExceptionDetails);
|
||||
void onDownloadComplete();
|
||||
void onDownloadCancelled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a download for an app is complete.
|
||||
* @param context
|
||||
* @param appId
|
||||
* @return -1 if download is not complete, otherwise the download id
|
||||
*/
|
||||
public static long isDownloadComplete(Context context, String appId) {
|
||||
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
|
||||
Cursor c = dm.query(query);
|
||||
int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
|
||||
int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID);
|
||||
private class DownloadThread extends Thread {
|
||||
|
||||
try {
|
||||
while (c.moveToNext()) {
|
||||
if (appId.equals(c.getString(columnAppId))) {
|
||||
return c.getLong(columnId);
|
||||
}
|
||||
public void run() {
|
||||
try {
|
||||
downloader.download();
|
||||
sendMessage(MSG_DOWNLOAD_COMPLETE);
|
||||
} catch (InterruptedException e) {
|
||||
sendMessage(MSG_DOWNLOAD_CANCELLED);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "I/O exception in download thread", e);
|
||||
Bundle data = new Bundle(1);
|
||||
data.putString(MSG_DATA, e.getLocalizedMessage());
|
||||
Message message = new Message();
|
||||
message.arg1 = MSG_ERROR;
|
||||
message.setData(data);
|
||||
AsyncDownloader.this.sendMessage(message);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return -1;
|
||||
private void sendMessage(int messageType) {
|
||||
Message message = new Message();
|
||||
message.arg1 = messageType;
|
||||
AsyncDownloader.this.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast receiver to listen for ACTION_DOWNLOAD_COMPLETE broadcasts
|
||||
*/
|
||||
BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
|
||||
long dId = getDownloadId(intent);
|
||||
String appId = getAppId(context, dId);
|
||||
if (listener != null && dId == downloadId && appId != null) {
|
||||
// our current download has just completed, so let's throw up install dialog
|
||||
// immediately
|
||||
try {
|
||||
context.unregisterReceiver(receiver);
|
||||
} catch (Exception e) {
|
||||
// ignore if receiver already unregistered
|
||||
}
|
||||
|
||||
// call download() to copy the file and start the installer
|
||||
download();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,326 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* A downloader that uses Android's DownloadManager to perform a download.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
|
||||
public class AsyncDownloaderFromAndroid extends AsyncDownloader {
|
||||
private final Context context;
|
||||
private final DownloadManager dm;
|
||||
private File localFile;
|
||||
private String remoteAddress;
|
||||
private String appName;
|
||||
private String appId;
|
||||
private Listener listener;
|
||||
|
||||
private long downloadId = -1;
|
||||
|
||||
/**
|
||||
* Normally the listener would be provided using a setListener method.
|
||||
* However for the purposes of this async downloader, it doesn't make
|
||||
* sense to have an async task without any way to notify the outside
|
||||
* world about completion. Therefore, we require the listener as a
|
||||
* parameter to the constructor.
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public AsyncDownloaderFromAndroid(Context context, Listener listener, String appName, String appId, String remoteAddress, File localFile) {
|
||||
super(null, listener);
|
||||
this.context = context;
|
||||
this.appName = appName;
|
||||
this.appId = appId;
|
||||
this.remoteAddress = remoteAddress;
|
||||
this.listener = listener;
|
||||
this.localFile = localFile;
|
||||
|
||||
if (appName == null || appName.trim().length() == 0) {
|
||||
this.appName = remoteAddress;
|
||||
}
|
||||
|
||||
dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download() {
|
||||
// Check if the download is complete
|
||||
if ((downloadId = isDownloadComplete(context, appId)) > 0) {
|
||||
// clear the notification
|
||||
dm.remove(downloadId);
|
||||
|
||||
try {
|
||||
// write the downloaded file to the expected location
|
||||
ParcelFileDescriptor fd = dm.openDownloadedFile(downloadId);
|
||||
copyFile(fd.getFileDescriptor(), localFile);
|
||||
listener.onDownloadComplete();
|
||||
} catch (IOException e) {
|
||||
listener.onErrorDownloading(e.getLocalizedMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the download is still in progress
|
||||
if (downloadId < 0) {
|
||||
downloadId = isDownloading(context, appId);
|
||||
}
|
||||
|
||||
// Start a new download
|
||||
if (downloadId < 0) {
|
||||
// set up download request
|
||||
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress));
|
||||
request.setTitle(appName);
|
||||
request.setDescription(appId); // we will retrieve this later from the description field
|
||||
this.downloadId = dm.enqueue(request);
|
||||
}
|
||||
|
||||
context.registerReceiver(receiver,
|
||||
new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy input file to output file
|
||||
* @param inputFile
|
||||
* @param outputFile
|
||||
* @throws IOException
|
||||
*/
|
||||
private void copyFile(FileDescriptor inputFile, File outputFile) throws IOException {
|
||||
InputStream is = new FileInputStream(inputFile);
|
||||
OutputStream os = new FileOutputStream(outputFile);
|
||||
byte[] buffer = new byte[1024];
|
||||
int count = 0;
|
||||
|
||||
try {
|
||||
while ((count = is.read(buffer, 0, buffer.length)) > 0) {
|
||||
os.write(buffer, 0, count);
|
||||
}
|
||||
} finally {
|
||||
os.close();
|
||||
is.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBytesRead() {
|
||||
if (downloadId < 0) return 0;
|
||||
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterById(downloadId);
|
||||
Cursor c = dm.query(query);
|
||||
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
// we use the description column to store the app id
|
||||
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
|
||||
return c.getInt(columnIndex);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalBytes() {
|
||||
if (downloadId < 0) return 0;
|
||||
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterById(downloadId);
|
||||
Cursor c = dm.query(query);
|
||||
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
// we use the description column to store the app id
|
||||
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
|
||||
return c.getInt(columnIndex);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attemptCancel(boolean userRequested) {
|
||||
try {
|
||||
context.unregisterReceiver(receiver);
|
||||
} catch (Exception e) {
|
||||
// ignore if receiver already unregistered
|
||||
}
|
||||
|
||||
if (userRequested && downloadId >= 0) {
|
||||
dm.remove(downloadId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the appId from a given download id.
|
||||
* @param context
|
||||
* @param downloadId
|
||||
* @return - appId or null if not found
|
||||
*/
|
||||
public static String getAppId(Context context, long downloadId) {
|
||||
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterById(downloadId);
|
||||
Cursor c = dm.query(query);
|
||||
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
// we use the description column to store the app id
|
||||
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
|
||||
return c.getString(columnIndex);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the download title from a given download id.
|
||||
* @param context
|
||||
* @param downloadId
|
||||
* @return - title or null if not found
|
||||
*/
|
||||
public static String getDownloadTitle(Context context, long downloadId) {
|
||||
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterById(downloadId);
|
||||
Cursor c = dm.query(query);
|
||||
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
// we use the description column to store the app id
|
||||
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TITLE);
|
||||
return c.getString(columnIndex);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the downloadId from an Intent sent by the DownloadManagerReceiver
|
||||
* @param intent
|
||||
* @return
|
||||
*/
|
||||
public static long getDownloadId(Intent intent) {
|
||||
if (intent != null) {
|
||||
if (intent.hasExtra(DownloadManager.EXTRA_DOWNLOAD_ID)) {
|
||||
// we have been passed a DownloadManager download id, so get the app id for it
|
||||
return intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
|
||||
}
|
||||
|
||||
if (intent.hasExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS)) {
|
||||
// we have been passed multiple download id's - just return the first one
|
||||
long[] downloadIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
|
||||
if (downloadIds != null && downloadIds.length > 0) {
|
||||
return downloadIds[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a download is running for the app
|
||||
* @param context
|
||||
* @param appId
|
||||
* @return -1 if not downloading, else the downloadId
|
||||
*/
|
||||
public static long isDownloading(Context context, String appId) {
|
||||
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
Cursor c = dm.query(query);
|
||||
int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
|
||||
int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID);
|
||||
|
||||
try {
|
||||
while (c.moveToNext()) {
|
||||
if (appId.equals(c.getString(columnAppId))) {
|
||||
return c.getLong(columnId);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a download for an app is complete.
|
||||
* @param context
|
||||
* @param appId
|
||||
* @return -1 if download is not complete, otherwise the download id
|
||||
*/
|
||||
public static long isDownloadComplete(Context context, String appId) {
|
||||
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
|
||||
Cursor c = dm.query(query);
|
||||
int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
|
||||
int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID);
|
||||
|
||||
try {
|
||||
while (c.moveToNext()) {
|
||||
if (appId.equals(c.getString(columnAppId))) {
|
||||
return c.getLong(columnId);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast receiver to listen for ACTION_DOWNLOAD_COMPLETE broadcasts
|
||||
*/
|
||||
BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
|
||||
long dId = getDownloadId(intent);
|
||||
String appId = getAppId(context, dId);
|
||||
if (listener != null && dId == downloadId && appId != null) {
|
||||
// our current download has just completed, so let's throw up install dialog
|
||||
// immediately
|
||||
try {
|
||||
context.unregisterReceiver(receiver);
|
||||
} catch (Exception e) {
|
||||
// ignore if receiver already unregistered
|
||||
}
|
||||
|
||||
// call download() to copy the file and start the installer
|
||||
download();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
public class DownloaderFactory {
|
||||
@ -51,7 +53,29 @@ public class DownloaderFactory {
|
||||
return "bluetooth".equalsIgnoreCase(url.getProtocol());
|
||||
}
|
||||
|
||||
public static AsyncDownloader createAsync(Context context, String urlString, File destFile, String title, String id, AsyncDownloader.Listener listener) throws IOException {
|
||||
return createAsync(context, new URL(urlString), destFile, title, id, listener);
|
||||
}
|
||||
|
||||
public static AsyncDownloader createAsync(Context context, URL url, File destFile, String title, String id, AsyncDownloader.Listener listener)
|
||||
throws IOException {
|
||||
if (canUseDownloadManager(url)) {
|
||||
return new AsyncDownloaderFromAndroid(context, listener, title, id, url.toString(), destFile);
|
||||
} else {
|
||||
return new AsyncDownloader(create(context, url, destFile), listener);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isOnionAddress(URL url) {
|
||||
return url.getHost().endsWith(".onion");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests to see if we can use Android's DownloadManager to download the APK, instead of
|
||||
* a downloader returned from DownloadFactory.
|
||||
*/
|
||||
private static boolean canUseDownloadManager(URL url) {
|
||||
return Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO && !isOnionAddress(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import android.support.v4.app.NotificationCompat;
|
||||
|
||||
import org.fdroid.fdroid.AppDetails;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.net.AsyncDownloader;
|
||||
import org.fdroid.fdroid.net.AsyncDownloaderFromAndroid;
|
||||
|
||||
/**
|
||||
* Receive notifications from the Android DownloadManager and pass them onto the
|
||||
@ -21,8 +21,8 @@ public class DownloadManagerReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// work out the app Id to send to the AppDetails Screen
|
||||
long downloadId = AsyncDownloader.getDownloadId(intent);
|
||||
String appId = AsyncDownloader.getAppId(context, downloadId);
|
||||
long downloadId = AsyncDownloaderFromAndroid.getDownloadId(intent);
|
||||
String appId = AsyncDownloaderFromAndroid.getAppId(context, downloadId);
|
||||
|
||||
if (appId == null) {
|
||||
// bogus broadcast (e.g. download cancelled, but system sent a DOWNLOAD_COMPLETE)
|
||||
@ -41,7 +41,7 @@ public class DownloadManagerReceiver extends BroadcastReceiver {
|
||||
context, 1, appDetails, PendingIntent.FLAG_ONE_SHOT);
|
||||
|
||||
// launch LocalRepoActivity if the user selects this notification
|
||||
String downloadTitle = AsyncDownloader.getDownloadTitle(context, downloadId);
|
||||
String downloadTitle = AsyncDownloaderFromAndroid.getDownloadTitle(context, downloadId);
|
||||
Notification notif = new NotificationCompat.Builder(context)
|
||||
.setContentTitle(downloadTitle)
|
||||
.setContentText(context.getString(R.string.tap_to_install))
|
||||
|
Loading…
x
Reference in New Issue
Block a user