diff --git a/F-Droid/res/layout/swap_app_list_item.xml b/F-Droid/res/layout/swap_app_list_item.xml
index 2478bef43..3abfdd9ef 100644
--- a/F-Droid/res/layout/swap_app_list_item.xml
+++ b/F-Droid/res/layout/swap_app_list_item.xml
@@ -73,4 +73,22 @@
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="F-Droid" />
+
+
diff --git a/F-Droid/src/org/fdroid/fdroid/Utils.java b/F-Droid/src/org/fdroid/fdroid/Utils.java
index a555f3a04..30191481a 100644
--- a/F-Droid/src/org/fdroid/fdroid/Utils.java
+++ b/F-Droid/src/org/fdroid/fdroid/Utils.java
@@ -38,6 +38,7 @@ import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import com.nostra13.universalimageloader.utils.StorageUtils;
import org.fdroid.fdroid.compat.FileCompat;
+import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.SanitizedFile;
import org.xml.sax.XMLReader;
@@ -403,6 +404,14 @@ public final class Utils {
return new Locale(languageTag);
}
+ public static String getApkUrl(Apk apk) {
+ return getApkUrl(apk.repoAddress, apk);
+ }
+
+ public static String getApkUrl(String repoAddress, Apk apk) {
+ return repoAddress + "/" + apk.apkName.replace(" ", "%20");
+ }
+
public static class CommaSeparatedList implements Iterable {
private final String value;
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java
index ec597f0cb..c413abda1 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java
@@ -64,12 +64,13 @@ import java.util.TimerTask;
* TODO: Show "Waiting for other device to finish setting up swap" when only F-Droid shown in swap
* TODO: Handle not connected to wifi more gracefully. For example, Bonjour discovery falls over.
* TODO: Remove peers from list of peers when no longer "visible".
- * TODO: Show feedback for "Setting up (wifi|bluetooth)" in start swap view.
+ * TODO: Feedback for "Setting up (wifi|bluetooth)" in start swap view is not as immediate as I had hoped.
* TODO: Turn off bluetooth after cancelling/timing out if we turned it on.
* TODO: Disable the Scan QR button unless visible via something. Could equally show relevant feedback.
*
* TODO: Starting wifi after cancelling swap and beginning again doesn't work properly
- * TODO: Scan QR hangs when updating repoo
+ * TODO: Scan QR hangs when updating repoo. Swapper was 2.3.3 and Swappee was 5.0
+ * TODO: Search in "touch to install apps" screen is busted, causes crash.
*
*/
public class SwapService extends Service {
diff --git a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java
index 98e76f0ba..fd4bbbac7 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java
@@ -21,8 +21,11 @@
package org.fdroid.fdroid.net;
import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
+import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.fdroid.fdroid.Hasher;
@@ -50,6 +53,10 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
public static final String EVENT_APK_DOWNLOAD_CANCELLED = "apkDownloadCancelled";
public static final String EVENT_ERROR = "apkDownloadError";
+ public static final String ACTION_STATUS = "apkDownloadStatus";
+ public static final String EXTRA_TYPE = "apkDownloadStatusType";
+ public static final String EXTRA_URL = "apkDownloadUrl";
+
public static final int ERROR_HASH_MISMATCH = 101;
public static final int ERROR_DOWNLOAD_FAILED = 102;
@@ -105,10 +112,6 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
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(File apkFile) {
Hasher hasher;
try {
@@ -186,7 +189,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
return false;
}
- String remoteAddress = getRemoteAddress();
+ String remoteAddress = Utils.getApkUrl(repoAddress, curApk);
Log.d(TAG, "Downloading apk from " + remoteAddress + " to " + localFile);
try {
@@ -212,6 +215,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
sendProgressEvent(new Event(EVENT_ERROR, data));
}
+ // TODO: Completely remove progress listener, only use broadcasts...
private void sendProgressEvent(Event event) {
event.getData().putLong(EVENT_SOURCE_ID, id);
@@ -219,6 +223,12 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
if (listener != null) {
listener.onProgress(event);
}
+
+ Intent intent = new Intent(ACTION_STATUS);
+ intent.putExtras(event.getData());
+ intent.putExtra(EXTRA_TYPE, event.type);
+ intent.putExtra(EXTRA_URL, Utils.getApkUrl(repoAddress, curApk));
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
@Override
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java
index 56302a00e..9564318ae 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java
@@ -4,11 +4,14 @@ import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.os.Looper;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
@@ -33,6 +36,7 @@ import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
+import android.widget.ProgressBar;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
@@ -41,10 +45,14 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
+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.localrepo.SwapService;
+import org.fdroid.fdroid.net.ApkDownloader;
+import org.fdroid.fdroid.net.Downloader;
import java.util.Timer;
import java.util.TimerTask;
@@ -254,6 +262,140 @@ public class SwapAppsView extends ListView implements
@SuppressWarnings("UnusedDeclaration")
private static final String TAG = "AppListAdapter";
+ private class ViewHolder {
+
+ private App app;
+
+ @Nullable
+ private Apk apkToInstall;
+
+ ProgressBar progressView;
+ TextView nameView;
+ ImageView iconView;
+ Button btnInstall;
+ TextView btnAttemptInstall;
+ TextView statusInstalled;
+ TextView statusIncompatible;
+
+ private BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Apk apk = getApkToInstall();
+
+ // Note: This can also be done by using the build in IntentFilter.matchData()
+ // functionality, matching against the Intent.getData() of the incoming intent.
+ // I've chosen to do this way, because otherwise we need to query the database
+ // once for each ViewHolder in order to get the repository address for the
+ // apkToInstall. This way, we can wait until we receive an incoming intent (if
+ // at all) and then lazily load the apk to install.
+ String broadcastUrl = intent.getStringExtra(ApkDownloader.EXTRA_URL);
+ if (!TextUtils.equals(Utils.getApkUrl(apk.repoAddress, apk), broadcastUrl)) {
+ return;
+ }
+
+ switch(intent.getStringExtra(ApkDownloader.EXTRA_TYPE)) {
+ // Fallthrough for each of these "downloader no longer going" events...
+ case ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE:
+ case ApkDownloader.EVENT_APK_DOWNLOAD_CANCELLED:
+ case ApkDownloader.EVENT_ERROR:
+ case ApkDownloader.EVENT_DATA_ERROR_TYPE:
+ resetView();
+ break;
+ }
+ }
+ };
+
+ private ContentObserver appObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ app = AppProvider.Helper.findById(getActivity().getContentResolver(), app.id);
+ apkToInstall = null; // Force lazy loading to fetch correct apk next time.
+ resetView();
+ }
+ };
+
+ public ViewHolder() {
+ // TODO: Unregister receiver correctly...
+ IntentFilter filter = new IntentFilter(ApkDownloader.ACTION_STATUS);
+ LocalBroadcastManager.getInstance(getActivity()).registerReceiver(downloadReceiver, filter);
+ }
+
+ public void setApp(@NonNull App app) {
+ if (this.app == null || !this.app.id.equals(app.id)) {
+ this.app = app;
+ apkToInstall = null; // Force lazy loading to fetch the correct apk next time.
+
+ // NOTE: Instead of continually unregistering and reregistering the observer
+ // (with a different URI), this could equally be done by only having one
+ // registration in the constructor, and using the ContentObserver.onChange(boolean, URI)
+ // method and inspecting the URI to see if it maches. However, this was only
+ // implemented on API-16, so leaving like this for now.
+ getActivity().getContentResolver().unregisterContentObserver(appObserver);
+ getActivity().getContentResolver().registerContentObserver(
+ AppProvider.getContentUri(this.app.id), true, appObserver);
+ }
+ resetView();
+ }
+
+ /**
+ * Lazily load the apk from the database the first time it is requested. Means it wont
+ * be loaded unless we receive a download event from the {@link ApkDownloader}.
+ */
+ private Apk getApkToInstall() {
+ if (apkToInstall == null) {
+ apkToInstall = ApkProvider.Helper.find(getActivity(), app.id, app.suggestedVercode);
+ }
+ return apkToInstall;
+ }
+
+ private void resetView() {
+
+ progressView.setVisibility(View.GONE);
+ nameView.setText(app.name);
+ ImageLoader.getInstance().displayImage(app.iconUrl, iconView, displayImageOptions);
+
+ btnInstall.setVisibility(View.GONE);
+ btnAttemptInstall.setVisibility(View.GONE);
+ statusInstalled.setVisibility(View.GONE);
+ statusIncompatible.setVisibility(View.GONE);
+
+ if (app.hasUpdates()) {
+ btnInstall.setText(R.string.menu_upgrade);
+ btnInstall.setVisibility(View.VISIBLE);
+ } else if (app.isInstalled()) {
+ statusInstalled.setVisibility(View.VISIBLE);
+ } else if (!app.compatible) {
+ btnAttemptInstall.setVisibility(View.VISIBLE);
+ statusIncompatible.setVisibility(View.VISIBLE);
+ } else {
+ btnInstall.setText(R.string.menu_install);
+ btnInstall.setVisibility(View.VISIBLE);
+ }
+
+ OnClickListener installListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (app.hasUpdates() || app.compatible) {
+ getActivity().install(app);
+ showProgress();
+ }
+ }
+ };
+
+ btnInstall.setOnClickListener(installListener);
+ btnAttemptInstall.setOnClickListener(installListener);
+
+ }
+
+ private void showProgress() {
+ progressView.setVisibility(View.VISIBLE);
+ btnInstall.setVisibility(View.GONE);
+ btnAttemptInstall.setVisibility(View.GONE);
+ statusInstalled.setVisibility(View.GONE);
+ statusIncompatible.setVisibility(View.GONE);
+ }
+ }
+
@Nullable
private LayoutInflater inflater;
@@ -282,55 +424,27 @@ public class SwapAppsView extends ListView implements
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = getInflater(context).inflate(R.layout.swap_app_list_item, parent, false);
+
+ ViewHolder holder = new ViewHolder();
+
+ holder.progressView = (ProgressBar)view.findViewById(R.id.progress);
+ holder.nameView = (TextView)view.findViewById(R.id.name);
+ holder.iconView = (ImageView)view.findViewById(android.R.id.icon);
+ holder.btnInstall = (Button)view.findViewById(R.id.btn_install);
+ holder.btnAttemptInstall = (TextView)view.findViewById(R.id.btn_attempt_install);
+ holder.statusInstalled = (TextView)view.findViewById(R.id.status_installed);
+ holder.statusIncompatible = (TextView)view.findViewById(R.id.status_incompatible);
+
+ view.setTag(holder);
bindView(view, context, cursor);
return view;
}
@Override
public void bindView(final View view, final Context context, final Cursor cursor) {
-
- TextView nameView = (TextView)view.findViewById(R.id.name);
- ImageView iconView = (ImageView)view.findViewById(android.R.id.icon);
- Button btnInstall = (Button)view.findViewById(R.id.btn_install);
- TextView btnAttemptInstall = (TextView)view.findViewById(R.id.btn_attempt_install);
- TextView statusInstalled = (TextView)view.findViewById(R.id.status_installed);
- TextView statusIncompatible = (TextView)view.findViewById(R.id.status_incompatible);
-
+ ViewHolder holder = (ViewHolder)view.getTag();
final App app = new App(cursor);
-
- nameView.setText(app.name);
- ImageLoader.getInstance().displayImage(app.iconUrl, iconView, displayImageOptions);
-
- btnInstall.setVisibility(View.GONE);
- btnAttemptInstall.setVisibility(View.GONE);
- statusInstalled.setVisibility(View.GONE);
- statusIncompatible.setVisibility(View.GONE);
-
- if (app.hasUpdates()) {
- btnInstall.setText(R.string.menu_upgrade);
- btnInstall.setVisibility(View.VISIBLE);
- } else if (app.isInstalled()) {
- statusInstalled.setVisibility(View.VISIBLE);
- } else if (!app.compatible) {
- btnAttemptInstall.setVisibility(View.VISIBLE);
- statusIncompatible.setVisibility(View.VISIBLE);
- } else {
- btnInstall.setText(R.string.menu_install);
- btnInstall.setVisibility(View.VISIBLE);
- }
-
- OnClickListener installListener = new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (app.hasUpdates() || app.compatible) {
- getActivity().install(app);
- }
- }
- };
-
- btnInstall.setOnClickListener(installListener);
- btnAttemptInstall.setOnClickListener(installListener);
-
+ holder.setApp(app);
}
}
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
index b0bc13448..5c939423e 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
@@ -75,6 +75,12 @@ public class SwapWorkflowActivity extends AppCompatActivity {
public static final String EXTRA_CONFIRM = "EXTRA_CONFIRM";
public static final String EXTRA_REPO_ID = "repoId";
+ /**
+ * Ensure that we don't try to handle specific intents more than once in onResume()
+ * (e.g. the "Do you want to swap back with ..." intent).
+ */
+ public static final String EXTRA_HANDLED = "handled";
+
private ViewGroup container;
/**
@@ -200,9 +206,10 @@ public class SwapWorkflowActivity extends AppCompatActivity {
private void checkIncomingIntent() {
Intent intent = getIntent();
- if (intent.getBooleanExtra(EXTRA_CONFIRM, false)) {
+ if (intent.getBooleanExtra(EXTRA_CONFIRM, false) && !intent.getBooleanExtra(EXTRA_HANDLED, false)) {
// Storing config in this variable will ensure that when showRelevantView() is next
// run, it will show the connect swap view (if the service is available).
+ intent.putExtra(EXTRA_HANDLED, true);
confirmSwapConfig = new NewRepoConfig(this, intent);
}
}