diff --git a/.gitignore b/.gitignore
index 22273c3f4..9708725fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,6 @@
/build.xml
*~
/.idea/
-/*.iml
+*.iml
out
/.settings/
diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java
index 96e82b6bf..fca4c0b10 100644
--- a/src/org/fdroid/fdroid/AppDetails.java
+++ b/src/org/fdroid/fdroid/AppDetails.java
@@ -19,31 +19,24 @@
package org.fdroid.fdroid;
-import android.content.*;
-import android.widget.*;
-
-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;
-import org.xml.sax.XMLReader;
-
+import android.app.Activity;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothAdapter;
+import android.content.*;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.Signature;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
-import android.os.Message;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v4.view.MenuItemCompat;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageInfo;
-import android.content.pm.Signature;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.database.ContentObserver;
import android.text.Editable;
import android.text.Html;
import android.text.Html.TagHandler;
@@ -58,24 +51,34 @@ import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
-import android.graphics.Bitmap;
-
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
-
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.*;
+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.Downloader;
+import org.xml.sax.XMLReader;
import java.io.File;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.List;
-public class AppDetails extends ListActivity {
- private static final String TAG = "AppDetails";
+public class AppDetails extends ListActivity implements ProgressListener {
+ private static final String TAG = "org.fdroid.fdroid.AppDetails";
public static final int REQUEST_ENABLE_BLUETOOTH = 2;
@@ -84,6 +87,7 @@ public class AppDetails extends ListActivity {
private FDroidApp fdroidApp;
private ApkListAdapter adapter;
+ private ProgressDialog progressDialog;
private static class ViewHolder {
TextView version;
@@ -98,7 +102,7 @@ public class AppDetails extends ListActivity {
// observer to update view when package has been installed/deleted
AppObserver myAppObserver;
- class AppObserver extends ContentObserver {
+ class AppObserver extends ContentObserver {
public AppObserver(Handler handler) {
super(handler);
}
@@ -110,7 +114,7 @@ public class AppDetails extends ListActivity {
@Override
public void onChange(boolean selfChange, Uri uri) {
- if (!reset()) {
+ if (!reset(app.id)) {
AppDetails.this.finish();
return;
}
@@ -267,10 +271,8 @@ public class AppDetails extends ListActivity {
private static final int SEND_VIA_BLUETOOTH = Menu.FIRST + 15;
private App app;
- private String appid;
private PackageManager mPm;
- private DownloadHandler downloadHandler;
- private boolean stateRetained;
+ private ApkDownloader downloadHandler;
private boolean startingIgnoreAll;
private int startingIgnoreThis;
@@ -282,6 +284,68 @@ public class AppDetails extends ListActivity {
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);
@@ -307,43 +371,27 @@ public class AppDetails extends ListActivity {
// 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;
}
@@ -377,7 +425,6 @@ public class AppDetails extends ListActivity {
@Override
protected void onResume() {
- Log.d(TAG, "onresume");
super.onResume();
// register observer to know when install status changes
@@ -386,17 +433,45 @@ public class AppDetails extends ListActivity {
AppProvider.getContentUri(app.id),
true,
myAppObserver);
-
- if (!reset()) {
- finish();
- return;
+ if (downloadHandler != null) {
+ if (downloadHandler.isComplete()) {
+ downloadCompleteInstallApk();
+ } else {
+ 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).
+ Log.d(TAG, "Showing dialog to user after resuming app details view, because a download was previously in progress");
+ updateProgressDialog();
+ }
}
+
updateViews();
MenuManager.create(this).invalidateOptionsMenu();
+ }
+ /**
+ * Remove progress listener, suppress progress dialog, set downloadHandler to null.
+ */
+ private void cleanUpFinishedDownload() {
if (downloadHandler != null) {
- downloadHandler.startUpdates();
+ downloadHandler.removeProgressListener();
+ removeProgressDialog();
+ downloadHandler = null;
+ }
+ }
+
+ /**
+ * Once the download completes successfully, call this method to start the install process
+ * with the file that was downloaded.
+ */
+ private void downloadCompleteInstallApk() {
+ if (downloadHandler != null) {
+ assert downloadHandler.isComplete();
+ installApk(downloadHandler.localFile(), downloadHandler.getApk().id);
+ cleanUpFinishedDownload();
}
}
@@ -405,13 +480,18 @@ public class AppDetails extends ListActivity {
if (myAppObserver != null) {
getContentResolver().unregisterContentObserver(myAppObserver);
}
- if (downloadHandler != null) {
- downloadHandler.stopUpdates();
- }
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();
}
@@ -430,65 +510,73 @@ public class AppDetails extends ListActivity {
@Override
public Object onRetainNonConfigurationInstance() {
- stateRetained = true;
- return this;
+ inProcessOfChangingConfiguration = true;
+ return new ConfigurationChangeHelper(downloadHandler, app);
}
@Override
protected void onDestroy() {
if (downloadHandler != null) {
- if (!stateRetained)
+ if (!inProcessOfChangingConfiguration) {
downloadHandler.cancel();
- downloadHandler.destroy();
+ cleanUpFinishedDownload();
+ }
}
+ inProcessOfChangingConfiguration = false;
super.onDestroy();
}
- // 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) {
- if (old.downloadHandler != null)
- downloadHandler = new DownloadHandler(old.downloadHandler);
- app = old.app;
- mInstalledSignature = old.mInstalledSignature;
- mInstalledSigID = old.mInstalledSigID;
+ private void removeProgressDialog() {
+ if (progressDialog != null) {
+ progressDialog.dismiss();
+ progressDialog = null;
+ }
}
// 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");
@@ -497,7 +585,6 @@ public class AppDetails extends ListActivity {
mInstalledSignature = null;
}
}
- return true;
}
private void startViews() {
@@ -511,10 +598,10 @@ public class AppDetails extends ListActivity {
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...
@@ -610,8 +697,7 @@ public class AppDetails extends ListActivity {
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);
}
}
}
@@ -912,6 +998,7 @@ public class AppDetails extends ListActivity {
// Install the version of this app denoted by 'app.curApk'.
private void install(final Apk apk) {
+ final Activity activity = this;
String [] projection = { RepoProvider.DataColumns.ADDRESS };
Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
if (repo == null || repo.address == null) {
@@ -926,10 +1013,8 @@ public class AppDetails extends ListActivity {
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
- int whichButton) {
- downloadHandler = new DownloadHandler(apk,
- repoaddress, Utils
- .getApkCacheDir(getBaseContext()));
+ int whichButton) {
+ startDownload(apk, repoaddress);
}
});
ask_alrt.setNegativeButton(getString(R.string.no),
@@ -958,9 +1043,17 @@ public class AppDetails extends ListActivity {
alert.show();
return;
}
- downloadHandler = new DownloadHandler(apk, repoaddress,
- Utils.getApkCacheDir(getBaseContext()));
+ startDownload(apk, repoaddress);
}
+
+ private void startDownload(Apk apk, String repoAddress) {
+ downloadHandler = new ApkDownloader(apk, repoAddress, Utils.getApkCacheDir(getBaseContext()));
+ downloadHandler.setProgressListener(this);
+ if (downloadHandler.download()) {
+ updateProgressDialog();
+ }
+ }
+
private void installApk(File file, String packageName) {
setProgressBarIndeterminateVisibility(true);
@@ -989,10 +1082,6 @@ public class AppDetails extends ListActivity {
@Override
public void run() {
if (operation == Installer.InstallerCallback.OPERATION_INSTALL) {
- if (downloadHandler != null) {
- downloadHandler = null;
- }
-
PackageManagerCompat.setInstaller(mPm, app.id);
}
@@ -1039,137 +1128,115 @@ public class AppDetails extends ListActivity {
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT, app.name);
- shareIntent.putExtra(Intent.EXTRA_TEXT, app.name+" ("+app.summary+") - https://f-droid.org/app/"+app.id);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, app.name + " (" + app.summary + ") - https://f-droid.org/app/" + app.id);
startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share)));
}
- private ProgressDialog createProgressDialog(String file, int p, int max) {
- final ProgressDialog pd = new ProgressDialog(this);
- pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
- pd.setMessage(getString(R.string.download_server) + ":\n " + file);
- pd.setMax(max);
- pd.setProgress(p);
- pd.setCancelable(true);
- pd.setCanceledOnTouchOutside(false);
- pd.setOnCancelListener(new DialogInterface.OnCancelListener() {
- @Override
- public void onCancel(DialogInterface dialog) {
- downloadHandler.cancel();
- }
- });
- pd.setButton(DialogInterface.BUTTON_NEUTRAL,
- getString(R.string.cancel),
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- pd.cancel();
+ private ProgressDialog getProgressDialog(String file) {
+ if (progressDialog == null) {
+ final ProgressDialog pd = new ProgressDialog(this);
+ pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ pd.setMessage(getString(R.string.download_server) + ":\n " + file);
+ pd.setCancelable(true);
+ pd.setCanceledOnTouchOutside(false);
+
+ // The indeterminate-ness will get overridden on the first progress event we receive.
+ pd.setIndeterminate(true);
+
+ pd.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ Log.d(TAG, "User clicked 'cancel' on download, attempting to interrupt download thread.");
+ if (downloadHandler != null) {
+ downloadHandler.cancel();
+ cleanUpFinishedDownload();
+ } else {
+ Log.e(TAG, "Tried to cancel, but the downloadHandler doesn't exist.");
}
- });
- pd.show();
- return pd;
+ progressDialog = null;
+ Toast.makeText(AppDetails.this, getString(R.string.download_cancelled), Toast.LENGTH_LONG).show();
+ }
+ });
+ pd.setButton(DialogInterface.BUTTON_NEUTRAL,
+ getString(R.string.cancel),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ pd.cancel();
+ }
+ }
+ );
+ progressDialog = pd;
+ }
+ return progressDialog;
}
- // Handler used to update the progress dialog while downloading.
- private class DownloadHandler extends Handler {
- private Downloader download;
- private ProgressDialog pd;
- private boolean updating;
- private String id;
-
- public DownloadHandler(Apk apk, String repoaddress, File destdir) {
- id = apk.id;
- download = new Downloader(apk, repoaddress, destdir);
- download.start();
- startUpdates();
+ /**
+ * 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() {
+ if (downloadHandler != null) {
+ updateProgressDialog(downloadHandler.getProgress(), downloadHandler.getTotalSize());
}
+ }
- public DownloadHandler(DownloadHandler oldHandler) {
- if (oldHandler != null) {
- download = oldHandler.download;
+ private void updateProgressDialog(int progress, int total) {
+ if (downloadHandler != null) {
+ ProgressDialog pd = getProgressDialog(downloadHandler.getRemoteAddress());
+ if (total > 0) {
+ pd.setIndeterminate(false);
+ pd.setProgress(progress);
+ pd.setMax(total);
+ } else {
+ pd.setIndeterminate(true);
+ pd.setProgress(progress);
+ pd.setMax(0);
}
- startUpdates();
- }
-
- public boolean updateProgress() {
- boolean finished = false;
- switch (download.getStatus()) {
- case RUNNING:
- if (pd == null) {
- pd = createProgressDialog(download.remoteFile(),
- download.getProgress(), download.getMax());
- } else {
- pd.setProgress(download.getProgress());
- }
- break;
- case ERROR:
- if (pd != null)
- pd.dismiss();
- String text;
- if (download.getErrorType() == Downloader.Error.CORRUPT)
- text = getString(R.string.corrupt_download);
- else
- text = download.getErrorMessage();
- Toast.makeText(AppDetails.this, text, Toast.LENGTH_LONG).show();
- finished = true;
- break;
- case DONE:
- if (pd != null)
- pd.dismiss();
- installApk(download.localFile(), id);
- finished = true;
- break;
- case CANCELLED:
- Toast.makeText(AppDetails.this,
- getString(R.string.download_cancelled),
- Toast.LENGTH_SHORT).show();
- finished = true;
- break;
- default:
- break;
- }
- return finished;
- }
-
- public void startUpdates() {
- if (!updating) {
- updating = true;
- sendEmptyMessage(0);
+ if (!pd.isShowing()) {
+ Log.d(TAG, "Showing progress dialog for download.");
+ pd.show();
}
}
+ }
- public void stopUpdates() {
- updating = false;
- removeMessages(0);
+ @Override
+ public void onProgress(Event event) {
+ if (downloadHandler == null || !downloadHandler.isEventFromThis(event)) {
+ // Choose not to respond to events from previous downloaders.
+ // We don't even care if we receive "cancelled" events or the like, because
+ // we dealt with cancellations in the onCancel listener of the dialog,
+ // rather than waiting to receive the event here. We try and be careful in
+ // the download thread to make sure that we check for cancellations before
+ // sending events, but it is not possible to be perfect, because the interruption
+ // which triggers the download can happen after the check to see if
+ Log.d(TAG, "Discarding downloader event \"" + event.type + "\" as it is from an old (probably cancelled) downloader.");
+ return;
}
- public void cancel() {
- if (download != null)
- download.interrupt();
- }
-
- public void destroy() {
- // The dialog can't be dismissed when it's not displayed,
- // so do it when the activity is being destroyed.
- if (pd != null) {
- pd.dismiss();
- pd = null;
- }
- // Cancel any scheduled updates so that we don't
- // accidentally recreate the progress dialog.
- stopUpdates();
- }
-
- // Repeatedly run updateProgress() until it's finished.
- @Override
- public void handleMessage(Message msg) {
- if (download == null)
- return;
- boolean finished = updateProgress();
- if (finished)
- download = null;
+ boolean finished = false;
+ if (event.type.equals(Downloader.EVENT_PROGRESS)) {
+ updateProgressDialog(event.progress, event.total);
+ } else if (event.type.equals(ApkDownloader.EVENT_ERROR)) {
+ final String text;
+ if (event.getData().getInt(ApkDownloader.EVENT_DATA_ERROR_TYPE) == ApkDownloader.ERROR_HASH_MISMATCH)
+ text = getString(R.string.corrupt_download);
else
- sendMessageDelayed(obtainMessage(), 50);
+ text = getString(R.string.details_notinstalled);
+ // this must be on the main UI thread
+ Toast.makeText(this, text, Toast.LENGTH_LONG).show();
+ finished = true;
+ } else if (event.type.equals(ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE)) {
+ downloadCompleteInstallApk();
+ finished = true;
+ }
+
+ if (finished) {
+ removeProgressDialog();
+ downloadHandler = null;
}
}
@@ -1183,7 +1250,7 @@ public class AppDetails extends ListActivity {
switch (requestCode) {
case REQUEST_ENABLE_BLUETOOTH:
fdroidApp.sendViaBluetooth(this, resultCode, app.id);
- break;
+ break;
}
}
}
diff --git a/src/org/fdroid/fdroid/Downloader.java b/src/org/fdroid/fdroid/Downloader.java
deleted file mode 100644
index 22bcac17e..000000000
--- a/src/org/fdroid/fdroid/Downloader.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright (C) 2010-2012 Ciaran Gultnieks
- * Copyright (C) 2011 Henrik Tunedal
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 3
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-package org.fdroid.fdroid;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URL;
-
-import android.util.Log;
-import org.fdroid.fdroid.data.Apk;
-
-public class Downloader extends Thread {
-
- private Apk curapk;
- private String repoaddress;
- private String filename;
- private File destdir;
- private File localfile;
-
- public static enum Status {
- STARTING, RUNNING, ERROR, DONE, CANCELLED
- }
-
- public static enum Error {
- CORRUPT, UNKNOWN
- }
-
- private Status status = Status.STARTING;
- private Error error;
- private int progress;
- private int max;
- private String errorMessage;
-
- // Constructor - creates a Downloader to download the given Apk,
- // which must have its detail populated.
- Downloader(Apk apk, String repoaddress, File destdir) {
- curapk = apk;
- this.repoaddress = repoaddress;
- this.destdir = destdir;
- }
-
- public synchronized Status getStatus() {
- return status;
- }
-
- // Current progress and maximum value for progress dialog
- public synchronized int getProgress() {
- return progress;
- }
-
- public synchronized int getMax() {
- return max;
- }
-
- // Error code and error message, only valid if status is ERROR
- public synchronized Error getErrorType() {
- return error;
- }
-
- public synchronized String getErrorMessage() {
- return errorMessage;
- }
-
- // The URL being downloaded or path to a cached file
- public synchronized String remoteFile() {
- return filename;
- }
-
- // The downloaded APK. Valid only when getStatus() has returned STATUS.DONE.
- public File localFile() {
- return localfile;
- }
-
- // The APK being downloaded
- public synchronized Apk getApk() {
- return curapk;
- }
-
- @Override
- public void run() {
-
- InputStream input = null;
- OutputStream output = null;
- String apkname = curapk.apkName;
- localfile = new File(destdir, apkname);
- try {
-
- // See if we already have this apk cached...
- if (localfile.exists()) {
- // We do - if its hash matches, we'll use it...
- Hasher hash = new Hasher(curapk.hashType, localfile);
- if (hash.match(curapk.hash)) {
- Log.d("FDroid", "Using cached apk at " + localfile);
- synchronized (this) {
- progress = 1;
- max = 1;
- status = Status.DONE;
- return;
- }
- } else {
- Log.d("FDroid", "Not using cached apk at " + localfile);
- localfile.delete();
- }
- }
-
- // If we haven't got the apk locally, we'll have to download it...
- String remotefile;
- remotefile = repoaddress + "/" + apkname.replace(" ", "%20");
- Log.d("FDroid", "Downloading apk from " + remotefile);
- synchronized (this) {
- filename = remotefile;
- progress = 0;
- max = curapk.size;
- status = Status.RUNNING;
- }
-
- input = new URL(remotefile).openStream();
- output = new FileOutputStream(localfile);
- byte data[] = new byte[Utils.BUFFER_SIZE];
- while (true) {
- if (isInterrupted()) {
- Log.d("FDroid", "Download cancelled!");
- break;
- }
- int count = input.read(data);
- if (count == -1) {
- break;
- }
- output.write(data, 0, count);
- synchronized (this) {
- progress += count;
- }
- }
-
- if (isInterrupted()) {
- localfile.delete();
- synchronized (this) {
- status = Status.CANCELLED;
- }
- return;
- }
- Hasher hash = new Hasher(curapk.hashType, localfile);
- if (!hash.match(curapk.hash)) {
- synchronized (this) {
- Log.d("FDroid", "Downloaded file hash of " + hash.getHash()
- + " did not match repo's " + curapk.hash);
- // No point keeping a bad file, whether we're
- // caching or not.
- localfile.delete();
- error = Error.CORRUPT;
- errorMessage = null;
- status = Status.ERROR;
- return;
- }
- }
- } catch (Exception e) {
- Log.e("FDroid", "Download failed:\n" + Log.getStackTraceString(e));
- synchronized (this) {
- localfile.delete();
- error = Error.UNKNOWN;
- errorMessage = e.toString();
- status = Status.ERROR;
- return;
- }
- } finally {
- Utils.closeQuietly(output);
- Utils.closeQuietly(input);
- }
-
- Log.d("FDroid", "Download finished: " + localfile);
- synchronized (this) {
- status = Status.DONE;
- }
- }
-}
diff --git a/src/org/fdroid/fdroid/ProgressListener.java b/src/org/fdroid/fdroid/ProgressListener.java
index 5550c1492..6af2daf4b 100644
--- a/src/org/fdroid/fdroid/ProgressListener.java
+++ b/src/org/fdroid/fdroid/ProgressListener.java
@@ -1,8 +1,10 @@
+
package org.fdroid.fdroid;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.TextUtils;
public interface ProgressListener {
@@ -15,7 +17,7 @@ public interface ProgressListener {
public static final int NO_VALUE = Integer.MIN_VALUE;
- public final int type;
+ public final String type;
public final Bundle data;
// These two are not final, so that you can create a template Event,
@@ -25,31 +27,19 @@ public interface ProgressListener {
public int progress;
public int total;
- public Event(int type) {
+ public Event(String type) {
this(type, NO_VALUE, NO_VALUE, null);
}
- public Event(int type, Bundle data) {
+ public Event(String type, Bundle data) {
this(type, NO_VALUE, NO_VALUE, data);
}
- public Event(int type, int progress) {
- this(type, progress, NO_VALUE, null);
- }
-
- public Event(int type, int progress, Bundle data) {
- this(type, NO_VALUE, NO_VALUE, data);
- }
-
- public Event(int type, int progress, int total) {
- this(type, progress, total, null);
- }
-
- public Event(int type, int progress, int total, Bundle data) {
+ public Event(String type, int progress, int total, Bundle data) {
this.type = type;
this.progress = progress;
this.total = total;
- this.data = data == null ? new Bundle() : data;
+ this.data = (data == null) ? new Bundle() : data;
}
@Override
@@ -59,7 +49,7 @@ public interface ProgressListener {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(type);
+ dest.writeString(type);
dest.writeInt(progress);
dest.writeInt(total);
dest.writeBundle(data);
@@ -68,7 +58,7 @@ public interface ProgressListener {
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
@Override
public Event createFromParcel(Parcel in) {
- return new Event(in.readInt(), in.readInt(), in.readInt(), in.readBundle());
+ return new Event(in.readString(), in.readInt(), in.readInt(), in.readBundle());
}
@Override
@@ -77,6 +67,16 @@ public interface ProgressListener {
}
};
+ /**
+ * Can help to provide context to the listener about what process is causing the event.
+ * For example, the repo updater uses one listener to listen to multiple downloaders.
+ * When it receives an event, it doesn't know which repo download is causing the event,
+ * so we pass that through to the downloader when we set the progress listener. This way,
+ * we can ask the event for the name of the repo.
+ */
+ public Bundle getData() {
+ return data;
+ }
}
}
diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java
index 1c7fb5009..f85a8eafc 100644
--- a/src/org/fdroid/fdroid/RepoXMLHandler.java
+++ b/src/org/fdroid/fdroid/RepoXMLHandler.java
@@ -279,12 +279,13 @@ public class RepoXMLHandler extends DefaultHandler {
} else if (localName.equals("application") && curapp == null) {
curapp = new App();
curapp.id = attributes.getValue("", "id");
- Bundle progressData = RepoUpdater.createProgressData(repo.address);
progressCounter ++;
+ Bundle data = new Bundle(1);
+ data.putString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS, repo.address);
progressListener.onProgress(
new ProgressListener.Event(
- RepoUpdater.PROGRESS_TYPE_PROCESS_XML, progressCounter,
- totalAppCount, progressData));
+ RepoUpdater.PROGRESS_TYPE_PROCESS_XML,
+ progressCounter, totalAppCount, data));
} else if (localName.equals("package") && curapp != null && curapk == null) {
curapk = new Apk();
diff --git a/src/org/fdroid/fdroid/UpdateService.java b/src/org/fdroid/fdroid/UpdateService.java
index 471b120f2..bf7fd8b71 100644
--- a/src/org/fdroid/fdroid/UpdateService.java
+++ b/src/org/fdroid/fdroid/UpdateService.java
@@ -33,6 +33,7 @@ import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import org.fdroid.fdroid.data.*;
+import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.updater.RepoUpdater;
import java.util.*;
@@ -47,6 +48,14 @@ public class UpdateService extends IntentService implements ProgressListener {
public static final int STATUS_ERROR = 2;
public static final int STATUS_INFO = 3;
+ // I don't like that I've had to dupliacte the statuses above with strings here, however
+ // one method of communication/notification is using ResultReceiver (int status codes)
+ // while the other uses progress events (string event types).
+ public static final String EVENT_COMPLETE_WITH_CHANGES = "repoUpdateComplete (changed)";
+ public static final String EVENT_COMPLETE_AND_SAME = "repoUpdateComplete (not changed)";
+ public static final String EVENT_ERROR = "repoUpdateError";
+ public static final String EVENT_INFO = "repoUpdateInfo";
+
public static final String EXTRA_RECEIVER = "receiver";
public static final String EXTRA_ADDRESS = "address";
@@ -97,28 +106,31 @@ public class UpdateService extends IntentService implements ProgressListener {
return this;
}
+ private void forwardEvent(String type) {
+ if (listener != null) {
+ listener.onProgress(new Event(type));
+ }
+ }
+
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
String message = resultData.getString(UpdateService.RESULT_MESSAGE);
boolean finished = false;
if (resultCode == UpdateService.STATUS_ERROR) {
+ forwardEvent(EVENT_ERROR);
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
finished = true;
- } else if (resultCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES
- || resultCode == UpdateService.STATUS_COMPLETE_AND_SAME) {
+ } else if (resultCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
+ forwardEvent(EVENT_COMPLETE_WITH_CHANGES);
+ finished = true;
+ } else if (resultCode == UpdateService.STATUS_COMPLETE_AND_SAME) {
+ forwardEvent(EVENT_COMPLETE_AND_SAME);
finished = true;
} else if (resultCode == UpdateService.STATUS_INFO) {
+ forwardEvent(EVENT_INFO);
dialog.setMessage(message);
}
- // Forward the progress event on to anybody else who'd like to know.
- if (listener != null) {
- Parcelable event = resultData.getParcelable(UpdateService.RESULT_EVENT);
- if (event != null && event instanceof Event) {
- listener.onProgress((Event)event);
- }
- }
-
if (finished && dialog.isShowing())
try {
dialog.dismiss();
@@ -185,17 +197,10 @@ public class UpdateService extends IntentService implements ProgressListener {
}
protected void sendStatus(int statusCode, String message) {
- sendStatus(statusCode, message, null);
- }
-
- protected void sendStatus(int statusCode, String message, Event event) {
if (receiver != null) {
Bundle resultData = new Bundle();
if (message != null && message.length() > 0)
resultData.putString(RESULT_MESSAGE, message);
- if (event == null)
- event = new Event(statusCode);
- resultData.putParcelable(RESULT_EVENT, event);
receiver.send(statusCode, resultData);
}
}
@@ -675,14 +680,15 @@ public class UpdateService extends IntentService implements ProgressListener {
@Override
public void onProgress(ProgressListener.Event event) {
String message = "";
- if (event.type == RepoUpdater.PROGRESS_TYPE_DOWNLOAD) {
- String repoAddress = event.data.getString(RepoUpdater.PROGRESS_DATA_REPO);
- String downloadedSize = Utils.getFriendlySize( event.progress );
- String totalSize = Utils.getFriendlySize( event.total );
+ // TODO: Switch to passing through Bundles of data with the event, rather than a repo address. They are
+ // now much more general purpose then just repo downloading.
+ String repoAddress = event.getData().getString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS);
+ if (event.type.equals(Downloader.EVENT_PROGRESS)) {
+ String downloadedSize = Utils.getFriendlySize(event.progress);
+ String totalSize = Utils.getFriendlySize(event.total);
int percent = (int)((double)event.progress/event.total * 100);
message = getString(R.string.status_download, repoAddress, downloadedSize, totalSize, percent);
- } else if (event.type == RepoUpdater.PROGRESS_TYPE_PROCESS_XML) {
- String repoAddress = event.data.getString(RepoUpdater.PROGRESS_DATA_REPO);
+ } else if (event.type.equals(RepoUpdater.PROGRESS_TYPE_PROCESS_XML)) {
message = getString(R.string.status_processing_xml, repoAddress, event.progress, event.total);
}
sendStatus(STATUS_INFO, message);
diff --git a/src/org/fdroid/fdroid/net/ApkDownloader.java b/src/org/fdroid/fdroid/net/ApkDownloader.java
new file mode 100644
index 000000000..baddfd5ef
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/ApkDownloader.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2010-2012 Ciaran Gultnieks
+ * Copyright (C) 2011 Henrik Tunedal
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+package org.fdroid.fdroid.net;
+
+import android.os.Bundle;
+import android.util.Log;
+import org.fdroid.fdroid.Hasher;
+import org.fdroid.fdroid.ProgressListener;
+import org.fdroid.fdroid.data.Apk;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Downloads and verifies (against the Apk.hash) the apk file.
+ * If the file has previously been downloaded, it will make use of that
+ * instead, without going to the network to download a new one.
+ */
+public class ApkDownloader implements AsyncDownloadWrapper.Listener {
+
+ private static final String TAG = "org.fdroid.fdroid.net.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 int ERROR_HASH_MISMATCH = 101;
+ public static final int ERROR_DOWNLOAD_FAILED = 102;
+ public static final int ERROR_UNKNOWN = 103;
+
+ private static final String EVENT_SOURCE_ID = "sourceId";
+ private static long downloadIdCounter = 0;
+
+ /**
+ * Used as a key to pass data through with an error event, explaining the type of event.
+ */
+ public static final String EVENT_DATA_ERROR_TYPE = "apkDownloadErrorType";
+
+ private Apk curApk;
+ private String repoAddress;
+ private File localFile;
+
+ private ProgressListener listener;
+ private AsyncDownloadWrapper dlWrapper = null;
+ private int progress = 0;
+ private int totalSize = 0;
+ private boolean isComplete = false;
+
+ private long id = ++downloadIdCounter;
+
+ public void setProgressListener(ProgressListener listener) {
+ this.listener = listener;
+ }
+
+ public void removeProgressListener() {
+ setProgressListener(null);
+ }
+
+ public ApkDownloader(Apk apk, String repoAddress, File destDir) {
+ curApk = apk;
+ this.repoAddress = repoAddress;
+ localFile = new File(destDir, curApk.apkName);
+ }
+
+ /**
+ * The downloaded APK. Valid only when getStatus() has returned STATUS.DONE.
+ */
+ public File localFile() {
+ return localFile;
+ }
+
+ /**
+ * When stopping/starting downloaders multiple times (on different threads), it can
+ * get weird whereby different threads are sending progress events. It is important
+ * to be able to see which downloader these progress events are coming from.
+ */
+ public boolean isEventFromThis(Event event) {
+ return event.getData().containsKey(EVENT_SOURCE_ID) && event.getData().getLong(EVENT_SOURCE_ID) == id;
+ }
+
+ public String getRemoteAddress() {
+ return repoAddress + "/" + curApk.apkName.replace(" ", "%20");
+ }
+
+ private Hasher createHasher() {
+ Hasher hasher;
+ try {
+ hasher = new Hasher(curApk.hashType, localFile);
+ } catch (NoSuchAlgorithmException e) {
+ Log.e("FDroid", "Error verifying hash of cached apk at " + localFile + ". " +
+ "I don't understand what the " + curApk.hashType + " hash algorithm is :(");
+ hasher = null;
+ }
+ return hasher;
+ }
+
+ private boolean hashMatches() {
+ if (!localFile.exists()) {
+ return false;
+ }
+ Hasher hasher = createHasher();
+ return hasher != null && hasher.match(curApk.hash);
+ }
+
+ /**
+ * If an existing cached version exists, and matches the hash of the apk we
+ * want to download, then we will return true. Otherwise, we return false
+ * (and remove the cached file - if it exists and didn't match the correct hash).
+ */
+ private boolean verifyOrDeleteCachedVersion() {
+ if (localFile.exists()) {
+ if (hashMatches()) {
+ Log.d("FDroid", "Using cached apk at " + localFile);
+ return true;
+ } else {
+ Log.d("FDroid", "Not using cached apk at " + localFile);
+ deleteLocalFile();
+ }
+ }
+ return false;
+ }
+
+ private void deleteLocalFile() {
+ if (localFile != null && localFile.exists()) {
+ localFile.delete();
+ }
+ }
+
+ private void sendCompleteMessage() {
+ isComplete = true;
+ sendMessage(EVENT_APK_DOWNLOAD_COMPLETE);
+ }
+
+ public boolean isComplete() {
+ return this.isComplete;
+ }
+
+ /**
+ * If the download successfully spins up a new thread to start downloading, then we return
+ * true, otherwise false. This is useful, e.g. when we use a cached version, and so don't
+ * want to bother with progress dialogs et al.
+ */
+ public boolean download() {
+
+ // Can we use the cached version?
+ if (verifyOrDeleteCachedVersion()) {
+ sendCompleteMessage();
+ return false;
+ }
+
+ String remoteAddress = getRemoteAddress();
+ Log.d(TAG, "Downloading apk from " + remoteAddress);
+
+ try {
+
+ Downloader downloader = new HttpDownloader(remoteAddress, localFile);
+ dlWrapper = new AsyncDownloadWrapper(downloader, this);
+ dlWrapper.download();
+ return true;
+
+ } catch (MalformedURLException e) {
+ onErrorDownloading(e.getLocalizedMessage());
+ } catch (IOException e) {
+ onErrorDownloading(e.getLocalizedMessage());
+ }
+
+ return false;
+ }
+
+ private void sendMessage(String type) {
+ sendProgressEvent(new ProgressListener.Event(type));
+ }
+
+ private void sendError(int errorType) {
+ Bundle data = new Bundle(1);
+ data.putInt(EVENT_DATA_ERROR_TYPE, errorType);
+ sendProgressEvent(new Event(EVENT_ERROR, data));
+ }
+
+ 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;
+ }
+
+ event.getData().putLong(EVENT_SOURCE_ID, id);
+
+ if (listener != null) {
+ listener.onProgress(event);
+ }
+ }
+
+ @Override
+ public void onReceiveTotalDownloadSize(int size) {
+ // Do nothing...
+ // Rather, we will obtain the total download size from the progress events
+ // when they start coming through.
+ }
+
+ @Override
+ public void onReceiveCacheTag(String cacheTag) {
+ // Do nothing...
+ }
+
+ @Override
+ public void onErrorDownloading(String localisedExceptionDetails) {
+ Log.e("FDroid", "Download failed: " + localisedExceptionDetails);
+ sendError(ERROR_DOWNLOAD_FAILED);
+ deleteLocalFile();
+ }
+
+ @Override
+ public void onDownloadComplete() {
+
+ if (!verifyOrDeleteCachedVersion()) {
+ sendError(ERROR_HASH_MISMATCH);
+ return;
+ }
+
+ Log.d("FDroid", "Download finished: " + localFile);
+ sendCompleteMessage();
+ }
+
+ @Override
+ public void onDownloadCancelled() {
+ sendMessage(EVENT_APK_DOWNLOAD_CANCELLED);
+ }
+
+ @Override
+ public void onProgress(Event event) {
+ sendProgressEvent(event);
+ }
+
+ /**
+ * Attempts to cancel the download (if in progress) and also removes the progress
+ * listener (to prevent
+ */
+ public void cancel() {
+ if (dlWrapper != null) {
+ dlWrapper.attemptCancel();
+ }
+ }
+
+ public Apk getApk() {
+ return curApk;
+ }
+
+ public int getProgress() {
+ return progress;
+ }
+
+ public int getTotalSize() {
+ return totalSize;
+ }
+}
diff --git a/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java b/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java
new file mode 100644
index 000000000..5e250aaa4
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java
@@ -0,0 +1,145 @@
+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;
+
+/**
+ * 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.
+ */
+public class AsyncDownloadWrapper extends Handler {
+
+ private static final String TAG = "org.fdroid.fdroid.net.AsyncDownloadWrapper";
+
+ private static final int MSG_PROGRESS = 1;
+ 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";
+
+ private Downloader downloader;
+ private Listener listener;
+ private DownloadThread downloadThread = null;
+
+ /**
+ * 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 AsyncDownloadWrapper(Downloader downloader, Listener listener) {
+ this.downloader = downloader;
+ this.listener = listener;
+ }
+
+ public void fetchTotalDownloadSize() {
+ int size = downloader.totalDownloadSize();
+ listener.onReceiveTotalDownloadSize(size);
+ }
+
+ public void fetchCacheTag() {
+ String cacheTag = downloader.getCacheTag();
+ listener.onReceiveCacheTag(cacheTag);
+ }
+
+ public void download() {
+ downloadThread = new DownloadThread();
+ downloadThread.start();
+ }
+
+ public void attemptCancel() {
+ if (downloadThread != null) {
+ downloadThread.interrupt();
+ }
+ }
+
+ public static class NotDownloadingException extends Exception {
+ public NotDownloadingException(String message) {
+ super(message);
+ }
+ }
+
+ public void cancelDownload() throws NotDownloadingException {
+ if (downloadThread == null) {
+ throw new RuntimeException("Can't cancel download, it hasn't started yet.");
+ } else if (!downloadThread.isAlive()) {
+ throw new RuntimeException("Can't cancel download, it is already finished.");
+ }
+
+ downloadThread.interrupt();
+ }
+
+ /**
+ * Receives "messages" from the download thread, and passes them onto the
+ * relevant {@link org.fdroid.fdroid.net.AsyncDownloadWrapper.Listener}
+ * @param message
+ */
+ public void handleMessage(Message message) {
+ if (message.arg1 == MSG_PROGRESS) {
+ Bundle data = message.getData();
+ ProgressListener.Event event = data.getParcelable(MSG_DATA);
+ listener.onProgress(event);
+ } else if (message.arg1 == MSG_DOWNLOAD_COMPLETE) {
+ listener.onDownloadComplete();
+ } else if (message.arg1 == MSG_DOWNLOAD_CANCELLED) {
+ listener.onDownloadCancelled();
+ } else if (message.arg1 == MSG_ERROR) {
+ listener.onErrorDownloading(message.getData().getString(MSG_DATA));
+ }
+ }
+
+ public interface Listener extends ProgressListener {
+ public void onReceiveTotalDownloadSize(int size);
+ public void onReceiveCacheTag(String cacheTag);
+ public void onErrorDownloading(String localisedExceptionDetails);
+ public void onDownloadComplete();
+ public void onDownloadCancelled();
+ }
+
+ private class DownloadThread extends Thread implements ProgressListener {
+
+ public void run() {
+ try {
+ downloader.setProgressListener(this);
+ downloader.download();
+ sendMessage(MSG_DOWNLOAD_COMPLETE);
+ } catch (InterruptedException e) {
+ sendMessage(MSG_DOWNLOAD_CANCELLED);
+ } catch (IOException e) {
+ Log.e(TAG, e.getMessage() + ": " + Log.getStackTraceString(e));
+ Bundle data = new Bundle(1);
+ data.putString(MSG_DATA, e.getLocalizedMessage());
+ Message message = new Message();
+ message.arg1 = MSG_ERROR;
+ message.setData(data);
+ AsyncDownloadWrapper.this.sendMessage(message);
+ }
+ }
+
+ private void sendMessage(int messageType) {
+ Message message = new Message();
+ message.arg1 = messageType;
+ AsyncDownloadWrapper.this.sendMessage(message);
+ }
+
+ @Override
+ public void onProgress(Event event) {
+ Message message = new Message();
+ Bundle data = new Bundle();
+ data.putParcelable(MSG_DATA, event);
+ message.setData(data);
+ message.arg1 = MSG_PROGRESS;
+ AsyncDownloadWrapper.this.sendMessage(message);
+ }
+ }
+
+}
diff --git a/src/org/fdroid/fdroid/net/Downloader.java b/src/org/fdroid/fdroid/net/Downloader.java
index 27fc6bb79..8c9f7cf77 100644
--- a/src/org/fdroid/fdroid/net/Downloader.java
+++ b/src/org/fdroid/fdroid/net/Downloader.java
@@ -1,55 +1,86 @@
package org.fdroid.fdroid.net;
-import java.io.*;
-import java.net.*;
-import android.content.*;
-import org.fdroid.fdroid.*;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
-public class Downloader {
+import org.fdroid.fdroid.ProgressListener;
+import org.fdroid.fdroid.Utils;
- private static final String HEADER_IF_NONE_MATCH = "If-None-Match";
- private static final String HEADER_FIELD_ETAG = "ETag";
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
- private URL sourceUrl;
+public abstract class Downloader {
+
+ private static final String TAG = "org.fdroid.fdroid.net.Downloader";
private OutputStream outputStream;
+
private ProgressListener progressListener = null;
- private ProgressListener.Event progressEvent = null;
- private String eTag = null;
- private final File outputFile;
- private HttpURLConnection connection;
- private int statusCode = -1;
+ private Bundle eventData = null;
+ private File outputFile;
+ protected String cacheTag = null;
+
+ public static final String EVENT_PROGRESS = "downloadProgress";
+
+ public abstract InputStream inputStream() throws IOException;
// The context is required for opening the file to write to.
- public Downloader(String source, String destFile, Context ctx)
+ public Downloader(String destFile, Context ctx)
throws FileNotFoundException, MalformedURLException {
- sourceUrl = new URL(source);
- outputStream = ctx.openFileOutput(destFile, Context.MODE_PRIVATE);
- outputFile = new File(ctx.getFilesDir() + File.separator + destFile);
+ this(new File(ctx.getFilesDir() + File.separator + destFile));
}
- /**
- * Downloads to a temporary file, which *you must delete yourself when
- * you are done*.
- * @see org.fdroid.fdroid.net.Downloader#getFile()
- */
- public Downloader(String source, Context ctx) throws IOException {
+ // The context is required for opening the file to write to.
+ public Downloader(Context ctx) throws IOException {
+ this(File.createTempFile("dl-", "", ctx.getCacheDir()));
+ }
+
+ public Downloader(File destFile)
+ throws FileNotFoundException, MalformedURLException {
// http://developer.android.com/guide/topics/data/data-storage.html#InternalCache
- outputFile = File.createTempFile("dl-", "", ctx.getCacheDir());
+ outputFile = destFile;
outputStream = new FileOutputStream(outputFile);
- sourceUrl = new URL(source);
}
- public Downloader(String source, OutputStream output)
+ public Downloader(OutputStream output)
throws MalformedURLException {
- sourceUrl = new URL(source);
outputStream = output;
outputFile = null;
}
- public void setProgressListener(ProgressListener progressListener,
- ProgressListener.Event progressEvent) {
- this.progressListener = progressListener;
- this.progressEvent = progressEvent;
+ public void setProgressListener(ProgressListener listener) {
+ setProgressListener(listener, null);
+ }
+
+ public void setProgressListener(ProgressListener listener, Bundle eventData) {
+ this.progressListener = listener;
+ this.eventData = eventData;
+ }
+
+ /**
+ * If you ask for the cacheTag before calling download(), you will get the
+ * same one you passed in (if any). If you call it after download(), you
+ * will get the new cacheTag from the server, or null if there was none.
+ */
+ public String getCacheTag() {
+ return cacheTag;
+ }
+
+ /**
+ * If this cacheTag matches that returned by the server, then no download will
+ * take place, and a status code of 304 will be returned by download().
+ */
+ public void setCacheTag(String cacheTag) {
+ this.cacheTag = cacheTag;
+ }
+
+ protected boolean wantToCheckCache() {
+ return cacheTag != null;
}
/**
@@ -61,82 +92,103 @@ public class Downloader {
return outputFile;
}
+ public abstract boolean hasChanged();
+
+ public abstract int totalDownloadSize();
+
/**
- * Only available after downloading a file.
+ * 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 int getStatusCode() {
- return statusCode;
+ public void downloadUninterrupted() throws IOException {
+ try {
+ download();
+ } catch (InterruptedException ignored) {}
+ }
+
+ public abstract void download() throws IOException, InterruptedException;
+
+ public abstract boolean isCached();
+
+ protected void downloadFromStream() throws IOException, InterruptedException {
+ Log.d(TAG, "Downloading from stream");
+ InputStream input = null;
+ try {
+ input = inputStream();
+
+ // Getting the input stream is slow(ish) for HTTP downloads, so we'll check if
+ // we were interrupted before proceeding to the download.
+ throwExceptionIfInterrupted();
+
+ copyInputToOutputStream(inputStream());
+ } finally {
+ Utils.closeQuietly(outputStream);
+ Utils.closeQuietly(input);
+ }
+
+ // Even if we have completely downloaded the file, we should probably respect
+ // the wishes of the user who wanted to cancel us.
+ throwExceptionIfInterrupted();
}
/**
- * If you ask for the eTag before calling download(), you will get the
- * same one you passed in (if any). If you call it after download(), you
- * will get the new eTag from the server, or null if there was none.
+ * In a synchronous download (the usual usage of the Downloader interface),
+ * you will not be able to interrupt this because the thread will block
+ * after you have called download(). However if you use the AsyncDownloadWrapper,
+ * then it will use this mechanism to cancel the download.
+ *
+ * After every network operation that could take a while, we will check if an
+ * interrupt occured during that blocking operation. The goal is to ensure we
+ * don't move onto another slow, network operation if we have cancelled the
+ * download.
+ * @throws InterruptedException
*/
- public String getETag() {
- return eTag;
+ private void throwExceptionIfInterrupted() throws InterruptedException {
+ if (Thread.interrupted()) {
+ Log.d(TAG, "Received interrupt, cancelling download");
+ throw new InterruptedException();
+ }
}
- /**
- * If this eTag matches that returned by the server, then no download will
- * take place, and a status code of 304 will be returned by download().
- */
- public void setETag(String eTag) {
- this.eTag = eTag;
- }
+ protected void copyInputToOutputStream(InputStream input) throws IOException, InterruptedException {
- // Get a remote file. Returns the HTTP response code.
- // If 'etag' is not null, it's passed to the server as an If-None-Match
- // header, in which case expect a 304 response if nothing changed.
- // In the event of a 200 response ONLY, 'retag' (which should be passed
- // empty) may contain an etag value for the response, or it may be left
- // empty if none was available.
- public int download() throws IOException {
- connection = (HttpURLConnection)sourceUrl.openConnection();
- setupCacheCheck();
- statusCode = connection.getResponseCode();
- if (statusCode == 200) {
- setupProgressListener();
- InputStream input = null;
- try {
- input = connection.getInputStream();
- Utils.copy(input, outputStream,
- progressListener, progressEvent);
- } finally {
- Utils.closeQuietly(outputStream);
- Utils.closeQuietly(input);
+ byte[] buffer = new byte[Utils.BUFFER_SIZE];
+ int bytesRead = 0;
+ int totalBytes = totalDownloadSize();
+
+ // Getting the total download size could potentially take time, depending on how
+ // it is implemented, so we may as well check this before we proceed.
+ throwExceptionIfInterrupted();
+
+ sendProgress(bytesRead, totalBytes);
+ while (true) {
+
+ int count = input.read(buffer);
+ throwExceptionIfInterrupted();
+
+ bytesRead += count;
+ sendProgress(bytesRead, totalBytes);
+ if (count == -1) {
+ Log.d(TAG, "Finished downloading from stream");
+ break;
}
- updateCacheCheck();
+ outputStream.write(buffer, 0, count);
}
- return statusCode;
+ outputStream.flush();
}
- protected void setupCacheCheck() {
- if (eTag != null) {
- connection.setRequestProperty(HEADER_IF_NONE_MATCH, eTag);
+ protected void sendProgress(int bytesRead, int totalBytes) {
+ sendProgress(new ProgressListener.Event(EVENT_PROGRESS, bytesRead, totalBytes, eventData));
+ }
+
+ protected void sendProgress(ProgressListener.Event event) {
+ if (progressListener != null) {
+ progressListener.onProgress(event);
}
}
- protected void updateCacheCheck() {
- eTag = connection.getHeaderField(HEADER_FIELD_ETAG);
- }
-
- protected void setupProgressListener() {
- if (progressListener != null && progressEvent != null) {
- // Testing in the emulator for me, showed that figuring out the
- // filesize took about 1 to 1.5 seconds.
- // To put this in context, downloading a repo of:
- // - 400k takes ~6 seconds
- // - 5k takes ~3 seconds
- // on my connection. I think the 1/1.5 seconds is worth it,
- // because as the repo grows, the tradeoff will
- // become more worth it.
- progressEvent.total = connection.getContentLength();
- }
- }
-
- public boolean hasChanged() {
- return this.statusCode == 200;
- }
-
}
diff --git a/src/org/fdroid/fdroid/net/HttpDownloader.java b/src/org/fdroid/fdroid/net/HttpDownloader.java
new file mode 100644
index 000000000..177fa6de1
--- /dev/null
+++ b/src/org/fdroid/fdroid/net/HttpDownloader.java
@@ -0,0 +1,128 @@
+package org.fdroid.fdroid.net;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.UnknownHostException;
+
+import javax.net.ssl.SSLHandshakeException;
+
+public class HttpDownloader extends Downloader {
+ private static final String TAG = "org.fdroid.fdroid.net.HttpDownloader";
+
+ private static final String HEADER_IF_NONE_MATCH = "If-None-Match";
+ private static final String HEADER_FIELD_ETAG = "ETag";
+
+ private URL sourceUrl;
+ private HttpURLConnection connection;
+ private int statusCode = -1;
+
+ // The context is required for opening the file to write to.
+ public HttpDownloader(String source, String destFile, Context ctx)
+ throws FileNotFoundException, MalformedURLException {
+ super(destFile, ctx);
+ sourceUrl = new URL(source);
+ }
+
+ // The context is required for opening the file to write to.
+ public HttpDownloader(String source, File destFile)
+ throws FileNotFoundException, MalformedURLException {
+ super(destFile);
+ sourceUrl = new URL(source);
+ }
+
+ /**
+ * Downloads to a temporary file, which *you must delete yourself when
+ * you are done*.
+ * @see org.fdroid.fdroid.net.HttpDownloader#getFile()
+ */
+ public HttpDownloader(String source, Context ctx) throws IOException {
+ super(ctx);
+ sourceUrl = new URL(source);
+ }
+
+ public HttpDownloader(String source, OutputStream output)
+ throws MalformedURLException {
+ super(output);
+ sourceUrl = new URL(source);
+ }
+
+ public InputStream inputStream() throws IOException {
+ return connection.getInputStream();
+ }
+
+ // Get a remote file. Returns the HTTP response code.
+ // If 'etag' is not null, it's passed to the server as an If-None-Match
+ // header, in which case expect a 304 response if nothing changed.
+ // In the event of a 200 response ONLY, 'retag' (which should be passed
+ // empty) may contain an etag value for the response, or it may be left
+ // empty if none was available.
+ @Override
+ public void download() throws IOException, InterruptedException {
+ try {
+ connection = (HttpURLConnection)sourceUrl.openConnection();
+
+ if (wantToCheckCache()) {
+ setupCacheCheck();
+ Log.i(TAG, "Checking cached status of " + sourceUrl);
+ statusCode = connection.getResponseCode();
+ }
+
+ if (isCached()) {
+ Log.i(TAG, sourceUrl + " is cached, so not downloading (HTTP " + statusCode + ")");
+ } else {
+ Log.i(TAG, "Downloading from " + sourceUrl);
+ downloadFromStream();
+ updateCacheCheck();
+ }
+ } catch (SSLHandshakeException e) {
+ // TODO this should be handled better, it is not internationalised here.
+ throw new IOException(
+ "A problem occurred while establishing an SSL " +
+ "connection. If this problem persists, AND you have a " +
+ "very old device, you could try using http instead of " +
+ "https for the repo URL." + Log.getStackTraceString(e) );
+ }
+ }
+
+ public boolean isCached() {
+ return wantToCheckCache() && statusCode == 304;
+ }
+
+ private void setupCacheCheck() {
+ if (cacheTag != null) {
+ connection.setRequestProperty(HEADER_IF_NONE_MATCH, cacheTag);
+ }
+ }
+
+ private void updateCacheCheck() {
+ cacheTag = connection.getHeaderField(HEADER_FIELD_ETAG);
+ }
+
+ // Testing in the emulator for me, showed that figuring out the
+ // filesize took about 1 to 1.5 seconds.
+ // To put this in context, downloading a repo of:
+ // - 400k takes ~6 seconds
+ // - 5k takes ~3 seconds
+ // on my connection. I think the 1/1.5 seconds is worth it,
+ // because as the repo grows, the tradeoff will
+ // become more worth it.
+ @Override
+ public int totalDownloadSize() {
+ return connection.getContentLength();
+ }
+
+ @Override
+ public boolean hasChanged() {
+ return this.statusCode != 304;
+ }
+
+}
diff --git a/src/org/fdroid/fdroid/updater/RepoUpdater.java b/src/org/fdroid/fdroid/updater/RepoUpdater.java
index 486798154..eab4291d7 100644
--- a/src/org/fdroid/fdroid/updater/RepoUpdater.java
+++ b/src/org/fdroid/fdroid/updater/RepoUpdater.java
@@ -4,6 +4,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
+
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.RepoXMLHandler;
import org.fdroid.fdroid.Utils;
@@ -12,24 +13,29 @@ import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.net.Downloader;
+import org.fdroid.fdroid.net.HttpDownloader;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
-import javax.net.ssl.SSLHandshakeException;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-import java.io.*;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
abstract public class RepoUpdater {
- public static final int PROGRESS_TYPE_DOWNLOAD = 1;
- public static final int PROGRESS_TYPE_PROCESS_XML = 2;
- public static final String PROGRESS_DATA_REPO = "repo";
+ public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml";
+
+ public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress";
public static RepoUpdater createUpdaterFor(Context ctx, Repo repo) {
if (repo.fingerprint == null && repo.pubkey == null) {
@@ -68,10 +74,6 @@ abstract public class RepoUpdater {
return apks;
}
- public boolean isInteractive() {
- return progressListener != null;
- }
-
/**
* For example, you may want to unzip a jar file to get the index inside,
* or if the file is not compressed, you can just return a reference to
@@ -85,47 +87,26 @@ abstract public class RepoUpdater {
protected abstract String getIndexAddress();
protected Downloader downloadIndex() throws UpdateException {
- Bundle progressData = createProgressData(repo.address);
Downloader downloader = null;
try {
- downloader = new Downloader(getIndexAddress(), context);
- downloader.setETag(repo.lastetag);
+ downloader = new HttpDownloader(getIndexAddress(), context);
+ downloader.setCacheTag(repo.lastetag);
- if (isInteractive()) {
- ProgressListener.Event event =
- new ProgressListener.Event(
- RepoUpdater.PROGRESS_TYPE_DOWNLOAD, progressData);
- downloader.setProgressListener(progressListener, event);
+ if (progressListener != null) { // interactive session, show progress
+ Bundle data = new Bundle(1);
+ data.putString(PROGRESS_DATA_REPO_ADDRESS, repo.address);
+ downloader.setProgressListener(progressListener, data);
}
- int status = downloader.download();
+ downloader.downloadUninterrupted();
- if (status == 304) {
+ if (downloader.isCached()) {
// The index is unchanged since we last read it. We just mark
// everything that came from this repo as being updated.
- Log.d("FDroid", "Repo index for " + repo.address
+ Log.d("FDroid", "Repo index for " + getIndexAddress()
+ " is up to date (by etag)");
- } else if (status == 200) {
- // Nothing needed to be done here...
- } else {
- // Is there any code other than 200 which still returns
- // content? Just in case, lets try to clean up.
- if (downloader.getFile() != null) {
- downloader.getFile().delete();
- }
- throw new UpdateException(
- repo,
- "Failed to update repo " + repo.address +
- " - HTTP response " + status);
}
- } catch (SSLHandshakeException e) {
- throw new UpdateException(
- repo,
- "A problem occurred while establishing an SSL " +
- "connection. If this problem persists, AND you have a " +
- "very old device, you could try using http instead of " +
- "https for the repo URL.",
- e );
+
} catch (IOException e) {
if (downloader != null && downloader.getFile() != null) {
downloader.getFile().delete();
@@ -138,12 +119,6 @@ abstract public class RepoUpdater {
return downloader;
}
- public static Bundle createProgressData(String repoAddress) {
- Bundle data = new Bundle();
- data.putString(PROGRESS_DATA_REPO, repoAddress);
- return data;
- }
-
private int estimateAppCount(File indexFile) {
int count = -1;
try {
@@ -182,7 +157,7 @@ abstract public class RepoUpdater {
XMLReader reader = parser.getXMLReader();
RepoXMLHandler handler = new RepoXMLHandler(repo, progressListener);
- if (isInteractive()) {
+ if (progressListener != null) {
// Only bother spending the time to count the expected apps
// if we can show that to the user...
handler.setTotalAppCount(estimateAppCount(indexFile));
@@ -195,7 +170,7 @@ abstract public class RepoUpdater {
reader.parse(is);
apps = handler.getApps();
apks = handler.getApks();
- updateRepo(handler, downloader.getETag());
+ updateRepo(handler, downloader.getCacheTag());
}
} catch (SAXException e) {
throw new UpdateException(
diff --git a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java
index 0a3ff5d2f..471a6a654 100644
--- a/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java
+++ b/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java
@@ -203,7 +203,7 @@ public class RepoDetailsFragment extends Fragment {
UpdateService.updateRepoNow(repo.address, getActivity()).setListener(new ProgressListener() {
@Override
public void onProgress(Event event) {
- if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
+ if (event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) {
repo = loadRepoDetails();
updateView((ViewGroup)getView());
}
diff --git a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java
index afd69cddf..5295af7de 100644
--- a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java
+++ b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java
@@ -218,8 +218,8 @@ public class RepoListFragment extends ListFragment
UpdateService.updateNow(getActivity()).setListener(new ProgressListener() {
@Override
public void onProgress(Event event) {
- if (event.type == UpdateService.STATUS_COMPLETE_AND_SAME ||
- event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
+ if (event.type.equals(UpdateService.EVENT_COMPLETE_AND_SAME) ||
+ event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) {
// No need to prompt to update any more, we just did it!
changed = false;
}