diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt new file mode 100644 index 00000000..944e2ffd --- /dev/null +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -0,0 +1,316 @@ +package com.cappielloantonio.tempo.service + +import android.annotation.SuppressLint +import android.app.PendingIntent.FLAG_IMMUTABLE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.app.TaskStackBuilder +import android.content.Intent +import android.os.Bundle +import androidx.media3.cast.CastPlayer +import androidx.media3.cast.SessionAvailabilityListener +import androidx.media3.common.* +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.session.* +import androidx.media3.session.MediaSession.ControllerInfo +import com.cappielloantonio.tempo.R +import com.cappielloantonio.tempo.ui.activity.MainActivity +import com.cappielloantonio.tempo.util.Constants +import com.cappielloantonio.tempo.util.DownloadUtil +import com.cappielloantonio.tempo.util.UIUtil +import com.google.android.gms.cast.framework.CastContext +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture + + +@UnstableApi +class MediaService : MediaLibraryService(), SessionAvailabilityListener { + private val librarySessionCallback = CustomMediaLibrarySessionCallback() + + private lateinit var player: ExoPlayer + private lateinit var castPlayer: CastPlayer + private lateinit var mediaLibrarySession: MediaLibrarySession + private lateinit var customCommands: List + + private var customLayout = ImmutableList.of() + + companion object { + private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON = + "android.media3.session.demo.SHUFFLE_ON" + private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = + "android.media3.session.demo.SHUFFLE_OFF" + } + + override fun onCreate() { + super.onCreate() + + initializeCustomCommands() + initializePlayer() + initializeCastPlayer() + initializeMediaLibrarySession() + initializePlayerListener() + + setPlayer( + null, + if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else player + ) + } + + override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession { + return mediaLibrarySession + } + + override fun onDestroy() { + releasePlayer() + super.onDestroy() + } + + private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback { + + override fun onConnect( + session: MediaSession, + controller: ControllerInfo + ): MediaSession.ConnectionResult { + val connectionResult = super.onConnect(session, controller) + val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() + + customCommands.forEach { commandButton -> + // TODO: Aggiungere i comandi personalizzati + // commandButton.sessionCommand?.let { availableSessionCommands.add(it) } + } + + return MediaSession.ConnectionResult.accept( + availableSessionCommands.build(), + connectionResult.availablePlayerCommands + ) + } + + override fun onPostConnect(session: MediaSession, controller: ControllerInfo) { + if (!customLayout.isEmpty() && controller.controllerVersion != 0) { + ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout)) + } + } + + override fun onCustomCommand( + session: MediaSession, + controller: ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) { + player.shuffleModeEnabled = true + customLayout = ImmutableList.of(customCommands[1]) + session.setCustomLayout(customLayout) + } else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) { + player.shuffleModeEnabled = false + customLayout = ImmutableList.of(customCommands[0]) + session.setCustomLayout(customLayout) + } + + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + + /* override fun onGetLibraryRoot( + session: MediaLibrarySession, + browser: ControllerInfo, + params: LibraryParams? + ): ListenableFuture> { + return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params)) + } + + override fun onGetItem( + session: MediaLibrarySession, + browser: ControllerInfo, + mediaId: String + ): ListenableFuture> { + val item = + MediaItemTree.getItem(mediaId) + ?: return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null)) + } + + override fun onSubscribe( + session: MediaLibrarySession, + browser: ControllerInfo, + parentId: String, + params: LibraryParams? + ): ListenableFuture> { + val children = + MediaItemTree.getChildren(parentId) + ?: return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + session.notifyChildrenChanged(browser, parentId, children.size, params) + return Futures.immediateFuture(LibraryResult.ofVoid()) + } + + override fun onGetChildren( + session: MediaLibrarySession, + browser: ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: LibraryParams? + ): ListenableFuture>> { + val children = + MediaItemTree.getChildren(parentId) + ?: return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + + return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) + }*/ + + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: ControllerInfo, + mediaItems: List + ): ListenableFuture> { + val updatedMediaItems = mediaItems.map { + it.buildUpon() + .setUri(it.requestMetadata.mediaUri) + .setMediaMetadata(it.mediaMetadata) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .build() + } + return Futures.immediateFuture(updatedMediaItems) + } + } + + private fun initializeCustomCommands() { + customCommands = + listOf( + getShuffleCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY) + ), + getShuffleCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY) + ) + ) + + customLayout = ImmutableList.of(customCommands[0]) + } + + private fun initializePlayer() { + player = ExoPlayer.Builder(this) + .setRenderersFactory(getRenderersFactory()) + .setMediaSourceFactory(getMediaSourceFactory()) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setHandleAudioBecomingNoisy(true) + .setWakeMode(C.WAKE_MODE_NETWORK) + .build() + } + + private fun initializeCastPlayer() { + if (UIUtil.isCastApiAvailable(this)) { + castPlayer = CastPlayer(CastContext.getSharedInstance(this)) + castPlayer.setSessionAvailabilityListener(this) + } + } + + private fun initializeMediaLibrarySession() { + val sessionActivityPendingIntent = + TaskStackBuilder.create(this).run { + addNextIntent(Intent(this@MediaService, MainActivity::class.java)) + getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT) + } + + mediaLibrarySession = + MediaLibrarySession.Builder(this, player, librarySessionCallback) + .setSessionActivity(sessionActivityPendingIntent) + .build() + + if (!customLayout.isEmpty()) { + mediaLibrarySession.setCustomLayout(customLayout) + } + } + + private fun initializePlayerListener() { + player.addListener(object : Player.Listener { + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + if (mediaItem == null) return + + if(reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { + MediaManager.setLastPlayedTimestamp(mediaItem) + } + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + if (!isPlaying) { + MediaManager.setPlayingPausedTimestamp( + player.currentMediaItem, + player.currentPosition + ) + } + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + super.onPositionDiscontinuity(oldPosition, newPosition, reason) + + if(reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { + if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { + MediaManager.scrobble(oldPosition.mediaItem) + MediaManager.saveChronology(oldPosition.mediaItem) + } + + if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { + MediaManager.setLastPlayedTimestamp(newPosition.mediaItem) + } + } + } + }) + } + + private fun setPlayer(oldPlayer: Player?, newPlayer: Player) { + if (oldPlayer === newPlayer) return + oldPlayer?.stop() + mediaLibrarySession.player = newPlayer + } + + private fun releasePlayer() { + if (this::castPlayer.isInitialized) castPlayer.setSessionAvailabilityListener(null) + if (this::castPlayer.isInitialized) castPlayer.release() + player.release() + mediaLibrarySession.release() + } + + @SuppressLint("PrivateResource") + private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton { + val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON + return CommandButton.Builder() + .setDisplayName( + getString( + if (isOn) R.string.exo_controls_shuffle_on_description + else R.string.exo_controls_shuffle_off_description + ) + ) + .setSessionCommand(sessionCommand) + .setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on) + .build() + } + + private fun ignoreFuture(customLayout: ListenableFuture) { + /* Do nothing. */ + } + + private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) + + private fun getMediaSourceFactory() = + DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this)) + + override fun onCastSessionAvailable() { + setPlayer(player, castPlayer) + } + + override fun onCastSessionUnavailable() { + setPlayer(castPlayer, player) + } +} \ No newline at end of file diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java b/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java new file mode 100644 index 00000000..69cb87ed --- /dev/null +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java @@ -0,0 +1,101 @@ +package com.cappielloantonio.tempo.ui.activity.base; + +import android.Manifest; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.offline.DownloadService; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.service.DownloaderService; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.dialog.BatteryOptimizationDialog; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.elevation.SurfaceColors; +import com.google.common.util.concurrent.ListenableFuture; + +@UnstableApi +public class BaseActivity extends AppCompatActivity { + private static final String TAG = "BaseActivity"; + + private ListenableFuture mediaBrowserListenableFuture; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + initializeDownloader(); + checkBatteryOptimization(); + checkPermission(); + } + + @Override + protected void onStart() { + super.onStart(); + setNavigationBarColor(); + initializeBrowser(); + } + + @Override + protected void onStop() { + releaseBrowser(); + super.onStop(); + } + + private void checkBatteryOptimization() { + if (detectBatteryOptimization() && Preferences.askForOptimization()) { + showBatteryOptimizationDialog(); + } + } + + private void checkPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, 101); + } + } + } + + private boolean detectBatteryOptimization() { + String packageName = getPackageName(); + PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); + return !powerManager.isIgnoringBatteryOptimizations(packageName); + } + + private void showBatteryOptimizationDialog() { + BatteryOptimizationDialog dialog = new BatteryOptimizationDialog(); + dialog.show(getSupportFragmentManager(), null); + } + + private void initializeBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(this, new SessionToken(this, new ComponentName(this, MediaService.class))).buildAsync(); + } + + private void releaseBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + public ListenableFuture getMediaBrowserListenableFuture() { + return mediaBrowserListenableFuture; + } + + private void initializeDownloader() { + try { + DownloadService.start(this, DownloaderService.class); + } catch (IllegalStateException e) { + DownloadService.startForeground(this, DownloaderService.class); + } + } + + private void setNavigationBarColor() { + getWindow().setNavigationBarColor(SurfaceColors.getColorForElevation(this, 10)); + } +} diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java b/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java new file mode 100644 index 00000000..f89f562a --- /dev/null +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java @@ -0,0 +1,168 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentDownloadBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.DownloadViewModel; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.Objects; + +@UnstableApi +public class DownloadFragment extends Fragment implements ClickCallback { + private FragmentDownloadBinding bind; + private MainActivity activity; + private DownloadViewModel downloadViewModel; + + private DownloadHorizontalAdapter downloadHorizontalAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.main_page_menu, menu); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentDownloadBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + downloadViewModel = new ViewModelProvider(requireActivity()).get(DownloadViewModel.class); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initAppBar(); + initDownloadedSongView(); + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + activity.setBottomNavigationBarVisibility(true); + activity.setBottomSheetVisibility(true); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_search) { + activity.navController.navigate(R.id.action_downloadFragment_to_searchFragment); + return true; + } else if (item.getItemId() == R.id.action_settings) { + activity.navController.navigate(R.id.action_downloadFragment_to_settingsFragment); + return true; + } + + return false; + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + } + + private void initDownloadedSongView() { + bind.downloadedTracksRecyclerView.setHasFixedSize(true); + + downloadHorizontalAdapter = new DownloadHorizontalAdapter(this); + bind.downloadedTracksRecyclerView.setAdapter(downloadHorizontalAdapter); + downloadViewModel.getDownloadedTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> { + if (songs != null) { + if (songs.isEmpty()) { + if (bind != null) { + bind.emptyDownloadLayout.setVisibility(View.VISIBLE); + bind.fragmentDownloadNestedScrollView.setVisibility(View.GONE); + + bind.downloadDownloadedTracksPlaceholder.placeholder.setVisibility(View.VISIBLE); + bind.downloadDownloadedTracksSector.setVisibility(View.GONE); + } + } else { + if (bind != null) { + bind.emptyDownloadLayout.setVisibility(View.GONE); + bind.fragmentDownloadNestedScrollView.setVisibility(View.VISIBLE); + + bind.downloadDownloadedTracksPlaceholder.placeholder.setVisibility(View.GONE); + bind.downloadDownloadedTracksSector.setVisibility(View.VISIBLE); + + bind.downloadedTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + + downloadHorizontalAdapter.setItems(songs); + } + } + + if (bind != null) bind.loadingProgressBar.setVisibility(View.GONE); + } + }); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onMediaClick(Bundle bundle) { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onMediaLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); + } +} diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java b/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java new file mode 100644 index 00000000..05c67979 --- /dev/null +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java @@ -0,0 +1,116 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentHomeBinding; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.fragment.pager.HomePager; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.tabs.TabLayoutMediator; + +import java.util.Objects; + +@UnstableApi +public class HomeFragment extends Fragment { + private static final String TAG = "HomeFragment"; + + private FragmentHomeBinding bind; + private MainActivity activity; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.main_page_menu, menu); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + bind = FragmentHomeBinding.inflate(inflater, container, false); + return bind.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initAppBar(); + initHomePager(); + } + + @Override + public void onStart() { + super.onStart(); + + activity.setBottomNavigationBarVisibility(true); + activity.setBottomSheetVisibility(true); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_search) { + activity.navController.navigate(R.id.action_homeFragment_to_searchFragment); + return true; + } else if (item.getItemId() == R.id.action_settings) { + activity.navController.navigate(R.id.action_homeFragment_to_settingsFragment); + return true; + } + + return false; + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + } + + private void initHomePager() { + HomePager pager = new HomePager(this); + + pager.addFragment(new HomeTabMusicFragment(), "Music", R.drawable.ic_home); + + if (Preferences.isPodcastSectionVisible()) + pager.addFragment(new HomeTabPodcastFragment(), "Podcast", R.drawable.ic_graphic_eq); + + if (Preferences.isRadioSectionVisible()) + pager.addFragment(new HomeTabRadioFragment(), "Radio", R.drawable.ic_play_for_work); + + bind.homeViewPager.setAdapter(pager); + bind.homeViewPager.setOffscreenPageLimit(3); + bind.homeViewPager.setUserInputEnabled(false); + + new TabLayoutMediator(bind.homeTabLayout, bind.homeViewPager, + (tab, position) -> { + tab.setText(pager.getPageTitle(position)); + // tab.setIcon(pager.getPageIcon(position)); + } + ).attach(); + + bind.homeTabLayout.setVisibility(Preferences.isPodcastSectionVisible() || Preferences.isRadioSectionVisible() ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java b/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java new file mode 100644 index 00000000..5529c93c --- /dev/null +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java @@ -0,0 +1,306 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentLibraryBinding; +import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter; +import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter; +import com.cappielloantonio.tempo.ui.adapter.GenreAdapter; +import com.cappielloantonio.tempo.ui.adapter.MusicFolderAdapter; +import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter; +import com.cappielloantonio.tempo.ui.dialog.PlaylistEditorDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.LibraryViewModel; + +import java.util.Objects; + +@UnstableApi +public class LibraryFragment extends Fragment implements ClickCallback { + private static final String TAG = "LibraryFragment"; + + private FragmentLibraryBinding bind; + private MainActivity activity; + private LibraryViewModel libraryViewModel; + + private MusicFolderAdapter musicFolderAdapter; + private AlbumAdapter albumAdapter; + private ArtistAdapter artistAdapter; + private GenreAdapter genreAdapter; + + private PlaylistHorizontalAdapter playlistHorizontalAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.main_page_menu, menu); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentLibraryBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + libraryViewModel = new ViewModelProvider(requireActivity()).get(LibraryViewModel.class); + + init(); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initAppBar(); + initMusicFolderView(); + initAlbumView(); + initArtistView(); + initGenreView(); + initPlaylistSlideView(); + } + + @Override + public void onStart() { + super.onStart(); + activity.setBottomNavigationBarVisibility(true); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_search) { + activity.navController.navigate(R.id.action_libraryFragment_to_searchFragment); + return true; + } else if (item.getItemId() == R.id.action_settings) { + activity.navController.navigate(R.id.action_libraryFragment_to_settingsFragment); + return true; + } + + return false; + } + + private void init() { + bind.albumCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_albumCatalogueFragment)); + bind.artistCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_artistCatalogueFragment)); + bind.genreCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_genreCatalogueFragment)); + bind.playlistCatalogueTextViewClickable.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.PLAYLIST_ALL, Constants.PLAYLIST_ALL); + activity.navController.navigate(R.id.action_libraryFragment_to_playlistCatalogueFragment, bundle); + }); + + bind.albumCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshAlbumSample(getViewLifecycleOwner()); + return true; + }); + bind.artistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshArtistSample(getViewLifecycleOwner()); + return true; + }); + bind.genreCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshGenreSample(getViewLifecycleOwner()); + return true; + }); + bind.playlistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshPlaylistSample(getViewLifecycleOwner()); + return true; + }); + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + } + + private void initMusicFolderView() { + bind.musicFolderRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.musicFolderRecyclerView.setHasFixedSize(true); + + musicFolderAdapter = new MusicFolderAdapter(this); + bind.musicFolderRecyclerView.setAdapter(musicFolderAdapter); + libraryViewModel.getMusicFolders(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), musicFolders -> { + if (musicFolders == null) { + if (bind != null) + bind.libraryMusicFolderPlaceholder.placeholder.setVisibility(View.VISIBLE); + if (bind != null) bind.libraryMusicFolderSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryMusicFolderPlaceholder.placeholder.setVisibility(View.GONE); + if (bind != null) + bind.libraryMusicFolderSector.setVisibility(!musicFolders.isEmpty() ? View.VISIBLE : View.GONE); + + musicFolderAdapter.setItems(musicFolders); + } + }); + } + + private void initAlbumView() { + bind.albumRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.albumRecyclerView.setHasFixedSize(true); + + albumAdapter = new AlbumAdapter(this); + bind.albumRecyclerView.setAdapter(albumAdapter); + libraryViewModel.getAlbumSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { + if (albums == null) { + if (bind != null) + bind.libraryAlbumPlaceholder.placeholder.setVisibility(View.VISIBLE); + if (bind != null) bind.libraryAlbumSector.setVisibility(View.GONE); + } else { + if (bind != null) bind.libraryAlbumPlaceholder.placeholder.setVisibility(View.GONE); + if (bind != null) + bind.libraryAlbumSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); + + albumAdapter.setItems(albums); + } + }); + + CustomLinearSnapHelper albumSnapHelper = new CustomLinearSnapHelper(); + albumSnapHelper.attachToRecyclerView(bind.albumRecyclerView); + } + + private void initArtistView() { + bind.artistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.artistRecyclerView.setHasFixedSize(true); + + artistAdapter = new ArtistAdapter(this, false, false); + bind.artistRecyclerView.setAdapter(artistAdapter); + libraryViewModel.getArtistSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { + if (artists == null) { + if (bind != null) + bind.libraryArtistPlaceholder.placeholder.setVisibility(View.VISIBLE); + if (bind != null) bind.libraryArtistSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryArtistPlaceholder.placeholder.setVisibility(View.GONE); + if (bind != null) + bind.libraryArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); + + artistAdapter.setItems(artists); + } + }); + + CustomLinearSnapHelper artistSnapHelper = new CustomLinearSnapHelper(); + artistSnapHelper.attachToRecyclerView(bind.artistRecyclerView); + } + + private void initGenreView() { + bind.genreRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3, GridLayoutManager.HORIZONTAL, false)); + bind.genreRecyclerView.setHasFixedSize(true); + + genreAdapter = new GenreAdapter(this); + bind.genreRecyclerView.setAdapter(genreAdapter); + + libraryViewModel.getGenreSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), genres -> { + if (genres == null) { + if (bind != null) + bind.libraryGenrePlaceholder.placeholder.setVisibility(View.VISIBLE); + if (bind != null) bind.libraryGenresSector.setVisibility(View.GONE); + } else { + if (bind != null) bind.libraryGenrePlaceholder.placeholder.setVisibility(View.GONE); + if (bind != null) + bind.libraryGenresSector.setVisibility(!genres.isEmpty() ? View.VISIBLE : View.GONE); + + genreAdapter.setItems(genres); + } + }); + + CustomLinearSnapHelper genreSnapHelper = new CustomLinearSnapHelper(); + genreSnapHelper.attachToRecyclerView(bind.genreRecyclerView); + } + + private void initPlaylistSlideView() { + bind.playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.playlistRecyclerView.setHasFixedSize(true); + + playlistHorizontalAdapter = new PlaylistHorizontalAdapter(this); + bind.playlistRecyclerView.setAdapter(playlistHorizontalAdapter); + libraryViewModel.getPlaylistSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), playlists -> { + if (playlists == null) { + if (bind != null) + bind.libraryPlaylistPlaceholder.placeholder.setVisibility(View.VISIBLE); + if (bind != null) bind.libraryPlaylistSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryPlaylistPlaceholder.placeholder.setVisibility(View.GONE); + if (bind != null) + bind.libraryPlaylistSector.setVisibility(!playlists.isEmpty() ? View.VISIBLE : View.GONE); + + playlistHorizontalAdapter.setItems(playlists); + } + }); + } + + @Override + public void onAlbumClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle); + } + + @Override + public void onAlbumLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumBottomSheetDialog, bundle); + } + + @Override + public void onArtistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistPageFragment, bundle); + } + + @Override + public void onArtistLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); + } + + @Override + public void onGenreClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songListPageFragment, bundle); + } + + @Override + public void onPlaylistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.playlistPageFragment, bundle); + } + + @Override + public void onPlaylistLongClick(Bundle bundle) { + PlaylistEditorDialog dialog = new PlaylistEditorDialog(); + dialog.setArguments(bundle); + dialog.show(activity.getSupportFragmentManager(), null); + } + + @Override + public void onMusicFolderClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.indexFragment, bundle); + } +} diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/util/UIUtil.java b/app/src/notquitemy/java/com/cappielloantonio/tempo/util/UIUtil.java new file mode 100644 index 00000000..7462a8b5 --- /dev/null +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/util/UIUtil.java @@ -0,0 +1,34 @@ +package com.cappielloantonio.tempo.util; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; + +import androidx.recyclerview.widget.DividerItemDecoration; + +public class UIUtil { + public static int getSpanCount(int itemCount, int maxSpan) { + int itemSize = itemCount == 0 ? 1 : itemCount; + + if (itemSize / maxSpan > 0) { + return maxSpan; + } else { + return itemSize % maxSpan; + } + } + + public static DividerItemDecoration getDividerItemDecoration(Context context) { + int[] ATTRS = new int[]{android.R.attr.listDivider}; + + TypedArray a = context.obtainStyledAttributes(ATTRS); + Drawable divider = a.getDrawable(0); + InsetDrawable insetDivider = new InsetDrawable(divider, 42, 0, 42, 42); + a.recycle(); + + DividerItemDecoration itemDecoration = new DividerItemDecoration(context, DividerItemDecoration.VERTICAL); + itemDecoration.setDrawable(insetDivider); + + return itemDecoration; + } +} diff --git a/app/src/notquitemy/res/menu/main_page_menu.xml b/app/src/notquitemy/res/menu/main_page_menu.xml new file mode 100644 index 00000000..4016fefa --- /dev/null +++ b/app/src/notquitemy/res/menu/main_page_menu.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt new file mode 100644 index 00000000..944e2ffd --- /dev/null +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -0,0 +1,316 @@ +package com.cappielloantonio.tempo.service + +import android.annotation.SuppressLint +import android.app.PendingIntent.FLAG_IMMUTABLE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.app.TaskStackBuilder +import android.content.Intent +import android.os.Bundle +import androidx.media3.cast.CastPlayer +import androidx.media3.cast.SessionAvailabilityListener +import androidx.media3.common.* +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.session.* +import androidx.media3.session.MediaSession.ControllerInfo +import com.cappielloantonio.tempo.R +import com.cappielloantonio.tempo.ui.activity.MainActivity +import com.cappielloantonio.tempo.util.Constants +import com.cappielloantonio.tempo.util.DownloadUtil +import com.cappielloantonio.tempo.util.UIUtil +import com.google.android.gms.cast.framework.CastContext +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture + + +@UnstableApi +class MediaService : MediaLibraryService(), SessionAvailabilityListener { + private val librarySessionCallback = CustomMediaLibrarySessionCallback() + + private lateinit var player: ExoPlayer + private lateinit var castPlayer: CastPlayer + private lateinit var mediaLibrarySession: MediaLibrarySession + private lateinit var customCommands: List + + private var customLayout = ImmutableList.of() + + companion object { + private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON = + "android.media3.session.demo.SHUFFLE_ON" + private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = + "android.media3.session.demo.SHUFFLE_OFF" + } + + override fun onCreate() { + super.onCreate() + + initializeCustomCommands() + initializePlayer() + initializeCastPlayer() + initializeMediaLibrarySession() + initializePlayerListener() + + setPlayer( + null, + if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else player + ) + } + + override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession { + return mediaLibrarySession + } + + override fun onDestroy() { + releasePlayer() + super.onDestroy() + } + + private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback { + + override fun onConnect( + session: MediaSession, + controller: ControllerInfo + ): MediaSession.ConnectionResult { + val connectionResult = super.onConnect(session, controller) + val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() + + customCommands.forEach { commandButton -> + // TODO: Aggiungere i comandi personalizzati + // commandButton.sessionCommand?.let { availableSessionCommands.add(it) } + } + + return MediaSession.ConnectionResult.accept( + availableSessionCommands.build(), + connectionResult.availablePlayerCommands + ) + } + + override fun onPostConnect(session: MediaSession, controller: ControllerInfo) { + if (!customLayout.isEmpty() && controller.controllerVersion != 0) { + ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout)) + } + } + + override fun onCustomCommand( + session: MediaSession, + controller: ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) { + player.shuffleModeEnabled = true + customLayout = ImmutableList.of(customCommands[1]) + session.setCustomLayout(customLayout) + } else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) { + player.shuffleModeEnabled = false + customLayout = ImmutableList.of(customCommands[0]) + session.setCustomLayout(customLayout) + } + + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + + /* override fun onGetLibraryRoot( + session: MediaLibrarySession, + browser: ControllerInfo, + params: LibraryParams? + ): ListenableFuture> { + return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params)) + } + + override fun onGetItem( + session: MediaLibrarySession, + browser: ControllerInfo, + mediaId: String + ): ListenableFuture> { + val item = + MediaItemTree.getItem(mediaId) + ?: return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null)) + } + + override fun onSubscribe( + session: MediaLibrarySession, + browser: ControllerInfo, + parentId: String, + params: LibraryParams? + ): ListenableFuture> { + val children = + MediaItemTree.getChildren(parentId) + ?: return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + session.notifyChildrenChanged(browser, parentId, children.size, params) + return Futures.immediateFuture(LibraryResult.ofVoid()) + } + + override fun onGetChildren( + session: MediaLibrarySession, + browser: ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: LibraryParams? + ): ListenableFuture>> { + val children = + MediaItemTree.getChildren(parentId) + ?: return Futures.immediateFuture( + LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) + ) + + return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) + }*/ + + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: ControllerInfo, + mediaItems: List + ): ListenableFuture> { + val updatedMediaItems = mediaItems.map { + it.buildUpon() + .setUri(it.requestMetadata.mediaUri) + .setMediaMetadata(it.mediaMetadata) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .build() + } + return Futures.immediateFuture(updatedMediaItems) + } + } + + private fun initializeCustomCommands() { + customCommands = + listOf( + getShuffleCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY) + ), + getShuffleCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY) + ) + ) + + customLayout = ImmutableList.of(customCommands[0]) + } + + private fun initializePlayer() { + player = ExoPlayer.Builder(this) + .setRenderersFactory(getRenderersFactory()) + .setMediaSourceFactory(getMediaSourceFactory()) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setHandleAudioBecomingNoisy(true) + .setWakeMode(C.WAKE_MODE_NETWORK) + .build() + } + + private fun initializeCastPlayer() { + if (UIUtil.isCastApiAvailable(this)) { + castPlayer = CastPlayer(CastContext.getSharedInstance(this)) + castPlayer.setSessionAvailabilityListener(this) + } + } + + private fun initializeMediaLibrarySession() { + val sessionActivityPendingIntent = + TaskStackBuilder.create(this).run { + addNextIntent(Intent(this@MediaService, MainActivity::class.java)) + getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT) + } + + mediaLibrarySession = + MediaLibrarySession.Builder(this, player, librarySessionCallback) + .setSessionActivity(sessionActivityPendingIntent) + .build() + + if (!customLayout.isEmpty()) { + mediaLibrarySession.setCustomLayout(customLayout) + } + } + + private fun initializePlayerListener() { + player.addListener(object : Player.Listener { + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + if (mediaItem == null) return + + if(reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { + MediaManager.setLastPlayedTimestamp(mediaItem) + } + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + if (!isPlaying) { + MediaManager.setPlayingPausedTimestamp( + player.currentMediaItem, + player.currentPosition + ) + } + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + super.onPositionDiscontinuity(oldPosition, newPosition, reason) + + if(reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { + if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { + MediaManager.scrobble(oldPosition.mediaItem) + MediaManager.saveChronology(oldPosition.mediaItem) + } + + if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { + MediaManager.setLastPlayedTimestamp(newPosition.mediaItem) + } + } + } + }) + } + + private fun setPlayer(oldPlayer: Player?, newPlayer: Player) { + if (oldPlayer === newPlayer) return + oldPlayer?.stop() + mediaLibrarySession.player = newPlayer + } + + private fun releasePlayer() { + if (this::castPlayer.isInitialized) castPlayer.setSessionAvailabilityListener(null) + if (this::castPlayer.isInitialized) castPlayer.release() + player.release() + mediaLibrarySession.release() + } + + @SuppressLint("PrivateResource") + private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton { + val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON + return CommandButton.Builder() + .setDisplayName( + getString( + if (isOn) R.string.exo_controls_shuffle_on_description + else R.string.exo_controls_shuffle_off_description + ) + ) + .setSessionCommand(sessionCommand) + .setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on) + .build() + } + + private fun ignoreFuture(customLayout: ListenableFuture) { + /* Do nothing. */ + } + + private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) + + private fun getMediaSourceFactory() = + DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this)) + + override fun onCastSessionAvailable() { + setPlayer(player, castPlayer) + } + + override fun onCastSessionUnavailable() { + setPlayer(castPlayer, player) + } +} \ No newline at end of file diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java b/app/src/tempo/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java new file mode 100644 index 00000000..7a984236 --- /dev/null +++ b/app/src/tempo/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java @@ -0,0 +1,108 @@ +package com.cappielloantonio.tempo.ui.activity.base; + +import android.Manifest; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.offline.DownloadService; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.service.DownloaderService; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.dialog.BatteryOptimizationDialog; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.util.UIUtil; +import com.google.android.gms.cast.framework.CastContext; +import com.google.android.material.elevation.SurfaceColors; +import com.google.common.util.concurrent.ListenableFuture; + +@UnstableApi +public class BaseActivity extends AppCompatActivity { + private static final String TAG = "BaseActivity"; + + private ListenableFuture mediaBrowserListenableFuture; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + initializeCastContext(); + initializeDownloader(); + checkBatteryOptimization(); + checkPermission(); + } + + @Override + protected void onStart() { + super.onStart(); + setNavigationBarColor(); + initializeBrowser(); + } + + @Override + protected void onStop() { + releaseBrowser(); + super.onStop(); + } + + private void checkBatteryOptimization() { + if (detectBatteryOptimization() && Preferences.askForOptimization()) { + showBatteryOptimizationDialog(); + } + } + + private void checkPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, 101); + } + } + } + + private boolean detectBatteryOptimization() { + String packageName = getPackageName(); + PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); + return !powerManager.isIgnoringBatteryOptimizations(packageName); + } + + private void showBatteryOptimizationDialog() { + BatteryOptimizationDialog dialog = new BatteryOptimizationDialog(); + dialog.show(getSupportFragmentManager(), null); + } + + private void initializeBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(this, new SessionToken(this, new ComponentName(this, MediaService.class))).buildAsync(); + } + + private void releaseBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + public ListenableFuture getMediaBrowserListenableFuture() { + return mediaBrowserListenableFuture; + } + + private void initializeDownloader() { + try { + DownloadService.start(this, DownloaderService.class); + } catch (IllegalStateException e) { + DownloadService.startForeground(this, DownloaderService.class); + } + } + + private void initializeCastContext() { + if (UIUtil.isCastApiAvailable(this)) CastContext.getSharedInstance(this); + } + + private void setNavigationBarColor() { + getWindow().setNavigationBarColor(SurfaceColors.getColorForElevation(this, 10)); + } +} diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java b/app/src/tempo/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java new file mode 100644 index 00000000..2ad61fd5 --- /dev/null +++ b/app/src/tempo/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java @@ -0,0 +1,170 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentDownloadBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.DownloadViewModel; +import com.google.android.gms.cast.framework.CastButtonFactory; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.Objects; + +@UnstableApi +public class DownloadFragment extends Fragment implements ClickCallback { + private FragmentDownloadBinding bind; + private MainActivity activity; + private DownloadViewModel downloadViewModel; + + private DownloadHorizontalAdapter downloadHorizontalAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.main_page_menu, menu); + CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentDownloadBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + downloadViewModel = new ViewModelProvider(requireActivity()).get(DownloadViewModel.class); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initAppBar(); + initDownloadedSongView(); + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + activity.setBottomNavigationBarVisibility(true); + activity.setBottomSheetVisibility(true); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_search) { + activity.navController.navigate(R.id.action_downloadFragment_to_searchFragment); + return true; + } else if (item.getItemId() == R.id.action_settings) { + activity.navController.navigate(R.id.action_downloadFragment_to_settingsFragment); + return true; + } + + return false; + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + } + + private void initDownloadedSongView() { + bind.downloadedTracksRecyclerView.setHasFixedSize(true); + + downloadHorizontalAdapter = new DownloadHorizontalAdapter(this); + bind.downloadedTracksRecyclerView.setAdapter(downloadHorizontalAdapter); + downloadViewModel.getDownloadedTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> { + if (songs != null) { + if (songs.isEmpty()) { + if (bind != null) { + bind.emptyDownloadLayout.setVisibility(View.VISIBLE); + bind.fragmentDownloadNestedScrollView.setVisibility(View.GONE); + + bind.downloadDownloadedTracksPlaceholder.placeholder.setVisibility(View.VISIBLE); + bind.downloadDownloadedTracksSector.setVisibility(View.GONE); + } + } else { + if (bind != null) { + bind.emptyDownloadLayout.setVisibility(View.GONE); + bind.fragmentDownloadNestedScrollView.setVisibility(View.VISIBLE); + + bind.downloadDownloadedTracksPlaceholder.placeholder.setVisibility(View.GONE); + bind.downloadDownloadedTracksSector.setVisibility(View.VISIBLE); + + bind.downloadedTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + + downloadHorizontalAdapter.setItems(songs); + } + } + + if (bind != null) bind.loadingProgressBar.setVisibility(View.GONE); + } + }); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onMediaClick(Bundle bundle) { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onMediaLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); + } +} diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java b/app/src/tempo/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java new file mode 100644 index 00000000..a88e0bf5 --- /dev/null +++ b/app/src/tempo/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java @@ -0,0 +1,118 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentHomeBinding; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.fragment.pager.HomePager; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.gms.cast.framework.CastButtonFactory; +import com.google.android.material.tabs.TabLayoutMediator; + +import java.util.Objects; + +@UnstableApi +public class HomeFragment extends Fragment { + private static final String TAG = "HomeFragment"; + + private FragmentHomeBinding bind; + private MainActivity activity; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.main_page_menu, menu); + CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + bind = FragmentHomeBinding.inflate(inflater, container, false); + return bind.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initAppBar(); + initHomePager(); + } + + @Override + public void onStart() { + super.onStart(); + + activity.setBottomNavigationBarVisibility(true); + activity.setBottomSheetVisibility(true); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_search) { + activity.navController.navigate(R.id.action_homeFragment_to_searchFragment); + return true; + } else if (item.getItemId() == R.id.action_settings) { + activity.navController.navigate(R.id.action_homeFragment_to_settingsFragment); + return true; + } + + return false; + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + } + + private void initHomePager() { + HomePager pager = new HomePager(this); + + pager.addFragment(new HomeTabMusicFragment(), "Music", R.drawable.ic_home); + + if (Preferences.isPodcastSectionVisible()) + pager.addFragment(new HomeTabPodcastFragment(), "Podcast", R.drawable.ic_graphic_eq); + + if (Preferences.isRadioSectionVisible()) + pager.addFragment(new HomeTabRadioFragment(), "Radio", R.drawable.ic_play_for_work); + + bind.homeViewPager.setAdapter(pager); + bind.homeViewPager.setOffscreenPageLimit(3); + bind.homeViewPager.setUserInputEnabled(false); + + new TabLayoutMediator(bind.homeTabLayout, bind.homeViewPager, + (tab, position) -> { + tab.setText(pager.getPageTitle(position)); + // tab.setIcon(pager.getPageIcon(position)); + } + ).attach(); + + bind.homeTabLayout.setVisibility(Preferences.isPodcastSectionVisible() || Preferences.isRadioSectionVisible() ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java b/app/src/tempo/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java new file mode 100644 index 00000000..6ceef14c --- /dev/null +++ b/app/src/tempo/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java @@ -0,0 +1,308 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentLibraryBinding; +import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter; +import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter; +import com.cappielloantonio.tempo.ui.adapter.GenreAdapter; +import com.cappielloantonio.tempo.ui.adapter.MusicFolderAdapter; +import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter; +import com.cappielloantonio.tempo.ui.dialog.PlaylistEditorDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.LibraryViewModel; +import com.google.android.gms.cast.framework.CastButtonFactory; + +import java.util.Objects; + +@UnstableApi +public class LibraryFragment extends Fragment implements ClickCallback { + private static final String TAG = "LibraryFragment"; + + private FragmentLibraryBinding bind; + private MainActivity activity; + private LibraryViewModel libraryViewModel; + + private MusicFolderAdapter musicFolderAdapter; + private AlbumAdapter albumAdapter; + private ArtistAdapter artistAdapter; + private GenreAdapter genreAdapter; + + private PlaylistHorizontalAdapter playlistHorizontalAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.main_page_menu, menu); + CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentLibraryBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + libraryViewModel = new ViewModelProvider(requireActivity()).get(LibraryViewModel.class); + + init(); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initAppBar(); + initMusicFolderView(); + initAlbumView(); + initArtistView(); + initGenreView(); + initPlaylistSlideView(); + } + + @Override + public void onStart() { + super.onStart(); + activity.setBottomNavigationBarVisibility(true); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_search) { + activity.navController.navigate(R.id.action_libraryFragment_to_searchFragment); + return true; + } else if (item.getItemId() == R.id.action_settings) { + activity.navController.navigate(R.id.action_libraryFragment_to_settingsFragment); + return true; + } + + return false; + } + + private void init() { + bind.albumCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_albumCatalogueFragment)); + bind.artistCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_artistCatalogueFragment)); + bind.genreCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_genreCatalogueFragment)); + bind.playlistCatalogueTextViewClickable.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.PLAYLIST_ALL, Constants.PLAYLIST_ALL); + activity.navController.navigate(R.id.action_libraryFragment_to_playlistCatalogueFragment, bundle); + }); + + bind.albumCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshAlbumSample(getViewLifecycleOwner()); + return true; + }); + bind.artistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshArtistSample(getViewLifecycleOwner()); + return true; + }); + bind.genreCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshGenreSample(getViewLifecycleOwner()); + return true; + }); + bind.playlistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshPlaylistSample(getViewLifecycleOwner()); + return true; + }); + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + } + + private void initMusicFolderView() { + bind.musicFolderRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.musicFolderRecyclerView.setHasFixedSize(true); + + musicFolderAdapter = new MusicFolderAdapter(this); + bind.musicFolderRecyclerView.setAdapter(musicFolderAdapter); + libraryViewModel.getMusicFolders(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), musicFolders -> { + if (musicFolders == null) { + if (bind != null) + bind.libraryMusicFolderPlaceholder.placeholder.setVisibility(View.VISIBLE); + if (bind != null) bind.libraryMusicFolderSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryMusicFolderPlaceholder.placeholder.setVisibility(View.GONE); + if (bind != null) + bind.libraryMusicFolderSector.setVisibility(!musicFolders.isEmpty() ? View.VISIBLE : View.GONE); + + musicFolderAdapter.setItems(musicFolders); + } + }); + } + + private void initAlbumView() { + bind.albumRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.albumRecyclerView.setHasFixedSize(true); + + albumAdapter = new AlbumAdapter(this); + bind.albumRecyclerView.setAdapter(albumAdapter); + libraryViewModel.getAlbumSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { + if (albums == null) { + if (bind != null) + bind.libraryAlbumPlaceholder.placeholder.setVisibility(View.VISIBLE); + if (bind != null) bind.libraryAlbumSector.setVisibility(View.GONE); + } else { + if (bind != null) bind.libraryAlbumPlaceholder.placeholder.setVisibility(View.GONE); + if (bind != null) + bind.libraryAlbumSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); + + albumAdapter.setItems(albums); + } + }); + + CustomLinearSnapHelper albumSnapHelper = new CustomLinearSnapHelper(); + albumSnapHelper.attachToRecyclerView(bind.albumRecyclerView); + } + + private void initArtistView() { + bind.artistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.artistRecyclerView.setHasFixedSize(true); + + artistAdapter = new ArtistAdapter(this, false, false); + bind.artistRecyclerView.setAdapter(artistAdapter); + libraryViewModel.getArtistSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { + if (artists == null) { + if (bind != null) + bind.libraryArtistPlaceholder.placeholder.setVisibility(View.VISIBLE); + if (bind != null) bind.libraryArtistSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryArtistPlaceholder.placeholder.setVisibility(View.GONE); + if (bind != null) + bind.libraryArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); + + artistAdapter.setItems(artists); + } + }); + + CustomLinearSnapHelper artistSnapHelper = new CustomLinearSnapHelper(); + artistSnapHelper.attachToRecyclerView(bind.artistRecyclerView); + } + + private void initGenreView() { + bind.genreRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3, GridLayoutManager.HORIZONTAL, false)); + bind.genreRecyclerView.setHasFixedSize(true); + + genreAdapter = new GenreAdapter(this); + bind.genreRecyclerView.setAdapter(genreAdapter); + + libraryViewModel.getGenreSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), genres -> { + if (genres == null) { + if (bind != null) + bind.libraryGenrePlaceholder.placeholder.setVisibility(View.VISIBLE); + if (bind != null) bind.libraryGenresSector.setVisibility(View.GONE); + } else { + if (bind != null) bind.libraryGenrePlaceholder.placeholder.setVisibility(View.GONE); + if (bind != null) + bind.libraryGenresSector.setVisibility(!genres.isEmpty() ? View.VISIBLE : View.GONE); + + genreAdapter.setItems(genres); + } + }); + + CustomLinearSnapHelper genreSnapHelper = new CustomLinearSnapHelper(); + genreSnapHelper.attachToRecyclerView(bind.genreRecyclerView); + } + + private void initPlaylistSlideView() { + bind.playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.playlistRecyclerView.setHasFixedSize(true); + + playlistHorizontalAdapter = new PlaylistHorizontalAdapter(this); + bind.playlistRecyclerView.setAdapter(playlistHorizontalAdapter); + libraryViewModel.getPlaylistSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), playlists -> { + if (playlists == null) { + if (bind != null) + bind.libraryPlaylistPlaceholder.placeholder.setVisibility(View.VISIBLE); + if (bind != null) bind.libraryPlaylistSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryPlaylistPlaceholder.placeholder.setVisibility(View.GONE); + if (bind != null) + bind.libraryPlaylistSector.setVisibility(!playlists.isEmpty() ? View.VISIBLE : View.GONE); + + playlistHorizontalAdapter.setItems(playlists); + } + }); + } + + @Override + public void onAlbumClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle); + } + + @Override + public void onAlbumLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumBottomSheetDialog, bundle); + } + + @Override + public void onArtistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistPageFragment, bundle); + } + + @Override + public void onArtistLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); + } + + @Override + public void onGenreClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songListPageFragment, bundle); + } + + @Override + public void onPlaylistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.playlistPageFragment, bundle); + } + + @Override + public void onPlaylistLongClick(Bundle bundle) { + PlaylistEditorDialog dialog = new PlaylistEditorDialog(); + dialog.setArguments(bundle); + dialog.show(activity.getSupportFragmentManager(), null); + } + + @Override + public void onMusicFolderClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.indexFragment, bundle); + } +} diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/util/UIUtil.java b/app/src/tempo/java/com/cappielloantonio/tempo/util/UIUtil.java new file mode 100644 index 00000000..99a37eb5 --- /dev/null +++ b/app/src/tempo/java/com/cappielloantonio/tempo/util/UIUtil.java @@ -0,0 +1,41 @@ +package com.cappielloantonio.tempo.util; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; + +import androidx.recyclerview.widget.DividerItemDecoration; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; + +public class UIUtil { + public static int getSpanCount(int itemCount, int maxSpan) { + int itemSize = itemCount == 0 ? 1 : itemCount; + + if (itemSize / maxSpan > 0) { + return maxSpan; + } else { + return itemSize % maxSpan; + } + } + + public static boolean isCastApiAvailable(Context context) { + return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS; + } + + public static DividerItemDecoration getDividerItemDecoration(Context context) { + int[] ATTRS = new int[]{android.R.attr.listDivider}; + + TypedArray a = context.obtainStyledAttributes(ATTRS); + Drawable divider = a.getDrawable(0); + InsetDrawable insetDivider = new InsetDrawable(divider, 42, 0, 42, 42); + a.recycle(); + + DividerItemDecoration itemDecoration = new DividerItemDecoration(context, DividerItemDecoration.VERTICAL); + itemDecoration.setDrawable(insetDivider); + + return itemDecoration; + } +}