diff --git a/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationController.java b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationController.java new file mode 100644 index 00000000..c190cc9e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationController.java @@ -0,0 +1,44 @@ +package com.cappielloantonio.tempo.navigation; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.navigation.NavController; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; + +public class NavigationController { + + NavigationHelper helper; + + public NavigationController(@NonNull NavigationHelper helper) { + this.helper = helper; + } + + public void syncWithBottomSheetBehavior(BottomSheetBehavior bottomSheetBehavior, + NavController navController) { + helper.syncWithBottomSheetBehavior(bottomSheetBehavior, navController); + + } + + public void setNavbarVisibility(boolean visibility) { + helper.setBottomNavigationBarVisibility(visibility); + } + + public void setDrawerLock(boolean visibility) { + helper.setNavigationDrawerLock(visibility); + } + + public boolean isNavigationDrawerLocked() { + return helper.isNavigationDrawerLocked(); + } + + public void toggleDrawerLockOnOrientation(AppCompatActivity activity) { + helper.toggleNavigationDrawerLockOnOrientationChange(activity); + } + + public void setSystemBarsVisibility(AppCompatActivity activity, boolean visibility) { + helper.setSystemBarsVisibility(activity, visibility); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationHelper.java b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationHelper.java new file mode 100644 index 00000000..fe47f82a --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationHelper.java @@ -0,0 +1,167 @@ +package com.cappielloantonio.tempo.navigation; + +import android.content.res.Configuration; +import android.view.View; +import android.view.Window; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatActivity; +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.NavDestination; +import androidx.navigation.fragment.NavHostFragment; +import androidx.navigation.ui.NavigationUI; + +import com.cappielloantonio.tempo.R; +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 org.jetbrains.annotations.Contract; + +public class NavigationHelper { + /* UI components */ + private BottomNavigationView bottomNavigationView; + private FrameLayout bottomNavigationViewFrame; + private DrawerLayout drawerLayout; + + /* Navigation components */ + private NavigationView navigationView; + private NavHostFragment navHostFragment; + + /* States that need to be remembered */ + // -- // + + /* Private constructor */ + public NavigationHelper(@NonNull BottomNavigationView bottomNavigationView, + @NonNull FrameLayout bottomNavigationViewFrame, + @NonNull DrawerLayout drawerLayout, + @NonNull NavigationView navigationView, + @NonNull NavHostFragment navHostFragment) { + this.bottomNavigationView = bottomNavigationView; + this.bottomNavigationViewFrame = bottomNavigationViewFrame; + this.drawerLayout = drawerLayout; + this.navigationView = navigationView; + this.navHostFragment = navHostFragment; + } + + public void syncWithBottomSheetBehavior(@NonNull BottomSheetBehavior bottomSheetBehavior, + @NonNull NavController navController) { + navController.addOnDestinationChangedListener( + (controller, destination, arguments) -> { + // React to the user clicking one of these on bottom-navbar/drawer + boolean isTarget = isTargetDestination(destination); + int currentState = bottomSheetBehavior.getState(); + + if (isTarget && currentState == BottomSheetBehavior.STATE_EXPANDED) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + + NavigationUI.setupWithNavController(bottomNavigationView, navController); + NavigationUI.setupWithNavController(navigationView, navController); + } + + @Contract(pure = true) + private static boolean isTargetDestination(NavDestination destination) { + int destId = destination.getId(); + return destId == R.id.homeFragment || + destId == R.id.libraryFragment || + destId == R.id.downloadFragment || + destId == R.id.albumCatalogueFragment || + destId == R.id.artistCatalogueFragment || + destId == R.id.genreCatalogueFragment || + destId == R.id.playlistCatalogueFragment; + } + + /* + 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 boolean isNavigationDrawerLocked() { + return drawerLayout.getDrawerLockMode(navigationView) != DrawerLayout.LOCK_MODE_UNLOCKED; + } + + @OptIn(markerClass = UnstableApi.class) + public void toggleNavigationDrawerLockOnOrientationChange( + AppCompatActivity activity) { + + int orientation = activity.getResources().getConfiguration().orientation; + boolean isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE; + + 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 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(AppCompatActivity 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); + } + } +} 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 b69f9581..4c77303f 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 @@ -4,23 +4,16 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; -import android.graphics.Rect; -import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.NetworkInfo; 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; import androidx.annotation.NonNull; import androidx.core.splashscreen.SplashScreen; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.core.view.WindowInsetsControllerCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; @@ -31,7 +24,6 @@ import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; import androidx.navigation.NavController; import androidx.navigation.fragment.NavHostFragment; -import androidx.navigation.ui.NavigationUI; import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.BuildConfig; @@ -39,8 +31,12 @@ import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.broadcast.receiver.ConnectivityStatusBroadcastReceiver; import com.cappielloantonio.tempo.databinding.ActivityMainBinding; import com.cappielloantonio.tempo.github.utils.UpdateUtil; +import com.cappielloantonio.tempo.navigation.NavigationController; +import com.cappielloantonio.tempo.navigation.NavigationHelper; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.ui.activity.base.BaseActivity; +import com.cappielloantonio.tempo.ui.controller.BottomSheetController; +import com.cappielloantonio.tempo.ui.controller.BottomSheetHelper; import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog; import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog; import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog; @@ -70,10 +66,12 @@ public class MainActivity extends BaseActivity { private NavHostFragment navHostFragment; private BottomNavigationView bottomNavigationView; private FrameLayout bottomNavigationViewFrame; - public NavController navController; private DrawerLayout drawerLayout; private NavigationView navigationView; - private BottomSheetBehavior bottomSheetBehavior; + public NavController navController; + private NavigationController navigationController; + private BottomSheetController bottomSheetController; + public BottomSheetBehavior bottomSheetBehavior; public boolean isLandscape = false; private AssetLinkNavigator assetLinkNavigator; private AssetLinkUtil.AssetLink pendingAssetLink; @@ -81,6 +79,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); @@ -147,7 +149,6 @@ public class MainActivity extends BaseActivity { } public void init() { - fragmentManager = getSupportFragmentManager(); initBottomSheet(); initNavigation(); @@ -162,49 +163,74 @@ public class MainActivity extends BaseActivity { } - // BOTTOM SHEET/NAVIGATION - private void initBottomSheet() { - bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.player_bottom_sheet)); - bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback); - fragmentManager.beginTransaction().replace(R.id.player_bottom_sheet, new PlayerBottomSheetFragment(), "PlayerBottomSheet").commit(); + private void initNavigation() { + // We link the nav_graph.xml with our navigationController + NavHostFragment navHostFragment = (NavHostFragment) this + .getSupportFragmentManager() + .findFragmentById(R.id.nav_host_fragment); + navController = Objects.requireNonNull(navHostFragment).getNavController(); + /* + navController is currently global since some legacy code still invokes it directly + the MainActivity methods that use it must be converted to NavigationHelper methods + */ - checkBottomSheetAfterStateChanged(); + // Helper + NavigationHelper navigationHelper = + new NavigationHelper( + findViewById(R.id.bottom_navigation), + findViewById(R.id.bottom_navigation_frame), + findViewById(R.id.drawer_layout), + findViewById(R.id.nav_view), + navHostFragment + ); + + // Controller + navigationController = new NavigationController(navigationHelper); + navigationController.syncWithBottomSheetBehavior(bottomSheetBehavior, navController); + } + + private void initBottomSheet() { + FragmentManager fragmentManager = getSupportFragmentManager(); + View bottomSheetView = findViewById(R.id.player_bottom_sheet); + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetView); + /* + bottomSheetBehavior is currently global since some legacy code still invokes it directly + the MainActivity methods that use it must be converted to BottomSheetHelper methods + */ + + // Helper + BottomSheetHelper bottomSheetHelper = + new BottomSheetHelper( + bottomSheetBehavior, + bottomSheetView, + fragmentManager + ); + + // Controller + bottomSheetController = new BottomSheetController(bottomSheetHelper); + bottomSheetController.addCallback(bottomSheetCallback); + bottomSheetController.replaceFragment(R.id.player_bottom_sheet); + bottomSheetController.checkAfterStateChanged(mainViewModel); } public void setBottomSheetInPeek(Boolean isVisible) { - if (isVisible) { - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } else { - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } + bottomSheetController.setStateInPeek(isVisible); } public void setBottomSheetVisibility(boolean visibility) { - if (visibility) { - findViewById(R.id.player_bottom_sheet).setVisibility(View.VISIBLE); - } else { - findViewById(R.id.player_bottom_sheet).setVisibility(View.GONE); - } - } - - private void checkBottomSheetAfterStateChanged() { - final Handler handler = new Handler(); - final Runnable runnable = () -> setBottomSheetInPeek(mainViewModel.isQueueLoaded()); - handler.postDelayed(runnable, 100); + bottomSheetController.setVisibility(visibility); } public void collapseBottomSheetDelayed() { - final Handler handler = new Handler(); - final Runnable runnable = () -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - handler.postDelayed(runnable, 100); + bottomSheetController.collapseDelayed(); } public void expandBottomSheet() { - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + bottomSheetController.expand(); } public void setBottomSheetDraggableState(Boolean isDraggable) { - bottomSheetBehavior.setDraggable(isDraggable); + bottomSheetController.setDraggable(isDraggable); } private final BottomSheetBehavior.BottomSheetCallback bottomSheetCallback = @@ -217,7 +243,7 @@ public class MainActivity extends BaseActivity { switch (state) { case BottomSheetBehavior.STATE_HIDDEN: - resetMusicSession(); + resetMusicSession(); // I can't put the callback inside BottomSheetHelper because of this line break; case BottomSheetBehavior.STATE_COLLAPSED: if (playerBottomSheetFragment != null) @@ -241,12 +267,7 @@ public class MainActivity extends BaseActivity { }; private void animateBottomSheet(float slideOffset) { - PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet"); - if (playerBottomSheetFragment != null) { - float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f; - playerBottomSheetFragment.getPlayerHeader().setAlpha(1 - condensedSlideOffset); - playerBottomSheetFragment.getPlayerHeader().setVisibility(condensedSlideOffset > 0.99 ? View.GONE : View.VISIBLE); - } + bottomSheetController.animate(slideOffset); } private void animateBottomNavigation(float slideOffset, int navigationHeight) { @@ -261,117 +282,56 @@ 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 || - destination.getId() == R.id.albumCatalogueFragment || - destination.getId() == R.id.artistCatalogueFragment || - destination.getId() == R.id.genreCatalogueFragment || - destination.getId() == R.id.playlistCatalogueFragment) - ) { - 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); - } + navigationController.setNavbarVisibility(visibility); } public void toggleBottomNavigationBarVisibilityOnOrientationChange() { + float displayDensity = getResources().getDisplayMetrics().density; // Ignore orientation change, bottom navbar always hidden if (Preferences.getHideBottomNavbarOnPortrait()) { - setBottomNavigationBarVisibility(false); - setPortraitPlayerBottomSheetPeekHeight(56); - setSystemBarsVisibility(!isLandscape); + navigationController.setNavbarVisibility(false); + bottomSheetController.setPeekHeight(56, displayDensity); + navigationController.setSystemBarsVisibility(this, !isLandscape); return; } if (!isLandscape) { // Show app navbar + show system bars - setPortraitPlayerBottomSheetPeekHeight(136); - setBottomNavigationBarVisibility(true); - setSystemBarsVisibility(true); + bottomSheetController.setPeekHeight(136, displayDensity); + navigationController.setNavbarVisibility(true); + navigationController.setSystemBarsVisibility(this, true); } else { // Hide app navbar + hide system bars - setPortraitPlayerBottomSheetPeekHeight(56); - setBottomNavigationBarVisibility(false); - setSystemBarsVisibility(false); + bottomSheetController.setPeekHeight(56, displayDensity); + navigationController.setNavbarVisibility(false); + navigationController.setSystemBarsVisibility(this, false); } } public void setNavigationDrawerLock(boolean locked) { - int mode = locked - ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED - : DrawerLayout.LOCK_MODE_UNLOCKED; - drawerLayout.setDrawerLockMode(mode); + navigationController.setDrawerLock(locked); + } + + public boolean isNavigationDrawerLocked() { + return navigationController.isNavigationDrawerLocked(); } public void toggleNavigationDrawerLockOnOrientationChange() { - // Ignore orientation check, drawer always unlocked - if (Preferences.getEnableDrawerOnPortrait()) { - setNavigationDrawerLock(false); - return; - } - if (!isLandscape) { - setNavigationDrawerLock(true); - } else { - setNavigationDrawerLock(false); - } + navigationController.toggleDrawerLockOnOrientation(this); } 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); - } + navigationController.setSystemBarsVisibility(this, visibility); } - private void setPortraitPlayerBottomSheetPeekHeight(int peekHeight) { - FrameLayout bottomSheet = findViewById(R.id.player_bottom_sheet); - BottomSheetBehavior behavior = - BottomSheetBehavior.from(bottomSheet); - - int newPeekPx = (int) (peekHeight * getResources().getDisplayMetrics().density); - behavior.setPeekHeight(newPeekPx); - } + /* + There are only 4 init functions that must exist up to here + 1. init() + 2. initNavigation() + 3. initBottomSheet() + 4. bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() { ... } + */ private void initService() { MediaManager.check(getMediaBrowserListenableFuture()); @@ -407,7 +367,7 @@ public class MainActivity extends BaseActivity { } private void goToHome() { - bottomNavigationView.setVisibility(View.VISIBLE); + setBottomNavigationBarVisibility(true); if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment) { navController.navigate(R.id.action_landingFragment_to_homeFragment); @@ -657,4 +617,4 @@ public class MainActivity extends BaseActivity { MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/controller/BottomSheetController.java b/app/src/main/java/com/cappielloantonio/tempo/ui/controller/BottomSheetController.java new file mode 100644 index 00000000..9ebff6d9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/controller/BottomSheetController.java @@ -0,0 +1,63 @@ +package com.cappielloantonio.tempo.ui.controller; + +import androidx.annotation.NonNull; + +import com.cappielloantonio.tempo.viewmodel.MainViewModel; +import com.google.android.material.bottomsheet.BottomSheetBehavior; + +public class BottomSheetController { + + BottomSheetHelper helper; + + public BottomSheetController(@NonNull BottomSheetHelper bottomSheetPlayerHelper) { + this.helper = bottomSheetPlayerHelper; + } + + public void expand() { + helper.setState(BottomSheetBehavior.STATE_EXPANDED); + } + + public void hide() { + helper.setState(BottomSheetBehavior.STATE_HIDDEN); + } + + public void setStateInPeek(boolean isVisible) { + helper.setStateInPeek(isVisible); + } + + public void setVisibility(boolean visibility) { + helper.setVisibility(visibility); + } + + public void addCallback(BottomSheetBehavior.BottomSheetCallback callback) { + helper.addCallback(callback); + } + + public void replaceFragment(int playerBottomSheet) { + helper.replaceFragment(playerBottomSheet); + } + + public void checkAfterStateChanged(MainViewModel mainViewModel) { + helper.checkAfterStateChanged(mainViewModel); + } + + public void collapseDelayed() { + helper.collapseDelayed(); + } + + public void setDraggable(Boolean isDraggable) { + helper.setDraggable(isDraggable); + } + + public int getState() { + return helper.getState(); + } + + public void animate(float slideOffset) { + helper.animate(slideOffset); + } + + public void setPeekHeight(int peekHeight, float displayDensity) { + helper.setPeekHeight(peekHeight, displayDensity); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/controller/BottomSheetHelper.java b/app/src/main/java/com/cappielloantonio/tempo/ui/controller/BottomSheetHelper.java new file mode 100644 index 00000000..a0c88938 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/controller/BottomSheetHelper.java @@ -0,0 +1,97 @@ +package com.cappielloantonio.tempo.ui.controller; + +import android.os.Handler; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment; +import com.cappielloantonio.tempo.viewmodel.MainViewModel; +import com.google.android.material.bottomsheet.BottomSheetBehavior; + +public class BottomSheetHelper { + + BottomSheetBehavior bottomSheetBehavior; + View bottomSheetView; + FragmentManager fragmentManager; // Of the entire activity + PlayerBottomSheetFragment playerBottomSheetFragment; + + public void setState(int state) { + bottomSheetBehavior.setState(state); + } + + public BottomSheetHelper(@NonNull BottomSheetBehavior bottomSheetBehavior, + @NonNull View bottomSheetView, + @NonNull FragmentManager fragmentManager) { + this.bottomSheetBehavior = bottomSheetBehavior; + this.bottomSheetView = bottomSheetView; + this.fragmentManager = fragmentManager; + this.playerBottomSheetFragment = new PlayerBottomSheetFragment(); + } + + public void addCallback(BottomSheetBehavior.BottomSheetCallback callback) { + bottomSheetBehavior.addBottomSheetCallback(callback); + } + + public void setStateInPeek(boolean isVisible) { + if (isVisible) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } else { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + } + + public void setVisibility(boolean visibility) { + if (visibility) { + bottomSheetView.setVisibility(View.VISIBLE); + } else { + bottomSheetView.setVisibility(View.GONE); + } + } + + public void replaceFragment(int playerBottomSheet) { + fragmentManager + .beginTransaction() + .replace( + playerBottomSheet, + playerBottomSheetFragment, + "PlayerBottomSheet") + .commit(); + } + + public void checkAfterStateChanged(MainViewModel mainViewModel) { + final Handler handler = new Handler(); + final Runnable runnable = () -> setStateInPeek(mainViewModel.isQueueLoaded()); + handler.postDelayed(runnable, 100); + } + + public void collapseDelayed() { + final Handler handler = new Handler(); + final Runnable runnable = () -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + handler.postDelayed(runnable, 100); + } + + public void setDraggable(Boolean isDraggable) { + bottomSheetBehavior.setDraggable((isDraggable)); + } + + public int getState() { + return bottomSheetBehavior.getState(); + } + + public void animate(float slideOffset) { + if (playerBottomSheetFragment != null) { + float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f; + playerBottomSheetFragment.getPlayerHeader().setAlpha(1 - condensedSlideOffset); + playerBottomSheetFragment.getPlayerHeader().setVisibility(condensedSlideOffset > 0.99 ? View.GONE : View.VISIBLE); + } + } + + public void setPeekHeight(int peekHeight, float displayDensity) { + int newPeekPx = (int) (peekHeight * displayDensity); + bottomSheetBehavior.setPeekHeight(newPeekPx); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java new file mode 100644 index 00000000..e770976a --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java @@ -0,0 +1,603 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.audiofx.AudioEffect; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.text.InputFilter; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.os.LocaleListCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.NavController; +import androidx.navigation.NavOptions; +import androidx.navigation.fragment.NavHostFragment; +import androidx.preference.EditTextPreference; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreference; + +import com.cappielloantonio.tempo.BuildConfig; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.helper.ThemeHelper; +import com.cappielloantonio.tempo.interfaces.DialogClickCallback; +import com.cappielloantonio.tempo.interfaces.ScanCallback; +import com.cappielloantonio.tempo.service.EqualizerManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog; +import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.util.UIUtil; +import com.cappielloantonio.tempo.viewmodel.SettingViewModel; + +import java.util.Locale; +import java.util.Map; + +@OptIn(markerClass = UnstableApi.class) +public class SettingsContainerFragment extends PreferenceFragmentCompat { + + private static final String TAG = "SettingsFragment"; + private MainActivity activity; + + private SettingViewModel settingViewModel; + + private ActivityResultLauncher directoryPickerLauncher; + + private MediaService.LocalBinder mediaServiceBinder; + private boolean isServiceBound = false; + private ActivityResultLauncher equalizerResultLauncher; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + equalizerResultLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> {} + ); + + if (!BuildConfig.FLAVOR.equals("tempus")) { + PreferenceCategory githubUpdateCategory = findPreference("settings_github_update_category_key"); + if (githubUpdateCategory != null) { + getPreferenceScreen().removePreference(githubUpdateCategory); + } + } + + directoryPickerLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + Intent data = result.getData(); + if (data != null) { + Uri uri = data.getData(); + if (uri != null) { + requireContext().getContentResolver().takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ); + + Preferences.setDownloadDirectoryUri(uri.toString()); + ExternalAudioReader.refreshCache(); + Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show(); + checkDownloadDirectory(); + } + } + } + }); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + View view = super.onCreateView(inflater, container, savedInstanceState); + settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class); + + if (view != null) { + getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom)); + } + + return view; + } + + @Override + public void onStart() { + super.onStart(); + activity.setBottomNavigationBarVisibility(false); + activity.setBottomSheetVisibility(false); + } + + @Override + public void onResume() { + super.onResume(); + + checkSystemEqualizer(); + checkCacheStorage(); + checkStorage(); + checkDownloadDirectory(); + + setStreamingCacheSize(); + setAppLanguage(); + setVersion(); + setNetorkPingTimeoutBase(); + + actionLogout(); + actionScan(); + actionSyncStarredAlbums(); + actionSyncStarredTracks(); + actionSyncStarredArtists(); + actionChangeStreamingCacheStorage(); + actionChangeDownloadStorage(); + actionSetDownloadDirectory(); + actionDeleteDownloadStorage(); + actionKeepScreenOn(); + actionAutoDownloadLyrics(); + actionMiniPlayerHeart(); + + bindMediaService(); + actionAppEqualizer(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.global_preferences, rootKey); + ListPreference themePreference = findPreference(Preferences.THEME); + if (themePreference != null) { + themePreference.setOnPreferenceChangeListener( + (preference, newValue) -> { + String themeOption = (String) newValue; + ThemeHelper.applyTheme(themeOption); + return true; + }); + } + } + + private void checkSystemEqualizer() { + Preference equalizer = findPreference("system_equalizer"); + + if (equalizer == null) return; + + Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); + + if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) { + equalizer.setOnPreferenceClickListener(preference -> { + equalizerResultLauncher.launch(intent); + return true; + }); + } else { + equalizer.setVisible(false); + } + } + + private void checkCacheStorage() { + Preference storage = findPreference("streaming_cache_storage"); + + if (storage == null) return; + + try { + if (requireContext().getExternalFilesDirs(null)[1] == null) { + storage.setVisible(false); + } else { + storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button); + } + } catch (Exception exception) { + storage.setVisible(false); + } + } + + private void checkStorage() { + Preference storage = findPreference("download_storage"); + + if (storage == null) return; + + try { + if (requireContext().getExternalFilesDirs(null)[1] == null) { + storage.setVisible(false); + } else { + int pref = Preferences.getDownloadStoragePreference(); + if (pref == 0) { + storage.setSummary(R.string.download_storage_internal_dialog_negative_button); + } else if (pref == 1) { + storage.setSummary(R.string.download_storage_external_dialog_positive_button); + } else { + storage.setSummary(R.string.download_storage_directory_dialog_neutral_button); + } + } + } catch (Exception exception) { + storage.setVisible(false); + } + } + + private void checkDownloadDirectory() { + Preference storage = findPreference("download_storage"); + Preference directory = findPreference("set_download_directory"); + + if (directory == null) return; + + String current = Preferences.getDownloadDirectoryUri(); + if (current != null) { + if (storage != null) storage.setVisible(false); + directory.setVisible(true); + directory.setIcon(R.drawable.ic_close); + directory.setTitle(R.string.settings_clear_download_folder); + directory.setSummary(current); + } else { + if (storage != null) storage.setVisible(true); + if (Preferences.getDownloadStoragePreference() == 2) { + directory.setVisible(true); + directory.setIcon(R.drawable.ic_folder); + directory.setTitle(R.string.settings_set_download_folder); + directory.setSummary(R.string.settings_choose_download_folder); + } else { + directory.setVisible(false); + } + } + } + + private void setNetorkPingTimeoutBase() { + EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base"); + + if (networkPingTimeoutBase != null) { + networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance()); + networkPingTimeoutBase.setOnBindEditTextListener(editText -> { + editText.setInputType(InputType.TYPE_CLASS_NUMBER); + editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> { + for (int i = start; i < end; i++) { + if (!Character.isDigit(source.charAt(i))) { + return ""; + } + } + return null; + }}); + }); + + networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> { + String input = (String) newValue; + return input != null && !input.isEmpty(); + }); + } + } + + private void setStreamingCacheSize() { + ListPreference streamingCachePreference = findPreference("streaming_cache_size"); + + if (streamingCachePreference != null) { + streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider() { + @Nullable + @Override + public CharSequence provideSummary(@NonNull ListPreference preference) { + CharSequence entry = preference.getEntry(); + + if (entry == null) return null; + + long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024); + + return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb)); + } + }); + } + } + + private void setAppLanguage() { + ListPreference localePref = (ListPreference) findPreference("language"); + + Map locales = UIUtil.getLangPreferenceDropdownEntries(requireContext()); + + CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]); + CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]); + + localePref.setEntries(entries); + localePref.setEntryValues(entryValues); + + String value = localePref.getValue(); + if ("default".equals(value)) { + localePref.setSummary(requireContext().getString(R.string.settings_system_language)); + } else { + localePref.setSummary(Locale.forLanguageTag(value).getDisplayName()); + } + + localePref.setOnPreferenceChangeListener((preference, newValue) -> { + if ("default".equals(newValue)) { + AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()); + preference.setSummary(requireContext().getString(R.string.settings_system_language)); + } else { + LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue); + AppCompatDelegate.setApplicationLocales(appLocale); + preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName()); + } + return true; + }); + } + + private void setVersion() { + findPreference("version").setSummary(BuildConfig.VERSION_NAME); + } + + private void actionLogout() { + findPreference("logout").setOnPreferenceClickListener(preference -> { + activity.quit(); + return true; + }); + } + + private void actionScan() { + findPreference("scan_library").setOnPreferenceClickListener(preference -> { + settingViewModel.launchScan(new ScanCallback() { + @Override + public void onError(Exception exception) { + findPreference("scan_library").setSummary(exception.getMessage()); + } + + @Override + public void onSuccess(boolean isScanning, long count) { + findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); + if (isScanning) getScanStatus(); + } + }); + + return true; + }); + } + + private void actionSyncStarredTracks() { + findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredSyncDialog dialog = new StarredSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionSyncStarredAlbums() { + findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionSyncStarredArtists() { + findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionChangeStreamingCacheStorage() { + findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> { + StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() { + @Override + public void onPositiveClick() { + findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button); + } + + @Override + public void onNegativeClick() { + findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button); + } + }); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionChangeDownloadStorage() { + findPreference("download_storage").setOnPreferenceClickListener(preference -> { + DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() { + @Override + public void onPositiveClick() { + findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button); + checkDownloadDirectory(); + } + + @Override + public void onNegativeClick() { + findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button); + checkDownloadDirectory(); + } + + @Override + public void onNeutralClick() { + findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button); + checkDownloadDirectory(); + } + }); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionSetDownloadDirectory() { + Preference pref = findPreference("set_download_directory"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + String current = Preferences.getDownloadDirectoryUri(); + + if (current != null) { + Preferences.setDownloadDirectoryUri(null); + Preferences.setDownloadStoragePreference(0); + ExternalAudioReader.refreshCache(); + Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show(); + checkStorage(); + checkDownloadDirectory(); + } else { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + directoryPickerLauncher.launch(intent); + } + return true; + }); + } + } + + private void actionDeleteDownloadStorage() { + findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> { + DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog(); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionMiniPlayerHeart() { + SwitchPreference preference = findPreference("mini_shuffle_button_visibility"); + if (preference == null) { + return; + } + + preference.setChecked(Preferences.showShuffleInsteadOfHeart()); + preference.setOnPreferenceChangeListener((pref, newValue) -> { + if (newValue instanceof Boolean) { + Preferences.setShuffleInsteadOfHeart((Boolean) newValue); + } + return true; + }); + } + + private void actionAutoDownloadLyrics() { + SwitchPreference preference = findPreference("auto_download_lyrics"); + if (preference == null) { + return; + } + + preference.setChecked(Preferences.isAutoDownloadLyricsEnabled()); + preference.setOnPreferenceChangeListener((pref, newValue) -> { + if (newValue instanceof Boolean) { + Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue); + } + return true; + }); + } + + private void getScanStatus() { + settingViewModel.getScanStatus(new ScanCallback() { + @Override + public void onError(Exception exception) { + findPreference("scan_library").setSummary(exception.getMessage()); + } + + @Override + public void onSuccess(boolean isScanning, long count) { + findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); + if (isScanning) getScanStatus(); + } + }); + } + + private void actionKeepScreenOn() { + findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + return true; + }); + } + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mediaServiceBinder = (MediaService.LocalBinder) service; + isServiceBound = true; + checkEqualizerBands(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mediaServiceBinder = null; + isServiceBound = false; + } + }; + + private void bindMediaService() { + Intent intent = new Intent(requireActivity(), MediaService.class); + intent.setAction(MediaService.ACTION_BIND_EQUALIZER); + requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + isServiceBound = true; + } + + private void checkEqualizerBands() { + if (mediaServiceBinder != null) { + EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager(); + short numBands = eqManager.getNumberOfBands(); + Preference appEqualizer = findPreference("app_equalizer"); + if (appEqualizer != null) { + appEqualizer.setVisible(numBands > 0); + } + } + } + + private void actionAppEqualizer() { + Preference appEqualizer = findPreference("app_equalizer"); + if (appEqualizer != null) { + appEqualizer.setOnPreferenceClickListener(preference -> { + NavController navController = NavHostFragment.findNavController(this); + NavOptions navOptions = new NavOptions.Builder() + .setLaunchSingleTop(true) + .setPopUpTo(R.id.equalizerFragment, true) + .build(); + activity.setBottomNavigationBarVisibility(true); + activity.setBottomSheetVisibility(true); + navController.navigate(R.id.equalizerFragment, null, navOptions); + return true; + }); + } + } + + @Override + public void onPause() { + super.onPause(); + if (isServiceBound) { + requireActivity().unbindService(serviceConnection); + isServiceBound = false; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index 2981fa7c..63752861 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -1,128 +1,61 @@ package com.cappielloantonio.tempo.ui.fragment; -import android.app.Activity; -import android.content.Context; -import android.content.ComponentName; -import android.content.Intent; -import android.content.ServiceConnection; -import android.media.audiofx.AudioEffect; -import android.net.Uri; +import static com.google.android.material.internal.ViewUtils.hideKeyboard; + import android.os.Bundle; -import android.os.IBinder; -import android.text.InputFilter; -import android.text.InputType; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.Toast; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.OptIn; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.os.LocaleListCompat; -import androidx.lifecycle.ViewModelProvider; -import androidx.media3.common.util.UnstableApi; -import androidx.navigation.NavController; -import androidx.navigation.NavOptions; -import androidx.navigation.fragment.NavHostFragment; -import androidx.preference.EditTextPreference; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.SwitchPreference; +import androidx.fragment.app.Fragment; -import com.cappielloantonio.tempo.BuildConfig; import com.cappielloantonio.tempo.R; -import com.cappielloantonio.tempo.helper.ThemeHelper; -import com.cappielloantonio.tempo.interfaces.DialogClickCallback; -import com.cappielloantonio.tempo.interfaces.ScanCallback; -import com.cappielloantonio.tempo.service.EqualizerManager; -import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.databinding.FragmentSettingsBinding; import com.cappielloantonio.tempo.ui.activity.MainActivity; -import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog; -import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog; -import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog; -import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog; -import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog; -import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog; -import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.Preferences; -import com.cappielloantonio.tempo.util.UIUtil; -import com.cappielloantonio.tempo.util.ExternalAudioReader; -import com.cappielloantonio.tempo.viewmodel.SettingViewModel; -import java.util.Locale; -import java.util.Map; - -@OptIn(markerClass = UnstableApi.class) -public class SettingsFragment extends PreferenceFragmentCompat { - private static final String TAG = "SettingsFragment"; +public class SettingsFragment extends Fragment { private MainActivity activity; - private SettingViewModel settingViewModel; - - private ActivityResultLauncher equalizerResultLauncher; - private ActivityResultLauncher directoryPickerLauncher; - - private MediaService.LocalBinder mediaServiceBinder; - private boolean isServiceBound = false; + private FragmentSettingsBinding bind; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - equalizerResultLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> {} - ); + activity = (MainActivity) getActivity(); - if (!BuildConfig.FLAVOR.equals("tempus")) { - PreferenceCategory githubUpdateCategory = findPreference("settings_github_update_category_key"); - if (githubUpdateCategory != null) { - getPreferenceScreen().removePreference(githubUpdateCategory); - } - } - - directoryPickerLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK) { - Intent data = result.getData(); - if (data != null) { - Uri uri = data.getData(); - if (uri != null) { - requireContext().getContentResolver().takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ); - - Preferences.setDownloadDirectoryUri(uri.toString()); - ExternalAudioReader.refreshCache(); - Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show(); - checkDownloadDirectory(); - } - } - } - }); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - activity = (MainActivity) getActivity(); + bind = FragmentSettingsBinding.inflate(inflater,container,false); + View view = bind.getRoot(); - View view = super.onCreateView(inflater, container, savedInstanceState); - settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class); - - if (view != null) { - getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom)); - } + initAppBar(); return view; + + } + + @Override + public void onViewCreated(@NonNull View view, + @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // Add the PreferenceFragment only the first time + if (savedInstanceState == null) { + SettingsContainerFragment prefFragment = new SettingsContainerFragment(); + + // Use the child fragment manager so the PreferenceFragment is scoped to this fragment + getChildFragmentManager() + .beginTransaction() + .replace(R.id.settings_container, prefFragment) + .setReorderingAllowed(true) // optional but recommended + .commit(); + } } @Override @@ -134,479 +67,21 @@ public class SettingsFragment extends PreferenceFragmentCompat { activity.setSystemBarsVisibility(!activity.isLandscape); } - @Override - public void onResume() { - super.onResume(); - - checkSystemEqualizer(); - checkCacheStorage(); - checkStorage(); - checkDownloadDirectory(); - - setStreamingCacheSize(); - setAppLanguage(); - setVersion(); - setNetorkPingTimeoutBase(); - - actionLogout(); - actionScan(); - actionSyncStarredAlbums(); - actionSyncStarredTracks(); - actionSyncStarredArtists(); - actionChangeStreamingCacheStorage(); - actionChangeDownloadStorage(); - actionSetDownloadDirectory(); - actionDeleteDownloadStorage(); - actionKeepScreenOn(); - actionAutoDownloadLyrics(); - actionMiniPlayerHeart(); - - bindMediaService(); - actionAppEqualizer(); - } - @Override public void onStop() { super.onStop(); activity.setBottomSheetVisibility(true); - activity.toggleNavigationDrawerLockOnOrientationChange(); - activity.setSystemBarsVisibility(!activity.isLandscape); - } - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.global_preferences, rootKey); - ListPreference themePreference = findPreference(Preferences.THEME); - if (themePreference != null) { - themePreference.setOnPreferenceChangeListener( - (preference, newValue) -> { - String themeOption = (String) newValue; - ThemeHelper.applyTheme(themeOption); - return true; - }); + if (activity.isLandscape) { + activity.setNavigationDrawerLock(false); + } else if (Preferences.getEnableDrawerOnPortrait()) { + activity.setNavigationDrawerLock(false); } } - private void checkSystemEqualizer() { - Preference equalizer = findPreference("system_equalizer"); - - if (equalizer == null) return; - - Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); - - if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) { - equalizer.setOnPreferenceClickListener(preference -> { - equalizerResultLauncher.launch(intent); - return true; - }); - } else { - equalizer.setVisible(false); - } - } - - private void checkCacheStorage() { - Preference storage = findPreference("streaming_cache_storage"); - - if (storage == null) return; - - try { - if (requireContext().getExternalFilesDirs(null)[1] == null) { - storage.setVisible(false); - } else { - storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button); - } - } catch (Exception exception) { - storage.setVisible(false); - } - } - - private void checkStorage() { - Preference storage = findPreference("download_storage"); - - if (storage == null) return; - - try { - if (requireContext().getExternalFilesDirs(null)[1] == null) { - storage.setVisible(false); - } else { - int pref = Preferences.getDownloadStoragePreference(); - if (pref == 0) { - storage.setSummary(R.string.download_storage_internal_dialog_negative_button); - } else if (pref == 1) { - storage.setSummary(R.string.download_storage_external_dialog_positive_button); - } else { - storage.setSummary(R.string.download_storage_directory_dialog_neutral_button); - } - } - } catch (Exception exception) { - storage.setVisible(false); - } - } - - private void checkDownloadDirectory() { - Preference storage = findPreference("download_storage"); - Preference directory = findPreference("set_download_directory"); - - if (directory == null) return; - - String current = Preferences.getDownloadDirectoryUri(); - if (current != null) { - if (storage != null) storage.setVisible(false); - directory.setVisible(true); - directory.setIcon(R.drawable.ic_close); - directory.setTitle(R.string.settings_clear_download_folder); - directory.setSummary(current); - } else { - if (storage != null) storage.setVisible(true); - if (Preferences.getDownloadStoragePreference() == 2) { - directory.setVisible(true); - directory.setIcon(R.drawable.ic_folder); - directory.setTitle(R.string.settings_set_download_folder); - directory.setSummary(R.string.settings_choose_download_folder); - } else { - directory.setVisible(false); - } - } - } - - private void setNetorkPingTimeoutBase() { - EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base"); - - if (networkPingTimeoutBase != null) { - networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance()); - networkPingTimeoutBase.setOnBindEditTextListener(editText -> { - editText.setInputType(InputType.TYPE_CLASS_NUMBER); - editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> { - for (int i = start; i < end; i++) { - if (!Character.isDigit(source.charAt(i))) { - return ""; - } - } - return null; - }}); + private void initAppBar() { + bind.settingsToolbar.setNavigationOnClickListener(v -> { + activity.navController.navigateUp(); }); - - networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> { - String input = (String) newValue; - return input != null && !input.isEmpty(); - }); - } - } - - private void setStreamingCacheSize() { - ListPreference streamingCachePreference = findPreference("streaming_cache_size"); - - if (streamingCachePreference != null) { - streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider() { - @Nullable - @Override - public CharSequence provideSummary(@NonNull ListPreference preference) { - CharSequence entry = preference.getEntry(); - - if (entry == null) return null; - - long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024); - - return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb)); - } - }); - } - } - - private void setAppLanguage() { - ListPreference localePref = (ListPreference) findPreference("language"); - - Map locales = UIUtil.getLangPreferenceDropdownEntries(requireContext()); - - CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]); - CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]); - - localePref.setEntries(entries); - localePref.setEntryValues(entryValues); - - String value = localePref.getValue(); - if ("default".equals(value)) { - localePref.setSummary(requireContext().getString(R.string.settings_system_language)); - } else { - localePref.setSummary(Locale.forLanguageTag(value).getDisplayName()); - } - - localePref.setOnPreferenceChangeListener((preference, newValue) -> { - if ("default".equals(newValue)) { - AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()); - preference.setSummary(requireContext().getString(R.string.settings_system_language)); - } else { - LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue); - AppCompatDelegate.setApplicationLocales(appLocale); - preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName()); - } - return true; - }); - } - - private void setVersion() { - findPreference("version").setSummary(BuildConfig.VERSION_NAME); - } - - private void actionLogout() { - findPreference("logout").setOnPreferenceClickListener(preference -> { - activity.quit(); - return true; - }); - } - - private void actionScan() { - findPreference("scan_library").setOnPreferenceClickListener(preference -> { - settingViewModel.launchScan(new ScanCallback() { - @Override - public void onError(Exception exception) { - findPreference("scan_library").setSummary(exception.getMessage()); - } - - @Override - public void onSuccess(boolean isScanning, long count) { - findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); - if (isScanning) getScanStatus(); - } - }); - - return true; - }); - } - - private void actionSyncStarredTracks() { - findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - StarredSyncDialog dialog = new StarredSyncDialog(() -> { - ((SwitchPreference)preference).setChecked(false); - }); - dialog.show(activity.getSupportFragmentManager(), null); - } - } - return true; - }); - } - - private void actionSyncStarredAlbums() { - findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> { - ((SwitchPreference)preference).setChecked(false); - }); - dialog.show(activity.getSupportFragmentManager(), null); - } - } - return true; - }); - } - - private void actionSyncStarredArtists() { - findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> { - ((SwitchPreference)preference).setChecked(false); - }); - dialog.show(activity.getSupportFragmentManager(), null); - } - } - return true; - }); - } - - private void actionChangeStreamingCacheStorage() { - findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> { - StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() { - @Override - public void onPositiveClick() { - findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button); - } - - @Override - public void onNegativeClick() { - findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button); - } - }); - dialog.show(activity.getSupportFragmentManager(), null); - return true; - }); - } - - private void actionChangeDownloadStorage() { - findPreference("download_storage").setOnPreferenceClickListener(preference -> { - DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() { - @Override - public void onPositiveClick() { - findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button); - checkDownloadDirectory(); - } - - @Override - public void onNegativeClick() { - findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button); - checkDownloadDirectory(); - } - - @Override - public void onNeutralClick() { - findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button); - checkDownloadDirectory(); - } - }); - dialog.show(activity.getSupportFragmentManager(), null); - return true; - }); - } - - private void actionSetDownloadDirectory() { - Preference pref = findPreference("set_download_directory"); - if (pref != null) { - pref.setOnPreferenceClickListener(preference -> { - String current = Preferences.getDownloadDirectoryUri(); - - if (current != null) { - Preferences.setDownloadDirectoryUri(null); - Preferences.setDownloadStoragePreference(0); - ExternalAudioReader.refreshCache(); - Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show(); - checkStorage(); - checkDownloadDirectory(); - } else { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - directoryPickerLauncher.launch(intent); - } - return true; - }); - } - } - - private void actionDeleteDownloadStorage() { - findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> { - DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog(); - dialog.show(activity.getSupportFragmentManager(), null); - return true; - }); - } - - private void actionMiniPlayerHeart() { - SwitchPreference preference = findPreference("mini_shuffle_button_visibility"); - if (preference == null) { - return; - } - - preference.setChecked(Preferences.showShuffleInsteadOfHeart()); - preference.setOnPreferenceChangeListener((pref, newValue) -> { - if (newValue instanceof Boolean) { - Preferences.setShuffleInsteadOfHeart((Boolean) newValue); - } - return true; - }); - } - - private void actionAutoDownloadLyrics() { - SwitchPreference preference = findPreference("auto_download_lyrics"); - if (preference == null) { - return; - } - - preference.setChecked(Preferences.isAutoDownloadLyricsEnabled()); - preference.setOnPreferenceChangeListener((pref, newValue) -> { - if (newValue instanceof Boolean) { - Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue); - } - return true; - }); - } - - private void getScanStatus() { - settingViewModel.getScanStatus(new ScanCallback() { - @Override - public void onError(Exception exception) { - findPreference("scan_library").setSummary(exception.getMessage()); - } - - @Override - public void onSuccess(boolean isScanning, long count) { - findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); - if (isScanning) getScanStatus(); - } - }); - } - - private void actionKeepScreenOn() { - findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } - return true; - }); - } - - private final ServiceConnection serviceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - mediaServiceBinder = (MediaService.LocalBinder) service; - isServiceBound = true; - checkEqualizerBands(); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - mediaServiceBinder = null; - isServiceBound = false; - } - }; - - private void bindMediaService() { - Intent intent = new Intent(requireActivity(), MediaService.class); - intent.setAction(MediaService.ACTION_BIND_EQUALIZER); - requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); - isServiceBound = true; - } - - private void checkEqualizerBands() { - if (mediaServiceBinder != null) { - EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager(); - short numBands = eqManager.getNumberOfBands(); - Preference appEqualizer = findPreference("app_equalizer"); - if (appEqualizer != null) { - appEqualizer.setVisible(numBands > 0); - } - } - } - - private void actionAppEqualizer() { - Preference appEqualizer = findPreference("app_equalizer"); - if (appEqualizer != null) { - appEqualizer.setOnPreferenceClickListener(preference -> { - NavController navController = NavHostFragment.findNavController(this); - NavOptions navOptions = new NavOptions.Builder() - .setLaunchSingleTop(true) - .setPopUpTo(R.id.equalizerFragment, true) - .build(); - activity.setBottomNavigationBarVisibility(true); - activity.setBottomSheetVisibility(true); - navController.navigate(R.id.equalizerFragment, null, navOptions); - return true; - }); - } - } - - @Override - public void onPause() { - super.onPause(); - if (isServiceBound) { - requireActivity().unbindService(serviceConnection); - isServiceBound = false; - } } } diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 3e8d9930..c5a98dc4 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -1,6 +1,22 @@ - \ No newline at end of file + android:layout_height="match_parent"> + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08264392..bcf80ebb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -415,8 +415,8 @@ If enabled, show the shuffle button, remove the heart in the mini player Show radio If enabled, show the radio section. Restart the app for it to take full effect. - Enable drawer on portrait [Experimental] - Unlocks the lateral landscape menu drawer on portrait. The changes will take effect on restart. + Enable drawer on portrait + Unlocks the lateral landscape menu drawer on portrait. Hide bottom navbar on portrait [Experimental] Experimental.Increases vertical space by removing the bottom navbar. The changes will take effect on restart. Auto download lyrics