From 68c6648da51e9e1288f2d20903ad83c3e7bd0772 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Mon, 25 May 2015 08:17:09 +1000 Subject: [PATCH] WIP: Store selected apps in SwapState. Handle back presses for swap. Currently the view to show after pressing the back button is defined by individual InnerView classes. For example, the JoinWifiView always says its previous step is the SelectAppsView. This works for the most part, but doesn't work for: * The first StartSwapView class (instead handled by a special case in the Activity). * WifiQrView which could either be going back to the NfcView or the JoinWifiView, depending on a few cases, which the WifiQrView probably shouldn't care about. --- .../fdroid/fdroid/localrepo/SwapState.java | 101 +++++++++++++++--- .../views/swap/SwapWorkflowActivity.java | 30 ++++-- .../fdroid/views/swap/views/JoinWifiView.java | 10 +- .../fdroid/views/swap/views/NfcView.java | 6 +- .../views/swap/views/SelectAppsView.java | 44 +++----- .../views/swap/views/StartSwapView.java | 15 +++ .../fdroid/views/swap/views/WifiQrView.java | 13 ++- 7 files changed, 168 insertions(+), 51 deletions(-) diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapState.java b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapState.java index 1783eb000..802de5cd9 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapState.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapState.java @@ -7,6 +7,9 @@ import android.support.annotation.NonNull; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; public class SwapState { @@ -18,13 +21,19 @@ public class SwapState { public static final int STEP_SHOW_NFC = 4; public static final int STEP_WIFI_QR = 5; - private int step; @NonNull private final Context context; - private SwapState(@NonNull Context context) { + @NonNull + private Set appsToSwap; + + private int step; + + private SwapState(@NonNull Context context, @SwapStep int step, @NonNull Set appsToSwap) { this.context = context; + this.step = step; + this.appsToSwap = appsToSwap; } /** @@ -38,27 +47,93 @@ public class SwapState { public SwapState setStep(@SwapStep int step) { this.step = step; - persist(); + persistStep(); return this; } - private static final String KEY_STEP = "step"; + public Set getAppsToSwap() { + return appsToSwap; + } + + private static final String KEY_STEP = "step"; + private static final String KEY_APPS_TO_SWAP = "appsToSwap"; + + private static SwapState instance; @NonNull public static SwapState load(@NonNull Context context) { - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); + if (instance == null) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); - @SwapStep int step = preferences.getInt(KEY_STEP, STEP_INTRO); + @SwapStep int step = preferences.getInt(KEY_STEP, STEP_INTRO); + Set appsToSwap = deserializePackages(preferences.getString(KEY_APPS_TO_SWAP, "")); - return new SwapState(context) - .setStep(step); + instance = new SwapState(context, step, appsToSwap); + } + + return instance; } - private void persist() { - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_APPEND); - preferences.edit() - .putInt(KEY_STEP, step) - .commit(); + private SharedPreferences persistence() { + return context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_APPEND); + } + + private void persistStep() { + persistence().edit().putInt(KEY_STEP, step).commit(); + } + + private void persistAppsToSwap() { + persistence().edit().putString(KEY_APPS_TO_SWAP, serializePackages(appsToSwap)).commit(); + } + + /** + * Replacement for {@link android.content.SharedPreferences.Editor#putStringSet(String, Set)} + * which is only available in API >= 11. + * Package names are reverse-DNS-style, so they should only have alpha numeric values. Thus, + * this uses a comma as the separator. + * @see SwapState#deserializePackages(String) + */ + private static String serializePackages(Set packages) { + StringBuilder sb = new StringBuilder(); + for (String pkg : packages) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(pkg); + } + return sb.toString(); + } + + /** + * @see SwapState#deserializePackages(String) + */ + private static Set deserializePackages(String packages) { + Set set = new HashSet<>(); + Collections.addAll(set, packages.split(",")); + return set; + } + + public void ensureFDroidSelected() { + String fdroid = context.getPackageName(); + if (!hasSelectedPackage(fdroid)) { + selectPackage(fdroid); + } + } + + public boolean hasSelectedPackage(String packageName) { + return appsToSwap.contains(packageName); + } + + public void selectPackage(String packageName) { + appsToSwap.add(packageName); + persistAppsToSwap(); + } + + public void deselectPackage(String packageName) { + if (appsToSwap.contains(packageName)) { + appsToSwap.remove(packageName); + } + persistAppsToSwap(); } /** 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 72e8e4b37..1dab7b974 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -45,8 +45,7 @@ public class SwapWorkflowActivity extends FragmentActivity { /** @return The step that this view represents. */ @SwapState.SwapStep int getStep(); - // TODO: Handle back presses with a method like this: - // @SwapState.SwapStep int getPreviousStep(); + @SwapState.SwapStep int getPreviousStep(); } private static final int CONNECT_TO_SWAP = 1; @@ -57,6 +56,17 @@ public class SwapWorkflowActivity extends FragmentActivity { private UpdateAsyncTask updateSwappableAppsTask = null; private Timer shutdownLocalRepoTimer; + @Override + public void onBackPressed() { + if (currentView.getStep() == SwapState.STEP_INTRO) { + finish(); + } else { + int nextStep = currentView.getPreviousStep(); + state.setStep(nextStep); + showRelevantView(); + } + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -105,6 +115,10 @@ public class SwapWorkflowActivity extends FragmentActivity { } } + public SwapState getState() { + return state; + } + private void showNfc() { if (!attemptToShowNfc()) { showWifiQr(); @@ -128,10 +142,12 @@ public class SwapWorkflowActivity extends FragmentActivity { inflateInnerView(R.layout.swap_select_apps); } - // TODO: Pass in the selected apps, then we can figure out whether they have changed. - public void onAppsSelected(Set selectedApps) { + // TODO: Figure out whether they have changed since last time UpdateAsyncTask was run. + // If the local repo is running, then we can ask it what apps it is swapping and compare with that. + // Otherwise, probably will need to scan the file system. + public void onAppsSelected() { if (updateSwappableAppsTask == null && !hasPreparedLocalRepo) { - updateSwappableAppsTask = new UpdateAsyncTask(this, selectedApps); + updateSwappableAppsTask = new UpdateAsyncTask(state.getAppsToSwap()); updateSwappableAppsTask.execute(); } else { showJoinWifi(); @@ -247,10 +263,10 @@ public class SwapWorkflowActivity extends FragmentActivity { @NonNull private final Context context; - public UpdateAsyncTask(Context c, @NonNull Set apps) { + public UpdateAsyncTask(@NonNull Set apps) { context = SwapWorkflowActivity.this.getApplicationContext(); selectedApps = apps; - progressDialog = new ProgressDialog(c); + progressDialog = new ProgressDialog(context); progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); progressDialog.setTitle(R.string.updating); sharingUri = Utils.getSharingUri(FDroidApp.repo); diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/views/JoinWifiView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/views/JoinWifiView.java index e77d0b9a6..44a16c4e4 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/views/JoinWifiView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/views/JoinWifiView.java @@ -46,7 +46,6 @@ public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity } private SwapWorkflowActivity getActivity() { - // TODO: Try and find a better way to get to the SwapActivity, which makes less asumptions. return (SwapWorkflowActivity)getContext(); } @@ -61,7 +60,8 @@ public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity }); refreshWifiState(); - // TODO: Listen for "Connecting..." state and reflect that in the view too. + // TODO: This is effectively swap state management code, shouldn't be isolated to the + // WifiStateChangeService, but should be bundled with the main swap state handling code. LocalBroadcastManager.getInstance(getActivity()).registerReceiver( new BroadcastReceiver() { @Override @@ -74,6 +74,7 @@ public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity } + // TODO: Listen for "Connecting..." state and reflect that in the view too. private void refreshWifiState() { TextView descriptionView = (TextView) findViewById(R.id.text_description); ImageView wifiIcon = (ImageView) findViewById(R.id.wifi_icon); @@ -123,4 +124,9 @@ public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity public int getStep() { return SwapState.STEP_JOIN_WIFI; } + + @Override + public int getPreviousStep() { + return SwapState.STEP_SELECT_APPS; + } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/views/NfcView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/views/NfcView.java index b3a566203..482e23f8d 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/views/NfcView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/views/NfcView.java @@ -38,7 +38,6 @@ public class NfcView extends RelativeLayout implements SwapWorkflowActivity.Inne } private SwapWorkflowActivity getActivity() { - // TODO: Try and find a better way to get to the SwapActivity, which makes less asumptions. return (SwapWorkflowActivity)getContext(); } @@ -73,4 +72,9 @@ public class NfcView extends RelativeLayout implements SwapWorkflowActivity.Inne public int getStep() { return SwapState.STEP_SHOW_NFC; } + + @Override + public int getPreviousStep() { + return SwapState.STEP_JOIN_WIFI; + } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java index 2917e8c02..b07347fc7 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java @@ -32,15 +32,11 @@ import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; -import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.InstalledAppProvider; -import org.fdroid.fdroid.localrepo.LocalRepoManager; import org.fdroid.fdroid.localrepo.SwapState; import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; -import java.util.HashSet; - public class SelectAppsView extends ListView implements SwapWorkflowActivity.InnerView, LoaderManager.LoaderCallbacks, @@ -67,6 +63,10 @@ public class SelectAppsView extends ListView implements return (SwapWorkflowActivity)getContext(); } + private SwapState getState() { + return getActivity().getState(); + } + private static final int LOADER_INSTALLED_APPS = 253341534; private AppListAdapter adapter; @@ -95,17 +95,6 @@ public class SelectAppsView extends ListView implements // either reconnect with an existing loader or start a new one getActivity().getSupportLoaderManager().initLoader(LOADER_INSTALLED_APPS, null, this); - // build list of existing apps from what is on the file system - if (FDroidApp.selectedApps == null) { - FDroidApp.selectedApps = new HashSet<>(); - for (String filename : LocalRepoManager.get(getActivity()).repoDir.list()) { - if (filename.matches(".*\\.apk")) { - String packageName = filename.substring(0, filename.indexOf("_")); - FDroidApp.selectedApps.add(packageName); - } - } - } - setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView parent, View v, int position, long id) { if (position > 0) { @@ -128,7 +117,7 @@ public class SelectAppsView extends ListView implements nextMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { - getActivity().onAppsSelected(FDroidApp.selectedApps); + getActivity().onAppsSelected(); return true; } }); @@ -148,13 +137,18 @@ public class SelectAppsView extends ListView implements return SwapState.STEP_SELECT_APPS; } + @Override + public int getPreviousStep() { + return SwapState.STEP_INTRO; + } + private void toggleAppSelected(int position) { Cursor c = (Cursor) adapter.getItem(position - 1); String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); - if (FDroidApp.selectedApps.contains(packageName)) { - FDroidApp.selectedApps.remove(packageName); + if (getState().hasSelectedPackage(packageName)) { + getState().deselectPackage(packageName); } else { - FDroidApp.selectedApps.add(packageName); + getState().selectPackage(packageName); } } @@ -179,17 +173,13 @@ public class SelectAppsView extends ListView implements public void onLoadFinished(Loader loader, Cursor cursor) { adapter.swapCursor(cursor); - String fdroid = loader.getContext().getPackageName(); for (int i = 0; i < getCount() - 1; i++) { Cursor c = ((Cursor) getItemAtPosition(i + 1)); String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)); - if (TextUtils.equals(packageName, fdroid)) { - setItemChecked(i + 1, true); // always include FDroid - } else { - for (String selected : FDroidApp.selectedApps) { - if (TextUtils.equals(packageName, selected)) { - setItemChecked(i + 1, true); - } + getState().ensureFDroidSelected(); + for (String selected : getState().getAppsToSwap()) { + if (TextUtils.equals(packageName, selected)) { + setItemChecked(i + 1, true); } } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java index a681f3de7..c251dd0bf 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java @@ -17,6 +17,12 @@ import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.InnerView { + // TODO: Is there a way to guarangee which of these constructors the inflater will call? + // Especially on different API levels? It would be nice to only have the one which accepts + // a Context, but I'm not sure if that is correct or not. As it stands, this class provides + // constructurs which match each of the ones available in the parent class. + // The same is true for the other views in the swap process too. + public StartSwapView(Context context) { super(context); } @@ -62,4 +68,13 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity. public int getStep() { return SwapState.STEP_INTRO; } + + @Override + public int getPreviousStep() { + // TODO: Currently this is handleed by the SwapWorkflowActivity as a special case, where + // if getStep is STEP_INTRO, don't even bother asking for getPreviousStep. But that is a + // bit messy. It would be nicer if this was handled using the same mechanism as everything + // else. + return SwapState.STEP_INTRO; + } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/views/WifiQrView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/views/WifiQrView.java index 47120a492..fc451de9b 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/views/WifiQrView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/views/WifiQrView.java @@ -60,7 +60,6 @@ public class WifiQrView extends ScrollView implements SwapWorkflowActivity.Inner } private SwapWorkflowActivity getActivity() { - // TODO: Try and find a better way to get to the SwapActivity, which makes less asumptions. return (SwapWorkflowActivity)getContext(); } @@ -78,6 +77,9 @@ public class WifiQrView extends ScrollView implements SwapWorkflowActivity.Inner openQr.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { + // TODO: Should probably ask the activity or some other class to do this for us. + // The view should be dumb and only know how to show things and delegate things to + // other classes that know how to do things. IntentIntegrator integrator = new IntentIntegrator(getActivity()); integrator.initiateScan(); } @@ -91,6 +93,9 @@ public class WifiQrView extends ScrollView implements SwapWorkflowActivity.Inner } }); + // TODO: As with the JoinWifiView, this should be refactored to be part of the SwapState. + // Otherwise, we are left with SwapState, LocalRepoService, WifiStateChangeService, and + // some static variables in FDroidApp all which manage the state for swap. LocalBroadcastManager.getInstance(getActivity()).registerReceiver( new BroadcastReceiver() { @Override @@ -112,6 +117,12 @@ public class WifiQrView extends ScrollView implements SwapWorkflowActivity.Inner return SwapState.STEP_WIFI_QR; } + @Override + public int getPreviousStep() { + // TODO: Find a way to make this optionally go back to the NFC screen if appropriate. + return SwapState.STEP_JOIN_WIFI; + } + private void setUIFromWifi() { if (TextUtils.isEmpty(FDroidApp.repo.address))