diff --git a/build.gradle b/build.gradle
index 6039557578e2..478aafdea268 100644
--- a/build.gradle
+++ b/build.gradle
@@ -345,6 +345,8 @@ dependencies {
// androidJacocoAnt "org.jacoco:org.jacoco.core:${jacocoVersion}"
// androidJacocoAnt "org.jacoco:org.jacoco.report:${jacocoVersion}"
// androidJacocoAnt "org.jacoco:org.jacoco.agent:${jacocoVersion}"
+
+ implementation "com.github.stateless4j:stateless4j:2.6.0"
}
configurations.all {
diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt
index 3560666719ff..b872400059e8 100644
--- a/scripts/analysis/findbugs-results.txt
+++ b/scripts/analysis/findbugs-results.txt
@@ -1 +1 @@
-429
+427
\ No newline at end of file
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 8ddccdbd0b72..f3ce8e29b0bc 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -307,7 +307,7 @@
-
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.media
+
+import android.media.AudioManager
+
+/**
+ * Simplified audio focus values, relevant to application's media player experience.
+ */
+internal enum class AudioFocus {
+
+ LOST,
+ DUCK,
+ FOCUS;
+
+ companion object {
+ fun fromPlatformFocus(audioFocus: Int): AudioFocus? = when (audioFocus) {
+ AudioManager.AUDIOFOCUS_GAIN -> FOCUS
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> FOCUS
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> FOCUS
+ AudioManager.AUDIOFOCUS_LOSS -> LOST
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> LOST
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> DUCK
+ else -> null
+ }
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/media/AudioFocusManager.kt b/src/main/java/com/nextcloud/client/media/AudioFocusManager.kt
new file mode 100644
index 000000000000..89580abcdcf8
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/media/AudioFocusManager.kt
@@ -0,0 +1,95 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ *
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.media
+
+import android.media.AudioFocusRequest
+import android.media.AudioManager
+import android.os.Build
+
+/**
+ * Wrapper around audio manager exposing simplified audio focus API and
+ * hiding platform API level differences.
+ *
+ * @param audioManger Platform audio manager
+ * @param onFocusChange Called when audio focus changes, including acquired and released focus states
+ */
+internal class AudioFocusManager(
+ private val audioManger: AudioManager,
+ private val onFocusChange: (AudioFocus) -> Unit
+) {
+
+ private val focusListener = object : AudioManager.OnAudioFocusChangeListener {
+ override fun onAudioFocusChange(focusChange: Int) {
+ val focus = when (focusChange) {
+ AudioManager.AUDIOFOCUS_GAIN -> AudioFocus.FOCUS
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> AudioFocus.FOCUS
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> AudioFocus.FOCUS
+ AudioManager.AUDIOFOCUS_LOSS -> AudioFocus.LOST
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> AudioFocus.LOST
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> AudioFocus.DUCK
+ else -> null
+ }
+ focus?.let { onFocusChange(it) }
+ }
+ }
+ private var focusRequest: AudioFocusRequest? = null
+
+ init {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
+ setWillPauseWhenDucked(true)
+ setOnAudioFocusChangeListener(focusListener)
+ }.build()
+ }
+ }
+
+ /**
+ * Request audio focus. Focus is reported via callback.
+ * If focus cannot be gained, lost of focus is reported.
+ */
+ fun requestFocus() {
+ val requestResult = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ focusRequest?.let { audioManger.requestAudioFocus(it) }
+ } else {
+ audioManger.requestAudioFocus(focusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
+ }
+
+ if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ focusListener.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN)
+ } else {
+ focusListener.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS)
+ }
+ }
+
+ /**
+ * Release audio focus. Loss of focus is reported via callback.
+ */
+ fun releaseFocus() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ focusRequest?.let {
+ audioManger.abandonAudioFocusRequest(it)
+ } ?: AudioManager.AUDIOFOCUS_REQUEST_FAILED
+ } else {
+ audioManger.abandonAudioFocus(focusListener)
+ }
+ focusListener.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS)
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/media/ErrorFormat.kt b/src/main/java/com/nextcloud/client/media/ErrorFormat.kt
new file mode 100644
index 000000000000..d785966e13cc
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/media/ErrorFormat.kt
@@ -0,0 +1,94 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author David A. Velasco
+ * @author masensio
+ * @author Chris Narkiewicz
+ * Copyright (C) 2013 David A. Velasco
+ * Copyright (C) 2016 masensio
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.media
+
+import android.content.Context
+import android.media.MediaPlayer
+import com.owncloud.android.R
+
+/**
+ * This code has been moved from legacy media player service.
+ */
+@Deprecated("This legacy helper should be refactored")
+@Suppress("ComplexMethod") // it's legacy code
+object ErrorFormat {
+
+ /** Error code for specific messages - see regular error codes at [MediaPlayer] */
+ const val OC_MEDIA_ERROR = 0
+
+ @JvmStatic
+ fun toString(context: Context?, what: Int, extra: Int): String {
+ val messageId: Int
+
+ if (what == OC_MEDIA_ERROR) {
+ messageId = extra
+ } else if (extra == MediaPlayer.MEDIA_ERROR_UNSUPPORTED) {
+ /* Added in API level 17
+ Bitstream is conforming to the related coding standard or file spec,
+ but the media framework does not support the feature.
+ Constant Value: -1010 (0xfffffc0e)
+ */
+ messageId = R.string.media_err_unsupported
+ } else if (extra == MediaPlayer.MEDIA_ERROR_IO) {
+ /* Added in API level 17
+ File or network related operation errors.
+ Constant Value: -1004 (0xfffffc14)
+ */
+ messageId = R.string.media_err_io
+ } else if (extra == MediaPlayer.MEDIA_ERROR_MALFORMED) {
+ /* Added in API level 17
+ Bitstream is not conforming to the related coding standard or file spec.
+ Constant Value: -1007 (0xfffffc11)
+ */
+ messageId = R.string.media_err_malformed
+ } else if (extra == MediaPlayer.MEDIA_ERROR_TIMED_OUT) {
+ /* Added in API level 17
+ Some operation takes too long to complete, usually more than 3-5 seconds.
+ Constant Value: -110 (0xffffff92)
+ */
+ messageId = R.string.media_err_timeout
+ } else if (what == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
+ /* Added in API level 3
+ The video is streamed and its container is not valid for progressive playback i.e the video's index
+ (e.g moov atom) is not at the start of the file.
+ Constant Value: 200 (0x000000c8)
+ */
+ messageId = R.string.media_err_invalid_progressive_playback
+ } else {
+ /* MediaPlayer.MEDIA_ERROR_UNKNOWN
+ Added in API level 1
+ Unspecified media player error.
+ Constant Value: 1 (0x00000001)
+ */
+ /* MediaPlayer.MEDIA_ERROR_SERVER_DIED)
+ Added in API level 1
+ Media server died. In this case, the application must release the MediaPlayer
+ object and instantiate a new one.
+ Constant Value: 100 (0x00000064)
+ */
+ messageId = R.string.media_err_unknown
+ }
+ return context?.getString(messageId) ?: "Media error"
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/media/LoadUrlTask.kt b/src/main/java/com/nextcloud/client/media/LoadUrlTask.kt
new file mode 100644
index 000000000000..fdd93606a138
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/media/LoadUrlTask.kt
@@ -0,0 +1,49 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * @author Tobias Kaminsky
+ *
+ * Copyright (C) 2019 Chris Narkiewicz
+ * Copyright (C) 2018 Tobias Kaminsky
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.media
+
+import android.os.AsyncTask
+import com.owncloud.android.files.StreamMediaFileOperation
+import com.owncloud.android.lib.common.OwnCloudClient
+
+internal class LoadUrlTask(
+ private val client: OwnCloudClient,
+ private val fileId: String,
+ private val onResult: (String?) -> Unit
+) : AsyncTask() {
+
+ override fun doInBackground(vararg args: Void): String? {
+ val operation = StreamMediaFileOperation(fileId)
+ val result = operation.execute(client)
+ return when (result.isSuccess) {
+ true -> result.data[0] as String
+ false -> null
+ }
+ }
+
+ override fun onPostExecute(url: String?) {
+ if (!isCancelled) {
+ onResult(url)
+ }
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/media/Player.kt b/src/main/java/com/nextcloud/client/media/Player.kt
new file mode 100644
index 000000000000..d3fbe7b87957
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/media/Player.kt
@@ -0,0 +1,310 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.media
+
+import android.accounts.Account
+import android.content.Context
+import android.media.AudioManager
+import android.media.MediaPlayer
+import android.os.PowerManager
+import android.widget.MediaController
+import com.nextcloud.client.media.PlayerStateMachine.Event
+import com.nextcloud.client.media.PlayerStateMachine.State
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.OwnCloudAccount
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
+import com.owncloud.android.lib.common.utils.Log_OC
+
+@Suppress("TooManyFunctions")
+internal class Player(
+ private val context: Context,
+ private val listener: Listener? = null,
+ audioManager: AudioManager,
+ private val mediaPlayerCreator: () -> MediaPlayer = { MediaPlayer() }
+) : MediaController.MediaPlayerControl {
+
+ private companion object {
+ const val DEFAULT_VOLUME = 1.0f
+ const val DUCK_VOLUME = 0.1f
+ const val MIN_DURATION_ALLOWING_SEEK = 3000
+ }
+
+ interface Listener {
+ fun onRunning(file: OCFile)
+ fun onStart()
+ fun onPause()
+ fun onStop()
+ fun onError(error: PlayerError)
+ }
+
+ private var stateMachine: PlayerStateMachine
+ private var loadUrlTask: LoadUrlTask? = null
+
+ private var enqueuedFile: PlaylistItem? = null
+
+ private var playedFile: OCFile? = null
+ private var startPositionMs: Int = 0
+ private var autoPlay = true
+ private var account: Account? = null
+ private var dataSource: String? = null
+ private var lastError: PlayerError? = null
+ private var mediaPlayer: MediaPlayer? = null
+ private val focusManager = AudioFocusManager(audioManager, this::onAudioFocusChange)
+
+ private val delegate = object : PlayerStateMachine.Delegate {
+ override val isDownloaded: Boolean get() = playedFile?.isDown ?: false
+ override val isAutoplayEnabled: Boolean get() = autoPlay
+ override val hasEnqueuedFile: Boolean get() = enqueuedFile != null
+
+ override fun onStartRunning() {
+ trace("onStartRunning()")
+ enqueuedFile.let {
+ if (it != null) {
+ playedFile = it.file
+ startPositionMs = it.startPositionMs
+ autoPlay = it.autoPlay
+ account = it.account
+ dataSource = if (it.file.isDown) it.file.storagePath else null
+ listener?.onRunning(it.file)
+ } else {
+ throw IllegalStateException("Player started without enqueued file.")
+ }
+ }
+ }
+
+ override fun onStartDownloading() {
+ trace("onStartDownloading()")
+ if (playedFile == null) {
+ throw IllegalStateException("File not set.")
+ }
+ playedFile?.let {
+ val client = buildClient()
+ val task = LoadUrlTask(client, it.remoteId, this@Player::onDownloaded)
+ task.execute()
+ loadUrlTask = task
+ }
+ }
+
+ override fun onPrepare() {
+ trace("onPrepare()")
+ mediaPlayer = mediaPlayerCreator.invoke()
+ mediaPlayer?.setOnErrorListener(this@Player::onMediaPlayerError)
+ mediaPlayer?.setOnPreparedListener(this@Player::onMediaPlayerPrepared)
+ mediaPlayer?.setOnCompletionListener(this@Player::onMediaPlayerCompleted)
+ mediaPlayer?.setOnBufferingUpdateListener(this@Player::onMediaPlayerBufferingUpdate)
+ mediaPlayer?.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
+ mediaPlayer?.setDataSource(dataSource)
+ mediaPlayer?.setAudioStreamType(AudioManager.STREAM_MUSIC)
+ mediaPlayer?.setVolume(DEFAULT_VOLUME, DEFAULT_VOLUME)
+ mediaPlayer?.prepareAsync()
+ }
+
+ override fun onStopped() {
+ trace("onStoppped()")
+ mediaPlayer?.stop()
+ mediaPlayer?.reset()
+ mediaPlayer?.release()
+ mediaPlayer = null
+
+ playedFile = null
+ startPositionMs = 0
+ account = null
+ autoPlay = true
+ dataSource = null
+ loadUrlTask?.cancel(true)
+ loadUrlTask = null
+ listener?.onStop()
+ }
+
+ override fun onError() {
+ trace("onError()")
+ this.onStopped()
+ lastError?.let {
+ this@Player.listener?.onError(it)
+ }
+ if (lastError == null) {
+ this@Player.listener?.onError(PlayerError("Unknown"))
+ }
+ }
+
+ override fun onStartPlayback() {
+ trace("onStartPlayback()")
+ mediaPlayer?.start()
+ listener?.onStart()
+ }
+
+ override fun onPausePlayback() {
+ trace("onPausePlayback()")
+ mediaPlayer?.pause()
+ listener?.onPause()
+ }
+
+ override fun onRequestFocus() {
+ trace("onRequestFocus()")
+ focusManager.requestFocus()
+ }
+
+ override fun onReleaseFocus() {
+ trace("onReleaseFocus()")
+ focusManager.releaseFocus()
+ }
+
+ override fun onAudioDuck(enabled: Boolean) {
+ trace("onAudioDuck(): $enabled")
+ if (enabled) {
+ mediaPlayer?.setVolume(DUCK_VOLUME, DUCK_VOLUME)
+ } else {
+ mediaPlayer?.setVolume(DEFAULT_VOLUME, DEFAULT_VOLUME)
+ }
+ }
+ }
+
+ init {
+ stateMachine = PlayerStateMachine(delegate)
+ }
+
+ fun play(item: PlaylistItem) {
+ if (item.file != playedFile) {
+ stateMachine.post(Event.STOP)
+ this.enqueuedFile = item
+ stateMachine.post(Event.PLAY)
+ }
+ }
+
+ fun stop() {
+ stateMachine.post(Event.STOP)
+ }
+
+ fun stop(file: OCFile) {
+ if (playedFile == file) {
+ stateMachine.post(Event.STOP)
+ }
+ }
+
+ private fun onMediaPlayerError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
+ lastError = PlayerError(ErrorFormat.toString(context, what, extra))
+ stateMachine.post(Event.ERROR)
+ return true
+ }
+
+ private fun onMediaPlayerPrepared(mp: MediaPlayer) {
+ trace("onMediaPlayerPrepared()")
+ stateMachine.post(Event.PREPARED)
+ }
+
+ private fun onMediaPlayerCompleted(mp: MediaPlayer) {
+ stateMachine.post(Event.STOP)
+ }
+
+ private fun onMediaPlayerBufferingUpdate(mp: MediaPlayer, percent: Int) {
+ trace("onMediaPlayerBufferingUpdate(): $percent")
+ }
+
+ private fun onDownloaded(url: String?) {
+ if (url != null) {
+ dataSource = url
+ stateMachine.post(Event.DOWNLOADED)
+ } else {
+ lastError = PlayerError(context.getString(R.string.media_err_io))
+ stateMachine.post(Event.ERROR)
+ }
+ }
+
+ private fun onAudioFocusChange(focus: AudioFocus) {
+ when (focus) {
+ AudioFocus.FOCUS -> stateMachine.post(Event.FOCUS_GAIN)
+ AudioFocus.DUCK -> stateMachine.post(Event.FOCUS_DUCK)
+ AudioFocus.LOST -> stateMachine.post(Event.FOCUS_LOST)
+ }
+ }
+
+ // this should be refactored into a proper, injectable factory
+ private fun buildClient(): OwnCloudClient {
+ val account = this.account
+ if (account != null) {
+ val ocAccount = OwnCloudAccount(account, context)
+ return OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context)
+ } else {
+ throw IllegalArgumentException("Account not set")
+ }
+ }
+
+ private fun trace(fmt: String, vararg args: Any?) {
+ Log_OC.v(javaClass.simpleName, fmt.format(args))
+ }
+
+ // region Media player controls
+
+ override fun isPlaying(): Boolean {
+ return stateMachine.isInState(State.PLAYING)
+ }
+
+ override fun canSeekForward(): Boolean {
+ return duration > MIN_DURATION_ALLOWING_SEEK
+ }
+
+ override fun canSeekBackward(): Boolean {
+ return duration > MIN_DURATION_ALLOWING_SEEK
+ }
+
+ override fun getDuration(): Int {
+ val hasDuration = setOf(State.PLAYING, State.PAUSED)
+ .find { stateMachine.isInState(it) } != null
+ return if (hasDuration) {
+ mediaPlayer?.duration ?: 0
+ } else {
+ 0
+ }
+ }
+
+ override fun pause() {
+ stateMachine.post(Event.PAUSE)
+ }
+
+ override fun getBufferPercentage(): Int {
+ return 0
+ }
+
+ override fun seekTo(pos: Int) {
+ if (stateMachine.isInState(State.PLAYING)) {
+ mediaPlayer?.seekTo(pos)
+ }
+ }
+
+ override fun getCurrentPosition(): Int {
+ return mediaPlayer?.currentPosition ?: 0
+ }
+
+ override fun start() {
+ stateMachine.post(Event.PLAY)
+ }
+
+ override fun getAudioSessionId(): Int {
+ return 0
+ }
+
+ override fun canPause(): Boolean {
+ return stateMachine.isInState(State.PLAYING)
+ }
+
+ // endregion
+}
diff --git a/src/main/java/com/nextcloud/client/media/PlayerError.kt b/src/main/java/com/nextcloud/client/media/PlayerError.kt
new file mode 100644
index 000000000000..6c56b139a145
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/media/PlayerError.kt
@@ -0,0 +1,3 @@
+package com.nextcloud.client.media
+
+data class PlayerError(val message: String)
diff --git a/src/main/java/com/nextcloud/client/media/PlayerService.kt b/src/main/java/com/nextcloud/client/media/PlayerService.kt
new file mode 100644
index 000000000000..1302d6d91970
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/media/PlayerService.kt
@@ -0,0 +1,148 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.media
+
+import android.accounts.Account
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.media.AudioManager
+import android.os.Bundle
+import android.os.IBinder
+import android.widget.MediaController
+import android.widget.Toast
+import androidx.core.app.NotificationCompat
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.ui.notifications.NotificationUtils
+import com.owncloud.android.utils.ThemeUtils
+import dagger.android.AndroidInjection
+import java.lang.IllegalArgumentException
+import javax.inject.Inject
+
+class PlayerService : Service() {
+
+ companion object {
+ const val EXTRA_ACCOUNT = "ACCOUNT"
+ const val EXTRA_FILE = "FILE"
+ const val EXTRA_AUTO_PLAY = "EXTRA_AUTO_PLAY"
+ const val EXTRA_START_POSITION_MS = "START_POSITION_MS"
+ const val ACTION_PLAY = "PLAY"
+ const val ACTION_STOP = "STOP"
+ const val ACTION_STOP_FILE = "STOP_FILE"
+ }
+
+ class Binder(val service: PlayerService) : android.os.Binder() {
+
+ /**
+ * This property returns current instance of media player interface.
+ * It is not cached and it is suitable for polling.
+ */
+ val player: MediaController.MediaPlayerControl get() = service.player
+ }
+
+ private val playerListener = object : Player.Listener {
+
+ override fun onRunning(file: OCFile) {
+ startForeground(file)
+ }
+
+ override fun onStart() {
+ // empty
+ }
+
+ override fun onPause() {
+ // empty
+ }
+
+ override fun onStop() {
+ stopForeground(true)
+ }
+
+ override fun onError(error: PlayerError) {
+ Toast.makeText(this@PlayerService, error.message, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ @Inject
+ protected lateinit var audioManager: AudioManager
+
+ private lateinit var player: Player
+ private lateinit var notificationBuilder: NotificationCompat.Builder
+
+ override fun onCreate() {
+ super.onCreate()
+ AndroidInjection.inject(this)
+ player = Player(applicationContext, playerListener, audioManager)
+ notificationBuilder = NotificationCompat.Builder(this)
+ notificationBuilder.color = ThemeUtils.primaryColor(this)
+ val stop = Intent(this, PlayerService::class.java)
+ stop.action = ACTION_STOP
+ val pendingStop = PendingIntent.getService(this, 0, stop, 0)
+ notificationBuilder.addAction(0, "STOP", pendingStop)
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return Binder(this)
+ }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ when (intent.action) {
+ ACTION_PLAY -> onActionPlay(intent)
+ ACTION_STOP -> onActionStop()
+ ACTION_STOP_FILE -> onActionStopFile(intent.extras)
+ }
+ return START_NOT_STICKY
+ }
+
+ private fun onActionPlay(intent: Intent) {
+ val account: Account = intent.getParcelableExtra(EXTRA_ACCOUNT)
+ val file: OCFile = intent.getParcelableExtra(EXTRA_FILE)
+ val startPos = intent.getIntExtra(EXTRA_START_POSITION_MS, 0)
+ val autoPlay = intent.getBooleanExtra(EXTRA_AUTO_PLAY, true)
+ val item = PlaylistItem(file = file, startPositionMs = startPos, autoPlay = autoPlay, account = account)
+ player.play(item)
+ }
+
+ private fun onActionStop() {
+ player.stop()
+ }
+
+ private fun onActionStopFile(args: Bundle?) {
+ val file: OCFile = args?.getParcelable(EXTRA_FILE) ?: throw IllegalArgumentException("Missing file argument")
+ player.stop(file)
+ }
+
+ private fun startForeground(currentFile: OCFile) {
+ val ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name))
+ val content = getString(R.string.media_state_playing, currentFile.getFileName())
+ notificationBuilder.setSmallIcon(R.drawable.ic_play_arrow)
+ notificationBuilder.setWhen(System.currentTimeMillis())
+ notificationBuilder.setOngoing(true)
+ notificationBuilder.setContentTitle(ticker)
+ notificationBuilder.setContentText(content)
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ notificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA)
+ }
+
+ startForeground(R.string.media_notif_ticker, notificationBuilder.build())
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/media/PlayerServiceConnection.kt b/src/main/java/com/nextcloud/client/media/PlayerServiceConnection.kt
new file mode 100644
index 000000000000..c46c4018d331
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/media/PlayerServiceConnection.kt
@@ -0,0 +1,134 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.media
+
+import android.accounts.Account
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.widget.MediaController
+import com.owncloud.android.datamodel.OCFile
+
+@Suppress("TooManyFunctions") // implementing large interface
+class PlayerServiceConnection(private val context: Context) : MediaController.MediaPlayerControl {
+
+ var isConnected: Boolean = false
+ private set
+
+ private var binder: PlayerService.Binder? = null
+
+ fun bind() {
+ val intent = Intent(context, PlayerService::class.java)
+ context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ }
+
+ fun unbind() {
+ if (isConnected) {
+ binder = null
+ isConnected = false
+ context.unbindService(connection)
+ }
+ }
+
+ fun start(account: Account, file: OCFile, playImmediately: Boolean, position: Int) {
+ val i = Intent(context, PlayerService::class.java)
+ i.putExtra(PlayerService.EXTRA_ACCOUNT, account)
+ i.putExtra(PlayerService.EXTRA_FILE, file)
+ i.putExtra(PlayerService.EXTRA_AUTO_PLAY, playImmediately)
+ i.putExtra(PlayerService.EXTRA_START_POSITION_MS, position)
+ i.action = PlayerService.ACTION_PLAY
+ context.startService(i)
+ }
+
+ fun stop(file: OCFile) {
+ val i = Intent(context, PlayerService::class.java)
+ i.putExtra(PlayerService.EXTRA_FILE, file)
+ i.action = PlayerService.ACTION_STOP_FILE
+ context.startService(i)
+ }
+
+ fun stop() {
+ val i = Intent(context, PlayerService::class.java)
+ i.action = PlayerService.ACTION_STOP
+ context.startService(i)
+ }
+
+ private val connection = object : ServiceConnection {
+ override fun onServiceDisconnected(name: ComponentName?) {
+ isConnected = false
+ binder = null
+ }
+
+ override fun onServiceConnected(name: ComponentName?, localBinder: IBinder?) {
+ binder = localBinder as PlayerService.Binder
+ isConnected = true
+ }
+ }
+
+ // region Media controller
+
+ override fun isPlaying(): Boolean {
+ return binder?.player?.isPlaying ?: false
+ }
+
+ override fun canSeekForward(): Boolean {
+ return binder?.player?.canSeekForward() ?: false
+ }
+
+ override fun getDuration(): Int {
+ return binder?.player?.duration ?: 0
+ }
+
+ override fun pause() {
+ binder?.player?.pause()
+ }
+
+ override fun getBufferPercentage(): Int {
+ return binder?.player?.bufferPercentage ?: 0
+ }
+
+ override fun seekTo(pos: Int) {
+ binder?.player?.seekTo(pos)
+ }
+
+ override fun getCurrentPosition(): Int {
+ return binder?.player?.currentPosition ?: 0
+ }
+
+ override fun canSeekBackward(): Boolean {
+ return binder?.player?.canSeekBackward() ?: false
+ }
+
+ override fun start() {
+ binder?.player?.start()
+ }
+
+ override fun getAudioSessionId(): Int {
+ return 0
+ }
+
+ override fun canPause(): Boolean {
+ return binder?.player?.canPause() ?: false
+ }
+
+ // endregion
+}
diff --git a/src/main/java/com/nextcloud/client/media/PlayerStateMachine.kt b/src/main/java/com/nextcloud/client/media/PlayerStateMachine.kt
new file mode 100644
index 000000000000..b99783fb14f4
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/media/PlayerStateMachine.kt
@@ -0,0 +1,229 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.media
+
+import com.github.oxo42.stateless4j.StateMachine
+import com.github.oxo42.stateless4j.StateMachineConfig
+import com.github.oxo42.stateless4j.delegates.Action
+import com.github.oxo42.stateless4j.transitions.Transition
+import java.util.ArrayDeque
+
+/*
+ * To see visual representation of the state machine, install PlanUml plugin.
+ * http://plantuml.com/
+ *
+ * @startuml
+ *
+ * note "> - entry action\n< - exit action\n[exp] - transition guard\nfunction() - transition action" as README
+ *
+ * [*] --> STOPPED
+ * STOPPED --> RUNNING: PLAY\n[hasEnqueuedFile]
+ * RUNNING --> STOPPED: STOP\nonStop
+ * RUNNING --> STOPPED: ERROR\nonError
+ * RUNNING: >onStartRunning
+ *
+ * state RUNNING {
+ * [*] --> DOWNLOADING: [!isDownloaded]
+ * [*] --> PREPARING: [isDownloaded]
+ * DOWNLOADING: >onStartDownloading
+ * DOWNLOADING --> PREPARING: DOWNLOADED
+ *
+ * PREPARING: >onPrepare
+ * PREPARING --> PLAYING: PREPARED\n[autoPlay]
+ * PREPARING --> PAUSED: PREPARED\n[!autoPlay]
+ * PLAYING --> PAUSED: PAUSE\nFOCUS_LOST
+ *
+ * PAUSED: >onPausePlayback
+ * PAUSED --> PLAYING: PLAY
+ *
+ * PLAYING: >onRequestFocus
+ * PLAYING: AWAIT_FOCUS
+ * AWAIT_FOCUS --> FOCUSED: FOCUS_GAIN\nonStartPlayback()
+ * FOCUSED -l-> DUCKED: FOCUS_DUCK
+ * DUCKED: >onAudioDuck(true)\n FOCUSED: FOCUS_GAIN
+ * }
+ * }
+ *
+ * @enduml
+ */
+internal class PlayerStateMachine(initialState: State, private val delegate: Delegate) {
+
+ constructor(delegate: Delegate) : this(State.STOPPED, delegate)
+
+ interface Delegate {
+ val isDownloaded: Boolean
+ val isAutoplayEnabled: Boolean
+ val hasEnqueuedFile: Boolean
+
+ fun onStartRunning()
+ fun onStartDownloading()
+ fun onPrepare()
+ fun onStopped()
+ fun onError()
+ fun onStartPlayback()
+ fun onPausePlayback()
+ fun onRequestFocus()
+ fun onReleaseFocus()
+ fun onAudioDuck(enabled: Boolean)
+ }
+
+ enum class State {
+ STOPPED,
+ RUNNING,
+ RUNNING_INITIAL,
+ DOWNLOADING,
+ PREPARING,
+ PAUSED,
+ PLAYING,
+ AWAIT_FOCUS,
+ FOCUSED,
+ DUCKED
+ }
+
+ enum class Event {
+ PLAY,
+ DOWNLOADED,
+ PREPARED,
+ STOP,
+ PAUSE,
+ ERROR,
+ FOCUS_LOST,
+ FOCUS_GAIN,
+ FOCUS_DUCK,
+ IMMEDIATE_TRANSITION,
+ }
+
+ private var pendingEvents = ArrayDeque()
+ private var isProcessing = false
+ private val stateMachine: StateMachine
+
+ /**
+ * Immediate state machine state. This attribute provides innermost active state.
+ * For checking parent states, use [PlayerStateMachine.isInState].
+ */
+ val state: State
+ get() {
+ return stateMachine.state
+ }
+
+ init {
+ val config = StateMachineConfig()
+
+ config.configure(State.STOPPED)
+ .permitIf(Event.PLAY, State.RUNNING_INITIAL) { delegate.hasEnqueuedFile }
+ .onEntryFrom(Event.STOP, delegate::onStopped)
+ .onEntryFrom(Event.ERROR, delegate::onError)
+
+ config.configure(State.RUNNING)
+ .permit(Event.STOP, State.STOPPED)
+ .permit(Event.ERROR, State.STOPPED)
+ .onEntry(delegate::onStartRunning)
+
+ config.configure(State.RUNNING_INITIAL)
+ .substateOf(State.RUNNING)
+ .permitIf(Event.IMMEDIATE_TRANSITION, State.DOWNLOADING, { !delegate.isDownloaded })
+ .permitIf(Event.IMMEDIATE_TRANSITION, State.PREPARING, { delegate.isDownloaded })
+ .onEntry(this::immediateTransition)
+
+ config.configure(State.DOWNLOADING)
+ .substateOf(State.RUNNING)
+ .permit(Event.DOWNLOADED, State.PREPARING)
+ .onEntry(delegate::onStartDownloading)
+
+ config.configure(State.PREPARING)
+ .substateOf(State.RUNNING)
+ .permitIf(Event.PREPARED, State.AWAIT_FOCUS) { delegate.isAutoplayEnabled }
+ .permitIf(Event.PREPARED, State.PAUSED) { !delegate.isAutoplayEnabled }
+ .onEntry(delegate::onPrepare)
+
+ config.configure(State.PLAYING)
+ .substateOf(State.RUNNING)
+ .permit(Event.PAUSE, State.PAUSED)
+ .permit(Event.FOCUS_LOST, State.PAUSED)
+ .onEntry(delegate::onRequestFocus)
+ .onExit(delegate::onReleaseFocus)
+
+ config.configure(State.PAUSED)
+ .substateOf(State.RUNNING)
+ .permit(Event.PLAY, State.AWAIT_FOCUS)
+ .onEntry(delegate::onPausePlayback)
+
+ config.configure(State.AWAIT_FOCUS)
+ .substateOf(State.PLAYING)
+ .permit(Event.FOCUS_GAIN, State.FOCUSED)
+
+ config.configure(State.FOCUSED)
+ .substateOf(State.PLAYING)
+ .permit(Event.FOCUS_DUCK, State.DUCKED)
+ .onEntry(this::onAudioFocusGain)
+
+ config.configure(State.DUCKED)
+ .substateOf(State.PLAYING)
+ .permit(Event.FOCUS_GAIN, State.FOCUSED)
+ .onEntry(Action { delegate.onAudioDuck(true) })
+ .onExit(Action { delegate.onAudioDuck(false) })
+
+ stateMachine = StateMachine(initialState, config)
+ stateMachine.onUnhandledTrigger { _, _ -> /* ignore unhandled event */ }
+ }
+
+ private fun immediateTransition() {
+ stateMachine.fire(Event.IMMEDIATE_TRANSITION)
+ }
+
+ private fun onAudioFocusGain(t: Transition) {
+ if (t.source == State.AWAIT_FOCUS) {
+ delegate.onStartPlayback()
+ }
+ }
+
+ /**
+ * Check if state machine is in a given state.
+ * Contrary to [PlayerStateMachine.state] attribute, this method checks for
+ * parent states.
+ */
+ fun isInState(state: State): Boolean {
+ return stateMachine.isInState(state)
+ }
+
+ /**
+ * Post state machine event to internal queue.
+ *
+ * This design ensures that we're not triggering multiple events
+ * from state machines callbacks before the transition is fully
+ * completed.
+ *
+ * Method is re-entrant.
+ */
+ fun post(event: Event) {
+ pendingEvents.addLast(event)
+ if (!isProcessing) {
+ isProcessing = true
+ while (pendingEvents.isNotEmpty()) {
+ val processedEvent = pendingEvents.removeFirst()
+ stateMachine.fire(processedEvent)
+ }
+ isProcessing = false
+ }
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/media/PlaylistItem.kt b/src/main/java/com/nextcloud/client/media/PlaylistItem.kt
new file mode 100644
index 000000000000..219a7c110ddf
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/media/PlaylistItem.kt
@@ -0,0 +1,6 @@
+package com.nextcloud.client.media
+
+import android.accounts.Account
+import com.owncloud.android.datamodel.OCFile
+
+data class PlaylistItem(val file: OCFile, val startPositionMs: Int, val autoPlay: Boolean, val account: Account)
diff --git a/src/main/java/com/owncloud/android/media/MediaControlView.java b/src/main/java/com/owncloud/android/media/MediaControlView.java
index 73201a0bc46f..c9ffe8659aff 100644
--- a/src/main/java/com/owncloud/android/media/MediaControlView.java
+++ b/src/main/java/com/owncloud/android/media/MediaControlView.java
@@ -55,17 +55,17 @@
*/
public class MediaControlView extends FrameLayout implements OnClickListener, OnSeekBarChangeListener {
private static final String TAG = MediaControlView.class.getSimpleName();
-
- private MediaPlayerControl mPlayer;
- private View mRoot;
- private ProgressBar mProgress;
- private TextView mEndTime;
- private TextView mCurrentTime;
- private boolean mDragging;
private static final int SHOW_PROGRESS = 1;
- private ImageButton mPauseButton;
- private ImageButton mFfwdButton;
- private ImageButton mRewButton;
+
+ private MediaPlayerControl playerControl;
+ private View root;
+ private ProgressBar progressBar;
+ private TextView endTime;
+ private TextView currentTime;
+ private boolean isDragging;
+ private ImageButton pauseButton;
+ private ImageButton forwardButton;
+ private ImageButton rewindButton;
public MediaControlView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -75,9 +75,9 @@ public MediaControlView(Context context, AttributeSet attrs) {
ViewGroup.LayoutParams.MATCH_PARENT
);
LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- mRoot = inflate.inflate(R.layout.media_control, null);
- initControllerView(mRoot);
- addView(mRoot, frameParams);
+ root = inflate.inflate(R.layout.media_control, null);
+ initControllerView(root);
+ addView(root, frameParams);
setFocusable(true);
setFocusableInTouchMode(true);
@@ -91,47 +91,50 @@ public void onFinishInflate() {
}
public void setMediaPlayer(MediaPlayerControl player) {
- mPlayer = player;
- mHandler.sendEmptyMessage(SHOW_PROGRESS);
- updatePausePlay();
+ playerControl = player;
+ handler.sendEmptyMessage(SHOW_PROGRESS);
+ handler.postDelayed(()-> {
+ updatePausePlay();
+ setProgress();
+ }, 100);
}
public void stopMediaPlayerMessages() {
- mHandler.removeMessages(SHOW_PROGRESS);
+ handler.removeMessages(SHOW_PROGRESS);
}
private void initControllerView(View v) {
- mPauseButton = v.findViewById(R.id.playBtn);
- if (mPauseButton != null) {
- mPauseButton.requestFocus();
- mPauseButton.setOnClickListener(this);
+ pauseButton = v.findViewById(R.id.playBtn);
+ if (pauseButton != null) {
+ pauseButton.requestFocus();
+ pauseButton.setOnClickListener(this);
}
- mFfwdButton = v.findViewById(R.id.forwardBtn);
- if (mFfwdButton != null) {
- mFfwdButton.setOnClickListener(this);
+ forwardButton = v.findViewById(R.id.forwardBtn);
+ if (forwardButton != null) {
+ forwardButton.setOnClickListener(this);
}
- mRewButton = v.findViewById(R.id.rewindBtn);
- if (mRewButton != null) {
- mRewButton.setOnClickListener(this);
+ rewindButton = v.findViewById(R.id.rewindBtn);
+ if (rewindButton != null) {
+ rewindButton.setOnClickListener(this);
}
- mProgress = v.findViewById(R.id.progressBar);
- if (mProgress != null) {
- if (mProgress instanceof SeekBar) {
- SeekBar seeker = (SeekBar) mProgress;
+ progressBar = v.findViewById(R.id.progressBar);
+ if (progressBar != null) {
+ if (progressBar instanceof SeekBar) {
+ SeekBar seeker = (SeekBar) progressBar;
ThemeUtils.colorHorizontalSeekBar(seeker, getContext());
seeker.setOnSeekBarChangeListener(this);
} else {
- ThemeUtils.colorHorizontalProgressBar(mProgress, ThemeUtils.primaryAccentColor(getContext()));
+ ThemeUtils.colorHorizontalProgressBar(progressBar, ThemeUtils.primaryAccentColor(getContext()));
}
- mProgress.setMax(1000);
+ progressBar.setMax(1000);
}
- mEndTime = v.findViewById(R.id.totalTimeText);
- mCurrentTime = v.findViewById(R.id.currentTimeText);
+ endTime = v.findViewById(R.id.totalTimeText);
+ currentTime = v.findViewById(R.id.currentTimeText);
}
/**
@@ -140,14 +143,14 @@ private void initControllerView(View v) {
*/
private void disableUnsupportedButtons() {
try {
- if (mPauseButton != null && !mPlayer.canPause()) {
- mPauseButton.setEnabled(false);
+ if (pauseButton != null && !playerControl.canPause()) {
+ pauseButton.setEnabled(false);
}
- if (mRewButton != null && !mPlayer.canSeekBackward()) {
- mRewButton.setEnabled(false);
+ if (rewindButton != null && !playerControl.canSeekBackward()) {
+ rewindButton.setEnabled(false);
}
- if (mFfwdButton != null && !mPlayer.canSeekForward()) {
- mFfwdButton.setEnabled(false);
+ if (forwardButton != null && !playerControl.canSeekForward()) {
+ forwardButton.setEnabled(false);
}
} catch (IncompatibleClassChangeError ex) {
// We were given an old version of the interface, that doesn't have
@@ -159,13 +162,14 @@ private void disableUnsupportedButtons() {
}
- private Handler mHandler = new Handler() {
+ private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
int pos;
if (msg.what == SHOW_PROGRESS) {
+ updatePausePlay();
pos = setProgress();
- if (!mDragging) {
+ if (!isDragging) {
msg = obtainMessage(SHOW_PROGRESS);
sendMessageDelayed(msg, 1000 - (pos % 1000));
}
@@ -173,7 +177,7 @@ public void handleMessage(Message msg) {
}
};
- private String stringForTime(int timeMs) {
+ private String formatTime(int timeMs) {
int totalSeconds = timeMs / 1000;
int seconds = totalSeconds % 60;
@@ -190,26 +194,27 @@ private String stringForTime(int timeMs) {
}
private int setProgress() {
- if (mPlayer == null || mDragging) {
+ if (playerControl == null || isDragging) {
return 0;
}
- int position = mPlayer.getCurrentPosition();
- int duration = mPlayer.getDuration();
- if (mProgress != null) {
+ int position = playerControl.getCurrentPosition();
+ int duration = playerControl.getDuration();
+ if (progressBar != null) {
if (duration > 0) {
// use long to avoid overflow
long pos = 1000L * position / duration;
- mProgress.setProgress((int) pos);
+ progressBar.setProgress((int) pos);
}
- int percent = mPlayer.getBufferPercentage();
- mProgress.setSecondaryProgress(percent * 10);
+ int percent = playerControl.getBufferPercentage();
+ progressBar.setSecondaryProgress(percent * 10);
}
- if (mEndTime != null) {
- mEndTime.setText(stringForTime(duration));
+ if (endTime != null) {
+ String endTime = duration > 0 ? formatTime(duration) : "--:--";
+ this.endTime.setText(endTime);
}
- if (mCurrentTime != null) {
- mCurrentTime.setText(stringForTime(position));
+ if (currentTime != null) {
+ currentTime.setText(formatTime(position));
}
return position;
}
@@ -226,21 +231,21 @@ public boolean dispatchKeyEvent(KeyEvent event) {
if (uniqueDown) {
doPauseResume();
//show(sDefaultTimeout);
- if (mPauseButton != null) {
- mPauseButton.requestFocus();
+ if (pauseButton != null) {
+ pauseButton.requestFocus();
}
}
return true;
} else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
- if (uniqueDown && !mPlayer.isPlaying()) {
- mPlayer.start();
+ if (uniqueDown && !playerControl.isPlaying()) {
+ playerControl.start();
updatePausePlay();
}
return true;
} else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
|| keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
- if (uniqueDown && mPlayer.isPlaying()) {
- mPlayer.pause();
+ if (uniqueDown && playerControl.isPlaying()) {
+ playerControl.pause();
updatePausePlay();
}
return true;
@@ -250,39 +255,54 @@ public boolean dispatchKeyEvent(KeyEvent event) {
}
public void updatePausePlay() {
- if (mRoot == null || mPauseButton == null) {
+ if (root == null || pauseButton == null) {
return;
}
- if (mPlayer.isPlaying()) {
- mPauseButton.setImageResource(android.R.drawable.ic_media_pause);
+ if (playerControl.isPlaying()) {
+ pauseButton.setImageResource(android.R.drawable.ic_media_pause);
} else {
- mPauseButton.setImageResource(android.R.drawable.ic_media_play);
+ pauseButton.setImageResource(android.R.drawable.ic_media_play);
}
+
+ final boolean canSeekFfd = playerControl.canSeekForward();
+ if (canSeekFfd) {
+ forwardButton.setVisibility(View.VISIBLE);
+ } else {
+ forwardButton.setVisibility(View.INVISIBLE);
+ }
+
+ final boolean canSeekBwd = playerControl.canSeekBackward();
+ if (canSeekBwd) {
+ rewindButton.setVisibility(View.VISIBLE);
+ } else {
+ rewindButton.setVisibility(View.INVISIBLE);
+ }
+
}
private void doPauseResume() {
- if (mPlayer.isPlaying()) {
- mPlayer.pause();
+ if (playerControl.isPlaying()) {
+ playerControl.pause();
} else {
- mPlayer.start();
+ playerControl.start();
}
updatePausePlay();
}
@Override
public void setEnabled(boolean enabled) {
- if (mPauseButton != null) {
- mPauseButton.setEnabled(enabled);
+ if (pauseButton != null) {
+ pauseButton.setEnabled(enabled);
}
- if (mFfwdButton != null) {
- mFfwdButton.setEnabled(enabled);
+ if (forwardButton != null) {
+ forwardButton.setEnabled(enabled);
}
- if (mRewButton != null) {
- mRewButton.setEnabled(enabled);
+ if (rewindButton != null) {
+ rewindButton.setEnabled(enabled);
}
- if (mProgress != null) {
- mProgress.setEnabled(enabled);
+ if (progressBar != null) {
+ progressBar.setEnabled(enabled);
}
disableUnsupportedButtons();
super.setEnabled(enabled);
@@ -291,7 +311,7 @@ public void setEnabled(boolean enabled) {
@Override
public void onClick(View v) {
int pos;
- boolean playing = mPlayer.isPlaying();
+ boolean playing = playerControl.isPlaying();
switch (v.getId()) {
case R.id.playBtn:
@@ -299,21 +319,21 @@ public void onClick(View v) {
break;
case R.id.rewindBtn:
- pos = mPlayer.getCurrentPosition();
+ pos = playerControl.getCurrentPosition();
pos -= 5000;
- mPlayer.seekTo(pos);
+ playerControl.seekTo(pos);
if (!playing) {
- mPlayer.pause(); // necessary in some 2.3.x devices
+ playerControl.pause(); // necessary in some 2.3.x devices
}
setProgress();
break;
case R.id.forwardBtn:
- pos = mPlayer.getCurrentPosition();
+ pos = playerControl.getCurrentPosition();
pos += 15000;
- mPlayer.seekTo(pos);
+ playerControl.seekTo(pos);
if (!playing) {
- mPlayer.pause(); // necessary in some 2.3.x devices
+ playerControl.pause(); // necessary in some 2.3.x devices
}
setProgress();
break;
@@ -329,11 +349,11 @@ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
return;
}
- long duration = mPlayer.getDuration();
+ long duration = playerControl.getDuration();
long newPosition = (duration * progress) / 1000L;
- mPlayer.seekTo((int) newPosition);
- if (mCurrentTime != null) {
- mCurrentTime.setText(stringForTime((int) newPosition));
+ playerControl.seekTo((int) newPosition);
+ if (currentTime != null) {
+ currentTime.setText(formatTime((int) newPosition));
}
}
@@ -344,8 +364,8 @@ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
*/
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
- mDragging = true; // monitors the duration of dragging
- mHandler.removeMessages(SHOW_PROGRESS); // grants no more updates with media player progress while dragging
+ isDragging = true; // monitors the duration of dragging
+ handler.removeMessages(SHOW_PROGRESS); // grants no more updates with media player progress while dragging
}
@@ -354,10 +374,10 @@ public void onStartTrackingTouch(SeekBar seekBar) {
*/
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
- mDragging = false;
+ isDragging = false;
setProgress();
updatePausePlay();
- mHandler.sendEmptyMessage(SHOW_PROGRESS); // grants future updates with media player progress
+ handler.sendEmptyMessage(SHOW_PROGRESS); // grants future updates with media player progress
}
@Override
diff --git a/src/main/java/com/owncloud/android/media/MediaService.java b/src/main/java/com/owncloud/android/media/MediaService.java
deleted file mode 100644
index afa8f59e04cc..000000000000
--- a/src/main/java/com/owncloud/android/media/MediaService.java
+++ /dev/null
@@ -1,716 +0,0 @@
-/*
- * ownCloud Android client application
- *
- * @author David A. Velasco
- * Copyright (C) 2016 ownCloud Inc.
- *
- * @author Tobias Kaminsky
- * Copyright (C) 2018 Tobias Kaminsky
- * Copyright (C) 2018 Nextcloud GmbH.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.owncloud.android.media;
-
-import android.accounts.Account;
-import android.accounts.AuthenticatorException;
-import android.accounts.OperationCanceledException;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.media.AudioManager;
-import android.media.MediaPlayer;
-import android.media.MediaPlayer.OnCompletionListener;
-import android.media.MediaPlayer.OnErrorListener;
-import android.media.MediaPlayer.OnPreparedListener;
-import android.net.wifi.WifiManager;
-import android.net.wifi.WifiManager.WifiLock;
-import android.os.AsyncTask;
-import android.os.IBinder;
-import android.os.PowerManager;
-import android.widget.Toast;
-
-import com.owncloud.android.R;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.StreamMediaFileOperation;
-import com.owncloud.android.lib.common.OwnCloudAccount;
-import com.owncloud.android.lib.common.OwnCloudClient;
-import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
-import com.owncloud.android.lib.common.accounts.AccountUtils;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.ui.activity.FileActivity;
-import com.owncloud.android.ui.activity.FileDisplayActivity;
-import com.owncloud.android.ui.notifications.NotificationUtils;
-import com.owncloud.android.utils.ThemeUtils;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-
-import androidx.core.app.NotificationCompat;
-import lombok.Getter;
-import lombok.Setter;
-
-
-/**
- * Service that handles media playback, both audio and video.
- *
- * Waits for Intents which signal the service to perform specific operations: Play, Pause,
- * Rewind, etc.
- */
-public class MediaService extends Service implements OnCompletionListener, OnPreparedListener,
- OnErrorListener, AudioManager.OnAudioFocusChangeListener {
-
- private static final String TAG = MediaService.class.getSimpleName();
-
- private static final String MY_PACKAGE = MediaService.class.getPackage() != null ?
- MediaService.class.getPackage().getName() : "com.owncloud.android.media";
-
- /// Intent actions that we are prepared to handle
- public static final String ACTION_PLAY_FILE = MY_PACKAGE + ".action.PLAY_FILE";
- public static final String ACTION_STOP_ALL = MY_PACKAGE + ".action.STOP_ALL";
-
- /// PreferenceKeys to add extras to the action
- public static final String EXTRA_FILE = MY_PACKAGE + ".extra.FILE";
- public static final String EXTRA_ACCOUNT = MY_PACKAGE + ".extra.ACCOUNT";
- public static final String EXTRA_START_POSITION = MY_PACKAGE + ".extra.START_POSITION";
- public static final String EXTRA_PLAY_ON_LOAD = MY_PACKAGE + ".extra.PLAY_ON_LOAD";
-
-
- /** Error code for specific messages - see regular error codes at {@link MediaPlayer} */
- public static final int OC_MEDIA_ERROR = 0;
-
- /** Time To keep the control panel visible when the user does not use it */
- public static final int MEDIA_CONTROL_SHORT_LIFE = 4000;
-
- /** Time To keep the control panel visible when the user does not use it */
- public static final int MEDIA_CONTROL_PERMANENT = 0;
-
- /** Volume to set when audio focus is lost and ducking is allowed */
- private static final float DUCK_VOLUME = 0.1f;
-
- /** Media player instance */
- @Getter private MediaPlayer player;
-
- /** Reference to the system AudioManager */
- private AudioManager audioManager;
-
-
- /** Values to indicate the state of the service */
- enum State {
- STOPPED,
- PREPARING,
- PLAYING,
- PAUSED
- }
-
- /** Current state */
- @Getter private State state = State.STOPPED;
-
- /** Possible focus values */
- enum AudioFocus {
- NO_FOCUS,
- NO_FOCUS_CAN_DUCK,
- FOCUS
- }
-
- /** Current focus state */
- private AudioFocus audioFocus = AudioFocus.NO_FOCUS;
-
- /** Wifi lock kept to prevents the device from shutting off the radio when streaming a file. */
- private WifiLock wifiLock;
-
- private static final String MEDIA_WIFI_LOCK_TAG = MY_PACKAGE + ".WIFI_LOCK";
-
- /** Notification to keep in the notification bar while a song is playing */
- private NotificationManager notificationManager;
-
- /** File being played */
- @Getter private OCFile currentFile;
-
- /** Account holding the file being played */
- private Account account;
-
- /** Flag signaling if the audio should be played immediately when the file is prepared */
- protected boolean playOnPrepared;
-
- /** Position, in milliseconds, where the audio should be started */
- private int startPosition;
-
- /** Interface to access the service through binding */
- private IBinder binder;
-
- /** Control panel shown to the user to control the playback, to register through binding */
- @Getter @Setter private MediaControlView mediaController;
-
- /** Notification builder to create notifications, new reuse way since Android 6 */
- private NotificationCompat.Builder notificationBuilder;
-
- /**
- * Helper method to get an error message suitable to show to users for errors occurred in media playback,
- *
- * @param context A context to access string resources.
- * @param what See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
- * @param extra See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
- * @return Message suitable to users.
- */
- public static String getMessageForMediaError(Context context, int what, int extra) {
- int messageId;
-
- if (what == OC_MEDIA_ERROR) {
- messageId = extra;
-
- } else if (extra == MediaPlayer.MEDIA_ERROR_UNSUPPORTED) {
- /* Added in API level 17
- Bitstream is conforming to the related coding standard or file spec,
- but the media framework does not support the feature.
- Constant Value: -1010 (0xfffffc0e)
- */
- messageId = R.string.media_err_unsupported;
-
- } else if (extra == MediaPlayer.MEDIA_ERROR_IO) {
- /* Added in API level 17
- File or network related operation errors.
- Constant Value: -1004 (0xfffffc14)
- */
- messageId = R.string.media_err_io;
-
- } else if (extra == MediaPlayer.MEDIA_ERROR_MALFORMED) {
- /* Added in API level 17
- Bitstream is not conforming to the related coding standard or file spec.
- Constant Value: -1007 (0xfffffc11)
- */
- messageId = R.string.media_err_malformed;
-
- } else if (extra == MediaPlayer.MEDIA_ERROR_TIMED_OUT) {
- /* Added in API level 17
- Some operation takes too long to complete, usually more than 3-5 seconds.
- Constant Value: -110 (0xffffff92)
- */
- messageId = R.string.media_err_timeout;
-
- } else if (what == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
- /* Added in API level 3
- The video is streamed and its container is not valid for progressive playback i.e the video's index
- (e.g moov atom) is not at the start of the file.
- Constant Value: 200 (0x000000c8)
- */
- messageId = R.string.media_err_invalid_progressive_playback;
-
- } else {
- /* MediaPlayer.MEDIA_ERROR_UNKNOWN
- Added in API level 1
- Unspecified media player error.
- Constant Value: 1 (0x00000001)
- */
- /* MediaPlayer.MEDIA_ERROR_SERVER_DIED)
- Added in API level 1
- Media server died. In this case, the application must release the MediaPlayer
- object and instantiate a new one.
- Constant Value: 100 (0x00000064)
- */
- messageId = R.string.media_err_unknown;
- }
- return context.getString(messageId);
- }
-
-
- /**
- * Initialize a service instance
- *
- * {@inheritDoc}
- */
- @Override
- public void onCreate() {
- super.onCreate();
- Log_OC.d(TAG, "Creating ownCloud media service");
-
- wifiLock = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE)).
- createWifiLock(WifiManager.WIFI_MODE_FULL, MEDIA_WIFI_LOCK_TAG);
-
- notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- notificationBuilder = new NotificationCompat.Builder(this);
- notificationBuilder.setColor(ThemeUtils.primaryColor(this));
- audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
- binder = new MediaServiceBinder(this);
- }
-
-
- /**
- * Entry point for Intents requesting actions, sent here via startService.
- *
- * {@inheritDoc}
- */
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- String action = intent.getAction();
- if (ACTION_PLAY_FILE.equals(action)) {
- processPlayFileRequest(intent);
- } else if (ACTION_STOP_ALL.equals(action)) {
- processStopRequest(true);
- }
-
- return START_NOT_STICKY; // don't want it to restart in case it's killed.
- }
-
-
- /**
- * Processes a request to play a media file received as a parameter
- *
- * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
- *
- * @param intent Intent received in the request with the data to identify the file to play.
- */
- private void processPlayFileRequest(Intent intent) {
- if (state != State.PREPARING) {
- currentFile = intent.getExtras().getParcelable(EXTRA_FILE);
- account = intent.getExtras().getParcelable(EXTRA_ACCOUNT);
- playOnPrepared = intent.getExtras().getBoolean(EXTRA_PLAY_ON_LOAD, false);
- startPosition = intent.getExtras().getInt(EXTRA_START_POSITION, 0);
- tryToGetAudioFocus();
- playMedia();
- }
- }
-
-
- /**
- * Processes a request to play a media file.
- */
- protected void processPlayRequest() {
- // request audio focus
- tryToGetAudioFocus();
-
- // actually play the song
- if (state == State.STOPPED) {
- // (re)start playback
- playMedia();
-
- } else if (state == State.PAUSED) {
- // continue playback
- state = State.PLAYING;
- setUpAsForeground(String.format(getString(R.string.media_state_playing), currentFile.getFileName()));
- configAndStartMediaPlayer();
- }
- }
-
-
- /**
- * Makes sure the media player exists and has been reset. This will create the media player
- * if needed. reset the existing media player if one already exists.
- */
- protected void createMediaPlayerIfNeeded() {
- if (player == null) {
- player = new MediaPlayer();
-
- // make sure the CPU won't go to sleep while media is playing
- player.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
-
- // the media player will notify the service when it's ready preparing, and when it's done playing
- player.setOnPreparedListener(this);
- player.setOnCompletionListener(this);
- player.setOnErrorListener(this);
-
- } else {
- player.reset();
- }
- }
-
- /**
- * Processes a request to pause the current playback
- */
- protected void processPauseRequest() {
- if (state == State.PLAYING) {
- state = State.PAUSED;
- player.pause();
- releaseResources(false); // retain media player in pause
- // TODO polite audio focus, instead of keep it owned; or not?
- }
- }
-
-
- /**
- * Processes a request to stop the playback.
- *
- * @param force When 'true', the playback is stopped no matter the value of state
- */
- protected void processStopRequest(boolean force) {
- if (state != State.PREPARING || force) {
- state = State.STOPPED;
- currentFile = null;
- account = null;
- releaseResources(true);
- giveUpAudioFocus();
- stopSelf(); // service is no longer necessary
- }
- }
-
-
- /**
- * Releases resources used by the service for playback. This includes the "foreground service"
- * status and notification, the wake locks and possibly the MediaPlayer.
- *
- * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
- */
- protected void releaseResources(boolean releaseMediaPlayer) {
- // stop being a foreground service
- stopForeground(true);
-
- // stop and release the Media Player, if it's available
- if (releaseMediaPlayer && player != null) {
- player.reset();
- player.release();
- player = null;
- }
-
- // release the Wifi lock, if holding it
- if (wifiLock.isHeld()) {
- wifiLock.release();
- }
- }
-
- /**
- * Fully releases the audio focus.
- */
- private void giveUpAudioFocus() {
- if (audioFocus == AudioFocus.FOCUS
- && audioManager != null
- && AudioManager.AUDIOFOCUS_REQUEST_GRANTED == audioManager.abandonAudioFocus(this)) {
-
- audioFocus = AudioFocus.NO_FOCUS;
- }
- }
-
-
- /**
- * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
- */
- protected void configAndStartMediaPlayer() {
- if (player == null) {
- throw new IllegalStateException("player is NULL");
- }
-
- if (audioFocus == AudioFocus.NO_FOCUS) {
- if (player.isPlaying()) {
- player.pause(); // have to be polite; but state is not changed, to resume when focus is received again
- }
-
- } else {
- if (audioFocus == AudioFocus.NO_FOCUS_CAN_DUCK) {
- player.setVolume(DUCK_VOLUME, DUCK_VOLUME);
-
- } else {
- player.setVolume(1.0f, 1.0f); // full volume
- }
-
- if (!player.isPlaying()) {
- player.start();
- }
- }
- }
-
-
- /**
- * Requests the audio focus to the Audio Manager
- */
- private void tryToGetAudioFocus() {
- if (audioFocus != AudioFocus.FOCUS
- && audioManager != null
- && AudioManager.AUDIOFOCUS_REQUEST_GRANTED == audioManager.requestAudioFocus(this,
- AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN)
- ) {
- audioFocus = AudioFocus.FOCUS;
- }
- }
-
-
- /**
- * Starts playing the current media file.
- */
- protected void playMedia() {
- state = State.STOPPED;
- releaseResources(false); // release everything except MediaPlayer
-
- try {
- if (currentFile == null) {
- Toast.makeText(this, R.string.media_err_nothing_to_play, Toast.LENGTH_LONG).show();
- processStopRequest(true);
- return;
-
- } else if (account == null) {
- Toast.makeText(this, R.string.media_err_not_in_owncloud, Toast.LENGTH_LONG).show();
- processStopRequest(true);
- return;
- }
-
- createMediaPlayerIfNeeded();
- player.setAudioStreamType(AudioManager.STREAM_MUSIC);
-
- if (currentFile.isDown()) {
- player.setDataSource(currentFile.getStoragePath());
- preparePlayer();
- } else {
- OwnCloudAccount ocAccount = new OwnCloudAccount(account, getBaseContext());
- OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton().
- getClientFor(ocAccount, getBaseContext());
-
- new LoadStreamUrl(this, client).execute(currentFile.getLocalId());
- }
- } catch (AccountUtils.AccountNotFoundException | OperationCanceledException | AuthenticatorException e) {
- Log_OC.e(TAG, "Loading stream url not possible: " + e.getMessage());
- } catch (SecurityException | IOException | IllegalStateException | IllegalArgumentException e) {
- Log_OC.e(TAG, e.getClass().getSimpleName() + " playing " + account.name + currentFile.getRemotePath(), e);
- Toast.makeText(this, String.format(getString(R.string.media_err_playing), currentFile.getFileName()),
- Toast.LENGTH_LONG).show();
- processStopRequest(true);
- }
- }
-
- private void preparePlayer() {
- state = State.PREPARING;
- setUpAsForeground(String.format(getString(R.string.media_state_loading), currentFile.getFileName()));
-
- // starts preparing the media player in background
- player.prepareAsync();
- }
-
- /** Called when media player is done playing current song. */
- public void onCompletion(MediaPlayer player) {
- Toast.makeText(this, String.format(getString(R.string.media_event_done), currentFile.getFileName()), Toast.LENGTH_LONG).show();
- if (mediaController != null) {
- // somebody is still bound to the service
- player.seekTo(0);
- processPauseRequest();
- mediaController.updatePausePlay();
- } else {
- // nobody is bound
- processStopRequest(true);
- }
- }
-
-
- /**
- * Called when media player is done preparing.
- *
- * Time to start.
- */
- public void onPrepared(MediaPlayer player) {
- state = State.PLAYING;
- updateNotification(String.format(getString(R.string.media_state_playing), currentFile.getFileName()));
- if (mediaController != null) {
- mediaController.setEnabled(true);
- }
- player.seekTo(startPosition);
- configAndStartMediaPlayer();
- if (!playOnPrepared) {
- processPauseRequest();
- }
-
- if (mediaController != null) {
- mediaController.updatePausePlay();
- }
- }
-
-
- /**
- * Updates the status notification
- */
- private void updateNotification(String content) {
- String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
-
- // TODO check if updating the Intent is really necessary
- Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class);
- showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, currentFile);
- showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, account);
- showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
- notificationBuilder.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
- (int) System.currentTimeMillis(),
- showDetailsIntent,
- PendingIntent.FLAG_UPDATE_CURRENT));
- notificationBuilder.setWhen(System.currentTimeMillis());
- notificationBuilder.setTicker(ticker);
- notificationBuilder.setContentTitle(ticker);
- notificationBuilder.setContentText(content);
-
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- notificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA);
- }
-
- notificationManager.notify(R.string.media_notif_ticker, notificationBuilder.build());
- }
-
-
- /**
- * Configures the service as a foreground service.
- *
- * The system will avoid finishing the service as much as possible when resources as low.
- *
- * A notification must be created to keep the user aware of the existence of the service.
- */
- private void setUpAsForeground(String content) {
- String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
-
- /// creates status notification
- // TODO put a progress bar to follow the playback progress
- notificationBuilder.setSmallIcon(R.drawable.ic_play_arrow);
- //mNotification.tickerText = text;
- notificationBuilder.setWhen(System.currentTimeMillis());
- notificationBuilder.setOngoing(true);
-
- /// includes a pending intent in the notification showing the details view of the file
- Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class);
- showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, currentFile);
- showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, account);
- showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- notificationBuilder.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
- (int) System.currentTimeMillis(),
- showDetailsIntent,
- PendingIntent.FLAG_UPDATE_CURRENT));
- notificationBuilder.setContentTitle(ticker);
- notificationBuilder.setContentText(content);
-
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- notificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA);
- }
-
- startForeground(R.string.media_notif_ticker, notificationBuilder.build());
- }
-
- /**
- * Called when there's an error playing media.
- *
- * Warns the user about the error and resets the media player.
- */
- public boolean onError(MediaPlayer mp, int what, int extra) {
- Log_OC.e(TAG, "Error in audio playback, what = " + what + ", extra = " + extra);
-
- String message = getMessageForMediaError(this, what, extra);
- Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
-
- processStopRequest(true);
- return true;
- }
-
- /**
- * Called by the system when another app tries to play some sound.
- *
- * {@inheritDoc}
- */
- @Override
- public void onAudioFocusChange(int focusChange) {
- if (focusChange > 0) {
- // focus gain; check AudioManager.AUDIOFOCUS_* values
- audioFocus = AudioFocus.FOCUS;
- // restart media player with new focus settings
- if (state == State.PLAYING) {
- configAndStartMediaPlayer();
- }
-
- } else if (focusChange < 0) {
- // focus loss; check AudioManager.AUDIOFOCUS_* values
- boolean canDuck = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK == focusChange;
- audioFocus = canDuck ? AudioFocus.NO_FOCUS_CAN_DUCK : AudioFocus.NO_FOCUS;
- // start/restart/pause media player with new focus settings
- if (player != null && player.isPlaying()) {
- configAndStartMediaPlayer();
- }
- }
-
- }
-
- /**
- * Called when the service is finished for final clean-up.
- *
- * {@inheritDoc}
- */
- @Override
- public void onDestroy() {
- state = State.STOPPED;
- releaseResources(true);
- giveUpAudioFocus();
- stopForeground(true);
- super.onDestroy();
- }
-
-
- /**
- * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
- */
- @Override
- public IBinder onBind(Intent arg) {
- return binder;
- }
-
-
- /**
- * Called when ALL the bound clients were onbound.
- *
- * The service is destroyed if playback stopped or paused
- */
- @Override
- public boolean onUnbind(Intent intent) {
- if (state == State.PAUSED || state == State.STOPPED) {
- processStopRequest(false);
- }
- return false; // not accepting rebinding (default behaviour)
- }
-
- private static class LoadStreamUrl extends AsyncTask {
-
- private OwnCloudClient client;
- private WeakReference mediaServiceWeakReference;
-
- public LoadStreamUrl(MediaService mediaService, OwnCloudClient client) {
- this.client = client;
- this.mediaServiceWeakReference = new WeakReference<>(mediaService);
- }
-
- @Override
- protected String doInBackground(String... fileId) {
- StreamMediaFileOperation sfo = new StreamMediaFileOperation(fileId[0]);
- RemoteOperationResult result = sfo.execute(client);
-
- if (!result.isSuccess()) {
- return null;
- }
-
- return (String) result.getData().get(0);
- }
-
- @Override
- protected void onPostExecute(String url) {
- MediaService mediaService = mediaServiceWeakReference.get();
-
- if (mediaService != null && mediaService.getCurrentFile() != null) {
- if (url != null) {
- try {
- mediaService.player.setDataSource(url);
-
- // prevent the Wifi from going to sleep when streaming
- mediaService.wifiLock.acquire();
- mediaService.preparePlayer();
- } catch (IOException e) {
- Log_OC.e(TAG, "Streaming not possible: " + e.getMessage());
- }
- } else {
- // we already show a toast with error from media player
- mediaService.processStopRequest(true);
- }
- }
- }
- }
-}
diff --git a/src/main/java/com/owncloud/android/media/MediaServiceBinder.java b/src/main/java/com/owncloud/android/media/MediaServiceBinder.java
deleted file mode 100644
index 2fd1e54c90ff..000000000000
--- a/src/main/java/com/owncloud/android/media/MediaServiceBinder.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author David A. Velasco
- * Copyright (C) 2016 ownCloud Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.owncloud.android.media;
-
-
-import android.accounts.Account;
-import android.content.Intent;
-import android.media.MediaPlayer;
-import android.os.Binder;
-import android.widget.MediaController;
-
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.media.MediaService.State;
-
-
-/**
- * Binder allowing client components to perform operations on on the MediaPlayer managed by a MediaService instance.
- *
- * Provides the operations of {@link MediaController.MediaPlayerControl}, and an extra method to check if
- * an {@link OCFile} instance is handled by the MediaService.
- */
-public class MediaServiceBinder extends Binder implements MediaController.MediaPlayerControl {
-
- private static final String TAG = MediaServiceBinder.class.getSimpleName();
- /**
- * {@link MediaService} instance to access with the binder
- */
- private MediaService mService;
-
- /**
- * Public constructor
- *
- * @param service A {@link MediaService} instance to access with the binder
- */
- public MediaServiceBinder(MediaService service) {
- if (service == null) {
- throw new IllegalArgumentException("Argument 'service' can not be null");
- }
- mService = service;
- }
-
- public boolean isPlaying(OCFile mFile) {
- return mFile != null && mFile.equals(mService.getCurrentFile());
- }
-
- @Override
- public boolean canPause() {
- return true;
- }
-
- @Override
- public boolean canSeekBackward() {
- return true;
- }
-
- @Override
- public boolean canSeekForward() {
- return true;
- }
-
- @Override
- public int getBufferPercentage() {
- MediaPlayer currentPlayer = mService.getPlayer();
- if (currentPlayer != null) {
- return 100;
- // TODO update for streamed playback; add OnBufferUpdateListener in MediaService
- } else {
- return 0;
- }
- }
-
- @Override
- public int getCurrentPosition() {
- MediaPlayer currentPlayer = mService.getPlayer();
- if (currentPlayer != null) {
- return currentPlayer.getCurrentPosition();
- } else {
- return 0;
- }
- }
-
- @Override
- public int getDuration() {
- MediaPlayer currentPlayer = mService.getPlayer();
- if (currentPlayer != null) {
- return currentPlayer.getDuration();
- } else {
- return 0;
- }
- }
-
-
- /**
- * Reports if the MediaService is playing a file or not.
- *
- * Considers that the file is being played when it is in preparation because the expected
- * client of this method is a {@link MediaController} , and we do not want that the 'play'
- * button is shown when the file is being prepared by the MediaService.
- */
- @Override
- public boolean isPlaying() {
- MediaService.State currentState = mService.getState();
- return currentState == State.PLAYING || (currentState == State.PREPARING && mService.playOnPrepared);
- }
-
-
- @Override
- public void pause() {
- Log_OC.d(TAG, "Pausing through binder...");
- mService.processPauseRequest();
- }
-
- @Override
- public void seekTo(int pos) {
- Log_OC.d(TAG, "Seeking " + pos + " through binder...");
- MediaPlayer currentPlayer = mService.getPlayer();
- MediaService.State currentState = mService.getState();
- if (currentPlayer != null && currentState != State.PREPARING && currentState != State.STOPPED) {
- currentPlayer.seekTo(pos);
- }
- }
-
- @Override
- public void start() {
- Log_OC.d(TAG, "Starting through binder...");
- mService.processPlayRequest(); // this will finish the service if there is no file preloaded to play
- }
-
- public void start(Account account, OCFile file, boolean playImmediately, int position) {
- Log_OC.d(TAG, "Loading and starting through binder...");
- Intent i = new Intent(mService, MediaService.class);
- i.putExtra(MediaService.EXTRA_ACCOUNT, account);
- i.putExtra(MediaService.EXTRA_FILE, file);
- i.putExtra(MediaService.EXTRA_PLAY_ON_LOAD, playImmediately);
- i.putExtra(MediaService.EXTRA_START_POSITION, position);
- i.setAction(MediaService.ACTION_PLAY_FILE);
- mService.startService(i);
- }
-
-
- public void registerMediaController(MediaControlView mediaController) {
- mService.setMediaController(mediaController);
- }
-
- public void unregisterMediaController(MediaControlView mediaController) {
- if (mediaController != null && mediaController == mService.getMediaController()) {
- mService.setMediaController(null);
- }
-
- }
-
- public boolean isInPlaybackState() {
- MediaService.State currentState = mService.getState();
- return currentState == MediaService.State.PLAYING || currentState == MediaService.State.PAUSED;
- }
-
- @Override
- public int getAudioSessionId() {
- return 1; // not really used
- }
-}
-
-
diff --git a/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
index a8381c81a663..7bb1c491aa10 100644
--- a/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
+++ b/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
@@ -56,6 +56,7 @@
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.client.appinfo.AppInfo;
import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.media.PlayerServiceConnection;
import com.nextcloud.client.network.ConnectivityService;
import com.nextcloud.client.preferences.AppPreferences;
import com.owncloud.android.MainApp;
@@ -77,8 +78,6 @@
import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
-import com.owncloud.android.media.MediaService;
-import com.owncloud.android.media.MediaServiceBinder;
import com.owncloud.android.operations.CopyFileOperation;
import com.owncloud.android.operations.CreateFolderOperation;
import com.owncloud.android.operations.CreateShareViaLinkOperation;
@@ -207,9 +206,6 @@ public class FileDisplayActivity extends FileActivity
private Collection