mirror of
https://github.com/nextcloud/android.git
synced 2024-11-22 21:25:35 +03:00
Merge pull request #13467 from nextcloud/improve-media
Improve Media Player
This commit is contained in:
commit
233d8a9567
11 changed files with 553 additions and 384 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"
|
||||
|
||||
|
|
|
@ -350,6 +350,16 @@
|
|||
android:exported="false"
|
||||
android:theme="@style/Theme.ownCloud.Media" />
|
||||
|
||||
<service
|
||||
android:name="com.nextcloud.client.media.BackgroundPlayerService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".authentication.AccountAuthenticatorService"
|
||||
android:exported="false">
|
||||
|
|
|
@ -17,6 +17,7 @@ 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.network.NetworkModule;
|
||||
import com.nextcloud.client.onboarding.OnboardingModule;
|
||||
import com.nextcloud.client.preferences.PreferencesModule;
|
||||
|
@ -27,6 +28,8 @@ import com.owncloud.android.ui.whatsnew.ProgressIndicator;
|
|||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Component;
|
||||
import dagger.android.support.AndroidSupportInjectionModule;
|
||||
|
@ -46,7 +49,7 @@ import dagger.android.support.AndroidSupportInjectionModule;
|
|||
ThemeModule.class,
|
||||
DatabaseModule.class,
|
||||
DispatcherModule.class,
|
||||
VariantModule.class
|
||||
VariantModule.class,
|
||||
})
|
||||
@Singleton
|
||||
public interface AppComponent {
|
||||
|
@ -55,6 +58,9 @@ public interface AppComponent {
|
|||
|
||||
void inject(MediaControlView mediaControlView);
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
void inject(BackgroundPlayerService backgroundPlayerService);
|
||||
|
||||
void inject(ThemeableSwitchPreference switchPreference);
|
||||
|
||||
void inject(FileUploadHelper fileUploadHelper);
|
||||
|
|
|
@ -17,6 +17,7 @@ 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.PlayerService;
|
||||
import com.nextcloud.client.migrations.Migrations;
|
||||
import com.nextcloud.client.onboarding.FirstRunActivity;
|
||||
|
@ -123,6 +124,8 @@ import com.owncloud.android.ui.preview.PreviewTextStringFragment;
|
|||
import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment;
|
||||
import com.owncloud.android.ui.trashbin.TrashbinActivity;
|
||||
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import dagger.Module;
|
||||
import dagger.android.ContributesAndroidInjector;
|
||||
|
||||
|
@ -481,7 +484,13 @@ abstract class ComponentsModule {
|
|||
|
||||
@ContributesAndroidInjector
|
||||
abstract TestJob testJob();
|
||||
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract InternalTwoWaySyncActivity internalTwoWaySyncActivity();
|
||||
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
@ContributesAndroidInjector
|
||||
abstract BackgroundPlayerService backgroundPlayerService();
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2024 Parneet Singh <gurayaparneet@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.nextcloud.client.media
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Player.COMMAND_PLAY_PAUSE
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSession.ConnectionResult
|
||||
import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionResult
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
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.nextcloud.utils.extensions.registerBroadcastReceiver
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.datamodel.ReceiverFlag
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class BackgroundPlayerService : MediaSessionService(), Injectable {
|
||||
|
||||
private val seekBackSessionCommand = SessionCommand(SESSION_COMMAND_ACTION_SEEK_BACK, Bundle.EMPTY)
|
||||
private val seekForwardSessionCommand = SessionCommand(SESSION_COMMAND_ACTION_SEEK_FORWARD, Bundle.EMPTY)
|
||||
|
||||
val seekForward =
|
||||
CommandButton.Builder()
|
||||
.setDisplayName("Seek Forward")
|
||||
.setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_FORWARD_15))
|
||||
.setSessionCommand(seekForwardSessionCommand)
|
||||
.setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 2) })
|
||||
.build()
|
||||
|
||||
val seekBackward =
|
||||
CommandButton.Builder()
|
||||
.setDisplayName("Seek Backward")
|
||||
.setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_BACK_5))
|
||||
.setSessionCommand(seekBackSessionCommand)
|
||||
.setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 0) })
|
||||
.build()
|
||||
|
||||
@Inject
|
||||
lateinit var clientFactory: ClientFactory
|
||||
|
||||
@Inject
|
||||
lateinit var userAccountManager: UserAccountManager
|
||||
lateinit var exoPlayer: ExoPlayer
|
||||
private var mediaSession: MediaSession? = null
|
||||
|
||||
private val stopReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
RELEASE_MEDIA_SESSION_BROADCAST_ACTION -> release()
|
||||
STOP_MEDIA_SESSION_BROADCAST_ACTION -> exoPlayer.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
registerBroadcastReceiver(
|
||||
stopReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(RELEASE_MEDIA_SESSION_BROADCAST_ACTION)
|
||||
addAction(STOP_MEDIA_SESSION_BROADCAST_ACTION)
|
||||
},
|
||||
ReceiverFlag.NotExported
|
||||
)
|
||||
|
||||
MainApp.getAppComponent().inject(this)
|
||||
initNextcloudExoPlayer()
|
||||
|
||||
setMediaNotificationProvider(object : DefaultMediaNotificationProvider(this) {
|
||||
override fun getMediaButtons(
|
||||
session: MediaSession,
|
||||
playerCommands: Player.Commands,
|
||||
customLayout: ImmutableList<CommandButton>,
|
||||
showPauseButton: Boolean
|
||||
): ImmutableList<CommandButton> {
|
||||
val playPauseButton =
|
||||
CommandButton.Builder()
|
||||
.setDisplayName("PlayPause")
|
||||
.setIconResId(
|
||||
CommandButton.getIconResIdForIconConstant(
|
||||
if (mediaSession?.player?.isPlaying == true) {
|
||||
CommandButton.ICON_PAUSE
|
||||
} else {
|
||||
CommandButton.ICON_PLAY
|
||||
}
|
||||
)
|
||||
)
|
||||
.setPlayerCommand(COMMAND_PLAY_PAUSE)
|
||||
.setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 1) })
|
||||
.build()
|
||||
|
||||
val myCustomButtonsLayout =
|
||||
ImmutableList.of(seekBackward, playPauseButton, seekForward)
|
||||
return myCustomButtonsLayout
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initNextcloudExoPlayer() {
|
||||
runBlocking {
|
||||
var nextcloudClient: NextcloudClient
|
||||
withContext(Dispatchers.IO) {
|
||||
nextcloudClient = clientFactory.createNextcloudClient(userAccountManager.user)
|
||||
}
|
||||
nextcloudClient.let {
|
||||
exoPlayer = createNextcloudExoplayer(this@BackgroundPlayerService, nextcloudClient)
|
||||
mediaSession =
|
||||
MediaSession.Builder(applicationContext, exoPlayer)
|
||||
// set id to distinct this session to avoid crash
|
||||
// in case session release delayed a bit and
|
||||
// we start another session for eg. video
|
||||
.setId(BACKGROUND_MEDIA_SESSION_ID)
|
||||
.setCustomLayout(listOf(seekBackward, seekForward))
|
||||
.setCallback(object : MediaSession.Callback {
|
||||
override fun onConnect(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo
|
||||
): ConnectionResult {
|
||||
return AcceptedResultBuilder(mediaSession!!)
|
||||
.setAvailablePlayerCommands(
|
||||
ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
|
||||
.remove(COMMAND_SEEK_TO_NEXT)
|
||||
.remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
|
||||
.remove(COMMAND_SEEK_TO_PREVIOUS)
|
||||
.remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
|
||||
.build()
|
||||
)
|
||||
.setAvailableSessionCommands(
|
||||
ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
||||
.addSessionCommands(
|
||||
listOf(seekBackSessionCommand, seekForwardSessionCommand)
|
||||
).build()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
||||
session.setCustomLayout(listOf(seekBackward, seekForward))
|
||||
}
|
||||
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
return when (customCommand.customAction) {
|
||||
SESSION_COMMAND_ACTION_SEEK_FORWARD -> {
|
||||
session.player.seekForward()
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
SESSION_COMMAND_ACTION_SEEK_BACK -> {
|
||||
session.player.seekBack()
|
||||
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
else -> super.onCustomCommand(session, controller, customCommand, args)
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
release()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterReceiver(stopReceiver)
|
||||
mediaSession?.run {
|
||||
player.release()
|
||||
release()
|
||||
mediaSession = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun release() {
|
||||
val player = mediaSession?.player
|
||||
if (player?.playWhenReady == true) {
|
||||
// Make sure the service is not in foreground.
|
||||
player.pause()
|
||||
}
|
||||
// Bug in Android 14, https://github.com/androidx/media/issues/805
|
||||
// that sometimes onTaskRemove() doesn't get called immediately
|
||||
// eventually gets called so the service stops but the notification doesn't clear out.
|
||||
// [WORKAROUND] So, explicitly removing the notification here.
|
||||
// TODO revisit after bug solved!
|
||||
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.cancel(DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onGetSession(p0: MediaSession.ControllerInfo): MediaSession? {
|
||||
return mediaSession
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SESSION_COMMAND_ACTION_SEEK_BACK = "SESSION_COMMAND_ACTION_SEEK_BACK"
|
||||
private const val SESSION_COMMAND_ACTION_SEEK_FORWARD = "SESSION_COMMAND_ACTION_SEEK_FORWARD"
|
||||
|
||||
private const val BACKGROUND_MEDIA_SESSION_ID = "com.nextcloud.client.media.BACKGROUND_MEDIA_SESSION_ID"
|
||||
|
||||
const val RELEASE_MEDIA_SESSION_BROADCAST_ACTION = "com.nextcloud.client.media.RELEASE_MEDIA_SESSION"
|
||||
const val STOP_MEDIA_SESSION_BROADCAST_ACTION = "com.nextcloud.client.media.STOP_MEDIA_SESSION"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
@ -72,10 +72,6 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
}, 100)
|
||||
}
|
||||
|
||||
fun stopMediaPlayerMessages() {
|
||||
handler.removeMessages(SHOW_PROGRESS)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun initControllerView() {
|
||||
binding.playBtn.requestFocus()
|
||||
|
@ -104,14 +100,15 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
*/
|
||||
private fun disableUnsupportedButtons() {
|
||||
try {
|
||||
if (playerControl?.canPause() == false) {
|
||||
binding.playBtn.setEnabled(false)
|
||||
if (playerControl?.isCommandAvailable(Player.COMMAND_PLAY_PAUSE)?.not() == true) {
|
||||
binding.playBtn.isEnabled = false
|
||||
}
|
||||
if (playerControl?.canSeekBackward() == false) {
|
||||
binding.rewindBtn.setEnabled(false)
|
||||
|
||||
if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_BACK)?.not() == true) {
|
||||
binding.rewindBtn.isEnabled = false
|
||||
}
|
||||
if (playerControl?.canSeekForward() == false) {
|
||||
binding.forwardBtn.setEnabled(false)
|
||||
if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_FORWARD)?.not() == true) {
|
||||
binding.forwardBtn.isEnabled = false
|
||||
}
|
||||
} catch (ex: IncompatibleClassChangeError) {
|
||||
// We were given an old version of the interface, that doesn't have
|
||||
|
@ -130,7 +127,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
val pos = setProgress()
|
||||
|
||||
if (!isDragging) {
|
||||
sendMessageDelayed(obtainMessage(SHOW_PROGRESS), (1000 - pos % 1000).toLong())
|
||||
sendMessageDelayed(obtainMessage(SHOW_PROGRESS), (1000 - pos % 1000))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +146,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 +161,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 +175,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 +199,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 +225,21 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
fun updatePausePlay() {
|
||||
binding.playBtn.icon = ContextCompat.getDrawable(
|
||||
context,
|
||||
// use isPlaying instead of playWhenReady
|
||||
// it represents only the play/pause state
|
||||
// which is needed to show play/pause icons
|
||||
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) == true) {
|
||||
VISIBLE
|
||||
} else {
|
||||
INVISIBLE
|
||||
}
|
||||
binding.rewindBtn.visibility = if (playerControl?.canSeekBackward() == true) {
|
||||
binding.rewindBtn.visibility = if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_BACK) == true) {
|
||||
VISIBLE
|
||||
} else {
|
||||
INVISIBLE
|
||||
|
@ -245,10 +248,10 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
|
||||
private fun doPauseResume() {
|
||||
playerControl?.run {
|
||||
if (isPlaying) {
|
||||
if (playWhenReady) {
|
||||
pause()
|
||||
} else {
|
||||
start()
|
||||
play()
|
||||
}
|
||||
}
|
||||
updatePausePlay()
|
||||
|
@ -267,30 +270,25 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
|
||||
@Suppress("MagicNumber")
|
||||
override fun onClick(v: View) {
|
||||
var pos: Int
|
||||
|
||||
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
|
||||
playerControl.seekTo(pos)
|
||||
playerControl.seekBack()
|
||||
if (!playing) {
|
||||
playerControl.pause() // necessary in some 2.3.x devices
|
||||
}
|
||||
setProgress()
|
||||
}
|
||||
R.id.forwardBtn -> {
|
||||
pos = playerControl.currentPosition
|
||||
pos += 15000
|
||||
playerControl.seekTo(pos)
|
||||
|
||||
R.id.forwardBtn -> {
|
||||
playerControl.seekForward()
|
||||
if (!playing) {
|
||||
playerControl.pause() // necessary in some 2.3.x devices
|
||||
}
|
||||
|
@ -313,10 +311,10 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
|
|||
}
|
||||
|
||||
playerControl?.let { playerControl ->
|
||||
val duration = playerControl.duration.toLong()
|
||||
val duration = playerControl.duration
|
||||
val newPosition = duration * progress / 1000L
|
||||
playerControl.seekTo(newPosition.toInt())
|
||||
binding.currentTimeText.text = formatTime(newPosition.toInt())
|
||||
playerControl.seekTo(newPosition)
|
||||
binding.currentTimeText.text = formatTime(newPosition)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,15 +13,12 @@
|
|||
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.DialogInterface
|
||||
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,21 +42,29 @@ 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.PlaybackException
|
||||
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.MediaSession
|
||||
import androidx.media3.session.SessionToken
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
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.ErrorFormat
|
||||
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
|
||||
import com.nextcloud.common.NextcloudClient
|
||||
|
@ -71,7 +76,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 +92,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
|
||||
|
@ -107,6 +110,7 @@ import javax.inject.Inject
|
|||
* instantiation too.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@OptIn(UnstableApi::class)
|
||||
class PreviewMediaActivity :
|
||||
FileActivity(),
|
||||
FileFragment.ContainerActivity,
|
||||
|
@ -117,9 +121,7 @@ class PreviewMediaActivity :
|
|||
private var user: User? = null
|
||||
private var savedPlaybackPosition: Long = 0
|
||||
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,7 +134,10 @@ class PreviewMediaActivity :
|
|||
|
||||
private lateinit var binding: ActivityPreviewMediaBinding
|
||||
private var emptyListView: ViewGroup? = null
|
||||
private var exoPlayer: ExoPlayer? = null
|
||||
private var videoPlayer: ExoPlayer? = null
|
||||
private var videoMediaSession: MediaSession? = null
|
||||
private var audioMediaController: MediaController? = null
|
||||
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
||||
private var nextcloudClient: NextcloudClient? = null
|
||||
private lateinit var windowInsetsController: WindowInsetsControllerCompat
|
||||
|
||||
|
@ -150,12 +155,36 @@ class PreviewMediaActivity :
|
|||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
applyWindowInsets()
|
||||
initArguments(savedInstanceState)
|
||||
mediaPlayerServiceConnection = PlayerServiceConnection(this)
|
||||
|
||||
if (MimeTypeUtil.isVideo(file)) {
|
||||
// release any background media session if exists
|
||||
sendAudioSessionReleaseBroadcast()
|
||||
} else if (MimeTypeUtil.isAudio(file)) {
|
||||
val stopPlayer = Intent(BackgroundPlayerService.STOP_MEDIA_SESSION_BROADCAST_ACTION).apply {
|
||||
setPackage(packageName)
|
||||
}
|
||||
sendBroadcast(stopPlayer)
|
||||
}
|
||||
|
||||
showMediaTypeViews()
|
||||
configureSystemBars()
|
||||
emptyListView = binding.emptyView.emptyListView
|
||||
showProgressLayout()
|
||||
addMarginForEmptyView()
|
||||
if (file == null) {
|
||||
return
|
||||
}
|
||||
if (MimeTypeUtil.isAudio(file)) {
|
||||
setGenericThumbnail()
|
||||
initializeAudioPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendAudioSessionReleaseBroadcast() {
|
||||
val intent = Intent(BackgroundPlayerService.RELEASE_MEDIA_SESSION_BROADCAST_ACTION).apply {
|
||||
setPackage(packageName)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun addMarginForEmptyView() {
|
||||
|
@ -173,25 +202,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 +213,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 +241,6 @@ class PreviewMediaActivity :
|
|||
|
||||
if (isFileVideo) {
|
||||
binding.root.setBackgroundColor(resources.getColor(R.color.black, null))
|
||||
} else {
|
||||
extractAndSetCoverArt(file)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -265,17 +259,17 @@ 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
|
||||
}
|
||||
|
||||
private fun setVideoErrorMessage(headline: String, @StringRes message: Int) {
|
||||
private fun setErrorMessage(headline: String, @StringRes message: Int) {
|
||||
binding.emptyView.run {
|
||||
emptyListViewHeadline.text = headline
|
||||
emptyListViewText.setText(message)
|
||||
|
@ -287,48 +281,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,17 +308,19 @@ class PreviewMediaActivity :
|
|||
|
||||
private fun saveMediaInstanceState(bundle: Bundle) {
|
||||
bundle.run {
|
||||
if (MimeTypeUtil.isVideo(file) && exoPlayer != null) {
|
||||
exoPlayer?.let {
|
||||
if (MimeTypeUtil.isVideo(file)) {
|
||||
videoPlayer?.let {
|
||||
savedPlaybackPosition = it.currentPosition
|
||||
autoplay = it.isPlaying
|
||||
autoplay = it.playWhenReady
|
||||
}
|
||||
} else {
|
||||
audioMediaController?.let {
|
||||
savedPlaybackPosition = it.currentPosition
|
||||
autoplay = it.playWhenReady
|
||||
}
|
||||
putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
|
||||
putBoolean(EXTRA_PLAYING, autoplay)
|
||||
} else if (mediaPlayerServiceConnection != null && mediaPlayerServiceConnection!!.isConnected) {
|
||||
putInt(EXTRA_PLAY_POSITION, mediaPlayerServiceConnection!!.currentPosition)
|
||||
putBoolean(EXTRA_PLAYING, mediaPlayerServiceConnection!!.isPlaying)
|
||||
}
|
||||
putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
|
||||
putBoolean(EXTRA_PLAYING, autoplay)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -375,42 +329,12 @@ 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)) {
|
||||
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 +342,10 @@ class PreviewMediaActivity :
|
|||
|
||||
nextcloudClient?.let { client ->
|
||||
handler.post {
|
||||
exoPlayer = createNextcloudExoplayer(this, client)
|
||||
videoPlayer = createNextcloudExoplayer(this, client)
|
||||
videoMediaSession = MediaSession.Builder(this, videoPlayer as Player).build()
|
||||
|
||||
exoPlayer?.let { player ->
|
||||
videoPlayer?.let { player ->
|
||||
player.addListener(
|
||||
ExoplayerListener(
|
||||
this,
|
||||
|
@ -439,6 +364,102 @@ class PreviewMediaActivity :
|
|||
}
|
||||
}
|
||||
|
||||
private fun releaseVideoPlayer() {
|
||||
videoPlayer?.let {
|
||||
savedPlaybackPosition = it.currentPosition
|
||||
autoplay = it.playWhenReady
|
||||
it.release()
|
||||
videoMediaSession?.release()
|
||||
}
|
||||
videoMediaSession = null
|
||||
videoPlayer = null
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun initializeAudioPlayer() {
|
||||
val sessionToken = SessionToken(this, ComponentName(this, BackgroundPlayerService::class.java))
|
||||
mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
|
||||
mediaControllerFuture?.addListener(
|
||||
{
|
||||
try {
|
||||
audioMediaController = mediaControllerFuture?.get()
|
||||
playAudio()
|
||||
binding.audioControllerView.setMediaPlayer(audioMediaController)
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "exception raised while getting the media controller ${e.message}")
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor()
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
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 for Audio not possible: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareAudioPlayer(uri: Uri) {
|
||||
audioMediaController?.let { audioPlayer ->
|
||||
audioPlayer.addListener(object : Player.Listener {
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
hideProgressLayout()
|
||||
binding.emptyView.emptyListView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
|
||||
super.onMediaMetadataChanged(mediaMetadata)
|
||||
val artworkBitmap = mediaMetadata.artworkData?.let { bytes: ByteArray ->
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
}
|
||||
if (artworkBitmap != null) {
|
||||
binding.imagePreview.setImageBitmap(artworkBitmap)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
super.onPlayerError(error)
|
||||
Log_OC.e(TAG, "Exoplayer error", error)
|
||||
val message = ErrorFormat.toString(this@PreviewMediaActivity, error)
|
||||
MaterialAlertDialogBuilder(this@PreviewMediaActivity)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.common_ok) { _: DialogInterface?, _: Int ->
|
||||
audioPlayer.seekToDefaultPosition()
|
||||
audioPlayer.pause()
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
})
|
||||
val mediaItem = MediaItem.Builder()
|
||||
.setUri(uri)
|
||||
.setMediaMetadata(MediaMetadata.Builder().setTitle(file.fileName).build())
|
||||
.build()
|
||||
audioPlayer.setMediaItem(mediaItem)
|
||||
audioPlayer.playWhenReady = autoplay
|
||||
audioPlayer.seekTo(savedPlaybackPosition)
|
||||
audioPlayer.prepare()
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseAudioPlayer() {
|
||||
audioMediaController?.let { audioPlayer ->
|
||||
audioPlayer.release()
|
||||
}
|
||||
audioMediaController = null
|
||||
}
|
||||
|
||||
private fun initWindowInsetsController() {
|
||||
windowInsetsController = WindowCompat.getInsetsController(
|
||||
window,
|
||||
|
@ -448,7 +469,6 @@ class PreviewMediaActivity :
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(markerClass = [UnstableApi::class])
|
||||
private fun applyWindowInsets() {
|
||||
val playerView = binding.exoplayerView
|
||||
val exoControls = playerView.findViewById<FrameLayout>(R.id.exo_bottom_bar)
|
||||
|
@ -477,7 +497,6 @@ class PreviewMediaActivity :
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun setupVideoView() {
|
||||
initWindowInsetsController()
|
||||
val type = WindowInsetsCompat.Type.systemBars()
|
||||
|
@ -495,14 +514,10 @@ class PreviewMediaActivity :
|
|||
}
|
||||
}
|
||||
)
|
||||
it.player = exoPlayer
|
||||
it.player = videoPlayer
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopAudio() {
|
||||
mediaPlayerServiceConnection?.stop()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.custom_menu_placeholder, menu)
|
||||
return true
|
||||
|
@ -567,7 +582,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 +629,7 @@ class PreviewMediaActivity :
|
|||
val removedFile = operation.file
|
||||
val fileAvailable: Boolean = storageManager.fileExists(removedFile.fileId)
|
||||
if (!fileAvailable && removedFile == file) {
|
||||
sendAudioSessionReleaseBroadcast()
|
||||
finish()
|
||||
}
|
||||
} else if (operation is SynchronizeFileOperation) {
|
||||
|
@ -670,30 +686,25 @@ class PreviewMediaActivity :
|
|||
setupVideoView()
|
||||
|
||||
if (file.isDown) {
|
||||
playVideoUri(file.storageUri)
|
||||
prepareVideoPlayer(file.storageUri)
|
||||
} else {
|
||||
try {
|
||||
LoadStreamUrl(this, user, clientFactory).execute(file.localId)
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "Loading stream url not possible: $e")
|
||||
Log_OC.e(TAG, "Loading stream url for Video not possible: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,11 +739,15 @@ 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(
|
||||
setErrorMessage(
|
||||
weakReference.getString(R.string.stream_not_possible_headline),
|
||||
R.string.stream_not_possible_message
|
||||
)
|
||||
|
@ -754,29 +769,16 @@ class PreviewMediaActivity :
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log_OC.v(TAG, "onDestroy")
|
||||
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mediaControlReceiver)
|
||||
|
||||
mediaControllerFuture?.let { MediaController.releaseFuture(it) }
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -830,22 +832,12 @@ class PreviewMediaActivity :
|
|||
|
||||
private fun stopPreview(stopAudio: Boolean) {
|
||||
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
|
||||
|
||||
|
|
|
@ -17,8 +17,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
|
@ -34,23 +32,23 @@ import android.view.View.OnTouchListener
|
|||
import android.view.ViewGroup
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
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.Companion.instance
|
||||
import com.nextcloud.client.media.BackgroundPlayerService
|
||||
import com.nextcloud.client.media.ExoplayerListener
|
||||
import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
|
||||
import com.nextcloud.client.media.PlayerServiceConnection
|
||||
import com.nextcloud.client.network.ClientFactory
|
||||
import com.nextcloud.client.network.ClientFactory.CreationException
|
||||
import com.nextcloud.common.NextcloudClient
|
||||
|
@ -61,7 +59,6 @@ import com.owncloud.android.MainApp
|
|||
import com.owncloud.android.R
|
||||
import com.owncloud.android.databinding.FragmentPreviewMediaBinding
|
||||
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.utils.Log_OC
|
||||
|
@ -105,7 +102,6 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
private var autoplay = true
|
||||
private var isLivePhoto = false
|
||||
private val prepared = false
|
||||
private var mediaPlayerServiceConnection: PlayerServiceConnection? = null
|
||||
|
||||
private var videoUri: Uri? = null
|
||||
|
||||
|
@ -121,16 +117,22 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
lateinit var binding: FragmentPreviewMediaBinding
|
||||
private var emptyListView: ViewGroup? = null
|
||||
private var exoPlayer: ExoPlayer? = null
|
||||
private var mediaSession: MediaSession? = null
|
||||
private var nextcloudClient: NextcloudClient? = null
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// release any background media session if exists
|
||||
val intent = Intent(BackgroundPlayerService.RELEASE_MEDIA_SESSION_BROADCAST_ACTION).apply {
|
||||
setPackage(requireActivity().packageName)
|
||||
}
|
||||
requireActivity().sendBroadcast(intent)
|
||||
|
||||
arguments?.let {
|
||||
initArguments(it)
|
||||
}
|
||||
|
||||
mediaPlayerServiceConnection = PlayerServiceConnection(requireContext())
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
@ -150,10 +152,6 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
|
||||
checkArgumentsAfterViewCreation(savedInstanceState)
|
||||
|
||||
if (file != null) {
|
||||
prepareExoPlayerView()
|
||||
}
|
||||
|
||||
toggleDrawerLockMode(containerActivity, DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
addMenuHost()
|
||||
}
|
||||
|
@ -198,62 +196,6 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
binding.progress.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
private fun extractAndSetCoverArt(file: OCFile) {
|
||||
if (!MimeTypeUtil.isAudio(file)) return
|
||||
|
||||
if (file.storagePath == null) {
|
||||
setThumbnailForAudio(file)
|
||||
} else {
|
||||
try {
|
||||
val mmr = MediaMetadataRetriever().apply {
|
||||
setDataSource(file.storagePath)
|
||||
}
|
||||
|
||||
val data = mmr.embeddedPicture
|
||||
if (data != null) {
|
||||
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
|
||||
binding.imagePreview.setImageBitmap(bitmap) // associated cover art in bitmap
|
||||
} else {
|
||||
setThumbnailForAudio(file)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
setGenericThumbnail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setThumbnailForAudio(file: OCFile) {
|
||||
val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
|
||||
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId
|
||||
)
|
||||
|
||||
if (thumbnail != null) {
|
||||
binding.imagePreview.setImageBitmap(thumbnail)
|
||||
} else {
|
||||
setGenericThumbnail()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set generic icon (logo) as placeholder for thumbnail in preview.
|
||||
*/
|
||||
private fun setGenericThumbnail() {
|
||||
AppCompatResources.getDrawable(requireContext(), R.drawable.logo)?.let { logo ->
|
||||
if (!resources.getBoolean(R.bool.is_branded_client)) {
|
||||
// only colour logo of non-branded client
|
||||
DrawableCompat.setTint(logo, resources.getColor(R.color.primary, requireContext().theme))
|
||||
}
|
||||
binding.imagePreview.setImageDrawable(logo)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
file.logFileSize(TAG)
|
||||
|
@ -263,15 +205,10 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
putParcelable(EXTRA_FILE, file)
|
||||
putParcelable(EXTRA_USER, user)
|
||||
|
||||
if (MimeTypeUtil.isVideo(file) && exoPlayer != null) {
|
||||
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
|
||||
autoplay = exoPlayer?.isPlaying ?: false
|
||||
putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
|
||||
putBoolean(EXTRA_PLAYING, autoplay)
|
||||
} else if (mediaPlayerServiceConnection != null && mediaPlayerServiceConnection?.isConnected == true) {
|
||||
putInt(EXTRA_PLAY_POSITION, mediaPlayerServiceConnection?.currentPosition ?: 0)
|
||||
putBoolean(EXTRA_PLAYING, mediaPlayerServiceConnection?.isPlaying ?: false)
|
||||
}
|
||||
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
|
||||
autoplay = exoPlayer?.isPlaying ?: false
|
||||
putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
|
||||
putBoolean(EXTRA_PLAYING, autoplay)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -285,22 +222,11 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
Log_OC.d(TAG, "File is null or fragment not attached to a context.")
|
||||
return
|
||||
}
|
||||
|
||||
mediaPlayerServiceConnection?.bind()
|
||||
|
||||
if (MimeTypeUtil.isAudio(file)) {
|
||||
prepareForAudio()
|
||||
} else if (MimeTypeUtil.isVideo(file)) {
|
||||
prepareForVideo(context ?: MainApp.getAppContext())
|
||||
}
|
||||
prepareForVideo(context ?: MainApp.getAppContext())
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "TooGenericExceptionCaught")
|
||||
private fun prepareForVideo(context: Context) {
|
||||
if (mediaPlayerServiceConnection?.isConnected == true) {
|
||||
// always stop player
|
||||
stopAudio()
|
||||
}
|
||||
if (exoPlayer != null) {
|
||||
playVideo()
|
||||
} else {
|
||||
|
@ -327,14 +253,22 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
val listener = ExoplayerListener(context, binding.exoplayerView, it) { goBackToLivePhoto() }
|
||||
it.addListener(listener)
|
||||
}
|
||||
// session id needs to be unique since this fragment is used in viewpager multiple fragments can exist at a time
|
||||
mediaSession = MediaSession.Builder(
|
||||
requireContext(),
|
||||
exoPlayer as Player
|
||||
).setId(System.currentTimeMillis().toString()).build()
|
||||
}
|
||||
|
||||
private fun prepareForAudio() {
|
||||
binding.mediaController.setMediaPlayer(mediaPlayerServiceConnection)
|
||||
binding.mediaController.visibility = View.VISIBLE
|
||||
mediaPlayerServiceConnection?.start(user!!, file, autoplay, savedPlaybackPosition)
|
||||
binding.emptyView.emptyListView.visibility = View.GONE
|
||||
binding.progress.visibility = View.GONE
|
||||
private fun releaseVideoPlayer() {
|
||||
exoPlayer?.let {
|
||||
savedPlaybackPosition = it.currentPosition
|
||||
autoplay = it.playWhenReady
|
||||
it.release()
|
||||
mediaSession?.release()
|
||||
}
|
||||
mediaSession = null
|
||||
exoPlayer = null
|
||||
}
|
||||
|
||||
private fun goBackToLivePhoto() {
|
||||
|
@ -346,17 +280,6 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
requireActivity().supportFragmentManager.popBackStack()
|
||||
}
|
||||
|
||||
private fun prepareExoPlayerView() {
|
||||
if (MimeTypeUtil.isVideo(file)) {
|
||||
binding.exoplayerView.visibility = View.VISIBLE
|
||||
binding.imagePreview.visibility = View.GONE
|
||||
} else {
|
||||
binding.exoplayerView.visibility = View.GONE
|
||||
binding.imagePreview.visibility = View.VISIBLE
|
||||
extractAndSetCoverArt(file)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showActionBar() {
|
||||
val currentActivity: Activity = requireActivity()
|
||||
if (currentActivity is PreviewImageActivity) {
|
||||
|
@ -374,10 +297,6 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
private fun stopAudio() {
|
||||
mediaPlayerServiceConnection?.stop()
|
||||
}
|
||||
|
||||
private fun addMenuHost() {
|
||||
val menuHost: MenuHost = requireActivity()
|
||||
|
||||
|
@ -490,12 +409,12 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
}
|
||||
|
||||
private fun seeDetails() {
|
||||
stopPreview(false)
|
||||
releaseVideoPlayer()
|
||||
containerActivity.showDetails(file)
|
||||
}
|
||||
|
||||
private fun sendShareFile() {
|
||||
stopPreview(false)
|
||||
releaseVideoPlayer()
|
||||
containerActivity.fileOperationsHelper.sendShareFile(file)
|
||||
}
|
||||
|
||||
|
@ -583,19 +502,7 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
}
|
||||
|
||||
override fun onStop() {
|
||||
Log_OC.v(TAG, "onStop")
|
||||
val file = file
|
||||
|
||||
if (MimeTypeUtil.isAudio(file) && mediaPlayerServiceConnection?.isPlaying == false) {
|
||||
stopAudio()
|
||||
} else if (MimeTypeUtil.isVideo(file) && exoPlayer != null && exoPlayer?.isPlaying == true) {
|
||||
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
|
||||
exoPlayer?.pause()
|
||||
}
|
||||
|
||||
mediaPlayerServiceConnection?.unbind()
|
||||
toggleDrawerLockMode(containerActivity, DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||
|
||||
releaseVideoPlayer()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
|
@ -641,19 +548,9 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
|
|||
* Opens the previewed file with an external application.
|
||||
*/
|
||||
private fun openFile() {
|
||||
stopPreview(true)
|
||||
containerActivity.fileOperationsHelper.openFile(file)
|
||||
}
|
||||
|
||||
private fun stopPreview(stopAudio: Boolean) {
|
||||
if (stopAudio && mediaPlayerServiceConnection != null) {
|
||||
mediaPlayerServiceConnection?.stop()
|
||||
} else if (exoPlayer != null) {
|
||||
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
|
||||
exoPlayer?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
val position: Long
|
||||
get() {
|
||||
if (prepared) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -18,15 +18,6 @@
|
|||
android:gravity="center"
|
||||
tools:context=".ui.preview.PreviewMediaFragment">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:contentDescription="@string/preview_image_description"
|
||||
android:src="@drawable/logo" />
|
||||
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/exoplayer_view"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -34,14 +25,6 @@
|
|||
android:layout_gravity="center"
|
||||
app:show_buffering="always" />
|
||||
|
||||
<com.owncloud.android.media.MediaControlView
|
||||
android:id="@+id/media_controller"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="@dimen/standard_margin"
|
||||
android:visibility="gone" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -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