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