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

View File

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

View File

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

View File

@ -102,7 +102,7 @@ public class RepoUpdater {
repo.getCredentials() repo.getCredentials()
); );
downloader.setCacheTag(repo.lastetag); downloader.setCacheTag(repo.lastetag);
downloader.downloadUninterrupted(); downloader.download();
if (downloader.isCached()) { if (downloader.isCached()) {
// The index is unchanged since we last read it. We just mark // 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); 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; return downloader;
} }

View File

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

View File

@ -12,7 +12,6 @@ class AsyncDownloadWrapper extends Handler implements AsyncDownloader {
private static final String TAG = "AsyncDownloadWrapper"; private static final String TAG = "AsyncDownloadWrapper";
private static final int MSG_DOWNLOAD_COMPLETE = 2; 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 int MSG_ERROR = 4;
private static final String MSG_DATA = "data"; private static final String MSG_DATA = "data";
@ -61,9 +60,6 @@ class AsyncDownloadWrapper extends Handler implements AsyncDownloader {
case MSG_DOWNLOAD_COMPLETE: case MSG_DOWNLOAD_COMPLETE:
listener.onDownloadComplete(); listener.onDownloadComplete();
break; break;
case MSG_DOWNLOAD_CANCELLED:
listener.onDownloadCancelled();
break;
case MSG_ERROR: case MSG_ERROR:
listener.onErrorDownloading(message.getData().getString(MSG_DATA)); listener.onErrorDownloading(message.getData().getString(MSG_DATA));
break; break;
@ -77,7 +73,7 @@ class AsyncDownloadWrapper extends Handler implements AsyncDownloader {
downloader.download(); downloader.download();
sendMessage(MSG_DOWNLOAD_COMPLETE); sendMessage(MSG_DOWNLOAD_COMPLETE);
} catch (InterruptedException e) { } catch (InterruptedException e) {
sendMessage(MSG_DOWNLOAD_CANCELLED); // ignored
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "I/O exception in download thread", e); Log.e(TAG, "I/O exception in download thread", e);
Bundle data = new Bundle(1); Bundle data = new Bundle(1);

View File

@ -8,8 +8,6 @@ public interface AsyncDownloader {
void onErrorDownloading(String localisedExceptionDetails); void onErrorDownloading(String localisedExceptionDetails);
void onDownloadComplete(); void onDownloadComplete();
void onDownloadCancelled();
} }
int getBytesRead(); 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(); 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 void download() throws IOException, InterruptedException;
public abstract boolean isCached(); public abstract boolean isCached();

View File

@ -1,14 +1,9 @@
package org.fdroid.fdroid.net; package org.fdroid.fdroid.net;
import android.annotation.TargetApi;
import android.app.DownloadManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.os.Build;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Credentials; import org.fdroid.fdroid.data.Credentials;
import java.io.File; import java.io.File;
@ -27,29 +22,9 @@ public class DownloaderFactory {
*/ */
public static Downloader create(Context context, String urlString) public static Downloader create(Context context, String urlString)
throws IOException { 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()); File destFile = File.createTempFile("dl-", "", context.getCacheDir());
destFile.deleteOnExit(); // this probably does nothing, but maybe... destFile.deleteOnExit(); // this probably does nothing, but maybe...
return create(context, url, destFile); return create(context, new URL(urlString), destFile, null);
}
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);
} }
public static Downloader create(Context context, URL url, File destFile, Credentials credentials) public static Downloader create(Context context, URL url, File destFile, Credentials credentials)
@ -89,63 +64,13 @@ public class DownloaderFactory {
return "file".equalsIgnoreCase(url.getProtocol()); 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 { public static AsyncDownloader createAsync(Context context, String urlString, File destFile, Credentials credentials, AsyncDownloader.Listener listener)
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 { throws IOException {
// To re-enable, fix the following: URL url = new URL(urlString);
// * 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");
return new AsyncDownloadWrapper(create(context, url, destFile, credentials), listener); return new AsyncDownloadWrapper(create(context, url, destFile, credentials), listener);
} }
private static boolean isOnionAddress(URL url) { private static boolean isOnionAddress(URL url) {
return url.getHost().endsWith(".onion"); 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 // apkToInstall. This way, we can wait until we receive an incoming intent (if
// at all) and then lazily load the apk to install. // at all) and then lazily load the apk to install.
String broadcastUrl = intent.getStringExtra(ApkDownloader.EXTRA_URL); String broadcastUrl = intent.getStringExtra(ApkDownloader.EXTRA_URL);
if (!TextUtils.equals(Utils.getApkUrl(apk.repoAddress, apk), broadcastUrl)) { if (TextUtils.equals(Utils.getApkUrl(apk.repoAddress, apk), broadcastUrl)) {
return; resetView();
}
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;
} }
} }
}; };

View File

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