Selaa lähdekoodia

Fix #23 and a few other bugs

Signed-off-by: Mario Danic <mario@lovelyhq.com>
Mario Danic 7 vuotta sitten
vanhempi
commit
bc7d2f9f71

+ 2 - 3
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<SignalingOverall>() {

+ 2 - 2
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()) {

+ 1 - 1
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 {

+ 0 - 222
app/src/main/java/com/nextcloud/talk/controllers/BottomNavigationController.java

@@ -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 <http://www.gnu.org/licenses/>.
- *
- * 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<Bundle> 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();
-    }
-}

+ 87 - 0
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 <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 <http://www.gnu.org/licenses/>.
+ *
+ * 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);
+    }
+}

+ 376 - 0
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 <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 <http://www.gnu.org/licenses/>.
+ *
+ * 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 <a
+ * href="https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-behavior">Material
+ * Design Guidelines</a>
+ *
+ * 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<Bundle> 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));
+    }
+}

+ 87 - 0
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 <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 <http://www.gnu.org/licenses/>.
+ *
+ * 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<? extends Controller> controllerClass;
+
+    BottomNavigationMenuItem(@IdRes int menuResId, Class<? extends Controller> 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<? extends Controller> 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<? extends Controller> getControllerClass() {
+        return controllerClass;
+    }
+
+    @Override
+    public String toString() {
+        return "BottomNavigationMenuItem{"
+                + "menuResId="
+                + menuResId
+                + ", controllerClass="
+                + controllerClass
+                + '}';
+    }
+}

+ 1 - 0
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;

+ 39 - 0
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 <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 <http://www.gnu.org/licenses/>.
+ *
+ * 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));
+    }
+}