From d1851bcc5a22c5d5635a551ccd863bd12b345ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Wed, 25 Feb 2026 01:56:03 -0300 Subject: [PATCH] refactor: delegate navigation to controller and helper --- .../tempo/ui/activity/MainActivity.java | 107 ++++------- .../activity/base/NavigationController.java | 12 ++ .../ui/activity/base/NavigationDelegate.java | 48 +++++ .../ui/activity/base/NavigationHelper.java | 178 ++++++++++++++++++ 4 files changed, 271 insertions(+), 74 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationController.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationDelegate.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationHelper.java diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java index f872b403..fd17a966 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java @@ -10,7 +10,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; -import android.view.Gravity; import android.view.View; import android.widget.FrameLayout; @@ -39,6 +38,9 @@ import com.cappielloantonio.tempo.databinding.ActivityMainBinding; import com.cappielloantonio.tempo.github.utils.UpdateUtil; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.ui.activity.base.BaseActivity; +import com.cappielloantonio.tempo.ui.activity.base.NavigationController; +import com.cappielloantonio.tempo.ui.activity.base.NavigationDelegate; +import com.cappielloantonio.tempo.ui.activity.base.NavigationHelper; import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog; import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog; import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog; @@ -62,6 +64,7 @@ public class MainActivity extends BaseActivity { private static final String TAG = "MainActivityLogs"; public ActivityMainBinding bind; + private NavigationHelper navigationHelper; private MainViewModel mainViewModel; private FragmentManager fragmentManager; @@ -71,7 +74,7 @@ public class MainActivity extends BaseActivity { public NavController navController; private DrawerLayout drawerLayout; private NavigationView navigationView; - private BottomSheetBehavior bottomSheetBehavior; + public BottomSheetBehavior bottomSheetBehavior; public boolean isLandscape = false; private AssetLinkNavigator assetLinkNavigator; private AssetLinkUtil.AssetLink pendingAssetLink; @@ -79,6 +82,10 @@ public class MainActivity extends BaseActivity { ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver; private Intent pendingDownloadPlaybackIntent; + public ActivityMainBinding getBinding() { + return bind; + } + @Override protected void onCreate(Bundle savedInstanceState) { SplashScreen.installSplashScreen(this); @@ -126,6 +133,10 @@ public class MainActivity extends BaseActivity { super.onDestroy(); connectivityStatusReceiverManager(false); bind = null; + NavigationDelegate.getInstance().unbind(); + if (navigationHelper != null) { + navigationHelper.release(); + } } @Override @@ -148,7 +159,16 @@ public class MainActivity extends BaseActivity { fragmentManager = getSupportFragmentManager(); initBottomSheet(); - initNavigation(); + + // All of the navigation stuff, contained here + NavigationDelegate.getInstance().bind(this); + navigationHelper = NavigationHelper.init(this); + + // This is for "backward compatibility" with old code + navController = navigationHelper.getNavController(); + bottomNavigationView = navigationHelper.getBottomNavigationView(); + bottomNavigationViewFrame = navigationHelper.getBottomNavigationViewFrame(); + drawerLayout = navigationHelper.getDrawerLayout(); if (Preferences.getPassword() != null || (Preferences.getToken() != null && Preferences.getSalt() != null)) { goFromLogin(); @@ -259,103 +279,42 @@ public class MainActivity extends BaseActivity { bind.bottomNavigation.setTranslationY(slideY); } - private void initNavigation() { - bottomNavigationView = findViewById(R.id.bottom_navigation); - bottomNavigationViewFrame = findViewById(R.id.bottom_navigation_frame); - navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.nav_host_fragment); - navController = Objects.requireNonNull(navHostFragment).getNavController(); - // This is the lateral slide-in drawer - drawerLayout = findViewById(R.id.drawer_layout); - navigationView = findViewById(R.id.nav_view); - - /* - * In questo modo intercetto il cambio schermata tramite navbar e se il bottom sheet รจ aperto, - * lo chiudo - */ - navController.addOnDestinationChangedListener((controller, destination, arguments) -> { - if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED && ( - destination.getId() == R.id.homeFragment || - destination.getId() == R.id.libraryFragment || - destination.getId() == R.id.downloadFragment) - ) { - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } - }); - - NavigationUI.setupWithNavController(bottomNavigationView, navController); - NavigationUI.setupWithNavController(navigationView, navController); - } - public void setBottomNavigationBarVisibility(boolean visibility) { - if (visibility) { - bottomNavigationView.setVisibility(View.VISIBLE); - bottomNavigationViewFrame.setVisibility(View.VISIBLE); - } else { - bottomNavigationView.setVisibility(View.GONE); - bottomNavigationViewFrame.setVisibility(View.GONE); - } + navigationHelper.setBottomNavigationBarVisibility(visibility); } public void toggleBottomNavigationBarVisibilityOnOrientationChange() { // Ignore orientation change, bottom navbar always hidden if (Preferences.getHideBottomNavbarOnPortrait()) { - setBottomNavigationBarVisibility(false); + navigationHelper.setBottomNavigationBarVisibility(false); setPortraitPlayerBottomSheetPeekHeight(56); - setSystemBarsVisibility(!isLandscape); + navigationHelper.setSystemBarsVisibility(this, !isLandscape); return; } if (!isLandscape) { // Show app navbar + show system bars setPortraitPlayerBottomSheetPeekHeight(136); - setBottomNavigationBarVisibility(true); - setSystemBarsVisibility(true); + navigationHelper.setBottomNavigationBarVisibility(true); + navigationHelper.setSystemBarsVisibility(this, true); } else { // Hide app navbar + hide system bars setPortraitPlayerBottomSheetPeekHeight(56); - setBottomNavigationBarVisibility(false); - setSystemBarsVisibility(false); + navigationHelper.setBottomNavigationBarVisibility(false); + navigationHelper.setSystemBarsVisibility(this, false); } } public void setNavigationDrawerLock(boolean locked) { - int mode = locked - ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED - : DrawerLayout.LOCK_MODE_UNLOCKED; - drawerLayout.setDrawerLockMode(mode); + navigationHelper.setNavigationDrawerLock(locked); } public void toggleNavigationDrawerLockOnOrientationChange() { - // Ignore orientation check, drawer always unlocked - if (Preferences.getEnableDrawerOnPortrait()) { - setNavigationDrawerLock(false); - return; - } - if (!isLandscape) { - setNavigationDrawerLock(true); - } else { - setNavigationDrawerLock(false); - } + navigationHelper.toggleNavigationDrawerLockOnOrientationChange(this, isLandscape); } public void setSystemBarsVisibility(boolean visibility) { - WindowInsetsControllerCompat insetsController; - View decorView = getWindow().getDecorView(); - insetsController = new WindowInsetsControllerCompat(getWindow(), decorView); - - if (visibility) { - WindowCompat.setDecorFitsSystemWindows(getWindow(), true); - insetsController.show(WindowInsetsCompat.Type.navigationBars()); - insetsController.show(WindowInsetsCompat.Type.statusBars()); - insetsController.setSystemBarsBehavior( - WindowInsetsControllerCompat.BEHAVIOR_DEFAULT); - } else { - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - insetsController.hide(WindowInsetsCompat.Type.navigationBars()); - insetsController.hide(WindowInsetsCompat.Type.statusBars()); - insetsController.setSystemBarsBehavior( - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - } + navigationHelper.setSystemBarsVisibility(this, visibility); } private void setPortraitPlayerBottomSheetPeekHeight(int peekHeight) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationController.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationController.java new file mode 100644 index 00000000..bc0b681e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationController.java @@ -0,0 +1,12 @@ +package com.cappielloantonio.tempo.ui.activity.base; + +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; + +public interface NavigationController { + + @OptIn(markerClass = UnstableApi.class) + default boolean isVisible() { + return NavigationDelegate.isVisible(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationDelegate.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationDelegate.java new file mode 100644 index 00000000..a241b874 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationDelegate.java @@ -0,0 +1,48 @@ +package com.cappielloantonio.tempo.ui.activity.base; + +import android.view.View; + +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.databinding.ActivityMainBinding; +import com.cappielloantonio.tempo.ui.activity.MainActivity; + +/* + The goal of this class is to stop instanciating MainActivity on each fragment */ +@UnstableApi +public final class NavigationDelegate { + + private static final NavigationDelegate INSTANCE = new NavigationDelegate(); + + private MainActivity activity; + private boolean visible = true; + + private NavigationDelegate() {} + + public static NavigationDelegate getInstance() { + return INSTANCE; + } + + /* Call inside onCreate() in MainActivity giving as argument `this` */ + public void bind(MainActivity activity) { + this.activity = activity; + } + + /* Call inside onDestroy() in MainActivity*/ + public void unbind() { + this.activity = null; + } + + public static boolean isVisible() { + return INSTANCE.visible; + } + + /** Change visibility and update the UI on the UI thread. */ + public void setVisibility(final boolean visible) { + this.visible = visible; + ActivityMainBinding bind = activity.getBinding(); + bind.bottomNavigation.setVisibility(visible + ? View.VISIBLE + : View.GONE); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationHelper.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationHelper.java new file mode 100644 index 00000000..f64e6bd8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationHelper.java @@ -0,0 +1,178 @@ +package com.cappielloantonio.tempo.ui.activity.base; + +import android.content.Context; +import android.view.View; +import android.view.Window; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; +import androidx.navigation.ui.NavigationUI; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.navigation.NavigationView; + +import java.util.Objects; + +public class NavigationHelper { + /* UI components */ + private BottomNavigationView bottomNavigationView; + private FrameLayout bottomNavigationViewFrame; + private DrawerLayout drawerLayout; + + /* Navigation components */ + private NavigationView navigationView; + private NavHostFragment navHostFragment; + private NavController navController; + + /* States that need to be remembered */ + private final Context context; + + /* Private constructor */ + private NavigationHelper(@NonNull Context context) { + this.context = context.getApplicationContext(); + } + + /* Call inside onCreate() in MainActivity giving as argument `this` */ + @OptIn(markerClass = UnstableApi.class) + public static NavigationHelper init(@NonNull MainActivity activity) { + NavigationHelper helper = new NavigationHelper(activity); + helper.bindViews(activity); + helper.setupNavigation(activity); + return helper; + } + + /* Call inside onDestroy() in MainActivity*/ + public void release() { + bottomNavigationView = null; + bottomNavigationViewFrame = null; + drawerLayout = null; + navigationView = null; + navHostFragment = null; + navController = null; + } + + /* Bind the views by finding them on the layout (XML id attr) */ + @OptIn(markerClass = UnstableApi.class) + private void bindViews(@NonNull MainActivity activity) { + bottomNavigationView = activity.findViewById(R.id.bottom_navigation); + bottomNavigationViewFrame = activity.findViewById(R.id.bottom_navigation_frame); + drawerLayout = activity.findViewById(R.id.drawer_layout); + navigationView = activity.findViewById(R.id.nav_view); + + navHostFragment = (NavHostFragment) activity + .getSupportFragmentManager() + .findFragmentById(R.id.nav_host_fragment); + navController = Objects.requireNonNull(navHostFragment).getNavController(); + } + + /* The navigation graph (untouched original implementation) */ + @OptIn(markerClass = UnstableApi.class) + private void setupNavigation(@NonNull MainActivity activity) { + navController.addOnDestinationChangedListener( + (controller, destination, arguments) -> { + int destId = destination.getId(); + boolean isTarget = destId == R.id.homeFragment || + destId == R.id.libraryFragment || + destId == R.id.downloadFragment; + + if (isTarget && + activity.bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { + activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + + NavigationUI.setupWithNavController(bottomNavigationView, navController); + NavigationUI.setupWithNavController(navigationView, navController); + } + + /* + Clean public methods + Removes the need to invoke the activity on the fragment + */ + + public void setBottomNavigationBarVisibility(boolean visible) { + int visibility = visible + ? View.VISIBLE + : View.GONE; + bottomNavigationView.setVisibility(visibility); + bottomNavigationViewFrame.setVisibility(visibility); + } + + public void setNavigationDrawerLock(boolean locked) { + int mode = locked + ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED + : DrawerLayout.LOCK_MODE_UNLOCKED; + drawerLayout.setDrawerLockMode(mode); + } + + public void toggleNavigationDrawerLockOnOrientationChange(MainActivity activity, boolean isLandscape) { + if (Preferences.getEnableDrawerOnPortrait()) { + setNavigationDrawerLock(false); + return; + } + setNavigationDrawerLock(!isLandscape); + } + + /* + All of these are the "backward compatible" changes that don't break the assumption + that everything was defined on the activity and is gobally available + */ + + @NonNull + public NavController getNavController() { + return navController; + } + + @NonNull + public BottomNavigationView getBottomNavigationView() { + return bottomNavigationView; + } + + @NonNull + public FrameLayout getBottomNavigationViewFrame() { + return bottomNavigationViewFrame; + } + + @NonNull + public DrawerLayout getDrawerLayout() { + return drawerLayout; + } + + /* + Auxiliar functions, could be moved somewhere else + */ + + @OptIn(markerClass = UnstableApi.class) + public void setSystemBarsVisibility(MainActivity activity, boolean visibility) { + WindowInsetsControllerCompat insetsController; + Window window = activity.getWindow(); + View decorView = window.getDecorView(); + insetsController = new WindowInsetsControllerCompat(window, decorView); + + if (visibility) { + WindowCompat.setDecorFitsSystemWindows(window, true); + insetsController.show(WindowInsetsCompat.Type.navigationBars()); + insetsController.show(WindowInsetsCompat.Type.statusBars()); + insetsController.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_DEFAULT); + } else { + WindowCompat.setDecorFitsSystemWindows(window, false); + insetsController.hide(WindowInsetsCompat.Type.navigationBars()); + insetsController.hide(WindowInsetsCompat.Type.statusBars()); + insetsController.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + } +}