<NME> AbstractBottomNavigationActivity.java
<BEF> package cgeo.geocaching.activity;

import cgeo.geocaching.CacheDetailActivity;
import cgeo.geocaching.CacheListActivity;
import cgeo.geocaching.CgeoApplication;
import cgeo.geocaching.MainActivity;
import cgeo.geocaching.R;
import cgeo.geocaching.SearchActivity;
import cgeo.geocaching.connector.ConnectorFactory;
import cgeo.geocaching.connector.IConnector;
import cgeo.geocaching.connector.capability.ILogin;
import cgeo.geocaching.databinding.ActivityBottomNavigationBinding;
import cgeo.geocaching.list.PseudoList;
import cgeo.geocaching.list.StoredList;
import cgeo.geocaching.maps.DefaultMap;
import cgeo.geocaching.models.Geocache;
import cgeo.geocaching.network.Network;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.storage.DataStore;
import cgeo.geocaching.ui.GeoItemSelectorUtils;
import cgeo.geocaching.ui.dialog.Dialogs;
import cgeo.geocaching.utils.AndroidRxUtils;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;

import java.util.concurrent.atomic.AtomicInteger;

import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import com.google.android.material.badge.BadgeDrawable;
import com.google.android.material.navigation.NavigationBarView;

    public static final @IdRes
    int MENU_HOME = R.id.page_home;
    public static final @IdRes
    int MENU_NOTHING_SELECTED = 0;
    public static final @IdRes
    int MENU_HIDE_BOTTOM_NAVIGATION = -1;

    private static Boolean loginSuccessful = null; // must be static so that the login state is stored while switching between activities
    public static final @IdRes
    int MENU_SEARCH = R.id.page_search;
    public static final @IdRes
    int MENU_NEARBY = R.id.page_nearby;
    public static final @IdRes
    int MENU_HOME = R.id.page_home;
    public static final @IdRes
    int MENU_HIDE_BOTTOM_NAVIGATION = -1;

    private static Boolean loginSuccessful = null; // must be static so that the login state is stored while switching between activities


    private ActivityBottomNavigationBinding binding = null;
        wrapper.activityContent.addView(contentView);
        super.setContentView(wrapper.getRoot());

        // --- other initialization --- //
        updateSelectedItemId();
        // add item selected listener only after item selection has been updated, as it would otherwise directly call the listener
        ((NavigationBarView) wrapper.activityBottomNavigation).setOnItemSelectedListener(this);
        // long click event listeners
        findViewById(R.id.page_list).setOnLongClickListener(view -> onListsLongClicked());
        // will be called if c:geo cannot log in
        registerLoginIssueHandler(loginHandler, getUpdateUserInfoHandler(), this::onLoginIssue);
    }

    @Nullable
    protected Handler getUpdateUserInfoHandler() {
        return null;
        findViewById(MENU_SEARCH).setOnLongClickListener(view -> onSearchLongClicked());
        // will be called if c:geo cannot log in
        startLoginIssueHandler();
    }

    @Nullable
    protected Handler getUpdateUserInfoHandler() {
        return null;
    }


    protected void onLoginIssue(final boolean issue) {
        synchronized (hasHighPrioNotification) {
            hasHighPrioNotification.set(issue);
                startActivity(CacheListActivity.getHistoryIntent(this));
            } else {
                Settings.setLastDisplayedList(selectedListId);
                CacheListActivity.startActivityOffline(this);
            }
            ActivityMixin.finishWithFadeTransition(this);
        }, false, PseudoList.NEW_LIST.id);
        return true;
    }
                Settings.setLastDisplayedList(selectedListId);
                startActivity(CacheListActivity.getActivityOfflineIntent(this));
            }
            ActivityMixin.overrideTransitionToFade(this);
        }, false, PseudoList.NEW_LIST.id);
        return true;
    }

    private boolean onSearchLongClicked() {
        final ArrayList<Geocache> lastCaches = new ArrayList<>(DataStore.getLastOpenedCaches());
        final ListAdapter adapter = new ArrayAdapter<Geocache>(this, R.layout.cacheslist_item_select, lastCaches) {
            @Override
            public View getView(final int position, final View convertView, @NonNull final ViewGroup parent) {
                return GeoItemSelectorUtils.createGeocacheItemView(AbstractBottomNavigationActivity.this, getItem(position),
                        GeoItemSelectorUtils.getOrCreateView(AbstractBottomNavigationActivity.this, convertView, parent));
    @Override
    protected void onResume() {
        super.onResume();
        registerLoginIssueHandler(loginHandler, getUpdateUserInfoHandler(), this::onLoginIssue);
        registerReceiver(connectivityChangeReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
    }

    @Override
    public void setContentView(final int layoutResID) {
        final View view = getLayoutInflater().inflate(layoutResID, null);
    public void onPause() {
        unregisterReceiver(connectivityChangeReceiver);
        super.onPause();
    }
    public void onBackPressed() {
        if (!isTaskRoot() || doubleBackToExitPressedOnce) {
            super.onBackPressed();
            return;
        }


    @Override
        new Handler().postDelayed(() -> doubleBackToExitPressedOnce = false, 2000);
    }

    /**
     * WARNING: This triggers {@link AbstractBottomNavigationActivity#onNavigationItemSelected} while changing the current selection.
     */
    private void updateSelectedItemId() {
        final int menuId = getSelectedBottomItemId();
        final MenuItem workaroundItem = ((NavigationBarView) wrapper.activityBottomNavigation).getMenu().getItem(0);

        // According to material design guidelines, bottom navigation activities always should be top-level.
        // Therefore hiding the bottom navigation to not confuse the users.
        if (menuId == MENU_HIDE_BOTTOM_NAVIGATION || !isTaskRoot()) {
            wrapper.activityBottomNavigation.setVisibility(View.GONE);
        } else if (menuId == MENU_NOTHING_SELECTED) {
            wrapper.activityBottomNavigation.setVisibility(View.VISIBLE);
            ((NavigationBarView) wrapper.activityBottomNavigation).setSelectedItemId(workaroundItem.getItemId());
            workaroundItem.setCheckable(false);
        } else {
            wrapper.activityBottomNavigation.setVisibility(View.VISIBLE);
            ((NavigationBarView) wrapper.activityBottomNavigation).setSelectedItemId(menuId);
            workaroundItem.setCheckable(true);
        }
    }

    /**
        final int menuId = getSelectedBottomItemId();

        if (menuId == MENU_HIDE_BOTTOM_NAVIGATION) {
            binding.activityBottomNavigation.setVisibility(View.GONE);
        } else {
            binding.activityBottomNavigation.setVisibility(View.VISIBLE);
            ((NavigationBarView) binding.activityBottomNavigation).setSelectedItemId(menuId);
        }

        // Don't show back button if bottom navigation is visible (although they can have a backstack as well)
        final ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {

        if (id == getSelectedBottomItemId()) {
            onNavigationItemReselected(item);
            return true;
        }
        return onNavigationItemSelectedIgnoreReselected(item);
    /**
     * @return the menu item id which should be selected
     */
        final int id = item.getItemId();

        if (id == MENU_MAP) {
            startActivity(DefaultMap.getLiveMapIntent(this));
        } else if (id == MENU_LIST) {
            CacheListActivity.startActivityOffline(this);
        } else if (id == MENU_SEARCH) {
            startActivity(new Intent(this, SearchActivity.class));
        } else if (id == MENU_NEARBY) {
            startActivity(CacheListActivity.getNearestIntent(this));
        } else if (id == MENU_HOME) {
            startActivity(new Intent(this, MainActivity.class));
        } else {
            throw new IllegalStateException("unknown navigation item selected"); // should never happen
        }
        // avoid weired transitions
        ActivityMixin.finishWithFadeTransition(this);
        return true;
    }

        final int id = item.getItemId();
        final Intent launchIntent;

        if (id == MENU_MAP) {
            launchIntent = DefaultMap.getLiveMapIntent(this);
        } else if (id == MENU_LIST) {
            launchIntent = CacheListActivity.getActivityOfflineIntent(this);
        } else if (id == MENU_SEARCH) {
            launchIntent = new Intent(this, SearchActivity.class);
        } else if (id == MENU_NEARBY) {
            launchIntent = CacheListActivity.getNearestIntent(this);
        } else if (id == MENU_HOME) {
            launchIntent = new Intent(this, MainActivity.class);
        } else {
            throw new IllegalStateException("unknown navigation item selected"); // should never happen
        }

        startActivity(launchIntent);

        // Clear activity stack if the user actively navigates via the bottom navigation
        clearBackStack();

        // avoid weired transitions
        ActivityMixin.overrideTransitionToFade(this);
        return true;
    }


    private final class ConnectivityChangeReceiver extends BroadcastReceiver {
        private boolean isConnected = Network.isConnected();

        @Override
        public void onReceive(final Context context, final Intent intent) {
            final boolean wasConnected = isConnected;
            isConnected = Network.isConnected();
            if (isConnected && !wasConnected) {
                startLoginIssueHandler();
            }
        }
    }

    /**
     * check if at least one connector has been logged in successfully
     */
    public static boolean anyConnectorLoggedIn() {
        final ILogin[] activeConnectors = ConnectorFactory.getActiveLiveConnectors();
        for (final IConnector conn : activeConnectors) {
            if (((ILogin) conn).isLoggedIn()) {
                return true;
            }
        }
        return false;
    }

    /**
     * detect whether c:geo is unable to log in
     *
     */
    public void startLoginIssueHandler() {
        if (loginSuccessful != null && !loginSuccessful) {
            loginSuccessful = anyConnectorLoggedIn();
        }
        if (loginSuccessful != null && !loginSuccessful) {
            onLoginIssue(true); // login still failing. Start loginIssueCallback
        }
        if (loginSuccessful != null && loginSuccessful) {
            onLoginIssue(false);
            return; // there was a successfully login
        }

        if (!Network.isConnected()) {
            onLoginIssue(true);
            return;
        }

        // We are probably not yet ready. Log in and wait a bit...
        startBackgroundLogin(getUpdateUserInfoHandler());
        loginHandler.postDelayed(() -> {
            loginSuccessful = anyConnectorLoggedIn();
            onLoginIssue(!loginSuccessful);
        }, 10000);
    }

    private void startBackgroundLogin(@Nullable final Handler updateUserInfoHandler) {

        final ILogin[] loginConns = ConnectorFactory.getActiveLiveConnectors();

        //ensure that login is not done while another login is still in progress
        synchronized (LOGINS_IN_PROGRESS) {
            if (LOGINS_IN_PROGRESS.get() > 0) {
                return;
            }
            LOGINS_IN_PROGRESS.set(loginConns.length);
        }

        final boolean mustLogin = ConnectorFactory.mustRelog();

        for (final ILogin conn : loginConns) {
            if (mustLogin || !conn.isLoggedIn()) {
                AndroidRxUtils.networkScheduler.scheduleDirect(() -> {
                    if (mustLogin) {
                        // Properly log out from geocaching.com
                        conn.logout();
                    }
                    conn.login();

                    LOGINS_IN_PROGRESS.addAndGet(-1);

                    // the login state might have changed...
                    if (anyConnectorLoggedIn()) {
                        runOnUiThread(() -> onLoginIssue(false));
                    }
                    if (updateUserInfoHandler != null) {
                        updateUserInfoHandler.sendEmptyMessage(-1);
                    }
                });
            }
        }
    }

    public void updateHomeBadge(final int delta) {
        final int badgeColor;
        synchronized (hasHighPrioNotification) {
            badgeColor = hasHighPrioNotification.get() ? 0xffff0000 : 0xff0a67e2;
        }
        synchronized (lowPrioNotificationCounter) {
            lowPrioNotificationCounter.set(lowPrioNotificationCounter.get() + delta);
            final BadgeDrawable badge = ((NavigationBarView) binding.activityBottomNavigation).getOrCreateBadge(MENU_HOME);
            badge.clearNumber();
            badge.setBackgroundColor(badgeColor);
            badge.setVisible(lowPrioNotificationCounter.get() > 0);
        }
    }
}

<MSG> keep activities alive in background (rel. to #12317 / #11926)

<DFF> @@ -28,6 +28,7 @@ import android.widget.Toast;
 import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
 import androidx.core.util.Consumer;
 
 import java.util.concurrent.atomic.AtomicInteger;
@@ -47,8 +48,6 @@ public abstract class AbstractBottomNavigationActivity extends AbstractActionBar
     public static final @IdRes
     int MENU_HOME = R.id.page_home;
     public static final @IdRes
-    int MENU_NOTHING_SELECTED = 0;
-    public static final @IdRes
     int MENU_HIDE_BOTTOM_NAVIGATION = -1;
 
     private static Boolean loginSuccessful = null; // must be static so that the login state is stored while switching between activities
@@ -68,16 +67,24 @@ public abstract class AbstractBottomNavigationActivity extends AbstractActionBar
         wrapper.activityContent.addView(contentView);
         super.setContentView(wrapper.getRoot());
 
+        // Don't show back button for top-level activity in bottom navigation context
+        final ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null && isTopLevelBottomNavActivity()) {
+            actionBar.setDisplayHomeAsUpEnabled(false);
+        }
+
         // --- other initialization --- //
         updateSelectedItemId();
-        // add item selected listener only after item selection has been updated, as it would otherwise directly call the listener
-        ((NavigationBarView) wrapper.activityBottomNavigation).setOnItemSelectedListener(this);
         // long click event listeners
         findViewById(R.id.page_list).setOnLongClickListener(view -> onListsLongClicked());
         // will be called if c:geo cannot log in
         registerLoginIssueHandler(loginHandler, getUpdateUserInfoHandler(), this::onLoginIssue);
     }
 
+    protected boolean isTopLevelBottomNavActivity() {
+        return (getIntent().getFlags() & Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) != 0 || isTaskRoot();
+    }
+
     @Nullable
     protected Handler getUpdateUserInfoHandler() {
         return null;
@@ -98,9 +105,9 @@ public abstract class AbstractBottomNavigationActivity extends AbstractActionBar
                 startActivity(CacheListActivity.getHistoryIntent(this));
             } else {
                 Settings.setLastDisplayedList(selectedListId);
-                CacheListActivity.startActivityOffline(this);
+                startActivity(CacheListActivity.getActivityOfflineIntent(this).addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
             }
-            ActivityMixin.finishWithFadeTransition(this);
+            ActivityMixin.overrideTransitionToFade(this);
         }, false, PseudoList.NEW_LIST.id);
         return true;
     }
@@ -122,10 +129,18 @@ public abstract class AbstractBottomNavigationActivity extends AbstractActionBar
     @Override
     protected void onResume() {
         super.onResume();
+        updateSelectedItemId();
         registerLoginIssueHandler(loginHandler, getUpdateUserInfoHandler(), this::onLoginIssue);
         registerReceiver(connectivityChangeReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
     }
 
+    @Override
+    protected void onNewIntent(final Intent intent) {
+        super.onNewIntent(intent);
+        // avoid weired transitions
+        ActivityMixin.overrideTransitionToFade(this);
+    }
+
     @Override
     public void setContentView(final int layoutResID) {
         final View view = getLayoutInflater().inflate(layoutResID, null);
@@ -136,6 +151,8 @@ public abstract class AbstractBottomNavigationActivity extends AbstractActionBar
     public void onBackPressed() {
         if (!isTaskRoot() || doubleBackToExitPressedOnce) {
             super.onBackPressed();
+            // avoid weired transitions
+            ActivityMixin.overrideTransitionToFade(this);
             return;
         }
 
@@ -144,26 +161,21 @@ public abstract class AbstractBottomNavigationActivity extends AbstractActionBar
         new Handler().postDelayed(() -> doubleBackToExitPressedOnce = false, 2000);
     }
 
-    /**
-     * WARNING: This triggers {@link AbstractBottomNavigationActivity#onNavigationItemSelected} while changing the current selection.
-     */
     private void updateSelectedItemId() {
+        // unregister listener as it would otherwise directly trigger the listener
+        ((NavigationBarView) wrapper.activityBottomNavigation).setOnItemSelectedListener(null);
+
         final int menuId = getSelectedBottomItemId();
-        final MenuItem workaroundItem = ((NavigationBarView) wrapper.activityBottomNavigation).getMenu().getItem(0);
 
-        // According to material design guidelines, bottom navigation activities always should be top-level.
-        // Therefore hiding the bottom navigation to not confuse the users.
-        if (menuId == MENU_HIDE_BOTTOM_NAVIGATION || !isTaskRoot()) {
+        if (menuId == MENU_HIDE_BOTTOM_NAVIGATION || !isTopLevelBottomNavActivity()) {
             wrapper.activityBottomNavigation.setVisibility(View.GONE);
-        } else if (menuId == MENU_NOTHING_SELECTED) {
-            wrapper.activityBottomNavigation.setVisibility(View.VISIBLE);
-            ((NavigationBarView) wrapper.activityBottomNavigation).setSelectedItemId(workaroundItem.getItemId());
-            workaroundItem.setCheckable(false);
         } else {
             wrapper.activityBottomNavigation.setVisibility(View.VISIBLE);
             ((NavigationBarView) wrapper.activityBottomNavigation).setSelectedItemId(menuId);
-            workaroundItem.setCheckable(true);
         }
+
+        // re-register the listener
+        ((NavigationBarView) wrapper.activityBottomNavigation).setOnItemSelectedListener(this);
     }
 
     /**
@@ -182,6 +194,7 @@ public abstract class AbstractBottomNavigationActivity extends AbstractActionBar
 
         if (id == getSelectedBottomItemId()) {
             onNavigationItemReselected(item);
+            ActivityMixin.overrideTransitionToFade(this);
             return true;
         }
         return onNavigationItemSelectedIgnoreReselected(item);
@@ -191,20 +204,20 @@ public abstract class AbstractBottomNavigationActivity extends AbstractActionBar
         final int id = item.getItemId();
 
         if (id == MENU_MAP) {
-            startActivity(DefaultMap.getLiveMapIntent(this));
+            startActivity(DefaultMap.getLiveMapIntent(this).addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
         } else if (id == MENU_LIST) {
-            CacheListActivity.startActivityOffline(this);
+            startActivity(CacheListActivity.getActivityOfflineIntent(this).addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
         } else if (id == MENU_SEARCH) {
-            startActivity(new Intent(this, SearchActivity.class));
+            startActivity(new Intent(this, SearchActivity.class).addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
         } else if (id == MENU_NEARBY) {
-            startActivity(CacheListActivity.getNearestIntent(this));
+            startActivity(CacheListActivity.getNearestIntent(this).addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
         } else if (id == MENU_HOME) {
-            startActivity(new Intent(this, MainActivity.class));
+            startActivity(new Intent(this, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); // the different behaviour for MainActivity is intended
         } else {
             throw new IllegalStateException("unknown navigation item selected"); // should never happen
         }
         // avoid weired transitions
-        ActivityMixin.finishWithFadeTransition(this);
+        ActivityMixin.overrideTransitionToFade(this);
         return true;
     }
 

keep activities alive in background (rel. to #12317 / #11926)