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:
commit
ec53f4e05c
@ -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',
|
||||
|
@ -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" />
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -8,8 +8,6 @@ public interface AsyncDownloader {
|
||||
void onErrorDownloading(String localisedExceptionDetails);
|
||||
|
||||
void onDownloadComplete();
|
||||
|
||||
void onDownloadCancelled();
|
||||
}
|
||||
|
||||
int getBytesRead();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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:
|
||||
if (TextUtils.equals(Utils.getApkUrl(apk.repoAddress, apk), broadcastUrl)) {
|
||||
resetView();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user