mirror of
https://github.com/nextcloud/talk-android
synced 2025-03-06 06:15:12 +00:00
Fix #23 and a few other bugs
Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
parent
29f6e84e02
commit
bc7d2f9f71
@ -442,9 +442,8 @@ public class CallActivity extends AppCompatActivity {
|
|||||||
userEntity.getToken()), ApiHelper.getUrlForSignaling(userEntity.getBaseUrl()))
|
userEntity.getToken()), ApiHelper.getUrlForSignaling(userEntity.getBaseUrl()))
|
||||||
.subscribeOn(Schedulers.newThread())
|
.subscribeOn(Schedulers.newThread())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
//.repeatWhen(observable -> observable.delay(1500, TimeUnit
|
.repeatWhen(observable -> observable.delay(1500,
|
||||||
// .MILLISECONDS))
|
TimeUnit.MILLISECONDS))
|
||||||
.repeatWhen(completed -> completed)
|
|
||||||
.repeatUntil(booleanSupplier)
|
.repeatUntil(booleanSupplier)
|
||||||
.retry(3)
|
.retry(3)
|
||||||
.subscribe(new Observer<SignalingOverall>() {
|
.subscribe(new Observer<SignalingOverall>() {
|
||||||
|
@ -34,7 +34,7 @@ import com.bluelinelabs.conductor.RouterTransaction;
|
|||||||
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
|
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
|
||||||
import com.nextcloud.talk.R;
|
import com.nextcloud.talk.R;
|
||||||
import com.nextcloud.talk.application.NextcloudTalkApplication;
|
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.ServerSelectionController;
|
||||||
import com.nextcloud.talk.controllers.base.providers.ActionBarProvider;
|
import com.nextcloud.talk.controllers.base.providers.ActionBarProvider;
|
||||||
import com.nextcloud.talk.events.CertificateEvent;
|
import com.nextcloud.talk.events.CertificateEvent;
|
||||||
@ -93,7 +93,7 @@ public final class MainActivity extends AppCompatActivity implements ActionBarPr
|
|||||||
router = Conductor.attachRouter(this, container, savedInstanceState);
|
router = Conductor.attachRouter(this, container, savedInstanceState);
|
||||||
|
|
||||||
if (!router.hasRootController() && userUtils.anyUserExists()) {
|
if (!router.hasRootController() && userUtils.anyUserExists()) {
|
||||||
router.setRoot(RouterTransaction.with(new BottomNavigationController(R.menu.menu_navigation))
|
router.setRoot(RouterTransaction.with(new MagicBottomNavigationController())
|
||||||
.pushChangeHandler(new HorizontalChangeHandler())
|
.pushChangeHandler(new HorizontalChangeHandler())
|
||||||
.popChangeHandler(new HorizontalChangeHandler()));
|
.popChangeHandler(new HorizontalChangeHandler()));
|
||||||
} else if (!router.hasRootController()) {
|
} else if (!router.hasRootController()) {
|
||||||
|
@ -150,7 +150,7 @@ public class AccountVerificationController extends BaseController {
|
|||||||
|
|
||||||
if (userUtils.getUsers().size() == 1) {
|
if (userUtils.getUsers().size() == 1) {
|
||||||
getRouter().setRoot(RouterTransaction.with(new
|
getRouter().setRoot(RouterTransaction.with(new
|
||||||
BottomNavigationController(R.menu.menu_navigation))
|
MagicBottomNavigationController())
|
||||||
.pushChangeHandler(new HorizontalChangeHandler())
|
.pushChangeHandler(new HorizontalChangeHandler())
|
||||||
.popChangeHandler(new HorizontalChangeHandler()));
|
.popChangeHandler(new HorizontalChangeHandler()));
|
||||||
} else {
|
} else {
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@ import lombok.Data;
|
|||||||
public class PeerConnectionEvent {
|
public class PeerConnectionEvent {
|
||||||
private final PeerConnectionEventType peerConnectionEventType;
|
private final PeerConnectionEventType peerConnectionEventType;
|
||||||
private final String sessionId;
|
private final String sessionId;
|
||||||
|
|
||||||
public PeerConnectionEvent(PeerConnectionEventType peerConnectionEventType, @Nullable String sessionId) {
|
public PeerConnectionEvent(PeerConnectionEventType peerConnectionEventType, @Nullable String sessionId) {
|
||||||
this.peerConnectionEventType = peerConnectionEventType;
|
this.peerConnectionEventType = peerConnectionEventType;
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user