purge disabled Android DownloadManager code

Unfortunately, this approach did not really work out.  It would have been
really nice to rely on the provided DownloadManager stuff, but it has too
many issues, like not working with Tor or other proxies, and being
difficult to tightly integrate.
This commit is contained in:
Hans-Christoph Steiner 2016-03-23 15:23:32 +01:00
parent acc2a3154f
commit 9d1743af33
5 changed files with 1 additions and 559 deletions

View File

@ -437,14 +437,6 @@
<action android:name="android.net.wifi.STATE_CHANGE" />
</intent-filter>
</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=".net.WifiStateChangeService" />

View File

@ -92,7 +92,6 @@ import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException;
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
import org.fdroid.fdroid.net.ApkDownloader;
import org.fdroid.fdroid.net.AsyncDownloaderFromAndroid;
import org.fdroid.fdroid.net.Downloader;
import java.io.File;
@ -434,18 +433,6 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
}
localBroadcastManager = LocalBroadcastManager.getInstance(this);
// Check if a download is running for this app
if (AsyncDownloaderFromAndroid.isDownloading(this, app.packageName) >= 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.packageName, app.suggestedVercode);
install(apkToInstall);
}
}
@Override

View File

@ -1,396 +0,0 @@
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.text.TextUtils;
import org.fdroid.fdroid.R;
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 final LocalBroadcastManager localBroadcastManager;
private final File localFile;
private final String remoteAddress;
private final String downloadTitle;
private final String uniqueDownloadId;
private final Listener listener;
private boolean isCancelled;
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.uniqueDownloadId = downloadId;
this.remoteAddress = remoteAddress;
this.listener = listener;
this.localFile = localFile;
if (TextUtils.isEmpty(downloadTitle)) {
this.downloadTitle = remoteAddress;
} else {
this.downloadTitle = downloadTitle;
}
dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
localBroadcastManager = LocalBroadcastManager.getInstance(context);
}
@Override
public void download() {
isCancelled = false;
// check if download failed
if (downloadManagerId >= 0) {
int status = validDownload(context, downloadManagerId);
if (status > 0) {
// error downloading
dm.remove(downloadManagerId);
if (listener != null) {
listener.onErrorDownloading(context.getString(R.string.download_error));
}
return;
}
}
// Check if the download is complete
downloadManagerId = isDownloadComplete(context, uniqueDownloadId);
if (downloadManagerId > 0) {
// clear the download
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));
Thread progressThread = new Thread() {
@Override
public void run() {
while (!isCancelled && isDownloading(context, uniqueDownloadId) >= 0) {
try {
Thread.sleep(1000);
} catch (Exception e) {
// ignore
}
sendProgress(getBytesRead(), getTotalBytes());
}
}
};
progressThread.start();
}
/**
* 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;
}
private void sendProgress(int bytesRead, int totalBytes) {
Intent intent = new Intent(Downloader.LOCAL_ACTION_PROGRESS);
intent.putExtra(Downloader.EXTRA_ADDRESS, remoteAddress);
intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead);
intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes);
localBroadcastManager.sendBroadcast(intent);
}
@Override
public void attemptCancel(boolean userRequested) {
isCancelled = true;
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
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
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) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
// TODO: remove. This is necessary because AppDetails calls this
// static method directly, without using the whole pipe through
// DownloaderFactory. This shouldn't be called at all on android-8
// devices, since AppDetails is really using the old downloader,
// not this one.
return -1;
}
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
Cursor c = dm.query(query);
if (c == null) {
// TODO: same as above.
return -1;
}
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
*/
private 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;
}
/**
* Check if download was valid, see issue
* http://code.google.com/p/android/issues/detail?id=18462
* From http://stackoverflow.com/questions/8937817/downloadmanager-action-download-complete-broadcast-receiver-receiving-same-downl
* @return 0 if successful, -1 if download doesn't exist, else the DownloadManager.ERROR_... code
*/
public static int validDownload(Context context, long downloadId) {
//Verify if download is a success
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor c = dm.query(new DownloadManager.Query().setFilterById(downloadId));
try {
if (c.moveToFirst()) {
int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
if (status == DownloadManager.STATUS_SUCCESSFUL) {
return 0; // Download is valid, celebrate
}
return c.getInt(c.getColumnIndex(DownloadManager.COLUMN_REASON));
}
} finally {
c.close();
}
return -1; // download doesn't exist
}
/**
* Broadcast receiver to listen for ACTION_DOWNLOAD_COMPLETE broadcasts
*/
private final 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,14 +1,9 @@
package org.fdroid.fdroid.net;
import android.annotation.TargetApi;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Build;
import android.support.v4.content.LocalBroadcastManager;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Credentials;
import java.io.File;
@ -90,62 +85,11 @@ public class DownloaderFactory {
}
public static AsyncDownloader createAsync(Context context, String urlString, File destFile, String title, String id, Credentials credentials, AsyncDownloader.Listener listener) throws IOException {
return createAsync(context, new URL(urlString), destFile, title, id, credentials, listener);
}
public static AsyncDownloader createAsync(Context context, URL url, File destFile, String title, String id, Credentials credentials, AsyncDownloader.Listener listener)
throws IOException {
// To re-enable, fix the following:
// * https://gitlab.com/fdroid/fdroidclient/issues/445
// * https://gitlab.com/fdroid/fdroidclient/issues/459
if (false && canUseDownloadManager(context, url)) {
Utils.debugLog(TAG, "Using AsyncDownloaderFromAndroid");
return new AsyncDownloaderFromAndroid(context, listener, title, id, url.toString(), destFile);
}
Utils.debugLog(TAG, "Using AsyncDownloadWrapper");
URL url = new URL(urlString);
return new AsyncDownloadWrapper(create(context, url, destFile, credentials), listener);
}
private static boolean isOnionAddress(URL url) {
return url.getHost().endsWith(".onion");
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private static boolean hasDownloadManager(Context context) {
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
if (dm == null) {
// Service was not found
return false;
}
DownloadManager.Query query = new DownloadManager.Query();
Cursor c = dm.query(query);
if (c == null) {
// Download Manager was disabled
return false;
}
c.close();
return true;
}
/**
* 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(Context context, URL url) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
// No HTTPS support on 2.3, no DownloadManager on 2.2. Don't have
// 3.0 devices to test on, so require 4.0.
return false;
}
if (isOnionAddress(url)) {
// We support onion addresses through our own downloader.
return false;
}
if (isBluetoothAddress(url)) {
// Completely differnet protocol not understood by the download manager.
return false;
}
return hasDownloadManager(context);
}
}

View File

@ -1,85 +0,0 @@
package org.fdroid.fdroid.receiver;
import android.annotation.TargetApi;
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.annotation.StringRes;
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
*/
@TargetApi(9)
public class DownloadManagerReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// work out the package name to send to the AppDetails Screen
long downloadId = AsyncDownloaderFromAndroid.getDownloadId(intent);
String packageName = AsyncDownloaderFromAndroid.getDownloadId(context, downloadId);
if (packageName == null) {
// bogus broadcast (e.g. download cancelled, but system sent a DOWNLOAD_COMPLETE)
return;
}
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
int status = AsyncDownloaderFromAndroid.validDownload(context, downloadId);
if (status == 0) {
// successful download
showNotification(context, packageName, intent, downloadId, R.string.tap_to_install);
} else {
// download failed!
showNotification(context, packageName, intent, downloadId, R.string.download_error);
// clear the download to allow user to download again
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
dm.remove(downloadId);
}
} 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, packageName);
context.startActivity(appDetails);
}
}
private void showNotification(Context context, String packageName, Intent intent, long downloadId,
@StringRes int messageResId) {
// 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.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
appDetails.putExtra(AppDetails.EXTRA_APPID, packageName);
// set separate pending intents per download id
PendingIntent pi = PendingIntent.getActivity(
context, (int) downloadId, appDetails, PendingIntent.FLAG_ONE_SHOT);
// build & show notification
String downloadTitle = AsyncDownloaderFromAndroid.getDownloadTitle(context, downloadId);
Notification notif = new NotificationCompat.Builder(context)
.setContentTitle(downloadTitle)
.setContentText(context.getString(messageResId))
.setSmallIcon(R.drawable.ic_stat_notify)
.setContentIntent(pi)
.setAutoCancel(true)
.build();
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify((int) downloadId, notif);
}
}