diff --git a/src/org/fdroid/fdroid/ApkDownloader.java b/src/org/fdroid/fdroid/ApkDownloader.java
index e8a58d046..07c34f5d5 100644
--- a/src/org/fdroid/fdroid/ApkDownloader.java
+++ b/src/org/fdroid/fdroid/ApkDownloader.java
@@ -56,13 +56,17 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
private ProgressListener listener;
private AsyncDownloadWrapper dlWrapper = null;
+ private int progress = 0;
+ private int totalSize = 0;
public void setProgressListener(ProgressListener listener) {
this.listener = listener;
}
- // Constructor - creates a Downloader to download the given Apk,
- // which must have its detail populated.
+ public void removeProgressListener() {
+ setProgressListener(null);
+ }
+
ApkDownloader(Apk apk, String repoAddress, File destDir) {
curApk = apk;
this.repoAddress = repoAddress;
@@ -157,6 +161,13 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
}
private void sendProgressEvent(Event event) {
+ if (event.type.equals(Downloader.EVENT_PROGRESS)) {
+ // Keep a copy of these ourselves, so people can interrogate us for the
+ // info (in addition to receiving events with the info).
+ totalSize = event.total;
+ progress = event.progress;
+ }
+
if (listener != null) {
listener.onProgress(event);
}
@@ -203,7 +214,12 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
sendProgressEvent(event);
}
+ /**
+ * Attempts to cancel the download (if in progress) and also removes the progress
+ * listener (to prevent
+ */
public void cancel() {
+ removeProgressListener();
if (dlWrapper != null) {
dlWrapper.attemptCancel();
}
@@ -212,4 +228,12 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
public Apk getApk() {
return curApk;
}
+
+ public int getProgress() {
+ return progress;
+ }
+
+ public int getTotalSize() {
+ return totalSize;
+ }
}
\ No newline at end of file
diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java
index cfc4b15c3..3baf3c3b8 100644
--- a/src/org/fdroid/fdroid/AppDetails.java
+++ b/src/org/fdroid/fdroid/AppDetails.java
@@ -24,11 +24,7 @@ import android.app.AlertDialog;
import android.app.ListActivity;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothAdapter;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
+import android.content.*;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -68,12 +64,7 @@ import org.fdroid.fdroid.Utils.CommaSeparatedList;
import org.fdroid.fdroid.compat.ActionBarCompat;
import org.fdroid.fdroid.compat.MenuManager;
import org.fdroid.fdroid.compat.PackageManagerCompat;
-import org.fdroid.fdroid.data.Apk;
-import org.fdroid.fdroid.data.ApkProvider;
-import org.fdroid.fdroid.data.App;
-import org.fdroid.fdroid.data.AppProvider;
-import org.fdroid.fdroid.data.Repo;
-import org.fdroid.fdroid.data.RepoProvider;
+import org.fdroid.fdroid.data.*;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException;
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
@@ -86,7 +77,7 @@ import java.util.Iterator;
import java.util.List;
public class AppDetails extends ListActivity implements ProgressListener {
- private static final String TAG = "AppDetails";
+ private static final String TAG = "org.fdroid.fdroid.AppDetails";
public static final int REQUEST_ENABLE_BLUETOOTH = 2;
@@ -122,7 +113,7 @@ public class AppDetails extends ListActivity implements ProgressListener {
@Override
public void onChange(boolean selfChange, Uri uri) {
- if (!reset()) {
+ if (!reset(app.id)) {
AppDetails.this.finish();
return;
}
@@ -279,10 +270,8 @@ public class AppDetails extends ListActivity implements ProgressListener {
private static final int SEND_VIA_BLUETOOTH = Menu.FIRST + 15;
private App app;
- private String appid;
private PackageManager mPm;
private ApkDownloader downloadHandler;
- private boolean stateRetained;
private boolean startingIgnoreAll;
private int startingIgnoreThis;
@@ -294,6 +283,68 @@ public class AppDetails extends ListActivity implements ProgressListener {
private DisplayImageOptions displayImageOptions;
private Installer installer;
+ /**
+ * Stores relevant data that we want to keep track of when destroying the activity
+ * with the expectation of it being recreated straight away (e.g. after an
+ * orientation change). One of the major things is that we want the download thread
+ * to stay active, but for it not to trigger any UI stuff (e.g. progress dialogs)
+ * between the activity being destroyed and recreated.
+ */
+ private static class ConfigurationChangeHelper {
+
+ public ApkDownloader downloader;
+ public App app;
+
+ public ConfigurationChangeHelper(ApkDownloader downloader, App app) {
+ this.downloader = downloader;
+ this.app = app;
+ }
+ }
+
+ private boolean inProcessOfChangingConfiguration = false;
+
+ /**
+ * Attempt to extract the appId from the intent which launched this activity.
+ * Various different intents could cause us to show this activity, such as:
+ *
+ * - market://details?id=[app_id]
+ * - https://f-droid.org/app/[app_id]
+ * - fdroid.app:[app_id]
+ *
+ * @return May return null, if we couldn't find the appId. In this case, you will
+ * probably want to do something drastic like finish the activity and show some
+ * feedback to the user (this method will not do that, it will just return
+ * null).
+ */
+ private String getAppIdFromIntent() {
+ Intent i = getIntent();
+ Uri data = i.getData();
+ String appId = null;
+ if (data != null) {
+ if (data.isHierarchical()) {
+ if (data.getHost() != null && data.getHost().equals("details")) {
+ // market://details?id=app.id
+ appId = data.getQueryParameter("id");
+ } else {
+ // https://f-droid.org/app/app.id
+ appId = data.getLastPathSegment();
+ if (appId != null && appId.equals("app")) {
+ appId = null;
+ }
+ }
+ } else {
+ // fdroid.app:app.id
+ appId = data.getEncodedSchemeSpecificPart();
+ }
+ Log.d("FDroid", "AppDetails launched from link, for '" + appId + "'");
+ } else if (!i.hasExtra(EXTRA_APPID)) {
+ Log.e("FDroid", "No application ID in AppDetails!?");
+ } else {
+ appId = i.getStringExtra(EXTRA_APPID);
+ }
+ return appId;
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
@@ -319,43 +370,27 @@ public class AppDetails extends ListActivity implements ProgressListener {
// for reason why.
ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
- Intent i = getIntent();
- Uri data = i.getData();
- if (data != null) {
- if (data.isHierarchical()) {
- if (data.getHost() != null && data.getHost().equals("details")) {
- // market://details?id=app.id
- appid = data.getQueryParameter("id");
- } else {
- // https://f-droid.org/app/app.id
- appid = data.getLastPathSegment();
- if (appid != null && appid.equals("app")) appid = null;
- }
- } else {
- // fdroid.app:app.id
- appid = data.getEncodedSchemeSpecificPart();
- }
- Log.d("FDroid", "AppDetails launched from link, for '" + appid + "'");
- } else if (!i.hasExtra(EXTRA_APPID)) {
- Log.d("FDroid", "No application ID in AppDetails!?");
- } else {
- appid = i.getStringExtra(EXTRA_APPID);
- }
-
- if (i.hasExtra(EXTRA_FROM)) {
- setTitle(i.getStringExtra(EXTRA_FROM));
+ if (getIntent().hasExtra(EXTRA_FROM)) {
+ setTitle(getIntent().getStringExtra(EXTRA_FROM));
}
mPm = getPackageManager();
+
installer = Installer.getActivityInstaller(this, mPm,
myInstallerCallback);
// Get the preferences we're going to use in this Activity...
- AppDetails old = (AppDetails) getLastNonConfigurationInstance();
- if (old != null) {
- copyState(old);
+ ConfigurationChangeHelper previousData = (ConfigurationChangeHelper)getLastNonConfigurationInstance();
+ if (previousData != null) {
+ Log.d(TAG, "Recreating view after configuration change.");
+ downloadHandler = previousData.downloader;
+ if (downloadHandler != null) {
+ Log.d(TAG, "Download was in progress before the configuration change, so we will start to listen to its events again.");
+ }
+ app = previousData.app;
+ setApp(app);
} else {
- if (!reset()) {
+ if (!reset(getAppIdFromIntent())) {
finish();
return;
}
@@ -398,11 +433,15 @@ public class AppDetails extends ListActivity implements ProgressListener {
AppProvider.getContentUri(app.id),
true,
myAppObserver);
-
- if (!reset()) {
- finish();
- return;
+ if (downloadHandler != null) {
+ downloadHandler.setProgressListener(this);
+
+ // Show the progress dialog, if for no other reason than to prevent them attempting
+ // to download again (i.e. we force them to touch 'cancel' before they can access
+ // the rest of the activity).
+ updateProgressDialog();
}
+
updateViews();
MenuManager.create(this).invalidateOptionsMenu();
@@ -415,8 +454,16 @@ public class AppDetails extends ListActivity implements ProgressListener {
}
if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
|| app.ignoreThisUpdate != startingIgnoreThis)) {
+ Log.d(TAG, "Updating 'ignore updates', as it has changed since we started the activity...");
setIgnoreUpdates(app.id, app.ignoreAllUpdates, app.ignoreThisUpdate);
}
+
+ if (downloadHandler != null) {
+ downloadHandler.removeProgressListener();
+ }
+
+ removeProgressDialog();
+
super.onPause();
}
@@ -435,18 +482,18 @@ public class AppDetails extends ListActivity implements ProgressListener {
@Override
public Object onRetainNonConfigurationInstance() {
- stateRetained = true;
- return this;
+ inProcessOfChangingConfiguration = true;
+ return new ConfigurationChangeHelper(downloadHandler, app);
}
@Override
protected void onDestroy() {
- // TODO: Generally need to verify the downloader stuff plays well with orientation changes...
if (downloadHandler != null) {
- if (!stateRetained)
+ if (!inProcessOfChangingConfiguration) {
downloadHandler.cancel();
- removeProgressDialog();
+ }
}
+ inProcessOfChangingConfiguration = false;
super.onDestroy();
}
@@ -457,55 +504,50 @@ public class AppDetails extends ListActivity implements ProgressListener {
}
}
- // Copy all relevant state from an old instance. This is used in
- // place of reset(), so it must initialize all fields normally set
- // there.
- private void copyState(AppDetails old) {
- // TODO: Reimplement copyState with the new downloader stuff... But really, if we start to use fragments for
- // this view, then it will probably not be relevant any more...
- /*
- if (old.downloadHandler != null)
- downloadHandler = new DownloadHandler(old.downloadHandler);
- app = old.app;
- mInstalledSignature = old.mInstalledSignature;
- mInstalledSigID = old.mInstalledSigID;
- */
- }
-
// Reset the display and list contents. Used when entering the activity, and
// also when something has been installed/uninstalled.
// Return true if the app was found, false otherwise.
- private boolean reset() {
+ private boolean reset(String appId) {
- Log.d("FDroid", "Getting application details for " + appid);
- app = null;
+ Log.d("FDroid", "Getting application details for " + appId);
+ App newApp = null;
- if (appid != null && appid.length() > 0) {
- app = AppProvider.Helper.findById(getContentResolver(), appid);
+ if (appId != null && appId.length() > 0) {
+ newApp = AppProvider.Helper.findById(getContentResolver(), appId);
}
- if (app == null) {
- Toast toast = Toast.makeText(this,
- getString(R.string.no_such_app), Toast.LENGTH_LONG);
- toast.show();
+ setApp(newApp);
+
+ return this.app != null;
+ }
+
+ /**
+ * If passed null, this will show a message to the user ("Could not find app ..." or something
+ * like that) and then finish the activity.
+ */
+ private void setApp(App newApp) {
+
+ if (newApp == null) {
+ Toast.makeText(this, getString(R.string.no_such_app), Toast.LENGTH_LONG).show();
finish();
- return false;
+ return;
}
+ app = newApp;
+
startingIgnoreAll = app.ignoreAllUpdates;
startingIgnoreThis = app.ignoreThisUpdate;
// Get the signature of the installed package...
mInstalledSignature = null;
mInstalledSigID = null;
+
if (app.isInstalled()) {
- PackageManager pm = getBaseContext().getPackageManager();
+ PackageManager pm = getPackageManager();
try {
- PackageInfo pi = pm.getPackageInfo(appid,
- PackageManager.GET_SIGNATURES);
+ PackageInfo pi = pm.getPackageInfo(app.id, PackageManager.GET_SIGNATURES);
mInstalledSignature = pi.signatures[0];
- Hasher hash = new Hasher("MD5", mInstalledSignature
- .toCharsString().getBytes());
+ Hasher hash = new Hasher("MD5", mInstalledSignature.toCharsString().getBytes());
mInstalledSigID = hash.getHash();
} catch (NameNotFoundException e) {
Log.d("FDroid", "Failed to get installed signature");
@@ -514,7 +556,6 @@ public class AppDetails extends ListActivity implements ProgressListener {
mInstalledSignature = null;
}
}
- return true;
}
private void startViews() {
@@ -528,10 +569,10 @@ public class AppDetails extends ListActivity implements ProgressListener {
headerView.removeAllViews();
if (landparent != null) {
landparent.addView(infoView);
- Log.d("FDroid", "Setting landparent infoview");
+ Log.d("FDroid", "Setting up landscape view");
} else {
headerView.addView(infoView);
- Log.d("FDroid", "Setting header infoview");
+ Log.d("FDroid", "Setting up portrait view");
}
// Set the icon...
@@ -627,8 +668,7 @@ public class AppDetails extends ListActivity implements ProgressListener {
if (permissionName.equals("ACCESS_SUPERUSER")) {
sb.append("\t• Full permissions to all device features and storage\n");
} else {
- Log.d("FDroid", "Permission not yet available: "
- +permissionName);
+ Log.d("FDroid", "Permission not yet available: " + permissionName);
}
}
}
@@ -979,9 +1019,9 @@ public class AppDetails extends ListActivity implements ProgressListener {
private void startDownload(Apk apk, String repoAddress) {
downloadHandler = new ApkDownloader(apk, repoAddress, Utils.getApkCacheDir(getBaseContext()));
- getProgressDialog(downloadHandler.getRemoteAddress()).show();
downloadHandler.setProgressListener(this);
downloadHandler.download();
+ updateProgressDialog();
}
private void installApk(File file, String packageName) {
setProgressBarIndeterminateVisibility(true);
@@ -1081,6 +1121,7 @@ public class AppDetails extends ListActivity implements ProgressListener {
@Override
public void onCancel(DialogInterface dialog) {
downloadHandler.cancel();
+ downloadHandler = null;
progressDialog = null;
Toast.makeText(AppDetails.this, getString(R.string.download_cancelled), Toast.LENGTH_LONG).show();
}
@@ -1099,11 +1140,28 @@ public class AppDetails extends ListActivity implements ProgressListener {
return progressDialog;
}
+ /**
+ * Looks at the current downloadHandler
and finds it's size and progress.
+ * This is in comparison to {@link org.fdroid.fdroid.AppDetails#updateProgressDialog(int, int)},
+ * which is used when you have the details from a freshly received
+ * {@link org.fdroid.fdroid.ProgressListener.Event}.
+ */
+ private void updateProgressDialog() {
+ updateProgressDialog(downloadHandler.getProgress(), downloadHandler.getTotalSize());
+ }
+
private void updateProgressDialog(int progress, int total) {
ProgressDialog pd = getProgressDialog(downloadHandler.getRemoteAddress());
- pd.setIndeterminate(false);
- pd.setProgress(progress);
- pd.setMax(total);
+ if (total > 0) {
+ pd.setIndeterminate(false);
+ pd.setProgress(progress);
+ pd.setMax(total);
+ } else {
+ pd.setIndeterminate(true);
+ pd.setProgress(progress);
+ pd.setMax(0);
+ }
+ pd.show();
}
@Override
@@ -1126,9 +1184,8 @@ public class AppDetails extends ListActivity implements ProgressListener {
}
if (finished) {
- // The dialog can't be dismissed when it's not displayed,
- // so do it when the activity is being destroyed.
removeProgressDialog();
+ downloadHandler = null;
}
}