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.
This commit is contained in:
Peter Serwylo 2015-05-25 08:17:09 +10:00
parent 38059ec324
commit 68c6648da5
7 changed files with 168 additions and 51 deletions

View File

@ -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();
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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))