From 3dd97c9925bad6e7b28b73bca8ed988a0d450fd6 Mon Sep 17 00:00:00 2001 From: Devin Duricka Date: Sat, 15 Oct 2022 19:11:12 -0700 Subject: [PATCH 1/3] Okay, Media3 is running --- app/build.gradle | 20 +- app/src/main/AndroidManifest.xml | 12 +- app/src/main/java/one/fable/fable/Fable.kt | 10 +- .../main/java/one/fable/fable/MainActivity.kt | 22 +- .../AudiobookPlayerFragment.kt | 68 ++- .../fable/exoplayer/AudioPlayerService.kt | 422 +++++++++--------- .../exoplayer/CustomPlayerControlView.kt | 11 +- .../fable/exoplayer/DescriptionAdapter.kt | 142 +++--- .../fable/exoplayer/ExoPlayerMasterObject.kt | 52 ++- .../fable/exoplayer/MediaPlayerService.kt | 18 + .../fable/fable/library/LibraryFragment.kt | 68 +-- .../res/layout/exo_player_control_view.xml | 2 +- app/src/main/res/layout/library_fragment.xml | 14 +- app/src/main/res/navigation/navigation.xml | 5 +- build.gradle | 8 +- 15 files changed, 446 insertions(+), 428 deletions(-) create mode 100644 app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt diff --git a/app/build.gradle b/app/build.gradle index 9d8e653..e216a43 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' android { @@ -13,20 +13,17 @@ android { jvmTarget = JavaVersion.VERSION_1_8.toString() } - compileSdkVersion 29 - buildToolsVersion "29.0.3" + compileSdkVersion 33 defaultConfig { applicationId "one.fable.fable" minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 33 versionCode 6 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - apply plugin: 'kotlin-kapt' - buildTypes { release { @@ -35,6 +32,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + dataBinding { enabled = true } @@ -66,8 +64,10 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03' //ExoPlayer - implementation 'com.google.android.exoplayer:exoplayer:2.11.4' - implementation 'com.google.android.exoplayer:extension-mediasession:2.10.0' + implementation "androidx.media3:media3-exoplayer:1.0.0-beta02" + implementation "androidx.media3:media3-ui:1.0.0-beta02" + implementation "androidx.media3:media3-exoplayer-dash:1.0.0-beta02" + implementation 'androidx.media3:media3-session:1.0.0-beta02' //Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5" @@ -85,9 +85,9 @@ dependencies { implementation "androidx.preference:preference-ktx:1.1.1" //Tutorial item highlight library - implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0' + implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3' //Palette API for images - implementation 'androidx.palette:palette:1.0.0' + implementation 'androidx.palette:palette-ktx:1.0.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 34af9e5..b67ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,8 @@ android:supportsRtl="true" android:theme="@style/AppTheme" android:name=".Fable"> - + @@ -24,7 +25,14 @@ - + + + + + \ No newline at end of file diff --git a/app/src/main/java/one/fable/fable/Fable.kt b/app/src/main/java/one/fable/fable/Fable.kt index bdb4d9f..f2c484b 100644 --- a/app/src/main/java/one/fable/fable/Fable.kt +++ b/app/src/main/java/one/fable/fable/Fable.kt @@ -1,14 +1,11 @@ package one.fable.fable import android.app.Application -import android.content.Intent -import android.media.AudioManager import androidx.appcompat.app.AppCompatDelegate import androidx.preference.PreferenceManager import one.fable.fable.database.FableDatabase import one.fable.fable.database.daos.AudiobookDao import one.fable.fable.database.daos.DirectoryDao -import one.fable.fable.exoplayer.AudioPlayerService import one.fable.fable.exoplayer.ExoPlayerMasterObject import timber.log.Timber @@ -19,8 +16,13 @@ class Fable : Application() { lateinit var directoryDao: DirectoryDao fun isdirectoriesDaoInitialized() :Boolean = this::directoryDao.isInitialized + companion object { + lateinit var instance: Fable private set + } + override fun onCreate() { super.onCreate() + instance = this Timber.plant(Timber.DebugTree()) initalizeDependecies() @@ -34,8 +36,6 @@ class Fable : Application() { audiobookDao = FableDatabase.getInstance(applicationContext).audiobookDao directoryDao = FableDatabase.getInstance(applicationContext).directoryDao - ExoPlayerMasterObject.applicationContext = applicationContext - ExoPlayerMasterObject.buildExoPlayer(applicationContext) ExoPlayerMasterObject.setSharedPreferencesAndListener(PreferenceManager.getDefaultSharedPreferences(applicationContext)) ExoPlayerMasterObject.audiobookDao = audiobookDao } diff --git a/app/src/main/java/one/fable/fable/MainActivity.kt b/app/src/main/java/one/fable/fable/MainActivity.kt index e370606..bc48e86 100644 --- a/app/src/main/java/one/fable/fable/MainActivity.kt +++ b/app/src/main/java/one/fable/fable/MainActivity.kt @@ -1,5 +1,6 @@ package one.fable.fable +import android.content.ComponentName import android.content.Intent import android.media.AudioManager import android.media.MediaMetadataRetriever @@ -8,16 +9,19 @@ import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.provider.DocumentsContract import android.provider.MediaStore +import android.widget.MediaController import android.widget.Toast import androidx.documentfile.provider.DocumentFile +import androidx.media3.session.SessionToken import androidx.navigation.Navigation import androidx.navigation.fragment.NavHostFragment import androidx.palette.graphics.Palette import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTargetView +import com.google.common.util.concurrent.MoreExecutors import kotlinx.coroutines.* import one.fable.fable.database.entities.* -import one.fable.fable.exoplayer.AudioPlayerService +import one.fable.fable.exoplayer.PlaybackService import one.fable.fable.library.flags import timber.log.Timber import java.util.concurrent.TimeUnit @@ -72,14 +76,18 @@ class MainActivity : AppCompatActivity() { // val navOptions = NavOptions.Builder().setPopUpTo(R.id.libraryFragment, true).build() // navController.navigate(R.id.libraryFragment, null, navOptions) // } - startAudioPlayerService() + //startAudioPlayerService() + val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java)) + val mediaControllerFuture = androidx.media3.session.MediaController.Builder(this, sessionToken).buildAsync() + mediaControllerFuture.addListener({}, MoreExecutors.directExecutor()) + super.onStart() } - fun startAudioPlayerService(){ - val intent = Intent(applicationContext, AudioPlayerService::class.java) - startService(intent) - } +// fun startAudioPlayerService(){ +// val intent = Intent(applicationContext, AudioPlayerService::class.java) +// startService(intent) +// } fun takePersistablePermissionsToDatabaseEntries(directoryUri : Uri){ @@ -206,7 +214,7 @@ class MainActivity : AppCompatActivity() { if(isLocal(authority)){ val mediaMetadataRetriever = MediaMetadataRetriever() mediaMetadataRetriever.setDataSource(applicationContext, childUri) - duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong() + duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() (mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM))?.let { title = it } author = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) (mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE))?.let { trackTitle = it } diff --git a/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt b/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt index a8200ee..c2bfb1d 100644 --- a/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt +++ b/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt @@ -19,19 +19,13 @@ import androidx.core.view.doOnPreDraw import androidx.core.view.marginTop import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer - -import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTargetSequence -import com.getkeepsafe.taptargetview.TapTargetView -import kotlinx.android.synthetic.main.exo_player_control_view.view.* -import one.fable.fable.MainActivity import one.fable.fable.R import one.fable.fable.databinding.AudiobookPlayerFragmentBinding -import one.fable.fable.exoplayer.AudioPlayerService import one.fable.fable.exoplayer.ExoPlayerMasterObject import timber.log.Timber import java.util.concurrent.TimeUnit @@ -49,7 +43,7 @@ class AudiobookPlayerFragment : Fragment(R.layout.audiobook_player_fragment) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - (activity as MainActivity).startAudioPlayerService() + //(activity as MainActivity).startAudioPlayerService() super.onViewCreated(view, savedInstanceState) @@ -79,36 +73,36 @@ class AudiobookPlayerFragment : Fragment(R.layout.audiobook_player_fragment) { //https://github.com/google/ExoPlayer/issues/6741 //binding.exoplayer.setShowMultiWindowTimeBar(true) - val chapterNameObserver = Observer { - binding.exoplayer.track_name.text = it - } - ExoPlayerMasterObject.chapterName.observe(viewLifecycleOwner, chapterNameObserver) - - binding.exoplayer.track_name_card.setOnClickListener { - - //view.height -// var trackLocationOnScreen = IntArray(2) -// binding.exoplayer.track_name.getLocationInWindow(trackLocationOnScreen) - //binding.exoplayer.trackName.getLocationOnScreen(trackLocationOnScreen) - //binding.exoplayer.trackName.getOffsetForPosition(x, y) -// Timber.i("Track selector is at this point: " + trackLocationOnScreen[0]) -// Timber.i("Track selector is at this point: " + trackLocationOnScreen[1]) -// Timber.i("Fragment Height: " + view.height) - - - val extras = FragmentNavigatorExtras( - binding.exoplayer.track_name to "track_name", - binding.exoplayer.track_name_card to "track_name_card" - ) - - - findNavController().navigate(R.id.action_audiobookPlayerFragment_to_trackListFragment, null, null, extras) - - - //binding.exoplayer - - //findNavController().navigate(AudiobookPlayerFragmentDirections.actionAudiobookPlayerFragmentToTrackListFragment(view.height/2), extras) - } +// val chapterNameObserver = Observer { +// binding.exoplayer.track_name.text = it +// } +// ExoPlayerMasterObject.chapterName.observe(viewLifecycleOwner, chapterNameObserver) +// +// binding.exoplayer.track_name_card.setOnClickListener { +// +// //view.height +//// var trackLocationOnScreen = IntArray(2) +//// binding.exoplayer.track_name.getLocationInWindow(trackLocationOnScreen) +// //binding.exoplayer.trackName.getLocationOnScreen(trackLocationOnScreen) +// //binding.exoplayer.trackName.getOffsetForPosition(x, y) +//// Timber.i("Track selector is at this point: " + trackLocationOnScreen[0]) +//// Timber.i("Track selector is at this point: " + trackLocationOnScreen[1]) +//// Timber.i("Fragment Height: " + view.height) +// +// +// val extras = FragmentNavigatorExtras( +// binding.exoplayer.track_name to "track_name", +// binding.exoplayer.track_name_card to "track_name_card" +// ) +// +// +// findNavController().navigate(R.id.action_audiobookPlayerFragment_to_trackListFragment, null, null, extras) +// +// +// //binding.exoplayer +// +// //findNavController().navigate(AudiobookPlayerFragmentDirections.actionAudiobookPlayerFragmentToTrackListFragment(view.height/2), extras) +// } binding.audiobookPlayerAppBar.setOnMenuItemClickListener { item: MenuItem? -> when (item?.itemId){ diff --git a/app/src/main/java/one/fable/fable/exoplayer/AudioPlayerService.kt b/app/src/main/java/one/fable/fable/exoplayer/AudioPlayerService.kt index 35399c2..f785be2 100644 --- a/app/src/main/java/one/fable/fable/exoplayer/AudioPlayerService.kt +++ b/app/src/main/java/one/fable/fable/exoplayer/AudioPlayerService.kt @@ -1,217 +1,217 @@ -package one.fable.fable.exoplayer - -import android.app.* -import android.content.Context -import android.content.Intent -import android.media.session.MediaSession -import android.net.Uri -import android.os.Binder -import android.os.Build -import android.os.IBinder -import android.support.v4.media.MediaDescriptionCompat -import android.support.v4.media.session.MediaSessionCompat -import com.google.android.exoplayer2.C -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.SimpleExoPlayer -import com.google.android.exoplayer2.audio.AudioAttributes -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -import com.google.android.exoplayer2.source.MediaSource -import com.google.android.exoplayer2.source.MediaSourceFactory -import com.google.android.exoplayer2.ui.PlayerNotificationManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import one.fable.fable.R -import timber.log.Timber - -class AudioPlayerService : Service() { - lateinit var exoPlayer : SimpleExoPlayer - lateinit var exoPlayerNotificationManager : PlayerNotificationManager - lateinit var mediaSession: MediaSessionCompat - lateinit var mediaSessionConnector: MediaSessionConnector //You need to also depend on com.google.android.exoplayer:extension-mediasession:2.6.1 - - val eventListener = ExoPlayerEventListener() - - val binder = LocalBinder() - override fun onTaskRemoved(rootIntent: Intent?) { - Timber.i("Task Removed") - stopForeground(true) - exoPlayer.playWhenReady = false - //exoPlayer.stop() - stopSelf() - //ExoPlayerInterface. - - - super.onTaskRemoved(rootIntent) - } - - inner class LocalBinder : Binder() { - fun getService() : AudioPlayerService = this@AudioPlayerService - } - - inner class ExoPlayerEventListener() : Player.EventListener{ - var isPlayingBool = false - - override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - isPlayingBool = isPlaying - Timber.i("Is playing changed to: " + isPlaying) - if(!isPlaying){ - //Timber.i("Remove the notification") - - //I've pretty much figured this out, but felt I should make note of where I found the info - //https://stackoverflow.com/questions/43596709/make-a-notification-from-a-foreground-service-cancelable-once-the-service-is-not - //https://github.com/google/ExoPlayer/issues/4256 - stopForeground(false) - CoroutineScope(Dispatchers.IO).launch { - ExoPlayerMasterObject.updateAudiobook() - } - } - - } - } - -// override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { -// Timber.i("OnStartCommand Called") -// return START_STICKY -// //return super.onStartCommand(intent, flags, startId) +//package one.fable.fable.exoplayer +// +//import android.app.* +//import android.content.Context +//import android.content.Intent +//import android.media.session.MediaSession +//import android.net.Uri +//import android.os.Binder +//import android.os.Build +//import android.os.IBinder +//import android.support.v4.media.MediaDescriptionCompat +//import android.support.v4.media.session.MediaSessionCompat +//import com.google.android.exoplayer2.C +//import com.google.android.exoplayer2.Player +//import com.google.android.exoplayer2.SimpleExoPlayer +//import com.google.android.exoplayer2.audio.AudioAttributes +//import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector +//import com.google.android.exoplayer2.source.MediaSource +//import com.google.android.exoplayer2.source.MediaSourceFactory +//import com.google.android.exoplayer2.ui.PlayerNotificationManager +//import kotlinx.coroutines.CoroutineScope +//import kotlinx.coroutines.Dispatchers +//import kotlinx.coroutines.launch +//import one.fable.fable.R +//import timber.log.Timber +// +//class AudioPlayerService : Service() { +// lateinit var exoPlayer : SimpleExoPlayer +// lateinit var exoPlayerNotificationManager : PlayerNotificationManager +// lateinit var mediaSession: MediaSessionCompat +// lateinit var mediaSessionConnector: MediaSessionConnector //You need to also depend on com.google.android.exoplayer:extension-mediasession:2.6.1 +// +// val eventListener = ExoPlayerEventListener() +// +// val binder = LocalBinder() +// override fun onTaskRemoved(rootIntent: Intent?) { +// Timber.i("Task Removed") +// stopForeground(true) +// exoPlayer.playWhenReady = false +// //exoPlayer.stop() +// stopSelf() +// //ExoPlayerInterface. +// +// +// super.onTaskRemoved(rootIntent) // } - - override fun onCreate() { - - Timber.i("On create Called") - super.onCreate() - exoPlayer = ExoPlayerMasterObject.exoPlayer - exoPlayer.addListener(eventListener) - //(application as Fable).exoPlayer = exoPlayer - - Timber.i("ExoPlayer Attached to the interface") - - - //https://medium.com/google-exoplayer/exoplayer-2-11-whats-new-e0e0701e4b6c - exoPlayer.setWakeMode(C.WAKE_MODE_NETWORK) - exoPlayer.setHandleAudioBecomingNoisy(true) - - - //https://medium.com/google-exoplayer/easy-audio-focus-with-exoplayer-a2dcbbe4640e - val audioBookAudioAttributes = AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) - .setContentType(C.CONTENT_TYPE_SPEECH) - .build() - - exoPlayer.setAudioAttributes(audioBookAudioAttributes, true) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Create the NotificationChannel - val name = "Audiobook Playback" - val descriptionText = "Fable audiobook player" - val importance = NotificationManager.IMPORTANCE_DEFAULT - val mChannel = NotificationChannel("PLAYBACK", name, importance) - mChannel.description = descriptionText - mChannel.setSound(null, null) - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - val notificationManager = getSystemService(Application.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(mChannel) - } - - //https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/PlayerNotificationManager.html - //https://medium.com/google-exoplayer/playback-notifications-with-exoplayer-a2f1a18cf93b - exoPlayerNotificationManager = PlayerNotificationManager(applicationContext, "PLAYBACK", 1, DescriptionAdapter(applicationContext), object: - PlayerNotificationManager.NotificationListener { - - override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) { - super.onNotificationCancelled(notificationId, dismissedByUser) - //exoPlayer.stop() - //stopSelf() - } - - - override fun onNotificationPosted( - notificationId: Int, - notification: Notification, - ongoing: Boolean - ) { - super.onNotificationPosted(notificationId, notification, ongoing) - - //if(eventListener.isPlayingBool){ - if(exoPlayer.isPlaying){ - startForeground(notificationId,notification) - } - Timber.i("Start Foreground called") - } - }) - - exoPlayerNotificationManager.setPlayer(exoPlayer) - - //Todo: Implement the mediabuttonreceiver for api 19 https://developer.android.com/guide/topics/media-apps/mediabuttons - - mediaSession = MediaSessionCompat(applicationContext, Context.MEDIA_SESSION_SERVICE) - mediaSession.isActive = true - exoPlayerNotificationManager.setMediaSessionToken(mediaSession.sessionToken) - exoPlayerNotificationManager.setSmallIcon(R.drawable.fable_icon_notification) - mediaSessionConnector = MediaSessionConnector(mediaSession) -// mediaSessionConnector.setQueueNavigator(object: TimelineQueueNavigator(mediaSession){ -// override fun getMediaDescription( -// player: Player?, -// windowIndex: Int -// ): MediaDescriptionCompat { -// TODO("Not yet implemented") -// return MediaDescriptionCompat.Builder() -// .setMediaId() -// .setIconBitmap() -// .setTitle() -// .setDescription() -// .setExtras() -// .build() +// +// inner class LocalBinder : Binder() { +// fun getService() : AudioPlayerService = this@AudioPlayerService +// } +// +// inner class ExoPlayerEventListener() : Player.EventListener{ +// var isPlayingBool = false +// +// override fun onIsPlayingChanged(isPlaying: Boolean) { +// super.onIsPlayingChanged(isPlaying) +// isPlayingBool = isPlaying +// Timber.i("Is playing changed to: " + isPlaying) +// if(!isPlaying){ +// //Timber.i("Remove the notification") +// +// //I've pretty much figured this out, but felt I should make note of where I found the info +// //https://stackoverflow.com/questions/43596709/make-a-notification-from-a-foreground-service-cancelable-once-the-service-is-not +// //https://github.com/google/ExoPlayer/issues/4256 +// stopForeground(false) +// CoroutineScope(Dispatchers.IO).launch { +// ExoPlayerMasterObject.updateAudiobook() +// } // } -// }) - mediaSessionConnector.setPlayer(exoPlayer) - - - } - - override fun onBind(intent: Intent?): IBinder? { - return binder - } - - override fun onLowMemory() { - super.onLowMemory() - stopForeground(true) - //exoPlayerNotificationManager.setPlayer(null) - - } - - override fun onDestroy() { - stopForeground(true) - ExoPlayerMasterObject.cancelSleepTimer() - exoPlayerNotificationManager.setPlayer(null) - mediaSessionConnector.setPlayer(null) - //exoPlayer.stop() - stopSelf() - super.onDestroy() - - //https://stackoverflow.com/questions/6330200/how-to-quit-android-application-programmatically - //I may want to do this if I start getting unintended crashes - } -// -// fun getExoPlayerInstance() : SimpleExoPlayer{ -// return exoPlayer +// +// } // } - - -// override fun onHandleIntent(intent: Intent?) { -// // Normally we would do some work here, like download a file. -// // For our sample, we just sleep for 5 seconds. -// try { -// -// //exoPlayer.prepare((Uri.parse("https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/01_-_Intro_-_The_Way_Of_Waking_Up_feat_Alan_Watts.mp3"))) -// Thread.sleep(5000) -// } catch (e: InterruptedException) { -// // Restore interrupt status. -// Thread.currentThread().interrupt() +// +//// override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { +//// Timber.i("OnStartCommand Called") +//// return START_STICKY +//// //return super.onStartCommand(intent, flags, startId) +//// } +// +// override fun onCreate() { +// +// Timber.i("On create Called") +// super.onCreate() +// exoPlayer = ExoPlayerMasterObject.exoPlayer +// exoPlayer.addListener(eventListener) +// //(application as Fable).exoPlayer = exoPlayer +// +// Timber.i("ExoPlayer Attached to the interface") +// +// +// //https://medium.com/google-exoplayer/exoplayer-2-11-whats-new-e0e0701e4b6c +// exoPlayer.setWakeMode(C.WAKE_MODE_NETWORK) +// exoPlayer.setHandleAudioBecomingNoisy(true) +// +// +// //https://medium.com/google-exoplayer/easy-audio-focus-with-exoplayer-a2dcbbe4640e +// val audioBookAudioAttributes = AudioAttributes.Builder() +// .setUsage(C.USAGE_MEDIA) +// .setContentType(C.CONTENT_TYPE_SPEECH) +// .build() +// +// exoPlayer.setAudioAttributes(audioBookAudioAttributes, true) +// +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +// // Create the NotificationChannel +// val name = "Audiobook Playback" +// val descriptionText = "Fable audiobook player" +// val importance = NotificationManager.IMPORTANCE_DEFAULT +// val mChannel = NotificationChannel("PLAYBACK", name, importance) +// mChannel.description = descriptionText +// mChannel.setSound(null, null) +// // Register the channel with the system; you can't change the importance +// // or other notification behaviors after this +// val notificationManager = getSystemService(Application.NOTIFICATION_SERVICE) as NotificationManager +// notificationManager.createNotificationChannel(mChannel) // } // +// //https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/PlayerNotificationManager.html +// //https://medium.com/google-exoplayer/playback-notifications-with-exoplayer-a2f1a18cf93b +// exoPlayerNotificationManager = PlayerNotificationManager(applicationContext, "PLAYBACK", 1, DescriptionAdapter(applicationContext), object: +// PlayerNotificationManager.NotificationListener { +// +// override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) { +// super.onNotificationCancelled(notificationId, dismissedByUser) +// //exoPlayer.stop() +// //stopSelf() +// } +// +// +// override fun onNotificationPosted( +// notificationId: Int, +// notification: Notification, +// ongoing: Boolean +// ) { +// super.onNotificationPosted(notificationId, notification, ongoing) +// +// //if(eventListener.isPlayingBool){ +// if(exoPlayer.isPlaying){ +// startForeground(notificationId,notification) +// } +// Timber.i("Start Foreground called") +// } +// }) +// +// exoPlayerNotificationManager.setPlayer(exoPlayer) +// +// //Todo: Implement the mediabuttonreceiver for api 19 https://developer.android.com/guide/topics/media-apps/mediabuttons +// +// mediaSession = MediaSessionCompat(applicationContext, Context.MEDIA_SESSION_SERVICE) +// mediaSession.isActive = true +// exoPlayerNotificationManager.setMediaSessionToken(mediaSession.sessionToken) +// exoPlayerNotificationManager.setSmallIcon(R.drawable.fable_icon_notification) +// mediaSessionConnector = MediaSessionConnector(mediaSession) +//// mediaSessionConnector.setQueueNavigator(object: TimelineQueueNavigator(mediaSession){ +//// override fun getMediaDescription( +//// player: Player?, +//// windowIndex: Int +//// ): MediaDescriptionCompat { +//// TODO("Not yet implemented") +//// return MediaDescriptionCompat.Builder() +//// .setMediaId() +//// .setIconBitmap() +//// .setTitle() +//// .setDescription() +//// .setExtras() +//// .build() +//// } +//// }) +// mediaSessionConnector.setPlayer(exoPlayer) +// +// // } - -} \ No newline at end of file +// +// override fun onBind(intent: Intent?): IBinder? { +// return binder +// } +// +// override fun onLowMemory() { +// super.onLowMemory() +// stopForeground(true) +// //exoPlayerNotificationManager.setPlayer(null) +// +// } +// +// override fun onDestroy() { +// stopForeground(true) +// ExoPlayerMasterObject.cancelSleepTimer() +// exoPlayerNotificationManager.setPlayer(null) +// mediaSessionConnector.setPlayer(null) +// //exoPlayer.stop() +// stopSelf() +// super.onDestroy() +// +// //https://stackoverflow.com/questions/6330200/how-to-quit-android-application-programmatically +// //I may want to do this if I start getting unintended crashes +// } +//// +//// fun getExoPlayerInstance() : SimpleExoPlayer{ +//// return exoPlayer +//// } +// +// +//// override fun onHandleIntent(intent: Intent?) { +//// // Normally we would do some work here, like download a file. +//// // For our sample, we just sleep for 5 seconds. +//// try { +//// +//// //exoPlayer.prepare((Uri.parse("https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/01_-_Intro_-_The_Way_Of_Waking_Up_feat_Alan_Watts.mp3"))) +//// Thread.sleep(5000) +//// } catch (e: InterruptedException) { +//// // Restore interrupt status. +//// Thread.currentThread().interrupt() +//// } +//// +//// } +// +//} \ No newline at end of file diff --git a/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt b/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt index e22c951..2ff9d51 100644 --- a/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt +++ b/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt @@ -5,17 +5,8 @@ import android.content.Context import android.util.AttributeSet import android.view.View import android.widget.* -import androidx.core.view.forEach -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.Observer -import androidx.navigation.NavController -import androidx.navigation.findNavController +import androidx.media3.ui.PlayerControlView import androidx.preference.PreferenceManager - -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.Timeline -import com.google.android.exoplayer2.ui.PlayerControlView import one.fable.fable.R import one.fable.fable.exoplayer.ExoPlayerMasterObject import timber.log.Timber diff --git a/app/src/main/java/one/fable/fable/exoplayer/DescriptionAdapter.kt b/app/src/main/java/one/fable/fable/exoplayer/DescriptionAdapter.kt index a647d4a..a88a461 100644 --- a/app/src/main/java/one/fable/fable/exoplayer/DescriptionAdapter.kt +++ b/app/src/main/java/one/fable/fable/exoplayer/DescriptionAdapter.kt @@ -1,73 +1,71 @@ -package one.fable.fable.exoplayer - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.ImageDecoder -import android.provider.MediaStore -import com.google.android.exoplayer2.* -import com.google.android.exoplayer2.ui.PlayerNotificationManager -import one.fable.fable.MainActivity - - -class DescriptionAdapter(context: Context) : PlayerNotificationManager.MediaDescriptionAdapter { - val context = context - - override fun createCurrentContentIntent(player: Player): PendingIntent? { - //TODO Open directly to the fragment - //https://stackoverflow.com/questions/26608627/how-to-open-fragment-page-when-pressed-a-notification-in-android - - //val intent = Intent(context, MainActivity::class.java) - val intent = Intent(context, MainActivity::class.java) - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) - } - - override fun getCurrentContentText(player: Player): CharSequence? { - val window = Timeline.Window() -// player.currentTimeline.getWindow(player.currentWindowIndex, window) +//package one.fable.fable.exoplayer // -// return window.tag.toString() - return ExoPlayerMasterObject.audioPlaybackWindows[player.currentWindowIndex].Name.toString() - } - - override fun getCurrentContentTitle(player: Player): CharSequence { - //val window = Timeline.Window() - //val period = Timeline.Period() - //player.currentTimeline.getWindow(player.currentWindowIndex, window) - //player.currentTimeline.getPeriod(player.currentPeriodIndex, period) - - return ExoPlayerMasterObject.audiobook.audiobookTitle - - - //return "Test 1" - } - - override fun getCurrentLargeIcon( - player: Player, - callback: PlayerNotificationManager.BitmapCallback - ): Bitmap? { - val bitmapUri = ExoPlayerMasterObject.audiobook.imgThumbnail - if (bitmapUri != null){ - var bitmapSource : Bitmap? = null - try { - //https://stackoverflow.com/questions/56651444/deprecated-getbitmap-with-api-29-any-alternative-codes - //"getBitmap" is deprecated. Someday I'll have to fix this. But clearly today is not that day. - - //https://stackoverflow.com/questions/48729200/how-to-convert-uri-to-bitmap/48729608 - bitmapSource = MediaStore.Images.Media.getBitmap(context.contentResolver, bitmapUri) - } catch(e: Exception) { - timber.log.Timber.e(e) - } - //val bitmapSource = bitmapUri?.let { ImageDecoder.createSource(context.contentResolver, it) } - return bitmapSource - - } else{ - return null - } - - //return MediaStore.Images.Media.getBitmap() - //return context.resources.getDrawable() - //return ExoPlayerInterface.ExoPlayerCompanionObject.coverUri. - } -} +//import android.app.PendingIntent +//import android.content.Context +//import android.content.Intent +//import android.graphics.Bitmap +//import android.graphics.ImageDecoder +//import android.provider.MediaStore +//import one.fable.fable.MainActivity +// +// +//class DescriptionAdapter(context: Context) : PlayerNotificationManager.MediaDescriptionAdapter { +// val context = context +// +// override fun createCurrentContentIntent(player: Player): PendingIntent? { +// //TODO Open directly to the fragment +// //https://stackoverflow.com/questions/26608627/how-to-open-fragment-page-when-pressed-a-notification-in-android +// +// //val intent = Intent(context, MainActivity::class.java) +// val intent = Intent(context, MainActivity::class.java) +// return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) +// } +// +// override fun getCurrentContentText(player: Player): CharSequence? { +// val window = Timeline.Window() +//// player.currentTimeline.getWindow(player.currentWindowIndex, window) +//// +//// return window.tag.toString() +// return ExoPlayerMasterObject.audioPlaybackWindows[player.currentWindowIndex].Name.toString() +// } +// +// override fun getCurrentContentTitle(player: Player): CharSequence { +// //val window = Timeline.Window() +// //val period = Timeline.Period() +// //player.currentTimeline.getWindow(player.currentWindowIndex, window) +// //player.currentTimeline.getPeriod(player.currentPeriodIndex, period) +// +// return ExoPlayerMasterObject.audiobook.audiobookTitle +// +// +// //return "Test 1" +// } +// +// override fun getCurrentLargeIcon( +// player: Player, +// callback: PlayerNotificationManager.BitmapCallback +// ): Bitmap? { +// val bitmapUri = ExoPlayerMasterObject.audiobook.imgThumbnail +// if (bitmapUri != null){ +// var bitmapSource : Bitmap? = null +// try { +// //https://stackoverflow.com/questions/56651444/deprecated-getbitmap-with-api-29-any-alternative-codes +// //"getBitmap" is deprecated. Someday I'll have to fix this. But clearly today is not that day. +// +// //https://stackoverflow.com/questions/48729200/how-to-convert-uri-to-bitmap/48729608 +// bitmapSource = MediaStore.Images.Media.getBitmap(context.contentResolver, bitmapUri) +// } catch(e: Exception) { +// timber.log.Timber.e(e) +// } +// //val bitmapSource = bitmapUri?.let { ImageDecoder.createSource(context.contentResolver, it) } +// return bitmapSource +// +// } else{ +// return null +// } +// +// //return MediaStore.Images.Media.getBitmap() +// //return context.resources.getDrawable() +// //return ExoPlayerInterface.ExoPlayerCompanionObject.coverUri. +// } +//} diff --git a/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt b/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt index 58b46ee..073dea2 100644 --- a/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt +++ b/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt @@ -9,15 +9,16 @@ import android.util.Log import android.util.Xml import android.widget.Toast import androidx.lifecycle.MutableLiveData -import com.google.android.exoplayer2.* -import com.google.android.exoplayer2.Player.TIMELINE_CHANGE_REASON_PREPARED -import com.google.android.exoplayer2.source.ClippingMediaSource -import com.google.android.exoplayer2.source.ConcatenatingMediaSource -import com.google.android.exoplayer2.source.ProgressiveMediaSource -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory -import com.google.android.exoplayer2.util.Util +import androidx.media3.common.* +import androidx.media3.common.util.Util +import androidx.media3.datasource.DefaultDataSourceFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.ClippingMediaSource +import androidx.media3.exoplayer.source.ConcatenatingMediaSource +import androidx.media3.exoplayer.source.ProgressiveMediaSource import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.* +import one.fable.fable.Fable import one.fable.fable.convertOverDriveTimeMarkersToMillisecondsAsLong import one.fable.fable.database.daos.AudiobookDao import one.fable.fable.database.entities.* @@ -37,8 +38,13 @@ Building feature-rich media apps with ExoPlayer (Google I/O '18) https://www.you */ object ExoPlayerMasterObject { + val exoPlayer : ExoPlayer = ExoPlayer.Builder(Fable.instance) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setHandleAudioBecomingNoisy(true) + .setWakeMode(C.WAKE_MODE_LOCAL) + .build() + - lateinit var applicationContext: Context lateinit var audiobookDao: AudiobookDao @@ -72,14 +78,14 @@ object ExoPlayerMasterObject { } } - lateinit var exoPlayer : SimpleExoPlayer - fun isExoPlayerInitialized() = this::exoPlayer.isInitialized //https://stackoverflow.com/questions/47549015/isinitialized-backing-field-of-lateinit-var-is-not-accessible-at-this-point - fun buildExoPlayer(context: Context){ - if (!this::exoPlayer.isInitialized){ - exoPlayer = SimpleExoPlayer.Builder(context).build() - exoPlayer.addListener(eventListener) - } - } + +// fun isExoPlayerInitialized() = this::exoPlayer.isInitialized //https://stackoverflow.com/questions/47549015/isinitialized-backing-field-of-lateinit-var-is-not-accessible-at-this-point +// fun buildExoPlayer(context: Context){ +// if (!this::exoPlayer.isInitialized){ +// exoPlayer = ExoPlayer.Builder(context).build() +// exoPlayer.addListener(eventListener) +// } +// } fun loadAudiobook(audiobookToLoad: Audiobook){ var loadNewBook = false @@ -138,8 +144,8 @@ object ExoPlayerMasterObject { suspend fun loadTracks(title :String){ Timber.i("Loading new tracks") - val dataSourceFactory = DefaultDataSourceFactory(applicationContext, - Util.getUserAgent(applicationContext, "Fable")) + val dataSourceFactory = DefaultDataSourceFactory(Fable.instance, + Util.getUserAgent(Fable.instance, "Fable")) val concatenatingMediaSource = ConcatenatingMediaSource() @@ -155,7 +161,7 @@ object ExoPlayerMasterObject { for (track in tracksWithChapters) { val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory) - .createMediaSource(track.track.trackUri) + .createMediaSource(MediaItem.fromUri(track.track.trackUri)) //If the track has no chapter data embeded, use the whole track as the window (a psuedo-chapter) if (track.chapters.isEmpty()) { @@ -245,7 +251,7 @@ object ExoPlayerMasterObject { if (!track.scannedSourceNames.contains("OverDrive")) { - val contentResolver = applicationContext.contentResolver + val contentResolver = Fable.instance.contentResolver var inputStream = contentResolver.openInputStream(track.trackUri) val bufferedReader = BufferedReader(InputStreamReader(inputStream)) @@ -361,7 +367,7 @@ object ExoPlayerMasterObject { //PLAYER EVENT LISTENER CODE todo private val eventListener = PlayerEventListener() - class PlayerEventListener() : Player.EventListener{ + class PlayerEventListener() : Player.Listener{ //var isPlayingBool = false override fun onIsPlayingChanged(isPlaying: Boolean) { @@ -417,7 +423,7 @@ object ExoPlayerMasterObject { progress.value = getTimelineDuration() } - if (reason == TIMELINE_CHANGE_REASON_PREPARED){ +// if (reason == TIMELINE_CHANGE_REASON_PREPARED){ windowDurationsSummation.clear() Timber.i("Last Index is: " + exoPlayer.currentTimeline.windowCount) for (index in 0 until timeline.windowCount){ @@ -456,7 +462,7 @@ object ExoPlayerMasterObject { windowDurationsSummation.add(previousValue.plus(duration)) } } - } +// } super.onTimelineChanged(timeline, reason) } } diff --git a/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt b/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt new file mode 100644 index 0000000..aa5aac5 --- /dev/null +++ b/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt @@ -0,0 +1,18 @@ +package one.fable.fable.exoplayer + +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSessionService + +// Extend MediaSessionService +class PlaybackService : MediaSessionService() { + private var mediaSession: MediaSession? = null + // Create your Player and MediaSession in the onCreate lifecycle event + override fun onCreate() { + super.onCreate() + mediaSession = MediaSession.Builder(this, ExoPlayerMasterObject.exoPlayer).build() + } + // Return a MediaSession to link with the MediaController that is making + // this request. + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? + = mediaSession +} diff --git a/app/src/main/java/one/fable/fable/library/LibraryFragment.kt b/app/src/main/java/one/fable/fable/library/LibraryFragment.kt index bd1f341..7b792d5 100644 --- a/app/src/main/java/one/fable/fable/library/LibraryFragment.kt +++ b/app/src/main/java/one/fable/fable/library/LibraryFragment.kt @@ -50,7 +50,7 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { private lateinit var libraryFragmentViewModel: LibraryFragmentViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - postponeEnterTransition() + //postponeEnterTransition() super.onViewCreated(view, savedInstanceState) //setHasOptionsMenu(true) binding = LibraryFragmentBinding.bind(view) @@ -77,39 +77,39 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { tab.text = getString(libraryTabText[position]) }.attach() - binding.libraryExoplayer.player = ExoPlayerMasterObject.exoPlayer - binding.libraryExoplayer.showTimeoutMs = -1 +// binding.libraryExoplayer.player = ExoPlayerMasterObject.exoPlayer +// binding.libraryExoplayer.showTimeoutMs = -1 - if (ExoPlayerMasterObject.isAudiobookInitialized()){ - //BottomSheetBehavior.from(binding.libraryExoplayer).state = BottomSheetBehavior.STATE_EXPANDED - binding.libraryExoplayer.visibility = View.VISIBLE - //https://stackoverflow.com/questions/9685658/add-padding-on-view-programmatically - val scale = resources.displayMetrics.density - binding.viewPagerLibrary.setPadding(0,0,0, (90*scale + 0.5F).toInt()) - - val clickableArea = binding.libraryExoplayer.findViewById(R.id.library_bottom_audiobook_player_clickable_area) - clickableArea.setOnClickListener { findNavController().navigate(R.id.action_libraryFragment_to_audiobookPlayerFragment) } - - - val chapterNameObserver = Observer { - binding.libraryExoplayer.findViewById(R.id.library_bottom_chapter_name).text = it - } - ExoPlayerMasterObject.chapterName.observe(viewLifecycleOwner, chapterNameObserver) - - binding.libraryExoplayer.findViewById(R.id.library_bottom_book_name).text = ExoPlayerMasterObject.audiobook.audiobookTitle - - val cover = binding.libraryExoplayer.findViewById(R.id.library_bottom_cover_image) - if (ExoPlayerMasterObject.audiobook.imgThumbnail != null){ - cover.setImageURI(ExoPlayerMasterObject.audiobook.imgThumbnail) - } else { - cover.visibility = View.GONE - } - - } else { - //BottomSheetBehavior.from(binding.libraryExoplayer).state = BottomSheetBehavior.STATE_HIDDEN - - binding.libraryExoplayer.visibility = View.GONE - } +// if (ExoPlayerMasterObject.isAudiobookInitialized()){ +// //BottomSheetBehavior.from(binding.libraryExoplayer).state = BottomSheetBehavior.STATE_EXPANDED +// binding.libraryExoplayer.visibility = View.VISIBLE +// //https://stackoverflow.com/questions/9685658/add-padding-on-view-programmatically +// val scale = resources.displayMetrics.density +// binding.viewPagerLibrary.setPadding(0,0,0, (90*scale + 0.5F).toInt()) +// +// val clickableArea = binding.libraryExoplayer.findViewById(R.id.library_bottom_audiobook_player_clickable_area) +// clickableArea.setOnClickListener { findNavController().navigate(R.id.action_libraryFragment_to_audiobookPlayerFragment) } +// +// +// val chapterNameObserver = Observer { +// binding.libraryExoplayer.findViewById(R.id.library_bottom_chapter_name).text = it +// } +// ExoPlayerMasterObject.chapterName.observe(viewLifecycleOwner, chapterNameObserver) +// +// binding.libraryExoplayer.findViewById(R.id.library_bottom_book_name).text = ExoPlayerMasterObject.audiobook.audiobookTitle +// +// val cover = binding.libraryExoplayer.findViewById(R.id.library_bottom_cover_image) +// if (ExoPlayerMasterObject.audiobook.imgThumbnail != null){ +// cover.setImageURI(ExoPlayerMasterObject.audiobook.imgThumbnail) +// } else { +// cover.visibility = View.GONE +// } +// +// } else { +// //BottomSheetBehavior.from(binding.libraryExoplayer).state = BottomSheetBehavior.STATE_HIDDEN +// +// binding.libraryExoplayer.visibility = View.GONE +// } if (libraryFragmentViewModel.firstLoad) { if (libraryFragmentViewModel.anyAudiobook != null){ @@ -154,7 +154,7 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { sharedPreferences.edit().putBoolean("first_library_load", false).apply() } - view.doOnPreDraw { startPostponedEnterTransition() } + //view.doOnPreDraw { startPostponedEnterTransition() } } diff --git a/app/src/main/res/layout/exo_player_control_view.xml b/app/src/main/res/layout/exo_player_control_view.xml index db8688f..7a02b23 100644 --- a/app/src/main/res/layout/exo_player_control_view.xml +++ b/app/src/main/res/layout/exo_player_control_view.xml @@ -180,7 +180,7 @@ android:textSize="14sp" android:textStyle="bold" /> - - + + + + + + diff --git a/app/src/main/res/navigation/navigation.xml b/app/src/main/res/navigation/navigation.xml index 062421a..98e318a 100644 --- a/app/src/main/res/navigation/navigation.xml +++ b/app/src/main/res/navigation/navigation.xml @@ -12,10 +12,7 @@ tools:layout="@layout/library_fragment" > + app:destination="@id/globalSettingsFragment" /> Date: Fri, 23 Dec 2022 18:52:17 -0800 Subject: [PATCH 2/3] Lots of work has been done, but still a lot to do! (Just checking in the code so I don't lose everything) --- app/build.gradle | 14 +- app/src/main/java/one/fable/fable/Fable.kt | 2 +- .../one/fable/fable/GlobalSettingsFragment.kt | 11 + .../AudiobookPlayerFragment.kt | 33 +- .../fable/audiobookPlayer/TrackListAdapter.kt | 4 - .../exoplayer/CustomPlayerControlView.kt | 142 +++--- .../fable/exoplayer/ExoPlayerMasterObject.kt | 440 +++++++++--------- .../fable/exoplayer/MediaPlayerService.kt | 2 + .../fable/fable/library/LibraryFragment.kt | 120 ++++- .../fable/library/LibraryFragmentAdapter.kt | 4 +- .../fable/library/LibraryFragmentViewModel.kt | 9 +- .../utils/extensions/MutableListExtensions.kt | 5 + ...black_24dp.xml => exo_controls_rewind.xml} | 0 .../res/layout/audiobook_player_fragment.xml | 145 +++--- .../res/layout/exo_player_control_view.xml | 217 --------- .../layout/exo_player_control_view_old.xml | 70 +++ app/src/main/res/layout/library_fragment.xml | 166 ++++--- app/src/main/res/layout/track_list_item.xml | 13 +- 18 files changed, 717 insertions(+), 680 deletions(-) create mode 100644 app/src/main/java/one/fable/fable/utils/extensions/MutableListExtensions.kt rename app/src/main/res/drawable/{ic_replay_black_24dp.xml => exo_controls_rewind.xml} (100%) delete mode 100644 app/src/main/res/layout/exo_player_control_view.xml create mode 100644 app/src/main/res/layout/exo_player_control_view_old.xml diff --git a/app/build.gradle b/app/build.gradle index e216a43..98bb15f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,12 +43,12 @@ dependencies { //Default Dependencies implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.3.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' @@ -61,7 +61,7 @@ dependencies { implementation 'androidx.navigation:navigation-ui-ktx:2.3.0-beta01' //RecyclerView - implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03' + implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01' //ExoPlayer implementation "androidx.media3:media3-exoplayer:1.0.0-beta02" @@ -71,10 +71,10 @@ dependencies { //Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" //Material Design - implementation 'com.google.android.material:material:1.1.0' + implementation 'com.google.android.material:material:1.7.0' //Room implementation "androidx.room:room-runtime:2.2.5" diff --git a/app/src/main/java/one/fable/fable/Fable.kt b/app/src/main/java/one/fable/fable/Fable.kt index f2c484b..e9b005c 100644 --- a/app/src/main/java/one/fable/fable/Fable.kt +++ b/app/src/main/java/one/fable/fable/Fable.kt @@ -36,7 +36,7 @@ class Fable : Application() { audiobookDao = FableDatabase.getInstance(applicationContext).audiobookDao directoryDao = FableDatabase.getInstance(applicationContext).directoryDao - ExoPlayerMasterObject.setSharedPreferencesAndListener(PreferenceManager.getDefaultSharedPreferences(applicationContext)) + //ExoPlayerMasterObject.setSharedPreferencesAndListener(PreferenceManager.getDefaultSharedPreferences(applicationContext)) ExoPlayerMasterObject.audiobookDao = audiobookDao } diff --git a/app/src/main/java/one/fable/fable/GlobalSettingsFragment.kt b/app/src/main/java/one/fable/fable/GlobalSettingsFragment.kt index 3d4139c..1dcc2ff 100644 --- a/app/src/main/java/one/fable/fable/GlobalSettingsFragment.kt +++ b/app/src/main/java/one/fable/fable/GlobalSettingsFragment.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.text.InputType import android.text.TextUtils import android.view.View +import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import androidx.preference.* import java.util.prefs.PreferenceChangeEvent @@ -36,10 +37,20 @@ class GlobalSettingsFragment : PreferenceFragmentCompat() { editText.inputType = InputType.TYPE_CLASS_NUMBER } + rewindPreference?.setOnPreferenceChangeListener { preference, newValue -> + Toast.makeText(context, "Preference will be reflected after app restart", Toast.LENGTH_LONG).show() + true + } + fastForwardPreference?.setOnBindEditTextListener { editText -> editText.inputType = InputType.TYPE_CLASS_NUMBER } + fastForwardPreference?.setOnPreferenceChangeListener { preference, newValue -> + Toast.makeText(context, "Preference will be reflected after app restart", Toast.LENGTH_LONG).show() + true + } + theme?.setOnPreferenceChangeListener(object : Preference.OnPreferenceChangeListener{ override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { when (newValue){ diff --git a/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt b/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt index c2bfb1d..bba1469 100644 --- a/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt +++ b/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt @@ -19,11 +19,15 @@ import androidx.core.view.doOnPreDraw import androidx.core.view.marginTop import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer +import androidx.media3.common.PlaybackParameters +import androidx.media3.exoplayer.ExoPlayer import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTargetSequence +import com.google.android.material.card.MaterialCardView +import com.google.android.material.textview.MaterialTextView import one.fable.fable.R import one.fable.fable.databinding.AudiobookPlayerFragmentBinding import one.fable.fable.exoplayer.ExoPlayerMasterObject @@ -67,18 +71,33 @@ class AudiobookPlayerFragment : Fragment(R.layout.audiobook_player_fragment) { binding.exoplayer.showTimeoutMs = -1 binding.exoplayer.player = ExoPlayerMasterObject.exoPlayer + binding.exoplayer.setShowMultiWindowTimeBar(true) + + binding.exoplayer.player + //binding.exoplayer.findViewById(androidx.media3.ui.R.id.exo_settings).visibility = View.GONE //Hide the settings cog + + //Assign nested Views +// val trackName = binding.exoplayer.findViewById(R.id.track_name) +// val trackNameCard = binding.exoplayer.findViewById(R.id.track_name_card) //todo: figure out how to add the multiwindow timebar //https://github.com/google/ExoPlayer/issues/2122 //https://github.com/google/ExoPlayer/issues/6741 //binding.exoplayer.setShowMultiWindowTimeBar(true) -// val chapterNameObserver = Observer { -// binding.exoplayer.track_name.text = it -// } -// ExoPlayerMasterObject.chapterName.observe(viewLifecycleOwner, chapterNameObserver) + val chapterNameObserver = Observer { + binding.trackName.text = it + } + ExoPlayerMasterObject.chapterName.observe(viewLifecycleOwner, chapterNameObserver) + + binding.trackName.setOnClickListener { + val extras = FragmentNavigatorExtras( + binding.trackName to "track_name" + ) + findNavController().navigate(R.id.action_audiobookPlayerFragment_to_trackListFragment, null, null, extras) + } // -// binding.exoplayer.track_name_card.setOnClickListener { +// trackNameCard.setOnClickListener { // // //view.height //// var trackLocationOnScreen = IntArray(2) @@ -91,8 +110,8 @@ class AudiobookPlayerFragment : Fragment(R.layout.audiobook_player_fragment) { // // // val extras = FragmentNavigatorExtras( -// binding.exoplayer.track_name to "track_name", -// binding.exoplayer.track_name_card to "track_name_card" +// trackName to "track_name", +// trackNameCard to "track_name_card" // ) // // diff --git a/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListAdapter.kt b/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListAdapter.kt index d819fad..e4916d8 100644 --- a/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListAdapter.kt +++ b/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListAdapter.kt @@ -34,12 +34,10 @@ class TrackListItemViewHolder private constructor(val binding: TrackListItemBind if (position == ExoPlayerMasterObject.exoPlayer.currentWindowIndex){ binding.trackListItemName.transitionName = "track_name" - binding.trackListItemCard.transitionName = "track_name_card" binding.trackListItemEqualizer.visibility = View.VISIBLE } else { binding.trackListItemEqualizer.visibility = View.GONE binding.trackListItemName.transitionName = null - binding.trackListItemCard.transitionName = null } binding.root.setOnClickListener { @@ -47,10 +45,8 @@ class TrackListItemViewHolder private constructor(val binding: TrackListItemBind ExoPlayerMasterObject.selectTrack(position) binding.trackListItemName.transitionName = "track_name" - binding.trackListItemCard.transitionName = "track_name_card" val extras = FragmentNavigatorExtras( - binding.trackListItemCard to "track_name_card", binding.trackListItemName to "exoplayer_track_name_transition" ) diff --git a/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt b/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt index 2ff9d51..ec87e17 100644 --- a/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt +++ b/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt @@ -1,71 +1,71 @@ -package com.example.appleaudiobookcoverapi - -import android.app.Activity -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.* -import androidx.media3.ui.PlayerControlView -import androidx.preference.PreferenceManager -import one.fable.fable.R -import one.fable.fable.exoplayer.ExoPlayerMasterObject -import timber.log.Timber -import java.lang.Exception -import java.util.concurrent.TimeUnit - -class CustomPlayerControlView : PlayerControlView { - constructor(context: Context) : super(context) - constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet) - constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr : Int) : super (context, attributeSet, defStyleAttr) - constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr : Int, playbackAttributeSet: AttributeSet?) : super (context, attributeSet, defStyleAttr, playbackAttributeSet) - - val customRewind : ImageButton = findViewById(R.id.exo_audiobook_rewind) - val customFastForward : ImageButton = findViewById(R.id.exo_audiobook_ffwd) - - private val rewind = "rewind" - private val fastForward = "fastForward" - - init { - customRewind.setOnClickListener{audiobookSeekInterval(rewind)} - customFastForward.setOnClickListener{audiobookSeekInterval(fastForward)} - } - - - fun audiobookSeekInterval(seekDirection : String){ - - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - val rewindAmountInSeconds = sharedPreferences.getString("rewind_seconds", "") - val fastforwardAmountInSeconds = sharedPreferences.getString("fastforward_seconds", "") - var rewindAmount : Long - var fastforwardAmount : Long - - try { - rewindAmount = TimeUnit.SECONDS.toMillis(rewindAmountInSeconds!!.toLong()) - } catch (e: Exception){ - rewindAmount = 10000L - } - - try{ - fastforwardAmount = TimeUnit.SECONDS.toMillis(fastforwardAmountInSeconds!!.toLong()) - } catch (e: Exception) { - fastforwardAmount = 30000L - } - - val player = super.getPlayer() - if(player != null){ - val timeline = player.currentTimeline - if (!timeline.isEmpty){ - if (seekDirection == rewind){ - ExoPlayerMasterObject.customRewind(rewindAmount) - } else if (seekDirection == fastForward){ - ExoPlayerMasterObject.customFastForward(fastforwardAmount) - } - } - } - } - - -} - - - +//package com.example.appleaudiobookcoverapi +// +//import android.app.Activity +//import android.content.Context +//import android.util.AttributeSet +//import android.view.View +//import android.widget.* +//import androidx.media3.ui.PlayerControlView +//import androidx.preference.PreferenceManager +//import one.fable.fable.R +//import one.fable.fable.exoplayer.ExoPlayerMasterObject +//import timber.log.Timber +//import java.lang.Exception +//import java.util.concurrent.TimeUnit +// +//class CustomPlayerControlView : PlayerControlView { +// constructor(context: Context) : super(context) +// constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet) +// constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr : Int) : super (context, attributeSet, defStyleAttr) +// constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr : Int, playbackAttributeSet: AttributeSet?) : super (context, attributeSet, defStyleAttr, playbackAttributeSet) +// +// val customRewind : ImageButton = findViewById(R.id.exo_audiobook_rewind) +// val customFastForward : ImageButton = findViewById(R.id.exo_audiobook_ffwd) +// +// private val rewind = "rewind" +// private val fastForward = "fastForward" +// +// init { +// customRewind.setOnClickListener{audiobookSeekInterval(rewind)} +// customFastForward.setOnClickListener{audiobookSeekInterval(fastForward)} +// } +// +// +// fun audiobookSeekInterval(seekDirection : String){ +// +// val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) +// val rewindAmountInSeconds = sharedPreferences.getString("rewind_seconds", "") +// val fastforwardAmountInSeconds = sharedPreferences.getString("fastforward_seconds", "") +// var rewindAmount : Long +// var fastforwardAmount : Long +// +// try { +// rewindAmount = TimeUnit.SECONDS.toMillis(rewindAmountInSeconds!!.toLong()) +// } catch (e: Exception){ +// rewindAmount = 10000L +// } +// +// try{ +// fastforwardAmount = TimeUnit.SECONDS.toMillis(fastforwardAmountInSeconds!!.toLong()) +// } catch (e: Exception) { +// fastforwardAmount = 30000L +// } +// +// val player = super.getPlayer() +// if(player != null){ +// val timeline = player.currentTimeline +// if (!timeline.isEmpty){ +// if (seekDirection == rewind){ +// ExoPlayerMasterObject.customRewind(rewindAmount) +// } else if (seekDirection == fastForward){ +// ExoPlayerMasterObject.customFastForward(fastforwardAmount) +// } +// } +// } +// } +// +// +//} +// +// +// diff --git a/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt b/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt index 073dea2..49b461a 100644 --- a/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt +++ b/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt @@ -1,36 +1,40 @@ package one.fable.fable.exoplayer -import android.content.Context import android.content.SharedPreferences +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import android.net.Uri +import android.os.Build import android.os.CountDownTimer import android.os.Handler -import android.util.Log +import android.os.Looper +import android.provider.MediaStore import android.util.Xml -import android.widget.Toast +import androidx.core.net.toFile import androidx.lifecycle.MutableLiveData import androidx.media3.common.* -import androidx.media3.common.util.Util -import androidx.media3.datasource.DefaultDataSourceFactory +import androidx.media3.common.MediaMetadata.PICTURE_TYPE_FRONT_COVER import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.ClippingMediaSource -import androidx.media3.exoplayer.source.ConcatenatingMediaSource -import androidx.media3.exoplayer.source.ProgressiveMediaSource -import com.google.android.material.snackbar.Snackbar +import androidx.media3.exoplayer.SeekParameters +import androidx.preference.PreferenceManager import kotlinx.coroutines.* import one.fable.fable.Fable import one.fable.fable.convertOverDriveTimeMarkersToMillisecondsAsLong import one.fable.fable.database.daos.AudiobookDao import one.fable.fable.database.entities.* import one.fable.fable.millisToMinutesSecondsString +import one.fable.fable.utils.extensions.appendDurationSummation import org.xmlpull.v1.XmlPullParser import timber.log.Timber import java.io.BufferedReader +import java.io.ByteArrayOutputStream import java.io.InputStreamReader import java.io.StringReader -import java.lang.Runnable import java.util.concurrent.TimeUnit + /* ExoPlayer Notes/URLs that I dont want to forget: Building feature-rich media apps with ExoPlayer (Google I/O '18) https://www.youtube.com/watch?v=svdq1BWl4r8&t=784s @@ -38,18 +42,34 @@ Building feature-rich media apps with ExoPlayer (Google I/O '18) https://www.you */ object ExoPlayerMasterObject { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(Fable.instance) + val rewindAmountInSeconds = sharedPreferences.getString("rewind_seconds", "") + val fastforwardAmountInSeconds = sharedPreferences.getString("fastforward_seconds", "") + + val rewindAmount : Long = rewindAmountInSeconds?.toLongOrNull()?.let { TimeUnit.SECONDS.toMillis(it) } ?: 10000L + val fastforwardAmount : Long = fastforwardAmountInSeconds?.toLongOrNull()?.let { TimeUnit.SECONDS.toMillis(it) } ?: 30000L + val exoPlayer : ExoPlayer = ExoPlayer.Builder(Fable.instance) - .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.AUDIO_CONTENT_TYPE_SPEECH) + .build(), + true + ) + .setSeekBackIncrementMs(rewindAmount) + .setSeekForwardIncrementMs(fastforwardAmount) .setHandleAudioBecomingNoisy(true) .setWakeMode(C.WAKE_MODE_LOCAL) .build() - lateinit var audiobookDao: AudiobookDao val audioPlaybackWindows = mutableListOf() + var coverByteArray : ByteArray? = null + var coverUri : Uri? = null var title : String? = null val chapterName = MutableLiveData() @@ -66,7 +86,7 @@ object ExoPlayerMasterObject { private var loadBookJob : Job? = null - lateinit var sharedPreferences: SharedPreferences + //lateinit var sharedPreferences: SharedPreferences val settingChangeEventListener = SharedPreferences.OnSharedPreferenceChangeListener{ sharedPreferences: SharedPreferences?, key: String? -> if (key == "playback_speed_seekbar"){ globalPlaybackSpeed.value = sharedPreferences?.getInt(key, 10) @@ -107,6 +127,7 @@ object ExoPlayerMasterObject { //showToastCoroutine("Test Toast", Toast.LENGTH_SHORT) if (loadNewBook){ + setCoverByteArray() touchAndUpdateAudiobook() loadBookJob = Job() CoroutineScope(Dispatchers.Main).launch { @@ -115,9 +136,32 @@ object ExoPlayerMasterObject { } } + val setCoverByteArrayJob = Job() + fun setCoverByteArray(){ + coverByteArray = null + audiobook.imgThumbnail?.let { + val inputStream = Fable.instance.contentResolver.openInputStream(it) + val byteArray = inputStream?.readBytes() + coverByteArray = byteArray + } + +// for (index in 0 until exoPlayer.mediaItemCount){ +// } + + audiobook.imgThumbnail?.let {uri -> + val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(Fable.instance.contentResolver, uri)) + } else { + MediaStore.Images.Media.getBitmap(Fable.instance.contentResolver, uri) + } + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 10, stream) + coverByteArray = stream.toByteArray() + } ?: return + } + - fun setSharedPreferencesAndListener(preferences: SharedPreferences){ - sharedPreferences = preferences + fun setSharedPreferencesAndListener(){ sharedPreferences.registerOnSharedPreferenceChangeListener(settingChangeEventListener) globalPlaybackSpeed.value = sharedPreferences.getInt("playback_speed_seekbar", 10) } @@ -141,104 +185,155 @@ object ExoPlayerMasterObject { } } - suspend fun loadTracks(title :String){ - Timber.i("Loading new tracks") - - val dataSourceFactory = DefaultDataSourceFactory(Fable.instance, - Util.getUserAgent(Fable.instance, "Fable")) - val concatenatingMediaSource = ConcatenatingMediaSource() - + suspend fun loadTracks(title: String){ + exoPlayer.clearMediaItems() + windowDurationsSummation.clear() + windowDurationsSummation.add(0L) + var invalidTrackDurationTrigger = false withContext(Dispatchers.IO){ getTracks(title) } -// CoroutineScope(Dispatchers.IO).launch { -// -// } - progress.value = audiobook.timelineDuration - - audioPlaybackWindows.clear() for (track in tracksWithChapters) { - val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory) - .createMediaSource(MediaItem.fromUri(track.track.trackUri)) - - //If the track has no chapter data embeded, use the whole track as the window (a psuedo-chapter) - if (track.chapters.isEmpty()) { - audioPlaybackWindows.add(AudioPlaybackWindow(track.track.trackTitle)) - concatenatingMediaSource.addMediaSource(mediaSource) - } else { - - val trackChapters = mutableListOf() - for (chapter in track.chapters){ - trackChapters.add(AudioPlaybackWindow(chapter.chapterName, chapter.chapterPositionMs)) + val trackUri = track.track.trackUri + //val tempDurationSummation = windowDurationsSummation.lastOrNull() ?: 0L //Capture the previous summation + + if (track.chapters.isEmpty()){ + val mediaItem = MediaItem.fromUri(trackUri) + exoPlayer.addMediaItem(mediaItem) + + if (!invalidTrackDurationTrigger){ + track.track.trackLength?.let { trackLength -> + windowDurationsSummation.appendDurationSummation(trackLength) + } ?: run { + invalidTrackDurationTrigger = true + } } - for (index in 0 until trackChapters.lastIndex){ - val chapterSource = ClippingMediaSource( - mediaSource, - TimeUnit.MILLISECONDS.toMicros(trackChapters[index].startPos), - TimeUnit.MILLISECONDS.toMicros(trackChapters[index + 1].startPos), - false, - true, - true - ) - audioPlaybackWindows.add(AudioPlaybackWindow(trackChapters[index].Name, - trackChapters[index].startPos, - trackChapters[index + 1].startPos, - trackChapters[index + 1].startPos - trackChapters[index].startPos)) - concatenatingMediaSource.addMediaSource(chapterSource) + } else { + for (index in 0 .. track.chapters.lastIndex){ + val startPosition = track.chapters.getOrNull(index)?.chapterPositionMs ?: 0L + val endPosition = track.chapters.getOrNull(index + 1)?.chapterPositionMs ?: track.track.trackLength ?: C.TIME_END_OF_SOURCE //Get the start position of the next chapter item. If it doesn't exist return null. We'll get the end of the track in that case. + +// endPosition?.let{ +// windowDurationsSummation.add(tempDurationSummation.plus(endPosition)) //The window of windowDurationsSummation should match up with exoPlayer timeline windows +// } + + + val clippingConfiguration = + MediaItem.ClippingConfiguration.Builder().apply { + setStartPositionMs(startPosition) + setEndPositionMs(endPosition) + }.build() + + val mediaMetaData = MediaMetadata.Builder().apply { + setAlbumTitle(audiobook.audiobookTitle) + setAlbumArtist(audiobook.audiobookAuthor) + setGenre(audiobook.audiobookGenre) + setArtworkUri(audiobook.imgThumbnail) + setTitle(track.chapters.getOrNull(index)?.chapterName?.trim() ?: track.track.trackTitle ?: audiobook.audiobookTitle) + }.build() + + val clippedMediaItem = + MediaItem.Builder() + .setUri(trackUri) + .setClippingConfiguration(clippingConfiguration) + .setMediaMetadata(mediaMetaData) + .build() + + exoPlayer.addMediaItem(clippedMediaItem) + + if (endPosition == C.TIME_END_OF_SOURCE){ + invalidTrackDurationTrigger = true + break + } else { + val chapterDuration = endPosition - startPosition + if (chapterDuration <= 0) { + invalidTrackDurationTrigger = true + break + } + windowDurationsSummation.appendDurationSummation(chapterDuration) + } } + } + } - val chapterSource = ClippingMediaSource( - mediaSource, - TimeUnit.MILLISECONDS.toMicros(trackChapters.last().startPos), - C.TIME_END_OF_SOURCE - ) + exoPlayer.prepare() + exoPlayer.playWhenReady = false - audioPlaybackWindows.add(AudioPlaybackWindow(trackChapters.last().Name, trackChapters.last().startPos, track.track.trackLength, - track.track.trackLength?.minus(trackChapters.last().startPos) - )) - concatenatingMediaSource.addMediaSource(chapterSource) - } + audiobook.playbackSpeed?.let { setPlaybackSpeed(it) } ?: run { + globalPlaybackSpeed.value?.let { + setPlaybackSpeed(it) + } } - exoPlayer.prepare(concatenatingMediaSource) + if (invalidTrackDurationTrigger) { + //TODO Delay movement to Audiobook fragment until entire duration is captured + val durationListener = DurationPlayerListener({}) + exoPlayer.addListener(durationListener) + exoPlayer.seekTo(0, 0) + } else { + val windowIndex = audiobook.windowIndex + val windowLocation = audiobook.windowLocation + + if (windowIndex != null && windowLocation != null) { + try { + exoPlayer.seekTo(windowIndex, windowLocation) + } catch (e : IllegalSeekPositionException){ + Timber.e(e) + } + } + } - exoPlayer.playWhenReady = false + //getMediaItemDurations() - //var index = 0 -// while(index <= exoPlayer.currentTimeline.getLastWindowIndex(false)){ -// var window = Timeline.Window() -// exoPlayer.currentTimeline.getWindow(index, window) -// windowDurationsSummation.last() -// index++ -// } - val windowIndex = audiobook.windowIndex - val windowLocation = audiobook.windowLocation + } - if (windowIndex != null && windowLocation != null) { - exoPlayer.seekTo(windowIndex, windowLocation) - } + private fun getMediaItemDurations(){ + windowDurationsSummation.clear() + + val timeline = exoPlayer.currentTimeline + for (index in 0 until timeline.windowCount){ + var window = Timeline.Window() + timeline.getWindow(index, window) + + var duration = window.durationMs //duration = window.getDurationUs() / 1000 + //TODO: Someday when the fix gets pushed to a release, get rid of this + //https://github.com/google/ExoPlayer/issues/7314 + if (duration <= 0){ + val outsideCalculatedDuration = audioPlaybackWindows[index].duration + if (outsideCalculatedDuration != null){ + duration = outsideCalculatedDuration + } else { + val period = Timeline.Period() + timeline.getPeriod(window.firstPeriodIndex, period) -// Timber.i("Last Index is: " + exoPlayer.currentTimeline.windowCount) -// for (index in 0 .. exoPlayer.currentTimeline.windowCount){ -// var window = Timeline.Window() -// exoPlayer.currentTimeline.getWindow(index, window) -// windowDurationsSummation.add(windowDurationsSummation.last().plus(window.durationMs)) -// } + duration = period.durationMs - audioPlaybackWindows[index].startPos + } + if (duration < 0){ + val trackAtIndexOrNull = audiobookTracks.elementAtOrNull(index) + if (trackAtIndexOrNull != null) { + trackAtIndexOrNull.trackLength?.let { duration = it } + } + } + if (duration < 0){ + duration = 0 + } + } - audiobook.playbackSpeed?.let { setPlaybackSpeed(it) } ?: run { - globalPlaybackSpeed.value?.let { - setPlaybackSpeed(it) + val previousValue = windowDurationsSummation.lastOrNull() + if (previousValue == null) { + windowDurationsSummation.add(duration) + } else { + windowDurationsSummation.add(previousValue.plus(duration)) } } - } suspend fun checkFileForChapterInfo(tracks: List) { @@ -350,7 +445,7 @@ object ExoPlayerMasterObject { //PROGRESS TRACKER CODE todo - val progressTrackerHandler = Handler() + val progressTrackerHandler = Handler(Looper.getMainLooper()) val progressTrackerRunnable = object : Runnable{ override fun run() { updateAudiobookObjectLocation() @@ -359,14 +454,50 @@ object ExoPlayerMasterObject { } fun getTimelineDuration() : Long{ - var timelineDuration = try {windowDurationsSummation[exoPlayer.currentWindowIndex -1]} + var timelineDuration = try {windowDurationsSummation[exoPlayer.currentMediaItemIndex]} catch(e: IndexOutOfBoundsException){0L} timelineDuration += exoPlayer.currentPosition return timelineDuration } + class DurationPlayerListener(private val callback: () -> Unit) : Player.Listener{ + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + super.onTimelineChanged(timeline, reason) + if (exoPlayer.contentDuration != C.TIME_UNSET){ + windowDurationsSummation.appendDurationSummation(exoPlayer.contentDuration) +// windowDurationsSummation.add( +// exoPlayer.contentDuration + (windowDurationsSummation.lastOrNull() ?: 0L) +// ) + + if (exoPlayer.hasNextMediaItem()) { + exoPlayer.seekToNextMediaItem() + } else { + exoPlayer.removeListener(this) + exoPlayer.addListener(eventListener) + + val windowIndex = audiobook.windowIndex + val windowLocation = audiobook.windowLocation + + if (windowIndex != null && windowLocation != null) { + try { + exoPlayer.seekTo(windowIndex, windowLocation) + } catch (e : IllegalSeekPositionException){ + Timber.e(e) + } + } + } +// else { +// exoPlayer.isLoading +// } + } + } + } + //PLAYER EVENT LISTENER CODE todo private val eventListener = PlayerEventListener() +// .also { +// exoPlayer.addListener(it) +// } class PlayerEventListener() : Player.Listener{ //var isPlayingBool = false @@ -383,30 +514,10 @@ object ExoPlayerMasterObject { super.onIsPlayingChanged(isPlaying) } - override fun onPositionDiscontinuity(reason: Int) { - /*------Detecting when playback transitions to another item------ - https://stackoverflow.com/questions/53866692/android-exoplayer-concatenatingmediasource-detect-end-of-first-source - https://exoplayer.dev/playlists.html - - The 2 discontinuity reasons are: - 1. Player.DISCONTINUITY_REASON_PERIOD_TRANSITION - This happens when playback automatically transitions from one item to the next. - 2. Player.DISCONTINUITY_REASON_SEEK - This happens when the current playback item changes as part of a seek operation, for example when calling Player.next. */ - super.onPositionDiscontinuity(reason) - if (reason == Player.DISCONTINUITY_REASON_SEEK){ - if (!exoPlayer.currentTimeline.isEmpty){ - progress.value = getTimelineDuration() - } - } - - //todo - if (audioPlaybackWindows.isNotEmpty()) { - Timber.i("Position Discontinuity") - Timber.i(reason.toString()) - chapterName.value = audioPlaybackWindows[exoPlayer.currentWindowIndex].Name - } -// } + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + super.onMediaItemTransition(mediaItem, reason) + chapterName.value = mediaItem?.mediaMetadata?.title.toString() + progress.value = getTimelineDuration() } override fun onTimelineChanged(timeline: Timeline, reason: Int) { @@ -416,67 +527,26 @@ object ExoPlayerMasterObject { 3. EventListener.onTimelineChanged with reason = Player.TIMELINE_CHANGE_REASON_DYNAMIC. This happens when the playlist changes, e.g. if items are added, moved, or removed. */ - //todo if (audioPlaybackWindows.isNotEmpty()){ Timber.i("Timeline Changed") - chapterName.value = audioPlaybackWindows[exoPlayer.currentWindowIndex].Name + chapterName.value = audioPlaybackWindows[exoPlayer.currentMediaItemIndex].Name progress.value = getTimelineDuration() } -// if (reason == TIMELINE_CHANGE_REASON_PREPARED){ - windowDurationsSummation.clear() - Timber.i("Last Index is: " + exoPlayer.currentTimeline.windowCount) - for (index in 0 until timeline.windowCount){ - var window = Timeline.Window() - timeline.getWindow(index, window) - - var duration = window.durationMs - duration = window.getDurationUs() / 1000 - //TODO: Someday when the fix gets pushed to a release, get rid of this - //https://github.com/google/ExoPlayer/issues/7314 - if (duration <= 0){ - val outsideCalculatedDuration = audioPlaybackWindows[index].duration - if (outsideCalculatedDuration != null){ - duration = outsideCalculatedDuration - } else { - val period = Timeline.Period() - timeline.getPeriod(window.firstPeriodIndex, period) - - duration = period.durationMs - audioPlaybackWindows[index].startPos - } - if (duration < 0){ - val trackAtIndexOrNull = audiobookTracks.elementAtOrNull(index) - if (trackAtIndexOrNull != null) { - trackAtIndexOrNull.trackLength?.let { duration = it } - } - } - if (duration < 0){ - duration = 0 - } - } - - val previousValue = windowDurationsSummation.lastOrNull() - if (previousValue == null) { - windowDurationsSummation.add(duration) - } else { - windowDurationsSummation.add(previousValue.plus(duration)) - } - } -// } super.onTimelineChanged(timeline, reason) } } fun updateAudiobookObjectLocation(){ - audiobook.windowIndex = exoPlayer.currentWindowIndex + audiobook.windowIndex = exoPlayer.currentMediaItemIndex audiobook.windowLocation = exoPlayer.currentPosition audiobook.timelineDuration = getTimelineDuration() progress.value = audiobook.timelineDuration audiobook.lastPlayedTimeStamp = System.currentTimeMillis() if (audiobook.duration > 0L) { - if (exoPlayer.currentWindowIndex == exoPlayer.currentTimeline.windowCount - 1 && (audiobook.timelineDuration.toDouble() / audiobook.duration.toDouble()) >= 0.95){ + if (exoPlayer.currentMediaItemIndex == exoPlayer.currentTimeline.windowCount - 1 && (audiobook.timelineDuration.toDouble() / audiobook.duration.toDouble()) >= 0.95){ audiobook.progressState = PROGRESS_FINISHED } else { audiobook.progressState = PROGRESS_IN_PROGRESS @@ -507,64 +577,12 @@ object ExoPlayerMasterObject { */ fun selectTrack(track : Int){ - if (track != exoPlayer.currentWindowIndex){ + if (track != exoPlayer.currentMediaItemIndex){ exoPlayer.seekTo(track, 0) //progress.value = getTimelineDuration() } } - fun customFastForward(fastforwardAmount: Long){ - val currentPosition = exoPlayer.currentPosition - val windowLength = exoPlayer.duration - var windowIndex = exoPlayer.currentWindowIndex - val trackRemainder = windowLength - currentPosition - - if (fastforwardAmount < trackRemainder){ - exoPlayer.seekTo(currentPosition + fastforwardAmount) - } else { - exoPlayer.seekTo(windowIndex + 1, fastforwardAmount - trackRemainder) - //todo implement the same "while" loop we have in the customRewind. - // It isn't guaranteed that the next window will have enough remaining time, - // so we may have to continue seeking tracks - } - } - - fun customRewind(rewindAmount: Long){ - val currentPosition = exoPlayer.currentPosition //Position (time) in current window/period - //val trackLength = exoPlayer.duration //Length of the window/period - var windowIndex = exoPlayer.currentWindowIndex - - if (currentPosition > rewindAmount) { - exoPlayer.seekTo(currentPosition - rewindAmount) - } else { - //If it's the first track, go to the start of the track - if (windowIndex == 0){ - exoPlayer.seekToDefaultPosition() - return - } - - //If the - var deficit = currentPosition - rewindAmount - var previousWindowDuration = 0L - var seekToWindowIndex : Int = exoPlayer.currentWindowIndex - - while (deficit < 0L && seekToWindowIndex > 0){ - seekToWindowIndex = seekToWindowIndex.dec() - var seekWindow = Timeline.Window() - exoPlayer.currentTimeline.getWindow(seekToWindowIndex, seekWindow) - previousWindowDuration = seekWindow.durationMs - deficit += previousWindowDuration - } - - if (seekToWindowIndex == 0 && deficit < 0L){ - exoPlayer.seekTo(seekToWindowIndex, 0L) - } else { - exoPlayer.seekTo(seekToWindowIndex, deficit) - } - } - } - - /* Playback Speed Functions--------------------------------------------------------------------- */ diff --git a/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt b/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt index aa5aac5..2e7fb01 100644 --- a/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt +++ b/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt @@ -1,5 +1,6 @@ package one.fable.fable.exoplayer +import androidx.media3.common.MediaMetadata import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService @@ -15,4 +16,5 @@ class PlaybackService : MediaSessionService() { // this request. override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession + } diff --git a/app/src/main/java/one/fable/fable/library/LibraryFragment.kt b/app/src/main/java/one/fable/fable/library/LibraryFragment.kt index 7b792d5..7f28d82 100644 --- a/app/src/main/java/one/fable/fable/library/LibraryFragment.kt +++ b/app/src/main/java/one/fable/fable/library/LibraryFragment.kt @@ -8,20 +8,24 @@ import android.os.Handler import android.view.* import android.widget.* import androidx.appcompat.view.menu.ActionMenuItemView +import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.SearchView import androidx.constraintlayout.widget.ConstraintLayout import androidx.fragment.app.Fragment import androidx.core.view.doOnPreDraw +import androidx.core.widget.TextViewCompat import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.viewpager2.adapter.FragmentStateAdapter import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTargetView import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.divider.MaterialDivider import com.google.android.material.tabs.TabLayoutMediator import one.fable.fable.MainActivity @@ -49,14 +53,79 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { private lateinit var binding : LibraryFragmentBinding private lateinit var libraryFragmentViewModel: LibraryFragmentViewModel + //In Progress UI components + private lateinit var inProgressHeader : AppCompatTextView + private lateinit var inProgressDivider : MaterialDivider + private lateinit var inProgressRecyclerView : RecyclerView + + //Not Started UI components + private lateinit var notStartedHeader : AppCompatTextView + private lateinit var notStartedDivider : MaterialDivider + private lateinit var notStartedRecyclerView : RecyclerView + + //Finished UI components + private lateinit var finishedHeader : AppCompatTextView + private lateinit var finishedDivider : MaterialDivider + private lateinit var finishedRecyclerView : RecyclerView + + private val inProgressItemAdapter = LibraryFragmentAdapter() + private val notStartedItemAdapter = LibraryFragmentAdapter() + private val finishedItemAdapter = LibraryFragmentAdapter() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { //postponeEnterTransition() super.onViewCreated(view, savedInstanceState) //setHasOptionsMenu(true) binding = LibraryFragmentBinding.bind(view) - binding.setLifecycleOwner(this) + binding.lifecycleOwner = this libraryFragmentViewModel = ViewModelProvider(requireActivity()).get(LibraryFragmentViewModel::class.java) + inProgressHeader = binding.inProgressHeader + inProgressDivider = binding.inProgressDivider + inProgressRecyclerView = binding.inProgressRecyclerView + + notStartedHeader = binding.notStartedHeader + notStartedDivider = binding.notStartedDivider + notStartedRecyclerView = binding.notStartedRecyclerView + + finishedHeader = binding.finishedHeader + finishedDivider = binding.finishedDivider + finishedRecyclerView = binding.finishedRecyclerView + + inProgressRecyclerView.adapter = inProgressItemAdapter + notStartedRecyclerView.adapter = notStartedItemAdapter + finishedRecyclerView.adapter = finishedItemAdapter + + + libraryFragmentViewModel.inProgressAudiobooks.observe(viewLifecycleOwner) { + if (it.isEmpty()){ + inProgressHeader.visibility = View.GONE + inProgressDivider.visibility = View.GONE + inProgressRecyclerView.visibility = View.GONE + } + inProgressItemAdapter.submitList(it) + } + + libraryFragmentViewModel.notStartedAudiobooks.observe(viewLifecycleOwner) { + if (it.isEmpty()){ + notStartedHeader.visibility = View.GONE + notStartedDivider.visibility = View.GONE + notStartedRecyclerView.visibility = View.GONE + } + notStartedItemAdapter.submitList(it) + } + + libraryFragmentViewModel.completeAudiobooks.observe(viewLifecycleOwner) { + if (it.isEmpty()){ + finishedHeader.visibility = View.GONE + finishedDivider.visibility = View.GONE + finishedRecyclerView.visibility = View.GONE + } + finishedItemAdapter.submitList(it) + } + + //TODO: actually do something with the swipe to refresh functionality // binding.swipeRefreshLayoutLibrary.setOnRefreshListener(object : SwipeRefreshLayout.OnRefreshListener{ // override fun onRefresh() { @@ -70,12 +139,12 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { //binding.swipeRefreshLayoutLibrary.setColorSchemeColors(resources.getColor(R.color.colorAccent) ) //binding.swipeRefreshLayoutLibrary.isRefreshing = true - val libraryPagerAdapter = LibraryPagerAdapter(this, libraryFragmentViewModel) - binding.viewPagerLibrary.adapter = libraryPagerAdapter - - TabLayoutMediator(binding.tabLayoutLibrary, binding.viewPagerLibrary){tab, position -> - tab.text = getString(libraryTabText[position]) - }.attach() +// val libraryPagerAdapter = LibraryPagerAdapter(this, libraryFragmentViewModel) +// binding.viewPagerLibrary.adapter = libraryPagerAdapter +// +// TabLayoutMediator(binding.tabLayoutLibrary, binding.viewPagerLibrary){tab, position -> +// tab.text = getString(libraryTabText[position]) +// }.attach() // binding.libraryExoplayer.player = ExoPlayerMasterObject.exoPlayer // binding.libraryExoplayer.showTimeoutMs = -1 @@ -111,16 +180,16 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { // binding.libraryExoplayer.visibility = View.GONE // } - if (libraryFragmentViewModel.firstLoad) { - if (libraryFragmentViewModel.anyAudiobook != null){ - binding.tabLayoutLibrary.getTabAt(PROGRESS_IN_PROGRESS)?.select() - binding.viewPagerLibrary.currentItem = PROGRESS_IN_PROGRESS - } else { - binding.tabLayoutLibrary.getTabAt(PROGRESS_NOT_STARTED)?.select() - binding.viewPagerLibrary.currentItem = PROGRESS_NOT_STARTED - } - libraryFragmentViewModel.firstLoad = false - } +// if (libraryFragmentViewModel.firstLoad) { +// if (libraryFragmentViewModel.anyAudiobook != null){ +// binding.tabLayoutLibrary.getTabAt(PROGRESS_IN_PROGRESS)?.select() +// binding.viewPagerLibrary.currentItem = PROGRESS_IN_PROGRESS +// } else { +// binding.tabLayoutLibrary.getTabAt(PROGRESS_NOT_STARTED)?.select() +// binding.viewPagerLibrary.currentItem = PROGRESS_NOT_STARTED +// } +// libraryFragmentViewModel.firstLoad = false +// } binding.libraryAppBar.setOnMenuItemClickListener { item: MenuItem? -> when (item?.itemId){ @@ -136,6 +205,8 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { } } + + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val firstLibraryLoad = sharedPreferences.getBoolean("first_library_load", true) if (firstLibraryLoad) { @@ -154,6 +225,21 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { sharedPreferences.edit().putBoolean("first_library_load", false).apply() } + +// override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +// Timber.i("LibraryTabFragment Created") +// super.onViewCreated(view, savedInstanceState) +// +// binding = LibraryTabFragmentBinding.bind(view) +// binding.lifecycleOwner = parentFragment +// +// binding.recyclerViewLibrary.adapter = libraryItemAdapter +// +// when (position){ +// +// } +// } + //view.doOnPreDraw { startPostponedEnterTransition() } diff --git a/app/src/main/java/one/fable/fable/library/LibraryFragmentAdapter.kt b/app/src/main/java/one/fable/fable/library/LibraryFragmentAdapter.kt index e2e4491..ed8ded4 100644 --- a/app/src/main/java/one/fable/fable/library/LibraryFragmentAdapter.kt +++ b/app/src/main/java/one/fable/fable/library/LibraryFragmentAdapter.kt @@ -55,7 +55,7 @@ class LibraryFragmentAdapter() : ListAdapter(L class LibraryItemViewHolder private constructor(val binding: LibraryItemBinding) : RecyclerView.ViewHolder(binding.root){ object libraryItemListener : LibraryItemListener - public fun bind(item: Audiobook){ + fun bind(item: Audiobook){ binding.gridItemTitle.text = item.audiobookTitle binding.gridItemAuthor.text = item.audiobookAuthor if (item.imgThumbnail != null){ @@ -127,7 +127,7 @@ class LibraryItemViewHolder private constructor(val binding: LibraryItemBinding) } companion object { - public fun from(parent: ViewGroup): LibraryItemViewHolder { + fun from(parent: ViewGroup): LibraryItemViewHolder { val layoutInflater = LayoutInflater.from(parent.context) // val view = layoutInflater.inflate( // R.layout.text_item_view, parent, false diff --git a/app/src/main/java/one/fable/fable/library/LibraryFragmentViewModel.kt b/app/src/main/java/one/fable/fable/library/LibraryFragmentViewModel.kt index e98bfbd..84def00 100644 --- a/app/src/main/java/one/fable/fable/library/LibraryFragmentViewModel.kt +++ b/app/src/main/java/one/fable/fable/library/LibraryFragmentViewModel.kt @@ -10,16 +10,13 @@ import one.fable.fable.database.entities.Audiobook import one.fable.fable.exoplayer.ExoPlayerMasterObject class LibraryFragmentViewModel : ViewModel() { - lateinit var notStartedAudiobooks : LiveData> - lateinit var inProgressAudiobooks : LiveData> - lateinit var completeAudiobooks : LiveData> + var notStartedAudiobooks : LiveData> = ExoPlayerMasterObject.audiobookDao.getNotStartedAudiobooks() + var inProgressAudiobooks : LiveData> = ExoPlayerMasterObject.audiobookDao.getInProgressAudiobooks() + var completeAudiobooks : LiveData> = ExoPlayerMasterObject.audiobookDao.getFinishedAudiobooks() var anyAudiobook : Audiobook? = null var firstLoad = true init { - inProgressAudiobooks = ExoPlayerMasterObject.audiobookDao.getInProgressAudiobooks() - notStartedAudiobooks = ExoPlayerMasterObject.audiobookDao.getNotStartedAudiobooks() - completeAudiobooks = ExoPlayerMasterObject.audiobookDao.getFinishedAudiobooks() CoroutineScope(Dispatchers.IO).launch { anyAudiobook = ExoPlayerMasterObject.audiobookDao.getAnyInProgressAudiobooks() } diff --git a/app/src/main/java/one/fable/fable/utils/extensions/MutableListExtensions.kt b/app/src/main/java/one/fable/fable/utils/extensions/MutableListExtensions.kt new file mode 100644 index 0000000..d0f7714 --- /dev/null +++ b/app/src/main/java/one/fable/fable/utils/extensions/MutableListExtensions.kt @@ -0,0 +1,5 @@ +package one.fable.fable.utils.extensions + +fun MutableList.appendDurationSummation(nextItemDuration : Long){ + this.add(nextItemDuration + (this.lastOrNull() ?: 0L)) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_replay_black_24dp.xml b/app/src/main/res/drawable/exo_controls_rewind.xml similarity index 100% rename from app/src/main/res/drawable/ic_replay_black_24dp.xml rename to app/src/main/res/drawable/exo_controls_rewind.xml diff --git a/app/src/main/res/layout/audiobook_player_fragment.xml b/app/src/main/res/layout/audiobook_player_fragment.xml index 5d2786b..dc7f7df 100644 --- a/app/src/main/res/layout/audiobook_player_fragment.xml +++ b/app/src/main/res/layout/audiobook_player_fragment.xml @@ -9,9 +9,12 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> + - + android:layout_height="match_parent"> - + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:ellipsize="end" + android:maxLines="2" + android:textSize="24sp" + android:textStyle="bold" + app:layout_constraintTop_toTopOf="parent" + tools:layout_editor_absoluteX="10dp" + tools:text="Title" /> - + - + - + - + + - + - - - diff --git a/app/src/main/res/layout/exo_player_control_view.xml b/app/src/main/res/layout/exo_player_control_view.xml deleted file mode 100644 index 7a02b23..0000000 --- a/app/src/main/res/layout/exo_player_control_view.xml +++ /dev/null @@ -1,217 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/exo_player_control_view_old.xml b/app/src/main/res/layout/exo_player_control_view_old.xml new file mode 100644 index 0000000..f0d536e --- /dev/null +++ b/app/src/main/res/layout/exo_player_control_view_old.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/library_fragment.xml b/app/src/main/res/layout/library_fragment.xml index 23719de..efd51d9 100644 --- a/app/src/main/res/layout/library_fragment.xml +++ b/app/src/main/res/layout/library_fragment.xml @@ -11,83 +11,131 @@ - - - - - - + android:elevation="8dp" + app:layout_constraintTop_toTopOf="parent"> - + + + + - - - + android:layout_height="match_parent"> - - - + + - - - - - + app:layout_constraintTop_toBottomOf="@id/inProgressHeader" + android:layout_marginTop="8dp" + app:dividerInsetStart="16dp" + app:dividerInsetEnd="16dp"/> + + + + + + + - + + + + + - + - - - - - - + - - + diff --git a/app/src/main/res/layout/track_list_item.xml b/app/src/main/res/layout/track_list_item.xml index e2545b5..44ac48b 100644 --- a/app/src/main/res/layout/track_list_item.xml +++ b/app/src/main/res/layout/track_list_item.xml @@ -3,18 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - - - - From 17e8c5d3d3372f1eced3daa69bd8e8752fddf8ea Mon Sep 17 00:00:00 2001 From: Devin Duricka Date: Sun, 5 Feb 2023 14:58:56 -0800 Subject: [PATCH 3/3] Fixed the shared prefs for the playback speed listener. --- app/src/main/java/one/fable/fable/Fable.kt | 2 +- .../one/fable/fable/GlobalSettingsFragment.kt | 66 +++--- .../main/java/one/fable/fable/MainActivity.kt | 17 +- .../AudiobookPlayerFragment.kt | 14 +- .../fable/audiobookPlayer/TrackListAdapter.kt | 78 ------- .../TrackListBottomSheetDialog.kt | 38 +++ .../audiobookPlayer/TrackListFragment.kt | 48 ---- .../audiobookPlayer/TrackListTitleAdapter.kt | 59 +++++ .../fable/exoplayer/AudioPlayerService.kt | 217 ------------------ .../exoplayer/CustomPlayerControlView.kt | 71 ------ .../fable/exoplayer/DescriptionAdapter.kt | 71 ------ .../fable/exoplayer/ExoPlayerMasterObject.kt | 195 ++++++---------- .../fable/exoplayer/MediaPlayerService.kt | 4 +- .../fable/fable/library/LibraryFragment.kt | 12 + .../fable/library/LibraryFragmentAdapter.kt | 77 +------ .../res/layout/audiobook_player_fragment.xml | 7 +- app/src/main/res/layout/library_item.xml | 5 +- app/src/main/res/navigation/navigation.xml | 24 +- 18 files changed, 256 insertions(+), 749 deletions(-) delete mode 100644 app/src/main/java/one/fable/fable/audiobookPlayer/TrackListAdapter.kt create mode 100644 app/src/main/java/one/fable/fable/audiobookPlayer/TrackListBottomSheetDialog.kt delete mode 100644 app/src/main/java/one/fable/fable/audiobookPlayer/TrackListFragment.kt create mode 100644 app/src/main/java/one/fable/fable/audiobookPlayer/TrackListTitleAdapter.kt delete mode 100644 app/src/main/java/one/fable/fable/exoplayer/AudioPlayerService.kt delete mode 100644 app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt delete mode 100644 app/src/main/java/one/fable/fable/exoplayer/DescriptionAdapter.kt diff --git a/app/src/main/java/one/fable/fable/Fable.kt b/app/src/main/java/one/fable/fable/Fable.kt index e9b005c..5b1228a 100644 --- a/app/src/main/java/one/fable/fable/Fable.kt +++ b/app/src/main/java/one/fable/fable/Fable.kt @@ -36,7 +36,7 @@ class Fable : Application() { audiobookDao = FableDatabase.getInstance(applicationContext).audiobookDao directoryDao = FableDatabase.getInstance(applicationContext).directoryDao - //ExoPlayerMasterObject.setSharedPreferencesAndListener(PreferenceManager.getDefaultSharedPreferences(applicationContext)) + //ExoPlayerMasterObject.setSharedPreferencesAndListener() ExoPlayerMasterObject.audiobookDao = audiobookDao } diff --git a/app/src/main/java/one/fable/fable/GlobalSettingsFragment.kt b/app/src/main/java/one/fable/fable/GlobalSettingsFragment.kt index 1dcc2ff..dcd1d43 100644 --- a/app/src/main/java/one/fable/fable/GlobalSettingsFragment.kt +++ b/app/src/main/java/one/fable/fable/GlobalSettingsFragment.kt @@ -1,81 +1,73 @@ package one.fable.fable -import android.content.SharedPreferences import android.os.Bundle import android.text.InputType -import android.text.TextUtils import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import androidx.preference.* -import java.util.prefs.PreferenceChangeEvent -import java.util.prefs.PreferenceChangeListener +import timber.log.Timber class GlobalSettingsFragment : PreferenceFragmentCompat() { - lateinit var sharedPreferences: SharedPreferences - lateinit var seekbarListener: SharedPreferences.OnSharedPreferenceChangeListener override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.global_preferences, rootKey) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val rewindPreference : EditTextPreference? = findPreference("rewind_seconds") val fastForwardPreference : EditTextPreference? = findPreference("fastforward_seconds") val speedSeekBar : SeekBarPreference? = findPreference("playback_speed_seekbar") val theme : ListPreference? = findPreference("theme") - val settingChangeEventListener: SharedPreferences.OnSharedPreferenceChangeListener - //val seekbar : SeekBarPreference? = findPreference("playback_speed") - - + //--Rewind Preference-- rewindPreference?.setOnBindEditTextListener { editText -> - editText.inputType = InputType.TYPE_CLASS_NUMBER + editText.inputType = InputType.TYPE_CLASS_NUMBER //Show only numbers for the rewind preference editor } - - rewindPreference?.setOnPreferenceChangeListener { preference, newValue -> + //Add a listener to show a toast when the rewind speed changes + //Don't add to the editTextListener above as it will show the toast when the keyboard appears + rewindPreference?.setOnPreferenceChangeListener { _, _ -> Toast.makeText(context, "Preference will be reflected after app restart", Toast.LENGTH_LONG).show() true } + + //--Fast Forward Preference fastForwardPreference?.setOnBindEditTextListener { editText -> - editText.inputType = InputType.TYPE_CLASS_NUMBER + editText.inputType = InputType.TYPE_CLASS_NUMBER //Show only numbers for the fast forward preference editor } - - fastForwardPreference?.setOnPreferenceChangeListener { preference, newValue -> + //Add a listener to show a toast when the fast forward speed changes + //Don't add to the editTextListener above as it will show the toast when the keyboard appears + fastForwardPreference?.setOnPreferenceChangeListener { _, _ -> Toast.makeText(context, "Preference will be reflected after app restart", Toast.LENGTH_LONG).show() true } - theme?.setOnPreferenceChangeListener(object : Preference.OnPreferenceChangeListener{ - override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { - when (newValue){ - "1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - "-1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - } - return true + //Theme preference listener + theme?.setOnPreferenceChangeListener { _, newValue -> + when (newValue){ + "1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + "-1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) } - }) - //AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - - speedSeekBar?.title = speedSeekBar?.value?.let { setSpeedSeekBarText(it) } + true + } - seekbarListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key -> - if (key == "playback_speed_seekbar"){ - speedSeekBar?.title = setSpeedSeekBarText(sharedPreferences.getInt(key, 10)) + //Speed Seekbar Preference + speedSeekBar?.title = speedSeekBar?.value?.let { formatSpeedSeekBarText(it) } //This will initialize the speedseekbar with the applicable text + speedSeekBar?.setOnPreferenceChangeListener { _, newValue -> + try { + speedSeekBar.title = formatSpeedSeekBarText(newValue as Int) //The new value should always be an Int, but just in case, catch it + } catch (e : Exception) { + Timber.e(e) } + true } - - sharedPreferences.registerOnSharedPreferenceChangeListener(seekbarListener) - } - fun setSpeedSeekBarText(speed: Int) : String{ + private fun formatSpeedSeekBarText(speed: Int) : String{ val playbackSpeedFloat = speed/10.0 return "Default playback speed: " + playbackSpeedFloat + "x" } diff --git a/app/src/main/java/one/fable/fable/MainActivity.kt b/app/src/main/java/one/fable/fable/MainActivity.kt index bc48e86..b5ce77a 100644 --- a/app/src/main/java/one/fable/fable/MainActivity.kt +++ b/app/src/main/java/one/fable/fable/MainActivity.kt @@ -404,5 +404,20 @@ fun Long.millisToMinutesSecondsString() : String{ //MM:SS fun Long.millisToHoursMinutesString() : String{ //HH:MM return String.format("%02d:%02d", TimeUnit.MILLISECONDS.toHours(this), - TimeUnit.MILLISECONDS.toMinutes(this) % TimeUnit.HOURS.toSeconds(1)) + TimeUnit.MILLISECONDS.toMinutes(this) % TimeUnit.HOURS.toMinutes(1)) +} + +fun Long.millisToHoursMinutesRemainingString() : String{ //HH:MM + return String.format("%02d hrs %02d min remaining", + TimeUnit.MILLISECONDS.toHours(this), + TimeUnit.MILLISECONDS.toMinutes(this) % TimeUnit.HOURS.toMinutes(1)) +} + +fun durationRemaining(elapsed : Long, duration: Long) : String { + val remaining = duration - elapsed + return if (remaining <= 60000L) { + "Audiobook Complete" + } else { + remaining.millisToHoursMinutesRemainingString() + } } diff --git a/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt b/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt index bba1469..4d0fcc6 100644 --- a/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt +++ b/app/src/main/java/one/fable/fable/audiobookPlayer/AudiobookPlayerFragment.kt @@ -36,10 +36,6 @@ import java.util.concurrent.TimeUnit class AudiobookPlayerFragment : Fragment(R.layout.audiobook_player_fragment) { - companion object { - fun newInstance() = AudiobookPlayerFragment() - } - //private lateinit var viewModel: AudiobookPlayerViewModel //= ViewModelProvider(requireActivity()).get(AudiobookPlayerViewModel::class.java) //private lateinit var viewModelFactory: AudiobookPlayerViewModelFactory private lateinit var binding: AudiobookPlayerFragmentBinding @@ -91,10 +87,12 @@ class AudiobookPlayerFragment : Fragment(R.layout.audiobook_player_fragment) { ExoPlayerMasterObject.chapterName.observe(viewLifecycleOwner, chapterNameObserver) binding.trackName.setOnClickListener { - val extras = FragmentNavigatorExtras( - binding.trackName to "track_name" - ) - findNavController().navigate(R.id.action_audiobookPlayerFragment_to_trackListFragment, null, null, extras) +// val extras = FragmentNavigatorExtras( +// binding.trackName to "track_name" +// ) +// findNavController().navigate(R.id.action_audiobookPlayerFragment_to_trackListFragment, null, null, extras) + + findNavController().navigate(R.id.action_audiobookPlayerFragment_to_trackListBottomSheetDialog) } // // trackNameCard.setOnClickListener { diff --git a/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListAdapter.kt b/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListAdapter.kt deleted file mode 100644 index e4916d8..0000000 --- a/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListAdapter.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.example.appleaudiobookcoverapi.exoplayer - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.navigation.NavOptions -import androidx.navigation.findNavController -import androidx.navigation.fragment.FragmentNavigatorExtras -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import one.fable.fable.database.entities.Chapter -import one.fable.fable.databinding.TrackListItemBinding -import one.fable.fable.exoplayer.ExoPlayerMasterObject -import one.fable.fable.R -import one.fable.fable.database.entities.AudioPlaybackWindow - -class TrackListAdapter() : ListAdapter(TrackListDataDiffCallback()) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackListItemViewHolder { - return TrackListItemViewHolder.from(parent) - } - - override fun onBindViewHolder(holder: TrackListItemViewHolder, position: Int) { - val item = getItem(position) - holder.bind(item, position) - } -} - -class TrackListItemViewHolder private constructor(val binding: TrackListItemBinding) : RecyclerView.ViewHolder(binding.root){ - fun bind (item: AudioPlaybackWindow, position : Int){ - binding.trackListItemName.text = item.Name - - - - if (position == ExoPlayerMasterObject.exoPlayer.currentWindowIndex){ - binding.trackListItemName.transitionName = "track_name" - binding.trackListItemEqualizer.visibility = View.VISIBLE - } else { - binding.trackListItemEqualizer.visibility = View.GONE - binding.trackListItemName.transitionName = null - } - - binding.root.setOnClickListener { - //todo - ExoPlayerMasterObject.selectTrack(position) - - binding.trackListItemName.transitionName = "track_name" - - val extras = FragmentNavigatorExtras( - binding.trackListItemName to "exoplayer_track_name_transition" - ) - - //val navOptions = NavOptions.Builder().setPopUpTo(R.id.audiobookPlayerFragment, true).build() - - //binding.root.findNavController().navigate(R.id.action_trackListFragment_to_audiobookPlayerFragment) - - binding.root.findNavController().popBackStack() - } - } - -companion object{ - fun from(parent: ViewGroup) : TrackListItemViewHolder{ - val layoutInflater = LayoutInflater.from(parent.context) - val binding = TrackListItemBinding.inflate(layoutInflater, parent, false) - return TrackListItemViewHolder(binding) - } -} -} - -class TrackListDataDiffCallback : DiffUtil.ItemCallback(){ - override fun areItemsTheSame(oldItem: AudioPlaybackWindow, newItem: AudioPlaybackWindow): Boolean { - return oldItem.Name == newItem.Name && oldItem.startPos == newItem.startPos - } - - override fun areContentsTheSame(oldItem: AudioPlaybackWindow, newItem: AudioPlaybackWindow): Boolean { - return oldItem == newItem - } -} diff --git a/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListBottomSheetDialog.kt b/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListBottomSheetDialog.kt new file mode 100644 index 0000000..8075be0 --- /dev/null +++ b/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListBottomSheetDialog.kt @@ -0,0 +1,38 @@ +package one.fable.fable.audiobookPlayer + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import one.fable.fable.databinding.TrackListBinding +import one.fable.fable.exoplayer.ExoPlayerMasterObject + +class TrackListBottomSheetDialog : BottomSheetDialogFragment() { + lateinit var binding : TrackListBinding + private val trackListTitleAdapter = TrackListTitleAdapter { + onClickCallback(it) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = TrackListBinding.inflate(inflater, container, false) + + binding.trackList.adapter = trackListTitleAdapter + trackListTitleAdapter.submitList(ExoPlayerMasterObject.getListOfTimelineWindows()) + + binding.trackList.scrollToPosition(ExoPlayerMasterObject.exoPlayer.currentMediaItemIndex) + + return binding.root + } + + private fun onClickCallback(position : Int) { + if (position != ExoPlayerMasterObject.exoPlayer.currentMediaItemIndex){ + ExoPlayerMasterObject.selectTrack(position) + } + this.dismiss() + } +} \ No newline at end of file diff --git a/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListFragment.kt b/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListFragment.kt deleted file mode 100644 index 5c8591a..0000000 --- a/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListFragment.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.example.appleaudiobookcoverapi.exoplayer - -import android.app.Activity -import android.os.Bundle -import android.transition.TransitionInflater -import android.util.DisplayMetrics -import android.view.View -import androidx.core.view.doOnPreDraw -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager -import one.fable.fable.R -import one.fable.fable.databinding.TrackListBinding -import one.fable.fable.exoplayer.ExoPlayerMasterObject -import timber.log.Timber - -class TrackListFragment : Fragment(R.layout.track_list) { - private lateinit var binding: TrackListBinding - private val trackListAdapter = TrackListAdapter() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - postponeEnterTransition() - - sharedElementEnterTransition = TransitionInflater.from(context) - .inflateTransition(R.transition.change_bounds_and_transform) - - binding = TrackListBinding.bind(view) - binding.setLifecycleOwner(this) - - binding.trackList.adapter = trackListAdapter - trackListAdapter.submitList(ExoPlayerMasterObject.audioPlaybackWindows) - - //val args = TrackListFragmentArgs.fromBundle(requireArguments()) - - - val displayMetrics = DisplayMetrics() - - Timber.i("The recylerview is this many high: " + displayMetrics.heightPixels) - - //https://stackoverflow.com/questions/52504534/kotlin-recyclerview-scrolltopositionwithoffset-not-showing-up - //https://stackoverflow.com/questions/37270265/how-to-center-the-clicked-position-in-the-recyclerview/44854796 - (binding.trackList.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(ExoPlayerMasterObject.exoPlayer.currentWindowIndex, 500) - //binding.trackList.scrollToPosition(ExoPlayerInterface.exoPlayer.currentWindowIndex) - - view.doOnPreDraw { startPostponedEnterTransition() } - - } -} \ No newline at end of file diff --git a/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListTitleAdapter.kt b/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListTitleAdapter.kt new file mode 100644 index 0000000..90e1b2e --- /dev/null +++ b/app/src/main/java/one/fable/fable/audiobookPlayer/TrackListTitleAdapter.kt @@ -0,0 +1,59 @@ +package one.fable.fable.audiobookPlayer + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import one.fable.fable.databinding.TrackListItemBinding +import one.fable.fable.exoplayer.ExoPlayerMasterObject + +class TrackListTitleAdapter(private val onClickCallback : (Int) -> Unit) : ListAdapter( + TrackListTitleDiffCallback() +) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackListTitleViewHolder { + return TrackListTitleViewHolder.from(parent, onClickCallback = onClickCallback) + } + + override fun onBindViewHolder(holder: TrackListTitleViewHolder, position: Int) { + val item = getItem(position) + holder.bind(item, position) + } +} + +class TrackListTitleViewHolder private constructor(val binding: TrackListItemBinding, private val onClickCallback: (Int) -> Unit) : RecyclerView.ViewHolder(binding.root){ + fun bind (title: String, position : Int){ + if (position == ExoPlayerMasterObject.exoPlayer.currentMediaItemIndex) { + binding.trackListItemName.text = "$title - Now Playing..." + binding.trackListItemEqualizer.visibility = View.VISIBLE + binding.root.alpha = 0.5f + } else { + binding.trackListItemName.text = title + binding.trackListItemEqualizer.visibility = View.GONE + binding.root.alpha = 1f + } + + binding.root.setOnClickListener { + onClickCallback(position) + } + } + +companion object{ + fun from(parent: ViewGroup, onClickCallback: (Int) -> Unit) : TrackListTitleViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = TrackListItemBinding.inflate(layoutInflater, parent, false) + return TrackListTitleViewHolder(binding, onClickCallback) + } +} +} + +class TrackListTitleDiffCallback : DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: String, newItem: String): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: String, newItem: String): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/one/fable/fable/exoplayer/AudioPlayerService.kt b/app/src/main/java/one/fable/fable/exoplayer/AudioPlayerService.kt deleted file mode 100644 index f785be2..0000000 --- a/app/src/main/java/one/fable/fable/exoplayer/AudioPlayerService.kt +++ /dev/null @@ -1,217 +0,0 @@ -//package one.fable.fable.exoplayer -// -//import android.app.* -//import android.content.Context -//import android.content.Intent -//import android.media.session.MediaSession -//import android.net.Uri -//import android.os.Binder -//import android.os.Build -//import android.os.IBinder -//import android.support.v4.media.MediaDescriptionCompat -//import android.support.v4.media.session.MediaSessionCompat -//import com.google.android.exoplayer2.C -//import com.google.android.exoplayer2.Player -//import com.google.android.exoplayer2.SimpleExoPlayer -//import com.google.android.exoplayer2.audio.AudioAttributes -//import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -//import com.google.android.exoplayer2.source.MediaSource -//import com.google.android.exoplayer2.source.MediaSourceFactory -//import com.google.android.exoplayer2.ui.PlayerNotificationManager -//import kotlinx.coroutines.CoroutineScope -//import kotlinx.coroutines.Dispatchers -//import kotlinx.coroutines.launch -//import one.fable.fable.R -//import timber.log.Timber -// -//class AudioPlayerService : Service() { -// lateinit var exoPlayer : SimpleExoPlayer -// lateinit var exoPlayerNotificationManager : PlayerNotificationManager -// lateinit var mediaSession: MediaSessionCompat -// lateinit var mediaSessionConnector: MediaSessionConnector //You need to also depend on com.google.android.exoplayer:extension-mediasession:2.6.1 -// -// val eventListener = ExoPlayerEventListener() -// -// val binder = LocalBinder() -// override fun onTaskRemoved(rootIntent: Intent?) { -// Timber.i("Task Removed") -// stopForeground(true) -// exoPlayer.playWhenReady = false -// //exoPlayer.stop() -// stopSelf() -// //ExoPlayerInterface. -// -// -// super.onTaskRemoved(rootIntent) -// } -// -// inner class LocalBinder : Binder() { -// fun getService() : AudioPlayerService = this@AudioPlayerService -// } -// -// inner class ExoPlayerEventListener() : Player.EventListener{ -// var isPlayingBool = false -// -// override fun onIsPlayingChanged(isPlaying: Boolean) { -// super.onIsPlayingChanged(isPlaying) -// isPlayingBool = isPlaying -// Timber.i("Is playing changed to: " + isPlaying) -// if(!isPlaying){ -// //Timber.i("Remove the notification") -// -// //I've pretty much figured this out, but felt I should make note of where I found the info -// //https://stackoverflow.com/questions/43596709/make-a-notification-from-a-foreground-service-cancelable-once-the-service-is-not -// //https://github.com/google/ExoPlayer/issues/4256 -// stopForeground(false) -// CoroutineScope(Dispatchers.IO).launch { -// ExoPlayerMasterObject.updateAudiobook() -// } -// } -// -// } -// } -// -//// override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { -//// Timber.i("OnStartCommand Called") -//// return START_STICKY -//// //return super.onStartCommand(intent, flags, startId) -//// } -// -// override fun onCreate() { -// -// Timber.i("On create Called") -// super.onCreate() -// exoPlayer = ExoPlayerMasterObject.exoPlayer -// exoPlayer.addListener(eventListener) -// //(application as Fable).exoPlayer = exoPlayer -// -// Timber.i("ExoPlayer Attached to the interface") -// -// -// //https://medium.com/google-exoplayer/exoplayer-2-11-whats-new-e0e0701e4b6c -// exoPlayer.setWakeMode(C.WAKE_MODE_NETWORK) -// exoPlayer.setHandleAudioBecomingNoisy(true) -// -// -// //https://medium.com/google-exoplayer/easy-audio-focus-with-exoplayer-a2dcbbe4640e -// val audioBookAudioAttributes = AudioAttributes.Builder() -// .setUsage(C.USAGE_MEDIA) -// .setContentType(C.CONTENT_TYPE_SPEECH) -// .build() -// -// exoPlayer.setAudioAttributes(audioBookAudioAttributes, true) -// -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { -// // Create the NotificationChannel -// val name = "Audiobook Playback" -// val descriptionText = "Fable audiobook player" -// val importance = NotificationManager.IMPORTANCE_DEFAULT -// val mChannel = NotificationChannel("PLAYBACK", name, importance) -// mChannel.description = descriptionText -// mChannel.setSound(null, null) -// // Register the channel with the system; you can't change the importance -// // or other notification behaviors after this -// val notificationManager = getSystemService(Application.NOTIFICATION_SERVICE) as NotificationManager -// notificationManager.createNotificationChannel(mChannel) -// } -// -// //https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/PlayerNotificationManager.html -// //https://medium.com/google-exoplayer/playback-notifications-with-exoplayer-a2f1a18cf93b -// exoPlayerNotificationManager = PlayerNotificationManager(applicationContext, "PLAYBACK", 1, DescriptionAdapter(applicationContext), object: -// PlayerNotificationManager.NotificationListener { -// -// override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) { -// super.onNotificationCancelled(notificationId, dismissedByUser) -// //exoPlayer.stop() -// //stopSelf() -// } -// -// -// override fun onNotificationPosted( -// notificationId: Int, -// notification: Notification, -// ongoing: Boolean -// ) { -// super.onNotificationPosted(notificationId, notification, ongoing) -// -// //if(eventListener.isPlayingBool){ -// if(exoPlayer.isPlaying){ -// startForeground(notificationId,notification) -// } -// Timber.i("Start Foreground called") -// } -// }) -// -// exoPlayerNotificationManager.setPlayer(exoPlayer) -// -// //Todo: Implement the mediabuttonreceiver for api 19 https://developer.android.com/guide/topics/media-apps/mediabuttons -// -// mediaSession = MediaSessionCompat(applicationContext, Context.MEDIA_SESSION_SERVICE) -// mediaSession.isActive = true -// exoPlayerNotificationManager.setMediaSessionToken(mediaSession.sessionToken) -// exoPlayerNotificationManager.setSmallIcon(R.drawable.fable_icon_notification) -// mediaSessionConnector = MediaSessionConnector(mediaSession) -//// mediaSessionConnector.setQueueNavigator(object: TimelineQueueNavigator(mediaSession){ -//// override fun getMediaDescription( -//// player: Player?, -//// windowIndex: Int -//// ): MediaDescriptionCompat { -//// TODO("Not yet implemented") -//// return MediaDescriptionCompat.Builder() -//// .setMediaId() -//// .setIconBitmap() -//// .setTitle() -//// .setDescription() -//// .setExtras() -//// .build() -//// } -//// }) -// mediaSessionConnector.setPlayer(exoPlayer) -// -// -// } -// -// override fun onBind(intent: Intent?): IBinder? { -// return binder -// } -// -// override fun onLowMemory() { -// super.onLowMemory() -// stopForeground(true) -// //exoPlayerNotificationManager.setPlayer(null) -// -// } -// -// override fun onDestroy() { -// stopForeground(true) -// ExoPlayerMasterObject.cancelSleepTimer() -// exoPlayerNotificationManager.setPlayer(null) -// mediaSessionConnector.setPlayer(null) -// //exoPlayer.stop() -// stopSelf() -// super.onDestroy() -// -// //https://stackoverflow.com/questions/6330200/how-to-quit-android-application-programmatically -// //I may want to do this if I start getting unintended crashes -// } -//// -//// fun getExoPlayerInstance() : SimpleExoPlayer{ -//// return exoPlayer -//// } -// -// -//// override fun onHandleIntent(intent: Intent?) { -//// // Normally we would do some work here, like download a file. -//// // For our sample, we just sleep for 5 seconds. -//// try { -//// -//// //exoPlayer.prepare((Uri.parse("https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/01_-_Intro_-_The_Way_Of_Waking_Up_feat_Alan_Watts.mp3"))) -//// Thread.sleep(5000) -//// } catch (e: InterruptedException) { -//// // Restore interrupt status. -//// Thread.currentThread().interrupt() -//// } -//// -//// } -// -//} \ No newline at end of file diff --git a/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt b/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt deleted file mode 100644 index ec87e17..0000000 --- a/app/src/main/java/one/fable/fable/exoplayer/CustomPlayerControlView.kt +++ /dev/null @@ -1,71 +0,0 @@ -//package com.example.appleaudiobookcoverapi -// -//import android.app.Activity -//import android.content.Context -//import android.util.AttributeSet -//import android.view.View -//import android.widget.* -//import androidx.media3.ui.PlayerControlView -//import androidx.preference.PreferenceManager -//import one.fable.fable.R -//import one.fable.fable.exoplayer.ExoPlayerMasterObject -//import timber.log.Timber -//import java.lang.Exception -//import java.util.concurrent.TimeUnit -// -//class CustomPlayerControlView : PlayerControlView { -// constructor(context: Context) : super(context) -// constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet) -// constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr : Int) : super (context, attributeSet, defStyleAttr) -// constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr : Int, playbackAttributeSet: AttributeSet?) : super (context, attributeSet, defStyleAttr, playbackAttributeSet) -// -// val customRewind : ImageButton = findViewById(R.id.exo_audiobook_rewind) -// val customFastForward : ImageButton = findViewById(R.id.exo_audiobook_ffwd) -// -// private val rewind = "rewind" -// private val fastForward = "fastForward" -// -// init { -// customRewind.setOnClickListener{audiobookSeekInterval(rewind)} -// customFastForward.setOnClickListener{audiobookSeekInterval(fastForward)} -// } -// -// -// fun audiobookSeekInterval(seekDirection : String){ -// -// val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) -// val rewindAmountInSeconds = sharedPreferences.getString("rewind_seconds", "") -// val fastforwardAmountInSeconds = sharedPreferences.getString("fastforward_seconds", "") -// var rewindAmount : Long -// var fastforwardAmount : Long -// -// try { -// rewindAmount = TimeUnit.SECONDS.toMillis(rewindAmountInSeconds!!.toLong()) -// } catch (e: Exception){ -// rewindAmount = 10000L -// } -// -// try{ -// fastforwardAmount = TimeUnit.SECONDS.toMillis(fastforwardAmountInSeconds!!.toLong()) -// } catch (e: Exception) { -// fastforwardAmount = 30000L -// } -// -// val player = super.getPlayer() -// if(player != null){ -// val timeline = player.currentTimeline -// if (!timeline.isEmpty){ -// if (seekDirection == rewind){ -// ExoPlayerMasterObject.customRewind(rewindAmount) -// } else if (seekDirection == fastForward){ -// ExoPlayerMasterObject.customFastForward(fastforwardAmount) -// } -// } -// } -// } -// -// -//} -// -// -// diff --git a/app/src/main/java/one/fable/fable/exoplayer/DescriptionAdapter.kt b/app/src/main/java/one/fable/fable/exoplayer/DescriptionAdapter.kt deleted file mode 100644 index a88a461..0000000 --- a/app/src/main/java/one/fable/fable/exoplayer/DescriptionAdapter.kt +++ /dev/null @@ -1,71 +0,0 @@ -//package one.fable.fable.exoplayer -// -//import android.app.PendingIntent -//import android.content.Context -//import android.content.Intent -//import android.graphics.Bitmap -//import android.graphics.ImageDecoder -//import android.provider.MediaStore -//import one.fable.fable.MainActivity -// -// -//class DescriptionAdapter(context: Context) : PlayerNotificationManager.MediaDescriptionAdapter { -// val context = context -// -// override fun createCurrentContentIntent(player: Player): PendingIntent? { -// //TODO Open directly to the fragment -// //https://stackoverflow.com/questions/26608627/how-to-open-fragment-page-when-pressed-a-notification-in-android -// -// //val intent = Intent(context, MainActivity::class.java) -// val intent = Intent(context, MainActivity::class.java) -// return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) -// } -// -// override fun getCurrentContentText(player: Player): CharSequence? { -// val window = Timeline.Window() -//// player.currentTimeline.getWindow(player.currentWindowIndex, window) -//// -//// return window.tag.toString() -// return ExoPlayerMasterObject.audioPlaybackWindows[player.currentWindowIndex].Name.toString() -// } -// -// override fun getCurrentContentTitle(player: Player): CharSequence { -// //val window = Timeline.Window() -// //val period = Timeline.Period() -// //player.currentTimeline.getWindow(player.currentWindowIndex, window) -// //player.currentTimeline.getPeriod(player.currentPeriodIndex, period) -// -// return ExoPlayerMasterObject.audiobook.audiobookTitle -// -// -// //return "Test 1" -// } -// -// override fun getCurrentLargeIcon( -// player: Player, -// callback: PlayerNotificationManager.BitmapCallback -// ): Bitmap? { -// val bitmapUri = ExoPlayerMasterObject.audiobook.imgThumbnail -// if (bitmapUri != null){ -// var bitmapSource : Bitmap? = null -// try { -// //https://stackoverflow.com/questions/56651444/deprecated-getbitmap-with-api-29-any-alternative-codes -// //"getBitmap" is deprecated. Someday I'll have to fix this. But clearly today is not that day. -// -// //https://stackoverflow.com/questions/48729200/how-to-convert-uri-to-bitmap/48729608 -// bitmapSource = MediaStore.Images.Media.getBitmap(context.contentResolver, bitmapUri) -// } catch(e: Exception) { -// timber.log.Timber.e(e) -// } -// //val bitmapSource = bitmapUri?.let { ImageDecoder.createSource(context.contentResolver, it) } -// return bitmapSource -// -// } else{ -// return null -// } -// -// //return MediaStore.Images.Media.getBitmap() -// //return context.resources.getDrawable() -// //return ExoPlayerInterface.ExoPlayerCompanionObject.coverUri. -// } -//} diff --git a/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt b/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt index 49b461a..6a00ae2 100644 --- a/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt +++ b/app/src/main/java/one/fable/fable/exoplayer/ExoPlayerMasterObject.kt @@ -1,10 +1,9 @@ package one.fable.fable.exoplayer +import android.annotation.SuppressLint import android.content.SharedPreferences import android.graphics.Bitmap import android.graphics.ImageDecoder -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import android.os.CountDownTimer @@ -12,12 +11,9 @@ import android.os.Handler import android.os.Looper import android.provider.MediaStore import android.util.Xml -import androidx.core.net.toFile import androidx.lifecycle.MutableLiveData import androidx.media3.common.* -import androidx.media3.common.MediaMetadata.PICTURE_TYPE_FRONT_COVER import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.SeekParameters import androidx.preference.PreferenceManager import kotlinx.coroutines.* import one.fable.fable.Fable @@ -42,13 +38,40 @@ Building feature-rich media apps with ExoPlayer (Google I/O '18) https://www.you */ object ExoPlayerMasterObject { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(Fable.instance) - val rewindAmountInSeconds = sharedPreferences.getString("rewind_seconds", "") - val fastforwardAmountInSeconds = sharedPreferences.getString("fastforward_seconds", "") + //To build the ExoPlayer object, we need to first get the app shared preferences (or set the shared preferences to default values) for the rewind and fast forward amounts + private val sharedPreferences : SharedPreferences = + PreferenceManager.getDefaultSharedPreferences(Fable.instance).also { + registerOnSharedPreferenceChangedListener(it) + } + //Get the shared preference values + private val rewindAmountInSeconds = sharedPreferences.getString("rewind_seconds", "10") //Get the string representation of the rewind amount (in seconds). If it doesn't exist yet, set to the default of 10 seconds + private val fastForwardAmountInSeconds = sharedPreferences.getString("fastforward_seconds", "30") //Get the string representation of the fast forward amount (in seconds). If it doesn't exist yet, set to the default of 30 seconds + private var globalPlaybackSpeed = sharedPreferences.getInt("playback_speed_seekbar", 10) //Initialize the global playback speed with the sharedPref value (or set the default of 10 aka 1.0x) + set(value) { field = value + //Essentially we're building on the sharedPref listener and when we change the global playback speed, we'll also trigger this code (from the set) + if(isAudiobookInitialized()){ + if (audiobook.playbackSpeed == null){ //Only change the playback speed if the audiobook itself doesn't have one already set (i.e. the audiobook uses the global speed) + setPlaybackSpeed(value) + } + } + } - val rewindAmount : Long = rewindAmountInSeconds?.toLongOrNull()?.let { TimeUnit.SECONDS.toMillis(it) } ?: 10000L - val fastforwardAmount : Long = fastforwardAmountInSeconds?.toLongOrNull()?.let { TimeUnit.SECONDS.toMillis(it) } ?: 30000L + //This listener can actually notify of any shared preference change. + //In this case, we only care about the playback speed, as rewind and fast forward cannot be changed after the ExoPlayer object has been built + private fun registerOnSharedPreferenceChangedListener(defaultSharedPreferences : SharedPreferences) { + defaultSharedPreferences.registerOnSharedPreferenceChangeListener { sharedPreferences, key -> + when (key) { + "playback_speed_seekbar" -> { //Listen for the global speed change + sharedPreferences?.getInt(key, 10)?.let { updatedGlobalPlaybackSpeed -> + globalPlaybackSpeed = updatedGlobalPlaybackSpeed //Invoke the custom setter (kinda like a "observer", but it doesn't depend on a viewLifecycleOwner) + } + } + } + } + } + //Build the ExoPlayer + @SuppressLint("UnsafeOptInUsageError") val exoPlayer : ExoPlayer = ExoPlayer.Builder(Fable.instance) .setAudioAttributes( AudioAttributes.Builder() @@ -57,8 +80,12 @@ object ExoPlayerMasterObject { .build(), true ) - .setSeekBackIncrementMs(rewindAmount) - .setSeekForwardIncrementMs(fastforwardAmount) + .setSeekBackIncrementMs( + rewindAmountInSeconds?.toLongOrNull()?.let { TimeUnit.SECONDS.toMillis(it) } ?: 10000L //Convert the rewind time in seconds to milliseconds. Or assign the default of 10000 milliseconds (10 seconds) + ) + .setSeekForwardIncrementMs( + fastForwardAmountInSeconds?.toLongOrNull()?.let { TimeUnit.SECONDS.toMillis(it) } ?: 30000L //Convert the fast forward time in seconds to milliseconds. Or assign the default of 30000 milliseconds (30 seconds) + ) .setHandleAudioBecomingNoisy(true) .setWakeMode(C.WAKE_MODE_LOCAL) .build() @@ -66,8 +93,6 @@ object ExoPlayerMasterObject { lateinit var audiobookDao: AudiobookDao - val audioPlaybackWindows = mutableListOf() - var coverByteArray : ByteArray? = null var coverUri : Uri? = null @@ -80,33 +105,10 @@ object ExoPlayerMasterObject { var audiobookTracks = listOf() var tracksWithChapters = listOf() - val progress = MutableLiveData() - val windowDurationsSummation = mutableListOf() private var loadBookJob : Job? = null - //lateinit var sharedPreferences: SharedPreferences - val settingChangeEventListener = SharedPreferences.OnSharedPreferenceChangeListener{ sharedPreferences: SharedPreferences?, key: String? -> - if (key == "playback_speed_seekbar"){ - globalPlaybackSpeed.value = sharedPreferences?.getInt(key, 10) - if(isAudiobookInitialized()){ - if (audiobook.playbackSpeed == null){ - globalPlaybackSpeed.value?.let { setPlaybackSpeed(it) } - } - } - } - } - - -// fun isExoPlayerInitialized() = this::exoPlayer.isInitialized //https://stackoverflow.com/questions/47549015/isinitialized-backing-field-of-lateinit-var-is-not-accessible-at-this-point -// fun buildExoPlayer(context: Context){ -// if (!this::exoPlayer.isInitialized){ -// exoPlayer = ExoPlayer.Builder(context).build() -// exoPlayer.addListener(eventListener) -// } -// } - fun loadAudiobook(audiobookToLoad: Audiobook){ var loadNewBook = false if (!isAudiobookInitialized()){ @@ -160,12 +162,6 @@ object ExoPlayerMasterObject { } ?: return } - - fun setSharedPreferencesAndListener(){ - sharedPreferences.registerOnSharedPreferenceChangeListener(settingChangeEventListener) - globalPlaybackSpeed.value = sharedPreferences.getInt("playback_speed_seekbar", 10) - } - suspend fun getTracks(title: String){ withContext(Dispatchers.IO){ audiobookTracks = audiobookDao.getAudiobookTracks(title) @@ -265,18 +261,21 @@ object ExoPlayerMasterObject { - audiobook.playbackSpeed?.let { setPlaybackSpeed(it) } ?: run { - globalPlaybackSpeed.value?.let { - setPlaybackSpeed(it) - } + audiobook.playbackSpeed?.let { audiobookPlaybackSpeed -> + setPlaybackSpeed(audiobookPlaybackSpeed) + } ?: run { + setPlaybackSpeed(globalPlaybackSpeed) //Reset to the global value } if (invalidTrackDurationTrigger) { + windowDurationsSummation.clear() //TODO Delay movement to Audiobook fragment until entire duration is captured val durationListener = DurationPlayerListener({}) exoPlayer.addListener(durationListener) exoPlayer.seekTo(0, 0) } else { + exoPlayer.addListener(PlayerListener) + val windowIndex = audiobook.windowIndex val windowLocation = audiobook.windowLocation @@ -290,50 +289,16 @@ object ExoPlayerMasterObject { } //getMediaItemDurations() - - - } - private fun getMediaItemDurations(){ - windowDurationsSummation.clear() - - val timeline = exoPlayer.currentTimeline - for (index in 0 until timeline.windowCount){ - var window = Timeline.Window() - timeline.getWindow(index, window) - - var duration = window.durationMs //duration = window.getDurationUs() / 1000 - //TODO: Someday when the fix gets pushed to a release, get rid of this - //https://github.com/google/ExoPlayer/issues/7314 - if (duration <= 0){ - val outsideCalculatedDuration = audioPlaybackWindows[index].duration - if (outsideCalculatedDuration != null){ - duration = outsideCalculatedDuration - } else { - val period = Timeline.Period() - timeline.getPeriod(window.firstPeriodIndex, period) - - duration = period.durationMs - audioPlaybackWindows[index].startPos - } - if (duration < 0){ - val trackAtIndexOrNull = audiobookTracks.elementAtOrNull(index) - if (trackAtIndexOrNull != null) { - trackAtIndexOrNull.trackLength?.let { duration = it } - } - } - if (duration < 0){ - duration = 0 - } - } - - val previousValue = windowDurationsSummation.lastOrNull() - if (previousValue == null) { - windowDurationsSummation.add(duration) - } else { - windowDurationsSummation.add(previousValue.plus(duration)) - } + fun getListOfTimelineWindows() : ArrayList { + val mediaItemTitles = ArrayList() + for (index in 0 .. exoPlayer.currentTimeline.windowCount) { + val window = Timeline.Window() + exoPlayer.currentTimeline.getWindow(index, window) + mediaItemTitles.add(window.mediaItem.mediaMetadata.title.toString()) } + return mediaItemTitles } suspend fun checkFileForChapterInfo(tracks: List) { @@ -356,9 +321,9 @@ object ExoPlayerMasterObject { while (iterator.hasNext() && iteratorCounter <= 500 && line.isBlank()) { val fileLine = iterator.next() - if (fileLine.contains("OverDrive MediaMarkers")) { - line = fileLine - } + if (fileLine.contains("OverDrive MediaMarkers")) { + line = fileLine + } iteratorCounter += 1 } bufferedReader.close() @@ -449,7 +414,7 @@ object ExoPlayerMasterObject { val progressTrackerRunnable = object : Runnable{ override fun run() { updateAudiobookObjectLocation() - progressTrackerHandler.postDelayed(this, 400) + progressTrackerHandler.postDelayed(this, 5000) //Update the audiobook location every 5 seconds } } @@ -460,6 +425,7 @@ object ExoPlayerMasterObject { return timelineDuration } + //This is kind of a beta item. It's intended to cycle through each media item to get the exact length of it. class DurationPlayerListener(private val callback: () -> Unit) : Player.Listener{ override fun onTimelineChanged(timeline: Timeline, reason: Int) { super.onTimelineChanged(timeline, reason) @@ -473,7 +439,7 @@ object ExoPlayerMasterObject { exoPlayer.seekToNextMediaItem() } else { exoPlayer.removeListener(this) - exoPlayer.addListener(eventListener) + exoPlayer.addListener(PlayerListener) val windowIndex = audiobook.windowIndex val windowLocation = audiobook.windowLocation @@ -494,11 +460,9 @@ object ExoPlayerMasterObject { } //PLAYER EVENT LISTENER CODE todo - private val eventListener = PlayerEventListener() -// .also { -// exoPlayer.addListener(it) -// } - class PlayerEventListener() : Player.Listener{ + //private val eventListener = PlayerEventListener() + + object PlayerListener : Player.Listener{ //var isPlayingBool = false override fun onIsPlayingChanged(isPlaying: Boolean) { @@ -517,32 +481,18 @@ object ExoPlayerMasterObject { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { super.onMediaItemTransition(mediaItem, reason) chapterName.value = mediaItem?.mediaMetadata?.title.toString() - progress.value = getTimelineDuration() - } - - override fun onTimelineChanged(timeline: Timeline, reason: Int) { - /*------Detecting when playback transitions to another item------ - (continuation of the info above) - This last one is actually a .onTimeLineChanged - 3. EventListener.onTimelineChanged with reason = Player.TIMELINE_CHANGE_REASON_DYNAMIC. - This happens when the playlist changes, e.g. if items are added, moved, or removed. - */ - if (audioPlaybackWindows.isNotEmpty()){ - Timber.i("Timeline Changed") - chapterName.value = audioPlaybackWindows[exoPlayer.currentMediaItemIndex].Name - progress.value = getTimelineDuration() + updateAudiobookObjectLocation() + CoroutineScope(Dispatchers.IO).launch{ + updateAudiobook() } - - super.onTimelineChanged(timeline, reason) } - } + } fun updateAudiobookObjectLocation(){ audiobook.windowIndex = exoPlayer.currentMediaItemIndex audiobook.windowLocation = exoPlayer.currentPosition audiobook.timelineDuration = getTimelineDuration() - progress.value = audiobook.timelineDuration audiobook.lastPlayedTimeStamp = System.currentTimeMillis() if (audiobook.duration > 0L) { @@ -552,12 +502,6 @@ object ExoPlayerMasterObject { audiobook.progressState = PROGRESS_IN_PROGRESS } } - -// if (exoPlayer.currentWindowIndex == exoPlayer.currentTimeline.windowCount - 1){ -// audiobook.progressState = PROGRESS_FINISHED -// } else { -// audiobook.progressState = PROGRESS_IN_PROGRESS -// } } suspend fun updateAudiobook(){ @@ -589,12 +533,10 @@ object ExoPlayerMasterObject { val playbackSpeed = MutableLiveData() val playbackSpeedAsInt = MutableLiveData() - val globalPlaybackSpeed = MutableLiveData() - fun setPlaybackSpeed(speedAsInt: Int){ val speedAsFloat = speedAsInt / 10.0f val playbackParameters = PlaybackParameters(speedAsFloat) - exoPlayer.setPlaybackParameters(playbackParameters) + exoPlayer.playbackParameters = playbackParameters playbackSpeedAsInt.value = speedAsInt playbackSpeed.value = speedAsFloat.toString() + "x" } @@ -605,7 +547,8 @@ object ExoPlayerMasterObject { } fun resetSpeedToDefault(){ - globalPlaybackSpeed.value?.let { setPlaybackSpeed(it) } + setPlaybackSpeed(globalPlaybackSpeed) + audiobook?.playbackSpeed = null CoroutineScope(Dispatchers.IO).launch{ updateAudiobook() diff --git a/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt b/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt index 2e7fb01..c5656e2 100644 --- a/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt +++ b/app/src/main/java/one/fable/fable/exoplayer/MediaPlayerService.kt @@ -1,6 +1,5 @@ package one.fable.fable.exoplayer -import androidx.media3.common.MediaMetadata import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService @@ -12,8 +11,7 @@ class PlaybackService : MediaSessionService() { super.onCreate() mediaSession = MediaSession.Builder(this, ExoPlayerMasterObject.exoPlayer).build() } - // Return a MediaSession to link with the MediaController that is making - // this request. + // Return a MediaSession to link with the MediaController that is making this request. override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession diff --git a/app/src/main/java/one/fable/fable/library/LibraryFragment.kt b/app/src/main/java/one/fable/fable/library/LibraryFragment.kt index 7f28d82..f740780 100644 --- a/app/src/main/java/one/fable/fable/library/LibraryFragment.kt +++ b/app/src/main/java/one/fable/fable/library/LibraryFragment.kt @@ -103,6 +103,10 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { inProgressHeader.visibility = View.GONE inProgressDivider.visibility = View.GONE inProgressRecyclerView.visibility = View.GONE + } else { + inProgressHeader.visibility = View.VISIBLE + inProgressDivider.visibility = View.VISIBLE + inProgressRecyclerView.visibility = View.VISIBLE } inProgressItemAdapter.submitList(it) } @@ -112,6 +116,10 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { notStartedHeader.visibility = View.GONE notStartedDivider.visibility = View.GONE notStartedRecyclerView.visibility = View.GONE + } else { + notStartedHeader.visibility = View.VISIBLE + notStartedDivider.visibility = View.VISIBLE + notStartedRecyclerView.visibility = View.VISIBLE } notStartedItemAdapter.submitList(it) } @@ -121,6 +129,10 @@ class LibraryFragment : Fragment(R.layout.library_fragment) { finishedHeader.visibility = View.GONE finishedDivider.visibility = View.GONE finishedRecyclerView.visibility = View.GONE + } else { + finishedHeader.visibility = View.VISIBLE + finishedDivider.visibility = View.VISIBLE + finishedRecyclerView.visibility = View.VISIBLE } finishedItemAdapter.submitList(it) } diff --git a/app/src/main/java/one/fable/fable/library/LibraryFragmentAdapter.kt b/app/src/main/java/one/fable/fable/library/LibraryFragmentAdapter.kt index ed8ded4..a8ca80b 100644 --- a/app/src/main/java/one/fable/fable/library/LibraryFragmentAdapter.kt +++ b/app/src/main/java/one/fable/fable/library/LibraryFragmentAdapter.kt @@ -1,59 +1,33 @@ package one.fable.fable.library -import android.view.LayoutInflater import android.view.LayoutInflater.from -import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.DialogFragment -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.Observer import androidx.navigation.findNavController import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.* import one.fable.fable.R import one.fable.fable.database.entities.Audiobook import one.fable.fable.databinding.LibraryItemBinding +import one.fable.fable.durationRemaining import one.fable.fable.exoplayer.ExoPlayerMasterObject -import one.fable.fable.millisToHoursMinutesSecondsString import timber.log.Timber -class LibraryFragmentAdapter() : ListAdapter(LibraryDataDiffCallback()) { +class LibraryFragmentAdapter : ListAdapter(LibraryDataDiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryItemViewHolder { - return LibraryItemViewHolder.from(parent) } override fun onBindViewHolder(holder: LibraryItemViewHolder, position: Int) { val item = getItem(position) - - val progressObserver = Observer { - Timber.i("3 Progress is: " + ExoPlayerMasterObject.progress.value) - Timber.i("4 Progress is:" + it.toString()) - item.duration = it - } - holder.bind(item) - -// if (ExoPlayerMasterObject.isAudiobookInitialized() && item.audiobookId == ExoPlayerMasterObject.audiobook.audiobookId){ -// holder.itemView.visibility = View.GONE -// holder.itemView.layoutParams = RecyclerView.LayoutParams(0,0) -// } else { -// holder.itemView.visibility = View.VISIBLE -// //holder.itemView.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) -// } - - //holder.binding.lifecycleOwner?.let { ExoPlayerMasterObject.progress.observe(it, progressObserver) } } } class LibraryItemViewHolder private constructor(val binding: LibraryItemBinding) : RecyclerView.ViewHolder(binding.root){ - object libraryItemListener : LibraryItemListener fun bind(item: Audiobook){ binding.gridItemTitle.text = item.audiobookTitle @@ -68,44 +42,28 @@ class LibraryItemViewHolder private constructor(val binding: LibraryItemBinding) binding.coverImage.setImageResource(R.drawable.ic_album) } + binding.audiobookDuration.text = + durationRemaining(item.timelineDuration, item.duration) + //item.timelineDuration.millisToHoursMinutesSecondsString() + "/" + item.duration.millisToHoursMinutesSecondsString() + binding.libraryItemProgressbar.max = item.duration.toInt() + binding.libraryItemProgressbar.progress = item.timelineDuration.toInt() - if (ExoPlayerMasterObject.isAudiobookInitialized() && item.audiobookId == ExoPlayerMasterObject.audiobook.audiobookId){ - Timber.i("Progress is: " + ExoPlayerMasterObject.progress.value) - binding.audiobookDuration.text = ExoPlayerMasterObject.progress.value?.millisToHoursMinutesSecondsString() - val progressObserver = Observer { - Timber.i("1 Progress is: " + ExoPlayerMasterObject.progress.value) - Timber.i("2 Progress is:" + it.toString()) - binding.audiobookDuration.text = it.millisToHoursMinutesSecondsString() + "/" + item.duration.millisToHoursMinutesSecondsString() - binding.libraryItemProgressbar.max = item.duration.toInt() - binding.libraryItemProgressbar.progress = it.toInt() - } - - //https://stackoverflow.com/questions/54825613/how-to-use-livedata-and-viewmodel-with-a-viewholder-as-lifecycle-owner - ExoPlayerMasterObject.progress.observe((binding.root.context as LifecycleOwner), progressObserver) - - } else { - binding.audiobookDuration.text = - item.timelineDuration.millisToHoursMinutesSecondsString() + "/" + item.duration.millisToHoursMinutesSecondsString() - binding.libraryItemProgressbar.max = item.duration.toInt() - binding.libraryItemProgressbar.progress = item.timelineDuration.toInt() - } binding.coverImage.transitionName = item.audiobookTitle + "cover_image" binding.gridItemTitle.transitionName = item.audiobookTitle binding.gridItemAuthor.transitionName = item.audiobookTitle + item.audiobookAuthor - binding.libraryItemCardView.transitionName = item.audiobookTitle + "material_card" + //binding.libraryItemCardView.transitionName = item.audiobookTitle + "material_card" binding.libraryItemProgressbar.transitionName = item.audiobookTitle + "progressbar" binding.root.setOnClickListener { - ExoPlayerMasterObject.loadAudiobook(item) - //ExoPlayerMasterObject.audiobook = item + ExoPlayerMasterObject.loadAudiobook(item) //Todo - Switch this to a callback at some point //https://medium.com/@serbelga/shared-elements-in-android-navigation-architecture-component-bc5e7922ecdf val extras = FragmentNavigatorExtras( binding.coverImage to "player_view_cover_image", binding.gridItemTitle to "player_view_title", binding.gridItemAuthor to "player_view_author", - binding.libraryItemCardView to "player_view_card", + //binding.libraryItemCardView to "player_view_card", binding.libraryItemProgressbar to "progressbar" ) @@ -128,24 +86,11 @@ class LibraryItemViewHolder private constructor(val binding: LibraryItemBinding) companion object { fun from(parent: ViewGroup): LibraryItemViewHolder { - val layoutInflater = LayoutInflater.from(parent.context) -// val view = layoutInflater.inflate( -// R.layout.text_item_view, parent, false -// ) as TextView + val layoutInflater = from(parent.context) val binding = LibraryItemBinding.inflate(layoutInflater, parent, false) - //val binding = TextItemViewBinding.inflate(layoutInflater, parent, false) - return LibraryItemViewHolder(binding) - - //return TextItemViewHolder(view) - } - } - - interface LibraryItemListener{ - fun LibraryItemClick(position: Int){ - } } } diff --git a/app/src/main/res/layout/audiobook_player_fragment.xml b/app/src/main/res/layout/audiobook_player_fragment.xml index dc7f7df..95a7f40 100644 --- a/app/src/main/res/layout/audiobook_player_fragment.xml +++ b/app/src/main/res/layout/audiobook_player_fragment.xml @@ -95,8 +95,13 @@ android:textSize="14sp" android:transitionName="player_view_author" app:layout_constraintBottom_toTopOf="@id/exoplayer" - android:text="TRACK 01" + android:visibility="visible" tools:text="Track 01" /> + + + + + + tools:text="8 hrs 25 min remaining" /> - - - + android:id="@+id/action_audiobookPlayerFragment_to_trackListBottomSheetDialog" + app:destination="@id/trackListBottomSheetDialog" /> + \ No newline at end of file