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<String> appsToSwap; + + private int step; + + private SwapState(@NonNull Context context, @SwapStep int step, @NonNull Set<String> 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<String> 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<String> 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<String> 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<String> deserializePackages(String packages) { + Set<String> 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<String> 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<String> apps) { + public UpdateAsyncTask(@NonNull Set<String> 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<Cursor>, @@ -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<Cursor> 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))