Merge branch 'cr-of-download-manager' into 'master'
Resumeable, simultaneous APK downloads in the background using Android's DownloadManager (post code-review) This is the CR'ed version of !132, ready for merging. Created a MR instead of direct merging because of CI goodness. Will merge when CI passes. Thanks for your work Toby. See merge request !133
This commit is contained in:
commit
da8811eadf
@ -450,6 +450,14 @@
|
|||||||
<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" />
|
||||||
|
@ -389,6 +389,7 @@
|
|||||||
<string name="perm_costs_money">this may cost you money</string>
|
<string name="perm_costs_money">this may cost you money</string>
|
||||||
<string name="uninstall_update_confirm">Do you want to replace this app with the factory version?</string>
|
<string name="uninstall_update_confirm">Do you want to replace this app with the factory version?</string>
|
||||||
<string name="uninstall_confirm">Do you want to uninstall this app?</string>
|
<string name="uninstall_confirm">Do you want to uninstall this app?</string>
|
||||||
|
<string name="tap_to_install">Download completed, tap to install</string>
|
||||||
|
|
||||||
<string name="perms_new_perm_prefix">NEW: </string>
|
<string name="perms_new_perm_prefix">NEW: </string>
|
||||||
<string name="perms_description_app">Provided by %1$s.</string>
|
<string name="perms_description_app">Provided by %1$s.</string>
|
||||||
|
@ -39,6 +39,7 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.ListFragment;
|
import android.support.v4.app.ListFragment;
|
||||||
import android.support.v4.app.NavUtils;
|
import android.support.v4.app.NavUtils;
|
||||||
@ -91,6 +92,7 @@ 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;
|
||||||
@ -364,6 +366,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
|||||||
Log.e(TAG, "No application ID found in the intent!");
|
Log.e(TAG, "No application ID found in the intent!");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return i.getStringExtra(EXTRA_APPID);
|
return i.getStringExtra(EXTRA_APPID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,6 +431,18 @@ 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.id) >= 0) {
|
||||||
|
// call install() to re-setup the listeners and downloaders
|
||||||
|
// the AsyncDownloader will not restart the download since the download is running,
|
||||||
|
// and thus the version we pass to install() is not important
|
||||||
|
refreshHeader();
|
||||||
|
refreshApkList();
|
||||||
|
final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode);
|
||||||
|
install(apkToInstall);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The signature of the installed version.
|
// The signature of the installed version.
|
||||||
@ -451,6 +466,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
|||||||
refreshApkList();
|
refreshApkList();
|
||||||
refreshHeader();
|
refreshHeader();
|
||||||
supportInvalidateOptionsMenu();
|
supportInvalidateOptionsMenu();
|
||||||
|
|
||||||
if (downloadHandler != null) {
|
if (downloadHandler != null) {
|
||||||
if (downloadHandler.isComplete()) {
|
if (downloadHandler.isComplete()) {
|
||||||
downloadCompleteInstallApk();
|
downloadCompleteInstallApk();
|
||||||
@ -554,7 +570,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
|||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
if (downloadHandler != null) {
|
if (downloadHandler != null) {
|
||||||
if (!inProcessOfChangingConfiguration) {
|
if (!inProcessOfChangingConfiguration) {
|
||||||
downloadHandler.cancel();
|
downloadHandler.cancel(false);
|
||||||
cleanUpFinishedDownload();
|
cleanUpFinishedDownload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -811,12 +827,8 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
|||||||
if (downloadHandler != null && !downloadHandler.isComplete())
|
if (downloadHandler != null && !downloadHandler.isComplete())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
final String[] projection = { RepoProvider.DataColumns.ADDRESS };
|
final String repoaddress = getRepoAddress(apk);
|
||||||
Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
|
if (repoaddress == null) return;
|
||||||
if (repo == null || repo.address == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final String repoaddress = repo.address;
|
|
||||||
|
|
||||||
if (!apk.compatible) {
|
if (!apk.compatible) {
|
||||||
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
|
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
|
||||||
@ -858,8 +870,18 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
|||||||
startDownload(apk, repoaddress);
|
startDownload(apk, repoaddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String getRepoAddress(Apk apk) {
|
||||||
|
final String[] projection = { RepoProvider.DataColumns.ADDRESS };
|
||||||
|
Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
|
||||||
|
if (repo == null || repo.address == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return repo.address;
|
||||||
|
}
|
||||||
|
|
||||||
private void startDownload(Apk apk, String repoAddress) {
|
private void startDownload(Apk apk, String repoAddress) {
|
||||||
downloadHandler = new ApkDownloader(getBaseContext(), apk, repoAddress);
|
downloadHandler = new ApkDownloader(getBaseContext(), app, apk, repoAddress);
|
||||||
localBroadcastManager.registerReceiver(downloaderProgressReceiver,
|
localBroadcastManager.registerReceiver(downloaderProgressReceiver,
|
||||||
new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
|
new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
|
||||||
downloadHandler.setProgressListener(this);
|
downloadHandler.setProgressListener(this);
|
||||||
@ -1517,7 +1539,7 @@ public class AppDetails extends AppCompatActivity implements ProgressListener, A
|
|||||||
if (activity == null || activity.downloadHandler == null)
|
if (activity == null || activity.downloadHandler == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
activity.downloadHandler.cancel();
|
activity.downloadHandler.cancel(true);
|
||||||
activity.cleanUpFinishedDownload();
|
activity.cleanUpFinishedDownload();
|
||||||
setProgressVisible(false);
|
setProgressVisible(false);
|
||||||
updateViews();
|
updateViews();
|
||||||
|
@ -142,8 +142,8 @@ public final class Utils {
|
|||||||
/**
|
/**
|
||||||
* Attempt to symlink, but if that fails, it will make a copy of the file.
|
* Attempt to symlink, but if that fails, it will make a copy of the file.
|
||||||
*/
|
*/
|
||||||
public static boolean symlinkOrCopyFile(SanitizedFile inFile, SanitizedFile outFile) {
|
public static boolean symlinkOrCopyFileQuietly(SanitizedFile inFile, SanitizedFile outFile) {
|
||||||
return FileCompat.symlink(inFile, outFile) || copy(inFile, outFile);
|
return FileCompat.symlink(inFile, outFile) || copyQuietly(inFile, outFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -161,17 +161,20 @@ public final class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean copy(File inFile, File outFile) {
|
public static boolean copyQuietly(File inFile, File outFile) {
|
||||||
|
InputStream input = null;
|
||||||
|
OutputStream output = null;
|
||||||
try {
|
try {
|
||||||
InputStream input = new FileInputStream(inFile);
|
input = new FileInputStream(inFile);
|
||||||
OutputStream output = new FileOutputStream(outFile);
|
output = new FileOutputStream(outFile);
|
||||||
Utils.copy(input, output);
|
Utils.copy(input, output);
|
||||||
output.close();
|
|
||||||
input.close();
|
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "I/O error when copying a file", e);
|
Log.e(TAG, "I/O error when copying a file", e);
|
||||||
return false;
|
return false;
|
||||||
|
} finally {
|
||||||
|
closeQuietly(output);
|
||||||
|
closeQuietly(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ public class LocalRepoManager {
|
|||||||
SanitizedFile apkFile = SanitizedFile.knownSanitized(appInfo.publicSourceDir);
|
SanitizedFile apkFile = SanitizedFile.knownSanitized(appInfo.publicSourceDir);
|
||||||
SanitizedFile fdroidApkLink = new SanitizedFile(webRoot, "fdroid.client.apk");
|
SanitizedFile fdroidApkLink = new SanitizedFile(webRoot, "fdroid.client.apk");
|
||||||
attemptToDelete(fdroidApkLink);
|
attemptToDelete(fdroidApkLink);
|
||||||
if (Utils.symlinkOrCopyFile(apkFile, fdroidApkLink))
|
if (Utils.symlinkOrCopyFileQuietly(apkFile, fdroidApkLink))
|
||||||
fdroidClientURL = "/" + fdroidApkLink.getName();
|
fdroidClientURL = "/" + fdroidApkLink.getName();
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
Log.e(TAG, "Could not set up F-Droid apk in the webroot", e);
|
Log.e(TAG, "Could not set up F-Droid apk in the webroot", e);
|
||||||
@ -220,7 +220,7 @@ public class LocalRepoManager {
|
|||||||
private void symlinkFileElsewhere(String fileName, String symlinkPrefix, File directory) {
|
private void symlinkFileElsewhere(String fileName, String symlinkPrefix, File directory) {
|
||||||
SanitizedFile index = new SanitizedFile(directory, fileName);
|
SanitizedFile index = new SanitizedFile(directory, fileName);
|
||||||
attemptToDelete(index);
|
attemptToDelete(index);
|
||||||
Utils.symlinkOrCopyFile(new SanitizedFile(new File(directory, symlinkPrefix), fileName), index);
|
Utils.symlinkOrCopyFileQuietly(new SanitizedFile(new File(directory, symlinkPrefix), fileName), index);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteContents(File path) {
|
private void deleteContents(File path) {
|
||||||
@ -249,7 +249,7 @@ public class LocalRepoManager {
|
|||||||
|
|
||||||
if (app.installedApk != null) {
|
if (app.installedApk != null) {
|
||||||
SanitizedFile outFile = new SanitizedFile(repoDir, app.installedApk.apkName);
|
SanitizedFile outFile = new SanitizedFile(repoDir, app.installedApk.apkName);
|
||||||
if (Utils.symlinkOrCopyFile(app.installedApk.installedFile, outFile))
|
if (Utils.symlinkOrCopyFileQuietly(app.installedApk.installedFile, outFile))
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// if we got here, something went wrong
|
// if we got here, something went wrong
|
||||||
|
@ -22,7 +22,6 @@ package org.fdroid.fdroid.net;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
@ -34,6 +33,7 @@ import org.fdroid.fdroid.ProgressListener;
|
|||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.compat.FileCompat;
|
import org.fdroid.fdroid.compat.FileCompat;
|
||||||
import org.fdroid.fdroid.data.Apk;
|
import org.fdroid.fdroid.data.Apk;
|
||||||
|
import org.fdroid.fdroid.data.App;
|
||||||
import org.fdroid.fdroid.data.SanitizedFile;
|
import org.fdroid.fdroid.data.SanitizedFile;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -45,7 +45,7 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
* If the file has previously been downloaded, it will make use of that
|
* If the file has previously been downloaded, it will make use of that
|
||||||
* instead, without going to the network to download a new one.
|
* instead, without going to the network to download a new one.
|
||||||
*/
|
*/
|
||||||
public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
public class ApkDownloader implements AsyncDownloader.Listener {
|
||||||
|
|
||||||
private static final String TAG = "ApkDownloader";
|
private static final String TAG = "ApkDownloader";
|
||||||
|
|
||||||
@ -68,6 +68,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
|||||||
*/
|
*/
|
||||||
public static final String EVENT_DATA_ERROR_TYPE = "apkDownloadErrorType";
|
public static final String EVENT_DATA_ERROR_TYPE = "apkDownloadErrorType";
|
||||||
|
|
||||||
|
@NonNull private final App app;
|
||||||
@NonNull private final Apk curApk;
|
@NonNull private final Apk curApk;
|
||||||
@NonNull private final Context context;
|
@NonNull private final Context context;
|
||||||
@NonNull private final String repoAddress;
|
@NonNull private final String repoAddress;
|
||||||
@ -75,7 +76,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
|||||||
@NonNull private final SanitizedFile potentiallyCachedFile;
|
@NonNull private final SanitizedFile potentiallyCachedFile;
|
||||||
|
|
||||||
private ProgressListener listener;
|
private ProgressListener listener;
|
||||||
private AsyncDownloadWrapper dlWrapper = null;
|
private AsyncDownloader dlWrapper = null;
|
||||||
private boolean isComplete = false;
|
private boolean isComplete = false;
|
||||||
|
|
||||||
private final long id = ++downloadIdCounter;
|
private final long id = ++downloadIdCounter;
|
||||||
@ -88,8 +89,9 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
|||||||
setProgressListener(null);
|
setProgressListener(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApkDownloader(@NonNull final Context context, @NonNull final Apk apk, @NonNull final String repoAddress) {
|
public ApkDownloader(@NonNull final Context context, @NonNull final App app, @NonNull final Apk apk, @NonNull final String repoAddress) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.app = app;
|
||||||
curApk = apk;
|
curApk = apk;
|
||||||
this.repoAddress = repoAddress;
|
this.repoAddress = repoAddress;
|
||||||
localFile = new SanitizedFile(Utils.getApkDownloadDir(context), apk.apkName);
|
localFile = new SanitizedFile(Utils.getApkDownloadDir(context), apk.apkName);
|
||||||
@ -184,7 +186,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
|||||||
// Can we use the cached version?
|
// Can we use the cached version?
|
||||||
if (verifyOrDelete(potentiallyCachedFile)) {
|
if (verifyOrDelete(potentiallyCachedFile)) {
|
||||||
delete(localFile);
|
delete(localFile);
|
||||||
Utils.copy(potentiallyCachedFile, localFile);
|
Utils.copyQuietly(potentiallyCachedFile, localFile);
|
||||||
prepareApkFileAndSendCompleteMessage();
|
prepareApkFileAndSendCompleteMessage();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -193,11 +195,9 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
|||||||
Utils.DebugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile);
|
Utils.DebugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Downloader downloader = DownloaderFactory.create(context, remoteAddress, localFile);
|
dlWrapper = DownloaderFactory.createAsync(context, remoteAddress, localFile, app.name + " " + curApk.version, curApk.id, this);
|
||||||
dlWrapper = new AsyncDownloadWrapper(downloader, this);
|
|
||||||
dlWrapper.download();
|
dlWrapper.download();
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
onErrorDownloading(e.getLocalizedMessage());
|
onErrorDownloading(e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
@ -241,7 +241,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
|||||||
private void cacheIfRequired() {
|
private void cacheIfRequired() {
|
||||||
if (Preferences.get().shouldCacheApks()) {
|
if (Preferences.get().shouldCacheApks()) {
|
||||||
Utils.DebugLog(TAG, "Copying .apk file to cache at " + potentiallyCachedFile.getAbsolutePath());
|
Utils.DebugLog(TAG, "Copying .apk file to cache at " + potentiallyCachedFile.getAbsolutePath());
|
||||||
Utils.copy(localFile, potentiallyCachedFile);
|
Utils.copyQuietly(localFile, potentiallyCachedFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,11 +271,13 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to cancel the download (if in progress) and also removes the progress
|
* Attempts to cancel the download (if in progress) and also removes the progress
|
||||||
* listener (to prevent
|
* listener
|
||||||
|
*
|
||||||
|
* @param userRequested - true if the user requested the cancel (via button click), otherwise false.
|
||||||
*/
|
*/
|
||||||
public void cancel() {
|
public void cancel(boolean userRequested) {
|
||||||
if (dlWrapper != null) {
|
if (dlWrapper != null) {
|
||||||
dlWrapper.attemptCancel();
|
dlWrapper.attemptCancel(userRequested);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,19 +5,9 @@ import android.os.Handler;
|
|||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.fdroid.fdroid.ProgressListener;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
class AsyncDownloadWrapper extends Handler implements AsyncDownloader {
|
||||||
* Given a {@link org.fdroid.fdroid.net.Downloader}, this wrapper will conduct the download operation on a
|
|
||||||
* separate thread. All progress/status/error/etc events will be forwarded from that thread to the thread
|
|
||||||
* that {@link AsyncDownloadWrapper#download()} was invoked on. If you want to respond with UI feedback
|
|
||||||
* to these events, it is important that you execute the download method of this class from the UI thread.
|
|
||||||
* That way, all forwarded events will be handled on that thread.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("serial")
|
|
||||||
public class AsyncDownloadWrapper extends Handler {
|
|
||||||
|
|
||||||
private static final String TAG = "AsyncDownloadWrapper";
|
private static final String TAG = "AsyncDownloadWrapper";
|
||||||
|
|
||||||
@ -27,9 +17,10 @@ public class AsyncDownloadWrapper extends Handler {
|
|||||||
private static final String MSG_DATA = "data";
|
private static final String MSG_DATA = "data";
|
||||||
|
|
||||||
private final Downloader downloader;
|
private final Downloader downloader;
|
||||||
private final Listener listener;
|
|
||||||
private DownloadThread downloadThread = null;
|
private DownloadThread downloadThread = null;
|
||||||
|
|
||||||
|
private final Listener listener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normally the listener would be provided using a setListener method.
|
* Normally the listener would be provided using a setListener method.
|
||||||
* However for the purposes of this async downloader, it doesn't make
|
* However for the purposes of this async downloader, it doesn't make
|
||||||
@ -42,12 +33,20 @@ public class AsyncDownloadWrapper extends Handler {
|
|||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getBytesRead() {
|
||||||
|
return downloader.getBytesRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalBytes() {
|
||||||
|
return downloader.getTotalBytes();
|
||||||
|
}
|
||||||
|
|
||||||
public void download() {
|
public void download() {
|
||||||
downloadThread = new DownloadThread();
|
downloadThread = new DownloadThread();
|
||||||
downloadThread.start();
|
downloadThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void attemptCancel() {
|
public void attemptCancel(boolean userRequested) {
|
||||||
if (downloadThread != null) {
|
if (downloadThread != null) {
|
||||||
downloadThread.interrupt();
|
downloadThread.interrupt();
|
||||||
}
|
}
|
||||||
@ -55,8 +54,7 @@ public class AsyncDownloadWrapper extends Handler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives "messages" from the download thread, and passes them onto the
|
* Receives "messages" from the download thread, and passes them onto the
|
||||||
* relevant {@link org.fdroid.fdroid.net.AsyncDownloadWrapper.Listener}
|
* relevant {@link AsyncDownloader.Listener}
|
||||||
* @param message
|
|
||||||
*/
|
*/
|
||||||
public void handleMessage(Message message) {
|
public void handleMessage(Message message) {
|
||||||
switch (message.arg1) {
|
switch (message.arg1) {
|
||||||
@ -72,20 +70,6 @@ public class AsyncDownloadWrapper extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getBytesRead() {
|
|
||||||
return downloader.getBytesRead();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTotalBytes() {
|
|
||||||
return downloader.getTotalBytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Listener extends ProgressListener {
|
|
||||||
void onErrorDownloading(String localisedExceptionDetails);
|
|
||||||
void onDownloadComplete();
|
|
||||||
void onDownloadCancelled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DownloadThread extends Thread {
|
private class DownloadThread extends Thread {
|
||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
|
25
F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java
Normal file
25
F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package org.fdroid.fdroid.net;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.ProgressListener;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface AsyncDownloader {
|
||||||
|
|
||||||
|
interface Listener extends ProgressListener {
|
||||||
|
void onErrorDownloading(String localisedExceptionDetails);
|
||||||
|
void onDownloadComplete();
|
||||||
|
void onDownloadCancelled();
|
||||||
|
}
|
||||||
|
|
||||||
|
int getBytesRead();
|
||||||
|
int getTotalBytes();
|
||||||
|
void download();
|
||||||
|
void attemptCancel(boolean userRequested);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,311 @@
|
|||||||
|
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.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
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 File localFile;
|
||||||
|
private String remoteAddress;
|
||||||
|
private String downloadTitle;
|
||||||
|
private String uniqueDownloadId;
|
||||||
|
private Listener listener;
|
||||||
|
|
||||||
|
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.downloadTitle = downloadTitle;
|
||||||
|
this.uniqueDownloadId = downloadId;
|
||||||
|
this.remoteAddress = remoteAddress;
|
||||||
|
this.listener = listener;
|
||||||
|
this.localFile = localFile;
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(downloadTitle)) {
|
||||||
|
this.downloadTitle = remoteAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download() {
|
||||||
|
// Check if the download is complete
|
||||||
|
if ((downloadManagerId = isDownloadComplete(context, uniqueDownloadId)) > 0) {
|
||||||
|
// clear the notification
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attemptCancel(boolean userRequested) {
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||||
|
DownloadManager.Query query = new DownloadManager.Query();
|
||||||
|
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 a specific download is complete.
|
||||||
|
* @return -1 if download is not complete, otherwise the download id
|
||||||
|
*/
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast receiver to listen for ACTION_DOWNLOAD_COMPLETE broadcasts
|
||||||
|
*/
|
||||||
|
BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
|
||||||
|
long dId = getDownloadId(intent);
|
||||||
|
String 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package org.fdroid.fdroid.net;
|
package org.fdroid.fdroid.net;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -51,7 +52,29 @@ public class DownloaderFactory {
|
|||||||
return "bluetooth".equalsIgnoreCase(url.getProtocol());
|
return "bluetooth".equalsIgnoreCase(url.getProtocol());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isOnionAddress(URL url) {
|
public static AsyncDownloader createAsync(Context context, String urlString, File destFile, String title, String id, AsyncDownloader.Listener listener) throws IOException {
|
||||||
|
return createAsync(context, new URL(urlString), destFile, title, id, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AsyncDownloader createAsync(Context context, URL url, File destFile, String title, String id, AsyncDownloader.Listener listener)
|
||||||
|
throws IOException {
|
||||||
|
if (canUseDownloadManager(url)) {
|
||||||
|
return new AsyncDownloaderFromAndroid(context, listener, title, id, url.toString(), destFile);
|
||||||
|
} else {
|
||||||
|
return new AsyncDownloadWrapper(create(context, url, destFile), listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isOnionAddress(URL url) {
|
||||||
return url.getHost().endsWith(".onion");
|
return url.getHost().endsWith(".onion");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests to see if we can use Android's DownloadManager to download the APK, instead of
|
||||||
|
* a downloader returned from DownloadFactory.
|
||||||
|
*/
|
||||||
|
private static boolean canUseDownloadManager(URL url) {
|
||||||
|
return Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO && !isOnionAddress(url);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
package org.fdroid.fdroid.receiver;
|
||||||
|
|
||||||
|
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.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
|
||||||
|
*/
|
||||||
|
public class DownloadManagerReceiver extends BroadcastReceiver {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
// work out the app Id to send to the AppDetails Screen
|
||||||
|
long downloadId = AsyncDownloaderFromAndroid.getDownloadId(intent);
|
||||||
|
String appId = AsyncDownloaderFromAndroid.getDownloadId(context, downloadId);
|
||||||
|
|
||||||
|
if (appId == null) {
|
||||||
|
// bogus broadcast (e.g. download cancelled, but system sent a DOWNLOAD_COMPLETE)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
|
||||||
|
// 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.putExtras(intent.getExtras());
|
||||||
|
appDetails.putExtra(AppDetails.EXTRA_APPID, appId);
|
||||||
|
|
||||||
|
PendingIntent pi = PendingIntent.getActivity(
|
||||||
|
context, 1, appDetails, PendingIntent.FLAG_ONE_SHOT);
|
||||||
|
|
||||||
|
// launch LocalRepoActivity if the user selects this notification
|
||||||
|
String downloadTitle = AsyncDownloaderFromAndroid.getDownloadTitle(context, downloadId);
|
||||||
|
Notification notif = new NotificationCompat.Builder(context)
|
||||||
|
.setContentTitle(downloadTitle)
|
||||||
|
.setContentText(context.getString(R.string.tap_to_install))
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_notify)
|
||||||
|
.setContentIntent(pi)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
nm.notify((int)downloadId, notif);
|
||||||
|
} 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, appId);
|
||||||
|
context.startActivity(appDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -785,7 +785,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
public void install(@NonNull final App app) {
|
public void install(@NonNull final App app) {
|
||||||
final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode);
|
final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.suggestedVercode);
|
||||||
final ApkDownloader downloader = new ApkDownloader(this, apkToInstall, apkToInstall.repoAddress);
|
final ApkDownloader downloader = new ApkDownloader(this, app, apkToInstall, apkToInstall.repoAddress);
|
||||||
downloader.setProgressListener(new ProgressListener() {
|
downloader.setProgressListener(new ProgressListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgress(Event event) {
|
public void onProgress(Event event) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user