diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java
index 63e873df6..329f25e7e 100644
--- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java
+++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java
@@ -16,13 +16,6 @@ import android.os.IBinder;
import android.text.TextUtils;
import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.ServiceCompat;
-import androidx.core.content.ContextCompat;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.NotificationHelper;
import org.fdroid.fdroid.Preferences;
@@ -45,6 +38,12 @@ import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.ServiceCompat;
+import androidx.core.content.ContextCompat;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import cc.mvdan.accesspoint.WifiApControl;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Completable;
@@ -52,8 +51,9 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
/**
- * Central service which manages all of the different moving parts of swap which are required
- * to enable p2p swapping of apps.
+ * Central service which manages all of the different moving parts of swap
+ * which are required to enable p2p swapping of apps. This is the background
+ * operations for {@link SwapWorkflowActivity}.
*/
public class SwapService extends Service {
private static final String TAG = "SwapService";
diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java
index aedb2545f..b0e3c399c 100644
--- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java
+++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java
@@ -63,6 +63,7 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.Stack;
import java.util.Timer;
import java.util.TimerTask;
@@ -81,15 +82,27 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import static org.fdroid.fdroid.views.main.MainActivity.ACTION_REQUEST_SWAP;
/**
- * This activity will do its best to show the most relevant screen about swapping to the user.
- * The problem comes when there are two competing goals - 1) Show the user a list of apps from another
- * device to download and install, and 2) Prepare your own list of apps to share.
+ * This is the core of the UI for the whole nearby swap experience. Each
+ * screen is implemented as a {@link View} with the related logic in this
+ * {@link android.app.Activity}. Long lived pieces work in {@link SwapService}.
+ * All these pieces of the UX are tracked here:
+ *
+ * - which WiFi network to use
+ * - whether to advertise via Bluetooth or WiFi+Bonjour
+ * - connect to another device's swap
+ * - choose which apps to share
+ * - ask if the other device would like to swap with us
+ * - help connect via QR Code or NFC
+ *
+ *
* There are lots of async events in this system, and the user can also change
* the views while things are working. The {@link ViewGroup}
* {@link SwapWorkflowActivity#container} can have all its widgets removed and
* replaced by a new view at any point. Therefore, any widget config that is
* based on fetching it from {@code container} must check that the result is
* not null before trying to config it.
+ *
+ * @see
*/
@SuppressWarnings("LineLength")
public class SwapWorkflowActivity extends AppCompatActivity {
@@ -110,7 +123,6 @@ public class SwapWorkflowActivity extends AppCompatActivity {
private static final int REQUEST_BLUETOOTH_DISCOVERABLE = 3;
private static final int REQUEST_BLUETOOTH_ENABLE_FOR_SEND = 4;
private static final int REQUEST_WRITE_SETTINGS_PERMISSION = 5;
- private static final int STEP_INTRO = 1; // TODO remove this special case, only use layoutResIds
private MaterialToolbar toolbar;
private SwapView currentView;
@@ -123,7 +135,8 @@ public class SwapWorkflowActivity extends AppCompatActivity {
private BluetoothAdapter bluetoothAdapter;
@LayoutRes
- private int currentSwapViewLayoutRes = STEP_INTRO;
+ private int currentSwapViewLayoutRes = R.layout.swap_start_swap;
+ private final Stack backstack = new Stack<>();
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
@@ -161,51 +174,78 @@ public class SwapWorkflowActivity extends AppCompatActivity {
return service;
}
+ /**
+ * Handle the back logic for the system back button.
+ *
+ * @see #inflateSwapView(int, boolean)
+ */
@Override
public void onBackPressed() {
- if (currentView.getLayoutResId() == STEP_INTRO) {
- SwapService.stop(this);
- finish();
+ if (backstack.isEmpty()) {
+ super.onBackPressed();
} else {
- // TODO: Currently StartSwapView is handleed by the SwapWorkflowActivity as a special case, where
- // if getLayoutResId 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.
- int nextStep = -1;
- switch (currentView.getLayoutResId()) {
- case R.layout.swap_confirm_receive:
- nextStep = STEP_INTRO;
- break;
- case R.layout.swap_connecting:
- nextStep = R.layout.swap_select_apps;
- break;
- case R.layout.swap_join_wifi:
- nextStep = STEP_INTRO;
- break;
- case R.layout.swap_nfc:
- nextStep = R.layout.swap_join_wifi;
- break;
- case R.layout.swap_select_apps:
- nextStep = getSwapService().isConnectingWithPeer() ? STEP_INTRO : R.layout.swap_join_wifi;
- break;
- case R.layout.swap_send_fdroid:
- nextStep = STEP_INTRO;
- break;
- case R.layout.swap_start_swap:
- nextStep = STEP_INTRO;
- break;
- case R.layout.swap_success:
- nextStep = STEP_INTRO;
- break;
- case R.layout.swap_wifi_qr:
- nextStep = R.layout.swap_join_wifi;
- break;
- }
- currentSwapViewLayoutRes = nextStep;
- showRelevantView();
+ int resId = backstack.pop();
+ inflateSwapView(resId, true);
}
}
+ /**
+ * Handle the back logic for the upper left back button in the toolbar.
+ * This has a simpler, hard-coded back logic than the system back button.
+ *
+ * @see #onBackPressed()
+ */
+ public void onToolbarBackPressed() {
+ int nextStep = R.layout.swap_start_swap;
+ switch (currentView.getLayoutResId()) {
+ case R.layout.swap_confirm_receive:
+ nextStep = backstack.peek();
+ break;
+ case R.layout.swap_connecting:
+ nextStep = R.layout.swap_select_apps;
+ break;
+ case R.layout.swap_join_wifi:
+ nextStep = R.layout.swap_start_swap;
+ break;
+ case R.layout.swap_nfc:
+ nextStep = R.layout.swap_join_wifi;
+ break;
+ case R.layout.swap_select_apps:
+ if (!backstack.isEmpty() && backstack.peek() == R.layout.swap_start_swap) {
+ nextStep = R.layout.swap_start_swap;
+ } else if (getSwapService() != null && getSwapService().isConnectingWithPeer()) {
+ nextStep = R.layout.swap_success;
+ } else {
+ nextStep = R.layout.swap_join_wifi;
+ }
+ break;
+ case R.layout.swap_send_fdroid:
+ nextStep = R.layout.swap_start_swap;
+ break;
+ case R.layout.swap_start_swap:
+ if (getSwapService() != null && getSwapService().isConnectingWithPeer()) {
+ nextStep = R.layout.swap_success;
+ } else {
+ SwapService.stop(this);
+ finish();
+ return;
+ }
+ break;
+ case R.layout.swap_success:
+ nextStep = R.layout.swap_start_swap;
+ break;
+ case R.layout.swap_wifi_qr:
+ if (!backstack.isEmpty() && backstack.peek() == R.layout.swap_start_swap) {
+ nextStep = R.layout.swap_start_swap;
+ } else {
+ nextStep = R.layout.swap_join_wifi;
+ }
+ break;
+ }
+ currentSwapViewLayoutRes = nextStep;
+ inflateSwapView(currentSwapViewLayoutRes);
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
FDroidApp fdroidApp = (FDroidApp) getApplication();
@@ -230,6 +270,8 @@ public class SwapWorkflowActivity extends AppCompatActivity {
container = (ViewGroup) findViewById(R.id.container);
+ backstack.clear();
+
localBroadcastManager = LocalBroadcastManager.getInstance(this);
localBroadcastManager.registerReceiver(downloaderInterruptedReceiver,
new IntentFilter(Downloader.ACTION_INTERRUPTED));
@@ -319,8 +361,15 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
private void setUpSearchView(Menu menu) {
- SearchView searchView = new SearchView(this);
+ MenuItem appsMenuItem = menu.findItem(R.id.action_apps);
+ if (appsMenuItem != null) {
+ appsMenuItem.setOnMenuItemClickListener(item -> {
+ inflateSwapView(R.layout.swap_select_apps);
+ return true;
+ });
+ }
+ SearchView searchView = new SearchView(this);
MenuItem searchMenuItem = menu.findItem(R.id.action_search);
searchMenuItem.setActionView(searchView);
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
@@ -478,6 +527,9 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
}
+ /**
+ * Handle events that trigger different swap views to be shown.
+ */
private void showRelevantView() {
if (confirmSwapConfig != null) {
@@ -488,7 +540,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
switch (currentSwapViewLayoutRes) {
- case STEP_INTRO:
+ case R.layout.swap_start_swap:
showIntro();
return;
case R.layout.swap_nfc:
@@ -506,8 +558,36 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
public void inflateSwapView(@LayoutRes int viewRes) {
+ inflateSwapView(viewRes, false);
+ }
+
+ /**
+ * The {@link #backstack} for the global back button is managed mostly here.
+ * The initial screen is never added to the {@code backstack} since the
+ * empty state is used to detect that the system's backstack should be used.
+ */
+ public void inflateSwapView(@LayoutRes int viewRes, boolean backPressed) {
getSwapService().initTimer();
+ if (!backPressed) {
+ switch (currentSwapViewLayoutRes) {
+ case R.layout.swap_connecting:
+ case R.layout.swap_confirm_receive:
+ // do not add to backstack
+ break;
+ default:
+ if (backstack.isEmpty()) {
+ if (viewRes != R.layout.swap_start_swap) {
+ backstack.push(currentSwapViewLayoutRes);
+ }
+ } else {
+ if (backstack.peek() != currentSwapViewLayoutRes) {
+ backstack.push(currentSwapViewLayoutRes);
+ }
+ }
+ }
+ }
+
container.removeAllViews();
View view = ContextCompat.getSystemService(this, LayoutInflater.class)
.inflate(viewRes, container, false);
@@ -516,11 +596,17 @@ public class SwapWorkflowActivity extends AppCompatActivity {
currentSwapViewLayoutRes = viewRes;
toolbar.setTitle(currentView.getToolbarTitle());
- toolbar.setNavigationOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- onToolbarCancel();
+ toolbar.setNavigationOnClickListener(v -> onToolbarBackPressed());
+ toolbar.setNavigationOnClickListener(v -> {
+ switch (currentView.getLayoutResId()) {
+ case R.layout.swap_start_swap:
+ SwapService.stop(this);
+ finish();
+ return;
+ default:
+ currentSwapViewLayoutRes = R.layout.swap_start_swap;
}
+ inflateSwapView(currentSwapViewLayoutRes);
});
container.addView(view);
supportInvalidateOptionsMenu();
@@ -549,11 +635,6 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
}
- private void onToolbarCancel() {
- SwapService.stop(this);
- finish();
- }
-
public void showIntro() {
// If we were previously swapping with a specific client, forget that we were doing that,
// as we are starting over now.
@@ -689,10 +770,9 @@ public class SwapWorkflowActivity extends AppCompatActivity {
*/
public void swapWith(NewRepoConfig repoConfig) {
Peer peer = repoConfig.toPeer();
- if (currentSwapViewLayoutRes == STEP_INTRO || currentSwapViewLayoutRes == R.layout.swap_confirm_receive) {
+ if (currentSwapViewLayoutRes == R.layout.swap_start_swap
+ || currentSwapViewLayoutRes == R.layout.swap_confirm_receive) {
// This will force the "Select apps to swap" workflow to begin.
- // TODO: Find a better way to decide whether we need to select the apps. Not sure if we
- // can or cannot be in STEP_INTRO with a full blown repo ready to swap.
swapWith(peer);
} else {
getSwapService().swapWith(peer);
diff --git a/app/src/full/res/menu/swap_search.xml b/app/src/full/res/menu/swap_search.xml
index 52bb58e01..418d70ad2 100644
--- a/app/src/full/res/menu/swap_search.xml
+++ b/app/src/full/res/menu/swap_search.xml
@@ -1,9 +1,16 @@
-