diff --git a/.github/workflows/pre_release.yml b/.github/workflows/pre_release.yml index 8d9ef8c7..6548cbe2 100644 --- a/.github/workflows/pre_release.yml +++ b/.github/workflows/pre_release.yml @@ -109,4 +109,4 @@ jobs: Commit details [here](${{ github.event.head_commit.url }}) run: | ESCAPED=`python3 -c 'import json,os,urllib.parse; print(urllib.parse.quote(json.dumps(os.environ["COMMIT_MESSAGE"])))'` - curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2Fpre_release_arm64_v8%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2Fpre_release_arm_v7%22%2C%22parse_mode%22%3A%22MarkdownV2%22%2C%22caption%22%3A${ESCAPED}%7D%5D" -F pre_release_arm64_v8="@$PRE_RELEASE_ARM64_V8" -F pre_release_arm_v7="@$PRE_RELEASE_ARM_V7" + curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2Fpre_release_arm64_v8%22%2C%22parse_mode%22%3A%22MarkdownV2%22%2C%22caption%22%3A${ESCAPED}%7D%5D" -F pre_release_arm64_v8="@$PRE_RELEASE_ARM64_V8" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d61c72c7..d4a83fe6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,7 +21,7 @@ android { minSdk = 24 targetSdk = 34 versionCode = 16 - versionName = "1.1-beta20" + versionName = "1.1-beta24" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -92,6 +92,12 @@ android { ) } } + androidResources { + ignoreAssetsPatterns += listOf( + "subfont.ttf", // mpv-android + "cacert.pem", // mpv-android + ) + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -116,7 +122,17 @@ android { "kotlin-tooling-metadata.json", "okhttp3/internal/publicsuffix/NOTICE", ) + jniLibs.excludes += mutableSetOf( + "lib/*/libffmpegkit.so", // mpv-android + "lib/*/libffmpegkit_abidetect.so", // mpv-android + ) + dex { + // Set to "true" because android:extractNativeLibs + // is set to "true" in AndroidManifest.xml + useLegacyPackaging = true + } } +// ndkVersion = "26.3.11579264" } tasks.withType(KotlinCompile::class.java).configureEach { @@ -139,51 +155,52 @@ tasks.withType(KotlinCompile::class.java).configureEach { dependencies { - implementation("androidx.core:core-ktx:1.13.0") + implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha13") implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") implementation("androidx.navigation:navigation-compose:2.7.7") implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") - implementation("androidx.compose.ui:ui:1.6.6") - implementation("androidx.compose.material:material:1.6.6") + implementation("androidx.compose.ui:ui:1.6.7") + implementation("androidx.compose.material:material:1.6.7") implementation("androidx.compose.material3:material3:1.3.0-alpha05") implementation("androidx.compose.material3:material3-window-size-class:1.2.1") - implementation("androidx.compose.material:material-icons-extended:1.6.6") + implementation("androidx.compose.material:material-icons-extended:1.6.7") implementation("com.materialkolor:material-kolor:1.4.4") implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-ktx:2.6.1") implementation("androidx.room:room-paging:2.6.1") ksp("androidx.room:room-compiler:2.6.1") implementation("androidx.work:work-runtime-ktx:2.9.0") - implementation("androidx.datastore:datastore-preferences:1.1.0") + implementation("androidx.datastore:datastore-preferences:1.1.1") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.core:core-splashscreen:1.0.1") - implementation("androidx.media3:media3-exoplayer:1.3.1") - implementation("androidx.media3:media3-exoplayer-dash:1.3.1") - implementation("androidx.media3:media3-ui:1.3.1") implementation("androidx.paging:paging-runtime-ktx:3.2.1") implementation("androidx.paging:paging-compose:3.3.0-beta01") implementation("androidx.preference:preference-ktx:1.2.1") - implementation("com.google.code.gson:gson:2.10.1") - implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.12.0") - implementation("com.google.dagger:hilt-android:2.51") - ksp("com.google.dagger:hilt-android-compiler:2.51") + implementation("com.google.dagger:hilt-android:2.51.1") + ksp("com.google.dagger:hilt-android-compiler:2.51.1") implementation("androidx.hilt:hilt-navigation-compose:1.2.0") implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:okhttp-coroutines-jvm:5.0.0-alpha.12") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") - implementation("com.squareup.retrofit2:retrofit:2.10.0") + implementation("com.squareup.retrofit2:retrofit:2.11.0") implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.7.3") + + implementation("com.github.aniyomiorg:aniyomi-mpv-lib:1.15.n") + implementation("com.github.jmir1:ffmpeg-kit:1.14") implementation("io.coil-kt:coil:2.6.0") implementation("io.coil-kt:coil-compose:2.6.0") - implementation("io.coil-kt:coil-gif:2.5.0") + implementation("io.coil-kt:coil-gif:2.6.0") implementation("com.rometools:rome:2.1.0") implementation("net.dankito.readability4j:readability4j:1.0.8") diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 5b5ff472..2d6cc7cc 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -107,4 +107,10 @@ public static final ** CREATOR; -keep class com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter$Proxy { *; } # Retrofit --keep, allowobfuscation, allowshrinking interface retrofit2.Call \ No newline at end of file +-keep, allowobfuscation, allowshrinking interface retrofit2.Call + +# FFmpeg +-dontwarn com.arthenica.smartexception.java.Exceptions + +# MPV +-keep,allowoptimization class is.xyz.mpv.MPVLib { public protected *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c4f570d6..f3d7773c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:enableOnBackInvokedCallback="true" + android:extractNativeLibs="true" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" diff --git a/app/src/main/java/com/skyd/anivu/base/BaseActivity.kt b/app/src/main/java/com/skyd/anivu/base/BaseActivity.kt index 3bd9cfb6..d0c967d9 100644 --- a/app/src/main/java/com/skyd/anivu/base/BaseActivity.kt +++ b/app/src/main/java/com/skyd/anivu/base/BaseActivity.kt @@ -24,9 +24,9 @@ abstract class BaseActivity : AppCompatActivity() { binding.initView() } - protected open fun T.initView() {} + protected open fun T.initView() = Unit - protected open fun beforeSetContentView() {} + protected open fun beforeSetContentView() = Unit private fun initTheme() { setTheme(ThemePreference.toResId(this)) diff --git a/app/src/main/java/com/skyd/anivu/base/BaseComposeActivity.kt b/app/src/main/java/com/skyd/anivu/base/BaseComposeActivity.kt new file mode 100644 index 00000000..ad0c76ac --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/base/BaseComposeActivity.kt @@ -0,0 +1,37 @@ +package com.skyd.anivu.base + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import com.skyd.anivu.model.preference.SettingsProvider +import com.skyd.anivu.model.preference.appearance.ThemePreference +import com.skyd.anivu.ui.local.LocalDarkMode +import com.skyd.anivu.ui.local.LocalWindowSizeClass +import com.skyd.anivu.ui.theme.AniVuTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +open class BaseComposeActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + initTheme() + } + + fun setContentBase(content: @Composable () -> Unit) = setContent { + CompositionLocalProvider( + LocalWindowSizeClass provides calculateWindowSizeClass(this@BaseComposeActivity) + ) { + SettingsProvider { AniVuTheme(darkTheme = LocalDarkMode.current, content) } + } + } + + private fun initTheme() { + setTheme(ThemePreference.toResId(this)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/base/BaseDialogFragment.kt b/app/src/main/java/com/skyd/anivu/base/BaseDialogFragment.kt index e388a9d9..74ae273e 100644 --- a/app/src/main/java/com/skyd/anivu/base/BaseDialogFragment.kt +++ b/app/src/main/java/com/skyd/anivu/base/BaseDialogFragment.kt @@ -32,7 +32,7 @@ abstract class BaseDialogFragment : DialogFragment() { binding.initView() } - protected open fun T.initView() {} + protected open fun T.initView() = Unit override fun onDestroyView() { super.onDestroyView() diff --git a/app/src/main/java/com/skyd/anivu/config/Const.kt b/app/src/main/java/com/skyd/anivu/config/Const.kt index ca103c35..51112886 100644 --- a/app/src/main/java/com/skyd/anivu/config/Const.kt +++ b/app/src/main/java/com/skyd/anivu/config/Const.kt @@ -37,4 +37,12 @@ object Const { val TORRENT_RESUME_DATA_DIR = File(appContext.filesDir.path, "TorrentResumeData").apply { if (!exists()) mkdirs() } + + val MPV_CONFIG_DIR = File("${appContext.filesDir.path}/Mpv", "Config").apply { + if (!exists()) mkdirs() + } + + val MPV_CACHE_DIR = File("${appContext.cacheDir.path}/Mpv", "Cache").apply { + if (!exists()) mkdirs() + } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/IOExt.kt b/app/src/main/java/com/skyd/anivu/ext/IOExt.kt index 4b54ba5a..814d20df 100644 --- a/app/src/main/java/com/skyd/anivu/ext/IOExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/IOExt.kt @@ -103,7 +103,7 @@ fun InputStream.saveTo(target: File): File { fun File.md5(): String? { var bi: BigInteger? = null - try { + runCatching { val buffer = ByteArray(4096) var len: Int val md = MessageDigest.getInstance("MD5") @@ -114,10 +114,12 @@ fun File.md5(): String? { } val b = md.digest() bi = BigInteger(1, b) - } catch (e: NoSuchAlgorithmException) { - e.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() + }.onFailure { + when (it) { + is NoSuchAlgorithmException -> it.printStackTrace() + is IOException -> it.printStackTrace() + else -> throw it + } } return bi?.toString(16) } diff --git a/app/src/main/java/com/skyd/anivu/ext/NumberExt.kt b/app/src/main/java/com/skyd/anivu/ext/NumberExt.kt index 928b630e..fb82c3b1 100644 --- a/app/src/main/java/com/skyd/anivu/ext/NumberExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/NumberExt.kt @@ -36,6 +36,6 @@ val Int.sp: Int Resources.getSystem().displayMetrics ).toInt() -fun Float.toPercentage(): String = "%.2f%%".format(this * 100) +fun Float.toPercentage(format: String = "%.2f%%"): String = format.format(this * 100) fun Float.toDegrees(): Float = (this * 180 / Math.PI).toFloat() \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/OffsetExt.kt b/app/src/main/java/com/skyd/anivu/ext/OffsetExt.kt new file mode 100644 index 00000000..11f7ee38 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ext/OffsetExt.kt @@ -0,0 +1,14 @@ +package com.skyd.anivu.ext + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.util.packFloats +import androidx.compose.ui.util.unpackFloat1 +import androidx.compose.ui.util.unpackFloat2 + +fun snapshotStateOffsetSaver() = Saver, Long>( + save = { state -> packFloats(state.value.x, state.value.y) }, + restore = { value -> mutableStateOf(Offset(unpackFloat1(value), unpackFloat2(value))) } +) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/PointerInputScopeExt.kt b/app/src/main/java/com/skyd/anivu/ext/PointerInputScopeExt.kt new file mode 100644 index 00000000..71c93677 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ext/PointerInputScopeExt.kt @@ -0,0 +1,121 @@ +package com.skyd.anivu.ext + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.calculateCentroid +import androidx.compose.foundation.gestures.calculateCentroidSize +import androidx.compose.foundation.gestures.calculatePan +import androidx.compose.foundation.gestures.calculateRotation +import androidx.compose.foundation.gestures.calculateZoom +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.positionChanged +import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastForEach +import kotlin.math.PI +import kotlin.math.abs + + +suspend fun PointerInputScope.detectDoubleFingerTransformGestures( + onVerticalDragStart: (Offset) -> Unit = { }, + onVerticalDragEnd: () -> Unit = { }, + onVerticalDragCancel: () -> Unit = { }, + onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit, + onHorizontalDragStart: (Offset) -> Unit = { }, + onHorizontalDragEnd: () -> Unit = { }, + onHorizontalDragCancel: () -> Unit = { }, + onHorizontalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit, + onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit, +) { + awaitEachGesture { + var rotation = 0f + var zoom = 1f + var pan = Offset.Zero + var singlePan = Offset.Zero + var pastTouchSlop = false + val touchSlop = viewConfiguration.touchSlop + var lockedToPanZoom = false + + var horizontalDrag = false + var verticalDrag = false + var transformDrag = false + + val firstDown = awaitFirstDown(requireUnconsumed = false) + var canceled: Boolean + + do { + val event = awaitPointerEvent() + canceled = event.changes.fastAny { it.isConsumed } + val count: Int = if (event.changes.size > 2) { + event.changes.takeIf { it.last().id != it.first().id }?.size ?: 1 + } else event.changes.size + + if (!canceled) { + val zoomChange = event.calculateZoom() + val panChange = event.calculatePan() + val rotationChange = event.calculateRotation() + if (!pastTouchSlop) { + if (count == 1) { + singlePan += panChange + val singlePanMotion = singlePan.getDistance() + if (singlePanMotion > touchSlop) { + pastTouchSlop = true + if (abs(singlePan.x) > abs(singlePan.y)) { + horizontalDrag = true + onHorizontalDragStart(firstDown.position) + } else { + verticalDrag = true + onVerticalDragStart(firstDown.position) + } + } + } else if (count > 1) { + zoom *= zoomChange + rotation += rotationChange + pan += panChange + + val centroidSize = event.calculateCentroidSize(useCurrent = false) + val zoomMotion = abs(1 - zoom) * centroidSize + val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f) + val panMotion = pan.getDistance() + + if (zoomMotion > touchSlop || + rotationMotion > touchSlop || + panMotion > touchSlop + ) { + transformDrag = true + pastTouchSlop = true + lockedToPanZoom = rotationMotion < touchSlop + } + } + } + if (pastTouchSlop) { + if (horizontalDrag) { + onHorizontalDrag(event.changes.first(), panChange.x) + } else if (verticalDrag) { + onVerticalDrag(event.changes.first(), panChange.y) + } else if (transformDrag) { + val centroid = event.calculateCentroid(useCurrent = false) + val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange + if (effectiveRotation != 0f || + zoomChange != 1f || + panChange != Offset.Zero + ) { + onGesture(centroid, panChange, zoomChange, effectiveRotation) + } + } + event.changes.fastForEach { + if (it.positionChanged()) { + it.consume() + } + } + } + } + } while (!canceled && event.changes.fastAny { it.pressed }) + if (horizontalDrag) { + if (canceled) onHorizontalDragCancel() else onHorizontalDragEnd() + } else if (verticalDrag) { + if (canceled) onVerticalDragCancel() else onVerticalDragEnd() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt b/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt index eb15e8df..eda756ad 100644 --- a/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt @@ -13,6 +13,8 @@ import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeLeftActionPr import com.skyd.anivu.model.preference.behavior.article.ArticleTapActionPreference import com.skyd.anivu.model.preference.behavior.article.DeduplicateTitleInDescPreference import com.skyd.anivu.model.preference.behavior.feed.HideEmptyDefaultPreference +import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference +import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference fun Preferences.toSettings(): Settings { return Settings( @@ -32,5 +34,9 @@ fun Preferences.toSettings(): Settings { articleTapAction = ArticleTapActionPreference.fromPreferences(this), articleSwipeLeftAction = ArticleSwipeLeftActionPreference.fromPreferences(this), hideEmptyDefault = HideEmptyDefaultPreference.fromPreferences(this), + + // Player + playerDoubleTap = PlayerDoubleTapPreference.fromPreferences(this), + playerShow85sButton = PlayerShow85sButtonPreference.fromPreferences(this), ) } diff --git a/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt b/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt index c2363153..72dfd807 100644 --- a/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt @@ -6,11 +6,9 @@ import android.graphics.Rect import android.view.DisplayCutout import android.view.View import android.view.ViewGroup -import android.view.ViewTreeObserver import android.view.Window import android.view.animation.AlphaAnimation import android.view.inputmethod.InputMethodManager -import androidx.annotation.OptIn import androidx.core.graphics.Insets import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat @@ -25,9 +23,6 @@ import androidx.core.view.marginTop import androidx.core.view.updatePadding import androidx.navigation.NavController import androidx.navigation.Navigation -import com.google.android.material.badge.BadgeDrawable -import com.google.android.material.badge.BadgeUtils -import com.google.android.material.badge.ExperimentalBadgeUtils import com.skyd.anivu.R @@ -295,19 +290,6 @@ fun View.inSafeInset(displayCutout: DisplayCutout): Boolean { return true } -@OptIn(ExperimentalBadgeUtils::class) -fun View.addBadge(init: BadgeDrawable.() -> Unit) { - viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - BadgeDrawable.create(context).apply { - this.init() - BadgeUtils.attachBadgeDrawable(this, this@addBadge) - } - viewTreeObserver.removeOnGlobalLayoutListener(this) - } - }) -} - fun View.findMainNavController(): NavController { return Navigation.findNavController(activity, R.id.nav_host_fragment_main) } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt b/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt index d9313ba0..bb329b9d 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt @@ -62,23 +62,15 @@ abstract class AppDatabase : RoomDatabase() { arrayOf(Migration1To2(), Migration2To3(), Migration3To4(), Migration4To5()) fun getInstance(context: Context): AppDatabase { - return if (instance == null) { - synchronized(this) { - if (instance == null) { - Room.databaseBuilder( - context.applicationContext, - AppDatabase::class.java, - APP_DATA_BASE_FILE_NAME - ) - .addMigrations(*migrations) - .build() - .apply { instance = this } - } else { - instance as AppDatabase - } - } - } else { - instance as AppDatabase + return instance ?: synchronized(this) { + instance ?: Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + APP_DATA_BASE_FILE_NAME + ) + .addMigrations(*migrations) + .build() + .apply { instance = this } } } } diff --git a/app/src/main/java/com/skyd/anivu/model/db/SearchDomainDatabase.kt b/app/src/main/java/com/skyd/anivu/model/db/SearchDomainDatabase.kt index 4c37c3db..2d91e5ed 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/SearchDomainDatabase.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/SearchDomainDatabase.kt @@ -31,22 +31,15 @@ abstract class SearchDomainDatabase : RoomDatabase() { private val migrations = arrayOf() fun getInstance(context: Context): SearchDomainDatabase { - return if (instance == null) { - synchronized(this) { - if (instance == null) { - Room.databaseBuilder( - context.applicationContext, - SearchDomainDatabase::class.java, - SEARCH_DOMAIN_DATA_BASE_FILE_NAME - ) - .addMigrations(*migrations) - .build() - } else { - instance as SearchDomainDatabase - } - } - } else { - instance as SearchDomainDatabase + return instance ?: synchronized(this) { + instance ?: Room.databaseBuilder( + context.applicationContext, + SearchDomainDatabase::class.java, + SEARCH_DOMAIN_DATA_BASE_FILE_NAME + ) + .addMigrations(*migrations) + .build() + .apply { instance = this } } } } diff --git a/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt b/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt index 450c8c13..ccb48b5b 100644 --- a/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt +++ b/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt @@ -18,6 +18,8 @@ import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeLeftActionPr import com.skyd.anivu.model.preference.behavior.article.ArticleTapActionPreference import com.skyd.anivu.model.preference.behavior.article.DeduplicateTitleInDescPreference import com.skyd.anivu.model.preference.behavior.feed.HideEmptyDefaultPreference +import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference +import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference import com.skyd.anivu.ui.local.LocalArticleSwipeLeftAction import com.skyd.anivu.ui.local.LocalArticleTapAction import com.skyd.anivu.ui.local.LocalDarkMode @@ -27,6 +29,8 @@ import com.skyd.anivu.ui.local.LocalFeedGroupExpand import com.skyd.anivu.ui.local.LocalHideEmptyDefault import com.skyd.anivu.ui.local.LocalIgnoreUpdateVersion import com.skyd.anivu.ui.local.LocalNavigationBarLabel +import com.skyd.anivu.ui.local.LocalPlayerDoubleTap +import com.skyd.anivu.ui.local.LocalPlayerShow85sButton import com.skyd.anivu.ui.local.LocalTextFieldStyle import com.skyd.anivu.ui.local.LocalTheme import kotlinx.coroutines.Dispatchers @@ -47,6 +51,9 @@ data class Settings( val articleTapAction: String = ArticleTapActionPreference.default, val articleSwipeLeftAction: String = ArticleSwipeLeftActionPreference.default, val hideEmptyDefault: Boolean = HideEmptyDefaultPreference.default, + // Player + val playerDoubleTap: String = PlayerDoubleTapPreference.default, + val playerShow85sButton: Boolean = PlayerShow85sButtonPreference.default, ) @Composable @@ -72,6 +79,9 @@ fun SettingsProvider( LocalArticleTapAction provides settings.articleTapAction, LocalArticleSwipeLeftAction provides settings.articleSwipeLeftAction, LocalHideEmptyDefault provides settings.hideEmptyDefault, + // Player + LocalPlayerDoubleTap provides settings.playerDoubleTap, + LocalPlayerShow85sButton provides settings.playerShow85sButton, ) { content() } diff --git a/app/src/main/java/com/skyd/anivu/model/preference/player/MpvConfigPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/player/MpvConfigPreference.kt new file mode 100644 index 00000000..dad4dbb4 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/player/MpvConfigPreference.kt @@ -0,0 +1,28 @@ +package com.skyd.anivu.model.preference.player + +import com.skyd.anivu.config.Const +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.io.File + +object MpvConfigPreference { + private var value: String? = null + + fun put(scope: CoroutineScope, value: String) { + this.value = value + scope.launch(Dispatchers.IO) { + File(Const.MPV_CONFIG_DIR, "mpv.conf") + .apply { if (!exists()) createNewFile() } + .writeText(value) + } + } + + fun getValue(): String = value ?: runBlocking(Dispatchers.IO) { + value = File(Const.MPV_CONFIG_DIR, "mpv.conf") + .apply { if (!exists()) createNewFile() } + .readText() + value.orEmpty() + } +} diff --git a/app/src/main/java/com/skyd/anivu/model/repository/FeedRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/FeedRepository.kt index 9320fcc0..0c43a9bc 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/FeedRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/FeedRepository.kt @@ -3,7 +3,6 @@ package com.skyd.anivu.model.repository import com.skyd.anivu.base.BaseRepository import com.skyd.anivu.model.bean.FeedBean import com.skyd.anivu.model.bean.GroupBean -import com.skyd.anivu.model.db.dao.ArticleDao import com.skyd.anivu.model.db.dao.FeedDao import com.skyd.anivu.model.db.dao.GroupDao import kotlinx.coroutines.Dispatchers @@ -16,7 +15,6 @@ import kotlinx.coroutines.flow.map import javax.inject.Inject class FeedRepository @Inject constructor( - private val articleDao: ArticleDao, private val feedDao: FeedDao, private val groupDao: GroupDao, private val rssHelper: RssHelper, diff --git a/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt b/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt index e970171d..3c97ea72 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt @@ -52,7 +52,7 @@ class RssHelper @Inject constructor( feed: FeedBean, latestLink: String?, // 日期最新的文章链接,更新时不会take比这个文章更旧的文章 ): List = - try { + runCatching { inputStream(okHttpClient, feed.url).use { inputStream -> SyndFeedInput().apply { isPreserveWireFeed = true } .build(XmlReader(inputStream)) @@ -62,11 +62,11 @@ class RssHelper @Inject constructor( .map { article(feed, it) } .toList() } - } catch (e: Exception) { + }.onFailure { e -> e.printStackTrace() Log.e("RLog", "queryRssXml[${feed.title}]: ${e.message}") throw e - } + }.getOrDefault(emptyList()) private fun article( feed: FeedBean, diff --git a/app/src/main/java/com/skyd/anivu/model/repository/SearchRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/SearchRepository.kt index d4d5d012..000400ad 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/SearchRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/SearchRepository.kt @@ -15,7 +15,6 @@ import com.skyd.anivu.ext.getOrDefault import com.skyd.anivu.model.bean.ARTICLE_TABLE_NAME import com.skyd.anivu.model.bean.ArticleBean import com.skyd.anivu.model.bean.ArticleWithFeed -import com.skyd.anivu.model.bean.FEED_TABLE_NAME import com.skyd.anivu.model.bean.FEED_VIEW_NAME import com.skyd.anivu.model.bean.FeedViewBean import com.skyd.anivu.model.db.dao.ArticleDao diff --git a/app/src/main/java/com/skyd/anivu/ui/activity/CrashActivity.kt b/app/src/main/java/com/skyd/anivu/ui/activity/CrashActivity.kt index bf71d036..746372c5 100644 --- a/app/src/main/java/com/skyd/anivu/ui/activity/CrashActivity.kt +++ b/app/src/main/java/com/skyd/anivu/ui/activity/CrashActivity.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material3.Button import androidx.compose.material3.Icon diff --git a/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt b/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt index 687728c2..0b4a00ec 100644 --- a/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt +++ b/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt @@ -1,35 +1,28 @@ package com.skyd.anivu.ui.activity -import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.WindowManager +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.core.content.IntentCompat +import androidx.core.util.Consumer import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat -import androidx.media3.common.MediaItem -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import com.skyd.anivu.base.BaseActivity -import com.skyd.anivu.databinding.ActivityPlayBinding -import com.skyd.anivu.ext.dataStore -import com.skyd.anivu.ext.getOrDefault -import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference -import com.skyd.anivu.ui.player.TorrentDataSource +import com.skyd.anivu.base.BaseComposeActivity +import com.skyd.anivu.ui.mpv.PlayerView -@SuppressLint("UnsafeOptInUsageError") -class PlayActivity : BaseActivity() { +class PlayActivity : BaseComposeActivity() { companion object { const val VIDEO_URI_KEY = "videoUri" } - private val player: ExoPlayer by lazy { ExoPlayer.Builder(this@PlayActivity).build() } - private var videoUri: Uri? = null - private var beforePauseIsPlaying = false - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -45,81 +38,29 @@ class PlayActivity : BaseActivity() { } // Keep screen on window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) + setContentBase { + var videoUri by remember { mutableStateOf(handleIntent(intent)) } - if (intent != null) { - val data = IntentCompat.getParcelableExtra(intent, VIDEO_URI_KEY, Uri::class.java) - ?: intent.data - if (data != null) { - videoUri = data - play() + DisposableEffect(Unit) { + val listener = Consumer { newIntent -> + videoUri = handleIntent(newIntent) + } + addOnNewIntentListener(listener) + onDispose { removeOnNewIntentListener(listener) } + } + videoUri?.let { uri -> + PlayerView( + uri = uri, + onBack = { finish() }, + ) } } } - override fun ActivityPlayBinding.initView() { - playerView.setForward85sButton(dataStore.getOrDefault(PlayerShow85sButtonPreference)) -// playerView.setOnScreenshotListener { -// val retriever = MediaMetadataRetriever() -// retriever.setDataSource(player.curren) -// val bitmap = retriever.getFrameAtTime(simpleExoPlayer.getCurrentPosition()) -// } - videoUri = IntentCompat.getParcelableExtra(intent, VIDEO_URI_KEY, Uri::class.java) + private fun handleIntent(intent: Intent?): Uri? { + intent ?: return null + return IntentCompat.getParcelableExtra(intent, VIDEO_URI_KEY, Uri::class.java) ?: intent.data - - if (videoUri != null) { - playerView.setOnBackButtonClickListener { finish() } - // Attach player to the view. - playerView.player = player - play() - } } - - private fun play(): Boolean { - if (videoUri == null) { - return false - } - if (videoUri.toString().startsWith("magnet:")) { - player.setMediaSource(DefaultMediaSourceFactory { TorrentDataSource() } - .createMediaSource(MediaItem.fromUri(videoUri!!))) - } else { - // Set the media item to be played. - player.setMediaItem(MediaItem.fromUri(videoUri!!)) - } -// player.setMediaItem(MediaItem.fromUri(Uri.parse("magnet:?xt=urn:btih:344a65fde5ab561370ad5f144319a9f4951ac125&dn=%5BLPSub%5D%20Kaii%20to%20Otome%20to%20Kamikakushi%20%5B01%5D%5BAVC%20AAC%5D%5B1080p%5D%5BJPTC%5D.mp4&tr=udp%3A%2F%2F104.143.10.186%3A8000%2Fannounce&tr=http%3A%2F%2F104.143.10.186%3A8000%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker3.itzmx.com%3A6961%2Fannounce&tr=udp%3A%2F%2Ftracker4.itzmx.com%3A2710%2Fannounce&tr=http%3A%2F%2Ftracker.publicbt.com%3A80%2Fannounce&tr=http%3A%2F%2Ftracker.prq.to%2Fannounce&tr=http%3A%2F%2Fopen.acgtracker.com%3A1096%2Fannounce&tr=https%3A%2F%2Ft-115.rhcloud.com%2Fonly_for_ylbud&tr=udp%3A%2F%2Ftracker1.itzmx.com%3A8080%2Fannounce&tr=udp%3A%2F%2Ftracker2.itzmx.com%3A6961%2Fannounce&tr=http%3A%2F%2Ftracker1.itzmx.com%3A8080%2Fannounce&tr=http%3A%2F%2Ftracker2.itzmx.com%3A6961%2Fannounce&tr=http%3A%2F%2Ftracker3.itzmx.com%3A6961%2Fannounce&tr=http%3A%2F%2Ftracker4.itzmx.com%3A2710%2Fannounce"))) - // Prepare the player. - player.prepare() - player.play() - return true - } - - override fun onDestroy() { - super.onDestroy() - - player.stop() - player.release() - } - - override fun onResume() { - super.onResume() - - if (beforePauseIsPlaying) { - beforePauseIsPlaying = false - player.play() - } - } - - override fun onPause() { - super.onPause() - - if (player.isPlaying) { - beforePauseIsPlaying = true - player.pause() - } - } - - override fun getViewBinding() = ActivityPlayBinding.inflate(layoutInflater) } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Enclosure1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Enclosure1Proxy.kt index a6e92171..b15a62cf 100644 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Enclosure1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Enclosure1Proxy.kt @@ -6,8 +6,6 @@ import android.view.ViewGroup import com.skyd.anivu.databinding.ItemEnclosure1Binding import com.skyd.anivu.ext.copy import com.skyd.anivu.ext.fileSize -import com.skyd.anivu.ext.gone -import com.skyd.anivu.ext.visible import com.skyd.anivu.model.bean.EnclosureBean import com.skyd.anivu.ui.adapter.variety.Enclosure1ViewHolder import com.skyd.anivu.ui.adapter.variety.VarietyAdapter @@ -15,7 +13,6 @@ import com.skyd.anivu.ui.adapter.variety.VarietyAdapter class Enclosure1Proxy( private val onDownload: (EnclosureBean) -> Unit, - private val onPlay: (EnclosureBean) -> Unit, ) : VarietyAdapter.Proxy() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Enclosure1ViewHolder = Enclosure1ViewHolder( @@ -37,14 +34,6 @@ class Enclosure1Proxy( tvEnclosure1Url.setOnClickListener { data.url.copy(context) } - if (data.url.startsWith("magnet:")) { - btnEnclosure1Play.visible() - } else { - btnEnclosure1Play.gone() - } - btnEnclosure1Play.setOnClickListener { - onPlay(data) - } btnEnclosure1Download.setOnClickListener { onDownload(data) } diff --git a/app/src/main/java/com/skyd/anivu/ui/component/OnLifecycleEvent.kt b/app/src/main/java/com/skyd/anivu/ui/component/OnLifecycleEvent.kt index cc295ca0..3df9ff79 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/OnLifecycleEvent.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/OnLifecycleEvent.kt @@ -3,10 +3,10 @@ package com.skyd.anivu.ui.component import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner @Composable fun OnLifecycleEvent(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) { diff --git a/app/src/main/java/com/skyd/anivu/ui/component/SettingsItem.kt b/app/src/main/java/com/skyd/anivu/ui/component/SettingsItem.kt index deeaedcd..07ae1d47 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/SettingsItem.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/SettingsItem.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.skyd.anivu.ext.alwaysLight +import java.util.Locale val LocalUseColorfulIcon = compositionLocalOf { false } val LocalVerticalPadding = compositionLocalOf { 16.dp } @@ -128,7 +129,7 @@ fun SliderSettingsItem( onValueChange = onValueChange, ) Spacer(modifier = Modifier.width(6.dp)) - Text(text = String.format(valueFormat, value)) + Text(text = String.format(Locale.getDefault(), valueFormat, value)) } } ) diff --git a/app/src/main/java/com/skyd/anivu/ui/component/dialog/TextFieldDialog.kt b/app/src/main/java/com/skyd/anivu/ui/component/dialog/TextFieldDialog.kt index 16f39fd0..71141bd7 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/dialog/TextFieldDialog.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/dialog/TextFieldDialog.kt @@ -24,7 +24,7 @@ fun TextFieldDialog( maxLines: Int = Int.MAX_VALUE, style: AniVuTextFieldStyle = AniVuTextFieldStyle.toEnum(LocalTextFieldStyle.current), icon: @Composable (() -> Unit)? = null, - title: String = "", + title: String? = null, value: String = "", placeholder: String = "", trailingIcon: @Composable (() -> Unit)? = DefaultTrailingIcon, @@ -44,7 +44,9 @@ fun TextFieldDialog( visible = visible, onDismissRequest = onDismissRequest, icon = icon, - title = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) }, + title = if (title == null) null else { + { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) } + }, text = { ClipboardTextField( modifier = modifier, diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt index 38aa42af..91efce7e 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt @@ -2,7 +2,6 @@ package com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy import android.content.Context import android.os.Bundle -import android.text.format.DateUtils import androidx.compose.animation.Animatable import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable diff --git a/app/src/main/java/com/skyd/anivu/ui/component/shape/SemiCircle.kt b/app/src/main/java/com/skyd/anivu/ui/component/shape/SemiCircle.kt new file mode 100644 index 00000000..160c9f12 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/shape/SemiCircle.kt @@ -0,0 +1,35 @@ +package com.skyd.anivu.ui.component.shape + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.toRect +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import com.skyd.anivu.ui.mpv.ForwardRippleDirect + +class ForwardRippleShape(private val direct: ForwardRippleDirect) : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val rect = size.toRect().run { + copy( + left = if (direct == ForwardRippleDirect.Forward) left else left - width, + right = if (direct == ForwardRippleDirect.Forward) right + width else right, + top = top - width / 2, + bottom = bottom + width / 2 + ) + } + val path = Path().apply { + addArc( + oval = rect, + startAngleDegrees = if (direct == ForwardRippleDirect.Forward) 90f else -90f, + sweepAngleDegrees = 180f + ) + } + return Outline.Generic(path) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt index 8514c2ba..81905797 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt @@ -140,7 +140,7 @@ fun FeedScreen(viewModel: FeedViewModel = hiltViewModel()) { ArrayList( (uiState.groupListState as? GroupListState.Success) ?.dataList - ?.filterIsInstance(FeedViewBean::class.java) + ?.filterIsInstance() ?.map { it.feed.url } .orEmpty() ) @@ -203,7 +203,7 @@ fun FeedScreen(viewModel: FeedViewModel = hiltViewModel()) { onShowAllArticles = { group -> val feedUrls = (uiState.groupListState as? GroupListState.Success) ?.dataList - ?.filterIsInstance(FeedViewBean::class.java) + ?.filterIsInstance() ?.filter { it.feed.groupId == group.groupId || group.isDefaultGroup() && it.feed.isDefaultGroup() } ?.map { it.feed.url } .orEmpty() diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt index df26e67f..b3abf4f5 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt @@ -145,7 +145,6 @@ private fun getLicenseList(): List { license = "Apache-2.0", link = "https://github.com/Kotlin/kotlinx.serialization", ), - LicenseBean( name = "MaterialKolor", license = "MIT", @@ -181,5 +180,10 @@ private fun getLicenseList(): List { license = "MIT", link = "https://github.com/aldenml/libtorrent4j", ), + LicenseBean( + name = "mpv-android", + license = "MIT", + link = "https://github.com/mpv-android/mpv-android", + ), ).sortedBy { it.name } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/read/EnclosureBottomSheet.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/read/EnclosureBottomSheet.kt index 0d077057..383d853e 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/read/EnclosureBottomSheet.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/read/EnclosureBottomSheet.kt @@ -1,11 +1,9 @@ package com.skyd.anivu.ui.fragment.read -import android.content.Intent import android.os.Build import android.view.LayoutInflater import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.net.toUri import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.divider.MaterialDividerItemDecoration @@ -15,7 +13,6 @@ import com.skyd.anivu.databinding.BottomSheetEnclosureBinding import com.skyd.anivu.model.bean.EnclosureBean import com.skyd.anivu.model.bean.LinkEnclosureBean import com.skyd.anivu.model.worker.download.DownloadTorrentWorker -import com.skyd.anivu.ui.activity.PlayActivity import com.skyd.anivu.ui.adapter.variety.AniSpanSize import com.skyd.anivu.ui.adapter.variety.VarietyAdapter import com.skyd.anivu.ui.adapter.variety.proxy.Enclosure1Proxy @@ -75,14 +72,7 @@ class EnclosureBottomSheet : BaseBottomSheetDialogFragment + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = paddingValues, + ) { + item { + BaseSettingsItem( + icon = rememberVectorPainter(Icons.Outlined.PlayCircle), + text = stringResource(id = R.string.player_config_advanced_screen_mpv_config), + descriptionText = null, + onClick = { + mpvConfEditDialogValue = MpvConfigPreference.getValue() + openMpvConfEditDialog = true + } + ) + } + } + + TextFieldDialog( + visible = openMpvConfEditDialog, + value = mpvConfEditDialogValue, + onValueChange = { mpvConfEditDialogValue = it }, + onConfirm = { + MpvConfigPreference.put( + scope = scope, + value = it, + ) + openMpvConfEditDialog = false + }, + onDismissRequest = { openMpvConfEditDialog = false }, + ) + } +} diff --git a/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt b/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt index cbbe6351..104afb12 100644 --- a/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt +++ b/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt @@ -14,6 +14,8 @@ import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeLeftActionPr import com.skyd.anivu.model.preference.behavior.article.ArticleTapActionPreference import com.skyd.anivu.model.preference.behavior.article.DeduplicateTitleInDescPreference import com.skyd.anivu.model.preference.behavior.feed.HideEmptyDefaultPreference +import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference +import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference val LocalNavController = compositionLocalOf { error("LocalNavController not initialized!") @@ -38,4 +40,8 @@ val LocalIgnoreUpdateVersion = compositionLocalOf { IgnoreUpdateVersionPreferenc val LocalDeduplicateTitleInDesc = compositionLocalOf { DeduplicateTitleInDescPreference.default } val LocalArticleTapAction = compositionLocalOf { ArticleTapActionPreference.default } val LocalArticleSwipeLeftAction = compositionLocalOf { ArticleSwipeLeftActionPreference.default } -val LocalHideEmptyDefault = compositionLocalOf { HideEmptyDefaultPreference.default } \ No newline at end of file +val LocalHideEmptyDefault = compositionLocalOf { HideEmptyDefaultPreference.default } + +// Player +val LocalPlayerDoubleTap = compositionLocalOf { PlayerDoubleTapPreference.default } +val LocalPlayerShow85sButton = compositionLocalOf { PlayerShow85sButtonPreference.default } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/ForwardRipple.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/ForwardRipple.kt new file mode 100644 index 00000000..085820d8 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/ForwardRipple.kt @@ -0,0 +1,114 @@ +package com.skyd.anivu.ui.mpv + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import com.skyd.anivu.ui.component.shape.ForwardRippleShape +import kotlin.math.sqrt + +enum class ForwardRippleDirect { + Forward, Backward +} + +@Composable +fun ForwardRipple( + modifier: Modifier = Modifier, + direct: ForwardRippleDirect = ForwardRippleDirect.Forward, + text: String, + icon: ImageVector, + controllerWidth: () -> Int, + parentLayoutCoordinates: LayoutCoordinates?, + rippleStartControllerOffset: Offset, + onHideRipple: () -> Unit, +) { + var finished by remember { mutableStateOf(false) } + var globallyPositioned by remember { mutableStateOf(false) } + var rippleStartOffset by remember { mutableStateOf(Offset.Zero) } + var maxRadius by remember { mutableFloatStateOf(0f) } + + val animateRadius by animateFloatAsState( + targetValue = if (!globallyPositioned) 0f else maxRadius, + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + label = "animateRadius", + finishedListener = { + onHideRipple() + finished = true + } + ) + + if (finished) onHideRipple() + + Box( + modifier = modifier + .clip(ForwardRippleShape(direct)) + .onSizeChanged { + maxRadius = sqrt(it.height * it.height.toDouble() + it.width * it.width).toFloat() + } + .width(with(LocalDensity.current) { controllerWidth().toDp() / 3f }) + .fillMaxHeight() + .onGloballyPositioned { coordinates -> + if (!globallyPositioned) { + rippleStartOffset = rippleStartControllerOffset - + (parentLayoutCoordinates?.localPositionOf(coordinates, Offset.Zero) + ?: Offset.Zero) + globallyPositioned = true + } + }, + ) { + Canvas(modifier = Modifier.fillMaxSize()) { + drawCircle( + color = ControllerLabelGray, + center = rippleStartOffset, + radius = animateRadius, + ) + } + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier.size(50.dp), + imageVector = icon, + contentDescription = null, + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt new file mode 100644 index 00000000..28f3d6ae --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt @@ -0,0 +1,415 @@ +package com.skyd.anivu.ui.mpv + +import android.content.Context +import android.os.Build +import android.os.Environment +import android.util.AttributeSet +import android.util.Log +import android.view.SurfaceHolder +import android.view.SurfaceView +import android.view.WindowManager +import `is`.xyz.mpv.MPVLib +import `is`.xyz.mpv.MPVLib.mpvFormat.MPV_FORMAT_DOUBLE +import `is`.xyz.mpv.MPVLib.mpvFormat.MPV_FORMAT_FLAG +import `is`.xyz.mpv.MPVLib.mpvFormat.MPV_FORMAT_INT64 +import `is`.xyz.mpv.MPVLib.mpvFormat.MPV_FORMAT_NONE +import `is`.xyz.mpv.MPVLib.mpvFormat.MPV_FORMAT_STRING +import `is`.xyz.mpv.R +import kotlin.math.log +import kotlin.reflect.KProperty + +class MPVView(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs), + SurfaceHolder.Callback { + private var surfaceCreated = false + + fun initialize(configDir: String, cacheDir: String, logLvl: String = "v", vo: String = "gpu") { + MPVLib.create(this.context, logLvl) + MPVLib.setOptionString("config", "yes") + MPVLib.setOptionString("config-dir", configDir) + for (opt in arrayOf("gpu-shader-cache-dir", "icc-cache-dir")) + MPVLib.setOptionString(opt, cacheDir) + initOptions(vo) // do this before init() so user-supplied config can override our choices + MPVLib.init() + /* Hardcoded options: */ + // we need to call write-watch-later manually + MPVLib.setOptionString("save-position-on-quit", "no") + // would crash before the surface is attached + MPVLib.setOptionString("force-window", "no") + // "no" wouldn't work and "yes" is not intended by the UI + MPVLib.setOptionString("idle", "yes") + + holder.addCallback(this) + observeProperties() + } + + private var voInUse: String = "" + + private fun initOptions(vo: String) { + // apply phone-optimized defaults + MPVLib.setOptionString("profile", "fast") + + // vo + voInUse = vo + + // hwdec + val hwdec = "auto"//"no" + + // vo: set display fps as reported by android + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val disp = wm.defaultDisplay + val refreshRate = disp.mode.refreshRate + + Log.v(TAG, "Display ${disp.displayId} reports FPS of $refreshRate") + MPVLib.setOptionString("display-fps-override", refreshRate.toString()) + MPVLib.setOptionString("video-sync", "audio") + + MPVLib.setOptionString("vo", vo) + MPVLib.setOptionString("gpu-context", "android") + MPVLib.setOptionString("opengl-es", "yes") + MPVLib.setOptionString("hwdec", hwdec) + MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1") + MPVLib.setOptionString("ao", "audiotrack,opensles") + MPVLib.setOptionString("input-default-bindings", "yes") + // Limit demuxer cache since the defaults are too high for mobile devices + val cacheMegs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) 64 else 32 + MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}") + MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}") + // + val screenshotDir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + screenshotDir.mkdirs() + MPVLib.setOptionString("screenshot-directory", screenshotDir.path) + } + + private var filePath: String? = null + + // Called when back button is pressed, or app is shutting down + fun destroy() { + // Disable surface callbacks to avoid using unintialized mpv state + holder.removeCallback(this) + + MPVLib.destroy() + } + + private fun observeProperties() { + // This observes all properties needed by MPVView, MPVActivity or other classes + data class Property(val name: String, val format: Int = MPV_FORMAT_NONE) + + val p = arrayOf( + Property("time-pos", MPV_FORMAT_INT64), + Property("duration", MPV_FORMAT_INT64), + Property("demuxer-cache-time", MPV_FORMAT_INT64), + Property("video-rotate", MPV_FORMAT_INT64), + Property("paused-for-cache", MPV_FORMAT_FLAG), + Property("seeking", MPV_FORMAT_FLAG), + Property("pause", MPV_FORMAT_FLAG), + Property("eof-reached", MPV_FORMAT_FLAG), + Property("paused-for-cache", MPV_FORMAT_FLAG), + Property("track-list"), + // observing double properties is not hooked up in the JNI code, but doing this + // will restrict updates to when it actually changes + Property("video-zoom", MPV_FORMAT_DOUBLE), + Property("video-params/aspect", MPV_FORMAT_DOUBLE), + Property("video-pan-x", MPV_FORMAT_DOUBLE), + Property("video-pan-y", MPV_FORMAT_DOUBLE), + Property("speed", MPV_FORMAT_DOUBLE), + Property("playlist-pos", MPV_FORMAT_INT64), + Property("playlist-count", MPV_FORMAT_INT64), + Property("video-format"), + Property("media-title", MPV_FORMAT_STRING), + Property("metadata/by-key/Artist", MPV_FORMAT_STRING), + Property("metadata/by-key/Album", MPV_FORMAT_STRING), + Property("loop-playlist"), + Property("loop-file"), + Property("shuffle", MPV_FORMAT_FLAG), + Property("hwdec-current") + ) + + for ((name, format) in p) { + MPVLib.observeProperty(name, format) + } + } + + fun addObserver(o: MPVLib.EventObserver) { + MPVLib.addObserver(o) + } + + fun removeObserver(o: MPVLib.EventObserver) { + MPVLib.removeObserver(o) + } + + data class Track(val mpvId: Int, val name: String) + + var tracks = mapOf>( + "audio" to arrayListOf(), + "video" to arrayListOf(), + "sub" to arrayListOf() + ) + + fun loadTracks() { + for (list in tracks.values) { + list.clear() + // pseudo-track to allow disabling audio/subs + list.add(Track(-1, context.getString(R.string.track_off))) + } + val count = MPVLib.getPropertyInt("track-list/count")!! + // Note that because events are async, properties might disappear at any moment + // so use ?: continue instead of !! + for (i in 0 until count) { + val type = MPVLib.getPropertyString("track-list/$i/type") ?: continue + if (!tracks.containsKey(type)) { + Log.w(TAG, "Got unknown track type: $type") + continue + } + val mpvId = MPVLib.getPropertyInt("track-list/$i/id") ?: continue + val lang = MPVLib.getPropertyString("track-list/$i/lang") + val title = MPVLib.getPropertyString("track-list/$i/title") + + val trackName = if (!lang.isNullOrEmpty() && !title.isNullOrEmpty()) + context.getString(R.string.ui_track_title_lang, mpvId, title, lang) + else if (!lang.isNullOrEmpty() || !title.isNullOrEmpty()) + context.getString(R.string.ui_track_text, mpvId, (lang ?: "") + (title ?: "")) + else + context.getString(R.string.ui_track, mpvId) + tracks.getValue(type).add( + Track( + mpvId = mpvId, + name = trackName + ) + ) + } + } + + data class PlaylistItem(val index: Int, val filename: String, val title: String?) + + fun loadPlaylist(): MutableList { + val playlist = mutableListOf() + val count = MPVLib.getPropertyInt("playlist-count")!! + for (i in 0 until count) { + val filename = MPVLib.getPropertyString("playlist/$i/filename")!! + val title = MPVLib.getPropertyString("playlist/$i/title") + playlist.add(PlaylistItem(index = i, filename = filename, title = title)) + } + return playlist + } + + data class Chapter(val index: Int, val title: String?, val time: Double) + + fun loadChapters(): MutableList { + val chapters = mutableListOf() + val count = MPVLib.getPropertyInt("chapter-list/count")!! + for (i in 0 until count) { + val title = MPVLib.getPropertyString("chapter-list/$i/title") + val time = MPVLib.getPropertyDouble("chapter-list/$i/time")!! + chapters.add( + Chapter( + index = i, + title = title, + time = time + ) + ) + } + return chapters + } + + // Property getters/setters + var paused: Boolean + get() = MPVLib.getPropertyBoolean("pause") + set(paused) = MPVLib.setPropertyBoolean("pause", paused) + val isIdling: Boolean + get() = MPVLib.getPropertyBoolean("idle-active") + val eofReached: Boolean + get() = MPVLib.getPropertyBoolean("eof-reached") + val keepOpen: Boolean + get() = MPVLib.getPropertyBoolean("keep-open") + + val duration: Int? + get() = MPVLib.getPropertyInt("duration") + + var timePos: Int + get() = MPVLib.getPropertyInt("time-pos") + set(progress) = MPVLib.setPropertyInt("time-pos", progress) + + val hwdecActive: String + get() = MPVLib.getPropertyString("hwdec-current") ?: "no" + + var playbackSpeed: Double + get() = MPVLib.getPropertyDouble("speed") + set(speed) = MPVLib.setPropertyDouble("speed", speed) + + val videoRotate: Int + get() = MPVLib.getPropertyInt("video-rotate") + + val videoZoom: Double + get() = MPVLib.getPropertyDouble("video-zoom") + + val videoPanX: Double + get() = MPVLib.getPropertyDouble("video-pan-x") + + val videoPanY: Double + get() = MPVLib.getPropertyDouble("video-pan-y") + + val estimatedVfFps: Double? + get() = MPVLib.getPropertyDouble("estimated-vf-fps") + + val videoW: Int? + get() = MPVLib.getPropertyInt("video-params/w") + + val videoH: Int? + get() = MPVLib.getPropertyInt("video-params/h") + + val videoDW: Int? + get() = MPVLib.getPropertyInt("video-params/dw") + + val videoDH: Int? + get() = MPVLib.getPropertyInt("video-params/dh") + + val videoAspect: Double? + get() = MPVLib.getPropertyDouble("video-params/aspect") + + class TrackDelegate(private val name: String) { + operator fun getValue(thisRef: Any?, property: KProperty<*>): Int { + val v = MPVLib.getPropertyString(name) + // we can get null here for "no" or other invalid value + return v?.toIntOrNull() ?: -1 + } + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { + if (value == -1) + MPVLib.setPropertyString(name, "no") + else + MPVLib.setPropertyInt(name, value) + } + } + + var vid: Int by TrackDelegate("vid") + var sid: Int by TrackDelegate("sid") + var secondarySid: Int by TrackDelegate("secondary-sid") + var aid: Int by TrackDelegate("aid") + + // Commands + + fun cyclePause() = MPVLib.command(arrayOf("cycle", "pause")) + fun cycleAudio() = MPVLib.command(arrayOf("cycle", "audio")) + fun cycleSub() = MPVLib.command(arrayOf("cycle", "sub")) + fun cycleHwdec() = MPVLib.command(arrayOf("cycle-values", "hwdec", "auto", "no")) + + fun cycleSpeed() { + val speeds = arrayOf(0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0) + val currentSpeed = playbackSpeed + val index = speeds.indexOfFirst { it > currentSpeed } + playbackSpeed = speeds[if (index == -1) 0 else index] + } + + fun getRepeat(): Int { + return when (MPVLib.getPropertyString("loop-playlist") + + MPVLib.getPropertyString("loop-file")) { + "noinf" -> 2 + "infno" -> 1 + else -> 0 + } + } + + fun cycleRepeat() { + when (val state = getRepeat()) { + 0, 1 -> { + MPVLib.setPropertyString("loop-playlist", if (state == 1) "no" else "inf") + MPVLib.setPropertyString("loop-file", if (state == 1) "inf" else "no") + } + + 2 -> MPVLib.setPropertyString("loop-file", "no") + } + } + + fun getShuffle(): Boolean { + return MPVLib.getPropertyBoolean("shuffle") + } + + fun changeShuffle(cycle: Boolean, value: Boolean = true) { + // Use the 'shuffle' property to store the shuffled state, since changing + // it at runtime doesn't do anything. + val state = getShuffle() + val newState = if (cycle) state.xor(value) else value + if (state == newState) + return + MPVLib.command(arrayOf(if (newState) "playlist-shuffle" else "playlist-unshuffle")) + MPVLib.setPropertyBoolean("shuffle", newState) + } + + // Surface callbacks + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + MPVLib.setPropertyString("android-surface-size", "${width}x$height") + } + + override fun surfaceCreated(holder: SurfaceHolder) { + Log.w(TAG, "attaching surface") + MPVLib.attachSurface(holder.surface) + // This forces mpv to render subs/osd/whatever into our surface even if it would ordinarily not + MPVLib.setOptionString("force-window", "yes") + + surfaceCreated = true + if (filePath != null) { + MPVLib.command(arrayOf("loadfile", filePath as String)) + filePath = null + } else { + // We disable video output when the context disappears, enable it back + MPVLib.setPropertyString("vo", voInUse) + } + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + Log.w(TAG, "detaching surface") + MPVLib.setPropertyString("vo", "null") + MPVLib.setOptionString("force-window", "no") + MPVLib.detachSurface() + } + + fun loadFile(filePath: String) { + if (surfaceCreated) MPVLib.command(arrayOf("loadfile", filePath)) + else this.filePath = filePath + } + + fun stop() { + MPVLib.command(arrayOf("stop")) + } + + fun seek(position: Int, precise: Boolean = false) { + if (precise) { + timePos = position + } else { + // seek faster than assigning to timePos but less precise + MPVLib.command(arrayOf("seek", position.toString(), "absolute+keyframes")) + } + } + + fun zoom(value: Float) { + MPVLib.setOptionString("video-zoom", log(value, 2f).toString()) + } + + fun rotate(value: Int) { + var scaledValue = value % 360 + scaledValue = if (scaledValue >= 0) scaledValue else scaledValue + 360 + MPVLib.setOptionString("video-rotate", scaledValue.toString()) + } + + fun offset(x: Int, y: Int) { + val dw = videoDW + val dh = videoDH + if (dw == null || dh == null) return + MPVLib.setOptionString("video-pan-x", (x.toFloat() / dw).toString()) + MPVLib.setOptionString("video-pan-y", (y.toFloat() / dh).toString()) + } + + fun playMediaAtIndex(index: Int? = null) { + when (index) { + null -> MPVLib.command(arrayOf("playlist-play-index", "none")) + -1 -> MPVLib.command(arrayOf("playlist-play-index", "current")) + else -> MPVLib.command(arrayOf("playlist-play-index", index.toString())) + } + } + + companion object { + private const val TAG = "mpv" + } +} diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerUtil.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerUtil.kt new file mode 100644 index 00000000..8b724a12 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerUtil.kt @@ -0,0 +1,61 @@ +package com.skyd.anivu.ui.mpv + +import android.content.Context +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.util.Log +import java.io.File +import java.io.FileInputStream +import java.io.InputStream + +internal fun Uri.resolveUri(context: Context): String? { + val filepath = when (scheme) { + "file" -> path + "content" -> openContentFd(context) + "http", "https", "rtmp", "rtmps", "rtp", "rtsp", + "mms", "mmst", "mmsh", "tcp", "udp", "lavf" -> this.toString() + + else -> null + } + + if (filepath == null) { + Log.e("resolveUri", "unknown scheme: $scheme") + } + return filepath +} + +private fun Uri.openContentFd(context: Context): String? { + val resolver = context.contentResolver + val fd = try { + resolver.openFileDescriptor(this, "r")!!.detachFd() + } catch (e: Exception) { + Log.e("openContentFd", "Failed to open content fd: $e") + return null + } + // See if we skip the indirection and read the real file directly + val path = findRealPath(fd) + if (path != null) { + Log.v("openContentFd", "Found real file path: $path") + ParcelFileDescriptor.adoptFd(fd).close() // we don't need that anymore + return path + } + // Else, pass the fd to mpv + return "fd://${fd}" +} + +fun findRealPath(fd: Int): String? { + var ins: InputStream? = null + try { + val path = File("/proc/self/fd/${fd}").canonicalPath + if (!path.startsWith("/proc") && File(path).canRead()) { + // Double check that we can read it + ins = FileInputStream(path) + ins.read() + return path + } + } catch (_: Exception) { + } finally { + ins?.close() + } + return null +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt new file mode 100644 index 00000000..a3f71204 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt @@ -0,0 +1,905 @@ +package com.skyd.anivu.ui.mpv + +import android.net.Uri +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.VolumeDown +import androidx.compose.material.icons.automirrored.rounded.VolumeMute +import androidx.compose.material.icons.automirrored.rounded.VolumeUp +import androidx.compose.material.icons.outlined.ArrowBackIosNew +import androidx.compose.material.icons.rounded.BrightnessHigh +import androidx.compose.material.icons.rounded.BrightnessLow +import androidx.compose.material.icons.rounded.BrightnessMedium +import androidx.compose.material.icons.rounded.FastForward +import androidx.compose.material.icons.rounded.FastRewind +import androidx.compose.material.icons.rounded.Pause +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.materialkolor.ktx.toColor +import com.materialkolor.ktx.toHct +import com.skyd.anivu.R +import com.skyd.anivu.config.Const +import com.skyd.anivu.ext.alwaysLight +import com.skyd.anivu.ext.snapshotStateOffsetSaver +import com.skyd.anivu.ext.startWith +import com.skyd.anivu.ext.toPercentage +import com.skyd.anivu.ui.local.LocalPlayerShow85sButton +import `is`.xyz.mpv.MPVLib +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.pow + +sealed interface PlayerCommand { + data class SetUri(val uri: Uri) : PlayerCommand + data object Destroy : PlayerCommand + data class Paused(val paused: Boolean) : PlayerCommand + data object GetPaused : PlayerCommand + data object PlayOrPause : PlayerCommand + data class SeekTo(val position: Int) : PlayerCommand + data class Rotate(val rotate: Int) : PlayerCommand + data class Zoom(val zoom: Float) : PlayerCommand + data object GetZoom : PlayerCommand + data class VideoOffset(val offset: Offset) : PlayerCommand + data object GetVideoOffsetX : PlayerCommand + data object GetVideoOffsetY : PlayerCommand + data class SetSpeed(val speed: Float) : PlayerCommand + data object GetSpeed : PlayerCommand +} + +private fun MPVView.solveCommand( + command: PlayerCommand, + uri: () -> Uri, + isPlayingChanged: (Boolean) -> Unit, + onVideoZoom: (Float) -> Unit, + videoOffset: () -> Offset, + onVideoOffset: (Offset) -> Unit, + onSpeedChanged: (Float) -> Unit, +) { + when (command) { + is PlayerCommand.SetUri -> uri().resolveUri(context) + ?.let { loadFile(it) } + + PlayerCommand.Destroy -> destroy() + is PlayerCommand.Paused -> { + if (!command.paused) { + if (keepOpen && eofReached) { + seek(0) + } else if (isIdling) { + uri().resolveUri(context)?.let { loadFile(it) } + } + } + paused = command.paused + } + + PlayerCommand.GetPaused -> isPlayingChanged(!paused) + PlayerCommand.PlayOrPause -> cyclePause() + is PlayerCommand.SeekTo -> seek(command.position.coerceIn(0..(duration ?: 0))) + is PlayerCommand.Rotate -> rotate(command.rotate) + is PlayerCommand.Zoom -> zoom(command.zoom) + PlayerCommand.GetZoom -> onVideoZoom(2.0.pow(videoZoom).toFloat()) + is PlayerCommand.VideoOffset -> offset( + command.offset.x.toInt(), command.offset.y.toInt() + ) + + PlayerCommand.GetVideoOffsetX -> videoDW?.let { dw -> + onVideoOffset(videoOffset().copy(x = (videoPanX * dw).toFloat())) + } + + PlayerCommand.GetVideoOffsetY -> videoDH?.let { dh -> + onVideoOffset(videoOffset().copy(y = (videoPanY * dh).toFloat())) + } + + is PlayerCommand.SetSpeed -> playbackSpeed = + command.speed.toDouble() + + PlayerCommand.GetSpeed -> onSpeedChanged(playbackSpeed.toFloat()) + } +} + +@Composable +fun PlayerView( + uri: Uri, + onBack: () -> Unit, + configDir: String = Const.MPV_CONFIG_DIR.path, + cacheDir: String = Const.MPV_CACHE_DIR.path, +) { + val commandQueue = remember { Channel(capacity = UNLIMITED) } + val scope = rememberCoroutineScope() + + var mediaLoaded by rememberSaveable { mutableStateOf(false) } + var isPlaying by rememberSaveable { mutableStateOf(false) } + var title by rememberSaveable { mutableStateOf("") } + var duration by rememberSaveable { mutableIntStateOf(0) } + var currentPosition by rememberSaveable { mutableIntStateOf(0) } + var isSeeking by rememberSaveable { mutableStateOf(false) } + var speed by rememberSaveable { mutableFloatStateOf(1f) } + var videoRotate by rememberSaveable { mutableIntStateOf(0) } + var videoZoom by rememberSaveable { mutableFloatStateOf(1f) } + var videoOffset by rememberSaveable(saver = snapshotStateOffsetSaver()) { mutableStateOf(Offset.Zero) } + + val mpvObserver = remember { + object : MPVLib.EventObserver { + override fun eventProperty(property: String) { + when (property) { + "video-zoom" -> commandQueue.trySend(PlayerCommand.GetZoom) + "video-pan-x" -> commandQueue.trySend(PlayerCommand.GetVideoOffsetX) + "video-pan-y" -> commandQueue.trySend(PlayerCommand.GetVideoOffsetY) + "speed" -> commandQueue.trySend(PlayerCommand.GetSpeed) + } + } + + override fun eventProperty(property: String, value: Long) { + when (property) { + "time-pos" -> currentPosition = value.toInt() + "duration" -> duration = value.toInt() + "video-rotate" -> videoRotate = value.toInt() + } + } + + override fun eventProperty(property: String, value: Boolean) { + when (property) { + "pause" -> isPlaying = !value + } + } + + override fun eventProperty(property: String, value: String) { + when (property) { + "media-title" -> title = value + } + } + + override fun event(eventId: Int) { + when (eventId) { + MPVLib.mpvEventId.MPV_EVENT_SEEK -> isSeeking = false + MPVLib.mpvEventId.MPV_EVENT_END_FILE -> { + mediaLoaded = false + isPlaying = false + } + + MPVLib.mpvEventId.MPV_EVENT_FILE_LOADED, + MPVLib.mpvEventId.MPV_EVENT_PLAYBACK_RESTART -> { + mediaLoaded = true + commandQueue.trySend(PlayerCommand.GetPaused) + } + } + } + + override fun efEvent(err: String?) { + } + } + } + + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + MPVView(context, null).apply { + initialize(configDir, cacheDir) + addObserver(mpvObserver) + scope.launch(Dispatchers.Main.immediate) { + commandQueue + .consumeAsFlow() + .startWith(PlayerCommand.SetUri(uri)) + .onEach { command -> + solveCommand( + command = command, + uri = { uri }, + isPlayingChanged = { isPlaying = it }, + onVideoZoom = { videoZoom = it }, + videoOffset = { videoOffset }, + onVideoOffset = { videoOffset = it }, + onSpeedChanged = { speed = it }, + ) + } + .collect() + } + + } + }, + ) + + PlayerController( + enabled = { mediaLoaded }, + isPlaying = { isPlaying }, + title = { title }, + onBack = onBack, + onPlayStateChanged = { commandQueue.trySend(PlayerCommand.Paused(isPlaying)) }, + currentPosition = { currentPosition }, + duration = { duration }, + isSeeking = { isSeeking }, + onSeekTo = { + isSeeking = true + commandQueue.trySend(PlayerCommand.SeekTo(it)) + }, + onPlayOrPause = { commandQueue.trySend(PlayerCommand.PlayOrPause) }, + speed = { speed }, + onSpeedChanged = { commandQueue.trySend(PlayerCommand.SetSpeed(it)) }, + videoRotate = { videoRotate.toFloat() }, + videoZoom = { videoZoom }, + onVideoRotate = { + commandQueue.trySend(PlayerCommand.Rotate(it.toInt())) + }, + onVideoZoom = { + commandQueue.trySend(PlayerCommand.Zoom(it)) + }, + videoOffset = { videoOffset }, + onVideoOffset = { commandQueue.trySend(PlayerCommand.VideoOffset(it)) } + ) + + var needPlayWhenResume by rememberSaveable { mutableStateOf(false) } + + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + if (needPlayWhenResume) { + commandQueue.trySend(PlayerCommand.Paused(false)) + } + } + + Lifecycle.Event.ON_PAUSE -> { + needPlayWhenResume = isPlaying + if (isPlaying) { + commandQueue.trySend(PlayerCommand.Paused(true)) + } + } + + Lifecycle.Event.ON_DESTROY -> { + commandQueue.trySend(PlayerCommand.Destroy) + } + + else -> Unit + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } +} + +private val ControllerBarGray = Color(0xAD000000) +internal val ControllerLabelGray = Color(0x70000000) + +@Composable +private fun PlayerController( + enabled: () -> Boolean, + isPlaying: () -> Boolean, + title: () -> String, + onBack: () -> Unit, + onPlayStateChanged: () -> Unit, + isSeeking: () -> Boolean, + currentPosition: () -> Int, + duration: () -> Int, + onSeekTo: (Int) -> Unit, + onPlayOrPause: () -> Unit, + speed: () -> Float, + onSpeedChanged: (Float) -> Unit, + videoRotate: () -> Float, + onVideoRotate: (Float) -> Unit, + videoZoom: () -> Float, + onVideoZoom: (Float) -> Unit, + videoOffset: () -> Offset, + onVideoOffset: (Offset) -> Unit, +) { + var showController by rememberSaveable { mutableStateOf(true) } + var controllerWidth by remember { mutableIntStateOf(0) } + var controllerHeight by remember { mutableIntStateOf(0) } + var controllerLayoutCoordinates by remember { mutableStateOf(null) } + + val view = LocalView.current + val autoHideControllerRunnable = remember { Runnable { showController = false } } + val cancelAutoHideControllerRunnable = { view.removeCallbacks(autoHideControllerRunnable) } + val restartAutoHideControllerRunnable = { + cancelAutoHideControllerRunnable() + if (showController) { + view.postDelayed(autoHideControllerRunnable, 5000) + } + } + LaunchedEffect(showController) { restartAutoHideControllerRunnable() } + + var showSeekTimePreview by remember { mutableStateOf(false) } + var seekTimePreview by remember { mutableIntStateOf(0) } + + var showBrightnessPreview by remember { mutableStateOf(false) } + var brightnessValue by remember { mutableFloatStateOf(0f) } + var brightnessRange by remember { mutableStateOf(0f..0f) } + + var showVolumePreview by remember { mutableStateOf(false) } + var volumeValue by remember { mutableIntStateOf(0) } + var volumeRange by remember { mutableStateOf(0..0) } + + var showForwardRipple by remember { mutableStateOf(false) } + var forwardRippleStartControllerOffset by remember { mutableStateOf(Offset.Zero) } + var showBackwardRipple by remember { mutableStateOf(false) } + var backwardRippleStartControllerOffset by remember { mutableStateOf(Offset.Zero) } + + var isLongPressing by remember { mutableStateOf(false) } + CompositionLocalProvider(LocalContentColor provides Color.White) { + Box( + modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { + controllerWidth = it.size.width + controllerHeight = it.size.height + controllerLayoutCoordinates = it + } + .detectPressGestures( + controllerWidth = { controllerWidth }, + currentPosition = currentPosition, + onSeekTo = onSeekTo, + onPlayOrPause = onPlayOrPause, + showController = { showController }, + onShowControllerChanged = { showController = it }, + speed = speed, + onSpeedChanged = onSpeedChanged, + isLongPressing = { isLongPressing }, + isLongPressingChanged = { isLongPressing = it }, + onShowForwardRipple = { + forwardRippleStartControllerOffset = it + showForwardRipple = true + }, + onShowBackwardRipple = { + backwardRippleStartControllerOffset = it + showBackwardRipple = true + }, + cancelAutoHideControllerRunnable = cancelAutoHideControllerRunnable, + restartAutoHideControllerRunnable = restartAutoHideControllerRunnable, + ) + .detectControllerGestures( + enabled = enabled, + controllerWidth = { controllerWidth }, + controllerHeight = { controllerHeight }, + onShowBrightness = { showBrightnessPreview = it }, + onBrightnessRangeChanged = { brightnessRange = it }, + onBrightnessChanged = { brightnessValue = it }, + onShowVolume = { showVolumePreview = it }, + onVolumeRangeChanged = { volumeRange = it }, + onVolumeChanged = { volumeValue = it }, + currentPosition = currentPosition, + onShowSeekTimePreview = { showSeekTimePreview = it }, + onSeekTo = onSeekTo, + onTimePreviewChanged = { seekTimePreview = it }, + videoRotate = videoRotate, + onVideoRotate = onVideoRotate, + videoZoom = videoZoom, + onVideoZoom = onVideoZoom, + videoOffset = videoOffset, + onVideoOffset = onVideoOffset, + cancelAutoHideControllerRunnable = cancelAutoHideControllerRunnable, + restartAutoHideControllerRunnable = restartAutoHideControllerRunnable, + ) + ) { + // Forward ripple + AnimatedVisibility( + visible = showForwardRipple, + modifier = Modifier.align(Alignment.CenterEnd), + enter = fadeIn(animationSpec = spring(stiffness = Spring.StiffnessHigh)), + exit = fadeOut(), + ) { + ForwardRipple( + direct = ForwardRippleDirect.Forward, + text = "+10s", + icon = Icons.Rounded.FastForward, + controllerWidth = { controllerWidth }, + parentLayoutCoordinates = controllerLayoutCoordinates, + rippleStartControllerOffset = forwardRippleStartControllerOffset, + onHideRipple = { showForwardRipple = false }, + ) + } + // Backward ripple + AnimatedVisibility( + visible = showBackwardRipple, + modifier = Modifier.align(Alignment.CenterStart), + enter = fadeIn(animationSpec = spring(stiffness = Spring.StiffnessHigh)), + exit = fadeOut(), + ) { + ForwardRipple( + direct = ForwardRippleDirect.Backward, + text = "-10s", + icon = Icons.Rounded.FastRewind, + controllerWidth = { controllerWidth }, + parentLayoutCoordinates = controllerLayoutCoordinates, + rippleStartControllerOffset = backwardRippleStartControllerOffset, + onHideRipple = { showBackwardRipple = false }, + ) + } + // Auto hide box + Box { + AnimatedVisibility( + visible = showController, + enter = fadeIn(), + exit = fadeOut(), + ) { + ConstraintLayout(modifier = Modifier.fillMaxSize()) { + val (topBar, bottomBar, forward85s, resetTransform) = createRefs() + + TopBar( + modifier = Modifier.constrainAs(topBar) { top.linkTo(parent.top) }, + title = title(), + onBack = onBack, + ) + BottomBar( + modifier = Modifier.constrainAs(bottomBar) { bottom.linkTo(parent.bottom) }, + isPlaying = isPlaying, + onPlayStateChanged = onPlayStateChanged, + isSeeking = isSeeking, + currentPosition = currentPosition, + duration = duration, + onPositionChanged = onSeekTo, + onRestartAutoHideControllerRunnable = restartAutoHideControllerRunnable + ) + + // +85s button + if (LocalPlayerShow85sButton.current) { + Forward85s( + modifier = Modifier + .constrainAs(forward85s) { + bottom.linkTo(bottomBar.top) + end.linkTo(parent.end) + } + .padding(end = 50.dp), + onClick = { onSeekTo(currentPosition() + 85) }, + ) + } + + // Reset transform + if (videoZoom() != 1f || videoRotate() != 0f) { + ResetTransform( + modifier = Modifier.constrainAs(resetTransform) { + bottom.linkTo(bottomBar.top) + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + verticalBias = 1f + }, + enabled = enabled, + onClick = { + onVideoOffset(Offset.Zero) + onVideoZoom(1f) + onVideoRotate(0f) + } + ) + } + } + } + } + + // Seek time preview + if (showSeekTimePreview) { + SeekTimePreview(value = { seekTimePreview }, duration = duration) + } + // Brightness preview + if (showBrightnessPreview) { + BrightnessPreview(value = { brightnessValue }, range = { brightnessRange }) + } + // Volume preview + if (showVolumePreview) { + VolumePreview(value = { volumeValue }, range = { volumeRange }) + } + // Long press speed preview + if (isLongPressing) { + LongPressSpeedPreview(speed = speed) + } + } + } +} + +@Composable +private fun BoxScope.SeekTimePreview( + value: () -> Int, + duration: () -> Int, +) { + Row( + modifier = Modifier + .align(Alignment.Center) + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .padding(horizontal = 16.dp, vertical = 10.dp), + ) { + Text( + text = value() + .coerceIn(0..duration()) + .toDurationString(), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + Text( + modifier = Modifier.padding(horizontal = 6.dp), + text = "/", + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + Text( + text = duration().toDurationString(), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + } +} + +@Composable +private fun BoxScope.BrightnessPreview( + value: () -> Float, + range: () -> ClosedFloatingPointRange, +) { + Row( + modifier = Modifier + .align(Alignment.Center) + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val start = range().start + val endInclusive = range().endInclusive + val length = endInclusive - start + val icon = when (value()) { + in start..start + length / 3 -> Icons.Rounded.BrightnessLow + in start + length * 2 / 3..endInclusive -> Icons.Rounded.BrightnessHigh + else -> Icons.Rounded.BrightnessMedium + } + val percentValue = (value() - start) / length + Icon(modifier = Modifier.size(30.dp), imageVector = icon, contentDescription = null) + LinearProgressIndicator( + progress = { percentValue }, + modifier = Modifier + .padding(horizontal = 16.dp) + .width(100.dp), + drawStopIndicator = null, + ) + Text( + modifier = Modifier.animateContentSize(), + text = percentValue.toPercentage(format = "%.0f%%"), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + } +} + +@Composable +private fun BoxScope.VolumePreview( + value: () -> Int, + range: () -> IntRange, +) { + Row( + modifier = Modifier + .align(Alignment.Center) + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val start = range().first + val endInclusive = range().last + val length = endInclusive - start + val v = value() + val icon = when { + v <= start -> Icons.AutoMirrored.Rounded.VolumeMute + v in start..start + length / 2 -> Icons.AutoMirrored.Rounded.VolumeDown + else -> Icons.AutoMirrored.Rounded.VolumeUp + } + val percentValue = (value() - start).toFloat() / length + Icon(modifier = Modifier.size(30.dp), imageVector = icon, contentDescription = null) + LinearProgressIndicator( + progress = { percentValue }, + modifier = Modifier + .padding(horizontal = 16.dp) + .width(100.dp), + drawStopIndicator = null, + ) + Text( + modifier = Modifier.animateContentSize(), + text = percentValue.toPercentage(format = "%.0f%%"), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + } +} + +@Composable +private fun BoxScope.LongPressSpeedPreview(speed: () -> Float) { + Row( + modifier = Modifier + .align(BiasAlignment(0f, -0.6f)) + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(30.dp), + imageVector = Icons.Rounded.FastForward, + contentDescription = stringResource(id = R.string.player_long_press_playback_speed), + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = "${speed()}x", + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + } +} + +@Composable +private fun ResetTransform( + modifier: Modifier = Modifier, + enabled: () -> Boolean, + onClick: () -> Unit, +) { + Text( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .clickable(enabled = enabled(), onClick = onClick) + .padding(horizontal = 16.dp, vertical = 10.dp), + text = stringResource(id = R.string.player_reset_zoom), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(16f, TextUnitType.Sp), + color = Color.White, + ) +} + +@Composable +private fun Forward85s( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Text( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 10.dp), + text = stringResource(id = R.string.player_forward_85s), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(16f, TextUnitType.Sp), + color = Color.White, + ) +} + +@Composable +private fun TopBar( + modifier: Modifier = Modifier, + title: String, + onBack: () -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf(ControllerBarGray, Color.Transparent) + ) + ) + .windowInsetsPadding( + WindowInsets.displayCutout.only( + WindowInsetsSides.Horizontal + WindowInsetsSides.Top + ) + ) + .padding(bottom = 30.dp) + .padding(horizontal = 6.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .clip(CircleShape) + .size(56.dp) + .clickable(onClick = onBack) + .padding(15.dp), + imageVector = Icons.Outlined.ArrowBackIosNew, + contentDescription = stringResource(id = R.string.back), + ) + Spacer(modifier = Modifier.width(3.dp)) + Text( + modifier = Modifier + .weight(1f) + .basicMarquee(), + text = title, + style = MaterialTheme.typography.titleMedium, + color = Color.White, + maxLines = 1, + ) + Spacer(modifier = Modifier.width(3.dp)) + } +} + +@Composable +private fun BottomBar( + modifier: Modifier = Modifier, + isPlaying: () -> Boolean, + onPlayStateChanged: () -> Unit, + isSeeking: () -> Boolean, + currentPosition: () -> Int, + duration: () -> Int, + onPositionChanged: (position: Int) -> Unit, + onRestartAutoHideControllerRunnable: () -> Unit, +) { + Column( + modifier = modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf(Color.Transparent, ControllerBarGray) + ) + ) + .windowInsetsPadding( + WindowInsets.displayCutout.only( + WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom + ) + ) + .padding(top = 30.dp) + .padding(horizontal = 6.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val sliderInteractionSource = remember { MutableInteractionSource() } + var sliderValue by rememberSaveable { mutableFloatStateOf(currentPosition().toFloat()) } + var valueIsChanging by rememberSaveable { mutableStateOf(false) } + if (!valueIsChanging && !isSeeking() && sliderValue != currentPosition().toFloat()) { + sliderValue = currentPosition().toFloat() + } + Text( + text = currentPosition().toDurationString(), + style = MaterialTheme.typography.labelLarge, + color = Color.White, + ) + Slider( + modifier = Modifier + .padding(6.dp) + .height(10.dp) + .weight(1f), + value = sliderValue, + onValueChange = { + valueIsChanging = true + onRestartAutoHideControllerRunnable() + sliderValue = it + }, + onValueChangeFinished = { + onPositionChanged(sliderValue.toInt()) + valueIsChanging = false + }, + valueRange = 0f..duration().toFloat(), + interactionSource = sliderInteractionSource, + thumb = { + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.Center, + ) { + Spacer( + modifier = Modifier + .padding(horizontal = 3.dp) + .clip(CircleShape) + .size(14.dp) + .background( + MaterialTheme.colorScheme.primary + .alwaysLight(true) + .toHct() + .withTone(90.0) + .toColor() + ) + ) + } + }, + track = { + Spacer( + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .fillMaxWidth() + .height(3.dp) + .background(SliderDefaults.colors().activeTrackColor) + ) + }, + ) + Text( + text = duration().toDurationString(), + style = MaterialTheme.typography.labelLarge, + color = Color.White, + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 3.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier + .clip(CircleShape) + .size(52.dp) + .clickable(onClick = onPlayStateChanged) + .padding(9.dp), + imageVector = if (isPlaying()) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + contentDescription = stringResource(if (isPlaying()) R.string.pause else R.string.play), + ) + Spacer(modifier = Modifier.width(10.dp)) + } + } +} + +fun Int.toDurationString(sign: Boolean = false): String { + if (sign) return (if (this >= 0) "+" else "-") + abs(this).toDurationString() + + val hours = this / 3600 + val minutes = this % 3600 / 60 + val seconds = this % 60 + return if (hours == 0) "%02d:%02d".format(minutes, seconds) + else "%d:%02d:%02d".format(hours, minutes, seconds) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/PointerInputDetector.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/PointerInputDetector.kt new file mode 100644 index 00000000..de182184 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/PointerInputDetector.kt @@ -0,0 +1,254 @@ +package com.skyd.anivu.ui.mpv + +import android.content.Context +import android.media.AudioManager +import android.os.Build +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import com.skyd.anivu.ext.activity +import com.skyd.anivu.ext.detectDoubleFingerTransformGestures +import com.skyd.anivu.ext.getScreenBrightness +import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference +import com.skyd.anivu.ui.local.LocalPlayerDoubleTap +import kotlin.math.abs + +private val inStatusBarArea: PointerInputScope.(y: Float) -> Boolean = { y -> + y / density <= 60 +} + +@Composable +internal fun Modifier.detectPressGestures( + controllerWidth: () -> Int, + currentPosition: () -> Int, + onSeekTo: (Int) -> Unit, + onPlayOrPause: () -> Unit, + speed: () -> Float, + onSpeedChanged: (Float) -> Unit, + showController: () -> Boolean, + onShowControllerChanged: (Boolean) -> Unit, + isLongPressing: () -> Boolean, + isLongPressingChanged: (Boolean) -> Unit, + onShowForwardRipple: (Offset) -> Unit, + onShowBackwardRipple: (Offset) -> Unit, + cancelAutoHideControllerRunnable: () -> Boolean, + restartAutoHideControllerRunnable: () -> Unit, +): Modifier { + var beforeLongPressingSpeed by remember { mutableFloatStateOf(speed()) } + + val playerDoubleTap = LocalPlayerDoubleTap.current + val onDoubleTapPausePlay: () -> Unit = remember { { onPlayOrPause() } } + + val onDoubleTapBackwardForward: (Offset) -> Unit = { offset -> + if (offset.x < controllerWidth() / 2f) { + onSeekTo(currentPosition() - 10) // -10s. + onShowBackwardRipple(offset) + } else { + onSeekTo(currentPosition() + 10) // +10s. + onShowForwardRipple(offset) + } + } + val onDoubleTapBackwardPausePlayForward: (Offset) -> Unit = { offset -> + if (offset.x <= controllerWidth() * 0.25f) { + onSeekTo(currentPosition() - 10) // -10s. + onShowBackwardRipple(offset) + } else if (offset.x >= controllerWidth() * 0.75f) { + onSeekTo(currentPosition() + 10) // +10s. + onShowForwardRipple(offset) + } else { + onDoubleTapPausePlay() + } + } + + val onDoubleTap: (Offset) -> Unit = { offset -> + when (playerDoubleTap) { + PlayerDoubleTapPreference.BACKWARD_FORWARD -> onDoubleTapBackwardForward(offset) + PlayerDoubleTapPreference.BACKWARD_PAUSE_PLAY_FORWARD -> + onDoubleTapBackwardPausePlayForward(offset) + + else -> onDoubleTapPausePlay() + } + } + + return pointerInput(playerDoubleTap) { + detectTapGestures( + onLongPress = { + beforeLongPressingSpeed = speed() + isLongPressingChanged(true) + onSpeedChanged(3f) + }, + onDoubleTap = { + restartAutoHideControllerRunnable() + onDoubleTap(it) + }, + onPress = { + tryAwaitRelease() + if (isLongPressing()) { + isLongPressingChanged(false) + onSpeedChanged(beforeLongPressingSpeed) + } + }, + onTap = { + cancelAutoHideControllerRunnable() + onShowControllerChanged(!showController()) + } + ) + } +} + +@Composable +internal fun Modifier.detectControllerGestures( + enabled: () -> Boolean, + controllerWidth: () -> Int, + controllerHeight: () -> Int, + onShowBrightness: (Boolean) -> Unit, + onBrightnessRangeChanged: (ClosedFloatingPointRange) -> Unit, + onBrightnessChanged: (Float) -> Unit, + onShowVolume: (Boolean) -> Unit, + onVolumeRangeChanged: (IntRange) -> Unit, + onVolumeChanged: (Int) -> Unit, + currentPosition: () -> Int, + onShowSeekTimePreview: (Boolean) -> Unit, + onTimePreviewChanged: (Int) -> Unit, + onSeekTo: (Int) -> Unit, + videoRotate: () -> Float, + onVideoRotate: (Float) -> Unit, + videoZoom: () -> Float, + onVideoZoom: (Float) -> Unit, + videoOffset: () -> Offset, + onVideoOffset: (Offset) -> Unit, + cancelAutoHideControllerRunnable: () -> Boolean, + restartAutoHideControllerRunnable: () -> Unit, +): Modifier { + if (!enabled()) { + onShowBrightness(false) + onShowVolume(false) + onShowSeekTimePreview(false) + restartAutoHideControllerRunnable() + return this + } + + var pointerStartX by rememberSaveable { mutableFloatStateOf(0f) } + var pointerStartY by rememberSaveable { mutableFloatStateOf(0f) } + + val context = LocalContext.current + val activity = remember(context) { context.activity } + val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } + val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + val minVolume = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC) + } else 0 + + var startBrightness by rememberSaveable { mutableFloatStateOf(0f) } + var startVolume by rememberSaveable { mutableIntStateOf(0) } + + var seekTimePreviewStartPosition by remember { mutableIntStateOf(0) } + var seekTimePreviewPositionDelta by remember { mutableIntStateOf(0) } + + return pointerInput(Unit) { + detectDoubleFingerTransformGestures( + onVerticalDragStart = onVerticalDragStart@{ + cancelAutoHideControllerRunnable() + pointerStartX = it.x + pointerStartY = it.y + if (inStatusBarArea(it.y)) return@onVerticalDragStart + when (pointerStartX) { + in 0f..controllerWidth() / 3f -> { + onBrightnessRangeChanged(0.01f..1f) + startBrightness = activity.window.attributes.apply { + if (screenBrightness <= 0.00f) { + val brightness = activity.getScreenBrightness() + if (brightness != null) { + screenBrightness = brightness / 255.0f + activity.window.setAttributes(this) + } + } + }.screenBrightness + onBrightnessChanged(startBrightness) + onShowBrightness(true) + } + + in controllerWidth() * 2 / 3f..controllerWidth().toFloat() -> { + onVolumeRangeChanged(minVolume..maxVolume) + startVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + onVolumeChanged(startVolume) + onShowVolume(true) + } + } + }, + onVerticalDragEnd = { + restartAutoHideControllerRunnable() + onShowBrightness(false) + onShowVolume(false) + }, + onVerticalDragCancel = { + restartAutoHideControllerRunnable() + onShowBrightness(false) + onShowVolume(false) + }, + onVerticalDrag = onVerticalDrag@{ change, _ -> + val deltaY = change.position.y - pointerStartY + if (inStatusBarArea(pointerStartY) || abs(deltaY) < 50) return@onVerticalDrag + + when (pointerStartX) { + in 0f..controllerWidth() / 3f -> { + val layoutParams = activity.window.attributes + layoutParams.screenBrightness = + (startBrightness - deltaY / controllerHeight()) + .coerceIn(0.01f..1f) + activity.window.setAttributes(layoutParams) + onBrightnessChanged(layoutParams.screenBrightness) + } + + in controllerWidth() * 2 / 3f..controllerWidth().toFloat() -> { + val desiredVolume = (startVolume - deltaY / controllerHeight() * + 1.2f * (maxVolume - minVolume)).toInt() + .coerceIn(minVolume..maxVolume) + onVolumeChanged(desiredVolume) + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, desiredVolume, 0) + } + } + }, + onHorizontalDragStart = onHorizontalDragStart@{ + cancelAutoHideControllerRunnable() + pointerStartX = it.x + pointerStartY = it.y + if (inStatusBarArea(it.y)) return@onHorizontalDragStart + seekTimePreviewStartPosition = currentPosition() + seekTimePreviewPositionDelta = 0 + onShowSeekTimePreview(true) + }, + onHorizontalDragEnd = onHorizontalDragEnd@{ + onShowSeekTimePreview(false) + restartAutoHideControllerRunnable() + if (inStatusBarArea(pointerStartY)) return@onHorizontalDragEnd + onSeekTo(seekTimePreviewStartPosition + seekTimePreviewPositionDelta) + }, + onHorizontalDragCancel = { + onShowSeekTimePreview(false) + restartAutoHideControllerRunnable() + }, + onHorizontalDrag = onHorizontalDrag@{ change, _ -> + if (inStatusBarArea(pointerStartY)) return@onHorizontalDrag + seekTimePreviewPositionDelta = + ((change.position.x - pointerStartX) / density / 8).toInt() + onTimePreviewChanged(seekTimePreviewStartPosition + seekTimePreviewPositionDelta) + }, + onGesture = onGesture@{ _: Offset, pan: Offset, zoom: Float, rotation: Float -> + onVideoOffset(videoOffset() + pan) + onVideoRotate(videoRotate() + rotation) + onVideoZoom(videoZoom() * zoom) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/player/ForwardRippleView.kt b/app/src/main/java/com/skyd/anivu/ui/player/ForwardRippleView.kt deleted file mode 100644 index e71c95d3..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/player/ForwardRippleView.kt +++ /dev/null @@ -1,243 +0,0 @@ -package com.skyd.anivu.ui.player - -import android.content.Context -import android.content.res.ColorStateList -import android.content.res.TypedArray -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.RectF -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.Gravity.CENTER -import android.view.View -import android.view.animation.AlphaAnimation -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.Space -import android.widget.TextView -import com.google.android.material.textview.MaterialTextView -import com.skyd.anivu.R -import com.skyd.anivu.ext.dp -import com.skyd.anivu.ext.isRunning -import com.skyd.anivu.ext.toDegrees -import kotlin.math.abs -import kotlin.math.atan2 - - -class ForwardRippleView : LinearLayout { - enum class RippleCenter { - StartCenter, EndCenter - } - - private lateinit var textView: TextView - private lateinit var imageView: ImageView - private lateinit var space: Space - - private var rippleCenter: RippleCenter = RippleCenter.StartCenter - private val ripplePaint: Paint = Paint().apply { - setColor(Color.parseColor("#77000000")) - style = Paint.Style.FILL - isAntiAlias = true - } - private lateinit var originRippleRect: RectF - private lateinit var rippleRect: RectF - private var startAngle: Float = 0f - private var sweepAngle: Float = 360f - private var maxRippleRadius = 0f - private var rippleRadiusExpandSpeed = 35f.dp - private var hideAnimation = AlphaAnimation(1f, 0f).apply { duration = 400L } - - constructor(context: Context?) : this(context, null) - constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) - : this(context, attrs, defStyleAttr, 0) - - constructor( - context: Context?, - attrs: AttributeSet?, - defStyleAttr: Int, - defStyleRes: Int - ) : super(context, attrs, defStyleAttr, defStyleRes) { - post { super.setVisibility(GONE) } - orientation = VERTICAL - gravity = CENTER - val a = context!!.obtainStyledAttributes( - attrs, R.styleable.ForwardRippleView, defStyleAttr, defStyleRes - ) - initRipple(a) - initTextView(a) - initImageView(a) - initSpace() - addView(imageView) - addView(space) - addView(textView) - - setWillNotDraw(false) - - a.recycle() - } - - private fun initRipple(a: TypedArray) { - if (a.hasValue(R.styleable.ForwardRippleView_center)) { - when (a.getInt(R.styleable.ForwardRippleView_center, 0)) { - 0 -> rippleCenter = RippleCenter.StartCenter - 1 -> rippleCenter = RippleCenter.EndCenter - } - } - if (a.hasValue(R.styleable.ForwardRippleView_expandSpeed)) { - rippleRadiusExpandSpeed = a.getDimension( - R.styleable.ForwardRippleView_expandSpeed, rippleRadiusExpandSpeed - ) - } - if (a.hasValue(R.styleable.ForwardRippleView_rippleColor)) { - ripplePaint.setColor( - a.getColor(R.styleable.ForwardRippleView_rippleColor, ripplePaint.color) - ) - } - } - - private fun initTextView(a: TypedArray) { - textView = MaterialTextView( - context!!, - null, - 0, - ).apply { - layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) - setTextAppearance( - com.google.android.material.R.style.TextAppearance_Material3_LabelLarge - ) - if (a.hasValue(R.styleable.ForwardRippleView_text)) { - text = a.getText(R.styleable.ForwardRippleView_text) - } - if (a.hasValue(R.styleable.ForwardRippleView_textColor)) { - setTextColor(a.getColorStateList(R.styleable.ForwardRippleView_textColor)) - } else { - setTextColor(Color.WHITE) - } - } - } - - private fun initImageView(a: TypedArray) { - imageView = ImageView(context).apply { - layoutParams = LayoutParams(50.dp, 50.dp) - if (a.hasValue(R.styleable.ForwardRippleView_drawable)) { - setImageDrawable(a.getDrawable(R.styleable.ForwardRippleView_drawable)) - } - imageTintList = if (a.hasValue(R.styleable.ForwardRippleView_drawableTint)) { - a.getColorStateList(R.styleable.ForwardRippleView_drawableTint) - } else { - ColorStateList.valueOf(Color.WHITE) - } - } - } - - private fun initSpace() { - space = Space(context).apply { - layoutParams = LayoutParams(0, 10.dp) - } - } - - override fun setVisibility(visibility: Int) { - when (visibility) { - VISIBLE -> { - hideAnimation.cancel() - super.setVisibility(VISIBLE) - rippleRect = RectF(originRippleRect) - invalidate() - } - - GONE -> { - if (!hideAnimation.isRunning && getVisibility() == VISIBLE) { - startAnimation(hideAnimation) - } - super.setVisibility(GONE) - } - - INVISIBLE -> { - if (!hideAnimation.isRunning && getVisibility() == VISIBLE) { - startAnimation(hideAnimation) - } - super.setVisibility(INVISIBLE) - } - } - } - - @JvmOverloads - fun visible(time: Long = 700L) { - visibility = View.VISIBLE - postDelayed({ visibility = View.GONE }, time) - } - - override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { - super.onSizeChanged(w, h, oldw, oldh) - - when (rippleCenter) { - RippleCenter.StartCenter -> { - // origin distance from the centerX to view border is w.toFloat() - originRippleRect = RectF().also { rect -> - rect.left = -w.toFloat() * 2 - rect.right = 0f - // use rect.width() because we want the rect is a square - rect.top = (h - rect.width()) / 2f - rect.bottom = (h + rect.width()) / 2f - } - rippleRect = RectF(originRippleRect) - maxRippleRadius = w - originRippleRect.centerX() - - val halfDeltaAngle = atan2(h / 2f, -rippleRect.centerX()).toDegrees() - startAngle = -halfDeltaAngle - sweepAngle = abs(halfDeltaAngle * 2) - } - - RippleCenter.EndCenter -> { - // origin distance from the centerX to view border is w.toFloat() - originRippleRect = RectF().also { rect -> - rect.left = w.toFloat() - rect.right = 3 * w.toFloat() - // use rect.width() because we want the rect is a square - rect.top = (h - rect.width()) / 2f - rect.bottom = (h + rect.width()) / 2f - } - rippleRect = RectF(originRippleRect) - maxRippleRadius = originRippleRect.centerX() - - val halfDeltaAngle = atan2(h / 2f, rippleRect.centerX() - w).toDegrees() - startAngle = 180 - halfDeltaAngle - sweepAngle = abs(halfDeltaAngle * 2) - } - } - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - canvas.drawArc(rippleRect, startAngle, sweepAngle, true, ripplePaint) - if (rippleRect.width() / 2 < maxRippleRadius) { - var delta = rippleRadiusExpandSpeed - val newWidth = rippleRect.right - rippleRect.left + 2 * delta - if (newWidth / 2f > maxRippleRadius) { - delta -= newWidth / 2f - maxRippleRadius - } - rippleRect.set( - /* left = */ rippleRect.left - delta, - /* top = */ rippleRect.top - delta, - /* right = */ rippleRect.right + delta, - /* bottom = */ rippleRect.bottom + delta - ) - invalidate() - } - } - - var text: CharSequence - get() = textView.text - set(value) { - textView.text = value - } - - var drawable: Drawable? - get() = imageView.drawable - set(value) { - imageView.setImageDrawable(value) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlView.java b/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlView.java deleted file mode 100644 index 089a02f9..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlView.java +++ /dev/null @@ -1,2568 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.skyd.anivu.ui.player; - -import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM; -import static androidx.media3.common.Player.COMMAND_GET_TIMELINE; -import static androidx.media3.common.Player.COMMAND_GET_TRACKS; -import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE; -import static androidx.media3.common.Player.COMMAND_SEEK_BACK; -import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD; -import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; -import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM; -import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT; -import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; -import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; -import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; -import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH; -import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS; -import static androidx.media3.common.Player.EVENT_AVAILABLE_COMMANDS_CHANGED; -import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED; -import static androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION; -import static androidx.media3.common.Player.EVENT_MEDIA_METADATA_CHANGED; -import static androidx.media3.common.Player.EVENT_METADATA; -import static androidx.media3.common.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED; -import static androidx.media3.common.Player.EVENT_PLAYBACK_STATE_CHANGED; -import static androidx.media3.common.Player.EVENT_PLAY_WHEN_READY_CHANGED; -import static androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY; -import static androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED; -import static androidx.media3.common.Player.EVENT_SEEK_BACK_INCREMENT_CHANGED; -import static androidx.media3.common.Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED; -import static androidx.media3.common.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED; -import static androidx.media3.common.Player.EVENT_TIMELINE_CHANGED; -import static androidx.media3.common.Player.EVENT_TRACKS_CHANGED; -import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.common.util.Util.castNonNull; -import static androidx.media3.common.util.Util.getDrawable; -import static androidx.media3.common.util.Util.msToUs; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Looper; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.DisplayCutout; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowInsets; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.PopupWindow; -import android.widget.TextView; - -import androidx.annotation.DrawableRes; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.core.content.res.ResourcesCompat; -import androidx.media3.common.C; -import androidx.media3.common.Format; -import androidx.media3.common.MediaItem; -import androidx.media3.common.MediaLibraryInfo; -import androidx.media3.common.MediaMetadata; -import androidx.media3.common.Player; -import androidx.media3.common.Player.Events; -import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackSelectionOverride; -import androidx.media3.common.TrackSelectionParameters; -import androidx.media3.common.Tracks; -import androidx.media3.common.util.Assertions; -import androidx.media3.common.util.RepeatModeUtil; -import androidx.media3.common.util.UnstableApi; -import androidx.media3.common.util.Util; -import androidx.media3.ui.DefaultTimeBar; -import androidx.media3.ui.DefaultTrackNameProvider; -import androidx.media3.ui.R; -import androidx.media3.ui.TimeBar; -import androidx.media3.ui.TrackNameProvider; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.progressindicator.LinearProgressIndicator; -import com.google.common.collect.ImmutableList; -import com.skyd.anivu.ext.IOExtKt; -import com.skyd.anivu.ext.PopupWindowExtKt; -import com.skyd.anivu.ext.ViewExtKt; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Formatter; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * A view for controlling {@link Player} instances. - * - *

A {@code PlayerControlView} can be customized by setting attributes (or calling corresponding - * methods), or overriding drawables. - * - *

Attributes

- *

- * The following attributes can be set on a {@code PlayerControlView} when used in a layout XML - * file: - * - *

    - *
  • {@code show_timeout} - The time between the last user interaction and the controls - * being automatically hidden, in milliseconds. Use zero if the controls should not - * automatically timeout. - *
      - *
    • Corresponding method: {@link #setShowTimeoutMs(int)} - *
    • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS} - *
    - *
  • {@code show_rewind_button} - Whether the rewind button is shown. - *
      - *
    • Corresponding method: {@link #setShowRewindButton(boolean)} - *
    • Default: true - *
    - *
  • {@code show_fastforward_button} - Whether the fast forward button is shown. - *
      - *
    • Corresponding method: {@link #setShowFastForwardButton(boolean)} - *
    • Default: true - *
    - *
  • {@code show_previous_button} - Whether the previous button is shown. - *
      - *
    • Corresponding method: {@link #setShowPreviousButton(boolean)} - *
    • Default: true - *
    - *
  • {@code show_next_button} - Whether the next button is shown. - *
      - *
    • Corresponding method: {@link #setShowNextButton(boolean)} - *
    • Default: true - *
    - *
  • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat - * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all}, - * or {@code one|all}. - *
      - *
    • Corresponding method: {@link #setRepeatToggleModes(int)} - *
    • Default: {@link #DEFAULT_REPEAT_TOGGLE_MODES} - *
    - *
  • {@code show_shuffle_button} - Whether the shuffle button is shown. - *
      - *
    • Corresponding method: {@link #setShowShuffleButton(boolean)} - *
    • Default: false - *
    - *
  • {@code show_subtitle_button} - Whether the subtitle button is shown. - *
      - *
    • Corresponding method: {@link #setShowSubtitleButton(boolean)} - *
    • Default: false - *
    - *
  • {@code animation_enabled} - Whether an animation is used to show and hide the - * playback controls. - *
      - *
    • Corresponding method: {@link #setAnimationEnabled(boolean)} - *
    • Default: true - *
    - *
  • {@code time_bar_min_update_interval} - Specifies the minimum interval between time - * bar position updates. - *
      - *
    • Corresponding method: {@link #setTimeBarMinUpdateInterval(int)} - *
    • Default: {@link #DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS} - *
    - *
  • All attributes that can be set on {@link DefaultTimeBar} can also be set on a {@code - * PlayerControlView}, and will be propagated to the inflated {@link DefaultTimeBar}. - *
- * - *

Overriding drawables

- *

- * The drawables used by {@code PlayerControlView} can be overridden by drawables with the same - * names defined in your application. The drawables that can be overridden are: - * - *

    - *
  • {@code exo_styled_controls_play} - The play icon. - *
  • {@code exo_styled_controls_pause} - The pause icon. - *
  • {@code exo_styled_controls_rewind} - The background of rewind icon. - *
  • {@code exo_styled_controls_fastforward} - The background of fast forward icon. - *
  • {@code exo_styled_controls_previous} - The previous icon. - *
  • {@code exo_styled_controls_next} - The next icon. - *
  • {@code exo_styled_controls_repeat_off} - The repeat icon for {@link - * Player#REPEAT_MODE_OFF}. - *
  • {@code exo_styled_controls_repeat_one} - The repeat icon for {@link - * Player#REPEAT_MODE_ONE}. - *
  • {@code exo_styled_controls_repeat_all} - The repeat icon for {@link - * Player#REPEAT_MODE_ALL}. - *
  • {@code exo_styled_controls_shuffle_off} - The shuffle icon when shuffling is - * disabled. - *
  • {@code exo_styled_controls_shuffle_on} - The shuffle icon when shuffling is enabled. - *
  • {@code exo_styled_controls_vr} - The VR icon. - *
- * - * @noinspection ALL - */ -@UnstableApi -public class PlayerControlView extends FrameLayout { - - static { - MediaLibraryInfo.registerModule("media3.ui"); - } - - /** - * @deprecated Register a {@link PlayerView.ControllerVisibilityListener} via {@link - * PlayerView#setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)} - * instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView} - * is deprecated. - */ - @Deprecated - public interface VisibilityListener { - - /** - * Called when the visibility changes. - * - * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}. - */ - void onVisibilityChange(int visibility); - } - - /** - * Listener to be notified when progress has been updated. - */ - public interface ProgressUpdateListener { - - /** - * Called when progress needs to be updated. - * - * @param position The current position. - * @param bufferedPosition The current buffered position. - */ - void onProgressUpdate(long position, long bufferedPosition); - } - - /** - * @deprecated Register a {@link PlayerView.FullscreenButtonClickListener} via {@link - * PlayerView#setFullscreenButtonClickListener(PlayerView.FullscreenButtonClickListener)} - * instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView} - * is deprecated. - */ - @Deprecated - public interface OnFullScreenModeChangedListener { - /** - * Called to indicate a fullscreen mode change. - * - * @param isFullScreen {@code true} if the video rendering surface should be fullscreen {@code - * false} otherwise. - */ - void onFullScreenModeChanged(boolean isFullScreen); - } - - /** - * The default show timeout, in milliseconds. - */ - public static final int DEFAULT_SHOW_TIMEOUT_MS = 5_000; - - /** - * The default repeat toggle modes. - */ - public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = - RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; - - /** - * The default minimum interval between time bar position updates. - */ - public static final int DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS = 200; - - /** - * The maximum number of windows that can be shown in a multi-window time bar. - */ - public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100; - - /** - * The maximum interval between time bar position updates. - */ - private static final int MAX_UPDATE_INTERVAL_MS = 1_000; - - // LINT.IfChange(playback_speeds) - private static final float[] PLAYBACK_SPEEDS = - new float[]{0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 2f}; - - private static final int SETTINGS_PLAYBACK_SPEED_POSITION = 0; - private static final int SETTINGS_AUDIO_TRACK_SELECTION_POSITION = 1; - - private final PlayerControlViewLayoutManager controlViewLayoutManager; - private final Resources resources; - private final ComponentListener componentListener; - - @SuppressWarnings("deprecation") // Using the deprecated type for now. - private final CopyOnWriteArrayList visibilityListeners; - - private final RecyclerView settingsView; - private final SettingsAdapter settingsAdapter; - private final PlaybackSpeedAdapter playbackSpeedAdapter; - private final TextTrackSelectionAdapter textTrackSelectionAdapter; - private final AudioTrackSelectionAdapter audioTrackSelectionAdapter; - // TODO(insun): Add setTrackNameProvider to use customized track name provider. - private final TrackNameProvider trackNameProvider; - private final PopupWindow settingsWindow; - private final int settingsWindowMargin; - - @Nullable - private final ViewGroup topBar; - @Nullable - private final ViewGroup bottomBar; - @Nullable - private final View previousButton; - @Nullable - private final View nextButton; - @Nullable - private final View playPauseButton; - @Nullable - private final View fastForwardButton; - @Nullable - private final View rewindButton; - @Nullable - private final TextView fastForwardButtonTextView; - @Nullable - private final TextView rewindButtonTextView; - @Nullable - private final ImageView repeatToggleButton; - @Nullable - private final ImageView shuffleButton; - @Nullable - private final View vrButton; - @Nullable - private final ImageView subtitleButton; - @Nullable - private final ImageView fullScreenButton; - @Nullable - private final ImageView minimalFullScreenButton; - @Nullable - private final View settingsButton; - @Nullable - private final TextView titleView; - @Nullable - private final View backButton; - @Nullable - private final View longPressPlaybackSpeedView; - @Nullable - private final TextView seekPreviewView; - @Nullable - private final View resetZoomView; - @Nullable - private final View forward85sView; - @Nullable - private final View screenshotView; - @Nullable - private final ViewGroup brightnessControlsView; - @Nullable - private final LinearProgressIndicator brightnessProgressView; - @Nullable - private final ViewGroup volumeControlsView; - @Nullable - private final LinearProgressIndicator volumeProgressView; - @Nullable - private final ForwardRippleView backwardRipple; - @Nullable - private final ForwardRippleView forwardRipple; - @Nullable - private final View playbackSpeedButton; - @Nullable - private final View audioTrackButton; - @Nullable - private final TextView durationView; - @Nullable - private final TextView positionView; - @Nullable - private final TimeBar timeBar; - private final StringBuilder formatBuilder; - private final Formatter formatter; - private final Timeline.Period period; - private final Timeline.Window window; - private final Runnable updateProgressAction; - - private final Drawable repeatOffButtonDrawable; - private final Drawable repeatOneButtonDrawable; - private final Drawable repeatAllButtonDrawable; - private final String repeatOffButtonContentDescription; - private final String repeatOneButtonContentDescription; - private final String repeatAllButtonContentDescription; - private final Drawable shuffleOnButtonDrawable; - private final Drawable shuffleOffButtonDrawable; - private final float buttonAlphaEnabled; - private final float buttonAlphaDisabled; - private final String shuffleOnContentDescription; - private final String shuffleOffContentDescription; - private final Drawable subtitleOnButtonDrawable; - private final Drawable subtitleOffButtonDrawable; - private final String subtitleOnContentDescription; - private final String subtitleOffContentDescription; - private final Drawable fullScreenExitDrawable; - private final Drawable fullScreenEnterDrawable; - private final String fullScreenExitContentDescription; - private final String fullScreenEnterContentDescription; - - @Nullable - private Player player; - @Nullable - private ProgressUpdateListener progressUpdateListener; - - @Nullable - private OnClickListener onBackButtonClickListener; - @Nullable - private OnClickListener onResetZoomButtonClickListener; - - @Nullable - private OnFullScreenModeChangedListener onFullScreenModeChangedListener; - private boolean isFullScreen; - private boolean isAttachedToWindow; - private boolean showMultiWindowTimeBar; - private boolean showPlayButtonIfSuppressed; - private boolean multiWindowTimeBar; - private boolean scrubbing; - private int showTimeoutMs; - private int timeBarMinUpdateIntervalMs; - private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; - private long[] adGroupTimesMs; - private boolean[] playedAdGroups; - private long[] extraAdGroupTimesMs; - private boolean[] extraPlayedAdGroups; - private long currentWindowOffset; - - private boolean needToHideBars; - - private boolean isZoom; - - public PlayerControlView(Context context) { - this(context, /* attrs= */ null); - } - - public PlayerControlView(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, /* defStyleAttr= */ 0); - } - - public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, attrs); - } - - // TODO: b/301602565 - See if there's a reasonable non-null root view group we could use below to - // resolve InflateParams lint. - @SuppressWarnings({ - "InflateParams", - "nullness:argument", - "nullness:assignment", - "nullness:method.invocation", - "nullness:methodref.receiver.bound" - }) - public PlayerControlView( - Context context, - @Nullable AttributeSet attrs, - int defStyleAttr, - @Nullable AttributeSet playbackAttrs) { - super(context, attrs, defStyleAttr); - int controllerLayoutId = R.layout.exo_player_control_view; - showPlayButtonIfSuppressed = true; - showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; - repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; - timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS; - boolean showRewindButton = true; - boolean showFastForwardButton = true; - boolean showPreviousButton = true; - boolean showNextButton = true; - boolean showShuffleButton = false; - boolean showSubtitleButton = false; - boolean animationEnabled = true; - boolean showVrButton = false; - - if (playbackAttrs != null) { - TypedArray a = - context - .getTheme() - .obtainStyledAttributes( - playbackAttrs, R.styleable.PlayerControlView, defStyleAttr, /* defStyleRes= */ 0); - try { - controllerLayoutId = - a.getResourceId(R.styleable.PlayerControlView_controller_layout_id, controllerLayoutId); - showTimeoutMs = a.getInt(R.styleable.PlayerControlView_show_timeout, showTimeoutMs); - repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); - showRewindButton = - a.getBoolean(R.styleable.PlayerControlView_show_rewind_button, showRewindButton); - showFastForwardButton = - a.getBoolean( - R.styleable.PlayerControlView_show_fastforward_button, showFastForwardButton); - showPreviousButton = - a.getBoolean(R.styleable.PlayerControlView_show_previous_button, showPreviousButton); - showNextButton = - a.getBoolean(R.styleable.PlayerControlView_show_next_button, showNextButton); - showShuffleButton = - a.getBoolean(R.styleable.PlayerControlView_show_shuffle_button, showShuffleButton); - showSubtitleButton = - a.getBoolean(R.styleable.PlayerControlView_show_subtitle_button, showSubtitleButton); - showVrButton = a.getBoolean(R.styleable.PlayerControlView_show_vr_button, showVrButton); - setTimeBarMinUpdateInterval( - a.getInt( - R.styleable.PlayerControlView_time_bar_min_update_interval, - timeBarMinUpdateIntervalMs)); - animationEnabled = - a.getBoolean(R.styleable.PlayerControlView_animation_enabled, animationEnabled); - } finally { - a.recycle(); - } - } - - LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this); - setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - - componentListener = new ComponentListener(); - visibilityListeners = new CopyOnWriteArrayList<>(); - period = new Timeline.Period(); - window = new Timeline.Window(); - formatBuilder = new StringBuilder(); - formatter = new Formatter(formatBuilder, Locale.getDefault()); - adGroupTimesMs = new long[0]; - playedAdGroups = new boolean[0]; - extraAdGroupTimesMs = new long[0]; - extraPlayedAdGroups = new boolean[0]; - updateProgressAction = this::updateProgress; - - durationView = findViewById(R.id.exo_duration); - positionView = findViewById(R.id.exo_position); - - subtitleButton = findViewById(R.id.exo_subtitle); - if (subtitleButton != null) { - subtitleButton.setOnClickListener(componentListener); - } - - fullScreenButton = findViewById(R.id.exo_fullscreen); - initializeFullScreenButton(fullScreenButton, this::onFullScreenButtonClicked); - minimalFullScreenButton = findViewById(R.id.exo_minimal_fullscreen); - initializeFullScreenButton(minimalFullScreenButton, this::onFullScreenButtonClicked); - - settingsButton = findViewById(R.id.exo_settings); - if (settingsButton != null) { - settingsButton.setOnClickListener(componentListener); - } - - titleView = findViewById(com.skyd.anivu.R.id.exo_title); - titleView.setSelected(true); - - backButton = findViewById(com.skyd.anivu.R.id.exo_back); - if (backButton != null) { - backButton.setOnClickListener(onBackButtonClickListener); - } - - resetZoomView = findViewById(com.skyd.anivu.R.id.exo_reset_zoom); - if (resetZoomView != null) { - resetZoomView.setOnClickListener(onResetZoomButtonClickListener); - } - isZoom = false; - - forward85sView = findViewById(com.skyd.anivu.R.id.exo_forward_85s); - if (forward85sView != null) { - forward85sView.setOnClickListener((v) -> { - if (player != null) { - player.seekTo(player.getCurrentPosition() + 85000); - } - }); - } - - screenshotView = findViewById(com.skyd.anivu.R.id.exo_screenshot); - if (screenshotView != null) { - if (!screenshotView.hasOnClickListeners()) screenshotView.setVisibility(View.GONE); - } - - longPressPlaybackSpeedView = findViewById(com.skyd.anivu.R.id.exo_long_press_playback_speed); - if (longPressPlaybackSpeedView != null) { - longPressPlaybackSpeedView.setVisibility(View.GONE); - } - - seekPreviewView = findViewById(com.skyd.anivu.R.id.exo_seek_preview); - if (seekPreviewView != null) { - seekPreviewView.setVisibility(View.GONE); - } - - playbackSpeedButton = findViewById(R.id.exo_playback_speed); - if (playbackSpeedButton != null) { - playbackSpeedButton.setOnClickListener(componentListener); - } - - brightnessControlsView = findViewById(com.skyd.anivu.R.id.exo_brightness_controls); - if (brightnessControlsView != null) { - brightnessControlsView.setVisibility(View.GONE); - } - brightnessProgressView = findViewById(com.skyd.anivu.R.id.exo_brightness_progress); - - volumeControlsView = findViewById(com.skyd.anivu.R.id.exo_volume_controls); - if (volumeControlsView != null) { - volumeControlsView.setVisibility(View.GONE); - } - volumeProgressView = findViewById(com.skyd.anivu.R.id.exo_volume_progress); - - backwardRipple = findViewById(com.skyd.anivu.R.id.exo_backward_ripple); - forwardRipple = findViewById(com.skyd.anivu.R.id.exo_forward_ripple); - - audioTrackButton = findViewById(R.id.exo_audio_track); - if (audioTrackButton != null) { - audioTrackButton.setOnClickListener(componentListener); - } - - TimeBar customTimeBar = findViewById(R.id.exo_progress); - View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder); - if (customTimeBar != null) { - timeBar = customTimeBar; - } else if (timeBarPlaceholder != null) { - // Propagate playbackAttrs as timebarAttrs so that DefaultTimeBar's custom attributes are - // transferred, but standard attributes (e.g. background) are not. - DefaultTimeBar defaultTimeBar = - new DefaultTimeBar(context, null, 0, playbackAttrs, R.style.ExoStyledControls_TimeBar); - defaultTimeBar.setId(R.id.exo_progress); - defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams()); - ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent()); - int timeBarIndex = parent.indexOfChild(timeBarPlaceholder); - parent.removeView(timeBarPlaceholder); - parent.addView(defaultTimeBar, timeBarIndex); - timeBar = defaultTimeBar; - } else { - timeBar = null; - } - if (timeBar != null) { - timeBar.addListener(componentListener); - } - - playPauseButton = findViewById(R.id.exo_play_pause); - if (playPauseButton != null) { - playPauseButton.setOnClickListener(componentListener); - } - previousButton = findViewById(R.id.exo_prev); - if (previousButton != null) { - previousButton.setOnClickListener(componentListener); - } - nextButton = findViewById(R.id.exo_next); - if (nextButton != null) { - nextButton.setOnClickListener(componentListener); - } - Typeface typeface = ResourcesCompat.getFont(context, R.font.roboto_medium_numbers); - View rewButton = findViewById(R.id.exo_rew); - rewindButtonTextView = rewButton == null ? findViewById(R.id.exo_rew_with_amount) : null; - if (rewindButtonTextView != null) { - rewindButtonTextView.setTypeface(typeface); - } - rewindButton = rewButton == null ? rewindButtonTextView : rewButton; - if (rewindButton != null) { - rewindButton.setOnClickListener(componentListener); - } - View ffwdButton = findViewById(R.id.exo_ffwd); - fastForwardButtonTextView = ffwdButton == null ? findViewById(R.id.exo_ffwd_with_amount) : null; - if (fastForwardButtonTextView != null) { - fastForwardButtonTextView.setTypeface(typeface); - } - fastForwardButton = ffwdButton == null ? fastForwardButtonTextView : ffwdButton; - if (fastForwardButton != null) { - fastForwardButton.setOnClickListener(componentListener); - } - repeatToggleButton = findViewById(R.id.exo_repeat_toggle); - if (repeatToggleButton != null) { - repeatToggleButton.setOnClickListener(componentListener); - } - shuffleButton = findViewById(R.id.exo_shuffle); - if (shuffleButton != null) { - shuffleButton.setOnClickListener(componentListener); - } - - resources = context.getResources(); - buttonAlphaEnabled = - (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_enabled) / 100; - buttonAlphaDisabled = - (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_disabled) / 100; - - vrButton = findViewById(R.id.exo_vr); - if (vrButton != null) { - updateButton(/* enabled= */ false, vrButton); - } - - controlViewLayoutManager = new PlayerControlViewLayoutManager(this); - controlViewLayoutManager.setAnimationEnabled(animationEnabled); - - String[] settingTexts = new String[2]; - Drawable[] settingIcons = new Drawable[2]; - settingTexts[SETTINGS_PLAYBACK_SPEED_POSITION] = - resources.getString(R.string.exo_controls_playback_speed); - settingIcons[SETTINGS_PLAYBACK_SPEED_POSITION] = - getDrawable(context, resources, R.drawable.exo_styled_controls_speed); - settingTexts[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] = - resources.getString(R.string.exo_track_selection_title_audio); - settingIcons[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] = - getDrawable(context, resources, R.drawable.exo_styled_controls_audiotrack); - settingsAdapter = new SettingsAdapter(settingTexts, settingIcons); - settingsWindowMargin = resources.getDimensionPixelSize(R.dimen.exo_settings_offset); - settingsView = - (RecyclerView) - LayoutInflater.from(context) - .inflate(R.layout.exo_styled_settings_list, /* root= */ null); - settingsView.setAdapter(settingsAdapter); - settingsView.setLayoutManager(new LinearLayoutManager(getContext())); - settingsWindow = - new PopupWindow(settingsView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true); - if (Util.SDK_INT < 23) { - // Work around issue where tapping outside of the menu area or pressing the back button - // doesn't dismiss the menu as expected. See: https://github.com/google/ExoPlayer/issues/8272. - settingsWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - } - settingsWindow.setOnDismissListener(componentListener); - needToHideBars = true; - - trackNameProvider = new DefaultTrackNameProvider(getResources()); - subtitleOnButtonDrawable = - getDrawable(context, resources, R.drawable.exo_styled_controls_subtitle_on); - subtitleOffButtonDrawable = - getDrawable(context, resources, R.drawable.exo_styled_controls_subtitle_off); - subtitleOnContentDescription = - resources.getString(R.string.exo_controls_cc_enabled_description); - subtitleOffContentDescription = - resources.getString(R.string.exo_controls_cc_disabled_description); - textTrackSelectionAdapter = new TextTrackSelectionAdapter(); - audioTrackSelectionAdapter = new AudioTrackSelectionAdapter(); - playbackSpeedAdapter = - new PlaybackSpeedAdapter( - resources.getStringArray(R.array.exo_controls_playback_speeds), PLAYBACK_SPEEDS); - - fullScreenExitDrawable = - getDrawable(context, resources, R.drawable.exo_styled_controls_fullscreen_exit); - fullScreenEnterDrawable = - getDrawable(context, resources, R.drawable.exo_styled_controls_fullscreen_enter); - repeatOffButtonDrawable = - getDrawable(context, resources, R.drawable.exo_styled_controls_repeat_off); - repeatOneButtonDrawable = - getDrawable(context, resources, R.drawable.exo_styled_controls_repeat_one); - repeatAllButtonDrawable = - getDrawable(context, resources, R.drawable.exo_styled_controls_repeat_all); - shuffleOnButtonDrawable = - getDrawable(context, resources, R.drawable.exo_styled_controls_shuffle_on); - shuffleOffButtonDrawable = - getDrawable(context, resources, R.drawable.exo_styled_controls_shuffle_off); - fullScreenExitContentDescription = - resources.getString(R.string.exo_controls_fullscreen_exit_description); - fullScreenEnterContentDescription = - resources.getString(R.string.exo_controls_fullscreen_enter_description); - repeatOffButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_off_description); - repeatOneButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_one_description); - repeatAllButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_all_description); - shuffleOnContentDescription = resources.getString(R.string.exo_controls_shuffle_on_description); - shuffleOffContentDescription = - resources.getString(R.string.exo_controls_shuffle_off_description); - - // TODO(insun) : Make showing bottomBar configurable. (ex. show_bottom_bar attribute). - bottomBar = findViewById(R.id.exo_bottom_bar); - controlViewLayoutManager.setShowButton(bottomBar, true); - controlViewLayoutManager.setShowButton(fastForwardButton, showFastForwardButton); - controlViewLayoutManager.setShowButton(rewindButton, showRewindButton); - controlViewLayoutManager.setShowButton(previousButton, showPreviousButton); - controlViewLayoutManager.setShowButton(nextButton, showNextButton); - controlViewLayoutManager.setShowButton(shuffleButton, showShuffleButton); - controlViewLayoutManager.setShowButton(subtitleButton, showSubtitleButton); - controlViewLayoutManager.setShowButton(vrButton, showVrButton); - controlViewLayoutManager.setShowButton( - repeatToggleButton, repeatToggleModes != RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE); - addOnLayoutChangeListener(this::onLayoutChange); - - topBar = findViewById(com.skyd.anivu.R.id.exo_top_bar); - controlViewLayoutManager.setShowButton(topBar, true); - } - - /** - * Returns the {@link Player} currently being controlled by this view, or null if no player is - * set. - */ - @Nullable - public Player getPlayer() { - return player; - } - - /** - * Sets the {@link Player} to control. - * - * @param player The {@link Player} to control, or {@code null} to detach the current player. Only - * players which are accessed on the main thread are supported ({@code - * player.getApplicationLooper() == Looper.getMainLooper()}). - */ - public void setPlayer(@Nullable Player player) { - Assertions.checkState(Looper.myLooper() == Looper.getMainLooper()); - Assertions.checkArgument( - player == null || player.getApplicationLooper() == Looper.getMainLooper()); - if (this.player == player) { - return; - } - if (this.player != null) { - this.player.removeListener(componentListener); - } - this.player = player; - if (player != null) { - player.addListener(componentListener); - } - updateAll(); - } - - /** - * @deprecated Replace multi-window time bar display by merging source windows together instead, - * for example using ExoPlayer's {@code ConcatenatingMediaSource2}. - */ - @Deprecated - public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { - this.showMultiWindowTimeBar = showMultiWindowTimeBar; - updateTimeline(); - } - - /** - * Sets whether a play button is shown if playback is {@linkplain - * Player#getPlaybackSuppressionReason() suppressed}. - * - *

The default is {@code true}. - * - * @param showPlayButtonIfSuppressed Whether to show a play button if playback is {@linkplain - * Player#getPlaybackSuppressionReason() suppressed}. - */ - public void setShowPlayButtonIfPlaybackIsSuppressed(boolean showPlayButtonIfSuppressed) { - this.showPlayButtonIfSuppressed = showPlayButtonIfSuppressed; - updatePlayPauseButton(); - } - - /** - * Sets the millisecond positions of extra ad markers relative to the start of the window (or - * timeline, if in multi-window mode) and whether each extra ad has been played or not. The - * markers are shown in addition to any ad markers for ads in the player's timeline. - * - * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or - * {@code null} to show no extra ad markers. - * @param extraPlayedAdGroups Whether each ad has been played. Must be the same length as {@code - * extraAdGroupTimesMs}, or {@code null} if {@code extraAdGroupTimesMs} is {@code null}. - */ - public void setExtraAdGroupMarkers( - @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { - if (extraAdGroupTimesMs == null) { - this.extraAdGroupTimesMs = new long[0]; - this.extraPlayedAdGroups = new boolean[0]; - } else { - extraPlayedAdGroups = checkNotNull(extraPlayedAdGroups); - Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length); - this.extraAdGroupTimesMs = extraAdGroupTimesMs; - this.extraPlayedAdGroups = extraPlayedAdGroups; - } - updateTimeline(); - } - - /** - * @deprecated Register a {@link PlayerView.ControllerVisibilityListener} via {@link - * PlayerView#setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)} - * instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView} - * is deprecated. - */ - @SuppressWarnings("deprecation") - @Deprecated - public void addVisibilityListener(VisibilityListener listener) { - checkNotNull(listener); - visibilityListeners.add(listener); - } - - /** - * @deprecated Register a {@link PlayerView.ControllerVisibilityListener} via {@link - * PlayerView#setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)} - * instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView} - * is deprecated. - */ - @SuppressWarnings("deprecation") - @Deprecated - public void removeVisibilityListener(VisibilityListener listener) { - visibilityListeners.remove(listener); - } - - /** - * Sets the {@link ProgressUpdateListener}. - * - * @param listener The listener to be notified about when progress is updated. - */ - public void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) { - this.progressUpdateListener = listener; - } - - /** - * Sets whether the rewind button is shown. - * - * @param showRewindButton Whether the rewind button is shown. - */ - public void setShowRewindButton(boolean showRewindButton) { - controlViewLayoutManager.setShowButton(rewindButton, showRewindButton); - updateNavigation(); - } - - /** - * Sets whether the fast forward button is shown. - * - * @param showFastForwardButton Whether the fast forward button is shown. - */ - public void setShowFastForwardButton(boolean showFastForwardButton) { - controlViewLayoutManager.setShowButton(fastForwardButton, showFastForwardButton); - updateNavigation(); - } - - /** - * Sets whether the previous button is shown. - * - * @param showPreviousButton Whether the previous button is shown. - */ - public void setShowPreviousButton(boolean showPreviousButton) { - controlViewLayoutManager.setShowButton(previousButton, showPreviousButton); - updateNavigation(); - } - - /** - * Sets whether the next button is shown. - * - * @param showNextButton Whether the next button is shown. - */ - public void setShowNextButton(boolean showNextButton) { - controlViewLayoutManager.setShowButton(nextButton, showNextButton); - updateNavigation(); - } - - /** - * Returns the playback controls timeout. The playback controls are automatically hidden after - * this duration of time has elapsed without user input. - * - * @return The duration in milliseconds. A non-positive value indicates that the controls will - * remain visible indefinitely. - */ - public int getShowTimeoutMs() { - return showTimeoutMs; - } - - /** - * Sets the playback controls timeout. The playback controls are automatically hidden after this - * duration of time has elapsed without user input. - * - * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls - * to remain visible indefinitely. - */ - public void setShowTimeoutMs(int showTimeoutMs) { - this.showTimeoutMs = showTimeoutMs; - if (isFullyVisible()) { - controlViewLayoutManager.resetHideCallbacks(); - } - } - - /** - * Returns which repeat toggle modes are enabled. - * - * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}. - */ - public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() { - return repeatToggleModes; - } - - /** - * Sets which repeat toggle modes are enabled. - * - * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. - */ - public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - this.repeatToggleModes = repeatToggleModes; - if (player != null && player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) { - @Player.RepeatMode int currentMode = player.getRepeatMode(); - if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE - && currentMode != Player.REPEAT_MODE_OFF) { - player.setRepeatMode(Player.REPEAT_MODE_OFF); - } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE - && currentMode == Player.REPEAT_MODE_ALL) { - player.setRepeatMode(Player.REPEAT_MODE_ONE); - } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL - && currentMode == Player.REPEAT_MODE_ONE) { - player.setRepeatMode(Player.REPEAT_MODE_ALL); - } - } - controlViewLayoutManager.setShowButton( - repeatToggleButton, repeatToggleModes != RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE); - updateRepeatModeButton(); - } - - /** - * Returns whether the shuffle button is shown. - */ - public boolean getShowShuffleButton() { - return controlViewLayoutManager.getShowButton(shuffleButton); - } - - /** - * Sets whether the shuffle button is shown. - * - * @param showShuffleButton Whether the shuffle button is shown. - */ - public void setShowShuffleButton(boolean showShuffleButton) { - controlViewLayoutManager.setShowButton(shuffleButton, showShuffleButton); - updateShuffleButton(); - } - - /** - * Returns whether the subtitle button is shown. - */ - public boolean getShowSubtitleButton() { - return controlViewLayoutManager.getShowButton(subtitleButton); - } - - /** - * Sets whether the subtitle button is shown. - * - * @param showSubtitleButton Whether the subtitle button is shown. - */ - public void setShowSubtitleButton(boolean showSubtitleButton) { - controlViewLayoutManager.setShowButton(subtitleButton, showSubtitleButton); - } - - /** - * Returns whether the VR button is shown. - */ - public boolean getShowVrButton() { - return controlViewLayoutManager.getShowButton(vrButton); - } - - /** - * Sets whether the VR button is shown. - * - * @param showVrButton Whether the VR button is shown. - */ - public void setShowVrButton(boolean showVrButton) { - controlViewLayoutManager.setShowButton(vrButton, showVrButton); - } - - /** - * Sets listener for the VR button. - * - * @param onClickListener Listener for the VR button, or null to clear the listener. - */ - public void setVrButtonListener(@Nullable OnClickListener onClickListener) { - if (vrButton != null) { - vrButton.setOnClickListener(onClickListener); - updateButton(onClickListener != null, vrButton); - } - } - - /** - * Sets whether an animation is used to show and hide the playback controls. - * - * @param animationEnabled Whether an animation is applied to show and hide playback controls. - */ - public void setAnimationEnabled(boolean animationEnabled) { - controlViewLayoutManager.setAnimationEnabled(animationEnabled); - } - - /** - * Returns whether an animation is used to show and hide the playback controls. - */ - public boolean isAnimationEnabled() { - return controlViewLayoutManager.isAnimationEnabled(); - } - - /** - * Sets the minimum interval between time bar position updates. - * - *

Note that smaller intervals, e.g. 33ms, will result in a smooth movement but will use more - * CPU resources while the time bar is visible, whereas larger intervals, e.g. 200ms, will result - * in a step-wise update with less CPU usage. - * - * @param minUpdateIntervalMs The minimum interval between time bar position updates, in - * milliseconds. - */ - public void setTimeBarMinUpdateInterval(int minUpdateIntervalMs) { - // Do not accept values below 16ms (60fps) and larger than the maximum update interval. - timeBarMinUpdateIntervalMs = - Util.constrainValue(minUpdateIntervalMs, 16, MAX_UPDATE_INTERVAL_MS); - } - - /** - * @deprecated Register a {@link PlayerView.FullscreenButtonClickListener} via {@link - * PlayerView#setFullscreenButtonClickListener(PlayerView.FullscreenButtonClickListener)} - * instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView} - * is deprecated. - */ - @SuppressWarnings("deprecation") - @Deprecated - public void setOnFullScreenModeChangedListener( - @Nullable OnFullScreenModeChangedListener listener) { - onFullScreenModeChangedListener = listener; - updateFullScreenButtonVisibility(fullScreenButton, listener != null); - updateFullScreenButtonVisibility(minimalFullScreenButton, listener != null); - } - - public void setOnBackButtonClickListener( - @Nullable View.OnClickListener listener) { - onBackButtonClickListener = listener; - backButton.setOnClickListener(onBackButtonClickListener); - } - - public void setOnResetZoomButtonClickListener( - @Nullable View.OnClickListener listener) { - onResetZoomButtonClickListener = listener; - resetZoomView.setOnClickListener(onResetZoomButtonClickListener); - } - - public void onZoomStateChanged(boolean isZoom) { - this.isZoom = isZoom; - if (isZoom) { - resetZoomView.setVisibility(View.VISIBLE); - } else { - resetZoomView.setVisibility(View.GONE); - } - } - - public void setForward85sButton(boolean visible) { - if (forward85sView != null) { - forward85sView.setVisibility(visible ? View.VISIBLE : View.GONE); - } - } - - public void setOnScreenshotListener(@Nullable View.OnClickListener listener) { - if (screenshotView != null) { - screenshotView.setVisibility(listener == null ? View.GONE : View.VISIBLE); - screenshotView.setOnClickListener(listener); - } - } - - public void setLongPressPlaybackSpeedVisibility(int visibility) { - if (longPressPlaybackSpeedView != null) { - longPressPlaybackSpeedView.setVisibility(visibility); - } - } - - public boolean updateSeekPreview(long newPositionPos) { - if (seekPreviewView != null && player != null) { - newPositionPos = Math.min(Math.max(newPositionPos, 0), player.getContentDuration()); - seekPreviewView.setText( - Util.getStringForTime(formatBuilder, formatter, newPositionPos) + " / " + - Util.getStringForTime(formatBuilder, formatter, player.getContentDuration()) - ); - return true; - } - return false; - } - - public void setSeekPreviewVisibility(int visibility) { - if (seekPreviewView != null) { - seekPreviewView.setVisibility(visibility); - } - } - - public boolean updateBrightnessProgress(int progress) { - if (brightnessProgressView != null) { - ImageView icon = findViewById(com.skyd.anivu.R.id.exo_brightness_icon); - if (icon != null) { - if (progress <= 20) { - icon.setImageResource(com.skyd.anivu.R.drawable.ic_brightness_low_24); - } else if (progress > 20 && progress <= 60) { - icon.setImageResource(com.skyd.anivu.R.drawable.ic_brightness_medium_24); - } else { - icon.setImageResource(com.skyd.anivu.R.drawable.ic_brightness_high_24); - } - } - brightnessProgressView.setProgressCompat(progress, false); - return true; - } - return false; - } - - private Runnable setBrightnessControlsGoneRunnable = new Runnable() { - @Override - public void run() { - if (brightnessControlsView != null) { - brightnessControlsView.setVisibility(GONE); - } - } - }; - - public void setBrightnessControlsVisibility(int visibility) { - if (brightnessControlsView != null) { - removeCallbacks(setBrightnessControlsGoneRunnable); - if (visibility == VISIBLE) { - brightnessControlsView.setVisibility(VISIBLE); - } else { - postDelayed(setBrightnessControlsGoneRunnable, 300); - } - } - } - - public void showBackwardRipple() { - if (backwardRipple != null) { - backwardRipple.visible(); - } - } - - public void showForwardRipple() { - if (forwardRipple != null) { - forwardRipple.visible(); - } - } - - public void setMaxVolume(int max) { - if (volumeProgressView != null) { - volumeProgressView.setMax(max); - } - } - - public void setMinVolume(int min) { - if (volumeProgressView != null) { - volumeProgressView.setMin(min); - } - } - - public boolean updateVolumeProgress(int progress) { - if (volumeProgressView != null) { - ImageView icon = findViewById(com.skyd.anivu.R.id.exo_volume_icon); - if (icon != null) { - if (progress <= 0) { - icon.setImageResource(com.skyd.anivu.R.drawable.ic_volume_mute_24); - } else if (progress > 0 && progress <= volumeProgressView.getMax() / 2) { - icon.setImageResource(com.skyd.anivu.R.drawable.ic_volume_down_24); - } else { - icon.setImageResource(com.skyd.anivu.R.drawable.ic_volume_up_24); - } - } - volumeProgressView.setProgressCompat(progress, false); - return true; - } - return false; - } - - private Runnable setVolumeControlsGoneRunnable = new Runnable() { - @Override - public void run() { - if (volumeControlsView != null) { - volumeControlsView.setVisibility(GONE); - } - } - }; - - public void setVolumeControlsVisibility(int visibility) { - if (volumeControlsView != null) { - removeCallbacks(setVolumeControlsGoneRunnable); - if (visibility == VISIBLE) { - volumeControlsView.setVisibility(VISIBLE); - } else { - postDelayed(setVolumeControlsGoneRunnable, 300); - } - } - } - - /** - * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will - * be automatically hidden after this duration of time has elapsed without user input. - */ - public void show() { - controlViewLayoutManager.show(); - } - - /** - * Hides the controller. - */ - public void hide() { - controlViewLayoutManager.hide(); - } - - /** - * Hides the controller without any animation. - */ - public void hideImmediately() { - controlViewLayoutManager.hideImmediately(); - } - - /** - * Returns whether the controller is fully visible, which means all UI controls are visible. - */ - public boolean isFullyVisible() { - return controlViewLayoutManager.isFullyVisible(); - } - - /** - * Returns whether the auto hidden controller is currently visible. - */ - public boolean isAutoHiddenControllerVisible() { - return findViewById(com.skyd.anivu.R.id.exo_auto_hidden_control_view).getVisibility() - == VISIBLE; - } - - @SuppressWarnings("deprecation") - // Calling the deprecated listener for now. - /* package */ void notifyOnVisibilityChange() { - for (VisibilityListener visibilityListener : visibilityListeners) { - visibilityListener.onVisibilityChange(getVisibility()); - } - } - - /* package */ void updateAll() { - updatePlayPauseButton(); - updateNavigation(); - updateRepeatModeButton(); - updateShuffleButton(); - updateTrackLists(); - updatePlaybackSpeedList(); - updateTimeline(); - updateTitle(); - } - - private void updatePlayPauseButton() { - if (!isAutoHiddenControllerVisible() || !isAttachedToWindow) { - return; - } - if (playPauseButton != null) { - boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed); - @DrawableRes - int drawableRes = - shouldShowPlayButton - ? R.drawable.exo_styled_controls_play - : R.drawable.exo_styled_controls_pause; - @StringRes - int stringRes = - shouldShowPlayButton - ? R.string.exo_controls_play_description - : R.string.exo_controls_pause_description; - ((ImageView) playPauseButton) - .setImageDrawable(getDrawable(getContext(), resources, drawableRes)); - playPauseButton.setContentDescription(resources.getString(stringRes)); - - boolean enablePlayPause = shouldEnablePlayPauseButton(); - updateButton(enablePlayPause, playPauseButton); - } - } - - private void updateNavigation() { - if (!isAutoHiddenControllerVisible() || !isAttachedToWindow) { - return; - } - - @Nullable Player player = this.player; - boolean enableSeeking = false; - boolean enablePrevious = false; - boolean enableRewind = false; - boolean enableFastForward = false; - boolean enableNext = false; - if (player != null) { - enableSeeking = - (showMultiWindowTimeBar && canShowMultiWindowTimeBar(player, window)) - ? player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM) - : player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM); - enablePrevious = player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS); - enableRewind = player.isCommandAvailable(COMMAND_SEEK_BACK); - enableFastForward = player.isCommandAvailable(COMMAND_SEEK_FORWARD); - enableNext = player.isCommandAvailable(COMMAND_SEEK_TO_NEXT); - } - - if (enableRewind) { - updateRewindButton(); - } - if (enableFastForward) { - updateFastForwardButton(); - } - - updateButton(enablePrevious, previousButton); - updateButton(enableRewind, rewindButton); - updateButton(enableFastForward, fastForwardButton); - updateButton(enableNext, nextButton); - if (timeBar != null) { - timeBar.setEnabled(enableSeeking); - } - } - - private void updateRewindButton() { - long rewindMs = - player != null ? player.getSeekBackIncrement() : C.DEFAULT_SEEK_BACK_INCREMENT_MS; - int rewindSec = (int) (rewindMs / 1_000); - if (rewindButtonTextView != null) { - rewindButtonTextView.setText(String.valueOf(rewindSec)); - } - if (rewindButton != null) { - rewindButton.setContentDescription( - resources.getQuantityString( - R.plurals.exo_controls_rewind_by_amount_description, rewindSec, rewindSec)); - } - } - - private void updateFastForwardButton() { - long fastForwardMs = - player != null ? player.getSeekForwardIncrement() : C.DEFAULT_SEEK_FORWARD_INCREMENT_MS; - int fastForwardSec = (int) (fastForwardMs / 1_000); - if (fastForwardButtonTextView != null) { - fastForwardButtonTextView.setText(String.valueOf(fastForwardSec)); - } - if (fastForwardButton != null) { - fastForwardButton.setContentDescription( - resources.getQuantityString( - R.plurals.exo_controls_fastforward_by_amount_description, - fastForwardSec, - fastForwardSec)); - } - } - - private void updateRepeatModeButton() { - if (!isAutoHiddenControllerVisible() || !isAttachedToWindow || repeatToggleButton == null) { - return; - } - - if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { - updateButton(/* enabled= */ false, repeatToggleButton); - return; - } - - @Nullable Player player = this.player; - if (player == null || !player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) { - updateButton(/* enabled= */ false, repeatToggleButton); - repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); - repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); - return; - } - - updateButton(/* enabled= */ true, repeatToggleButton); - switch (player.getRepeatMode()) { - case Player.REPEAT_MODE_OFF: - repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); - repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); - break; - case Player.REPEAT_MODE_ONE: - repeatToggleButton.setImageDrawable(repeatOneButtonDrawable); - repeatToggleButton.setContentDescription(repeatOneButtonContentDescription); - break; - case Player.REPEAT_MODE_ALL: - repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); - repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); - break; - default: - // Never happens. - } - } - - private void updateShuffleButton() { - if (!isAutoHiddenControllerVisible() || !isAttachedToWindow || shuffleButton == null) { - return; - } - - @Nullable Player player = this.player; - if (!controlViewLayoutManager.getShowButton(shuffleButton)) { - updateButton(/* enabled= */ false, shuffleButton); - } else if (player == null || !player.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)) { - updateButton(/* enabled= */ false, shuffleButton); - shuffleButton.setImageDrawable(shuffleOffButtonDrawable); - shuffleButton.setContentDescription(shuffleOffContentDescription); - } else { - updateButton(/* enabled= */ true, shuffleButton); - shuffleButton.setImageDrawable( - player.getShuffleModeEnabled() ? shuffleOnButtonDrawable : shuffleOffButtonDrawable); - shuffleButton.setContentDescription( - player.getShuffleModeEnabled() - ? shuffleOnContentDescription - : shuffleOffContentDescription); - } - } - - private void updateTrackLists() { - initTrackSelectionAdapter(); - updateButton(textTrackSelectionAdapter.getItemCount() > 0, subtitleButton); - updateSettingsButton(); - } - - private void initTrackSelectionAdapter() { - textTrackSelectionAdapter.clear(); - audioTrackSelectionAdapter.clear(); - if (player == null - || !player.isCommandAvailable(COMMAND_GET_TRACKS) - || !player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { - return; - } - Tracks tracks = player.getCurrentTracks(); - audioTrackSelectionAdapter.init(gatherSupportedTrackInfosOfType(tracks, C.TRACK_TYPE_AUDIO)); - if (controlViewLayoutManager.getShowButton(subtitleButton)) { - textTrackSelectionAdapter.init(gatherSupportedTrackInfosOfType(tracks, C.TRACK_TYPE_TEXT)); - } else { - textTrackSelectionAdapter.init(ImmutableList.of()); - } - } - - private ImmutableList gatherSupportedTrackInfosOfType( - Tracks tracks, @C.TrackType int trackType) { - ImmutableList.Builder trackInfos = new ImmutableList.Builder<>(); - List trackGroups = tracks.getGroups(); - for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.size(); trackGroupIndex++) { - Tracks.Group trackGroup = trackGroups.get(trackGroupIndex); - if (trackGroup.getType() != trackType) { - continue; - } - for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (!trackGroup.isTrackSupported(trackIndex)) { - continue; - } - Format trackFormat = trackGroup.getTrackFormat(trackIndex); - if ((trackFormat.selectionFlags & C.SELECTION_FLAG_FORCED) != 0) { - continue; - } - String trackName = trackNameProvider.getTrackName(trackFormat); - trackInfos.add(new TrackInformation( - tracks, trackGroupIndex, trackIndex, trackName, trackFormat.label)); - } - } - return trackInfos.build(); - } - - private void updateTimeline() { - @Nullable Player player = this.player; - if (player == null) { - return; - } - multiWindowTimeBar = showMultiWindowTimeBar && canShowMultiWindowTimeBar(player, window); - currentWindowOffset = 0; - long durationUs = 0; - int adGroupCount = 0; - Timeline timeline = - player.isCommandAvailable(COMMAND_GET_TIMELINE) - ? player.getCurrentTimeline() - : Timeline.EMPTY; - if (!timeline.isEmpty()) { - int currentWindowIndex = player.getCurrentMediaItemIndex(); - int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex; - int lastWindowIndex = multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex; - for (int i = firstWindowIndex; i <= lastWindowIndex; i++) { - if (i == currentWindowIndex) { - currentWindowOffset = Util.usToMs(durationUs); - } - timeline.getWindow(i, window); - if (window.durationUs == C.TIME_UNSET) { - Assertions.checkState(!multiWindowTimeBar); - break; - } - for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { - timeline.getPeriod(j, period); - int removedGroups = period.getRemovedAdGroupCount(); - int totalGroups = period.getAdGroupCount(); - for (int adGroupIndex = removedGroups; adGroupIndex < totalGroups; adGroupIndex++) { - long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex); - if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) { - if (period.durationUs == C.TIME_UNSET) { - // Don't show ad markers for postrolls in periods with unknown duration. - continue; - } - adGroupTimeInPeriodUs = period.durationUs; - } - long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); - if (adGroupTimeInWindowUs >= 0) { - if (adGroupCount == adGroupTimesMs.length) { - int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; - adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); - playedAdGroups = Arrays.copyOf(playedAdGroups, newLength); - } - adGroupTimesMs[adGroupCount] = Util.usToMs(durationUs + adGroupTimeInWindowUs); - playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex); - adGroupCount++; - } - } - } - durationUs += window.durationUs; - } - } else if (player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { - long playerDurationMs = player.getContentDuration(); - if (playerDurationMs != C.TIME_UNSET) { - durationUs = msToUs(playerDurationMs); - } - } - long durationMs = Util.usToMs(durationUs); - if (durationView != null) { - durationView.setText(Util.getStringForTime(formatBuilder, formatter, durationMs)); - } - if (timeBar != null) { - timeBar.setDuration(durationMs); - int extraAdGroupCount = extraAdGroupTimesMs.length; - int totalAdGroupCount = adGroupCount + extraAdGroupCount; - if (totalAdGroupCount > adGroupTimesMs.length) { - adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount); - playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount); - } - System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount); - System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount); - timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount); - } - updateProgress(); - } - - private void updateTitle() { - @Nullable Player player = this.player; - if (player == null) { - return; - } - MediaMetadata mediaMetadata = player.getMediaMetadata(); - if (mediaMetadata != null && mediaMetadata.title != null) { - titleView.setText(mediaMetadata.title); - } else { - MediaItem currentMediaItem = player.getCurrentMediaItem(); - if (currentMediaItem != null && currentMediaItem.localConfiguration != null - && currentMediaItem.localConfiguration.uri != null) { - titleView.setText(IOExtKt.fileName(currentMediaItem.localConfiguration.uri)); - } else titleView.setText(""); - } - } - - private void updateProgress() { - if (!isAutoHiddenControllerVisible() || !isAttachedToWindow) { - return; - } - @Nullable Player player = this.player; - long position = 0; - long bufferedPosition = 0; - if (player != null && player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { - position = currentWindowOffset + player.getContentPosition(); - bufferedPosition = currentWindowOffset + player.getContentBufferedPosition(); - } - if (positionView != null && !scrubbing) { - positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); - } - if (timeBar != null) { - timeBar.setPosition(position); - timeBar.setBufferedPosition(bufferedPosition); - } - if (progressUpdateListener != null) { - progressUpdateListener.onProgressUpdate(position, bufferedPosition); - } - - // Cancel any pending updates and schedule a new one if necessary. - removeCallbacks(updateProgressAction); - int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); - if (player != null && player.isPlaying()) { - long mediaTimeDelayMs = - timeBar != null ? timeBar.getPreferredUpdateDelay() : MAX_UPDATE_INTERVAL_MS; - - // Limit delay to the start of the next full second to ensure position display is smooth. - long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000; - mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs); - - // Calculate the delay until the next update in real time, taking playback speed into account. - float playbackSpeed = player.getPlaybackParameters().speed; - long delayMs = - playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS; - - // Constrain the delay to avoid too frequent / infrequent updates. - delayMs = Util.constrainValue(delayMs, timeBarMinUpdateIntervalMs, MAX_UPDATE_INTERVAL_MS); - postDelayed(updateProgressAction, delayMs); - } else if (playbackState != Player.STATE_ENDED && playbackState != Player.STATE_IDLE) { - postDelayed(updateProgressAction, MAX_UPDATE_INTERVAL_MS); - } - } - - private void updatePlaybackSpeedList() { - if (player == null) { - return; - } - playbackSpeedAdapter.updateSelectedIndex(player.getPlaybackParameters().speed); - settingsAdapter.setSubTextAtPosition( - SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedAdapter.getSelectedText()); - updateSettingsButton(); - } - - private void updateSettingsButton() { - updateButton(settingsAdapter.hasSettingsToShow(), settingsButton); - } - - private void updateSettingsWindowSize() { - settingsView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); - - int maxWidth = getWidth() - settingsWindowMargin * 2; - int itemWidth = settingsView.getMeasuredWidth(); - int width = Math.min(itemWidth, maxWidth); - settingsWindow.setWidth(width); - - int maxHeight = getHeight() - settingsWindowMargin * 2; - int totalHeight = settingsView.getMeasuredHeight(); - int height = Math.min(maxHeight, totalHeight); - settingsWindow.setHeight(height); - } - - private void displaySettingsWindow(RecyclerView.Adapter adapter, View anchorView) { - settingsView.setAdapter(adapter); - - updateSettingsWindowSize(); - - needToHideBars = false; - settingsWindow.dismiss(); - needToHideBars = true; - - int xoff = getWidth() - settingsWindow.getWidth() - settingsWindowMargin; - int yoff = -settingsWindow.getHeight() - settingsWindowMargin; - - PopupWindowExtKt.showAsDropDownImmersively(settingsWindow, anchorView, xoff, yoff); - } - - private void setPlaybackSpeed(float speed) { - if (player == null || !player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)) { - return; - } - player.setPlaybackParameters(player.getPlaybackParameters().withSpeed(speed)); - } - - /* package */ void requestPlayPauseFocus() { - if (playPauseButton != null) { - playPauseButton.requestFocus(); - } - } - - private void updateButton(boolean enabled, @Nullable View view) { - if (view == null) { - return; - } - view.setEnabled(enabled); - view.setAlpha(enabled ? buttonAlphaEnabled : buttonAlphaDisabled); - } - - private void seekToTimeBarPosition(Player player, long positionMs) { - if (multiWindowTimeBar) { - if (player.isCommandAvailable(COMMAND_GET_TIMELINE) - && player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)) { - Timeline timeline = player.getCurrentTimeline(); - int windowCount = timeline.getWindowCount(); - int windowIndex = 0; - while (true) { - long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); - if (positionMs < windowDurationMs) { - break; - } else if (windowIndex == windowCount - 1) { - // Seeking past the end of the last window should seek to the end of the timeline. - positionMs = windowDurationMs; - break; - } - positionMs -= windowDurationMs; - windowIndex++; - } - player.seekTo(windowIndex, positionMs); - } - } else if (player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)) { - player.seekTo(positionMs); - } - updateProgress(); - } - - private void onFullScreenButtonClicked(View v) { - if (onFullScreenModeChangedListener == null) { - return; - } - - isFullScreen = !isFullScreen; - updateFullScreenButtonForState(fullScreenButton, isFullScreen); - updateFullScreenButtonForState(minimalFullScreenButton, isFullScreen); - if (onFullScreenModeChangedListener != null) { - onFullScreenModeChangedListener.onFullScreenModeChanged(isFullScreen); - } - } - - private void updateFullScreenButtonForState( - @Nullable ImageView fullScreenButton, boolean isFullScreen) { - if (fullScreenButton == null) { - return; - } - if (isFullScreen) { - fullScreenButton.setImageDrawable(fullScreenExitDrawable); - fullScreenButton.setContentDescription(fullScreenExitContentDescription); - } else { - fullScreenButton.setImageDrawable(fullScreenEnterDrawable); - fullScreenButton.setContentDescription(fullScreenEnterContentDescription); - } - } - - private void onSettingViewClicked(int position) { - if (position == SETTINGS_PLAYBACK_SPEED_POSITION) { - displaySettingsWindow(playbackSpeedAdapter, checkNotNull(settingsButton)); - } else if (position == SETTINGS_AUDIO_TRACK_SELECTION_POSITION) { - displaySettingsWindow(audioTrackSelectionAdapter, checkNotNull(settingsButton)); - } else { - settingsWindow.dismiss(); - } - } - - public void playOrPause() { - Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed); - } - - private void supportDisplayCutouts() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - Activity activity = ViewExtKt.getTryActivity(this); - if (activity != null) { - View decorView = activity.getWindow().getDecorView(); - decorView.post(() -> { - var windowInsets = decorView.getRootWindowInsets(); - if (windowInsets == null) { - return; - } - DisplayCutout displayCutout = windowInsets.getDisplayCutout(); - if (displayCutout == null) { - return; - } - if (topBar != null) { - ViewExtKt.updateSafeInset(topBar, displayCutout); - } - if (bottomBar != null) { - ViewExtKt.updateSafeInset(bottomBar, displayCutout); - } - if (timeBar != null && timeBar instanceof View) { - ViewExtKt.updateSafeInset((View) timeBar, displayCutout); - } - }); - } - } - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE || - newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { - supportDisplayCutouts(); - } - } - - @Override - public WindowInsets onApplyWindowInsets(WindowInsets insets) { - WindowInsets windowInsets = super.onApplyWindowInsets(insets); - supportDisplayCutouts(); - return windowInsets; - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - controlViewLayoutManager.onAttachedToWindow(); - isAttachedToWindow = true; - if (isFullyVisible()) { - controlViewLayoutManager.resetHideCallbacks(); - } - updateAll(); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - controlViewLayoutManager.onDetachedFromWindow(); - isAttachedToWindow = false; - removeCallbacks(updateProgressAction); - controlViewLayoutManager.removeHideCallbacks(); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - } - - /** - * Called to process media key events. Any {@link KeyEvent} can be passed but only media key - * events will be handled. - * - * @param event A key event. - * @return Whether the key event was handled. - */ - public boolean dispatchMediaKeyEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - @Nullable Player player = this.player; - if (player == null || !isHandledMediaKey(keyCode)) { - return false; - } - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - if (player.getPlaybackState() != Player.STATE_ENDED - && player.isCommandAvailable(COMMAND_SEEK_FORWARD)) { - player.seekForward(); - } - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND - && player.isCommandAvailable(COMMAND_SEEK_BACK)) { - player.seekBack(); - } else if (event.getRepeatCount() == 0) { - switch (keyCode) { - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - case KeyEvent.KEYCODE_HEADSETHOOK: - Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - Util.handlePlayButtonAction(player); - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - Util.handlePauseButtonAction(player); - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - if (player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)) { - player.seekToNext(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - if (player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) { - player.seekToPrevious(); - } - break; - default: - break; - } - } - } - return true; - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - controlViewLayoutManager.onLayout(left, top, right, bottom); - } - - private void onLayoutChange( - View v, - int left, - int top, - int right, - int bottom, - int oldLeft, - int oldTop, - int oldRight, - int oldBottom) { - int width = right - left; - int height = bottom - top; - int oldWidth = oldRight - oldLeft; - int oldHeight = oldBottom - oldTop; - - if ((width != oldWidth || height != oldHeight) && settingsWindow.isShowing()) { - updateSettingsWindowSize(); - int xOffset = getWidth() - settingsWindow.getWidth() - settingsWindowMargin; - int yOffset = -settingsWindow.getHeight() - settingsWindowMargin; - settingsWindow.update(v, xOffset, yOffset, -1, -1); - } - } - - private boolean shouldEnablePlayPauseButton() { - return player != null - && player.isCommandAvailable(COMMAND_PLAY_PAUSE) - && (!player.isCommandAvailable(COMMAND_GET_TIMELINE) - || !player.getCurrentTimeline().isEmpty()); - } - - @SuppressLint("InlinedApi") - private static boolean isHandledMediaKey(int keyCode) { - return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND - || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - || keyCode == KeyEvent.KEYCODE_HEADSETHOOK - || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY - || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE - || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT - || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS; - } - - /** - * Returns whether the specified {@code player} can be shown on a multi-window time bar. - * - * @param player The {@link Player} to check. - * @param window A scratch {@link Timeline.Window} instance. - * @return Whether the specified timeline can be shown on a multi-window time bar. - */ - private static boolean canShowMultiWindowTimeBar(Player player, Timeline.Window window) { - if (!player.isCommandAvailable(COMMAND_GET_TIMELINE)) { - return false; - } - Timeline timeline = player.getCurrentTimeline(); - int windowCount = timeline.getWindowCount(); - if (windowCount <= 1 || windowCount > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { - return false; - } - for (int i = 0; i < windowCount; i++) { - if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) { - return false; - } - } - return true; - } - - private static void initializeFullScreenButton(View fullScreenButton, OnClickListener listener) { - if (fullScreenButton == null) { - return; - } - fullScreenButton.setVisibility(GONE); - fullScreenButton.setOnClickListener(listener); - } - - private static void updateFullScreenButtonVisibility( - @Nullable View fullScreenButton, boolean visible) { - if (fullScreenButton == null) { - return; - } - if (visible) { - fullScreenButton.setVisibility(VISIBLE); - } else { - fullScreenButton.setVisibility(GONE); - } - } - - @SuppressWarnings("ResourceType") - private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes( - TypedArray a, @RepeatModeUtil.RepeatToggleModes int defaultValue) { - return a.getInt(R.styleable.PlayerControlView_repeat_toggle_modes, defaultValue); - } - - private final class ComponentListener - implements Player.Listener, - TimeBar.OnScrubListener, - OnClickListener, - PopupWindow.OnDismissListener { - - @Override - public void onEvents(Player player, Events events) { - if (events.containsAny( - EVENT_PLAYBACK_STATE_CHANGED, - EVENT_PLAY_WHEN_READY_CHANGED, - EVENT_AVAILABLE_COMMANDS_CHANGED)) { - updatePlayPauseButton(); - } - if (events.containsAny( - EVENT_PLAYBACK_STATE_CHANGED, - EVENT_PLAY_WHEN_READY_CHANGED, - EVENT_IS_PLAYING_CHANGED, - EVENT_AVAILABLE_COMMANDS_CHANGED)) { - updateProgress(); - } - if (events.containsAny(EVENT_REPEAT_MODE_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { - updateRepeatModeButton(); - } - if (events.containsAny( - EVENT_SHUFFLE_MODE_ENABLED_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { - updateShuffleButton(); - } - if (events.containsAny( - EVENT_REPEAT_MODE_CHANGED, - EVENT_SHUFFLE_MODE_ENABLED_CHANGED, - EVENT_POSITION_DISCONTINUITY, - EVENT_TIMELINE_CHANGED, - EVENT_SEEK_BACK_INCREMENT_CHANGED, - EVENT_SEEK_FORWARD_INCREMENT_CHANGED, - EVENT_AVAILABLE_COMMANDS_CHANGED)) { - updateNavigation(); - } - if (events.containsAny( - EVENT_POSITION_DISCONTINUITY, EVENT_TIMELINE_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { - updateTimeline(); - } - if (events.containsAny(EVENT_PLAYBACK_PARAMETERS_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { - updatePlaybackSpeedList(); - } - if (events.containsAny(EVENT_TRACKS_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { - updateTrackLists(); - } - if (events.containsAny(EVENT_METADATA, EVENT_MEDIA_ITEM_TRANSITION, EVENT_MEDIA_METADATA_CHANGED)) { - updateTitle(); - } - } - - @Override - public void onScrubStart(TimeBar timeBar, long position) { - scrubbing = true; - if (positionView != null) { - positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); - } - controlViewLayoutManager.removeHideCallbacks(); - } - - @Override - public void onScrubMove(TimeBar timeBar, long position) { - if (positionView != null) { - positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); - } - } - - @Override - public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { - scrubbing = false; - if (!canceled && player != null) { - seekToTimeBarPosition(player, position); - } - controlViewLayoutManager.resetHideCallbacks(); - } - - @Override - public void onDismiss() { - if (needToHideBars) { - controlViewLayoutManager.resetHideCallbacks(); - } - } - - @Override - public void onClick(View view) { - @Nullable Player player = PlayerControlView.this.player; - if (player == null) { - return; - } - controlViewLayoutManager.resetHideCallbacks(); - if (nextButton == view) { - if (player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)) { - player.seekToNext(); - } - } else if (previousButton == view) { - if (player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) { - player.seekToPrevious(); - } - } else if (fastForwardButton == view) { - if (player.getPlaybackState() != Player.STATE_ENDED - && player.isCommandAvailable(COMMAND_SEEK_FORWARD)) { - player.seekForward(); - } - } else if (rewindButton == view) { - if (player.isCommandAvailable(COMMAND_SEEK_BACK)) { - player.seekBack(); - } - } else if (playPauseButton == view) { - Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed); - } else if (repeatToggleButton == view) { - if (player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) { - player.setRepeatMode( - RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); - } - } else if (shuffleButton == view) { - if (player.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)) { - player.setShuffleModeEnabled(!player.getShuffleModeEnabled()); - } - } else if (settingsButton == view) { - controlViewLayoutManager.removeHideCallbacks(); - displaySettingsWindow(settingsAdapter, settingsButton); - } else if (playbackSpeedButton == view) { - controlViewLayoutManager.removeHideCallbacks(); - displaySettingsWindow(playbackSpeedAdapter, playbackSpeedButton); - } else if (audioTrackButton == view) { - controlViewLayoutManager.removeHideCallbacks(); - displaySettingsWindow(audioTrackSelectionAdapter, audioTrackButton); - } else if (subtitleButton == view) { - controlViewLayoutManager.removeHideCallbacks(); - displaySettingsWindow(textTrackSelectionAdapter, subtitleButton); - } - } - } - - private class SettingsAdapter extends RecyclerView.Adapter { - - private final String[] mainTexts; - private final String[] subTexts; - private final Drawable[] iconIds; - - public SettingsAdapter(String[] mainTexts, Drawable[] iconIds) { - this.mainTexts = mainTexts; - this.subTexts = new String[mainTexts.length]; - this.iconIds = iconIds; - } - - @Override - public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = - LayoutInflater.from(getContext()) - .inflate(R.layout.exo_styled_settings_list_item, parent, /* attachToRoot= */ false); - return new SettingViewHolder(v); - } - - @Override - public void onBindViewHolder(SettingViewHolder holder, int position) { - if (shouldShowSetting(position)) { - holder.itemView.setLayoutParams( - new RecyclerView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - } else { - holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(0, 0)); - } - - holder.mainTextView.setText(mainTexts[position]); - - if (subTexts[position] == null) { - holder.subTextView.setVisibility(GONE); - } else { - holder.subTextView.setText(subTexts[position]); - } - - if (iconIds[position] == null) { - holder.iconView.setVisibility(GONE); - } else { - holder.iconView.setImageDrawable(iconIds[position]); - } - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public int getItemCount() { - return mainTexts.length; - } - - public void setSubTextAtPosition(int position, String subText) { - this.subTexts[position] = subText; - } - - public boolean hasSettingsToShow() { - return shouldShowSetting(SETTINGS_AUDIO_TRACK_SELECTION_POSITION) - || shouldShowSetting(SETTINGS_PLAYBACK_SPEED_POSITION); - } - - private boolean shouldShowSetting(int position) { - if (player == null) { - return false; - } - switch (position) { - case SETTINGS_AUDIO_TRACK_SELECTION_POSITION: - return player.isCommandAvailable(COMMAND_GET_TRACKS) - && player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS); - case SETTINGS_PLAYBACK_SPEED_POSITION: - return player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH); - default: - return true; - } - } - } - - private final class SettingViewHolder extends RecyclerView.ViewHolder { - - private final TextView mainTextView; - private final TextView subTextView; - private final ImageView iconView; - - public SettingViewHolder(View itemView) { - super(itemView); - if (Util.SDK_INT < 26) { - // Workaround for https://github.com/google/ExoPlayer/issues/9061. - itemView.setFocusable(true); - } - mainTextView = itemView.findViewById(R.id.exo_main_text); - subTextView = itemView.findViewById(R.id.exo_sub_text); - iconView = itemView.findViewById(R.id.exo_icon); - itemView.setOnClickListener(v -> onSettingViewClicked(getAdapterPosition())); - } - } - - private final class PlaybackSpeedAdapter extends RecyclerView.Adapter { - - private final String[] playbackSpeedTexts; - private final float[] playbackSpeeds; - private int selectedIndex; - - public PlaybackSpeedAdapter(String[] playbackSpeedTexts, float[] playbackSpeeds) { - this.playbackSpeedTexts = playbackSpeedTexts; - this.playbackSpeeds = playbackSpeeds; - } - - public void updateSelectedIndex(float playbackSpeed) { - int closestMatchIndex = 0; - float closestMatchDifference = Float.MAX_VALUE; - for (int i = 0; i < playbackSpeeds.length; i++) { - float difference = Math.abs(playbackSpeed - playbackSpeeds[i]); - if (difference < closestMatchDifference) { - closestMatchIndex = i; - closestMatchDifference = difference; - } - } - selectedIndex = closestMatchIndex; - } - - public String getSelectedText() { - return playbackSpeedTexts[selectedIndex]; - } - - @Override - public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = - LayoutInflater.from(getContext()) - .inflate( - R.layout.exo_styled_sub_settings_list_item, parent, /* attachToRoot= */ false); - return new SubSettingViewHolder(v); - } - - @Override - public void onBindViewHolder(SubSettingViewHolder holder, int position) { - if (position < playbackSpeedTexts.length) { - holder.textView.setText(playbackSpeedTexts[position]); - } - holder.description.setVisibility(GONE); - holder.itemView.setSelected(position == selectedIndex); - holder.checkView.setVisibility(position == selectedIndex ? VISIBLE : INVISIBLE); - holder.itemView.setOnClickListener(v -> { - if (position != selectedIndex) { - setPlaybackSpeed(playbackSpeeds[position]); - } - settingsWindow.dismiss(); - }); - } - - @Override - public int getItemCount() { - return playbackSpeedTexts.length; - } - } - - private static final class TrackInformation { - - public final Tracks.Group trackGroup; - public final int trackIndex; - public final String trackName; - public final String trackLabel; - - public TrackInformation(Tracks tracks, - int trackGroupIndex, - int trackIndex, - String trackName, - String trackLabel) { - this.trackGroup = tracks.getGroups().get(trackGroupIndex); - this.trackIndex = trackIndex; - this.trackName = trackName; - this.trackLabel = trackLabel; - } - - public boolean isSelected() { - return trackGroup.isTrackSelected(trackIndex); - } - } - - private final class TextTrackSelectionAdapter extends TrackSelectionAdapter { - @Override - public void init(List trackInformations) { - boolean subtitleIsOn = false; - for (int i = 0; i < trackInformations.size(); i++) { - if (trackInformations.get(i).isSelected()) { - subtitleIsOn = true; - break; - } - } - - if (subtitleButton != null) { - subtitleButton.setImageDrawable( - subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable); - subtitleButton.setContentDescription( - subtitleIsOn ? subtitleOnContentDescription : subtitleOffContentDescription); - } - this.tracks = trackInformations; - } - - @Override - public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) { - // CC options include "Off" at the first position, which disables text rendering. - holder.textView.setText(R.string.exo_track_selection_none); - holder.description.setVisibility(View.GONE); - boolean isTrackSelectionOff = true; - for (int i = 0; i < tracks.size(); i++) { - if (tracks.get(i).isSelected()) { - isTrackSelectionOff = false; - break; - } - } - holder.checkView.setVisibility(isTrackSelectionOff ? VISIBLE : INVISIBLE); - holder.itemView.setOnClickListener( - v -> { - if (player != null - && player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { - TrackSelectionParameters trackSelectionParameters = - player.getTrackSelectionParameters(); - player.setTrackSelectionParameters( - trackSelectionParameters - .buildUpon() - .clearOverridesOfType(C.TRACK_TYPE_TEXT) - .setIgnoredTextSelectionFlags(~C.SELECTION_FLAG_FORCED) - .build()); - settingsWindow.dismiss(); - } - }); - } - - @Override - public void onBindViewHolder(SubSettingViewHolder holder, int position) { - super.onBindViewHolder(holder, position); - if (position > 0) { - TrackInformation track = tracks.get(position - 1); - holder.checkView.setVisibility(track.isSelected() ? VISIBLE : INVISIBLE); - } - } - - @Override - public void onTrackSelection(String subtext) { - // No-op - } - } - - private final class AudioTrackSelectionAdapter extends TrackSelectionAdapter { - - @Override - public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) { - // Audio track selection option includes "Auto" at the top. - holder.textView.setText(R.string.exo_track_selection_auto); - holder.description.setVisibility(View.GONE); - // hasSelectionOverride is true means there is an explicit track selection, not "Auto". - TrackSelectionParameters parameters = checkNotNull(player).getTrackSelectionParameters(); - boolean hasSelectionOverride = hasSelectionOverride(parameters); - holder.checkView.setVisibility(hasSelectionOverride ? INVISIBLE : VISIBLE); - holder.itemView.setOnClickListener( - v -> { - if (player == null - || !player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { - return; - } - TrackSelectionParameters trackSelectionParameters = - player.getTrackSelectionParameters(); - castNonNull(player) - .setTrackSelectionParameters( - trackSelectionParameters - .buildUpon() - .clearOverridesOfType(C.TRACK_TYPE_AUDIO) - .setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, /* disabled= */ false) - .build()); - settingsAdapter.setSubTextAtPosition( - SETTINGS_AUDIO_TRACK_SELECTION_POSITION, - getResources().getString(R.string.exo_track_selection_auto)); - settingsWindow.dismiss(); - }); - } - - private boolean hasSelectionOverride(TrackSelectionParameters trackSelectionParameters) { - for (int i = 0; i < tracks.size(); i++) { - TrackGroup trackGroup = tracks.get(i).trackGroup.getMediaTrackGroup(); - if (trackSelectionParameters.overrides.containsKey(trackGroup)) { - return true; - } - } - return false; - } - - @Override - public void onTrackSelection(String subtext) { - settingsAdapter.setSubTextAtPosition(SETTINGS_AUDIO_TRACK_SELECTION_POSITION, subtext); - } - - @Override - public void init(List trackInformations) { - this.tracks = trackInformations; - // Update subtext in settings menu with current audio track selection. - TrackSelectionParameters params = checkNotNull(player).getTrackSelectionParameters(); - if (trackInformations.isEmpty()) { - settingsAdapter.setSubTextAtPosition( - SETTINGS_AUDIO_TRACK_SELECTION_POSITION, - getResources().getString(R.string.exo_track_selection_none)); - // TODO(insun) : Make the audio item in main settings (settingsAdapater) - // to be non-clickable. - } else if (!hasSelectionOverride(params)) { - settingsAdapter.setSubTextAtPosition( - SETTINGS_AUDIO_TRACK_SELECTION_POSITION, - getResources().getString(R.string.exo_track_selection_auto)); - } else { - for (int i = 0; i < trackInformations.size(); i++) { - TrackInformation track = trackInformations.get(i); - if (track.isSelected()) { - settingsAdapter.setSubTextAtPosition( - SETTINGS_AUDIO_TRACK_SELECTION_POSITION, track.trackName); - break; - } - } - } - } - } - - private abstract class TrackSelectionAdapter extends RecyclerView.Adapter { - - protected List tracks; - - protected TrackSelectionAdapter() { - this.tracks = new ArrayList<>(); - } - - public abstract void init(List trackInformations); - - @Override - public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = - LayoutInflater.from(getContext()) - .inflate( - R.layout.exo_styled_sub_settings_list_item, parent, /* attachToRoot= */ false); - return new SubSettingViewHolder(v); - } - - protected abstract void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder); - - protected abstract void onTrackSelection(String subtext); - - @Override - public void onBindViewHolder(SubSettingViewHolder holder, int position) { - @Nullable Player player = PlayerControlView.this.player; - if (player == null) { - return; - } - if (position == 0) { - onBindViewHolderAtZeroPosition(holder); - } else { - TrackInformation track = tracks.get(position - 1); - TrackGroup mediaTrackGroup = track.trackGroup.getMediaTrackGroup(); - TrackSelectionParameters params = player.getTrackSelectionParameters(); - boolean explicitlySelected = - params.overrides.get(mediaTrackGroup) != null && track.isSelected(); - holder.textView.setText(track.trackName); - if (TextUtils.isEmpty(track.trackLabel)) { - holder.description.setVisibility(View.GONE); - } else { - holder.description.setVisibility(View.VISIBLE); - holder.description.setText(track.trackLabel); - } - holder.checkView.setVisibility(explicitlySelected ? VISIBLE : INVISIBLE); - holder.itemView.setOnClickListener( - v -> { - if (!player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { - return; - } - TrackSelectionParameters trackSelectionParameters = - player.getTrackSelectionParameters(); - player.setTrackSelectionParameters( - trackSelectionParameters - .buildUpon() - .setOverrideForType( - new TrackSelectionOverride( - mediaTrackGroup, ImmutableList.of(track.trackIndex))) - .setTrackTypeDisabled(track.trackGroup.getType(), /* disabled= */ false) - .build()); - onTrackSelection(track.trackName); - settingsWindow.dismiss(); - }); - } - } - - @Override - public int getItemCount() { - return tracks.isEmpty() ? 0 : tracks.size() + 1; - } - - protected void clear() { - tracks = Collections.emptyList(); - } - } - - private static class SubSettingViewHolder extends RecyclerView.ViewHolder { - - public final TextView textView; - public final TextView description; - public final View checkView; - - public SubSettingViewHolder(View itemView) { - super(itemView); - if (Util.SDK_INT < 26) { - // Workaround for https://github.com/google/ExoPlayer/issues/9061. - itemView.setFocusable(true); - } - textView = itemView.findViewById(R.id.exo_text); - description = itemView.findViewById(com.skyd.anivu.R.id.exo_description); - checkView = itemView.findViewById(R.id.exo_check); - } - } -} diff --git a/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlViewLayoutManager.kt b/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlViewLayoutManager.kt deleted file mode 100644 index 249d5259..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlViewLayoutManager.kt +++ /dev/null @@ -1,604 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.skyd.anivu.ui.player - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.AnimatorSet -import android.animation.ObjectAnimator -import android.animation.ValueAnimator -import android.annotation.SuppressLint -import android.view.View -import android.view.View.OnLayoutChangeListener -import android.view.ViewGroup -import android.view.ViewGroup.MarginLayoutParams -import android.view.animation.LinearInterpolator -import androidx.media3.ui.DefaultTimeBar -import com.skyd.anivu.R -import kotlin.math.max - -@SuppressLint("UnsafeOptInUsageError", "PrivateResource") -internal class PlayerControlViewLayoutManager(private val playerControlView: PlayerControlView) { - // ViewGroup that can be automatically hidden - private val autoHiddenControllerView: ViewGroup = - playerControlView.findViewById(R.id.exo_auto_hidden_control_view) - - // Relating to Center View - private val controlsBackground: View? = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_controls_background) - private val centerControls: ViewGroup? = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_center_controls) - - // Relating to Bottom Bar View - private val bottomBar: ViewGroup? = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_bottom_bar) - - // Relating to Top Bar View - private val topBar: ViewGroup? = playerControlView.findViewById(R.id.exo_top_bar) - - // Relating to Reset Zoom View - private val resetZoomButton: View? = playerControlView.findViewById(R.id.exo_reset_zoom) - - // Relating to Forward 85s View - private val forward85sButton: View? = playerControlView.findViewById(R.id.exo_forward_85s) - - // Relating to Minimal Layout - private val minimalControls: ViewGroup? = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_minimal_controls) - - // Relating to Bottom Bar Right View - private val basicControls: ViewGroup? = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_basic_controls) - private val extraControls: ViewGroup? = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_extra_controls) - private val extraControlsScrollView: ViewGroup? = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_extra_controls_scroll_view) - private val overflowHideButton = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_overflow_hide) - - // Relating to Bottom Bar Left View - private val timeView: ViewGroup? = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_time) - private val timeBar: View? = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_progress) - - private val overflowShowButton: View? = - playerControlView.findViewById(androidx.media3.ui.R.id.exo_overflow_show) - private val hideMainBarAnimator: AnimatorSet = AnimatorSet() - private val hideProgressBarAnimator: AnimatorSet = AnimatorSet() - private val hideAllBarsAnimator: AnimatorSet = AnimatorSet() - private val showMainBarAnimator: AnimatorSet = AnimatorSet() - private val showAllBarsAnimator: AnimatorSet = AnimatorSet() - private val overflowShowAnimator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) - private val overflowHideAnimator: ValueAnimator = ValueAnimator.ofFloat(1.0f, 0.0f) - private val showAllBarsRunnable: Runnable = Runnable { showAllBars() } - private val hideAllBarsRunnable: Runnable = Runnable { hideAllBars() } - private val hideProgressBarRunnable: Runnable = Runnable { hideProgressBar() } - private val hideMainBarRunnable: Runnable = Runnable { hideMainBar() } - private val hideControllerRunnable: Runnable = Runnable { hideController() } - private val onLayoutChangeListener: OnLayoutChangeListener = - OnLayoutChangeListener { v: View, left: Int, _: Int, right: Int, _: Int, - oldLeft: Int, _: Int, oldRight: Int, _: Int -> - onLayoutChange(v, left, right, oldLeft, oldRight) - } - private val shownButtons: MutableList = mutableListOf() - private var uxState: Int = UX_STATE_ALL_VISIBLE - private var isMinimalMode = false - private var needToShowBars = false - var isAnimationEnabled: Boolean = true - - init { - if (overflowShowButton != null && overflowHideButton != null) { - overflowShowButton.setOnClickListener(::onOverflowButtonClick) - overflowHideButton.setOnClickListener(::onOverflowButtonClick) - } - - val fadeOutAnimator = ValueAnimator.ofFloat(1.0f, 0.0f) - fadeOutAnimator.interpolator = LinearInterpolator() - fadeOutAnimator.addUpdateListener { animation: ValueAnimator -> - val animatedValue = animation.getAnimatedValue() as Float - controlsBackground?.setAlpha(animatedValue) - centerControls?.setAlpha(animatedValue) - minimalControls?.setAlpha(animatedValue) - } - fadeOutAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - if (timeBar is DefaultTimeBar && !isMinimalMode) { - timeBar.hideScrubber(DURATION_FOR_HIDING_ANIMATION_MS) - } - } - - override fun onAnimationEnd(animation: Animator) { - controlsBackground?.visibility = View.INVISIBLE - centerControls?.visibility = View.INVISIBLE - minimalControls?.visibility = View.INVISIBLE - } - }) - - val fadeInAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) - fadeInAnimator.interpolator = LinearInterpolator() - fadeInAnimator.addUpdateListener { animation -> - val animatedValue = animation.getAnimatedValue() as Float - controlsBackground?.setAlpha(animatedValue) - centerControls?.setAlpha(animatedValue) - minimalControls?.setAlpha(animatedValue) - } - fadeInAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - controlsBackground?.visibility = View.VISIBLE - centerControls?.visibility = View.VISIBLE - minimalControls?.visibility = if (isMinimalMode) View.VISIBLE else View.INVISIBLE - if (timeBar is DefaultTimeBar && !isMinimalMode) { - timeBar.showScrubber(DURATION_FOR_SHOWING_ANIMATION_MS) - } - } - }) - - hideMainBarAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS) - hideMainBarAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) = setUxState(UX_STATE_ANIMATING_HIDE) - - override fun onAnimationEnd(animation: Animator) { - setUxState(UX_STATE_ONLY_PROGRESS_VISIBLE) - if (needToShowBars) { - playerControlView.post(showAllBarsRunnable) - needToShowBars = false - } - } - }) - hideMainBarAnimator - .play(fadeOutAnimator) - .with(ofAlpha(1f, 0f, timeBar)) - .with(ofAlpha(1f, 0f, bottomBar)) - .with(ofAlpha(1f, 0f, topBar)) - .with(ofAlpha(1f, 0f, resetZoomButton)) - .with(ofAlpha(1f, 0f, forward85sButton)) - - hideProgressBarAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS) - hideProgressBarAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) = setUxState(UX_STATE_ANIMATING_HIDE) - - override fun onAnimationEnd(animation: Animator) { - setUxState(UX_STATE_NONE_VISIBLE) - if (needToShowBars) { - playerControlView.post(showAllBarsRunnable) - needToShowBars = false - } - } - }) - hideProgressBarAnimator - .play(ofAlpha(1f, 0f, timeBar)) - .with(ofAlpha(1f, 0f, bottomBar)) - .with(ofAlpha(1f, 0f, topBar)) - .with(ofAlpha(1f, 0f, resetZoomButton)) - .with(ofAlpha(1f, 0f, forward85sButton)) - - hideAllBarsAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS) - hideAllBarsAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) = setUxState(UX_STATE_ANIMATING_HIDE) - - override fun onAnimationEnd(animation: Animator) { - setUxState(UX_STATE_NONE_VISIBLE) - if (needToShowBars) { - playerControlView.post(showAllBarsRunnable) - needToShowBars = false - } - } - }) - hideAllBarsAnimator - .play(fadeOutAnimator) - .with(ofAlpha(1f, 0f, timeBar)) - .with(ofAlpha(1f, 0f, bottomBar)) - .with(ofAlpha(1f, 0f, topBar)) - .with(ofAlpha(1f, 0f, resetZoomButton)) - .with(ofAlpha(1f, 0f, forward85sButton)) - - showMainBarAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS) - showMainBarAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) = setUxState(UX_STATE_ANIMATING_SHOW) - override fun onAnimationEnd(animation: Animator) = setUxState(UX_STATE_ALL_VISIBLE) - }) - showMainBarAnimator - .play(fadeInAnimator) - .with(ofAlpha(0f, 1f, timeBar)) - .with(ofAlpha(0f, 1f, bottomBar)) - .with(ofAlpha(0f, 1f, topBar)) - .with(ofAlpha(0f, 1f, resetZoomButton)) - .with(ofAlpha(0f, 1f, forward85sButton)) - - showAllBarsAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS) - showAllBarsAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) = setUxState(UX_STATE_ANIMATING_SHOW) - override fun onAnimationEnd(animation: Animator) = setUxState(UX_STATE_ALL_VISIBLE) - }) - showAllBarsAnimator - .play(fadeInAnimator) - .with(ofAlpha(0f, 1f, timeBar)) - .with(ofAlpha(0f, 1f, bottomBar)) - .with(ofAlpha(0f, 1f, topBar)) - .with(ofAlpha(0f, 1f, resetZoomButton)) - .with(ofAlpha(0f, 1f, forward85sButton)) - - overflowShowAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS) - overflowShowAnimator.addUpdateListener { animation -> - animateOverflow(animation.getAnimatedValue() as Float) - } - overflowShowAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - extraControlsScrollView?.apply { - visibility = View.VISIBLE - translationX = width.toFloat() - scrollTo(width, 0) - } - } - - override fun onAnimationEnd(animation: Animator) { - basicControls?.visibility = View.INVISIBLE - } - }) - - overflowHideAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS) - overflowHideAnimator.addUpdateListener { animation -> - animateOverflow(animation.getAnimatedValue() as Float) - } - overflowHideAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - basicControls?.visibility = View.VISIBLE - } - - override fun onAnimationEnd(animation: Animator) { - extraControlsScrollView?.visibility = View.INVISIBLE - } - }) - } - - fun show() { - if (!playerControlView.isAutoHiddenControllerVisible) { - autoHiddenControllerView.visibility = View.VISIBLE - playerControlView.updateAll() - playerControlView.requestPlayPauseFocus() - } - showAllBars() - } - - fun hide() { - if (uxState == UX_STATE_ANIMATING_HIDE || uxState == UX_STATE_NONE_VISIBLE) { - return - } - removeHideCallbacks() - if (!isAnimationEnabled) { - hideController() - } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { - hideProgressBar() - } else { - hideAllBars() - } - } - - fun hideImmediately() { - if (uxState == UX_STATE_ANIMATING_HIDE || uxState == UX_STATE_NONE_VISIBLE) { - return - } - removeHideCallbacks() - hideController() - } - - fun resetHideCallbacks() { - if (uxState == UX_STATE_ANIMATING_HIDE) { - return - } - removeHideCallbacks() - val showTimeoutMs = playerControlView.showTimeoutMs - if (showTimeoutMs > 0) { - if (!isAnimationEnabled) { - postDelayedRunnable(hideControllerRunnable, showTimeoutMs.toLong()) - } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { - postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS) - } else { - postDelayedRunnable(hideMainBarRunnable, showTimeoutMs.toLong()) - } - } - } - - fun removeHideCallbacks() { - playerControlView.apply { - removeCallbacks(hideControllerRunnable) - removeCallbacks(hideAllBarsRunnable) - removeCallbacks(hideMainBarRunnable) - removeCallbacks(hideProgressBarRunnable) - } - } - - fun onAttachedToWindow() { - playerControlView.addOnLayoutChangeListener(onLayoutChangeListener) - } - - fun onDetachedFromWindow() { - playerControlView.removeOnLayoutChangeListener(onLayoutChangeListener) - } - - val isFullyVisible: Boolean - get() = uxState == UX_STATE_ALL_VISIBLE && playerControlView.isAutoHiddenControllerVisible - - fun setShowButton(button: View?, showButton: Boolean) { - button ?: return - if (!showButton) { - button.visibility = View.GONE - shownButtons.remove(button) - return - } - button.visibility = if (isMinimalMode && shouldHideInMinimalMode(button)) { - View.INVISIBLE - } else { - View.VISIBLE - } - shownButtons.add(button) - } - - fun getShowButton(button: View?): Boolean { - return button != null && shownButtons.contains(button) - } - - private fun setUxState(uxState: Int) { - val prevUxState = this.uxState - this.uxState = uxState - if (uxState == UX_STATE_NONE_VISIBLE) { - autoHiddenControllerView.visibility = View.GONE - } else if (prevUxState == UX_STATE_NONE_VISIBLE) { - autoHiddenControllerView.visibility = View.VISIBLE - } - // TODO(insun): Notify specific uxState. Currently reuses legacy visibility listener for API - // compatibility. - if (prevUxState != uxState) { - playerControlView.notifyOnVisibilityChange() - } - } - - fun onLayout(left: Int, top: Int, right: Int, bottom: Int) { - controlsBackground?.layout(0, 0, right - left, bottom - top) - } - - private fun onLayoutChange(v: View, left: Int, right: Int, oldLeft: Int, oldRight: Int) { - val useMinimalMode = useMinimalMode() - if (isMinimalMode != useMinimalMode) { - isMinimalMode = useMinimalMode - v.post(::updateLayoutForSizeChange) - } - val widthChanged = right - left != oldRight - oldLeft - if (!isMinimalMode && widthChanged) { - v.post(::onLayoutWidthChanged) - } - } - - private fun onOverflowButtonClick(v: View) { - resetHideCallbacks() - if (v.id == androidx.media3.ui.R.id.exo_overflow_show) { - overflowShowAnimator.start() - } else if (v.id == androidx.media3.ui.R.id.exo_overflow_hide) { - overflowHideAnimator.start() - } - } - - private fun showAllBars() { - if (!isAnimationEnabled) { - setUxState(UX_STATE_ALL_VISIBLE) - resetHideCallbacks() - return - } - when (uxState) { - UX_STATE_NONE_VISIBLE -> showAllBarsAnimator.start() - UX_STATE_ONLY_PROGRESS_VISIBLE -> showMainBarAnimator.start() - UX_STATE_ANIMATING_HIDE -> needToShowBars = true - UX_STATE_ANIMATING_SHOW -> return - else -> Unit - } - resetHideCallbacks() - } - - private fun hideAllBars() { - hideAllBarsAnimator.start() - } - - private fun hideProgressBar() { - hideProgressBarAnimator.start() - } - - private fun hideMainBar() { - hideMainBarAnimator.start() - postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS) - } - - private fun hideController() { - setUxState(UX_STATE_NONE_VISIBLE) - } - - private fun postDelayedRunnable(runnable: Runnable, interval: Long) { - if (interval >= 0) { - playerControlView.postDelayed(runnable, interval) - } - } - - private fun animateOverflow(animatedValue: Float) { - extraControlsScrollView?.apply { - translationX = width * (1 - animatedValue) - } - timeView?.setAlpha(1 - animatedValue) - basicControls?.setAlpha(1 - animatedValue) - } - - private fun useMinimalMode(): Boolean { - val width = playerControlView.width - - playerControlView.getPaddingLeft() - - playerControlView.getPaddingRight() - val height = playerControlView.height - - playerControlView.paddingBottom - - playerControlView.paddingTop - val centerControlWidth = getWidthWithMargins(centerControls) - - if (centerControls != null) { - centerControls.getPaddingLeft() + centerControls.getPaddingRight() - } else 0 - val centerControlHeight = getHeightWithMargins(centerControls) - - if (centerControls != null) { - centerControls.paddingTop + centerControls.paddingBottom - } else 0 - val defaultModeMinimumWidth = max( - centerControlWidth, - getWidthWithMargins(timeView) + getWidthWithMargins(overflowShowButton) - ) - val defaultModeMinimumHeight = (centerControlHeight + getHeightWithMargins(bottomBar) - + getHeightWithMargins(topBar)) /* + (2 * getHeightWithMargins(bottomBar))*/ - return width <= defaultModeMinimumWidth || height <= defaultModeMinimumHeight - } - - private fun updateLayoutForSizeChange() { - minimalControls?.visibility = if (isMinimalMode) View.VISIBLE else View.INVISIBLE - if (timeBar != null) { - val timeBarMarginBottom = playerControlView.resources - .getDimensionPixelSize(androidx.media3.ui.R.dimen.exo_styled_progress_margin_bottom) - val timeBarParams = timeBar.layoutParams as? MarginLayoutParams - if (timeBarParams != null) { - timeBarParams.bottomMargin = if (isMinimalMode) 0 else timeBarMarginBottom - timeBar.setLayoutParams(timeBarParams) - } - if (timeBar is DefaultTimeBar) { - if (isMinimalMode) { - timeBar.hideScrubber(/* disableScrubberPadding= */ true) - } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { - timeBar.hideScrubber(/* disableScrubberPadding= */ false) - } else if (uxState != UX_STATE_ANIMATING_HIDE) { - timeBar.showScrubber() - } - } - } - for (v in shownButtons) { - v.visibility = - if (isMinimalMode && shouldHideInMinimalMode(v)) View.INVISIBLE else View.VISIBLE - } - } - - private fun shouldHideInMinimalMode(button: View): Boolean { - val id = button.id - return id == androidx.media3.ui.R.id.exo_bottom_bar || - id == androidx.media3.ui.R.id.exo_prev || - id == androidx.media3.ui.R.id.exo_next || - id == androidx.media3.ui.R.id.exo_rew || - id == androidx.media3.ui.R.id.exo_rew_with_amount || - id == androidx.media3.ui.R.id.exo_ffwd || - id == androidx.media3.ui.R.id.exo_ffwd_with_amount - } - - private fun onLayoutWidthChanged() { - if (basicControls == null || extraControls == null) return - val width = playerControlView.width - - playerControlView.getPaddingLeft() - - playerControlView.getPaddingRight() - - // Reset back to all controls being basic controls and the overflow not being needed. The last - // child of extraControls is the overflow hide button, which shouldn't be moved back. - while (extraControls.childCount > 1) { - val controlViewIndex = extraControls.childCount - 2 - val controlView = extraControls.getChildAt(controlViewIndex) - extraControls.removeViewAt(controlViewIndex) - basicControls.addView(controlView, /* index= */ 0) - } - overflowShowButton?.visibility = View.GONE - - // Calculate how much of the available width is occupied. The last child of basicControls is the - // overflow show button, which we're currently assuming will not be visible. - var occupiedWidth = getWidthWithMargins(timeView) - val endIndex = basicControls.childCount - 1 - for (i in 0 until endIndex) { - val controlView = basicControls.getChildAt(i) - occupiedWidth += getWidthWithMargins(controlView) - } - if (occupiedWidth > width) { - // We need to move some controls to extraControls. - overflowShowButton?.visibility = View.VISIBLE - occupiedWidth += getWidthWithMargins(overflowShowButton) - val controlsToMove = ArrayList() - // The last child of basicControls is the overflow show button, which shouldn't be moved. - for (i in 0 until endIndex) { - val control = basicControls.getChildAt(i) - occupiedWidth -= getWidthWithMargins(control) - controlsToMove.add(control) - if (occupiedWidth <= width) { - break - } - } - if (controlsToMove.isNotEmpty()) { - basicControls.removeViews(/* start= */ 0, controlsToMove.size) - for (i in controlsToMove.indices) { - // The last child of extraControls is the overflow hide button. Add controls before it. - val index = extraControls.childCount - 1 - extraControls.addView(controlsToMove[i], index) - } - } - } else { - // If extraControls are visible, hide them since they're now empty. - if (extraControlsScrollView?.visibility == View.VISIBLE && - !overflowHideAnimator.isStarted - ) { - overflowShowAnimator.cancel() - overflowHideAnimator.start() - } - } - } - - companion object { - private const val ANIMATION_INTERVAL_MS: Long = 2000 - private const val DURATION_FOR_HIDING_ANIMATION_MS: Long = 250 - private const val DURATION_FOR_SHOWING_ANIMATION_MS: Long = 250 - - // Int for defining the UX state where all the views (ProgressBar, BottomBar) are all visible. - private const val UX_STATE_ALL_VISIBLE = 0 - - // Int for defining the UX state where only the ProgressBar view is visible. - private const val UX_STATE_ONLY_PROGRESS_VISIBLE = 1 - - // Int for defining the UX state where none of the views are visible. - private const val UX_STATE_NONE_VISIBLE = 2 - - // Int for defining the UX state where the views are being animated to be hidden. - private const val UX_STATE_ANIMATING_HIDE = 3 - - // Int for defining the UX state where the views are being animated to be shown. - private const val UX_STATE_ANIMATING_SHOW = 4 - - private fun ofAlpha(startValue: Float, endValue: Float, target: View?): ObjectAnimator { - return ObjectAnimator.ofFloat(target, "alpha", startValue, endValue) - } - - private fun getWidthWithMargins(v: View?): Int { - v ?: return 0 - var width = v.width - val layoutParams = v.layoutParams - if (layoutParams is MarginLayoutParams) { - width += layoutParams.leftMargin + layoutParams.rightMargin - } - return width - } - - private fun getHeightWithMargins(v: View?): Int { - v ?: return 0 - var height = v.height - val layoutParams = v.layoutParams - if (layoutParams is MarginLayoutParams) { - height += layoutParams.topMargin + layoutParams.bottomMargin - } - return height - } - } -} diff --git a/app/src/main/java/com/skyd/anivu/ui/player/PlayerGestureDetector.kt b/app/src/main/java/com/skyd/anivu/ui/player/PlayerGestureDetector.kt deleted file mode 100644 index a0ee4f9d..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/player/PlayerGestureDetector.kt +++ /dev/null @@ -1,257 +0,0 @@ -package com.skyd.anivu.ui.player - -import android.animation.Animator -import android.animation.AnimatorSet -import android.animation.ObjectAnimator -import android.animation.ValueAnimator -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.view.MotionEvent -import android.view.View -import android.view.ViewConfiguration -import android.view.animation.DecelerateInterpolator -import androidx.core.animation.addListener -import kotlin.math.abs -import kotlin.math.atan2 -import kotlin.math.sqrt - - -class PlayerGestureDetector( - private val mListener: PlayerGestureListener -) { - // ================================================= 双指操作的变量 - private var isZoom = false - var useAnimate: Boolean = true - - var scale = 1f // 伸缩比例 - var translationX = 0f // 移动X - var translationY = 0f // 移动Y - var rotation = 0f // 旋转角度 - - private var lastScale = 1f // 上次松开触摸前的伸缩比例 - private var lastTranslationX = 0f // 上次松开触摸前的移动X - private var lastTranslationY = 0f // 上次松开触摸前的移动Y - private var lastRotation = 0f // 上次松开触摸前的旋转角度 - - // 移动过程中临时变量 - private var actionX = 0f - private var actionY = 0f - private var spacing = 0f - private var degree = 0f - private var doublePointer = 0 - // ================================================= - - // ================================================= 单指移动的变量 - private var singleMoveDownX = 0f - private var singleMoveDownY = 0f - private var singleMove = 0 // 0:已抬起、1:按下、2:已移动并且移动距离超过阈值 - // ================================================= - - // 恢复初始Zoom状态 - fun restoreZoom(targetView: View? = null) { - val animators: Array - if (targetView == null) { - animators = arrayOf( - ValueAnimator.ofFloat(rotation, 0f), - ValueAnimator.ofFloat(translationX, 0f), - ValueAnimator.ofFloat(translationY, 0f), - ValueAnimator.ofFloat(scale, 1f) - ) - } else { - animators = arrayOf( - ObjectAnimator.ofFloat(targetView, View.ROTATION, rotation, 0f), - ObjectAnimator.ofFloat(targetView, View.TRANSLATION_X, translationX, 0f), - ObjectAnimator.ofFloat(targetView, View.TRANSLATION_Y, translationY, 0f), - ObjectAnimator.ofFloat(targetView, View.SCALE_X, scale, 1f), - ObjectAnimator.ofFloat(targetView, View.SCALE_Y, scale, 1f) - ) - } - - AnimatorSet().apply { - playTogether(*animators) - duration = if (useAnimate) 260 else 0 - interpolator = DecelerateInterpolator() - addListener(onEnd = { - scale = 1f - translationX = 0f - translationY = 0f - rotation = 0f - lastScale = 1f - lastTranslationX = 0f - lastTranslationY = 0f - lastRotation = 0f - actionX = 0f - actionY = 0f - spacing = 0f - degree = 0f - doublePointer = 0 - mListener.onZoomUpdate(this@PlayerGestureDetector) - }) - start() - } - - isZoom = false - mListener.isZoomChanged(false) - } - - private var longPressed = false - private val longPressHandler: Handler = Handler(Looper.getMainLooper()) - private var longPressedRunnable = Runnable { - longPressed = true - mListener.onLongPress() - } - - fun onTouchEvent(event: MotionEvent): Boolean { - var handled = false - val x = event.x - val y = event.y - - when (event.actionMasked) { - MotionEvent.ACTION_DOWN -> { - if (event.pointerCount == 1) { - singleMove = 1 -// Log.e("TAG", "onTouchEvent: move down") - singleMoveDownX = x - singleMoveDownY = y - - longPressHandler.postDelayed( - longPressedRunnable, - ViewConfiguration.getLongPressTimeout().toLong() - ) - - handled = true - } - } - - MotionEvent.ACTION_POINTER_DOWN -> { - if (!longPressed && singleMove != 2 && event.pointerCount == 2) { -// Log.e("TAG", "onTouchEvent: scale down") - doublePointer = 1 - val centerX = getCenterX(event) - val centerY = getCenterY(event) - actionX = centerX - actionY = centerY - spacing = getSpacing(event) - degree = getDegree(event) - - handled = true - } - } - - MotionEvent.ACTION_MOVE -> { - if (longPressed) { // 长按后,即使手指移动,也不再响应其他事件 - doublePointer = 0 - singleMove = 0 - handled = true - } else if (singleMove > 0 && event.pointerCount == 1) { - doublePointer = 0 -// Log.e("TAG", "onTouchEvent: move move") - val deltaX = x - singleMoveDownX - val deltaY = y - singleMoveDownY - val absDeltaX = abs(deltaX) - val absDeltaY = abs(deltaY) - handled = if (singleMove == 2 || absDeltaX > 50 || absDeltaY > 50) { - longPressHandler.removeCallbacks(longPressedRunnable) // 取消长按监听 - singleMove = 2 - mListener.onSingleMoving(deltaX, deltaY, x, y) - } else false - } else if (doublePointer == 1 && event.pointerCount == 2) { - longPressHandler.removeCallbacks(longPressedRunnable) // 取消长按监听 - singleMove = 0 -// Log.e("TAG", "onTouchEvent: scale move") - val centerX = getCenterX(event) - val centerY = getCenterY(event) - translationX = lastTranslationX + centerX - actionX - translationY = lastTranslationY + centerY - actionY - - scale = lastScale * getSpacing(event) / spacing - rotation = lastRotation + getDegree(event) - degree - // 把rotation限制在[-359, 359] - rotation %= 360 - - if (!isZoom && - (translationX != 0f || translationY != 0f || scale != 1f || rotation != 0f) - ) { - isZoom = true - mListener.isZoomChanged(true) - } - - handled = mListener.onZoomUpdate(this) - } else { - doublePointer = 0 - } - } - - MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { - longPressHandler.removeCallbacks(longPressedRunnable) // 取消长按监听 - if (longPressed && event.pointerCount == 1) { - handled = mListener.onLongPressUp() - longPressed = false - } else if (singleMove == 2/* && event.pointerCount == 1*/) { - // 单指滑动了一段距离,第二个手指又落下滑动,然后两个手指抬起 - // 这时候应该只响应单指滑动,因为它先产生的。 - // 所以这时event.pointerCount可能不1,所以不能加&& event.pointerCount == 1 - val deltaX = x - singleMoveDownX - val deltaY = y - singleMoveDownY - handled = mListener.onSingleMoved(deltaX, deltaY, x, y) - } else if (doublePointer == 1 && event.pointerCount == 2) { - doublePointer = 0 - lastScale = scale - lastTranslationX = translationX - lastTranslationY = translationY - lastRotation = rotation - - handled = true - } - singleMove = 0 - doublePointer = 0 - } - } - return handled - } - - // 触碰两点间中心点X - private fun getCenterX(event: MotionEvent): Float { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && event.pointerCount > 1) - // 安卓10及以上才有public float getRawX(int pointerIndex)方法 - (event.getRawX(0) + event.getRawX(1)) / 2f - else event.rawX - } - - // 触碰两点间中心点Y - private fun getCenterY(event: MotionEvent): Float { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && event.pointerCount > 1) - // 安卓10及以上才有public float getRawX(int pointerIndex)方法 - (event.getRawY(0) + event.getRawY(1)) / 2f - else event.rawY - } - - // 触碰两点间距离 - private fun getSpacing(event: MotionEvent): Float { - if (event.pointerCount <= 1) return 0f - // 通过三角函数得到两点间的距离 - val x = event.getX(0) - event.getX(1) - val y = event.getY(0) - event.getY(1) - return sqrt(x * x + y * y.toDouble()).toFloat() - } - - // 取旋转角度 - private fun getDegree(event: MotionEvent): Float { - if (event.pointerCount <= 1) return 0f - // 得到两个手指间的旋转角度 - val deltaX = event.getX(0) - event.getX(1).toDouble() - val deltaY = event.getY(0) - event.getY(1).toDouble() - val radians = atan2(deltaY, deltaX) - return Math.toDegrees(radians).toFloat() - } - - interface PlayerGestureListener { - fun onZoomUpdate(detector: PlayerGestureDetector): Boolean = false - fun onSingleMoving(deltaX: Float, deltaY: Float, x: Float, y: Float): Boolean = false - fun onSingleMoved(deltaX: Float, deltaY: Float, x: Float, y: Float): Boolean = false - fun isZoomChanged(isZoom: Boolean) {} - fun onLongPressUp(): Boolean = false - fun onLongPress() {} - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/player/PlayerView.kt b/app/src/main/java/com/skyd/anivu/ui/player/PlayerView.kt deleted file mode 100644 index c7c1d48f..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/player/PlayerView.kt +++ /dev/null @@ -1,1725 +0,0 @@ -/* -* Copyright 2019 The Android Open Source Project -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -package com.skyd.anivu.ui.player - -import android.annotation.SuppressLint -import android.content.Context -import android.content.res.Resources -import android.graphics.BitmapFactory -import android.graphics.Matrix -import android.graphics.RectF -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable -import android.media.AudioManager -import android.opengl.GLSurfaceView -import android.os.Build -import android.os.Looper -import android.os.Vibrator -import android.os.VibratorManager -import android.util.AttributeSet -import android.view.GestureDetector -import android.view.GestureDetector.SimpleOnGestureListener -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.SurfaceView -import android.view.TextureView -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView -import android.widget.ImageView.ScaleType -import android.widget.TextView -import androidx.annotation.ColorInt -import androidx.annotation.DoNotInline -import androidx.annotation.IntDef -import androidx.annotation.RequiresApi -import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat -import androidx.media3.common.AdOverlayInfo -import androidx.media3.common.AdViewProvider -import androidx.media3.common.C -import androidx.media3.common.ErrorMessageProvider -import androidx.media3.common.PlaybackException -import androidx.media3.common.Player -import androidx.media3.common.Player.DiscontinuityReason -import androidx.media3.common.Player.PlayWhenReadyChangeReason -import androidx.media3.common.Timeline -import androidx.media3.common.Tracks -import androidx.media3.common.VideoSize -import androidx.media3.common.text.CueGroup -import androidx.media3.common.util.Assertions -import androidx.media3.common.util.RepeatModeUtil.RepeatToggleModes -import androidx.media3.common.util.UnstableApi -import androidx.media3.common.util.Util -import androidx.media3.ui.AspectRatioFrameLayout -import androidx.media3.ui.AspectRatioFrameLayout.AspectRatioListener -import androidx.media3.ui.AspectRatioFrameLayout.ResizeMode -import androidx.media3.ui.R -import androidx.media3.ui.SubtitleView -import com.skyd.anivu.ext.dataStore -import com.skyd.anivu.ext.dp -import com.skyd.anivu.ext.getOrDefault -import com.skyd.anivu.ext.getScreenBrightness -import com.skyd.anivu.ext.tickVibrate -import com.skyd.anivu.ext.tryActivity -import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference -import com.skyd.anivu.ui.player.PlayerGestureDetector.PlayerGestureListener -import com.skyd.anivu.ui.player.PlayerView.ArtworkDisplayMode -import com.skyd.anivu.ui.player.PlayerView.Companion.ARTWORK_DISPLAY_MODE_FIT -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min - -/** - * A high level view for [Player] media playbacks. It displays video, subtitles and album art - * during playback, and displays playback controls using a [PlayerControlView]. - * - * A PlayerView can be customized by setting attributes (or calling corresponding methods), or - * overriding drawables. - * - *

Attributes

- * - * The following attributes can be set on a PlayerView when used in a layout XML file: - * - * * **`artwork_display_mode`** - Whether artwork is used if available in audio streams - * and [ArtworkDisplayMode] how it is displayed. - * - * * Corresponding field: [artworkDisplayMode] - * * Default: [ARTWORK_DISPLAY_MODE_FIT] - * - * * **`default_artwork`** - Default artwork to use if no artwork available in audio - * streams. - * - * * Corresponding field: [defaultArtwork] - * * Default: `null` - * - * * **`use_controller`** - Whether the playback controls can be shown. - * - * * Corresponding field: [useController] - * * Default: `true` - * - * * **`hide_on_touch`** - Whether the playback controls are hidden by touch events. - * - * * Corresponding field: [controllerHideOnTouch] - * * Default: `true` - * - * * **`auto_show`** - Whether the playback controls are automatically shown when - * playback starts, pauses, ends, or fails. If set to false, the playback controls can be - * manually operated with [showController] and [hideController]. - * - * * Corresponding field: [controllerAutoShow] - * * Default: `true` - * - * * **`hide_during_ads`** - Whether the playback controls are hidden during ads. - * Controls are always shown during ads if they are enabled and the player is paused. - * - * * Corresponding field: [controllerHideDuringAds] - * * Default: `true` - * - * * **`show_buffering`** - Whether the buffering spinner is displayed when the player - * is buffering. Valid values are `never`, `when_playing` and `always`. - * - * * Corresponding field: [showBuffering] - * * Default: `never` - * - * * **`resize_mode`** - Controls how video and album art is resized within the view. - * Valid values are `fit`, `fixed_width`, `fixed_height`, `fill` and - * `zoom`. - * - * * Corresponding field: [resizeMode] - * * Default: `fit` - * - * * **`surface_type`** - The type of surface view used for video playbacks. Valid - * values are `surface_view`, `texture_view`, `spherical_gl_surface_view`, - * `video_decoder_gl_surface_view` and `none`. Using `none` is recommended - * for audio only applications, since creating the surface can be expensive. - * Using `surface_view` is recommended for video applications. - * Note, TextureView can only be used in a hardware accelerated window. - * When rendered in software, TextureView will draw nothing. - * - * * Corresponding method: None - * * Default: `surface_view` - * - * * **`shutter_background_color`** - The background color of the `exo_shutter` - * view. - * - * * Corresponding method: [setShutterBackgroundColor] - * * Default: `unset` - * - * * **`keep_content_on_player_reset`** - Whether the currently displayed video frame - * or media artwork is kept visible when the player is reset. - * - * * Corresponding method: [keepContentOnPlayerReset] - * * Default: `false` - * - * * All attributes that can be set on [PlayerControlView] and - * [androidx.media3.ui.DefaultTimeBar] can also be set on a PlayerView, and will be - * propagated to the inflated [PlayerControlView]. - * - *

Overriding drawables

- * - * The drawables used by [PlayerControlView] can be overridden by drawables with the same - * names defined in your application. See the [PlayerControlView] documentation for a list of - * drawables that can be overridden. - */ -@SuppressLint("UnsafeOptInUsageError", "PrivateResource") -class PlayerView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr), AdViewProvider { - /** - * Listener to be notified about changes of the visibility of the UI controls. - */ - interface ControllerVisibilityListener { - /** - * Called when the visibility changes. - * - * @param visibility The new visibility. Either [View.VISIBLE] or [View.GONE]. - */ - fun onVisibilityChanged(visibility: Int) - } - - /** - * Listener invoked when the fullscreen button is clicked. The implementation is responsible for - * changing the UI layout. - */ - interface FullscreenButtonClickListener { - /** - * Called when the fullscreen button is clicked. - * - * @param isFullScreen `true` if the video rendering surface should be fullscreen, - * `false` otherwise. - */ - fun onFullscreenButtonClick(isFullScreen: Boolean) - } - - /** - * Determines the artwork display mode. One of [ARTWORK_DISPLAY_MODE_OFF], - * [ARTWORK_DISPLAY_MODE_FIT] or [ARTWORK_DISPLAY_MODE_FILL]. - */ - @MustBeDocumented - @Retention(AnnotationRetention.SOURCE) - @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.TYPE_PARAMETER) - @IntDef(ARTWORK_DISPLAY_MODE_OFF, ARTWORK_DISPLAY_MODE_FIT, ARTWORK_DISPLAY_MODE_FILL) - annotation class ArtworkDisplayMode - - /** - * Determines when the buffering view is shown. One of [SHOW_BUFFERING_NEVER], - * [SHOW_BUFFERING_WHEN_PLAYING] or [SHOW_BUFFERING_ALWAYS]. - */ - @MustBeDocumented - @Retention(AnnotationRetention.SOURCE) - @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.TYPE_PARAMETER) - @IntDef(SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS) - annotation class ShowBuffering - - private val componentListener: ComponentListener = ComponentListener() - private val contentFrame: AspectRatioFrameLayout? - private var shutterView: View? = null - - /** - * Gets the view onto which video is rendered. This is a: - * - * [SurfaceView] by default, or if the `surface_type` attribute is set to `surface_view`. - * [TextureView] if `surface_type` is `texture_view`. - * `SphericalGLSurfaceView` if `surface_type` is `spherical_gl_surface_view`. - * `VideoDecoderGLSurfaceView` if `surface_type` is `video_decoder_gl_surface_view`. - * `null` if `surface_type` is `none`. - * - * @return The [SurfaceView], [TextureView], `SphericalGLSurfaceView`, `VideoDecoderGLSurfaceView` or `null`. - */ - private val videoSurfaceView: View? - private val surfaceViewIgnoresVideoAspectRatio: Boolean - private var artworkView: ImageView? = null - - /** - * Gets the [SubtitleView]. - * - * @return The [SubtitleView], or `null` if the layout has been customized and the - * subtitle view is not present. - */ - var subtitleView: SubtitleView? = null - private var bufferingView: View? = null - private var errorMessageView: TextView? = null - private val controller: PlayerControlView? - private var adOverlayFrameLayout: FrameLayout? = null - - /** - * Gets the overlay [FrameLayout], which can be populated with UI elements to show on top of - * the player. - * - * @return The overlay [FrameLayout], or `null` if the layout has been customized and - * the overlay is not present. - */ - private var overlayFrameLayout: FrameLayout? = null - - /** - * The player currently set on this view, or null if no player is set. - */ - var player: Player? = null - /** - * Sets the [Player] to use. - * - * To transition a [Player] from targeting one view to another, it's recommended to use - * [switchTargetView] rather than this method. If you do wish to use this method directly, - * be sure to attach the player to the new view *before* calling `player = null` to - * detach it from the old one. This ordering is significantly more efficient and may allow - * for more seamless transitions. - * - * @param value The [Player] to use, or `null` to detach the current player. Only - * players which are accessed on the main thread are supported - * (`value.applicationLooper == Looper.getMainLooper()`). - */ - set(value) { - Assertions.checkState(Looper.myLooper() == Looper.getMainLooper()) - Assertions.checkArgument( - value == null || value.applicationLooper == Looper.getMainLooper() - ) - if (field === value) { - return - } - field?.let { oldPlayer -> - oldPlayer.removeListener(componentListener) - if (oldPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) { - if (videoSurfaceView is TextureView) { - oldPlayer.clearVideoTextureView(videoSurfaceView as? TextureView) - } else if (videoSurfaceView is SurfaceView) { - oldPlayer.clearVideoSurfaceView(videoSurfaceView as? SurfaceView) - } - } - } - subtitleView?.setCues(null) - field = value - if (useController() && controller != null) { - controller.setPlayer(value) - } - updateBuffering() - updateErrorMessage() - updateForCurrentTrackSelections(true) - if (value != null) { - if (value.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) { - if (videoSurfaceView is TextureView) { - value.setVideoTextureView(videoSurfaceView as? TextureView) - } else if (videoSurfaceView is SurfaceView) { - value.setVideoSurfaceView(videoSurfaceView as? SurfaceView) - } - if (!value.isCommandAvailable(Player.COMMAND_GET_TRACKS) - || value.currentTracks.isTypeSupported(C.TRACK_TYPE_VIDEO) - ) { - // If the player already is or was playing a video, - // onVideoSizeChanged isn't called. - updateAspectRatio() - } - } - if (value.isCommandAvailable(Player.COMMAND_GET_TEXT)) { - subtitleView?.setCues(value.currentCues.cues) - } - value.addListener(componentListener) - maybeShowController(false) - } else { - hideController() - } - } - - private var useController: Boolean = true - /** - * Sets whether the playback controls can be shown. If set to `false` the playback controls - * are never visible and are disconnected from the player. - * - * This call will update whether the view is clickable. After the call, the view will be - * clickable if playback controls can be shown or if the view has a registered click listener. - */ - set(value) { - Assertions.checkState(!value || controller != null) - isClickable = value || hasOnClickListeners() - if (field == value) return - field = value - if (useController()) { - controller?.setPlayer(player) - } else { - controller?.hide() - controller?.setPlayer(null) - } - updateContentDescription() - } - - /** - * The listener to be notified about visibility changes, or null to remove the - * current listener. - */ - private var controllerVisibilityListener: ControllerVisibilityListener? = null - - /** - * The listener to be notified when the fullscreen button is clicked, or null to - * remove the current listener and hide the fullscreen button. - */ - var fullscreenButtonClickListener: FullscreenButtonClickListener? = null - private var artworkDisplayMode: @ArtworkDisplayMode Int = ARTWORK_DISPLAY_MODE_FIT - /** - * Sets whether and how artwork is displayed if present in the media. - */ - set(value) { - Assertions.checkState( - artworkDisplayMode == ARTWORK_DISPLAY_MODE_OFF || artworkView != null - ) - if (field != value) { - field = value - updateForCurrentTrackSelections(false) - } - } - - private var defaultArtwork: Drawable? = null - /** - * Sets the default artwork to display if `useArtwork` is `true` and no artwork is - * present in the media. - */ - set(value) { - if (field != value) { - field = value - updateForCurrentTrackSelections(false) - } - } - - /** - * Whether a buffering spinner is displayed when the player is in the buffering state. The - * buffering spinner is not displayed by default. - */ - private var showBuffering: @ShowBuffering Int = SHOW_BUFFERING_NEVER - /** - * @param value The mode that defines when the buffering spinner is displayed. One of - * [SHOW_BUFFERING_NEVER], [SHOW_BUFFERING_WHEN_PLAYING] and [SHOW_BUFFERING_ALWAYS]. - */ - set(value) { - if (field != value) { - field = value - updateBuffering() - } - } - - /** - * Whether the currently displayed video frame or media artwork is kept visible when the - * player is reset. A player reset is defined to mean the player being re-prepared with different - * media, the player transitioning to unprepared media or an empty list of media items, or the - * player being replaced or cleared by calling [player]. - * - * If enabled, the currently displayed video frame or media artwork will be kept visible until - * the player set on the view has been successfully prepared with new media and loaded enough of - * it to have determined the available tracks. Hence enabling this option allows transitioning - * from playing one piece of media to another, or from using one player instance to another, - * without clearing the view's content. - * - * If disabled, the currently displayed video frame or media artwork will be hidden as soon as - * the player is reset. Note that the video frame is hidden by making `exo_shutter` visible. - * Hence the video frame will not be hidden if using a custom layout that omits this view. - */ - private var keepContentOnPlayerReset = false - /** - * @param value Whether the currently displayed video frame or media - * artwork is kept visible when the player is reset. - */ - set(value) { - if (field != value) { - field = value - updateForCurrentTrackSelections(false) - } - } - - /** - * Sets the optional [ErrorMessageProvider]. - */ - private var errorMessageProvider: ErrorMessageProvider? = null - set(value) { - if (field != value) { - field = value - updateErrorMessage() - } - } - - /** - * Sets a custom error message to be displayed by the view. The error message will be displayed - * permanently, unless it is cleared by passing `null` to this method. - */ - private var customErrorMessage: CharSequence? = null - /** - * @param value The message to display, or `null` to clear a previously set message. - */ - set(value) { - Assertions.checkState(errorMessageView != null) - field = value - updateErrorMessage() - } - - /** - * The playback controls timeout. The playback controls are automatically hidden after this - * duration of time has elapsed without user input and with playback or buffering in progress. - * - * A non-positive value will cause the controller to remain visible indefinitely. - */ - private var controllerShowTimeoutMs: Int = 0 - set(value) { - field = value - if (controller?.isFullyVisible == true) { - // Update the controller's timeout if necessary. - showController() - } - } - - /** - * Whether the playback controls are automatically shown when playback starts, pauses, - * ends, or fails. If set to false, the playback controls can be manually operated with - * [showController] and [hideController]. - */ - private var controllerAutoShow: Boolean = true - - /** - * Whether the playback controls are hidden when ads are playing. Controls are always shown - * during ads if they are enabled and the player is paused. - */ - var controllerHideDuringAds: Boolean = true - - /** - * Whether the playback controls are hidden by touch events. - */ - private var controllerHideOnTouch: Boolean = true - set(value) { - Assertions.checkStateNotNull(controller) - field = value - updateContentDescription() - } - - private var textureViewRotation = 0 - - init { - if (isInEditMode) { - contentFrame = null - videoSurfaceView = null - surfaceViewIgnoresVideoAspectRatio = false - controller = null - val logo = ImageView(context) - configureEditModeLogo(context, resources, logo) - addView(logo) - } else { - var shutterColorSet = false - var shutterColor = 0 - var playerLayoutId = R.layout.exo_player_view - var useArtwork = true - var artworkDisplayMode = ARTWORK_DISPLAY_MODE_FIT - var defaultArtworkId = 0 - var useController = true - var surfaceType = SURFACE_TYPE_SURFACE_VIEW - var resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT - var controllerShowTimeoutMs = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS - var controllerHideOnTouch = true - var controllerAutoShow = true - var controllerHideDuringAds = true - var showBuffering = SHOW_BUFFERING_NEVER - - if (attrs != null) { - val a = context.theme.obtainStyledAttributes( - attrs, R.styleable.PlayerView, defStyleAttr, 0 - ) - @Suppress("KotlinConstantConditions") - try { - shutterColorSet = a.hasValue(R.styleable.PlayerView_shutter_background_color) - shutterColor = a.getColor( - R.styleable.PlayerView_shutter_background_color, shutterColor - ) - playerLayoutId = a.getResourceId( - R.styleable.PlayerView_player_layout_id, playerLayoutId - ) - useArtwork = a.getBoolean(R.styleable.PlayerView_use_artwork, useArtwork) - artworkDisplayMode = a.getInt( - R.styleable.PlayerView_artwork_display_mode, artworkDisplayMode - ) - defaultArtworkId = a.getResourceId( - R.styleable.PlayerView_default_artwork, defaultArtworkId - ) - useController = a.getBoolean( - R.styleable.PlayerView_use_controller, useController - ) - surfaceType = a.getInt(R.styleable.PlayerView_surface_type, surfaceType) - resizeMode = a.getInt(R.styleable.PlayerView_resize_mode, resizeMode) - controllerShowTimeoutMs = a.getInt( - R.styleable.PlayerView_show_timeout, controllerShowTimeoutMs - ) - controllerHideOnTouch = a.getBoolean( - R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch - ) - controllerAutoShow = a.getBoolean( - R.styleable.PlayerView_auto_show, controllerAutoShow - ) - showBuffering = a.getInteger( - R.styleable.PlayerView_show_buffering, showBuffering - ) - keepContentOnPlayerReset = a.getBoolean( - R.styleable.PlayerView_keep_content_on_player_reset, - keepContentOnPlayerReset - ) - controllerHideDuringAds = a.getBoolean( - R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds - ) - } finally { - a.recycle() - } - } - LayoutInflater.from(context).inflate(playerLayoutId, this) - setDescendantFocusability(FOCUS_AFTER_DESCENDANTS) - - // Content frame. - contentFrame = findViewById(R.id.exo_content_frame)?.apply { - setResizeModeRaw(this, resizeMode) - } - - // Shutter view. - shutterView = findViewById(R.id.exo_shutter).apply { - if (shutterColorSet) { - setBackgroundColor(shutterColor) - } - } - - // Create a surface view and insert it into the content frame, if there is one. - var surfaceViewIgnoresVideoAspectRatio = false - if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { - val params = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT - ) - when (surfaceType) { - SURFACE_TYPE_TEXTURE_VIEW -> videoSurfaceView = TextureView(context) - SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW -> { - try { - videoSurfaceView = Class.forName( - "androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView" - ).getConstructor(Context::class.java).newInstance(context) as View - } catch (e: Exception) { - throw IllegalStateException( - "spherical_gl_surface_view requires an ExoPlayer dependency", e - ) - } - surfaceViewIgnoresVideoAspectRatio = true - } - - SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW -> { - try { - videoSurfaceView = Class.forName( - "androidx.media3.exoplayer.video.VideoDecoderGLSurfaceView" - ).getConstructor(Context::class.java).newInstance(context) as View - } catch (e: Exception) { - throw IllegalStateException( - "video_decoder_gl_surface_view requires an ExoPlayer dependency", e - ) - } - } - - else -> { - val view = SurfaceView(context) - if (Util.SDK_INT >= 34) { - Api34.setSurfaceLifecycleToFollowsAttachment(view) - } - videoSurfaceView = view - } - } - videoSurfaceView.setLayoutParams(params) - // We don't want surfaceView to be clickable separately to the PlayerView itself, - // but we do want to register as an OnClickListener so that surfaceView - // implementations can propagate click events up to the PlayerView by calling - // their own performClick method. - videoSurfaceView.setOnClickListener(componentListener) - videoSurfaceView.isClickable = false - contentFrame.addView(videoSurfaceView, 0) - } else { - videoSurfaceView = null - } - this.surfaceViewIgnoresVideoAspectRatio = surfaceViewIgnoresVideoAspectRatio - - // Ad overlay frame layout. - adOverlayFrameLayout = findViewById(R.id.exo_ad_overlay) - - // Overlay frame layout. - overlayFrameLayout = findViewById(R.id.exo_overlay) - - // Artwork view. - artworkView = findViewById(R.id.exo_artwork) - val isArtworkEnabled = - useArtwork && artworkDisplayMode != ARTWORK_DISPLAY_MODE_OFF && artworkView != null - this.artworkDisplayMode = - if (isArtworkEnabled) artworkDisplayMode else ARTWORK_DISPLAY_MODE_OFF - if (defaultArtworkId != 0) { - defaultArtwork = ContextCompat.getDrawable(context, defaultArtworkId) - } - - // Subtitle view. - subtitleView = findViewById(R.id.exo_subtitles)?.apply { - setUserDefaultStyle() - setUserDefaultTextSize() - } - - // Buffering view. - bufferingView = findViewById(R.id.exo_buffering)?.apply { - visibility = GONE - } - this.showBuffering = showBuffering - - // Error message view. - errorMessageView = findViewById(R.id.exo_error_message)?.apply { - visibility = GONE - } - - // Playback control view. - val customController = findViewById(R.id.exo_controller) - val controllerPlaceholder = findViewById(R.id.exo_controller_placeholder) - if (customController != null) { - controller = customController - } else if (controllerPlaceholder != null) { - // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are - // transferred, but standard attributes (e.g. background) are not. - controller = PlayerControlView(context, null, 0, attrs).apply { - setId(R.id.exo_controller) - setLayoutParams(controllerPlaceholder.layoutParams) - } - val parent = controllerPlaceholder.parent as ViewGroup - val controllerIndex = parent.indexOfChild(controllerPlaceholder) - parent.removeView(controllerPlaceholder) - parent.addView(controller, controllerIndex) - } else { - controller = null - } - this.controllerShowTimeoutMs = if (controller != null) controllerShowTimeoutMs else 0 - this.controllerHideOnTouch = controllerHideOnTouch - this.controllerAutoShow = controllerAutoShow - this.controllerHideDuringAds = controllerHideDuringAds - this.useController = useController && controller != null - controller?.apply { - hideImmediately() - @Suppress("DEPRECATION") - controller.addVisibilityListener(componentListener) - onZoomStateChanged(false) - setOnResetZoomButtonClickListener { - playerGestureDetector.restoreZoom(contentFrame) - } - } - if (useController) { - isClickable = true - } - updateContentDescription() - } - } - - override fun setVisibility(visibility: Int) { - super.setVisibility(visibility) - // Work around https://github.com/google/ExoPlayer/issues/3160. - (videoSurfaceView as? SurfaceView)?.setVisibility(visibility) - } - - private var resizeMode: @ResizeMode Int - get() = contentFrame!!.resizeMode - set(resizeMode) { - contentFrame!!.resizeMode = resizeMode - } - - /** - * Sets the background color of the `exo_shutter` view. - * - * @param color The background color. - */ - private fun setShutterBackgroundColor(@ColorInt color: Int) { - shutterView?.setBackgroundColor(color) - } - - override fun dispatchKeyEvent(event: KeyEvent): Boolean { - if (player?.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) == true - && player?.isPlayingAd == true - ) { - return super.dispatchKeyEvent(event) - } - val isDpadKey = isDpadKey(event.keyCode) - var handled = false - if (isDpadKey && useController() && !controller!!.isFullyVisible) { - // Handle the key event by showing the controller. - maybeShowController(true) - handled = true - } else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) { - // The key event was handled as a media key or by the super class. We should also show the - // controller, or extend its show timeout if already visible. - maybeShowController(true) - handled = true - } else if (isDpadKey && useController()) { - // The key event wasn't handled, but we should extend the controller's show timeout. - maybeShowController(true) - } - return handled - } - - /** - * Called to process media key events. Any [KeyEvent] can be passed but only media key - * events will be handled. Does nothing if playback controls are disabled. - * - * @param event A key event. - * @return Whether the key event was handled. - */ - private fun dispatchMediaKeyEvent(event: KeyEvent?): Boolean { - return useController() && controller!!.dispatchMediaKeyEvent(event) - } - - /** - * Whether the controller is currently fully visible. - */ - val isControllerFullyVisible: Boolean - get() = controller?.isFullyVisible == true - - /** - * Shows the playback controls. Does nothing if playback controls are disabled. - * - * - * The playback controls are automatically hidden during playback after - * [controllerShowTimeoutMs]. They are shown indefinitely when playback has - * not started yet, is paused, has ended or failed. - */ - private fun showController() { - showController(shouldShowControllerIndefinitely()) - } - - /** - * Hides the playback controls. Does nothing if playback controls are disabled. - */ - fun hideController() { - controller?.hide() - } - - fun setOnBackButtonClickListener(listener: OnClickListener?) { - controller?.setOnBackButtonClickListener(listener) - } - - /** - * Sets whether the rewind button is shown. - * - * @param showRewindButton Whether the rewind button is shown. - */ - fun setShowRewindButton(showRewindButton: Boolean) { - controller?.setShowRewindButton(showRewindButton) - } - - /** - * Sets whether the fast forward button is shown. - * - * @param showFastForwardButton Whether the fast forward button is shown. - */ - fun setShowFastForwardButton(showFastForwardButton: Boolean) { - controller?.setShowFastForwardButton(showFastForwardButton) - } - - /** - * Sets whether the previous button is shown. - * - * @param showPreviousButton Whether the previous button is shown. - */ - fun setShowPreviousButton(showPreviousButton: Boolean) { - controller?.setShowPreviousButton(showPreviousButton) - } - - /** - * Sets whether the next button is shown. - * - * @param showNextButton Whether the next button is shown. - */ - fun setShowNextButton(showNextButton: Boolean) { - controller?.setShowNextButton(showNextButton) - } - - /** - * Sets which repeat toggle modes are enabled. - * - * @param repeatToggleModes A set of - * [androidx.media3.common.util.RepeatModeUtil.RepeatToggleModes]. - */ - fun setRepeatToggleModes(repeatToggleModes: @RepeatToggleModes Int) { - controller?.setRepeatToggleModes(repeatToggleModes) - } - - fun setForward85sButton(visible: Boolean) { - controller?.setForward85sButton(visible) - } - - fun setOnScreenshotListener(listener: OnClickListener?) { - controller?.setOnScreenshotListener(listener) - } - - /** - * Sets whether the shuffle button is shown. - * - * @param showShuffleButton Whether the shuffle button is shown. - */ - fun setShowShuffleButton(showShuffleButton: Boolean) { - controller?.setShowShuffleButton(showShuffleButton) - } - - /** - * Sets whether the subtitle button is shown. - * - * @param showSubtitleButton Whether the subtitle button is shown. - */ - fun setShowSubtitleButton(showSubtitleButton: Boolean) { - controller?.showSubtitleButton = showSubtitleButton - } - - /** - * Sets whether the vr button is shown. - * - * @param showVrButton Whether the vr button is shown. - */ - fun setShowVrButton(showVrButton: Boolean) { - controller?.showVrButton = showVrButton - } - - /** - * Sets whether a play button is shown if playback is [Player.getPlaybackSuppressionReason]. - * The default is `true`. - * - * @param showPlayButtonIfSuppressed Whether to show a play button if playback is - * [Player.getPlaybackSuppressionReason]. - */ - @UnstableApi - fun setShowPlayButtonIfPlaybackIsSuppressed(showPlayButtonIfSuppressed: Boolean) { - controller?.setShowPlayButtonIfPlaybackIsSuppressed(showPlayButtonIfSuppressed) - } - - /** - * Sets the millisecond positions of extra ad markers relative to the start of the window (or - * timeline, if in multi-window mode) and whether each extra ad has been played or not. The - * markers are shown in addition to any ad markers for ads in the player's timeline. - * - * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or - * `null` to show no extra ad markers. - * @param extraPlayedAdGroups Whether each ad has been played, or `null` to show no extra ad - * markers. - */ - fun setExtraAdGroupMarkers( - extraAdGroupTimesMs: LongArray?, - extraPlayedAdGroups: BooleanArray?, - ) { - controller?.setExtraAdGroupMarkers(extraAdGroupTimesMs, extraPlayedAdGroups) - } - - /** - * Sets the [AspectRatioFrameLayout.AspectRatioListener]. - * - * @param listener The listener to be notified about aspect ratios changes of the video - * content or the content frame. - */ - fun setAspectRatioListener(listener: AspectRatioListener?) { - contentFrame?.setAspectRatioListener(listener) - } - - // @Override - // public boolean performClick() { - // toggleControllerVisibility(); - // return super.performClick(); - // } - override fun onTrackballEvent(ev: MotionEvent): Boolean { - if (!useController() || player == null) { - return false - } - maybeShowController(true) - return true - } - - /** - * Should be called when the player is visible to the user, if the `surface_type` extends - * [GLSurfaceView]. It is the counterpart to [onPause]. - * - * This method should typically be called in `Activity.onStart()`, or `Activity.onResume()` - * for API versions <= 23. - */ - private fun onResume() { - if (videoSurfaceView is GLSurfaceView) { - videoSurfaceView.onResume() - } - } - - /** - * Should be called when the player is no longer visible to the user, if the `surface_type` - * extends [GLSurfaceView]. It is the counterpart to [onResume]. - * - * This method should typically be called in `Activity.onStop()`, or `Activity.onPause()` - * for API versions <= 23. - */ - private fun onPause() { - if (videoSurfaceView is GLSurfaceView) { - videoSurfaceView.onPause() - } - } - - /** - * Called when there's a change in the desired aspect ratio of the content frame. The default - * implementation sets the aspect ratio of the content frame to the specified value. - * - * @param contentFrame The content frame, or `null`. - * @param aspectRatio The aspect ratio to apply. - */ - private fun onContentAspectRatioChanged( - contentFrame: AspectRatioFrameLayout?, - aspectRatio: Float, - ) { - contentFrame?.setAspectRatio(aspectRatio) - } - - // AdsLoader.AdViewProvider implementation. - override fun getAdViewGroup(): ViewGroup { - return Assertions.checkStateNotNull( - adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback" - ) - } - - override fun getAdOverlayInfos(): List { - val overlayViews: MutableList = mutableListOf() - overlayFrameLayout?.let { overlayFrameLayout -> - overlayViews.add( - AdOverlayInfo.Builder(overlayFrameLayout, AdOverlayInfo.PURPOSE_NOT_VISIBLE) - .setDetailedReason("Transparent overlay does not impact viewability") - .build() - ) - } - if (controller != null) { - overlayViews.add( - AdOverlayInfo.Builder(controller, AdOverlayInfo.PURPOSE_CONTROLS).build() - ) - } - return overlayViews - } - - private fun useController(): Boolean { - if (useController) { - Assertions.checkStateNotNull(controller) - return true - } - return false - } - - private fun useArtwork(): Boolean { - if (artworkDisplayMode != ARTWORK_DISPLAY_MODE_OFF) { - Assertions.checkStateNotNull(artworkView) - return true - } - return false - } - - private fun toggleControllerVisibility() { - if (!useController() || player == null || controller == null) { - return - } - if (!controller.isFullyVisible) { - maybeShowController(true) - } else if (controllerHideOnTouch) { - controller.hide() - } - } - - /** - * Shows the playback controls, but only if forced or shown indefinitely. - */ - private fun maybeShowController(isForced: Boolean) { - if (isPlayingAd && controllerHideDuringAds || controller == null) { - return - } - if (useController()) { - val wasShowingIndefinitely = controller.isFullyVisible && controller.showTimeoutMs <= 0 - val shouldShowIndefinitely = shouldShowControllerIndefinitely() - if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { - showController(shouldShowIndefinitely) - } - } - } - - private fun shouldShowControllerIndefinitely(): Boolean { - player?.let { player -> - val playbackState = player.playbackState - return (controllerAutoShow - && (!player.isCommandAvailable(Player.COMMAND_GET_TIMELINE) - || !player.currentTimeline.isEmpty) - && (playbackState == Player.STATE_IDLE || - playbackState == Player.STATE_ENDED || - !Assertions.checkNotNull(player).playWhenReady)) - } ?: return true - } - - private fun showController(showIndefinitely: Boolean) { - if (!useController() || controller == null) { - return - } - controller.setShowTimeoutMs(if (showIndefinitely) 0 else controllerShowTimeoutMs) - controller.show() - } - - private val isPlayingAd: Boolean - get() = (player?.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) == true - && player?.isPlayingAd == true - && player?.playWhenReady == true) - - private fun updateForCurrentTrackSelections(isNewPlayer: Boolean) { - val player = player - if (player == null || !player.isCommandAvailable(Player.COMMAND_GET_TRACKS) - || player.currentTracks.isEmpty - ) { - if (!keepContentOnPlayerReset) { - hideArtwork() - closeShutter() - } - return - } - if (isNewPlayer && !keepContentOnPlayerReset) { - // Hide any video from the previous player. - closeShutter() - } - if (player.currentTracks.isTypeSelected(C.TRACK_TYPE_VIDEO)) { - // Video enabled, so artwork must be hidden. If the shutter is closed, it will be opened - // in onRenderedFirstFrame(). - hideArtwork() - return - } - - // Video disabled so the shutter must be closed. - closeShutter() - // Display artwork if enabled and available, else hide it. - if (useArtwork()) { - if (setArtworkFromMediaMetadata(player)) { - return - } - if (setDrawableArtwork(defaultArtwork)) { - return - } - } - // Artwork disabled or unavailable. - hideArtwork() - } - - private fun setArtworkFromMediaMetadata(player: Player): Boolean { - if (!player.isCommandAvailable(Player.COMMAND_GET_METADATA)) { - return false - } - val artworkData = player.mediaMetadata.artworkData ?: return false - val bitmap = BitmapFactory.decodeByteArray( - artworkData, /* offset= */ 0, artworkData.size - ) - return setDrawableArtwork(BitmapDrawable(resources, bitmap)) - } - - private fun setDrawableArtwork(drawable: Drawable?): Boolean { - if (drawable != null) { - val drawableWidth = drawable.intrinsicWidth - val drawableHeight = drawable.intrinsicHeight - if (drawableWidth > 0 && drawableHeight > 0) { - var artworkLayoutAspectRatio = drawableWidth.toFloat() / drawableHeight - var scaleStyle = ScaleType.FIT_XY - if (artworkDisplayMode == ARTWORK_DISPLAY_MODE_FILL) { - artworkLayoutAspectRatio = width.toFloat() / height - scaleStyle = ScaleType.CENTER_CROP - } - onContentAspectRatioChanged(contentFrame, artworkLayoutAspectRatio) - artworkView?.apply { - setScaleType(scaleStyle) - setImageDrawable(drawable) - setVisibility(VISIBLE) - } - return true - } - } - return false - } - - private fun hideArtwork() { - artworkView?.setImageResource(android.R.color.transparent) // Clears any bitmap reference. - artworkView?.setVisibility(INVISIBLE) - } - - private fun closeShutter() { - shutterView?.visibility = VISIBLE - } - - private fun updateBuffering() { - val showBufferingSpinner = player?.playbackState == Player.STATE_BUFFERING && - (showBuffering == SHOW_BUFFERING_ALWAYS || - showBuffering == SHOW_BUFFERING_WHEN_PLAYING && - player?.playWhenReady == true) - bufferingView?.visibility = if (showBufferingSpinner) VISIBLE else GONE - } - - private fun updateErrorMessage() { - errorMessageView?.apply { - if (customErrorMessage != null) { - text = customErrorMessage - visibility = VISIBLE - return - } - val error = player?.playerError - if (error != null) { - val errorMessage = errorMessageProvider?.getErrorMessage(error)?.second - if (errorMessage != null) { - text = errorMessage - visibility = VISIBLE - } - } else { - visibility = GONE - } - } - } - - private fun updateContentDescription() { - if (controller == null || !useController) { - setContentDescription(null) - } else if (controller.isFullyVisible) { - setContentDescription( - if (controllerHideOnTouch) resources.getString(R.string.exo_controls_hide) - else null - ) - } else { - setContentDescription(resources.getString(R.string.exo_controls_show)) - } - } - - private fun updateControllerVisibility() { - if (isPlayingAd && controllerHideDuringAds) { - hideController() - } else { - maybeShowController(false) - } - } - - private fun updateAspectRatio() { - val videoSize = player?.videoSize ?: VideoSize.UNKNOWN - val width = videoSize.width - val height = videoSize.height - val unappliedRotationDegrees = videoSize.unappliedRotationDegrees - var videoAspectRatio: Float = if (height == 0 || width == 0) 0f - else width * videoSize.pixelWidthHeightRatio / height - - if (videoSurfaceView is TextureView) { - // Try to apply rotation transformation when our surface is a TextureView. - if (videoAspectRatio > 0 - && (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) - ) { - // We will apply a rotation 90/270 degree to the output texture of the TextureView. - // In this case, the output video's width and height will be swapped. - videoAspectRatio = 1 / videoAspectRatio - } - if (textureViewRotation != 0) { - videoSurfaceView.removeOnLayoutChangeListener(componentListener) - } - textureViewRotation = unappliedRotationDegrees - if (textureViewRotation != 0) { - // The texture view's dimensions might be changed after layout step. - // So add an OnLayoutChangeListener to apply rotation after layout step. - videoSurfaceView.addOnLayoutChangeListener(componentListener) - } - applyTextureViewRotation(videoSurfaceView, textureViewRotation) - } - onContentAspectRatioChanged( - contentFrame, if (surfaceViewIgnoresVideoAspectRatio) 0f else videoAspectRatio - ) - } - - private fun isDpadKey(keyCode: Int): Boolean { - return keyCode == KeyEvent.KEYCODE_DPAD_UP || - keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT || - keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || - keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT || - keyCode == KeyEvent.KEYCODE_DPAD_DOWN || - keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT || - keyCode == KeyEvent.KEYCODE_DPAD_LEFT || - keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT || - keyCode == KeyEvent.KEYCODE_DPAD_CENTER - } - - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent): Boolean { - return gestureDetector.onTouchEvent(event) - || playerGestureDetector.onTouchEvent(event) - || super.onTouchEvent(event) - } - - // Implementing the deprecated PlayerControlView.VisibilityListener and - // PlayerControlView.OnFullScreenModeChangedListener for now. - @Suppress("deprecation") - private inner class ComponentListener : Player.Listener, OnLayoutChangeListener, - OnClickListener, PlayerControlView.VisibilityListener, - PlayerControlView.OnFullScreenModeChangedListener { - private val period: Timeline.Period = Timeline.Period() - private var lastPeriodUidWithTracks: Any? = null - - // Player.Listener implementation - override fun onCues(cueGroup: CueGroup) { - subtitleView?.setCues(cueGroup.cues) - } - - override fun onVideoSizeChanged(videoSize: VideoSize) { - if (videoSize == VideoSize.UNKNOWN || player?.playbackState == Player.STATE_IDLE) { - return - } - updateAspectRatio() - } - - override fun onRenderedFirstFrame() { - shutterView?.visibility = INVISIBLE - } - - override fun onTracksChanged(tracks: Tracks) { - // Suppress the update if transitioning to an unprepared period within the same window. This - // is necessary to avoid closing the shutter when such a transition occurs. See: - // https://github.com/google/ExoPlayer/issues/5507. - val player = Assertions.checkNotNull(player) - val timeline = if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) { - player.currentTimeline - } else { - Timeline.EMPTY - } - if (timeline.isEmpty) { - lastPeriodUidWithTracks = null - } else if (player.isCommandAvailable(Player.COMMAND_GET_TRACKS) - && !player.currentTracks.isEmpty - ) { - lastPeriodUidWithTracks = - timeline.getPeriod(player.currentPeriodIndex, period, /* setIds= */ true).uid - } else if (lastPeriodUidWithTracks != null) { - val lastPeriodIndexWithTracks = timeline.getIndexOfPeriod( - lastPeriodUidWithTracks!! - ) - if (lastPeriodIndexWithTracks != C.INDEX_UNSET) { - val lastWindowIndexWithTracks = - timeline.getPeriod(lastPeriodIndexWithTracks, period).windowIndex - if (player.currentMediaItemIndex == lastWindowIndexWithTracks) { - // We're in the same media item. Suppress the update. - return - } - } - lastPeriodUidWithTracks = null - } - updateForCurrentTrackSelections(false) - } - - override fun onPlaybackStateChanged(playbackState: @Player.State Int) { - updateBuffering() - updateErrorMessage() - updateControllerVisibility() - } - - override fun onPlayWhenReadyChanged( - playWhenReady: Boolean, - reason: @PlayWhenReadyChangeReason Int, - ) { - updateBuffering() - updateControllerVisibility() - } - - override fun onPositionDiscontinuity( - oldPosition: Player.PositionInfo, - newPosition: Player.PositionInfo, - reason: @DiscontinuityReason Int, - ) { - if (isPlayingAd && controllerHideDuringAds) { - hideController() - } - } - - // OnLayoutChangeListener implementation - override fun onLayoutChange( - view: View, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int, - ) { - applyTextureViewRotation(view as TextureView, textureViewRotation) - } - - // OnClickListener implementation - override fun onClick(view: View) { - toggleControllerVisibility() - } - - // PlayerControlView.VisibilityListener implementation - override fun onVisibilityChange(visibility: Int) { - updateContentDescription() - controllerVisibilityListener?.onVisibilityChanged(visibility) - } - - // PlayerControlView.OnFullScreenModeChangedListener implementation - override fun onFullScreenModeChanged(isFullScreen: Boolean) { - fullscreenButtonClickListener?.onFullscreenButtonClick(isFullScreen) - } - } - - - private val playerGestureDetector = PlayerGestureDetector(object : PlayerGestureListener { - // 全屏手动滑动下拉状态栏的起始偏移位置 - private val statusBarOffset = 50 - - // 全屏手动滑动右滑左侧的起始偏移位置 - private val horizontalOffset = 50 - - // 只有在第一次显示SeekPreview前会限制deltaX和deltaY的比例和大小等 - // 在本次Touch已经显示过SeekPreview后,不会再限制 - private var startMovingVideoPos: Long = -1 // 负数表示还没开始移动 - private var startMovingBrightnessPos = -1f // 负数表示还没开始移动 - private var startMovingVolumePos = -1f // 负数表示还没开始移动 - private fun isSeekMoving(deltaX: Float, deltaY: Float, x: Float): Boolean { - val absDeltaX = abs(deltaX) - val absDeltaY = abs(deltaY) - // 确保在屏幕水平边界不会触发 - return startMovingBrightnessPos < 0 && startMovingVolumePos < 0 && - (startMovingVideoPos >= 0 || absDeltaX > absDeltaY) && - x - deltaX > horizontalOffset.dp && - x - deltaX < width - horizontalOffset.dp - } - - private fun isBrightnessMoving( - deltaX: Float, - deltaY: Float, - x: Float, - y: Float, - ): Boolean { - val absDeltaX = abs(deltaX) - val absDeltaY = abs(deltaY) - // 确保在状态栏处不会触发 - return startMovingVideoPos < 0 && startMovingVolumePos < 0 && - (startMovingBrightnessPos >= 0 || - absDeltaX < absDeltaY && x - deltaX < width / 3.0f) && - y - deltaY > statusBarOffset.dp - } - - private fun isVolumeMoving(deltaX: Float, deltaY: Float, x: Float, y: Float): Boolean { - val absDeltaX = abs(deltaX) - val absDeltaY = abs(deltaY) - // 确保在状态栏处不会触发 - return startMovingVideoPos < 0 && startMovingBrightnessPos < 0 && - (startMovingVolumePos >= 0 || - absDeltaX < absDeltaY && x - deltaX > width * 2 / 3.0f) && - y - deltaY > statusBarOffset.dp - } - - override fun onSingleMoved(deltaX: Float, deltaY: Float, x: Float, y: Float): Boolean { - // 这里集中隐藏提示View,例如快进、亮度、声音等 - controller?.apply { - setSeekPreviewVisibility(GONE) - setBrightnessControlsVisibility(GONE) - setVolumeControlsVisibility(GONE) - } - - // seekTo - // 在记录过 seek起始点 或者符合约束时执行seek - if (isSeekMoving(deltaX, deltaY, x)) { - player?.apply { - seekTo(startMovingVideoPos + (deltaX.dp * 13).toLong()) - startMovingVideoPos = -1 - return true - } - } else if (isBrightnessMoving(deltaX, deltaY, x, y)) { - // Brightness - startMovingBrightnessPos = -1f - return true - } else if (isVolumeMoving(deltaX, deltaY, x, y)) { - // Volume - startMovingVolumePos = -1f - return true - } - return false - } - - override fun onSingleMoving(deltaX: Float, deltaY: Float, x: Float, y: Float): Boolean { - // seekTo - // 在记录过 seek起始点 或者符合约束时更新SeekPreview - if (isSeekMoving(deltaX, deltaY, x)) { - if (startMovingVideoPos < 0) { - startMovingVideoPos = player?.currentPosition ?: 0 - } - controller?.apply { - setSeekPreviewVisibility(VISIBLE) - return updateSeekPreview( - startMovingVideoPos + (deltaX.dp * 13).toLong() - ) - } - } else if (isBrightnessMoving(deltaX, deltaY, x, y)) { - // Brightness - val activity = context.tryActivity ?: return false - val layoutParams = activity.window.attributes - if (startMovingBrightnessPos < 0) { - // 设置亮度默认值 - if (layoutParams.screenBrightness <= 0.00f) { - val brightness = activity.getScreenBrightness() - if (brightness != null) { - layoutParams.screenBrightness = brightness / 255.0f - activity.window.setAttributes(layoutParams) - } - } - // 记录亮度起始值 - startMovingBrightnessPos = layoutParams.screenBrightness - } - controller?.let { controller -> - // 设置屏幕亮度值,使用起始值作为基准,根据手势移动的距离计算新的亮度值 - layoutParams.screenBrightness = - max(0.01f, min(1.0f, startMovingBrightnessPos - deltaY / height)) - activity.window.setAttributes(layoutParams) - controller.setBrightnessControlsVisibility(VISIBLE) - controller.updateBrightnessProgress((layoutParams.screenBrightness * 100).toInt()) - } - } else if (isVolumeMoving(deltaX, deltaY, x, y)) { - // Volume - val audioManager = - context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - if (startMovingVolumePos < 0) { - startMovingVolumePos = - audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() - } - controller?.let { controller -> - val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) - controller.setMaxVolume(maxVolume) - var minVolume = 0 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - minVolume = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC) - } - val desiredVolume = - (startMovingVolumePos - deltaY / height * 1.2f * (maxVolume - minVolume)).toInt() - audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, desiredVolume, 0) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - controller.setMinVolume(audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)) - } - controller.setVolumeControlsVisibility(VISIBLE) - controller.updateVolumeProgress(desiredVolume) - } - } - return false - } - - override fun onZoomUpdate(detector: PlayerGestureDetector): Boolean { - contentFrame?.apply { - rotation = detector.rotation - scaleX = detector.scale - scaleY = detector.scale - translationX = detector.translationX - translationY = detector.translationY - } - return true - } - - override fun isZoomChanged(isZoom: Boolean) { - controller?.onZoomStateChanged(isZoom) - } - - private var isLongPress = false - private var beforeLongPressSpeed = 1.0f - val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - (context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager) - .defaultVibrator - } else { - @Suppress("DEPRECATION") - context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator - } - - override fun onLongPress() { - player?.let { player -> - vibrator.tickVibrate(35) - isLongPress = true - beforeLongPressSpeed = player.playbackParameters.speed - player.setPlaybackSpeed(3.0f) - controller?.setLongPressPlaybackSpeedVisibility(VISIBLE) - } - } - - override fun onLongPressUp(): Boolean { - var handled = false - if (isLongPress) { - player?.let { player -> - isLongPress = false - player.setPlaybackSpeed(beforeLongPressSpeed) - controller?.setLongPressPlaybackSpeedVisibility(GONE) - handled = true - } - } - return handled - } - } - ) - private val gestureDetector = GestureDetector(context, object : SimpleOnGestureListener() { - private fun onDoubleTapPausePlay(): Boolean = controller?.playOrPause() == Unit - - private fun onDoubleTapBackwardForward(e: MotionEvent): Boolean { - player?.let { player -> - if (e.x < width / 2f) { - controller?.showBackwardRipple() - player.seekTo(player.currentPosition - 10000) // -10s. - } else { - controller?.showForwardRipple() - player.seekTo(player.currentPosition + 10000) // +10s. - } - return true - } - return false - } - - private fun onDoubleTapBackwardPausePlayForward(e: MotionEvent): Boolean { - val player = this@PlayerView.player - val controller = this@PlayerView.controller - if (player == null) { - return onDoubleTapPausePlay() - } - if (controller == null) { - return onDoubleTapBackwardForward(e) - } - if (e.x <= width * 0.25f) { - controller.showBackwardRipple() - player.seekTo(player.currentPosition - 10000) // -10s. - } else if (e.x >= width * 0.75f) { - controller.showForwardRipple() - player.seekTo(player.currentPosition + 10000) // +10s. - } else { - controller.playOrPause() - } - return true - } - - override fun onDoubleTap(e: MotionEvent): Boolean { - val doubleTapPreference = context.dataStore.getOrDefault(PlayerDoubleTapPreference) - return when (doubleTapPreference) { - PlayerDoubleTapPreference.BACKWARD_FORWARD -> onDoubleTapBackwardForward(e) - PlayerDoubleTapPreference.BACKWARD_PAUSE_PLAY_FORWARD -> { - onDoubleTapBackwardPausePlayForward(e) - } - - else -> onDoubleTapPausePlay() - } - } - - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - toggleControllerVisibility() - return true - } - }) - - @RequiresApi(34) - private object Api34 { - @DoNotInline - fun setSurfaceLifecycleToFollowsAttachment(surfaceView: SurfaceView) { - surfaceView.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT) - } - } - - companion object { - /** - * No artwork is shown. - */ - const val ARTWORK_DISPLAY_MODE_OFF = 0 - - /** - * The artwork is fit into the player view and centered creating a letterbox style. - */ - const val ARTWORK_DISPLAY_MODE_FIT = 1 - - /** - * The artwork covers the entire space of the player view. - * If the aspect ratio of the image is different than the player view some areas of - * the image are cropped. - */ - const val ARTWORK_DISPLAY_MODE_FILL = 2 - - /** - * The buffering view is never shown. - */ - const val SHOW_BUFFERING_NEVER = 0 - - /** - * The buffering view is shown when the player is in the [Player.STATE_BUFFERING] buffering - * state and [Player.getPlayWhenReady] playWhenReady is `true`. - */ - const val SHOW_BUFFERING_WHEN_PLAYING = 1 - - /** - * The buffering view is always shown when the player is in the - * [Player.STATE_BUFFERING] buffering state. - */ - const val SHOW_BUFFERING_ALWAYS = 2 - - private const val SURFACE_TYPE_NONE = 0 - private const val SURFACE_TYPE_SURFACE_VIEW = 1 - private const val SURFACE_TYPE_TEXTURE_VIEW = 2 - private const val SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW = 3 - private const val SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW = 4 - - /** - * Switches the view targeted by a given [Player]. - * - * @param player The player whose target view is being switched. - * @param oldPlayerView The old view to detach from the player. - * @param newPlayerView The new view to attach to the player. - */ - fun switchTargetView( - player: Player?, - oldPlayerView: PlayerView?, - newPlayerView: PlayerView?, - ) { - if (oldPlayerView == newPlayerView) { - return - } - // We attach the new view before detaching the old one because this ordering allows the player - // to swap directly from one surface to another, without transitioning through a state where no - // surface is attached. This is significantly more efficient and achieves a more seamless - // transition when using platform provided video decoders. - newPlayerView?.player = player - oldPlayerView?.player = null - } - - private fun configureEditModeLogo(context: Context, resources: Resources, logo: ImageView) { - logo.setImageDrawable( - ResourcesCompat.getDrawable(resources, R.drawable.exo_edit_mode_logo, context.theme) - ) - logo.setBackgroundColor( - resources.getColor(R.color.exo_edit_mode_background_color, null) - ) - } - - private fun setResizeModeRaw(aspectRatioFrame: AspectRatioFrameLayout, resizeMode: Int) { - aspectRatioFrame.resizeMode = resizeMode - } - - /** - * Applies a texture rotation to a [TextureView]. - */ - private fun applyTextureViewRotation(textureView: TextureView, textureViewRotation: Int) { - val transformMatrix = Matrix() - val textureViewWidth = textureView.width.toFloat() - val textureViewHeight = textureView.height.toFloat() - if (textureViewWidth != 0f && textureViewHeight != 0f && textureViewRotation != 0) { - val pivotX = textureViewWidth / 2 - val pivotY = textureViewHeight / 2 - transformMatrix.postRotate(textureViewRotation.toFloat(), pivotX, pivotY) - - // After rotation, scale the rotated texture to fit the TextureView size. - val originalTextureRect = RectF(0f, 0f, textureViewWidth, textureViewHeight) - val rotatedTextureRect = RectF() - transformMatrix.mapRect(rotatedTextureRect, originalTextureRect) - transformMatrix.postScale( - textureViewWidth / rotatedTextureRect.width(), - textureViewHeight / rotatedTextureRect.height(), - pivotX, - pivotY, - ) - } - textureView.setTransform(transformMatrix) - } - } -} diff --git a/app/src/main/java/com/skyd/anivu/ui/player/Torrent.kt b/app/src/main/java/com/skyd/anivu/ui/player/Torrent.kt deleted file mode 100644 index 83adbd1a..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/player/Torrent.kt +++ /dev/null @@ -1,331 +0,0 @@ -package com.skyd.anivu.ui.player - -import android.util.Log -import org.libtorrent4j.AlertListener -import org.libtorrent4j.Priority -import org.libtorrent4j.TorrentFlags -import org.libtorrent4j.TorrentHandle -import org.libtorrent4j.alerts.Alert -import org.libtorrent4j.alerts.AlertType -import org.libtorrent4j.alerts.PieceFinishedAlert -import java.io.File -import java.io.FileInputStream -import java.io.InputStream -import java.lang.ref.WeakReference - -class Torrent( - val torrentHandle: TorrentHandle, -) : AlertListener { - enum class State { - RETRIEVING_META, - STARTING, - STREAMING - } - - private var lastPieceIndex: Int = -1 - private var firstPieceIndex: Int = -1 - private var selectedFileIndex = -1 - - /** - * Get the index of the piece we're currently interested in - * - * @return Interested piece index - */ - private var interestedPieceIndex = 0 - private var hasPieces: BooleanArray? = null - private val torrentStreamReferences: MutableList> = - mutableListOf() - - /** - * Get current torrent state - * - * @return [State] - */ - var state = State.RETRIEVING_META - private set - - /** - * First the largest file in the download is selected as the file for playback - * - * After setting this priority, the first and last index of the pieces that make up this file are determined. - * And last: amount of pieces that are needed for playback are calculated (needed for playback means: make up 10 megabyte of the file) - */ - init { - if (selectedFileIndex == -1) { - setLargestFile() - } - startDownload() - } - - /** - * Reset piece priorities of selected file to normal - */ - private fun resetPriorities() { - val priorities = torrentHandle.piecePriorities() - for (i in priorities.indices) { - if (i in firstPieceIndex..lastPieceIndex) { - torrentHandle.piecePriority(i, Priority.IGNORE) - } else { - torrentHandle.piecePriority(i, Priority.IGNORE) - } - } - } - - val videoFile: File - get() = File( - torrentHandle.savePath(), - torrentHandle.torrentFile().files().filePath(selectedFileIndex) - ) - - val videoStream: InputStream - /** - * Get an InputStream for the video file. - * Read is be blocked until the requested piece(s) is downloaded. - * - * @return [InputStream] - */ - get() { - val inputStream = TorrentInputStream(this, FileInputStream(videoFile)) - torrentStreamReferences.add(WeakReference(inputStream)) - return inputStream - } - val saveLocation: File - /** - * Get the location of the file that is being downloaded - * - * @return [File] The file location - */ - get() = File(torrentHandle.savePath(), torrentHandle.getName()) - - fun resume() { - torrentHandle.resume() - } - - fun pause() { - torrentHandle.pause() - } - - /** - * Set the selected file index to the largest file in the torrent - */ - fun setLargestFile() { - setSelectedFileIndex(-1) - } - - /** - * Set the index of the file that should be downloaded - * If the given index is -1, then the largest file is chosen - * - * @param selectedFileIndex [Integer] Index of the file - */ - fun setSelectedFileIndex(selectedFileIndex: Int) { - var newSelectedFileIndex = selectedFileIndex - val torrentInfo = torrentHandle.torrentFile() - val fileStorage = torrentInfo.files() - if (newSelectedFileIndex == -1) { - var highestFileSize: Long = 0 - var selectedFile = -1 - for (i in 0 until fileStorage.numFiles()) { - val fileSize = fileStorage.fileSize(i) - if (highestFileSize < fileSize) { - highestFileSize = fileSize - torrentHandle.filePriority(selectedFile, Priority.IGNORE) - selectedFile = i - torrentHandle.filePriority(i, Priority.DEFAULT) - } else { - torrentHandle.filePriority(i, Priority.IGNORE) - } - } - newSelectedFileIndex = selectedFile - } else { - for (i in 0 until fileStorage.numFiles()) { - if (i == newSelectedFileIndex) { - torrentHandle.filePriority(i, Priority.DEFAULT) - } else { - torrentHandle.filePriority(i, Priority.IGNORE) - } - } - } - this.selectedFileIndex = newSelectedFileIndex - val piecePriorities = torrentHandle.piecePriorities() - var firstPieceIndexLocal = -1 - var lastPieceIndexLocal = -1 - for (i in piecePriorities.indices) { - if (piecePriorities[i] != Priority.IGNORE) { - if (firstPieceIndexLocal == -1) { - firstPieceIndexLocal = i - } - piecePriorities[i] = Priority.IGNORE - } else { - if (firstPieceIndexLocal != -1 && lastPieceIndexLocal == -1) { - lastPieceIndexLocal = i - 1 - } - } - } - if (firstPieceIndexLocal == -1) { - firstPieceIndexLocal = 0 - } - if (lastPieceIndexLocal == -1) { - lastPieceIndexLocal = piecePriorities.size - 1 - } - firstPieceIndex = firstPieceIndexLocal - interestedPieceIndex = firstPieceIndex - lastPieceIndex = lastPieceIndexLocal - } - - val fileNames: Array - /** - * Get the filenames of the files in the torrent - * - * @return [String[]] - */ - get() { - val fileStorage = torrentHandle.torrentFile().files() - val fileNames = arrayOfNulls(fileStorage.numFiles()) - for (i in 0 until fileStorage.numFiles()) { - fileNames[i] = fileStorage.fileName(i) - } - return fileNames - } - - /** - * Prepare torrent for playback. - */ - fun startDownload() { - if (state == State.STREAMING || state == State.STARTING) return - state = State.STARTING - val priorities = torrentHandle.piecePriorities() - for (i in priorities.indices) { - if (priorities[i] != Priority.IGNORE) { - torrentHandle.piecePriority(i, Priority.DEFAULT) - } - } - hasPieces = BooleanArray(lastPieceIndex - firstPieceIndex + 1) { false } - torrentStreamReferences.clear() - torrentHandle.resume() - } - - /** - * Check if the piece that contains the specified bytes were downloaded already - * - * @param bytes The bytes you're interested in - * @return `true` if downloaded, `false` if not - */ - fun hasBytes(bytes: Long): Boolean { - if (hasPieces == null) { - return false - } - val pieceIndex = (bytes / torrentHandle.torrentFile().pieceLength()).toInt() - return hasPieces!![pieceIndex] - } - - /** - * Set the bytes of the selected file that you're interested in - * The piece of that specific offset is selected and that piece plus the 1 preceding and the 3 after it. - * These pieces will then be prioritised, which results in continuing the sequential download after that piece - * - * @param bytes The bytes you're interested in - */ - fun setInterestedBytes(bytes: Long) { - val hasPieces = this.hasPieces ?: return - val pieceIndex = (bytes / torrentHandle.torrentFile().pieceLength()).toInt() - interestedPieceIndex = pieceIndex - if (!hasPieces[pieceIndex] && - torrentHandle.piecePriority(pieceIndex + firstPieceIndex) != Priority.TOP_PRIORITY - ) { - val pieces = 5 - for (i in hasPieces.indices) { - if (!hasPieces[i]) { - if (i < pieceIndex) { - torrentHandle.piecePriority(i + firstPieceIndex, Priority.IGNORE) - } else if (i > pieceIndex + pieces) { - torrentHandle.piecePriority(i + firstPieceIndex, Priority.IGNORE) - } else { - // Set full priority to first found piece that is not confirmed finished - torrentHandle.piecePriority(i + firstPieceIndex, Priority.TOP_PRIORITY) - torrentHandle.setPieceDeadline(i + firstPieceIndex, 1000) - } - } - } - } - } - /** - * Checks if the interesting pieces are downloaded already - * - * @return `true` if the 5 pieces that were selected using `setInterestedBytes` are all reported complete including the `nextPieces`, `false` if not - */ - /** - * Checks if the interesting pieces are downloaded already - * - * @return `true` if the 5 pieces that were selected using `setInterestedBytes` are all reported complete, `false` if not - */ - @JvmOverloads - fun hasInterestedBytes(nextPieces: Int = 5): Boolean { - for (i in 0 until 5 + nextPieces) { - val index = interestedPieceIndex + i - if (hasPieces!!.size <= index || index < 0) { - continue - } - if (!hasPieces!![interestedPieceIndex + i]) { - return false - } - } - return true - } - - /** - * Start sequential mode downloading - */ - private fun startSequentialMode() { - resetPriorities() - if (hasPieces == null) { - torrentHandle.flags = torrentHandle.flags.and_(TorrentFlags.SEQUENTIAL_DOWNLOAD) - } - } - - private fun pieceFinished(alert: PieceFinishedAlert) { - if (state != State.STREAMING) { - startSequentialMode() - state = State.STREAMING - } - if (state == State.STREAMING && hasPieces != null) { - val pieceIndex = alert.pieceIndex() - firstPieceIndex - Log.e("TAG", "pieceFinished: $pieceIndex") - hasPieces?.set(pieceIndex, true) - if (pieceIndex >= interestedPieceIndex && hasInterestedBytes()) { - for (i in pieceIndex until hasPieces!!.size) { - // Set full priority to first found piece that is not confirmed finished - if (!hasPieces!![i]) { - torrentHandle.piecePriority(i + firstPieceIndex, Priority.TOP_PRIORITY) - torrentHandle.setPieceDeadline(i + firstPieceIndex, 1000) - break - } - } - } - } - } - - override fun types() = intArrayOf( - AlertType.PIECE_FINISHED.swig(), - ) - - override fun alert(alert: Alert<*>) { - when (alert) { - is PieceFinishedAlert -> pieceFinished(alert) - } - val i = torrentStreamReferences.iterator() - while (i.hasNext()) { - val reference = i.next() - val inputStream = reference.get() - if (inputStream == null) { - i.remove() - continue - } - inputStream.alert(alert) - } - } - - companion object { - private const val SEQUENTIAL_CONCURRENT_PIECES_COUNT = 5 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/player/TorrentDataSource.kt b/app/src/main/java/com/skyd/anivu/ui/player/TorrentDataSource.kt deleted file mode 100644 index cfc9e3b8..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/player/TorrentDataSource.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.skyd.anivu.ui.player - -import android.annotation.SuppressLint -import android.net.Uri -import android.util.Log -import androidx.media3.datasource.BaseDataSource -import androidx.media3.datasource.DataSpec -import com.skyd.anivu.BuildConfig -import com.skyd.anivu.appContext -import com.skyd.anivu.config.Const -import com.skyd.anivu.ext.getAppName -import com.skyd.anivu.ext.getAppVersionName -import com.skyd.anivu.model.worker.download.initProxySettings -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.suspendCancellableCoroutine -import org.libtorrent4j.AlertListener -import org.libtorrent4j.SessionManager -import org.libtorrent4j.SessionParams -import org.libtorrent4j.TorrentFlags -import org.libtorrent4j.TorrentHandle -import org.libtorrent4j.alerts.AddTorrentAlert -import org.libtorrent4j.alerts.Alert -import org.libtorrent4j.alerts.AlertType -import org.libtorrent4j.alerts.FileErrorAlert -import org.libtorrent4j.alerts.LogAlert -import org.libtorrent4j.alerts.MetadataReceivedAlert -import org.libtorrent4j.alerts.TorrentCheckedAlert -import org.libtorrent4j.alerts.TorrentErrorAlert -import org.libtorrent4j.swig.settings_pack -import java.io.File -import java.io.FileInputStream -import java.util.concurrent.CountDownLatch -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -@SuppressLint("UnsafeOptInUsageError") -class TorrentDataSource( - private val fileIndex: Int = 0, -) : BaseDataSource(true) { - private lateinit var dataSpec: DataSpec - private var closed = false - private lateinit var sessionManager: SessionManager - private var torrentHandle: TorrentHandle? = null - private var file: File? = null - private var inputStream: TorrentInputStream? = null - private var fileSize = 0L - - override fun open(dataSpec: DataSpec): Long { - this.dataSpec = dataSpec - sessionManager = SessionManager(BuildConfig.DEBUG) - val sessionParams = SessionParams() - - sessionParams.settings = initProxySettings( - context = appContext, - settings = sessionParams.settings, - ).setString( - settings_pack.string_types.user_agent.swigValue(), - "${appContext.getAppName() ?: "AniVu"}/${appContext.getAppVersionName()}" - ) - - sessionManager.start(sessionParams) - sessionManager.startDht() - - val countDownLatch = CountDownLatch(3) - runBlocking { - suspendCancellableCoroutine { - sessionManager.addListener(object : AlertListener { - override fun types() = intArrayOf( - AlertType.LOG.swig(), - AlertType.ADD_TORRENT.swig(), - AlertType.TORRENT_CHECKED.swig(), - AlertType.ADD_TORRENT.swig(), - AlertType.METADATA_RECEIVED.swig(), - AlertType.TORRENT_ERROR.swig(), - AlertType.FILE_ERROR.swig(), - ) - - private fun countDownAndTryResume() { - countDownLatch.countDown() - if (countDownLatch.count == 0L) { - it.resume(Unit) - } - } - - override fun alert(alert: Alert<*>?) { - when (alert) { - is LogAlert -> { - Log.e("TAG", "alert: ${alert.logMessage()}") - } - - is AddTorrentAlert -> { - if (torrentHandle == null) { - torrentHandle = sessionManager.find(alert.handle().infoHash()) - countDownAndTryResume() - } - } - - is TorrentCheckedAlert -> { - if (file == null) { - file = File( - alert.handle().torrentFile().files() - .filePath(0, Const.DOWNLOADING_VIDEO_DIR.path) - ).apply { - if (!exists()) createNewFile() - } - countDownAndTryResume() - } - } - - is MetadataReceivedAlert -> { - if (fileSize == 0L) { - val files = alert.handle().torrentFile().files() - fileSize = files.fileSize(fileIndex) - countDownAndTryResume() - } - } - - is TorrentErrorAlert -> { - it.resumeWithException(RuntimeException(alert.message())) - } - // If the storage fails to read or write files that it needs access to, - // this alert is generated and the torrent is paused. - is FileErrorAlert -> { - it.resumeWithException(RuntimeException(alert.message())) - } - } - } - }) - - sessionManager.download( - dataSpec.uri.toString(), - Const.DOWNLOADING_VIDEO_DIR, - TorrentFlags.SEQUENTIAL_DOWNLOAD, - ) - } - } - - inputStream = TorrentInputStream( - Torrent(torrentHandle!!).apply { sessionManager.addListener(this) }, - FileInputStream(file!!) - ).apply { - sessionManager.addListener(this) - } - return fileSize - } - - override fun read(buffer: ByteArray, offset: Int, length: Int): Int { - val inputStream = inputStream - if (length == 0 || inputStream == null) { - return 0 - } - return inputStream.read(buffer, offset, length) - } - - override fun close() { - if (closed) return - closed = true - inputStream?.let { - Log.e("TAG", "close: ") - sessionManager.removeListener(it) - sessionManager.removeListener(it.torrent) - } - if (torrentHandle?.isValid == true) { - torrentHandle?.pause() - } - sessionManager.pause() - sessionManager.stopDht() - sessionManager.stop() - inputStream?.close() - } - - override fun getUri(): Uri = dataSpec.uri -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/player/TorrentInputStream.kt b/app/src/main/java/com/skyd/anivu/ui/player/TorrentInputStream.kt deleted file mode 100644 index 7c8c43cb..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/player/TorrentInputStream.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.skyd.anivu.ui.player - -import android.util.Log -import org.libtorrent4j.AlertListener -import org.libtorrent4j.alerts.Alert -import org.libtorrent4j.alerts.AlertType -import org.libtorrent4j.alerts.PieceFinishedAlert -import java.io.FilterInputStream -import java.io.IOException -import java.io.InputStream - -internal class TorrentInputStream(val torrent: Torrent, inputStream: InputStream) : - FilterInputStream(inputStream), AlertListener { - private var stopped = false - private var location: Long = 0 - - @Synchronized - private fun waitForPiece(offset: Long): Boolean { - while (!Thread.currentThread().isInterrupted && !stopped) { - try { - if (torrent.hasBytes(offset)) { - return true - } - torrent.setInterestedBytes(offset) - (this as Object).wait() - } catch (ex: InterruptedException) { - Thread.currentThread().interrupt() - } - } - return false - } - - @Synchronized - @Throws(IOException::class) - override fun read(): Int { - if (!waitForPiece(location)) { - return -1 - } - location++ - return super.read() - } - - @Synchronized - @Throws(IOException::class) - override fun read(buffer: ByteArray, offset: Int, length: Int): Int { - val pieceLength: Int = torrent.torrentHandle.torrentFile().pieceLength() - var i = 0 - Log.e("TAG", "wantread: $location") - while (i < length) { - if (!waitForPiece(location + i)) { - return -1 - } - i += pieceLength - } - location += length.toLong() - Log.e("TAG", "read: $location") - return super.read(buffer, offset, length) - } - - @Throws(IOException::class) - override fun close() { - synchronized(this) { - stopped = true - (this as Object).notifyAll() - } - super.close() - } - - @Synchronized - @Throws(IOException::class) - override fun skip(n: Long): Long { - location += n - return super.skip(n) - } - - override fun markSupported() = false - - @Synchronized - private fun pieceFinished() { - (this as Object).notifyAll() - } - - override fun types() = intArrayOf( - AlertType.PIECE_FINISHED.swig() - ) - - override fun alert(alert: Alert<*>) { - when (alert) { - is PieceFinishedAlert -> pieceFinished() - else -> {} - } - } -} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_back_ios_new_24.xml b/app/src/main/res/drawable/ic_arrow_back_ios_new_24.xml deleted file mode 100644 index 46bf1df2..00000000 --- a/app/src/main/res/drawable/ic_arrow_back_ios_new_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_brightness_high_24.xml b/app/src/main/res/drawable/ic_brightness_high_24.xml deleted file mode 100644 index ac65b71e..00000000 --- a/app/src/main/res/drawable/ic_brightness_high_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_brightness_low_24.xml b/app/src/main/res/drawable/ic_brightness_low_24.xml deleted file mode 100644 index 90cccb36..00000000 --- a/app/src/main/res/drawable/ic_brightness_low_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_brightness_medium_24.xml b/app/src/main/res/drawable/ic_brightness_medium_24.xml deleted file mode 100644 index 159c48d0..00000000 --- a/app/src/main/res/drawable/ic_brightness_medium_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_fast_rewind_24.xml b/app/src/main/res/drawable/ic_fast_rewind_24.xml deleted file mode 100644 index 00245704..00000000 --- a/app/src/main/res/drawable/ic_fast_rewind_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_pause_fill_24.xml b/app/src/main/res/drawable/ic_pause_fill_24.xml deleted file mode 100644 index 74746aac..00000000 --- a/app/src/main/res/drawable/ic_pause_fill_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_photo_camera_24.xml b/app/src/main/res/drawable/ic_photo_camera_24.xml deleted file mode 100644 index 8cc2ebc1..00000000 --- a/app/src/main/res/drawable/ic_photo_camera_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_play_arrow_fill_24.xml b/app/src/main/res/drawable/ic_play_arrow_fill_24.xml deleted file mode 100644 index 3d65eb5d..00000000 --- a/app/src/main/res/drawable/ic_play_arrow_fill_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_settings_fill_24.xml b/app/src/main/res/drawable/ic_settings_fill_24.xml deleted file mode 100644 index 4e1cb7be..00000000 --- a/app/src/main/res/drawable/ic_settings_fill_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_skip_next_fill_24.xml b/app/src/main/res/drawable/ic_skip_next_fill_24.xml deleted file mode 100644 index f21013f5..00000000 --- a/app/src/main/res/drawable/ic_skip_next_fill_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_volume_down_24.xml b/app/src/main/res/drawable/ic_volume_down_24.xml deleted file mode 100644 index 00cb3313..00000000 --- a/app/src/main/res/drawable/ic_volume_down_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_volume_mute_24.xml b/app/src/main/res/drawable/ic_volume_mute_24.xml deleted file mode 100644 index efe49cb0..00000000 --- a/app/src/main/res/drawable/ic_volume_mute_24.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_volume_up_24.xml b/app/src/main/res/drawable/ic_volume_up_24.xml deleted file mode 100644 index 0893efa4..00000000 --- a/app/src/main/res/drawable/ic_volume_up_24.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/shape_fill_circle_corner_6.xml b/app/src/main/res/drawable/shape_fill_circle_corner_6.xml deleted file mode 100644 index 28f3421f..00000000 --- a/app/src/main/res/drawable/shape_fill_circle_corner_6.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_gray_transparent_angle_270.xml b/app/src/main/res/drawable/shape_gray_transparent_angle_270.xml deleted file mode 100644 index c3617f6c..00000000 --- a/app/src/main/res/drawable/shape_gray_transparent_angle_270.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_gray_transparent_angle_90.xml b/app/src/main/res/drawable/shape_gray_transparent_angle_90.xml deleted file mode 100644 index 410fc14b..00000000 --- a/app/src/main/res/drawable/shape_gray_transparent_angle_90.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_play.xml b/app/src/main/res/layout/activity_play.xml deleted file mode 100644 index 917883d1..00000000 --- a/app/src/main/res/layout/activity_play.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/exo_player_view.xml b/app/src/main/res/layout/exo_player_view.xml deleted file mode 100644 index e80ca399..00000000 --- a/app/src/main/res/layout/exo_player_view.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/exo_styled_sub_settings_list_item.xml b/app/src/main/res/layout/exo_styled_sub_settings_list_item.xml deleted file mode 100644 index 5848fda7..00000000 --- a/app/src/main/res/layout/exo_styled_sub_settings_list_item.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_enclosure_1.xml b/app/src/main/res/layout/item_enclosure_1.xml index 194ec2ef..c1b4b48f 100644 --- a/app/src/main/res/layout/item_enclosure_1.xml +++ b/app/src/main/res/layout/item_enclosure_1.xml @@ -16,7 +16,7 @@ android:layout_marginEnd="16dp" android:ellipsize="end" android:maxLines="2" - app:layout_constraintEnd_toStartOf="@+id/btn_enclosure_1_play" + app:layout_constraintEnd_toStartOf="@+id/btn_enclosure_1_download" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="magnet:?xt=urn:btih:7XVM7JHRJSFPEEF3GWH3V3JQLSKVPFPEEF3GWH3V3JQLSKVPFPEEF3GWH3V3JQLSKVPFPEEF3GWH3V3JQLSKVP" /> @@ -40,22 +40,12 @@ android:layout_marginHorizontal="16dp" android:layout_marginTop="6dp" android:maxLines="1" - app:layout_constraintEnd_toStartOf="@+id/btn_enclosure_1_play" + app:layout_constraintEnd_toStartOf="@+id/btn_enclosure_1_download" app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toEndOf="@id/tv_enclosure_1_length" app:layout_constraintTop_toBottomOf="@id/tv_enclosure_1_url" tools:text="123" /> - - diff --git a/app/src/main/res/layout/player_control_view.xml b/app/src/main/res/layout/player_control_view.xml deleted file mode 100644 index 8ab75ad5..00000000 --- a/app/src/main/res/layout/player_control_view.xml +++ /dev/null @@ -1,312 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index d5636221..c3bdf78e 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -170,4 +170,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 460226b6..33e0e850 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -12,7 +12,6 @@ 确定 信息 请稍后… - 格式非法 关闭 关闭 删除 @@ -199,6 +198,11 @@ 显示 选中时显示 导航栏标签 + 暂停 + 播放 + 高级 + 高级播放器设置 + 编辑 mpv.conf 每 %d 分钟 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index fef1974c..afa505ee 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -12,7 +12,6 @@ 確定 資訊 請稍後… - 格式非法 關閉 關閉 刪除 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index d54b5767..026035b9 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -2,19 +2,6 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 41f6c617..4df05bcf 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -2,13 +2,4 @@ 16dp 16dp 16dp - - 55dp - 60dp - 60dp - 50dp - 100dp - 40dp - 25dp - \ No newline at end of file diff --git a/app/src/main/res/values/drawables.xml b/app/src/main/res/values/drawables.xml deleted file mode 100644 index 1e0578c5..00000000 --- a/app/src/main/res/values/drawables.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - @drawable/ic_play_arrow_fill_24 - @drawable/ic_pause_fill_24 - @drawable/ic_skip_next_fill_24 - @drawable/ic_settings_fill_24 - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 15e8e2c9..00d06b71 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,7 +12,6 @@ OK Info Waiting… - Illegal format Close Exit Remove @@ -207,6 +206,11 @@ Show Show on active Navi bar label + Pause + Play + Advanced + Advanced config + Edit mpv.conf Every %d minute Every %d minutes diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 57cf4790..2177b4db 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -12,114 +12,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/themes_dynamic.xml b/app/src/main/res/values/themes_dynamic.xml index b3b08fdf..2412b300 100644 --- a/app/src/main/res/values/themes_dynamic.xml +++ b/app/src/main/res/values/themes_dynamic.xml @@ -7,7 +7,6 @@ shortEdges - true true true false diff --git a/app/src/main/res/values/themes_pink.xml b/app/src/main/res/values/themes_pink.xml index 965edf34..93f1006c 100644 --- a/app/src/main/res/values/themes_pink.xml +++ b/app/src/main/res/values/themes_pink.xml @@ -7,7 +7,6 @@ shortEdges - true true true diff --git a/build.gradle.kts b/build.gradle.kts index 3ddf0924..4bc53985 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.3.1" apply false + id("com.android.application") version "8.4.0" apply false id("org.jetbrains.kotlin.android") version "1.9.23" apply false id("com.google.dagger.hilt.android") version "2.51" apply false id("com.google.devtools.ksp") version "1.9.23-1.0.19" apply false