mirror of
https://github.com/nextcloud/android.git
synced 2024-12-18 06:51:55 +03:00
setup media player service with session
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
This commit is contained in:
parent
a2eba07608
commit
47479d5745
13 changed files with 316 additions and 201 deletions
|
@ -333,6 +333,7 @@ dependencies {
|
|||
implementation 'org.conscrypt:conscrypt-android:2.5.3'
|
||||
|
||||
implementation "androidx.media3:media3-ui:$androidxMediaVersion"
|
||||
implementation "androidx.media3:media3-session:$androidxMediaVersion"
|
||||
implementation "androidx.media3:media3-exoplayer:$androidxMediaVersion"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:$androidxMediaVersion"
|
||||
|
||||
|
|
|
@ -127,7 +127,14 @@
|
|||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:replace="android:allowBackup">
|
||||
|
||||
<service
|
||||
android:name="com.nextcloud.client.media.BackgroundPlayerService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_config" />
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ import com.nextcloud.client.integrations.IntegrationsModule;
|
|||
import com.nextcloud.client.jobs.JobsModule;
|
||||
import com.nextcloud.client.jobs.download.FileDownloadHelper;
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper;
|
||||
import com.nextcloud.client.media.BackgroundPlayerService;
|
||||
import com.nextcloud.client.media.CustomExoPlayer;
|
||||
import com.nextcloud.client.network.NetworkModule;
|
||||
import com.nextcloud.client.onboarding.OnboardingModule;
|
||||
import com.nextcloud.client.preferences.PreferencesModule;
|
||||
|
@ -46,7 +48,7 @@ import dagger.android.support.AndroidSupportInjectionModule;
|
|||
ThemeModule.class,
|
||||
DatabaseModule.class,
|
||||
DispatcherModule.class,
|
||||
VariantModule.class
|
||||
VariantModule.class,
|
||||
})
|
||||
@Singleton
|
||||
public interface AppComponent {
|
||||
|
@ -54,6 +56,8 @@ public interface AppComponent {
|
|||
void inject(MainApp app);
|
||||
|
||||
void inject(MediaControlView mediaControlView);
|
||||
void inject(CustomExoPlayer customExoPlayer);
|
||||
void inject(BackgroundPlayerService backgroundPlayerService);
|
||||
|
||||
void inject(ThemeableSwitchPreference switchPreference);
|
||||
|
||||
|
|
|
@ -17,6 +17,9 @@ import com.nextcloud.client.jobs.transfer.FileTransferService;
|
|||
import com.nextcloud.client.jobs.upload.FileUploadHelper;
|
||||
import com.nextcloud.client.logger.ui.LogsActivity;
|
||||
import com.nextcloud.client.logger.ui.LogsViewModel;
|
||||
import com.nextcloud.client.media.BackgroundPlayerService;
|
||||
import com.nextcloud.client.media.CustomExoPlayer;
|
||||
import com.nextcloud.client.media.NextcloudExoPlayer;
|
||||
import com.nextcloud.client.media.PlayerService;
|
||||
import com.nextcloud.client.migrations.Migrations;
|
||||
import com.nextcloud.client.onboarding.FirstRunActivity;
|
||||
|
@ -481,7 +484,12 @@ abstract class ComponentsModule {
|
|||
|
||||
@ContributesAndroidInjector
|
||||
abstract TestJob testJob();
|
||||
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract InternalTwoWaySyncActivity internalTwoWaySyncActivity();
|
||||
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract BackgroundPlayerService backgroundPlayerService();
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.media
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
|
||||
import com.nextcloud.client.network.ClientFactory
|
||||
import com.nextcloud.common.NextcloudClient
|
||||
import com.owncloud.android.MainApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class BackgroundPlayerService : MediaSessionService(), Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var clientFactory: ClientFactory
|
||||
|
||||
@Inject
|
||||
lateinit var userAccountManager: UserAccountManager
|
||||
lateinit var exoPlayer: ExoPlayer
|
||||
private var mediaSession: MediaSession? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
MainApp.getAppComponent().inject(this)
|
||||
initNextcloudExoPlayer()
|
||||
}
|
||||
|
||||
private fun initNextcloudExoPlayer() {
|
||||
runBlocking {
|
||||
var nextcloudClient: NextcloudClient
|
||||
withContext(Dispatchers.IO) {
|
||||
nextcloudClient = clientFactory.createNextcloudClient(userAccountManager.user)
|
||||
}
|
||||
nextcloudClient?.let {
|
||||
exoPlayer = createNextcloudExoplayer(this@BackgroundPlayerService, nextcloudClient)
|
||||
println(exoPlayer)
|
||||
mediaSession =
|
||||
MediaSession.Builder(applicationContext, exoPlayer).setCallback(object : MediaSession.Callback {
|
||||
override fun onDisconnected(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
||||
stopSelf()
|
||||
}
|
||||
}).build()
|
||||
}
|
||||
println("created client $nextcloudClient")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
val player = mediaSession?.player
|
||||
if (player!!.playWhenReady) {
|
||||
// Make sure the service is not in foreground.
|
||||
player.pause()
|
||||
}
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaSession?.run {
|
||||
player.release()
|
||||
release()
|
||||
mediaSession = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onGetSession(p0: MediaSession.ControllerInfo): MediaSession? {
|
||||
return mediaSession
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.media
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.account.UserAccountManagerImpl
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.network.ClientFactory
|
||||
import com.owncloud.android.MainApp
|
||||
import javax.inject.Inject
|
||||
|
||||
class CustomExoPlayer {
|
||||
|
||||
}
|
|
@ -15,8 +15,11 @@ import androidx.media3.datasource.DefaultDataSource
|
|||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.network.ClientFactory
|
||||
import com.nextcloud.common.NextcloudClient
|
||||
import com.owncloud.android.MainApp
|
||||
import javax.inject.Inject
|
||||
|
||||
object NextcloudExoPlayer {
|
||||
private const val FIVE_SECONDS_IN_MILLIS = 5000L
|
||||
|
@ -45,4 +48,5 @@ object NextcloudExoPlayer {
|
|||
.setSeekForwardIncrementMs(FIVE_SECONDS_IN_MILLIS)
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,10 +23,10 @@ import android.view.View
|
|||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.MediaController.MediaPlayerControl
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.Player
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.MediaControlBinding
|
||||
|
@ -50,7 +50,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
View.OnClickListener,
|
||||
OnSeekBarChangeListener {
|
||||
|
||||
private var playerControl: MediaPlayerControl? = null
|
||||
private var playerControl: Player? = null
|
||||
private var binding: MediaControlBinding
|
||||
private var isDragging = false
|
||||
|
||||
|
@ -62,7 +62,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun setMediaPlayer(player: MediaPlayerControl?) {
|
||||
fun setMediaPlayer(player: Player?) {
|
||||
playerControl = player
|
||||
handler.sendEmptyMessage(SHOW_PROGRESS)
|
||||
|
||||
|
@ -104,14 +104,16 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
*/
|
||||
private fun disableUnsupportedButtons() {
|
||||
try {
|
||||
if (playerControl?.canPause() == false) {
|
||||
binding.playBtn.setEnabled(false)
|
||||
//TODO: should we check for nullability && see if we need try catch block
|
||||
if (playerControl!!.isCommandAvailable(Player.COMMAND_PLAY_PAUSE).not()) {
|
||||
binding.playBtn.isEnabled = false
|
||||
}
|
||||
if (playerControl?.canSeekBackward() == false) {
|
||||
binding.rewindBtn.setEnabled(false)
|
||||
|
||||
if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_BACK).not()) {
|
||||
binding.rewindBtn.isEnabled = false
|
||||
}
|
||||
if (playerControl?.canSeekForward() == false) {
|
||||
binding.forwardBtn.setEnabled(false)
|
||||
if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_FORWARD).not()) {
|
||||
binding.forwardBtn.isEnabled = false
|
||||
}
|
||||
} catch (ex: IncompatibleClassChangeError) {
|
||||
// We were given an old version of the interface, that doesn't have
|
||||
|
@ -149,7 +151,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun formatTime(timeMs: Int): String {
|
||||
private fun formatTime(timeMs: Long): String {
|
||||
val totalSeconds = timeMs / 1000
|
||||
val seconds = totalSeconds % 60
|
||||
val minutes = totalSeconds / 60 % 60
|
||||
|
@ -164,8 +166,8 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun setProgress(): Int {
|
||||
var position = 0
|
||||
private fun setProgress(): Long {
|
||||
var position = 0L
|
||||
if (playerControl == null || isDragging) {
|
||||
position = 0
|
||||
}
|
||||
|
@ -178,7 +180,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
val pos = 1000L * position / duration
|
||||
binding.progressBar.progress = pos.toInt()
|
||||
}
|
||||
val percent = playerControl.bufferPercentage
|
||||
val percent = playerControl.bufferedPercentage
|
||||
binding.progressBar.setSecondaryProgress(percent * 10)
|
||||
val endTime = if (duration > 0) formatTime(duration) else "--:--"
|
||||
binding.totalTimeText.text = endTime
|
||||
|
@ -202,22 +204,25 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> {
|
||||
if (uniqueDown && playerControl?.isPlaying == false) {
|
||||
playerControl?.start()
|
||||
if (uniqueDown && playerControl?.playWhenReady == false) {
|
||||
playerControl?.play()
|
||||
updatePausePlay()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_STOP,
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE
|
||||
-> {
|
||||
if (uniqueDown && playerControl?.isPlaying == true) {
|
||||
if (uniqueDown && playerControl?.playWhenReady == true) {
|
||||
playerControl?.pause()
|
||||
updatePausePlay()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return super.dispatchKeyEvent(event)
|
||||
}
|
||||
}
|
||||
|
@ -225,18 +230,21 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
fun updatePausePlay() {
|
||||
binding.playBtn.icon = ContextCompat.getDrawable(
|
||||
context,
|
||||
// isPlaying reflects if the playback is actually moving forward, If the media is buffering and it will play when ready
|
||||
// it would still return that it is not playing. So, in case of buffering it will show the pause icon which would show that
|
||||
// media is loading, when user has not paused but moved the progress to a different position this works as a buffering signal.
|
||||
if (playerControl?.isPlaying == true) {
|
||||
R.drawable.ic_pause
|
||||
} else {
|
||||
R.drawable.ic_play
|
||||
}
|
||||
)
|
||||
binding.forwardBtn.visibility = if (playerControl?.canSeekForward() == true) {
|
||||
binding.forwardBtn.visibility = if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_FORWARD)) {
|
||||
VISIBLE
|
||||
} else {
|
||||
INVISIBLE
|
||||
}
|
||||
binding.rewindBtn.visibility = if (playerControl?.canSeekBackward() == true) {
|
||||
binding.rewindBtn.visibility = if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_BACK)) {
|
||||
VISIBLE
|
||||
} else {
|
||||
INVISIBLE
|
||||
|
@ -245,10 +253,10 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
|
||||
private fun doPauseResume() {
|
||||
playerControl?.run {
|
||||
if (isPlaying) {
|
||||
if (playWhenReady) {
|
||||
pause()
|
||||
} else {
|
||||
start()
|
||||
play()
|
||||
}
|
||||
}
|
||||
updatePausePlay()
|
||||
|
@ -267,16 +275,17 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
|
||||
@Suppress("MagicNumber")
|
||||
override fun onClick(v: View) {
|
||||
var pos: Int
|
||||
var pos: Long
|
||||
|
||||
playerControl?.let { playerControl ->
|
||||
val playing = playerControl.isPlaying
|
||||
val playing = playerControl.playWhenReady
|
||||
val id = v.id
|
||||
|
||||
when (id) {
|
||||
R.id.playBtn -> {
|
||||
doPauseResume()
|
||||
}
|
||||
|
||||
R.id.rewindBtn -> {
|
||||
pos = playerControl.currentPosition
|
||||
pos -= 5000
|
||||
|
@ -286,6 +295,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
}
|
||||
setProgress()
|
||||
}
|
||||
|
||||
R.id.forwardBtn -> {
|
||||
pos = playerControl.currentPosition
|
||||
pos += 15000
|
||||
|
@ -315,8 +325,8 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
playerControl?.let { playerControl ->
|
||||
val duration = playerControl.duration.toLong()
|
||||
val newPosition = duration * progress / 1000L
|
||||
playerControl.seekTo(newPosition.toInt())
|
||||
binding.currentTimeText.text = formatTime(newPosition.toInt())
|
||||
playerControl.seekTo(newPosition)
|
||||
binding.currentTimeText.text = formatTime(newPosition)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1832,6 +1832,7 @@ public class FileDisplayActivity extends FileActivity
|
|||
((PreviewMediaFragment) fileFragment).updateFile(renamedFile);
|
||||
if (PreviewMediaFragment.canBePreviewed(renamedFile)) {
|
||||
long position = ((PreviewMediaFragment) fileFragment).getPosition();
|
||||
System.out.println("Start Fragment Media Preview");
|
||||
startMediaPreview(renamedFile, position, true, true, true, false);
|
||||
} else {
|
||||
getFileOperationsHelper().openFile(renamedFile);
|
||||
|
|
|
@ -13,15 +13,11 @@
|
|||
package com.owncloud.android.ui.preview
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
|
@ -45,20 +41,24 @@ import androidx.core.view.WindowInsetsControllerCompat
|
|||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.di.Injectable
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager
|
||||
import com.nextcloud.client.jobs.download.FileDownloadHelper
|
||||
import com.nextcloud.client.media.BackgroundPlayerService
|
||||
import com.nextcloud.client.media.ExoplayerListener
|
||||
import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
|
||||
import com.nextcloud.client.media.PlayerService
|
||||
import com.nextcloud.client.media.PlayerServiceConnection
|
||||
import com.nextcloud.client.network.ClientFactory
|
||||
import com.nextcloud.client.network.ClientFactory.CreationException
|
||||
|
@ -71,7 +71,6 @@ import com.nextcloud.utils.extensions.statusBarHeight
|
|||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.ActivityPreviewMediaBinding
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.datamodel.ThumbnailsCacheManager
|
||||
import com.owncloud.android.files.StreamMediaFileOperation
|
||||
import com.owncloud.android.lib.common.OwnCloudClient
|
||||
import com.owncloud.android.lib.common.operations.OnRemoteOperationListener
|
||||
|
@ -88,7 +87,6 @@ import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment
|
|||
import com.owncloud.android.ui.dialog.SendShareDialog
|
||||
import com.owncloud.android.ui.fragment.FileFragment
|
||||
import com.owncloud.android.ui.fragment.OCFileListFragment
|
||||
import com.owncloud.android.utils.BitmapUtils
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.ErrorMessageAdapter
|
||||
import com.owncloud.android.utils.MimeTypeUtil
|
||||
|
@ -119,7 +117,7 @@ class PreviewMediaActivity :
|
|||
private var autoplay = true
|
||||
private val prepared = false
|
||||
private var mediaPlayerServiceConnection: PlayerServiceConnection? = null
|
||||
private var videoUri: Uri? = null
|
||||
private var streamUri: Uri? = null
|
||||
|
||||
@Inject
|
||||
lateinit var clientFactory: ClientFactory
|
||||
|
@ -132,13 +130,15 @@ class PreviewMediaActivity :
|
|||
|
||||
private lateinit var binding: ActivityPreviewMediaBinding
|
||||
private var emptyListView: ViewGroup? = null
|
||||
private var exoPlayer: ExoPlayer? = null
|
||||
private var videoPlayer: ExoPlayer? = null
|
||||
private var audioMediaController: MediaController? = null
|
||||
private var nextcloudClient: NextcloudClient? = null
|
||||
private lateinit var windowInsetsController: WindowInsetsControllerCompat
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) {
|
||||
setTheme(R.style.Theme_ownCloud_Toolbar)
|
||||
}
|
||||
|
@ -156,6 +156,13 @@ class PreviewMediaActivity :
|
|||
emptyListView = binding.emptyView.emptyListView
|
||||
showProgressLayout()
|
||||
addMarginForEmptyView()
|
||||
if (file == null) {
|
||||
return
|
||||
}
|
||||
if (MimeTypeUtil.isAudio(file)) {
|
||||
setGenericThumbnail()
|
||||
initializeAudioPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMarginForEmptyView() {
|
||||
|
@ -173,25 +180,6 @@ class PreviewMediaActivity :
|
|||
emptyListView?.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
private fun registerMediaControlReceiver() {
|
||||
val filter = IntentFilter(MEDIA_CONTROL_READY_RECEIVER)
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mediaControlReceiver, filter)
|
||||
}
|
||||
|
||||
private val mediaControlReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
intent.getBooleanExtra(PlayerService.IS_MEDIA_CONTROL_LAYOUT_READY, false).run {
|
||||
if (this) {
|
||||
hideProgressLayout()
|
||||
mediaPlayerServiceConnection?.bind()
|
||||
setupAudioPlayerServiceConnection()
|
||||
} else {
|
||||
showProgressLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initArguments(savedInstanceState: Bundle?) {
|
||||
intent?.let {
|
||||
initWithIntent(it)
|
||||
|
@ -203,20 +191,6 @@ class PreviewMediaActivity :
|
|||
} else {
|
||||
initWithBundle(savedInstanceState)
|
||||
}
|
||||
|
||||
if (MimeTypeUtil.isAudio(file)) {
|
||||
preparePreviewForAudioFile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun preparePreviewForAudioFile() {
|
||||
registerMediaControlReceiver()
|
||||
|
||||
if (file.isDown) {
|
||||
return
|
||||
}
|
||||
|
||||
requestForDownload(file)
|
||||
}
|
||||
|
||||
private fun initWithIntent(intent: Intent) {
|
||||
|
@ -245,8 +219,6 @@ class PreviewMediaActivity :
|
|||
|
||||
if (isFileVideo) {
|
||||
binding.root.setBackgroundColor(resources.getColor(R.color.black, null))
|
||||
} else {
|
||||
extractAndSetCoverArt(file)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -265,13 +237,13 @@ class PreviewMediaActivity :
|
|||
|
||||
private fun showProgressLayout() {
|
||||
binding.progress.visibility = View.VISIBLE
|
||||
binding.mediaController.visibility = View.GONE
|
||||
binding.audioControllerView.visibility = View.GONE
|
||||
binding.emptyView.emptyListView.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun hideProgressLayout() {
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.mediaController.visibility = View.VISIBLE
|
||||
binding.audioControllerView.visibility = View.VISIBLE
|
||||
binding.emptyView.emptyListView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
|
@ -287,48 +259,6 @@ class PreviewMediaActivity :
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tries to read the cover art from the audio file and sets it as cover art.
|
||||
*
|
||||
* @param file audio file with potential cover art
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught", "NestedBlockDepth")
|
||||
private fun extractAndSetCoverArt(file: OCFile) {
|
||||
if (!MimeTypeUtil.isAudio(file)) {
|
||||
return
|
||||
}
|
||||
|
||||
val bitmap = if (file.storagePath == null) {
|
||||
getAudioThumbnail(file)
|
||||
} else {
|
||||
getThumbnail(file.storagePath) ?: getAudioThumbnail(file)
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
binding.imagePreview.setImageBitmap(bitmap)
|
||||
} else {
|
||||
setGenericThumbnail()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun getThumbnail(storagePath: String?): Bitmap? {
|
||||
return try {
|
||||
MediaMetadataRetriever().run {
|
||||
setDataSource(storagePath)
|
||||
BitmapFactory.decodeByteArray(embeddedPicture, 0, embeddedPicture?.size ?: 0)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
BitmapUtils.drawableToBitmap(genericThumbnail())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAudioThumbnail(file: OCFile): Bitmap? {
|
||||
return ThumbnailsCacheManager.getBitmapFromDiskCache(
|
||||
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId
|
||||
)
|
||||
}
|
||||
|
||||
private fun setGenericThumbnail() {
|
||||
binding.imagePreview.setImageDrawable(genericThumbnail())
|
||||
}
|
||||
|
@ -356,8 +286,8 @@ class PreviewMediaActivity :
|
|||
|
||||
private fun saveMediaInstanceState(bundle: Bundle) {
|
||||
bundle.run {
|
||||
if (MimeTypeUtil.isVideo(file) && exoPlayer != null) {
|
||||
exoPlayer?.let {
|
||||
if (MimeTypeUtil.isVideo(file) && audioMediaController != null) {
|
||||
audioMediaController?.let {
|
||||
savedPlaybackPosition = it.currentPosition
|
||||
autoplay = it.isPlaying
|
||||
}
|
||||
|
@ -375,42 +305,13 @@ class PreviewMediaActivity :
|
|||
|
||||
Log_OC.v(TAG, "onStart")
|
||||
|
||||
if (file == null) {
|
||||
return
|
||||
}
|
||||
|
||||
mediaPlayerServiceConnection?.bind()
|
||||
|
||||
if (MimeTypeUtil.isAudio(file)) {
|
||||
setupAudioPlayerServiceConnection()
|
||||
} else if (MimeTypeUtil.isVideo(file)) {
|
||||
if (mediaPlayerServiceConnection?.isConnected == true) {
|
||||
stopAudio()
|
||||
}
|
||||
|
||||
if (exoPlayer != null) {
|
||||
playVideo()
|
||||
} else {
|
||||
initNextcloudExoPlayer()
|
||||
}
|
||||
if (MimeTypeUtil.isVideo(file)) {
|
||||
//TODO: should we somehow release any previous audio sessions?
|
||||
initializeVideoPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAudioPlayerServiceConnection() {
|
||||
binding.mediaController.run {
|
||||
setMediaPlayer(mediaPlayerServiceConnection)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
user?.let {
|
||||
mediaPlayerServiceConnection?.start(it, file, autoplay, savedPlaybackPosition)
|
||||
}
|
||||
|
||||
binding.emptyView.emptyListView.visibility = View.GONE
|
||||
binding.progress.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun initNextcloudExoPlayer() {
|
||||
private fun initializeVideoPlayer() {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
Executors.newSingleThreadExecutor().execute {
|
||||
try {
|
||||
|
@ -418,9 +319,10 @@ class PreviewMediaActivity :
|
|||
|
||||
nextcloudClient?.let { client ->
|
||||
handler.post {
|
||||
exoPlayer = createNextcloudExoplayer(this, client)
|
||||
videoPlayer = createNextcloudExoplayer(this, client)
|
||||
|
||||
exoPlayer?.let { player ->
|
||||
|
||||
videoPlayer?.let { player ->
|
||||
player.addListener(
|
||||
ExoplayerListener(
|
||||
this,
|
||||
|
@ -439,6 +341,72 @@ class PreviewMediaActivity :
|
|||
}
|
||||
}
|
||||
|
||||
private fun releaseVideoPlayer() {
|
||||
videoPlayer?.let {
|
||||
savedPlaybackPosition = it.currentPosition
|
||||
autoplay = it.playWhenReady
|
||||
it.release()
|
||||
}
|
||||
videoPlayer = null
|
||||
}
|
||||
|
||||
private fun initializeAudioPlayer() {
|
||||
val sessionToken = SessionToken(this, ComponentName(this, BackgroundPlayerService::class.java))
|
||||
val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
|
||||
controllerFuture.addListener(
|
||||
{
|
||||
try {
|
||||
audioMediaController = controllerFuture.get()
|
||||
playAudio()
|
||||
binding.audioControllerView.setMediaPlayer(audioMediaController)
|
||||
} catch (e: Exception) {
|
||||
println("exception raised while getting the media controller ${e.message}")
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor()
|
||||
)
|
||||
}
|
||||
|
||||
private fun playAudio() {
|
||||
if (file.isDown) {
|
||||
prepareAudioPlayer(file.storageUri)
|
||||
} else {
|
||||
try {
|
||||
LoadStreamUrl(this, user, clientFactory).execute(file.localId)
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "Loading stream url not possible: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareAudioPlayer(uri: Uri) {
|
||||
hideProgressLayout()
|
||||
audioMediaController?.let { audioPlayer ->
|
||||
audioPlayer.addListener(object : Player.Listener {
|
||||
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
|
||||
val artworkBitmap = mediaMetadata.artworkData?.let { bytes: ByteArray ->
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
}
|
||||
if (artworkBitmap != null) {
|
||||
binding.imagePreview.setImageBitmap(artworkBitmap)
|
||||
}
|
||||
}
|
||||
})
|
||||
audioPlayer.setMediaItem(MediaItem.fromUri(uri))
|
||||
audioPlayer.playWhenReady = autoplay
|
||||
audioPlayer.seekTo(savedPlaybackPosition)
|
||||
audioPlayer.prepare()
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseAudioPlayer() {
|
||||
audioMediaController?.let { audioPlayer ->
|
||||
audioPlayer.stop()
|
||||
audioPlayer.release()
|
||||
}
|
||||
audioMediaController = null
|
||||
}
|
||||
|
||||
private fun initWindowInsetsController() {
|
||||
windowInsetsController = WindowCompat.getInsetsController(
|
||||
window,
|
||||
|
@ -495,7 +463,7 @@ class PreviewMediaActivity :
|
|||
}
|
||||
}
|
||||
)
|
||||
it.player = exoPlayer
|
||||
it.player = videoPlayer
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -567,7 +535,7 @@ class PreviewMediaActivity :
|
|||
}
|
||||
|
||||
R.id.action_remove_file -> {
|
||||
exoPlayer?.stop()
|
||||
videoPlayer?.pause()
|
||||
val dialog = RemoveFilesDialogFragment.newInstance(file)
|
||||
dialog.show(supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION)
|
||||
}
|
||||
|
@ -614,6 +582,7 @@ class PreviewMediaActivity :
|
|||
val removedFile = operation.file
|
||||
val fileAvailable: Boolean = storageManager.fileExists(removedFile.fileId)
|
||||
if (!fileAvailable && removedFile == file) {
|
||||
releaseAudioPlayer()
|
||||
finish()
|
||||
}
|
||||
} else if (operation is SynchronizeFileOperation) {
|
||||
|
@ -670,7 +639,7 @@ class PreviewMediaActivity :
|
|||
setupVideoView()
|
||||
|
||||
if (file.isDown) {
|
||||
playVideoUri(file.storageUri)
|
||||
prepareVideoPlayer(file.storageUri)
|
||||
} else {
|
||||
try {
|
||||
LoadStreamUrl(this, user, clientFactory).execute(file.localId)
|
||||
|
@ -680,20 +649,15 @@ class PreviewMediaActivity :
|
|||
}
|
||||
}
|
||||
|
||||
private fun playVideoUri(uri: Uri) {
|
||||
private fun prepareVideoPlayer(uri: Uri) {
|
||||
binding.progress.visibility = View.GONE
|
||||
|
||||
exoPlayer?.run {
|
||||
setMediaItem(MediaItem.fromUri(uri))
|
||||
val videoMediaItem = MediaItem.fromUri(uri)
|
||||
videoPlayer?.run {
|
||||
setMediaItem(videoMediaItem)
|
||||
playWhenReady = autoplay
|
||||
seekTo(savedPlaybackPosition)
|
||||
prepare()
|
||||
|
||||
if (savedPlaybackPosition >= 0) {
|
||||
seekTo(savedPlaybackPosition)
|
||||
}
|
||||
}
|
||||
|
||||
autoplay = false
|
||||
}
|
||||
|
||||
private class LoadStreamUrl(
|
||||
|
@ -728,8 +692,12 @@ class PreviewMediaActivity :
|
|||
val weakReference = previewMediaActivityWeakReference.get()
|
||||
weakReference?.apply {
|
||||
if (uri != null) {
|
||||
videoUri = uri
|
||||
playVideoUri(uri)
|
||||
streamUri = uri
|
||||
if (MimeTypeUtil.isVideo(file)) {
|
||||
prepareVideoPlayer(uri)
|
||||
} else if (MimeTypeUtil.isAudio(file)) {
|
||||
prepareAudioPlayer(uri)
|
||||
}
|
||||
} else {
|
||||
emptyListView?.visibility = View.VISIBLE
|
||||
setVideoErrorMessage(
|
||||
|
@ -754,29 +722,15 @@ class PreviewMediaActivity :
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log_OC.v(TAG, "onDestroy")
|
||||
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mediaControlReceiver)
|
||||
|
||||
super.onDestroy()
|
||||
exoPlayer?.run {
|
||||
stop()
|
||||
release()
|
||||
}
|
||||
|
||||
Log_OC.v(TAG, "onDestroy")
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Log_OC.v(TAG, "onStop")
|
||||
|
||||
file?.let {
|
||||
if (MimeTypeUtil.isVideo(it) && exoPlayer != null && exoPlayer?.isPlaying == true) {
|
||||
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
|
||||
}
|
||||
}
|
||||
|
||||
exoPlayer?.pause()
|
||||
stopAudio()
|
||||
mediaPlayerServiceConnection?.unbind()
|
||||
releaseVideoPlayer()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
|
@ -829,23 +783,15 @@ class PreviewMediaActivity :
|
|||
}
|
||||
|
||||
private fun stopPreview(stopAudio: Boolean) {
|
||||
//TODO: stop removes the media item attached but not release the player
|
||||
// do we want to keep this behaviour or release the player too just like in onStop?
|
||||
if (MimeTypeUtil.isAudio(file) && stopAudio) {
|
||||
mediaPlayerServiceConnection?.pause()
|
||||
audioMediaController?.pause()
|
||||
} else if (MimeTypeUtil.isVideo(file)) {
|
||||
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
|
||||
exoPlayer?.stop()
|
||||
releaseVideoPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
val position: Long
|
||||
get() {
|
||||
if (prepared) {
|
||||
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
|
||||
}
|
||||
Log_OC.v(TAG, "getting position: $savedPlaybackPosition")
|
||||
return savedPlaybackPosition
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = PreviewMediaActivity::class.java.simpleName
|
||||
|
||||
|
|
|
@ -330,7 +330,7 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
}
|
||||
|
||||
private fun prepareForAudio() {
|
||||
binding.mediaController.setMediaPlayer(mediaPlayerServiceConnection)
|
||||
binding.mediaController.setMediaPlayer(null)
|
||||
binding.mediaController.visibility = View.VISIBLE
|
||||
mediaPlayerServiceConnection?.start(user!!, file, autoplay, savedPlaybackPosition)
|
||||
binding.emptyView.emptyListView.visibility = View.GONE
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
app:show_buffering="always" />
|
||||
|
||||
<com.owncloud.android.media.MediaControlView
|
||||
android:id="@+id/media_controller"
|
||||
android:id="@+id/audio_controller_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
|
@ -65,7 +65,9 @@
|
|||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
>
|
||||
|
||||
<include
|
||||
android:id="@+id/empty_view"
|
||||
|
|
|
@ -2548,6 +2548,14 @@
|
|||
<sha256 value="3670ba201f837bdce5ffaf4adc766a0d21cfd08db74efed5657513544c054eba" origin="Generated by Gradle" reason="Artifact is not signed"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.media3" name="media3-session" version="1.4.0">
|
||||
<artifact name="media3-session-1.4.0.aar">
|
||||
<sha256 value="a5daaaea8fc9a87ebb4411f1d97bcf887069132068b3af15374205cfd458bb7c" origin="Generated by Gradle" reason="Artifact is not signed"/>
|
||||
</artifact>
|
||||
<artifact name="media3-session-1.4.0.module">
|
||||
<sha256 value="6bccbca5b01eaa3fd0502300f3530ba1f1cdc952927a0a0f3fb1b1ae39860ed6" origin="Generated by Gradle" reason="Artifact is not signed"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.media3" name="media3-ui" version="1.2.0">
|
||||
<artifact name="media3-ui-1.2.0.aar">
|
||||
<sha256 value="fee39edbf615f9432f53af1cc9b20dd5706bfbc5dbd7fe581253a59eedd91482" origin="Generated by Gradle" reason="Artifact is not signed"/>
|
||||
|
@ -9718,6 +9726,11 @@
|
|||
<sha256 value="16e05e9f49621b87c53e69350140f3c46d42d966c67a933bdf4b063a2b1c8fc5" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jacoco" name="org.jacoco.agent" version="0.8.12">
|
||||
<artifact name="org.jacoco.agent-0.8.12.pom">
|
||||
<sha256 value="0f9da994abd9827f957fc1ba7c5bad3fe918f62601c1d743f216b0615efe480e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.jacoco" name="org.jacoco.ant" version="0.8.11">
|
||||
<artifact name="org.jacoco.ant-0.8.11.jar">
|
||||
<sha256 value="81d7eb8890d9be30a939612c295603541063529cdd03a53265aba74474b70b7c" origin="Generated by Gradle"/>
|
||||
|
@ -11041,6 +11054,11 @@
|
|||
<sha256 value="92eee24bc3c843e4881d46c1dd6505471ee3142facfb466b428cfea5a56c6b60" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.ow2.asm" name="asm" version="9.7">
|
||||
<artifact name="asm-9.7.pom">
|
||||
<sha256 value="de00115f1d84f3a0b2ee3a4b6f6192d066f86d185d67b9d1522f2c80feac5f00" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.ow2.asm" name="asm-analysis" version="9.2">
|
||||
<artifact name="asm-analysis-9.2.jar">
|
||||
<sha256 value="878fbe521731c072d14d2d65b983b1beae6ad06fda0007b6a8bae81f73f433c4" origin="Generated by Gradle"/>
|
||||
|
@ -11091,6 +11109,11 @@
|
|||
<sha256 value="a98ae4895334baf8ff86bd66516210dbd9a03f1a6e15e47dda82afcf6b53d77c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.ow2.asm" name="asm-commons" version="9.7">
|
||||
<artifact name="asm-commons-9.7.pom">
|
||||
<sha256 value="5acee3ee7252ed90b8074c755d022787499a95fafff98ac4a685107c4da409b4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.ow2.asm" name="asm-tree" version="9.2">
|
||||
<artifact name="asm-tree-9.2.jar">
|
||||
<sha256 value="aabf9bd23091a4ebfc109c1f3ee7cf3e4b89f6ba2d3f51c5243f16b3cffae011" origin="Generated by Gradle"/>
|
||||
|
@ -11115,6 +11138,11 @@
|
|||
<sha256 value="1bcb481d7fc16b955bb60ca07c8cfa2424bcee78bdc405bba31c7d6f5dc2d113" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.ow2.asm" name="asm-tree" version="9.7">
|
||||
<artifact name="asm-tree-9.7.pom">
|
||||
<sha256 value="a34ea1e3e4128c01038db43c6976e88c779cf5af84b0505da266dfe6965668ec" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.ow2.asm" name="asm-util" version="9.2">
|
||||
<artifact name="asm-util-9.2.jar">
|
||||
<sha256 value="ff5b3cd331ae8a9a804768280da98f50f424fef23dd3c788bb320e08c94ee598" origin="Generated by Gradle"/>
|
||||
|
|
Loading…
Reference in a new issue