feat: new video player

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-06-27 22:56:34 +02:00
parent bf2d1520a6
commit f0d27b9a80
9 changed files with 418 additions and 2 deletions

View File

@ -118,6 +118,11 @@
android:configChanges="orientation|screenSize"
android:theme="@style/Theme.Revolt" />
<activity
android:name=".activities.media.VideoViewActivity2"
android:configChanges="orientation|screenSize"
android:theme="@style/Theme.Revolt" />
<!-- Backport photo picker via Google Play Services -->
<service
android:name="com.google.android.gms.metadata.ModuleDependencies"

View File

@ -0,0 +1,220 @@
package chat.revolt.activities.media
import android.content.ContentValues
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import chat.revolt.R
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltHttp
import chat.revolt.api.schemas.AutumnResource
import chat.revolt.databinding.ActivityVideoplayerBinding
import chat.revolt.provider.getAttachmentContentUri
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import io.ktor.client.request.get
import io.ktor.client.statement.readBytes
import kotlinx.coroutines.launch
class VideoViewActivity2 : FragmentActivity() {
private lateinit var binding: ActivityVideoplayerBinding
private var player: ExoPlayer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val autumnResource = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("autumnResource", AutumnResource::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra("autumnResource")
}
if (autumnResource?.id == null) {
Log.e("VideoViewActivity", "No AutumnResource provided")
finish()
return
}
val resourceUrl =
"$REVOLT_FILES/attachments/${autumnResource.id}/${autumnResource.filename}"
WindowCompat.setDecorFitsSystemWindows(window, false)
binding = ActivityVideoplayerBinding.inflate(layoutInflater)
binding.tbTop.title = autumnResource.filename
binding.tbTop.setNavigationOnClickListener { finish() }
binding.tbTop.setOnMenuItemClickListener {
when (it.itemId) {
R.id.mi_save -> {
downloadFile(autumnResource, resourceUrl)
true
}
R.id.mi_share_file -> {
shareFile(autumnResource, resourceUrl)
true
}
R.id.mi_share_link -> {
shareUrl(resourceUrl)
true
}
else -> false
}
}
player = ExoPlayer.Builder(this).build().apply {
setMediaItem(MediaItem.fromUri(resourceUrl))
prepare()
play()
}
binding.xpPlayer.player = player
binding.xpPlayer.setFullscreenButtonClickListener {
when (binding.alTop.visibility) {
android.view.View.VISIBLE -> {
binding.alTop.visibility = android.view.View.GONE
WindowInsetsControllerCompat(window, binding.root).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
binding.xpPlayer.background = ColorDrawable(Color.BLACK)
}
else -> {
binding.alTop.visibility = android.view.View.VISIBLE
WindowInsetsControllerCompat(window, binding.root).let { controller ->
controller.show(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
}
binding.xpPlayer.background = null
}
}
}
setContentView(binding.root)
}
override fun onDestroy() {
super.onDestroy()
player?.release()
}
override fun onPause() {
super.onPause()
player?.pause()
}
private fun shareUrl(resourceUrl: String) {
val intent =
Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(
Intent.EXTRA_TEXT,
resourceUrl
)
val shareIntent = Intent.createChooser(intent, null)
startActivity(shareIntent)
}
private fun shareFile(resource: AutumnResource, resourceUrl: String) {
lifecycleScope.launch {
val contentUri = getAttachmentContentUri(
this@VideoViewActivity2,
resourceUrl,
resource.id!!,
resource.filename ?: "video"
)
val intent =
Intent(Intent.ACTION_SEND)
intent.type = resource.contentType ?: "video/*"
intent.putExtra(
Intent.EXTRA_TITLE,
resource.filename
)
intent.putExtra(
Intent.EXTRA_SUBJECT,
resource.filename
)
intent.putExtra(
Intent.EXTRA_STREAM,
contentUri
)
val shareIntent = Intent.createChooser(intent, null)
startActivity(shareIntent)
}
}
private fun downloadFile(resource: AutumnResource, resourceUrl: String) {
lifecycleScope.launch {
this@VideoViewActivity2.applicationContext.let {
it.contentResolver.insert(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, resource.filename)
put(MediaStore.Video.Media.MIME_TYPE, resource.contentType)
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/Revolt")
put(MediaStore.Video.Media.IS_PENDING, 1)
}
)
}?.let { uri ->
this@VideoViewActivity2.contentResolver.openOutputStream(uri).use { stream ->
val video = RevoltHttp.get(resourceUrl).readBytes()
stream?.write(video)
this@VideoViewActivity2.applicationContext.let {
it.contentResolver.update(
uri,
ContentValues().apply {
put(MediaStore.Video.Media.IS_PENDING, 0)
},
null,
null
)
}
Snackbar.make(
binding.xpPlayer,
R.string.media_viewer_saved,
Snackbar.LENGTH_SHORT
).setAction(
R.string.media_viewer_open
) {
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, resource.contentType)
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
startActivity(intent)
}
.setActionTextColor(
MaterialColors.getColor(
binding.xpPlayer,
com.google.android.material.R.attr.colorPrimary
)
)
.show()
}
}
}
}
}

View File

@ -30,6 +30,19 @@ sealed class MediaConversationsVariates {
data class Restricted(val predicate: () -> Boolean) : MediaConversationsVariates()
}
@FeatureFlag("VideoViewActivity2")
sealed class ViewViewActivity2Variates {
@Treatment(
"Enable the new XML-based video player activity for all users"
)
object Enabled : ViewViewActivity2Variates()
@Treatment(
"Enable the new XML-based video player activity for users that meet certain or all criteria (implementation-specific)"
)
data class Restricted(val predicate: () -> Boolean) : ViewViewActivity2Variates()
}
object FeatureFlags {
@FeatureFlag("LabsAccessControl")
var labsAccessControl by mutableStateOf<LabsAccessControlVariates>(
@ -55,4 +68,17 @@ object FeatureFlags {
is MediaConversationsVariates.Enabled -> true
is MediaConversationsVariates.Restricted -> (mediaConversations as MediaConversationsVariates.Restricted).predicate()
}
@FeatureFlag("VideoViewActivity2")
var videoViewActivity2 by mutableStateOf<ViewViewActivity2Variates>(
ViewViewActivity2Variates.Restricted {
RevoltAPI.selfId == SpecialUsers.JENNIFER
}
)
val videoViewActivity2Granted: Boolean
get() = when (videoViewActivity2) {
is ViewViewActivity2Variates.Enabled -> true
is ViewViewActivity2Variates.Restricted -> (videoViewActivity2 as ViewViewActivity2Variates.Restricted).predicate()
}
}

View File

@ -54,6 +54,7 @@ import androidx.compose.ui.unit.sp
import chat.revolt.R
import chat.revolt.activities.media.ImageViewActivity
import chat.revolt.activities.media.VideoViewActivity
import chat.revolt.activities.media.VideoViewActivity2
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.BrushCompat
@ -66,6 +67,7 @@ import chat.revolt.api.routes.channel.unreact
import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl
import chat.revolt.api.schemas.AutumnResource
import chat.revolt.api.schemas.User
import chat.revolt.api.settings.FeatureFlags
import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.MessageReplyStyle
import chat.revolt.callbacks.Action
@ -387,7 +389,10 @@ fun Message(
attachmentView.launch(
Intent(
context,
VideoViewActivity::class.java
when (FeatureFlags.videoViewActivity2Granted) {
true -> VideoViewActivity2::class.java
else -> VideoViewActivity::class.java
}
).apply {
putExtra("autumnResource", attachment)
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" />
</vector>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
<font
app:font="@font/inter_display_thin"
app:fontStyle="normal"
app:fontWeight="100" />
<font
app:font="@font/inter_display_extralight"
app:fontStyle="normal"
app:fontWeight="200" />
<font
app:font="@font/inter_display_light"
app:fontStyle="normal"
app:fontWeight="300" />
<font
app:font="@font/inter_display_regular"
app:fontStyle="normal"
app:fontWeight="400" />
<font
app:font="@font/inter_display_medium"
app:fontStyle="normal"
app:fontWeight="500" />
<font
app:font="@font/inter_display_semibold"
app:fontStyle="normal"
app:fontWeight="600" />
<font
app:font="@font/inter_display_bold"
app:fontStyle="normal"
app:fontWeight="700" />
<font
app:font="@font/inter_display_extrabold"
app:fontStyle="normal"
app:fontWeight="800" />
<font
app:font="@font/inter_display_black"
app:fontStyle="normal"
app:fontWeight="900" />
<font
app:font="@font/inter_display_thin_italic"
app:fontStyle="italic"
app:fontWeight="100" />
<font
app:font="@font/inter_display_extralight_italic"
app:fontStyle="italic"
app:fontWeight="200" />
<font
app:font="@font/inter_display_light_italic"
app:fontStyle="italic"
app:fontWeight="300" />
<font
app:font="@font/inter_display_italic"
app:fontStyle="italic"
app:fontWeight="400" />
<font
app:font="@font/inter_display_medium_italic"
app:fontStyle="italic"
app:fontWeight="500" />
<font
app:font="@font/inter_display_semibold_italic"
app:fontStyle="italic"
app:fontWeight="600" />
<font
app:font="@font/inter_display_bold_italic"
app:fontStyle="italic"
app:fontWeight="700" />
<font
app:font="@font/inter_display_extrabold_italic"
app:fontStyle="italic"
app:fontWeight="800" />
<font
app:font="@font/inter_display_black_italic"
app:fontStyle="italic"
app:fontWeight="900" />
</font-family>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/al_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/tb_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:title="@string/unknown"
app:navigationIcon="@drawable/ic_arrow_left_24dp"
app:menu="@menu/menu_videoplayer"
app:navigationIconTint="?attr/colorOnSurface"
style="@style/Widget.Revolt.Toolbar" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.media3.ui.PlayerView
android:id="@+id/xp_player"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:show_buffering="always" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/mi_share"
android:icon="@drawable/ic_share_24dp"
android:iconTint="?attr/colorControlNormal"
android:iconTintMode="multiply"
android:title="@string/share"
app:showAsAction="ifRoom"
tools:targetApi="o">
<menu>
<item
android:id="@+id/mi_share_link"
android:title="@string/media_viewer_share_url" />
<item
android:id="@+id/mi_share_file"
android:title="@string/media_viewer_share_video" />
</menu>
</item>
<item
android:id="@+id/mi_save"
android:icon="@drawable/ic_download_24dp"
android:iconTint="?attr/colorControlNormal"
android:title="@string/media_viewer_save"
app:showAsAction="ifRoom"
tools:targetApi="o" />
</menu>

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android">
<style name="Theme.Revolt" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="textAppearanceTitleLarge">@style/TextAppearance.Revolt.TitleLarge</item>
</style>
<style name="Theme.Revolt.Starting" parent="Theme.SplashScreen" tools:targetApi="tiramisu">
@ -12,4 +13,13 @@
<item name="android:windowSplashScreenBehavior">icon_preferred</item>
<item name="postSplashScreenTheme">@style/Theme.Revolt</item>
</style>
<style name="TextAppearance.Revolt.TitleLarge" parent="TextAppearance.Material3.TitleLarge">
<item name="fontFamily">@font/inter_display_semibold</item>
<item name="android:fontFamily">@font/inter_display_semibold</item>
</style>
<style name="Widget.Revolt.Toolbar" parent="Widget.Material3.Toolbar">
<item name="titleTextAppearance">@style/TextAppearance.Revolt.TitleLarge</item>
</style>
</resources>