Skip to content

Commit

Permalink
Use Activity Result APIs for External player
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxr1998 committed Mar 24, 2021
1 parent e2f134c commit dafc618
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 44 deletions.
60 changes: 32 additions & 28 deletions app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ import android.content.Intent
import android.net.Uri
import android.webkit.JavascriptInterface
import android.widget.Toast
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.LifecycleOwner
import org.jellyfin.mobile.AppPreferences
import org.jellyfin.mobile.R
import org.jellyfin.mobile.fragment.WebViewFragment
import org.jellyfin.mobile.player.source.JellyfinMediaSource
import org.jellyfin.mobile.settings.ExternalPlayerPackage
import org.jellyfin.mobile.settings.VideoPlayerType
import org.jellyfin.mobile.utils.Constants
import org.jellyfin.mobile.utils.isPackageInstalled
import org.jellyfin.mobile.utils.runOnUiThread
import org.jellyfin.mobile.utils.toast
import org.jellyfin.mobile.webapp.WebappFunctionChannel
import org.json.JSONException
Expand All @@ -24,12 +25,36 @@ import org.koin.core.KoinComponent
import org.koin.core.inject
import timber.log.Timber

class ExternalPlayer(private val fragment: WebViewFragment) : KoinComponent {
private val context: Context = fragment.requireContext()
class ExternalPlayer(
private val context: Context,
lifecycleOwner: LifecycleOwner,
registry: ActivityResultRegistry,
) : KoinComponent {

private val appPreferences: AppPreferences by inject()
private val webappFunctionChannel: WebappFunctionChannel by inject()

private val playerContract = registry.register("externalplayer", lifecycleOwner, ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val intent = result.data
when (val action = intent?.action) {
Constants.MPV_PLAYER_RESULT_ACTION -> handleMPVPlayer(resultCode, intent)
Constants.MX_PLAYER_RESULT_ACTION -> handleMXPlayer(resultCode, intent)
Constants.VLC_PLAYER_RESULT_ACTION -> handleVLCPlayer(resultCode, intent)
else -> {
if (action != null && resultCode != Activity.RESULT_CANCELED) {
Timber.d("Unknown action $action [resultCode: $resultCode]")
notifyEvent(Constants.EVENT_CANCELED)
context.toast(R.string.external_player_not_supported_yet, Toast.LENGTH_LONG)
} else {
Timber.d("Playback canceled [no player selected or player without action result]")
notifyEvent(Constants.EVENT_CANCELED)
context.toast(R.string.external_player_invalid_player, Toast.LENGTH_LONG)
}
}
}
}

@JavascriptInterface
fun isEnabled() = appPreferences.videoPlayerType == VideoPlayerType.EXTERNAL_PLAYER

Expand All @@ -39,7 +64,7 @@ class ExternalPlayer(private val fragment: WebViewFragment) : KoinComponent {
val mediaSource = JellyfinMediaSource(JSONObject(args))
if (mediaSource.playMethod == "DirectStream") {
val playerIntent = Intent(Intent.ACTION_VIEW).apply {
if (fragment.isPackageInstalled(appPreferences.externalPlayerApp)) {
if (context.packageManager.isPackageInstalled(appPreferences.externalPlayerApp)) {
component = getComponent(appPreferences.externalPlayerApp)
}
setDataAndType(mediaSource.uri, mediaSource.mimeType)
Expand All @@ -56,7 +81,7 @@ class ExternalPlayer(private val fragment: WebViewFragment) : KoinComponent {
putExtra("subs.enable", arrayOf(Uri.parse(selectedTrack)))
}
}
fragment.startActivityForResult(playerIntent, Constants.HANDLE_EXTERNAL_PLAYER)
playerContract.launch(playerIntent)
Timber.d("Starting playback [id: ${mediaSource.id}, title: ${mediaSource.title}, playMethod: ${mediaSource.playMethod}, mediaStartMs: ${mediaSource.mediaStartMs}]")
} else {
Timber.d("Play Method '${mediaSource.playMethod}' not tested, ignoring...")
Expand All @@ -70,28 +95,7 @@ class ExternalPlayer(private val fragment: WebViewFragment) : KoinComponent {

private fun notifyEvent(event: String, parameters: String = "") {
if (event in arrayOf(Constants.EVENT_CANCELED, Constants.EVENT_ENDED, Constants.EVENT_TIME_UPDATE) && parameters == parameters.filter { it.isDigit() }) {
fragment.runOnUiThread {
webappFunctionChannel.call("window.ExtPlayer.notify$event($parameters)")
}
}
}

fun handleActivityResult(resultCode: Int, data: Intent?) {
when (data?.action) {
Constants.MPV_PLAYER_RESULT_ACTION -> handleMPVPlayer(resultCode, data)
Constants.MX_PLAYER_RESULT_ACTION -> handleMXPlayer(resultCode, data)
Constants.VLC_PLAYER_RESULT_ACTION -> handleVLCPlayer(resultCode, data)
else -> {
if (data?.action != null && resultCode != Activity.RESULT_CANCELED) {
Timber.d("Unknown action [resultCode: $resultCode, action: ${data.action}]")
notifyEvent(Constants.EVENT_CANCELED)
context.toast(R.string.external_player_not_supported_yet, Toast.LENGTH_LONG)
} else {
Timber.d("Playback canceled [no player selected or player without action result]")
notifyEvent(Constants.EVENT_CANCELED)
context.toast(R.string.external_player_invalid_player, Toast.LENGTH_LONG)
}
}
webappFunctionChannel.call("window.ExtPlayer.notify$event($parameters)")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class WebViewFragment : Fragment(), NativePlayerHost {
val apiClient: ApiClient by inject()
private val serverController: ServerController by inject()
private val webappFunctionChannel: WebappFunctionChannel by inject()
private val externalPlayer by lazy { ExternalPlayer(this) }
private lateinit var externalPlayer: ExternalPlayer

private var serverId: Long = 0
private lateinit var instanceUrl: String
Expand All @@ -67,6 +67,9 @@ class WebViewFragment : Fragment(), NativePlayerHost {
val args = requireArguments()
serverId = requireNotNull(args.getLong(FRAGMENT_WEB_VIEW_EXTRA_SERVER_ID)) { "Server id has not been supplied!" }
instanceUrl = requireNotNull(args.getString(FRAGMENT_WEB_VIEW_EXTRA_URL)) { "Server url has not been supplied!" }

externalPlayer = ExternalPlayer(requireContext(), this, requireActivity().activityResultRegistry)

requireActivity().onBackPressedDispatcher.addCallback(this) {
if (!connected || !webappFunctionChannel.goBack()) {
isEnabled = false
Expand Down Expand Up @@ -276,11 +279,4 @@ class WebViewFragment : Fragment(), NativePlayerHost {
addToBackStack(null)
}.commit()
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == Constants.HANDLE_EXTERNAL_PLAYER) {
externalPlayer.handleActivityResult(resultCode, data)
}
}
}
15 changes: 13 additions & 2 deletions app/src/main/java/org/jellyfin/mobile/settings/SettingsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,24 @@ class SettingsFragment : Fragment() {
titleRes = R.string.pref_exoplayer_allow_background_audio
enabled = appPreferences.videoPlayerType == VideoPlayerType.EXO_PLAYER
}

// Generate available external player options
val packageManager = requireContext().packageManager
val externalPlayerOptions = listOf(
SelectionItem(ExternalPlayerPackage.SYSTEM_DEFAULT, R.string.external_player_system_default, R.string.external_player_system_default_description),
SelectionItem(ExternalPlayerPackage.MPV_PLAYER, R.string.external_player_mpv, R.string.external_player_mpv_description),
SelectionItem(ExternalPlayerPackage.MX_PLAYER_FREE, R.string.external_player_mx_player_free, R.string.external_player_mx_player_free_description),
SelectionItem(ExternalPlayerPackage.MX_PLAYER_PRO, R.string.external_player_mx_player_pro, R.string.external_player_mx_player_pro_description),
SelectionItem(ExternalPlayerPackage.VLC_PLAYER, R.string.external_player_vlc_player, R.string.external_player_vlc_player_description),
).filter { isPackageInstalled(it.key) }.plus(SelectionItem(ExternalPlayerPackage.SYSTEM_DEFAULT, R.string.external_player_system_default, R.string.external_player_system_default_description))
if (!isPackageInstalled(appPreferences.externalPlayerApp)) appPreferences.externalPlayerApp = ExternalPlayerPackage.SYSTEM_DEFAULT
).filter { item ->
item.key == ExternalPlayerPackage.SYSTEM_DEFAULT || packageManager.isPackageInstalled(item.key)
}

// Revert if current selection isn't available
if (!packageManager.isPackageInstalled(appPreferences.externalPlayerApp)) {
appPreferences.externalPlayerApp = ExternalPlayerPackage.SYSTEM_DEFAULT
}

externalPlayerChoicePreference = singleChoice(Constants.PREF_EXTERNAL_PLAYER_APP, externalPlayerOptions) {
titleRes = R.string.external_player_app
enabled = appPreferences.videoPlayerType == VideoPlayerType.EXTERNAL_PLAYER
Expand Down
3 changes: 0 additions & 3 deletions app/src/main/java/org/jellyfin/mobile/utils/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,6 @@ object Constants {
// Video player intent extras
const val EXTRA_MEDIA_SOURCE_ITEM = "org.jellyfin.mobile.MEDIA_SOURCE_ITEM"

// External player result code
const val HANDLE_EXTERNAL_PLAYER = 1

// External player result actions
const val MPV_PLAYER_RESULT_ACTION = "is.xyz.mpv.MPVActivity.result"
const val MX_PLAYER_RESULT_ACTION = "com.mxtech.intent.result.VIEW"
Expand Down
5 changes: 2 additions & 3 deletions app/src/main/java/org/jellyfin/mobile/utils/SystemUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import android.provider.Settings
import android.provider.Settings.System.ACCELEROMETER_ROTATION
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
Expand Down Expand Up @@ -121,8 +120,8 @@ private fun Context.downloadFile(request: DownloadManager.Request, @DownloadMeth

fun Activity.isAutoRotateOn() = Settings.System.getInt(contentResolver, ACCELEROMETER_ROTATION, 0) == 1

fun Fragment.isPackageInstalled(@ExternalPlayerPackage packageName: String) = try {
packageName.isNotEmpty() && requireContext().packageManager.getApplicationInfo(packageName, 0).enabled
fun PackageManager.isPackageInstalled(@ExternalPlayerPackage packageName: String) = try {
packageName.isNotEmpty() && getApplicationInfo(packageName, 0).enabled
} catch (e: PackageManager.NameNotFoundException) {
false
}
Expand Down

0 comments on commit dafc618

Please sign in to comment.