Merge branch 'simplify-downloaders' into 'master'

Simplify Downloaders

This is some groundwork to simplify the Downloader stuff in preparation to moving it to something like an `IntentService`, as part of #601.  This mostly removes unused bits that I've found in the process of writing the `DownloaderService`.  Some of these events will be added back in a more consistent way, so that there is one event type for the same idea throughout the code base.

See merge request !236
This commit is contained in:
Daniel Martí 2016-03-29 12:46:19 +00:00
commit ec53f4e05c
14 changed files with 13 additions and 858 deletions

View File

@ -19,6 +19,7 @@ dependencies {
compile 'eu.chainfire:libsuperuser:1.0.0.201602271131'
compile 'cc.mvdan.accesspoint:library:0.1.3'
compile 'info.guardianproject.netcipher:netcipher:1.2.1'
compile 'commons-io:commons-io:2.4'
compile 'commons-net:commons-net:3.4'
compile 'org.openhab.jmdns:jmdns:3.4.2'
compile('ch.acra:acra:4.8.2') {
@ -72,6 +73,7 @@ if (!hasProperty('sourceDeps')) {
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'eu.chainfire:libsuperuser:018344ff19ee94d252c14b4a503ee8b519184db473a5af83513f5837c413b128',
'cc.mvdan.accesspoint:library:dc89a085d6bc40381078b8dd7776b12bde0dbaf8ffbcddb17ec4ebc3edecc7ba',
'commons-io:commons-io:cc6a41dc3eaacc9e440a6bd0d2890b20d36b4ee408fe2d67122f328bb6e01581',
'commons-net:commons-net:38cf2eca826b8bcdb236fc1f2e79e0c6dd8e7e0f5c44a3b8e839a1065b2fbe2e',
'info.guardianproject.netcipher:netcipher:611ec5bde9d799fd57e1efec5c375f9f460de2cdda98918541decc9a7d02f2ad',
'org.openhab.jmdns:jmdns:7a4b34b5606bbd2aff7fdfe629edcb0416fccd367fb59a099f210b9aba4f0bce',

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

@ -1,231 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.input;
import java.io.IOException;
import java.io.InputStream;
/**
* This is a stream that will only supply bytes up to a certain length - if its
* position goes above that, it will stop.
* <p>
* This is useful to wrap ServletInputStreams. The ServletInputStream will block
* if you try to read content from it that isn't there, because it doesn't know
* whether the content hasn't arrived yet or whether the content has finished.
* So, one of these, initialized with the Content-length sent in the
* ServletInputStream's header, will stop it blocking, providing it's been sent
* with a correct content length.
*
* @version $Id: BoundedInputStream.java 1307462 2012-03-30 15:13:11Z ggregory $
* @since 2.0
*/
public class BoundedInputStream extends InputStream {
/** the wrapped input stream */
private final InputStream in;
/** the max length to provide */
private final long max;
/** the number of bytes already returned */
private long pos;
/** the marked position */
private long mark = -1;
/** flag if close shoud be propagated */
private boolean propagateClose = true;
/**
* Creates a new <code>BoundedInputStream</code> that wraps the given input
* stream and limits it to a certain size.
*
* @param in The wrapped input stream
* @param size The maximum number of bytes to return
*/
public BoundedInputStream(InputStream in, long size) {
// Some badly designed methods - eg the servlet API - overload length
// such that "-1" means stream finished
this.max = size;
this.in = in;
}
/**
* Creates a new <code>BoundedInputStream</code> that wraps the given input
* stream and is unlimited.
*
* @param in The wrapped input stream
*/
public BoundedInputStream(InputStream in) {
this(in, -1);
}
/**
* Invokes the delegate's <code>read()</code> method if
* the current position is less than the limit.
* @return the byte read or -1 if the end of stream or
* the limit has been reached.
* @throws IOException if an I/O error occurs
*/
@Override
public int read() throws IOException {
if (max >= 0 && pos >= max) {
return -1;
}
int result = in.read();
pos++;
return result;
}
/**
* Invokes the delegate's <code>read(byte[])</code> method.
* @param b the buffer to read the bytes into
* @return the number of bytes read or -1 if the end of stream or
* the limit has been reached.
* @throws IOException if an I/O error occurs
*/
@Override
public int read(byte[] b) throws IOException {
return this.read(b, 0, b.length);
}
/**
* Invokes the delegate's <code>read(byte[], int, int)</code> method.
* @param b the buffer to read the bytes into
* @param off The start offset
* @param len The number of bytes to read
* @return the number of bytes read or -1 if the end of stream or
* the limit has been reached.
* @throws IOException if an I/O error occurs
*/
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (max >= 0 && pos >= max) {
return -1;
}
long maxRead = max >= 0 ? Math.min(len, max - pos) : len;
int bytesRead = in.read(b, off, (int) maxRead);
if (bytesRead == -1) {
return -1;
}
pos += bytesRead;
return bytesRead;
}
/**
* Invokes the delegate's <code>skip(long)</code> method.
* @param n the number of bytes to skip
* @return the actual number of bytes skipped
* @throws IOException if an I/O error occurs
*/
@Override
public long skip(long n) throws IOException {
long toSkip = max >= 0 ? Math.min(n, max - pos) : n;
long skippedBytes = in.skip(toSkip);
pos += skippedBytes;
return skippedBytes;
}
/**
* {@inheritDoc}
*/
@Override
public int available() throws IOException {
if (max >= 0 && pos >= max) {
return 0;
}
return in.available();
}
/**
* Invokes the delegate's <code>toString()</code> method.
* @return the delegate's <code>toString()</code>
*/
@Override
public String toString() {
return in.toString();
}
/**
* Invokes the delegate's <code>close()</code> method
* if {@link #isPropagateClose()} is {@code true}.
* @throws IOException if an I/O error occurs
*/
@Override
public void close() throws IOException {
if (propagateClose) {
in.close();
}
}
/**
* Invokes the delegate's <code>reset()</code> method.
* @throws IOException if an I/O error occurs
*/
@Override
public synchronized void reset() throws IOException {
in.reset();
pos = mark;
}
/**
* Invokes the delegate's <code>mark(int)</code> method.
* @param readlimit read ahead limit
*/
@Override
public synchronized void mark(int readlimit) {
in.mark(readlimit);
mark = pos;
}
/**
* Invokes the delegate's <code>markSupported()</code> method.
* @return true if mark is supported, otherwise false
*/
@Override
public boolean markSupported() {
return in.markSupported();
}
/**
* Indicates whether the {@link #close()} method
* should propagate to the underling {@link InputStream}.
*
* @return {@code true} if calling {@link #close()}
* propagates to the <code>close()</code> method of the
* underlying stream or {@code false} if it does not.
*/
public boolean isPropagateClose() {
return propagateClose;
}
/**
* Set whether the {@link #close()} method
* should propagate to the underling {@link InputStream}.
*
* @param propagateClose {@code true} if calling
* {@link #close()} propagates to the <code>close()</code>
* method of the underlying stream or
* {@code false} if it does not.
*/
public void setPropagateClose(boolean propagateClose) {
this.propagateClose = propagateClose;
}
}

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

@ -102,7 +102,7 @@ public class RepoUpdater {
repo.getCredentials()
);
downloader.setCacheTag(repo.lastetag);
downloader.downloadUninterrupted();
downloader.download();
if (downloader.isCached()) {
// The index is unchanged since we last read it. We just mark
@ -118,6 +118,9 @@ public class RepoUpdater {
}
throw new UpdateException(repo, "Error getting index file", e);
} catch (InterruptedException e) {
// ignored if canceled, the local database just won't be updated
e.printStackTrace();
}
return downloader;
}

View File

@ -51,15 +51,12 @@ public class ApkDownloader implements AsyncDownloader.Listener {
private static final String TAG = "ApkDownloader";
public static final String EVENT_APK_DOWNLOAD_COMPLETE = "apkDownloadComplete";
public static final String EVENT_APK_DOWNLOAD_CANCELLED = "apkDownloadCancelled";
public static final String EVENT_ERROR = "apkDownloadError";
public static final String ACTION_STATUS = "apkDownloadStatus";
public static final String EXTRA_TYPE = "apkDownloadStatusType";
public static final String EXTRA_URL = "apkDownloadUrl";
public static final int ERROR_HASH_MISMATCH = 101;
public static final int ERROR_DOWNLOAD_FAILED = 102;
private static final String EVENT_SOURCE_ID = "sourceId";
private static long downloadIdCounter;
@ -197,7 +194,7 @@ public class ApkDownloader implements AsyncDownloader.Listener {
Utils.debugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile);
try {
dlWrapper = DownloaderFactory.createAsync(context, remoteAddress, localFile, app.name + " " + curApk.version, curApk.packageName, credentials, this);
dlWrapper = DownloaderFactory.createAsync(context, remoteAddress, localFile, credentials, this);
dlWrapper.download();
return true;
} catch (IOException e) {
@ -228,7 +225,6 @@ public class ApkDownloader implements AsyncDownloader.Listener {
Intent intent = new Intent(ACTION_STATUS);
intent.putExtras(event.getData());
intent.putExtra(EXTRA_TYPE, event.type);
intent.putExtra(EXTRA_URL, Utils.getApkUrl(repoAddress, curApk));
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
@ -236,7 +232,6 @@ public class ApkDownloader implements AsyncDownloader.Listener {
@Override
public void onErrorDownloading(String localisedExceptionDetails) {
Log.e(TAG, "Download failed: " + localisedExceptionDetails);
sendError(ERROR_DOWNLOAD_FAILED);
delete(localFile);
}
@ -261,11 +256,6 @@ public class ApkDownloader implements AsyncDownloader.Listener {
prepareApkFileAndSendCompleteMessage();
}
@Override
public void onDownloadCancelled() {
sendMessage(EVENT_APK_DOWNLOAD_CANCELLED);
}
@Override
public void onProgress(Event event) {
sendProgressEvent(event);

View File

@ -12,7 +12,6 @@ class AsyncDownloadWrapper extends Handler implements AsyncDownloader {
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";
@ -61,9 +60,6 @@ class AsyncDownloadWrapper extends Handler implements AsyncDownloader {
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;
@ -77,7 +73,7 @@ class AsyncDownloadWrapper extends Handler implements AsyncDownloader {
downloader.download();
sendMessage(MSG_DOWNLOAD_COMPLETE);
} catch (InterruptedException e) {
sendMessage(MSG_DOWNLOAD_CANCELLED);
// ignored
} catch (IOException e) {
Log.e(TAG, "I/O exception in download thread", e);
Bundle data = new Bundle(1);

View File

@ -8,8 +8,6 @@ public interface AsyncDownloader {
void onErrorDownloading(String localisedExceptionDetails);
void onDownloadComplete();
void onDownloadCancelled();
}
int getBytesRead();

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

@ -91,20 +91,6 @@ public abstract class Downloader {
protected abstract int totalDownloadSize();
/**
* Helper function for synchronous downloads (i.e. those *not* using AsyncDownloadWrapper),
* which don't really want to bother dealing with an InterruptedException.
* The InterruptedException thrown from download() is there to enable cancelling asynchronous
* downloads, but regular synchronous downloads cannot be cancelled because download() will
* block until completed.
* @throws IOException
*/
public void downloadUninterrupted() throws IOException {
try {
download();
} catch (InterruptedException ignored) { }
}
public abstract void download() throws IOException, InterruptedException;
public abstract boolean isCached();

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;
@ -27,29 +22,9 @@ public class DownloaderFactory {
*/
public static Downloader create(Context context, String urlString)
throws IOException {
return create(context, new URL(urlString));
}
/**
* Downloads to a temporary file, which *you must delete yourself when
* you are done. It is stored in {@link Context#getCacheDir()} and starts
* with the prefix {@code dl-}.
*/
public static Downloader create(Context context, URL url)
throws IOException {
File destFile = File.createTempFile("dl-", "", context.getCacheDir());
destFile.deleteOnExit(); // this probably does nothing, but maybe...
return create(context, url, destFile);
}
public static Downloader create(Context context, String urlString, File destFile)
throws IOException {
return create(context, new URL(urlString), destFile);
}
public static Downloader create(Context context, URL url, File destFile)
throws IOException {
return create(context, url, destFile, null);
return create(context, new URL(urlString), destFile, null);
}
public static Downloader create(Context context, URL url, File destFile, Credentials credentials)
@ -89,63 +64,13 @@ public class DownloaderFactory {
return "file".equalsIgnoreCase(url.getProtocol());
}
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)
public static AsyncDownloader createAsync(Context context, String urlString, File destFile, 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);
}
}

View File

@ -279,18 +279,8 @@ public class SwapAppsView extends ListView implements
// apkToInstall. This way, we can wait until we receive an incoming intent (if
// at all) and then lazily load the apk to install.
String broadcastUrl = intent.getStringExtra(ApkDownloader.EXTRA_URL);
if (!TextUtils.equals(Utils.getApkUrl(apk.repoAddress, apk), broadcastUrl)) {
return;
}
switch (intent.getStringExtra(ApkDownloader.EXTRA_TYPE)) {
// Fallthrough for each of these "downloader no longer going" events...
case ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE:
case ApkDownloader.EVENT_APK_DOWNLOAD_CANCELLED:
case ApkDownloader.EVENT_ERROR:
case ApkDownloader.EVENT_DATA_ERROR_TYPE:
resetView();
break;
if (TextUtils.equals(Utils.getApkUrl(apk.repoAddress, apk), broadcastUrl)) {
resetView();
}
}
};

View File

@ -789,8 +789,6 @@ public class SwapWorkflowActivity extends AppCompatActivity {
case ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE:
handleDownloadComplete(downloader.localFile(), app.packageName);
break;
case ApkDownloader.EVENT_ERROR:
break;
}
}
});