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); } }