From 82f9679da7793069d1dfb295f8dd95fd65072dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Sat, 21 Feb 2026 01:14:08 -0300 Subject: [PATCH 01/16] feat: enhance navigation --- .../tempo/ui/activity/MainActivity.java | 106 +++++++++++++++--- .../tempo/ui/fragment/DownloadFragment.java | 2 +- .../tempo/ui/fragment/HomeFragment.java | 2 +- .../tempo/ui/fragment/LibraryFragment.java | 2 +- .../tempo/ui/fragment/SettingsFragment.java | 4 + .../tempo/util/Preferences.kt | 12 ++ app/src/main/res/drawable/ic_albums.xml | 23 ++++ app/src/main/res/drawable/ic_artists.xml | 32 ++++++ app/src/main/res/drawable/ic_genres.xml | 11 ++ app/src/main/res/drawable/ic_playlist.xml | 33 ++++++ .../main/res/layout-land/activity_main.xml | 30 ++--- app/src/main/res/layout/activity_main.xml | 40 +++++-- app/src/main/res/layout/nav_drawer_header.xml | 31 +++++ app/src/main/res/menu/nav_drawer.xml | 60 ++++++++++ app/src/main/res/navigation/nav_graph.xml | 4 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/global_preferences.xml | 10 ++ 17 files changed, 365 insertions(+), 41 deletions(-) create mode 100644 app/src/main/res/drawable/ic_albums.xml create mode 100644 app/src/main/res/drawable/ic_artists.xml create mode 100644 app/src/main/res/drawable/ic_genres.xml create mode 100644 app/src/main/res/drawable/ic_playlist.xml create mode 100644 app/src/main/res/layout/nav_drawer_header.xml create mode 100644 app/src/main/res/menu/nav_drawer.xml 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 3509175f4..d63d799c6 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 @@ -3,7 +3,6 @@ import android.content.Context; import android.content.Intent; import android.content.res.Configuration; -import android.graphics.Rect; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.NetworkInfo; @@ -11,12 +10,16 @@ import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; -import android.util.Log; +import android.view.Gravity; import android.view.View; -import android.view.ViewGroup; +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; import androidx.media3.common.MediaItem; @@ -48,6 +51,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.color.DynamicColors; +import com.google.android.material.navigation.NavigationView; import com.google.common.util.concurrent.MoreExecutors; import java.util.Objects; @@ -63,9 +67,12 @@ public class MainActivity extends BaseActivity { private FragmentManager fragmentManager; private NavHostFragment navHostFragment; private BottomNavigationView bottomNavigationView; + private FrameLayout bottomNavigationViewFrame; public NavController navController; + private DrawerLayout drawerLayout; + private NavigationView navigationView; private BottomSheetBehavior bottomSheetBehavior; - private boolean isLandscape = false; + public boolean isLandscape = false; private AssetLinkNavigator assetLinkNavigator; private AssetLinkUtil.AssetLink pendingAssetLink; @@ -111,6 +118,7 @@ protected void onStart() { protected void onResume() { super.onResume(); pingServer(); + toggleNavigationDrawerLockOnOrientationChange(); } @Override @@ -148,14 +156,8 @@ public void init() { goToLogin(); } - // Set bottom navigation height - if (isLandscape) { - ViewGroup.LayoutParams layoutParams = bottomNavigationView.getLayoutParams(); - Rect windowRect = new Rect(); - bottomNavigationView.getWindowVisibleDisplayFrame(windowRect); - layoutParams.width = windowRect.height(); - bottomNavigationView.setLayoutParams(layoutParams); - } + toggleNavigationDrawerLockOnOrientationChange(); + } // BOTTOM SHEET/NAVIGATION @@ -259,8 +261,12 @@ private void animateBottomNavigation(float slideOffset, int navigationHeight) { 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, @@ -277,16 +283,90 @@ private void initNavigation() { }); 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); + } + } + + public void toggleBottomNavigationBarVisibilityOnOrientationChange() { + // Ignore orientation change, bottom navbar always hidden + if (Preferences.getHideBottomNavbarOnPortrait()) { + setBottomNavigationBarVisibility(false); + setPortraitPlayerBottomSheetPeekHeight(56); + setSystemBarsVisibility(!isLandscape); + return; + } + + if (!isLandscape) { + // Show app navbar + show system bars + setPortraitPlayerBottomSheetPeekHeight(136); + setBottomNavigationBarVisibility(true); + setSystemBarsVisibility(true); + } else { + // Hide app navbar + hide system bars + setPortraitPlayerBottomSheetPeekHeight(56); + setBottomNavigationBarVisibility(false); + setSystemBarsVisibility(false); + } + } + + public void setNavigationDrawerLock(boolean locked) { + int mode = locked + ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED + : DrawerLayout.LOCK_MODE_UNLOCKED; + drawerLayout.setDrawerLockMode(mode); + } + + private void toggleNavigationDrawerLockOnOrientationChange() { + // Ignore orientation check, drawer always unlocked + if (Preferences.getEnableDrawerOnPortrait()) { + setNavigationDrawerLock(false); + return; + } + if (!isLandscape) { + setNavigationDrawerLock(true); + } else { + setNavigationDrawerLock(false); } } + 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); + } + } + + 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); + } + private void initService() { MediaManager.check(getMediaBrowserListenableFuture()); @@ -570,4 +650,4 @@ private void playDownloadedMedia(Intent intent) { MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java index 12ca24349..72d1e0f30 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java @@ -83,7 +83,7 @@ public void onStart() { super.onStart(); initializeMediaBrowser(); - activity.setBottomNavigationBarVisibility(true); + activity.toggleBottomNavigationBarVisibilityOnOrientationChange(); activity.setBottomSheetVisibility(true); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java index a0d0380c3..7697c01f7 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java @@ -53,7 +53,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat public void onStart() { super.onStart(); - activity.setBottomNavigationBarVisibility(true); + activity.toggleBottomNavigationBarVisibilityOnOrientationChange(); activity.setBottomSheetVisibility(true); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java index b50ee60f9..daa5d4121 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java @@ -87,7 +87,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat public void onStart() { super.onStart(); initializeMediaBrowser(); - activity.setBottomNavigationBarVisibility(true); + activity.toggleBottomNavigationBarVisibilityOnOrientationChange(); } @Override 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 aab615936..2a6ac691b 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 @@ -130,6 +130,8 @@ public void onStart() { super.onStart(); activity.setBottomNavigationBarVisibility(false); activity.setBottomSheetVisibility(false); + activity.setNavigationDrawerLock(true); + activity.setSystemBarsVisibility(!activity.isLandscape); } @Override @@ -167,6 +169,8 @@ public void onResume() { public void onStop() { super.onStop(); activity.setBottomSheetVisibility(true); + activity.setNavigationDrawerLock(false); + activity.setSystemBarsVisibility(!activity.isLandscape); } @Override diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index a95c84ae1..47c30cc00 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -30,6 +30,8 @@ object Preferences { private const val IMAGE_CACHE_SIZE = "image_cache_size" private const val STREAMING_CACHE_SIZE = "streaming_cache_size" private const val LANDSCAPE_ITEMS_PER_ROW = "landscape_items_per_row" + private const val ENABLE_DRAWER_ON_PORTRAIT = "enable_drawer_on_portrait" + private const val HIDE_BOTTOM_NAVBAR_ON_PORTRAIT = "hide_bottom_navbar_on_portrait" private const val IMAGE_SIZE = "image_size" private const val MAX_BITRATE_WIFI = "max_bitrate_wifi" private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile" @@ -310,6 +312,16 @@ object Preferences { return App.getInstance().preferences.getString(LANDSCAPE_ITEMS_PER_ROW, "4")!!.toInt() } + @JvmStatic + fun getEnableDrawerOnPortrait(): Boolean { + return App.getInstance().preferences.getBoolean(ENABLE_DRAWER_ON_PORTRAIT, false) + } + + @JvmStatic + fun getHideBottomNavbarOnPortrait(): Boolean { + return App.getInstance().preferences.getBoolean(HIDE_BOTTOM_NAVBAR_ON_PORTRAIT, false) + } + @JvmStatic fun getImageSize(): Int { return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt() diff --git a/app/src/main/res/drawable/ic_albums.xml b/app/src/main/res/drawable/ic_albums.xml new file mode 100644 index 000000000..4c1c697df --- /dev/null +++ b/app/src/main/res/drawable/ic_albums.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_artists.xml b/app/src/main/res/drawable/ic_artists.xml new file mode 100644 index 000000000..03d8308f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_artists.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_genres.xml b/app/src/main/res/drawable/ic_genres.xml new file mode 100644 index 000000000..3e7b66795 --- /dev/null +++ b/app/src/main/res/drawable/ic_genres.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist.xml b/app/src/main/res/drawable/ic_playlist.xml new file mode 100644 index 000000000..08225cc90 --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist.xml @@ -0,0 +1,33 @@ + + + + + + diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 581aa4ce7..1b6d2e9c7 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -1,16 +1,15 @@ - + android:background="?attr/colorSurface"> + android:layout_height="match_parent"> - - - + + - - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 01edf7f5f..15a8d99cd 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,16 +1,16 @@ - + android:background="?attr/colorSurface"> + + android:layout_height="match_parent"> + + + + + + - - + diff --git a/app/src/main/res/layout/nav_drawer_header.xml b/app/src/main/res/layout/nav_drawer_header.xml new file mode 100644 index 000000000..b6ff546c5 --- /dev/null +++ b/app/src/main/res/layout/nav_drawer_header.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav_drawer.xml b/app/src/main/res/menu/nav_drawer.xml new file mode 100644 index 000000000..a01dfcedf --- /dev/null +++ b/app/src/main/res/menu/nav_drawer.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index c6aba5abb..d234d5440 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -220,6 +220,10 @@ + 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 + Unlocks the lateral landscape menu drawer on portrait. The changes will take effect on restart. + Hide bottom navbar on portrait + Increases vertical space by removing the bottom navbar. The changes will take effect on restart. Auto download lyrics Automatically save lyrics when they are available so they can be shown while offline. Set replay gain mode diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index 4ee034e53..cdb875a9f 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -54,6 +54,16 @@ android:defaultValue="false" android:key="always_on_display" /> + + + + Date: Sat, 21 Feb 2026 14:31:22 -0300 Subject: [PATCH 02/16] fix: leaving settings always unlocks drawer --- .../com/cappielloantonio/tempo/ui/activity/MainActivity.java | 2 +- .../cappielloantonio/tempo/ui/fragment/SettingsFragment.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 d63d799c6..f872b403f 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 @@ -325,7 +325,7 @@ public void setNavigationDrawerLock(boolean locked) { drawerLayout.setDrawerLockMode(mode); } - private void toggleNavigationDrawerLockOnOrientationChange() { + public void toggleNavigationDrawerLockOnOrientationChange() { // Ignore orientation check, drawer always unlocked if (Preferences.getEnableDrawerOnPortrait()) { setNavigationDrawerLock(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 2a6ac691b..2981fa7c9 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 @@ -169,7 +169,7 @@ public void onResume() { public void onStop() { super.onStop(); activity.setBottomSheetVisibility(true); - activity.setNavigationDrawerLock(false); + activity.toggleNavigationDrawerLockOnOrientationChange(); activity.setSystemBarsVisibility(!activity.isLandscape); } From 52cfd36b09c461de72bed9b07a8c8852856c3421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Mon, 23 Feb 2026 00:03:41 -0300 Subject: [PATCH 03/16] feat: set app settings inside a frame layout In order to add a toolbar with a back button in settings I needed to extend from a fragment so I converted SettingsFragment into a fragment and created SettingsContainerFragment, the latter is injected as a child of SettingsFragment inside a FrameLayout. Since SettingsContainerFragment extends from PreferenceFragmentCompat, this allows to swap it for other and, in the bigger picture, allow an arbitrary organization. --- .../fragment/SettingsContainerFragment.java | 609 ++++++++++++++++++ .../tempo/ui/fragment/SettingsFragment.java | 557 ++-------------- app/src/main/res/layout/fragment_settings.xml | 24 +- 3 files changed, 665 insertions(+), 525 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java 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 000000000..172f21217 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java @@ -0,0 +1,609 @@ +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 onStop() { + super.onStop(); + activity.setBottomSheetVisibility(true); + } + + @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 2981fa7c9..7d3464b7a 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,5 +1,7 @@ package com.cappielloantonio.tempo.ui.fragment; +import static com.google.android.material.internal.ViewUtils.hideKeyboard; + import android.app.Activity; import android.content.Context; import android.content.ComponentName; @@ -22,8 +24,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; import androidx.core.os.LocaleListCompat; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; import androidx.navigation.NavController; @@ -38,6 +45,8 @@ import com.cappielloantonio.tempo.BuildConfig; import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentAlbumCatalogueBinding; +import com.cappielloantonio.tempo.databinding.FragmentSettingsBinding; import com.cappielloantonio.tempo.helper.ThemeHelper; import com.cappielloantonio.tempo.interfaces.DialogClickCallback; import com.cappielloantonio.tempo.interfaces.ScanCallback; @@ -59,554 +68,60 @@ 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 -> {} - ); - - 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 - ); + activity = (MainActivity) getActivity(); - 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); + bind = FragmentSettingsBinding.inflate(inflater,container,false); + View view = bind.getRoot(); - if (view != null) { - getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom)); - } + initAppBar(); return view; - } - @Override - public void onStart() { - super.onStart(); - activity.setBottomNavigationBarVisibility(false); - activity.setBottomSheetVisibility(false); - activity.setNavigationDrawerLock(true); - activity.setSystemBarsVisibility(!activity.isLandscape); } @Override - public void onResume() { - super.onResume(); + public void onViewCreated(@NonNull View view, + @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); - checkSystemEqualizer(); - checkCacheStorage(); - checkStorage(); - checkDownloadDirectory(); + // Add the PreferenceFragment only the first time + if (savedInstanceState == null) { + SettingsContainerFragment prefFragment = new SettingsContainerFragment(); - setStreamingCacheSize(); - setAppLanguage(); - setVersion(); - setNetorkPingTimeoutBase(); - - actionLogout(); - actionScan(); - actionSyncStarredAlbums(); - actionSyncStarredTracks(); - actionSyncStarredArtists(); - actionChangeStreamingCacheStorage(); - actionChangeDownloadStorage(); - actionSetDownloadDirectory(); - actionDeleteDownloadStorage(); - actionKeepScreenOn(); - actionAutoDownloadLyrics(); - actionMiniPlayerHeart(); - - bindMediaService(); - actionAppEqualizer(); + // 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 - public void onStop() { - super.onStop(); - activity.setBottomSheetVisibility(true); - activity.toggleNavigationDrawerLockOnOrientationChange(); + public void onStart() { + super.onStart(); + activity.setBottomNavigationBarVisibility(false); + activity.setBottomSheetVisibility(false); + activity.setNavigationDrawerLock(true); 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; - }); - } - } - - 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 3e8d99309..c5a98dc4a 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"> + + + + + From 34d354d8039ac70798b880bf99c808ef00a1e330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Mon, 23 Feb 2026 00:18:50 -0300 Subject: [PATCH 04/16] fix: onStop declaration on wrong class --- .../tempo/ui/fragment/SettingsContainerFragment.java | 6 ------ .../tempo/ui/fragment/SettingsFragment.java | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) 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 index 172f21217..e770976a3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java @@ -164,12 +164,6 @@ public void onResume() { actionAppEqualizer(); } - @Override - public void onStop() { - super.onStop(); - activity.setBottomSheetVisibility(true); - } - @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.global_preferences, rootKey); 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 7d3464b7a..c1399d750 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 @@ -119,6 +119,12 @@ public void onStart() { activity.setSystemBarsVisibility(!activity.isLandscape); } + @Override + public void onStop() { + super.onStop(); + activity.setBottomSheetVisibility(true); + } + private void initAppBar() { bind.settingsToolbar.setNavigationOnClickListener(v -> { activity.navController.navigateUp(); From eeb125542d41760059e3a7c7653abf4d54a538f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Mon, 23 Feb 2026 00:19:20 -0300 Subject: [PATCH 05/16] fix: equalizer not respecting navigation ui directives --- .../tempo/ui/fragment/EqualizerFragment.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt index ac6608c73..56f6419bd 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt @@ -21,10 +21,12 @@ import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.service.EqualizerManager import com.cappielloantonio.tempo.service.BaseMediaService import com.cappielloantonio.tempo.service.MediaService +import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.Preferences class EqualizerFragment : Fragment() { + private lateinit var activity: MainActivity private var equalizerManager: EqualizerManager? = null private lateinit var eqBandsContainer: LinearLayout private lateinit var eqSwitch: Switch @@ -33,6 +35,13 @@ class EqualizerFragment : Fragment() { private val bandSeekBars = mutableListOf() private var receiverRegistered = false + + @OptIn(UnstableApi::class) + override fun onAttach(context: Context) { + super.onAttach(context) + activity = requireActivity() as MainActivity + } + private val equalizerUpdatedReceiver = object : BroadcastReceiver() { @OptIn(UnstableApi::class) override fun onReceive(context: Context?, intent: Intent?) { @@ -73,8 +82,13 @@ class EqualizerFragment : Fragment() { ) receiverRegistered = true } + activity.setBottomNavigationBarVisibility(false) + activity.setBottomSheetVisibility(false) + activity.setNavigationDrawerLock(true) + activity.setSystemBarsVisibility(!activity.isLandscape) } + @OptIn(UnstableApi::class) override fun onStop() { super.onStop() requireActivity().unbindService(connection) @@ -87,6 +101,8 @@ class EqualizerFragment : Fragment() { } receiverRegistered = false } + + activity.setBottomSheetVisibility(true); } override fun onCreateView( From 0b7d1e629b592c274afdeb563dbabe532a05cfdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Tue, 24 Feb 2026 18:09:30 -0300 Subject: [PATCH 06/16] Revert "fix: equalizer not respecting navigation ui directives" This reverts commit eeb125542d41760059e3a7c7653abf4d54a538f0. --- .../tempo/ui/fragment/EqualizerFragment.kt | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt index 56f6419bd..ac6608c73 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt @@ -21,12 +21,10 @@ import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.service.EqualizerManager import com.cappielloantonio.tempo.service.BaseMediaService import com.cappielloantonio.tempo.service.MediaService -import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.Preferences class EqualizerFragment : Fragment() { - private lateinit var activity: MainActivity private var equalizerManager: EqualizerManager? = null private lateinit var eqBandsContainer: LinearLayout private lateinit var eqSwitch: Switch @@ -35,13 +33,6 @@ class EqualizerFragment : Fragment() { private val bandSeekBars = mutableListOf() private var receiverRegistered = false - - @OptIn(UnstableApi::class) - override fun onAttach(context: Context) { - super.onAttach(context) - activity = requireActivity() as MainActivity - } - private val equalizerUpdatedReceiver = object : BroadcastReceiver() { @OptIn(UnstableApi::class) override fun onReceive(context: Context?, intent: Intent?) { @@ -82,13 +73,8 @@ class EqualizerFragment : Fragment() { ) receiverRegistered = true } - activity.setBottomNavigationBarVisibility(false) - activity.setBottomSheetVisibility(false) - activity.setNavigationDrawerLock(true) - activity.setSystemBarsVisibility(!activity.isLandscape) } - @OptIn(UnstableApi::class) override fun onStop() { super.onStop() requireActivity().unbindService(connection) @@ -101,8 +87,6 @@ class EqualizerFragment : Fragment() { } receiverRegistered = false } - - activity.setBottomSheetVisibility(true); } override fun onCreateView( From 2db9cb80d157cbaa892bd90bada71ad3de565a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Tue, 24 Feb 2026 18:23:03 -0300 Subject: [PATCH 07/16] fix: navbar + bottom sheet behavior on equalizer fragment --- .../tempo/ui/fragment/EqualizerFragment.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt index ac6608c73..04aae67c5 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt @@ -21,18 +21,26 @@ import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.service.EqualizerManager import com.cappielloantonio.tempo.service.BaseMediaService import com.cappielloantonio.tempo.service.MediaService +import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.Preferences class EqualizerFragment : Fragment() { + private lateinit var activity: MainActivity private var equalizerManager: EqualizerManager? = null private lateinit var eqBandsContainer: LinearLayout private lateinit var eqSwitch: Switch private lateinit var resetButton: Button private lateinit var safeSpace: Space private val bandSeekBars = mutableListOf() - private var receiverRegistered = false + + @OptIn(UnstableApi::class) + override fun onAttach(context: Context) { + super.onAttach(context) + activity = requireActivity() as MainActivity + } + private val equalizerUpdatedReceiver = object : BroadcastReceiver() { @OptIn(UnstableApi::class) override fun onReceive(context: Context?, intent: Intent?) { @@ -73,6 +81,8 @@ class EqualizerFragment : Fragment() { ) receiverRegistered = true } + val showBottomBar = !Preferences.getHideBottomNavbarOnPortrait() + activity.setBottomNavigationBarVisibility(showBottomBar) } override fun onStop() { 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 08/16] 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 f872b403f..fd17a9669 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.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.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 @@ protected void onDestroy() { super.onDestroy(); connectivityStatusReceiverManager(false); bind = null; + NavigationDelegate.getInstance().unbind(); + if (navigationHelper != null) { + navigationHelper.release(); + } } @Override @@ -148,7 +159,16 @@ public void init() { 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 @@ private void animateBottomNavigation(float slideOffset, int navigationHeight) { 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 000000000..bc0b681e2 --- /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 000000000..a241b8745 --- /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 000000000..f64e6bd86 --- /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); + } + } +} From 6ce10636398f508a8e4a08a8f74ce7d1f240f646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Wed, 25 Feb 2026 09:59:52 -0300 Subject: [PATCH 09/16] Revert "fix: onStop declaration on wrong class" This reverts commit 34d354d8039ac70798b880bf99c808ef00a1e330. --- .../tempo/ui/fragment/SettingsContainerFragment.java | 6 ++++++ .../tempo/ui/fragment/SettingsFragment.java | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) 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 index e770976a3..172f21217 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java @@ -164,6 +164,12 @@ public void onResume() { actionAppEqualizer(); } + @Override + public void onStop() { + super.onStop(); + activity.setBottomSheetVisibility(true); + } + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.global_preferences, rootKey); 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 c1399d750..7d3464b7a 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 @@ -119,12 +119,6 @@ public void onStart() { activity.setSystemBarsVisibility(!activity.isLandscape); } - @Override - public void onStop() { - super.onStop(); - activity.setBottomSheetVisibility(true); - } - private void initAppBar() { bind.settingsToolbar.setNavigationOnClickListener(v -> { activity.navController.navigateUp(); From ae183122ac1db8dac3fea23db1929fcef02891f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Wed, 25 Feb 2026 09:59:59 -0300 Subject: [PATCH 10/16] Revert "feat: set app settings inside a frame layout" This reverts commit 52cfd36b09c461de72bed9b07a8c8852856c3421. --- .../fragment/SettingsContainerFragment.java | 609 ------------------ .../tempo/ui/fragment/SettingsFragment.java | 557 ++++++++++++++-- app/src/main/res/layout/fragment_settings.xml | 24 +- 3 files changed, 525 insertions(+), 665 deletions(-) delete mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java 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 deleted file mode 100644 index 172f21217..000000000 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java +++ /dev/null @@ -1,609 +0,0 @@ -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 onStop() { - super.onStop(); - activity.setBottomSheetVisibility(true); - } - - @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 7d3464b7a..2981fa7c9 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,7 +1,5 @@ package com.cappielloantonio.tempo.ui.fragment; -import static com.google.android.material.internal.ViewUtils.hideKeyboard; - import android.app.Activity; import android.content.Context; import android.content.ComponentName; @@ -24,13 +22,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; -import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; -import androidx.appcompat.widget.Toolbar; import androidx.core.os.LocaleListCompat; -import androidx.core.view.ViewCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; import androidx.navigation.NavController; @@ -45,8 +38,6 @@ import com.cappielloantonio.tempo.BuildConfig; import com.cappielloantonio.tempo.R; -import com.cappielloantonio.tempo.databinding.FragmentAlbumCatalogueBinding; -import com.cappielloantonio.tempo.databinding.FragmentSettingsBinding; import com.cappielloantonio.tempo.helper.ThemeHelper; import com.cappielloantonio.tempo.interfaces.DialogClickCallback; import com.cappielloantonio.tempo.interfaces.ScanCallback; @@ -68,46 +59,70 @@ import java.util.Locale; import java.util.Map; -public class SettingsFragment extends Fragment { +@OptIn(markerClass = UnstableApi.class) +public class SettingsFragment extends PreferenceFragmentCompat { + private static final String TAG = "SettingsFragment"; private MainActivity activity; - private FragmentSettingsBinding bind; + private SettingViewModel settingViewModel; + + private ActivityResultLauncher equalizerResultLauncher; + private ActivityResultLauncher directoryPickerLauncher; + + private MediaService.LocalBinder mediaServiceBinder; + private boolean isServiceBound = false; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - activity = (MainActivity) getActivity(); - - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - bind = FragmentSettingsBinding.inflate(inflater,container,false); - View view = bind.getRoot(); + equalizerResultLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> {} + ); - initAppBar(); + if (!BuildConfig.FLAVOR.equals("tempus")) { + PreferenceCategory githubUpdateCategory = findPreference("settings_github_update_category_key"); + if (githubUpdateCategory != null) { + getPreferenceScreen().removePreference(githubUpdateCategory); + } + } - return view; + 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 void onViewCreated(@NonNull View view, - @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); - // Add the PreferenceFragment only the first time - if (savedInstanceState == null) { - SettingsContainerFragment prefFragment = new SettingsContainerFragment(); + View view = super.onCreateView(inflater, container, savedInstanceState); + settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class); - // 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(); + if (view != null) { + getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom)); } + + return view; } @Override @@ -119,9 +134,479 @@ public void onStart() { activity.setSystemBarsVisibility(!activity.isLandscape); } - private void initAppBar() { - bind.settingsToolbar.setNavigationOnClickListener(v -> { - activity.navController.navigateUp(); + @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; + }); + } + } + + 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/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index c5a98dc4a..3e8d99309 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -1,22 +1,6 @@ - - - - - - + android:layout_height="match_parent" + android:paddingTop="20dp" + android:paddingBottom="@dimen/global_padding_bottom" /> \ No newline at end of file From 0268bb42c37e3c2a2864eb508d9f19126c6090e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Wed, 25 Feb 2026 11:07:21 -0300 Subject: [PATCH 11/16] chore: set experimental label to settings title Hide bottom navigation bar on portrait and unlock drawer on portrait --- app/src/main/res/values/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 28080343f..0dcacb8b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -384,10 +384,10 @@ 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 + Enable drawer on portrait [Experimental] Unlocks the lateral landscape menu drawer on portrait. The changes will take effect on restart. - Hide bottom navbar on portrait - Increases vertical space by removing the bottom navbar. The changes will take effect on restart. + 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 Automatically save lyrics when they are available so they can be shown while offline. Set replay gain mode From ecec442e4b439fe7badc641b8b0d5341a65ef479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Wed, 25 Feb 2026 11:17:22 -0300 Subject: [PATCH 12/16] refactor: move controller to dedicated pakckage --- .../base => navigation}/NavigationController.java | 2 +- .../activity/base => navigation}/NavigationDelegate.java | 2 +- .../activity/base => navigation}/NavigationHelper.java | 2 +- .../cappielloantonio/tempo/ui/activity/MainActivity.java | 9 ++------- 4 files changed, 5 insertions(+), 10 deletions(-) rename app/src/main/java/com/cappielloantonio/tempo/{ui/activity/base => navigation}/NavigationController.java (82%) rename app/src/main/java/com/cappielloantonio/tempo/{ui/activity/base => navigation}/NavigationDelegate.java (96%) rename app/src/main/java/com/cappielloantonio/tempo/{ui/activity/base => navigation}/NavigationHelper.java (99%) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationController.java b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationController.java similarity index 82% rename from app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationController.java rename to app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationController.java index bc0b681e2..8b89f37ae 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationController.java +++ b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationController.java @@ -1,4 +1,4 @@ -package com.cappielloantonio.tempo.ui.activity.base; +package com.cappielloantonio.tempo.navigation; import androidx.annotation.OptIn; import androidx.media3.common.util.UnstableApi; diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationDelegate.java b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationDelegate.java similarity index 96% rename from app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationDelegate.java rename to app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationDelegate.java index a241b8745..bac29a888 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationDelegate.java +++ b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationDelegate.java @@ -1,4 +1,4 @@ -package com.cappielloantonio.tempo.ui.activity.base; +package com.cappielloantonio.tempo.navigation; import android.view.View; diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationHelper.java b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationHelper.java similarity index 99% rename from app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationHelper.java rename to app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationHelper.java index f64e6bd86..c9cb2711e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/NavigationHelper.java +++ b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationHelper.java @@ -1,4 +1,4 @@ -package com.cappielloantonio.tempo.ui.activity.base; +package com.cappielloantonio.tempo.navigation; import android.content.Context; import android.view.View; 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 fd17a9669..2293307c5 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 @@ -15,9 +15,6 @@ 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; @@ -28,7 +25,6 @@ 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; @@ -38,9 +34,8 @@ 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.navigation.NavigationDelegate; +import com.cappielloantonio.tempo.navigation.NavigationHelper; import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog; import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog; import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog; From 4803ce676a26ed32aa07f05542296320813310d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Wed, 25 Feb 2026 13:30:53 -0300 Subject: [PATCH 13/16] fix: remove old navigation controller delegate --- .../tempo/navigation/NavigationDelegate.java | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationDelegate.java diff --git a/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationDelegate.java b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationDelegate.java deleted file mode 100644 index bac29a888..000000000 --- a/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationDelegate.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.cappielloantonio.tempo.navigation; - -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); - } -} From 0b23d4355e840548e41ba09cfa3a239652d3450c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Wed, 25 Feb 2026 13:32:09 -0300 Subject: [PATCH 14/16] feat: stabilize public methods and their implementations --- .../navigation/NavigationController.java | 40 +++++++- .../tempo/navigation/NavigationHelper.java | 99 +++++++++---------- 2 files changed, 80 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationController.java b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationController.java index 8b89f37ae..a6437dca9 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationController.java +++ b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationController.java @@ -1,12 +1,44 @@ package com.cappielloantonio.tempo.navigation; +import android.view.View; + +import androidx.annotation.NonNull; import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.util.UnstableApi; +import androidx.navigation.NavController; + +import com.cappielloantonio.tempo.navigation.NavigationHelper; + +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 interface NavigationController { + } + + public void setNavbarVisibility(boolean visibility) { + helper.setBottomNavigationBarVisibility(visibility); + } + + public void setDrawerLock(boolean visibility) { + helper.setNavigationDrawerLock(visibility); + } + + public void toggleDrawerLockOnOrientation(AppCompatActivity activity) { + helper.toggleNavigationDrawerLockOnOrientationChange(activity); + } - @OptIn(markerClass = UnstableApi.class) - default boolean isVisible() { - return NavigationDelegate.isVisible(); + 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 index c9cb2711e..b5916f73b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationHelper.java +++ b/app/src/main/java/com/cappielloantonio/tempo/navigation/NavigationHelper.java @@ -1,18 +1,21 @@ package com.cappielloantonio.tempo.navigation; import android.content.Context; +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; @@ -23,6 +26,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.navigation.NavigationView; +import org.jetbrains.annotations.Contract; + import java.util.Objects; public class NavigationHelper { @@ -34,62 +39,33 @@ public class NavigationHelper { /* 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; + 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; } - /* 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) { + public void syncWithBottomSheetBehavior(@NonNull BottomSheetBehavior bottomSheetBehavior, + @NonNull NavController navController) { 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); + // 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); } }); @@ -97,6 +73,18 @@ private void setupNavigation(@NonNull MainActivity activity) { 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 @@ -117,7 +105,13 @@ public void setNavigationDrawerLock(boolean locked) { drawerLayout.setDrawerLockMode(mode); } - public void toggleNavigationDrawerLockOnOrientationChange(MainActivity activity, boolean isLandscape) { + @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; @@ -130,11 +124,6 @@ public void toggleNavigationDrawerLockOnOrientationChange(MainActivity activity, that everything was defined on the activity and is gobally available */ - @NonNull - public NavController getNavController() { - return navController; - } - @NonNull public BottomNavigationView getBottomNavigationView() { return bottomNavigationView; @@ -155,7 +144,7 @@ public DrawerLayout getDrawerLayout() { */ @OptIn(markerClass = UnstableApi.class) - public void setSystemBarsVisibility(MainActivity activity, boolean visibility) { + public void setSystemBarsVisibility(AppCompatActivity activity, boolean visibility) { WindowInsetsControllerCompat insetsController; Window window = activity.getWindow(); View decorView = window.getDecorView(); From dba4e4b4288b72479bffe3769a8883279edd9885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Wed, 25 Feb 2026 13:33:17 -0300 Subject: [PATCH 15/16] feat: migrate to new navigation controller --- .../tempo/ui/activity/MainActivity.java | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) 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 2293307c5..1264a1dbc 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 @@ -32,10 +32,10 @@ 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.navigation.NavigationDelegate; -import com.cappielloantonio.tempo.navigation.NavigationHelper; import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog; import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog; import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog; @@ -59,16 +59,14 @@ public class MainActivity extends BaseActivity { private static final String TAG = "MainActivityLogs"; public ActivityMainBinding bind; - private NavigationHelper navigationHelper; private MainViewModel mainViewModel; private FragmentManager fragmentManager; private NavHostFragment navHostFragment; private BottomNavigationView bottomNavigationView; - private FrameLayout bottomNavigationViewFrame; public NavController navController; - private DrawerLayout drawerLayout; private NavigationView navigationView; + private NavigationController navigationController; public BottomSheetBehavior bottomSheetBehavior; public boolean isLandscape = false; private AssetLinkNavigator assetLinkNavigator; @@ -128,10 +126,6 @@ protected void onDestroy() { super.onDestroy(); connectivityStatusReceiverManager(false); bind = null; - NavigationDelegate.getInstance().unbind(); - if (navigationHelper != null) { - navigationHelper.release(); - } } @Override @@ -154,16 +148,7 @@ public void init() { fragmentManager = getSupportFragmentManager(); initBottomSheet(); - - // 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(); + initNavigation(); if (Preferences.getPassword() != null || (Preferences.getToken() != null && Preferences.getSalt() != null)) { goFromLogin(); @@ -175,6 +160,36 @@ public void init() { } + private void initNavigation() { + // Bind views + // -> Saves in global variables for backward compatibility + bottomNavigationView = findViewById(R.id.bottom_navigation); + navigationView = findViewById(R.id.nav_view); + + // Bind swappable fragment of activity to navController + navHostFragment = (NavHostFragment) this + .getSupportFragmentManager() + .findFragmentById(R.id.nav_host_fragment); + navController = Objects.requireNonNull(navHostFragment).getNavController(); + + // Helper + NavigationHelper navigationHelper = + new NavigationHelper( + bottomNavigationView, + findViewById(R.id.bottom_navigation_frame), + findViewById(R.id.drawer_layout), + navigationView, + navHostFragment + ); + + // Controller + navigationController = new NavigationController(navigationHelper); + navigationController.syncWithBottomSheetBehavior(bottomSheetBehavior, navController); + + // Expose in activity for backward compatibility + bottomNavigationView = navigationHelper.getBottomNavigationView(); + } + // BOTTOM SHEET/NAVIGATION private void initBottomSheet() { bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.player_bottom_sheet)); @@ -275,41 +290,41 @@ private void animateBottomNavigation(float slideOffset, int navigationHeight) { } public void setBottomNavigationBarVisibility(boolean visibility) { - navigationHelper.setBottomNavigationBarVisibility(visibility); + navigationController.setNavbarVisibility(visibility); } public void toggleBottomNavigationBarVisibilityOnOrientationChange() { // Ignore orientation change, bottom navbar always hidden if (Preferences.getHideBottomNavbarOnPortrait()) { - navigationHelper.setBottomNavigationBarVisibility(false); + navigationController.setNavbarVisibility(false); setPortraitPlayerBottomSheetPeekHeight(56); - navigationHelper.setSystemBarsVisibility(this, !isLandscape); + navigationController.setSystemBarsVisibility(this, !isLandscape); return; } if (!isLandscape) { // Show app navbar + show system bars setPortraitPlayerBottomSheetPeekHeight(136); - navigationHelper.setBottomNavigationBarVisibility(true); - navigationHelper.setSystemBarsVisibility(this, true); + navigationController.setNavbarVisibility(true); + navigationController.setSystemBarsVisibility(this, true); } else { // Hide app navbar + hide system bars setPortraitPlayerBottomSheetPeekHeight(56); - navigationHelper.setBottomNavigationBarVisibility(false); - navigationHelper.setSystemBarsVisibility(this, false); + navigationController.setNavbarVisibility(false); + navigationController.setSystemBarsVisibility(this, false); } } public void setNavigationDrawerLock(boolean locked) { - navigationHelper.setNavigationDrawerLock(locked); + navigationController.setDrawerLock(locked); } public void toggleNavigationDrawerLockOnOrientationChange() { - navigationHelper.toggleNavigationDrawerLockOnOrientationChange(this, isLandscape); + navigationController.toggleDrawerLockOnOrientation(this); } public void setSystemBarsVisibility(boolean visibility) { - navigationHelper.setSystemBarsVisibility(this, visibility); + navigationController.setSystemBarsVisibility(this, visibility); } private void setPortraitPlayerBottomSheetPeekHeight(int peekHeight) { From 91506a308d8fcde6fc71b409d0ce59b84ca977d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Wed, 25 Feb 2026 13:51:08 -0300 Subject: [PATCH 16/16] feat: remove unnecessary global variables --- .../tempo/ui/activity/MainActivity.java | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) 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 1264a1dbc..20d8c1680 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 @@ -48,7 +48,6 @@ import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.color.DynamicColors; -import com.google.android.material.navigation.NavigationView; import com.google.common.util.concurrent.MoreExecutors; import java.util.Objects; @@ -62,10 +61,8 @@ public class MainActivity extends BaseActivity { private MainViewModel mainViewModel; private FragmentManager fragmentManager; - private NavHostFragment navHostFragment; - private BottomNavigationView bottomNavigationView; + public NavController navController; - private NavigationView navigationView; private NavigationController navigationController; public BottomSheetBehavior bottomSheetBehavior; public boolean isLandscape = false; @@ -161,33 +158,29 @@ public void init() { } private void initNavigation() { - // Bind views - // -> Saves in global variables for backward compatibility - bottomNavigationView = findViewById(R.id.bottom_navigation); - navigationView = findViewById(R.id.nav_view); - - // Bind swappable fragment of activity to navController - navHostFragment = (NavHostFragment) this + // 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 + */ // Helper NavigationHelper navigationHelper = new NavigationHelper( - bottomNavigationView, + findViewById(R.id.bottom_navigation), findViewById(R.id.bottom_navigation_frame), findViewById(R.id.drawer_layout), - navigationView, + findViewById(R.id.nav_view), navHostFragment ); // Controller navigationController = new NavigationController(navigationHelper); navigationController.syncWithBottomSheetBehavior(bottomSheetBehavior, navController); - - // Expose in activity for backward compatibility - bottomNavigationView = navigationHelper.getBottomNavigationView(); } // BOTTOM SHEET/NAVIGATION @@ -370,7 +363,7 @@ private void goToLogin() { } 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);