overhaul nav and backstace to be non-linear & add "Apps" button

This commit is contained in:
Hans-Christoph Steiner
2021-09-03 12:52:54 +02:00
parent 218d0fb04d
commit f6752edebe
3 changed files with 156 additions and 69 deletions

View File

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

View File

@@ -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:
* <ul>
* <li>which WiFi network to use</li>
* <li>whether to advertise via Bluetooth or WiFi+Bonjour</li>
* <li>connect to another device's swap</li>
* <li>choose which apps to share</li>
* <li>ask if the other device would like to swap with us</li>
* <li>help connect via QR Code or NFC</li>
* </ul>
* <p>
* 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 <a href="https://developer.squareup.com/blog/advocating-against-android-fragments/"></a>
*/
@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<Integer> 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);

View File

@@ -1,9 +1,16 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
android:title="@string/menu_search"
android:titleCondensed="@string/menu_search"/>
android:titleCondensed="@string/menu_search" />
<item
android:id="@+id/action_apps"
android:icon="@drawable/ic_apps"
android:title="@string/apps"
app:showAsAction="ifRoom" />
</menu>