feat: graduate VVA2 to GA

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-06-30 19:32:53 +02:00
parent f0d27b9a80
commit 70927bd1a4
5 changed files with 117 additions and 463 deletions

View File

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

View File

@ -2,64 +2,36 @@ package chat.revolt.activities.media
import android.content.ContentValues import android.content.ContentValues
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.WindowCompat 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.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.REVOLT_FILES import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltHttp import chat.revolt.api.RevoltHttp
import chat.revolt.api.schemas.AutumnResource import chat.revolt.api.schemas.AutumnResource
import chat.revolt.api.settings.GlobalState import chat.revolt.databinding.ActivityVideoplayerBinding
import chat.revolt.api.settings.SyncedSettings
import chat.revolt.provider.getAttachmentContentUri import chat.revolt.provider.getAttachmentContentUri
import chat.revolt.ui.theme.RevoltTheme import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.readBytes import io.ktor.client.statement.readBytes
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class VideoViewActivity : ComponentActivity() { class VideoViewActivity : FragmentActivity() {
private lateinit var binding: ActivityVideoplayerBinding
private var player: ExoPlayer? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -76,32 +48,82 @@ class VideoViewActivity : ComponentActivity() {
return return
} }
val resourceUrl =
"$REVOLT_FILES/attachments/${autumnResource.id}/${autumnResource.filename}"
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { binding = ActivityVideoplayerBinding.inflate(layoutInflater)
VideoViewScreen(resource = autumnResource, onClose = { finish() })
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)
} }
}
@OptIn(ExperimentalMaterial3Api::class) override fun onDestroy() {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) super.onDestroy()
@Composable player?.release()
fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) { }
val resourceUrl = "$REVOLT_FILES/attachments/${resource.id}/${resource.filename}"
val context = LocalContext.current override fun onPause() {
val coroutineScope = rememberCoroutineScope() super.onPause()
val activityLauncher = rememberLauncherForActivityResult( player?.pause()
ActivityResultContracts.StartActivityForResult() }
) {}
val shareSubmenuIsOpen = remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }
fun shareUrl() {
shareSubmenuIsOpen.value = false
private fun shareUrl(resourceUrl: String) {
val intent = val intent =
Intent(Intent.ACTION_SEND) Intent(Intent.ACTION_SEND)
intent.type = "text/plain" intent.type = "text/plain"
@ -111,15 +133,13 @@ fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
) )
val shareIntent = Intent.createChooser(intent, null) val shareIntent = Intent.createChooser(intent, null)
activityLauncher.launch(shareIntent) startActivity(shareIntent)
} }
fun shareVideo() { private fun shareFile(resource: AutumnResource, resourceUrl: String) {
shareSubmenuIsOpen.value = false lifecycleScope.launch {
coroutineScope.launch {
val contentUri = getAttachmentContentUri( val contentUri = getAttachmentContentUri(
context, this@VideoViewActivity,
resourceUrl, resourceUrl,
resource.id!!, resource.id!!,
resource.filename ?: "video" resource.filename ?: "video"
@ -128,19 +148,27 @@ fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
val intent = val intent =
Intent(Intent.ACTION_SEND) Intent(Intent.ACTION_SEND)
intent.type = resource.contentType ?: "video/*" intent.type = resource.contentType ?: "video/*"
intent.putExtra(
Intent.EXTRA_TITLE,
resource.filename
)
intent.putExtra(
Intent.EXTRA_SUBJECT,
resource.filename
)
intent.putExtra( intent.putExtra(
Intent.EXTRA_STREAM, Intent.EXTRA_STREAM,
contentUri contentUri
) )
val shareIntent = Intent.createChooser(intent, null) val shareIntent = Intent.createChooser(intent, null)
activityLauncher.launch(shareIntent) startActivity(shareIntent)
} }
} }
fun saveToGallery() { private fun downloadFile(resource: AutumnResource, resourceUrl: String) {
coroutineScope.launch { lifecycleScope.launch {
context.applicationContext.let { this@VideoViewActivity.applicationContext.let {
it.contentResolver.insert( it.contentResolver.insert(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
ContentValues().apply { ContentValues().apply {
@ -151,11 +179,11 @@ fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
} }
) )
}?.let { uri -> }?.let { uri ->
context.contentResolver.openOutputStream(uri).use { stream -> this@VideoViewActivity.contentResolver.openOutputStream(uri).use { stream ->
val video = RevoltHttp.get(resourceUrl).readBytes() val video = RevoltHttp.get(resourceUrl).readBytes()
stream?.write(video) stream?.write(video)
context.applicationContext.let { this@VideoViewActivity.applicationContext.let {
it.contentResolver.update( it.contentResolver.update(
uri, uri,
ContentValues().apply { ContentValues().apply {
@ -166,145 +194,27 @@ fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
) )
} }
val snackbar = snackbarHostState.showSnackbar( Snackbar.make(
message = context.getString(R.string.media_viewer_saved), binding.xpPlayer,
actionLabel = context.getString(R.string.media_viewer_open), R.string.media_viewer_saved,
duration = SnackbarDuration.Short Snackbar.LENGTH_SHORT
) ).setAction(
R.string.media_viewer_open
if (snackbar == SnackbarResult.ActionPerformed) { ) {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, resource.contentType) intent.setDataAndType(uri, resource.contentType)
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
activityLauncher.launch(intent) startActivity(intent)
} }
} .setActionTextColor(
} MaterialColors.getColor(
} binding.xpPlayer,
} com.google.android.material.R.attr.colorPrimary
)
val player = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(resourceUrl))
prepare()
play()
}
}
DisposableEffect(player) {
onDispose {
player.release()
}
}
RevoltTheme(
requestedTheme = GlobalState.theme,
colourOverrides = SyncedSettings.android.colourOverrides
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(
id = R.string.media_viewer_title_video,
resource.filename ?: resource.id!!
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
}, .show()
navigationIcon = {
IconButton(onClick = {
onClose()
}) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource(id = R.string.back)
)
}
},
actions = {
IconButton(onClick = {
shareSubmenuIsOpen.value = true
}) {
Icon(
painter = painterResource(id = R.drawable.ic_share_24dp),
contentDescription = stringResource(id = R.string.share)
)
}
DropdownMenu(
expanded = shareSubmenuIsOpen.value,
onDismissRequest = {
shareSubmenuIsOpen.value = false
}
) {
DropdownMenuItem(
onClick = {
shareUrl()
},
text = {
Text(
stringResource(id = R.string.media_viewer_share_url)
)
}
)
DropdownMenuItem(
onClick = {
shareVideo()
},
text = {
Text(
stringResource(
id = R.string.media_viewer_share_video
)
)
}
)
}
IconButton(onClick = {
saveToGallery()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_download_24dp),
contentDescription = stringResource(
id = R.string.media_viewer_save
)
)
}
}
)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) { pv ->
Surface(
modifier = Modifier
.padding(pv)
.background(MaterialTheme.colorScheme.background)
.fillMaxSize()
) {
Box(
modifier = Modifier
.clip(RectangleShape)
.fillMaxSize()
) {
AndroidView(
factory = { context ->
PlayerView(context).apply {
setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS)
}
},
update = {
it.player = player
},
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
)
} }
} }
} }
} }
} }

View File

@ -1,220 +0,0 @@
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,19 +30,6 @@ sealed class MediaConversationsVariates {
data class Restricted(val predicate: () -> Boolean) : 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 { object FeatureFlags {
@FeatureFlag("LabsAccessControl") @FeatureFlag("LabsAccessControl")
var labsAccessControl by mutableStateOf<LabsAccessControlVariates>( var labsAccessControl by mutableStateOf<LabsAccessControlVariates>(
@ -68,17 +55,4 @@ object FeatureFlags {
is MediaConversationsVariates.Enabled -> true is MediaConversationsVariates.Enabled -> true
is MediaConversationsVariates.Restricted -> (mediaConversations as MediaConversationsVariates.Restricted).predicate() 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,7 +54,6 @@ import androidx.compose.ui.unit.sp
import chat.revolt.R import chat.revolt.R
import chat.revolt.activities.media.ImageViewActivity import chat.revolt.activities.media.ImageViewActivity
import chat.revolt.activities.media.VideoViewActivity import chat.revolt.activities.media.VideoViewActivity
import chat.revolt.activities.media.VideoViewActivity2
import chat.revolt.api.REVOLT_FILES import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.BrushCompat import chat.revolt.api.internals.BrushCompat
@ -67,7 +66,6 @@ import chat.revolt.api.routes.channel.unreact
import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl
import chat.revolt.api.schemas.AutumnResource import chat.revolt.api.schemas.AutumnResource
import chat.revolt.api.schemas.User import chat.revolt.api.schemas.User
import chat.revolt.api.settings.FeatureFlags
import chat.revolt.api.settings.GlobalState import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.MessageReplyStyle import chat.revolt.api.settings.MessageReplyStyle
import chat.revolt.callbacks.Action import chat.revolt.callbacks.Action
@ -389,10 +387,7 @@ fun Message(
attachmentView.launch( attachmentView.launch(
Intent( Intent(
context, context,
when (FeatureFlags.videoViewActivity2Granted) { VideoViewActivity::class.java
true -> VideoViewActivity2::class.java
else -> VideoViewActivity::class.java
}
).apply { ).apply {
putExtra("autumnResource", attachment) putExtra("autumnResource", attachment)
} }