From f6752edebe03e1d53be77a016fed5d370c7bdb5e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 3 Sep 2021 12:52:54 +0200 Subject: [PATCH] overhaul nav and backstace to be non-linear & add "Apps" button --- .../org/fdroid/fdroid/nearby/SwapService.java | 18 +- .../fdroid/nearby/SwapWorkflowActivity.java | 196 ++++++++++++------ app/src/full/res/menu/swap_search.xml | 11 +- 3 files changed, 156 insertions(+), 69 deletions(-) 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: + * + *

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

+ + android:titleCondensed="@string/menu_search" /> + + \ No newline at end of file