From bc7d2f9f71bce9a264d89d91476289b9e70310e1 Mon Sep 17 00:00:00 2001 From: Mario Danic Date: Tue, 19 Dec 2017 17:19:36 +0100 Subject: [PATCH] Fix #23 and a few other bugs Signed-off-by: Mario Danic --- .../talk/activities/CallActivity.java | 5 +- .../talk/activities/MainActivity.java | 4 +- .../AccountVerificationController.java | 2 +- .../BottomNavigationController.java | 222 ----------- .../MagicBottomNavigationController.java | 87 ++++ .../BottomNavigationController.java | 376 ++++++++++++++++++ .../BottomNavigationMenuItem.java | 87 ++++ .../talk/events/PeerConnectionEvent.java | 1 + .../talk/utils/BottomNavigationUtils.java | 39 ++ 9 files changed, 595 insertions(+), 228 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/controllers/BottomNavigationController.java create mode 100644 app/src/main/java/com/nextcloud/talk/controllers/MagicBottomNavigationController.java create mode 100644 app/src/main/java/com/nextcloud/talk/controllers/base/bottomnavigation/BottomNavigationController.java create mode 100644 app/src/main/java/com/nextcloud/talk/controllers/base/bottomnavigation/BottomNavigationMenuItem.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/BottomNavigationUtils.java diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index 9dc24f744..d9e5fb0e6 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java @@ -442,9 +442,8 @@ public class CallActivity extends AppCompatActivity { userEntity.getToken()), ApiHelper.getUrlForSignaling(userEntity.getBaseUrl())) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) - //.repeatWhen(observable -> observable.delay(1500, TimeUnit - // .MILLISECONDS)) - .repeatWhen(completed -> completed) + .repeatWhen(observable -> observable.delay(1500, + TimeUnit.MILLISECONDS)) .repeatUntil(booleanSupplier) .retry(3) .subscribe(new Observer() { diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.java b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.java index bbec64027..98142dac2 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.java @@ -34,7 +34,7 @@ import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; import com.nextcloud.talk.R; import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.controllers.BottomNavigationController; +import com.nextcloud.talk.controllers.MagicBottomNavigationController; import com.nextcloud.talk.controllers.ServerSelectionController; import com.nextcloud.talk.controllers.base.providers.ActionBarProvider; import com.nextcloud.talk.events.CertificateEvent; @@ -93,7 +93,7 @@ public final class MainActivity extends AppCompatActivity implements ActionBarPr router = Conductor.attachRouter(this, container, savedInstanceState); if (!router.hasRootController() && userUtils.anyUserExists()) { - router.setRoot(RouterTransaction.with(new BottomNavigationController(R.menu.menu_navigation)) + router.setRoot(RouterTransaction.with(new MagicBottomNavigationController()) .pushChangeHandler(new HorizontalChangeHandler()) .popChangeHandler(new HorizontalChangeHandler())); } else if (!router.hasRootController()) { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java b/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java index 09485ab44..afd7ad521 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java @@ -150,7 +150,7 @@ public class AccountVerificationController extends BaseController { if (userUtils.getUsers().size() == 1) { getRouter().setRoot(RouterTransaction.with(new - BottomNavigationController(R.menu.menu_navigation)) + MagicBottomNavigationController()) .pushChangeHandler(new HorizontalChangeHandler()) .popChangeHandler(new HorizontalChangeHandler())); } else { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/BottomNavigationController.java b/app/src/main/java/com/nextcloud/talk/controllers/BottomNavigationController.java deleted file mode 100644 index 761eec423..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/BottomNavigationController.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * The bottom navigation was taken from a PR to Conductor by Chris6647@gmail.com - * https://github.com/bluelinelabs/Conductor/pull/316 - * and of course modified by yours truly. - */ - -package com.nextcloud.talk.controllers; - -import android.os.Bundle; -import android.support.annotation.MenuRes; -import android.support.annotation.NonNull; -import android.support.design.widget.BottomNavigationView; -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; - -import com.bluelinelabs.conductor.ChangeHandlerFrameLayout; -import com.bluelinelabs.conductor.Controller; -import com.bluelinelabs.conductor.Router; -import com.bluelinelabs.conductor.RouterTransaction; -import com.nextcloud.talk.R; -import com.nextcloud.talk.controllers.base.BaseController; -import com.nextcloud.talk.utils.bundle.BundleBuilder; - -import butterknife.BindView; - -/** - * Backstack per menu item goes against Google Design Guidelines. - * https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-behavior - */ -public class BottomNavigationController extends BaseController { - - public static final String TAG = "BottomNavigationController"; - - private static final String KEY_MENU_RESOURCE = "key_menu_resource"; - private static final String KEY_STATE_ROUTER_BUNDLES = "key_state_router_bundles"; - private static final String KEY_STATE_CURRENTLY_SELECTED_ID = "key_state_currently_selected_id"; - - @BindView(R.id.bottom_navigation_root) - LinearLayout bottomNavigationRoot; - - @BindView(R.id.navigation) - BottomNavigationView bottomNavigationView; - - @BindView(R.id.bottom_navigation_controller_container) - ChangeHandlerFrameLayout controllerContainer; - - private int currentlySelectedItemId; - - private SparseArray routerBundles; - - private Router childRouter; - - public BottomNavigationController(@MenuRes int menu) { - this(new BundleBuilder(new Bundle()).putInt(KEY_MENU_RESOURCE, menu).build()); - } - - public BottomNavigationController(Bundle args) { - super(args); - } - - private static Controller getControllerFor(int menuItemId) { - Controller controller; - switch (menuItemId) { - case R.id.navigation_calls: - controller = new CallsListController(); - break; - case R.id.navigation_contacts: - controller = new ContactsController(); - break; - case R.id.navigation_settings: - controller = new SettingsController(); - break; - default: - throw new IllegalStateException( - "Unknown bottomNavigationView item selected."); - } - return controller; - } - - @NonNull - @Override - protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { - return inflater.inflate(R.layout.controller_bottom_navigation, container, false); - } - - @Override - protected void onViewBound(@NonNull View view) { - super.onViewBound(view); - - if (getActionBar() != null) { - getActionBar().show(); - } - - /* Setup the BottomNavigationView with the constructor supplied Menu resource */ - bottomNavigationView.inflateMenu(getMenuResource()); - - Menu menu = bottomNavigationView.getMenu(); - int menuSize = menu.size(); - - childRouter = getChildRouter(controllerContainer); - - /* - * Not having access to Backstack or RouterTransaction constructors, - * we have to save/restore the entire routers for each backstack. - */ - if (routerBundles == null) { - routerBundles = new SparseArray<>(menuSize); - for (int i = 0; i < menuSize; i++) { - MenuItem menuItem = menu.getItem(i); - int itemId = menuItem.getItemId(); - /* Ensure the first checked item is shown */ - if (menuItem.isChecked()) { - childRouter.setRoot(RouterTransaction.with(BottomNavigationController.getControllerFor( - itemId))); - bottomNavigationView.setSelectedItemId(itemId); - currentlySelectedItemId = bottomNavigationView.getSelectedItemId(); - break; - } - } - } else { - /* - * Since we are restoring our state, - * and onRestoreInstanceState is called before onViewBound, - * all we need to do is rebind. - */ - childRouter.rebindIfNeeded(); - } - - bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { - @Override - public boolean onNavigationItemSelected(@NonNull MenuItem item) { - if (currentlySelectedItemId != item.getItemId()) { - saveChildRouter(currentlySelectedItemId); - clearChildRouter(); - - currentlySelectedItemId = item.getItemId(); - Bundle routerBundle = routerBundles.get(currentlySelectedItemId); - if (routerBundle != null && !routerBundle.isEmpty()) { - childRouter.restoreInstanceState(routerBundle); - childRouter.rebindIfNeeded(); - } else { - childRouter.setRoot(RouterTransaction.with(BottomNavigationController.getControllerFor( - currentlySelectedItemId))); - } - return true; - } else { - return false; - } - } - }); - } - - private void saveChildRouter(int itemId) { - Bundle routerBundle = new Bundle(); - childRouter.saveInstanceState(routerBundle); - routerBundles.put(itemId, routerBundle); - } - - /** - * Removes ALL {@link Controller}'s in the child{@link Router}'s backstack - */ - private void clearChildRouter() { - childRouter.setPopsLastView(true); /* Ensure the last view can be removed while we do this */ - childRouter.popToRoot(); - childRouter.popCurrentController(); - childRouter.setPopsLastView(false); - } - - private int getMenuResource() { - return getArgs().getInt(KEY_MENU_RESOURCE); - } - - @Override - protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { - routerBundles = savedInstanceState.getSparseParcelableArray(KEY_STATE_ROUTER_BUNDLES); - currentlySelectedItemId = savedInstanceState.getInt(KEY_STATE_CURRENTLY_SELECTED_ID); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - saveChildRouter(currentlySelectedItemId); - outState.putSparseParcelableArray(KEY_STATE_ROUTER_BUNDLES, routerBundles); - /* - * For some reason the BottomNavigationView does not seem to correctly restore its - * selectedId, even though the view appears with the correct state. - * So we keep track of it manually - */ - outState.putInt(KEY_STATE_CURRENTLY_SELECTED_ID, currentlySelectedItemId); - } - - @Override - public boolean handleBack() { - /* - * The childRouter should handleBack, - * as this BottomNavigationController doesn't have a back step sensible to the user. - */ - return childRouter.handleBack(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/controllers/MagicBottomNavigationController.java b/app/src/main/java/com/nextcloud/talk/controllers/MagicBottomNavigationController.java new file mode 100644 index 000000000..11a400a4e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/MagicBottomNavigationController.java @@ -0,0 +1,87 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * The bottom navigation was taken from a PR to Conductor by Chris6647@gmail.com + * https://github.com/bluelinelabs/Conductor/pull/316 and https://github.com/chris6647/Conductor/pull/1/files + * and of course modified by yours truly. + */ + +package com.nextcloud.talk.controllers; + +import android.support.annotation.IdRes; + +import com.bluelinelabs.conductor.Controller; +import com.nextcloud.talk.R; +import com.nextcloud.talk.controllers.base.bottomnavigation.BottomNavigationController; +import com.nextcloud.talk.controllers.base.bottomnavigation.BottomNavigationMenuItem; + +import java.lang.reflect.Constructor; + +public class MagicBottomNavigationController extends BottomNavigationController { + + public MagicBottomNavigationController() { + super(R.menu.menu_navigation); + } + + /** + * Supplied MenuItemId must match a {@link Controller} as defined in {@link + * BottomNavigationMenuItem} or an {@link IllegalArgumentException} will be thrown. + * + * @param itemId + */ + @Override + protected Controller getControllerFor(@IdRes int itemId) { + Constructor[] constructors = + BottomNavigationMenuItem.getEnum(itemId).getControllerClass().getConstructors(); + Controller controller = null; + try { + /* Determine default or Bundle constructor */ + for (Constructor constructor : constructors) { + if (constructor.getParameterTypes().length == 0) { + controller = (Controller) constructor.newInstance(); + } + } + } catch (Exception e) { + throw new RuntimeException( + "An exception occurred while creating a new instance for mapping of " + + itemId + + ". " + + e.getMessage(), + e); + } + + if (controller == null) { + throw new RuntimeException( + "Controller must have a public empty constructor. " + + itemId); + } + return controller; + } + + /** + * Supplied Controller must match a MenuItemId as defined in {@link BottomNavigationMenuItem} or + * an {@link IllegalArgumentException} will be thrown. + * + * @param controller + */ + public void navigateTo(Controller controller) { + BottomNavigationMenuItem item = BottomNavigationMenuItem.getEnum(controller.getClass()); + navigateTo(item.getMenuResId(), controller); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/bottomnavigation/BottomNavigationController.java b/app/src/main/java/com/nextcloud/talk/controllers/base/bottomnavigation/BottomNavigationController.java new file mode 100644 index 000000000..56ed9eb61 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/base/bottomnavigation/BottomNavigationController.java @@ -0,0 +1,376 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * The bottom navigation was taken from a PR to Conductor by Chris6647@gmail.com + * https://github.com/bluelinelabs/Conductor/pull/316 and https://github.com/chris6647/Conductor/pull/1/files + * and of course modified by yours truly. + */ + +package com.nextcloud.talk.controllers.base.bottomnavigation; + +import android.os.Bundle; +import android.support.annotation.MenuRes; +import android.support.annotation.NonNull; +import android.support.design.widget.BottomNavigationView; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import com.bluelinelabs.conductor.ChangeHandlerFrameLayout; +import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.Router; +import com.bluelinelabs.conductor.RouterTransaction; +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler; +import com.nextcloud.talk.R; +import com.nextcloud.talk.controllers.base.BaseController; +import com.nextcloud.talk.utils.BottomNavigationUtils; +import com.nextcloud.talk.utils.bundle.BundleBuilder; + +import butterknife.BindView; + +/** + * The {@link Controller} for the Bottom Navigation View. Populates a {@link BottomNavigationView} + * with the supplied {@link Menu} resource. The first item set as checked will be shown by default. + * The backstack of each {@link MenuItem} is switched out, in order to maintain a separate backstack + * for each {@link MenuItem} - even though that is against the Google Design Guidelines: + * + * @author chris6647@gmail.com + * @see Material + * Design Guidelines + * + * Internally works similarly to {@link com.bluelinelabs.conductor.support.RouterPagerAdapter}, + * in the sense that it keeps track of the currently active {@link MenuItem} and the paired + * Child {@link Router}. Everytime we navigate from one to another, + * or {@link Controller#onSaveInstanceState(Bundle)} is called, we save the entire instance state + * of the Child {@link Router}, and cache it, so we have it available when we navigate to + * another {@link MenuItem} and can then restore the correct Child {@link Router} + * (and thus the entire backstack) + */ +public abstract class BottomNavigationController extends BaseController { + + @SuppressWarnings("unused") + public static final String TAG = "BottomNavigationController"; + + private static final String KEY_MENU_RESOURCE = "key_menu_resource"; + private static final String KEY_STATE_ROUTER_BUNDLES = "key_state_router_bundles"; + private static final String KEY_STATE_CURRENTLY_SELECTED_ID = "key_state_currently_selected_id"; + + @BindView(R.id.bottom_navigation_root) + LinearLayout bottomNavigationRoot; + + @BindView(R.id.navigation) + BottomNavigationView bottomNavigationView; + + @BindView(R.id.bottom_navigation_controller_container) + ChangeHandlerFrameLayout controllerContainer; + + private int currentlySelectedItemId; + + private SparseArray routerSavedStateBundles; + private Bundle cachedSavedInstanceState; + private Router lastActiveChildRouter; + + public BottomNavigationController(@MenuRes int menu) { + this(new BundleBuilder(new Bundle()).putInt(KEY_MENU_RESOURCE, menu).build()); + } + + public BottomNavigationController(Bundle args) { + super(args); + } + + /** + * Create an internally used name to identify the Child {@link Router}s + * + * @param viewId + * @param id + * @return + */ + private static String makeRouterName(int viewId, long id) { + return viewId + ":" + id; + } + + @NonNull + @Override + protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + return inflater.inflate(R.layout.controller_bottom_navigation, container, false); + } + + @Override + protected void onViewBound(@NonNull View view) { + super.onViewBound(view); + + /* Setup the BottomNavigationView with the constructor supplied Menu resource */ + bottomNavigationView.inflateMenu(getMenuResource()); + + /* Fresh start, setup everything */ + if (routerSavedStateBundles == null) { + Menu menu = bottomNavigationView.getMenu(); + int menuSize = menu.size(); + routerSavedStateBundles = new SparseArray<>(menuSize); + for (int i = 0; i < menuSize; i++) { + MenuItem menuItem = menu.getItem(i); + /* Ensure the first checked item is shown */ + if (menuItem.isChecked()) { + /* + * Seems like the BottomNavigationView always initializes index 0 as isChecked / Selected, + * regardless of what was set in the menu xml originally. + * So basically all we're doing here is always setting up menuItem index 0. + */ + int itemId = menuItem.getItemId(); + configureRouter(getChildRouter(itemId), itemId); + bottomNavigationView.setSelectedItemId(itemId); + currentlySelectedItemId = bottomNavigationView.getSelectedItemId(); + break; + } + } + } else { + /* + * Since we are restoring our state, + * and onRestoreInstanceState is called before onViewBound, + * all we need to do is rebind. + */ + Router childRouter = getChildRouter(currentlySelectedItemId); + childRouter.rebindIfNeeded(); + lastActiveChildRouter = childRouter; + } + + bottomNavigationView.setOnNavigationItemSelectedListener( + new BottomNavigationView.OnNavigationItemSelectedListener() { + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + if (currentlySelectedItemId != item.getItemId()) { + BottomNavigationController.this.destroyChildRouter(BottomNavigationController.this.getChildRouter(currentlySelectedItemId), currentlySelectedItemId); + currentlySelectedItemId = item.getItemId(); + BottomNavigationController.this.configureRouter(BottomNavigationController.this.getChildRouter(currentlySelectedItemId), currentlySelectedItemId); + } else { + BottomNavigationController.this.resetCurrentBackstack(); + } + return true; + } + }); + } + + /** + * Get the Child {@link Router} matching the supplied ItemId. + * + * @param itemId MenuItem ID + * @return + */ + protected Router getChildRouter(int itemId) { + return getChildRouter(controllerContainer, makeRouterName(controllerContainer.getId(), itemId)); + } + + /** + * Correctly configure the {@link Router} given the cached routerSavedState. + * + * @param childRouter {@link Router} to configure + * @param itemId {@link MenuItem} ID + * @return true if {@link Router} was restored + */ + private boolean configureRouter(@NonNull Router childRouter, int itemId) { + lastActiveChildRouter = childRouter; + Bundle routerSavedState = routerSavedStateBundles.get(itemId); + if (routerSavedState != null && !routerSavedState.isEmpty()) { + childRouter.restoreInstanceState(routerSavedState); + childRouter.rebindIfNeeded(); + return true; + } + + if (!childRouter.hasRootController()) { + childRouter.setRoot(RouterTransaction.with(getControllerFor(itemId))); + } + return false; + } + + /** + * Save the {@link Router}, and remove(/destroy) it. + * + * @param childRouter {@link Router} to destroy + * @param itemId {@link MenuItem} ID + */ + protected void destroyChildRouter(@NonNull Router childRouter, int itemId) { + save(childRouter, itemId); + removeChildRouter(childRouter); + } + + /** + * Resets the current backstack to the {@link Controller}, supplied by {@link + * BottomNavigationController#getControllerFor(int)}, using a {@link FadeChangeHandler}. + */ + protected void resetCurrentBackstack() { + lastActiveChildRouter + .setRoot( + RouterTransaction.with(this.getControllerFor(currentlySelectedItemId)) + .pushChangeHandler(new FadeChangeHandler()) + .popChangeHandler(new FadeChangeHandler())); + } + + /** + * Navigate to the supplied {@link Controller}, while setting the menuItemId as selected on the + * {@link BottomNavigationView}. + * + * @param itemId {@link MenuItem} ID + * @param controller {@link Controller} matching the itemId + */ + protected void navigateTo(int itemId, @NonNull Controller controller) { + if (currentlySelectedItemId != itemId) { + destroyChildRouter(lastActiveChildRouter, currentlySelectedItemId); + + /* Ensure correct Checked state based on new selection */ + Menu menu = bottomNavigationView.getMenu(); + for (int i = 0; i < menu.size(); i++) { + MenuItem menuItem = menu.getItem(i); + if (menuItem.isChecked() && menuItem.getItemId() != itemId) { + menuItem.setChecked(false); + } else if (menuItem.getItemId() == itemId) { + menuItem.setChecked(true); + } + } + + currentlySelectedItemId = itemId; + Router childRouter = getChildRouter(currentlySelectedItemId); + if (configureRouter(childRouter, currentlySelectedItemId)) { + /* Determine if a Controller of same class already exists in the backstack */ + Controller backstackController; + int size = childRouter.getBackstackSize(); + for (int i = 0; i < size; i++) { + backstackController = childRouter.getBackstack().get(i).controller(); + if (BottomNavigationUtils.equals(backstackController.getClass(), controller.getClass())) { + /* Match found at root - so just set new root */ + if (i == size - 1) { + childRouter.setRoot(RouterTransaction.with(controller)); + } else { + /* Match found at i - pop until we're at the matching Controller */ + for (int j = size; j < i; j--) { + childRouter.popCurrentController(); + } + /* Replace the existing matching Controller with the new */ + childRouter.replaceTopController(RouterTransaction.with(controller)); + } + } + } + } + } else { + resetCurrentBackstack(); + } + } + + /** + * Saves the Child {@link Router} into a {@link Bundle} and caches that {@link Bundle}. + * + * @param childRouter to call {@link Router#saveInstanceState(Bundle)} on + * @param itemId {@link MenuItem} ID + */ + private void save(Router childRouter, int itemId) { + if (childRouter != null) { + Bundle routerBundle = new Bundle(); + childRouter.saveInstanceState(routerBundle); + routerSavedStateBundles.put(itemId, routerBundle); + } + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + routerSavedStateBundles = savedInstanceState.getSparseParcelableArray(KEY_STATE_ROUTER_BUNDLES); + currentlySelectedItemId = savedInstanceState.getInt(KEY_STATE_CURRENTLY_SELECTED_ID); + if (savedInstanceState.containsKey(KEY_STATE_ROUTER_BUNDLES) + || savedInstanceState.containsKey(KEY_STATE_CURRENTLY_SELECTED_ID)) { + cachedSavedInstanceState = savedInstanceState; + } + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + if (lastActiveChildRouter == null && cachedSavedInstanceState != null) { + /* + * Here we assume that we're in a state + * where the BottomNavigationController itself is in the backstack, + * it has been restored, and is now being saved again. + * In this case, the BottomNavigationController won't ever have had onViewBound() called, + * and thus won't have any views to setup the Child Routers with. + * In this case, we assume that we've previously had onSaveInstanceState() called + * on us successfully, and thus have a cachedSavedInstanceState to use. + * + * To replicate issue this solves: + * Navigate from BottomNavigationController to another controller not hosted in + * the childRouter, background the app + * (with developer setting "don't keep activities in memory" enabled on the device), + * open the app again, and background it once more, and open it again to see it crash. + */ + outState.putSparseParcelableArray( + KEY_STATE_ROUTER_BUNDLES, + cachedSavedInstanceState.getSparseParcelableArray(KEY_STATE_ROUTER_BUNDLES)); + outState.putInt( + KEY_STATE_CURRENTLY_SELECTED_ID, + cachedSavedInstanceState.getInt(KEY_STATE_CURRENTLY_SELECTED_ID)); + } else if (currentlySelectedItemId != 0) { + /* + * Only save state if we have a valid item selected. + * + * Otherwise we may be in a state where we are in a backstack, but have never been shown. + * I.e. if we are put in a synthesized backstack, we've never been shown any UI, + * and therefore have nothing to save. + */ + save(lastActiveChildRouter, currentlySelectedItemId); + outState.putSparseParcelableArray(KEY_STATE_ROUTER_BUNDLES, routerSavedStateBundles); + /* + * For some reason the BottomNavigationView does not seem to correctly restore its + * selectedId, even though the view appears with the correct state. + * So we keep track of it manually + */ + outState.putInt(KEY_STATE_CURRENTLY_SELECTED_ID, currentlySelectedItemId); + lastActiveChildRouter = null; + } + } + + @Override + public boolean handleBack() { + /* + * The childRouter should handleBack, + * as this BottomNavigationController doesn't have a back step sensible to the user. + */ + return lastActiveChildRouter.handleBack(); + } + + /** + * Get the {@link Menu} Resource ID from {@link Controller#getArgs()} + * + * @return the {@link Menu} Resource ID + */ + private int getMenuResource() { + return getArgs().getInt(KEY_MENU_RESOURCE); + } + + /** + * Return a target instance of {@link Controller} for given menu item ID + * + * @param itemId the ID tapped by the user + * @return the {@link Controller} instance to navigate to + */ + protected abstract Controller getControllerFor(int itemId); + + private boolean equals(Object a, Object b) { + return (a == b) || (a != null && a.equals(b)); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/bottomnavigation/BottomNavigationMenuItem.java b/app/src/main/java/com/nextcloud/talk/controllers/base/bottomnavigation/BottomNavigationMenuItem.java new file mode 100644 index 000000000..e3626ca10 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/base/bottomnavigation/BottomNavigationMenuItem.java @@ -0,0 +1,87 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * The bottom navigation was taken from a PR to Conductor by Chris6647@gmail.com + * https://github.com/bluelinelabs/Conductor/pull/316 and https://github.com/chris6647/Conductor/pull/1/files + * and of course modified by yours truly. + */ + +package com.nextcloud.talk.controllers.base.bottomnavigation; + +import android.support.annotation.IdRes; + +import com.bluelinelabs.conductor.Controller; +import com.nextcloud.talk.R; +import com.nextcloud.talk.controllers.CallsListController; +import com.nextcloud.talk.controllers.ContactsController; +import com.nextcloud.talk.controllers.SettingsController; +import com.nextcloud.talk.utils.BottomNavigationUtils; + +/** + * Enum representation of valid Bottom Navigation Menu Items + */ +public enum BottomNavigationMenuItem { + CALLS(R.id.navigation_calls, CallsListController.class), + CONTACTS(R.id.navigation_contacts, ContactsController.class), + SETTINGS(R.id.navigation_settings, SettingsController.class); + + private int menuResId; + private Class controllerClass; + + BottomNavigationMenuItem(@IdRes int menuResId, Class controllerClass) { + this.menuResId = menuResId; + this.controllerClass = controllerClass; + } + + public static BottomNavigationMenuItem getEnum(@IdRes int menuResId) { + for (BottomNavigationMenuItem type : BottomNavigationMenuItem.values()) { + if (menuResId == type.getMenuResId()) { + return type; + } + } + throw new IllegalArgumentException("Unable to map " + menuResId); + } + + public static BottomNavigationMenuItem getEnum(Class controllerClass) { + for (BottomNavigationMenuItem type : BottomNavigationMenuItem.values()) { + if (BottomNavigationUtils.equals(controllerClass, type.getControllerClass())) { + return type; + } + } + throw new IllegalArgumentException("Unable to map " + controllerClass); + } + + public int getMenuResId() { + return menuResId; + } + + public Class getControllerClass() { + return controllerClass; + } + + @Override + public String toString() { + return "BottomNavigationMenuItem{" + + "menuResId=" + + menuResId + + ", controllerClass=" + + controllerClass + + '}'; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/events/PeerConnectionEvent.java b/app/src/main/java/com/nextcloud/talk/events/PeerConnectionEvent.java index f0ca7fb49..e3749f3aa 100644 --- a/app/src/main/java/com/nextcloud/talk/events/PeerConnectionEvent.java +++ b/app/src/main/java/com/nextcloud/talk/events/PeerConnectionEvent.java @@ -28,6 +28,7 @@ import lombok.Data; public class PeerConnectionEvent { private final PeerConnectionEventType peerConnectionEventType; private final String sessionId; + public PeerConnectionEvent(PeerConnectionEventType peerConnectionEventType, @Nullable String sessionId) { this.peerConnectionEventType = peerConnectionEventType; this.sessionId = sessionId; diff --git a/app/src/main/java/com/nextcloud/talk/utils/BottomNavigationUtils.java b/app/src/main/java/com/nextcloud/talk/utils/BottomNavigationUtils.java new file mode 100644 index 000000000..a02230a71 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/BottomNavigationUtils.java @@ -0,0 +1,39 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * The bottom navigation was taken from a PR to Conductor by Chris6647@gmail.com + * https://github.com/bluelinelabs/Conductor/pull/316 and https://github.com/chris6647/Conductor/pull/1/files + * and of course modified by yours truly. + */ + +package com.nextcloud.talk.utils; + +public class BottomNavigationUtils { + + /** + * Copy/paste from {@link java.util.Objects#equals(Object, Object)} to support lower API version + * + * @param a + * @param b + * @return + */ + public static boolean equals(Object a, Object b) { + return (a == b) || (a != null && a.equals(b)); + } +}