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: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

@ -2,64 +2,36 @@ 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.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.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 androidx.media3.ui.PlayerView
import chat.revolt.R
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltHttp
import chat.revolt.api.schemas.AutumnResource
import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings
import chat.revolt.databinding.ActivityVideoplayerBinding
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.statement.readBytes
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?) {
super.onCreate(savedInstanceState)
@ -76,32 +48,82 @@ class VideoViewActivity : ComponentActivity() {
return
}
val resourceUrl =
"$REVOLT_FILES/attachments/${autumnResource.id}/${autumnResource.filename}"
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
VideoViewScreen(resource = autumnResource, onClose = { finish() })
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)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@Composable
fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
val resourceUrl = "$REVOLT_FILES/attachments/${resource.id}/${resource.filename}"
override fun onDestroy() {
super.onDestroy()
player?.release()
}
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val activityLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {}
val shareSubmenuIsOpen = remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }
fun shareUrl() {
shareSubmenuIsOpen.value = false
override fun onPause() {
super.onPause()
player?.pause()
}
private fun shareUrl(resourceUrl: String) {
val intent =
Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
@ -111,15 +133,13 @@ fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
)
val shareIntent = Intent.createChooser(intent, null)
activityLauncher.launch(shareIntent)
startActivity(shareIntent)
}
fun shareVideo() {
shareSubmenuIsOpen.value = false
coroutineScope.launch {
private fun shareFile(resource: AutumnResource, resourceUrl: String) {
lifecycleScope.launch {
val contentUri = getAttachmentContentUri(
context,
this@VideoViewActivity,
resourceUrl,
resource.id!!,
resource.filename ?: "video"
@ -128,19 +148,27 @@ fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
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)
activityLauncher.launch(shareIntent)
startActivity(shareIntent)
}
}
fun saveToGallery() {
coroutineScope.launch {
context.applicationContext.let {
private fun downloadFile(resource: AutumnResource, resourceUrl: String) {
lifecycleScope.launch {
this@VideoViewActivity.applicationContext.let {
it.contentResolver.insert(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
ContentValues().apply {
@ -151,11 +179,11 @@ fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
}
)
}?.let { uri ->
context.contentResolver.openOutputStream(uri).use { stream ->
this@VideoViewActivity.contentResolver.openOutputStream(uri).use { stream ->
val video = RevoltHttp.get(resourceUrl).readBytes()
stream?.write(video)
context.applicationContext.let {
this@VideoViewActivity.applicationContext.let {
it.contentResolver.update(
uri,
ContentValues().apply {
@ -166,145 +194,27 @@ fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
)
}
val snackbar = snackbarHostState.showSnackbar(
message = context.getString(R.string.media_viewer_saved),
actionLabel = context.getString(R.string.media_viewer_open),
duration = SnackbarDuration.Short
)
if (snackbar == SnackbarResult.ActionPerformed) {
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
activityLauncher.launch(intent)
startActivity(intent)
}
}
}
}
}
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,
.setActionTextColor(
MaterialColors.getColor(
binding.xpPlayer,
com.google.android.material.R.attr.colorPrimary
)
)
},
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)
)
.show()
}
}
}
}
}
}

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()
}
@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>(
@ -68,17 +55,4 @@ 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,7 +54,6 @@ 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
@ -67,7 +66,6 @@ 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
@ -389,10 +387,7 @@ fun Message(
attachmentView.launch(
Intent(
context,
when (FeatureFlags.videoViewActivity2Granted) {
true -> VideoViewActivity2::class.java
else -> VideoViewActivity::class.java
}
VideoViewActivity::class.java
).apply {
putExtra("autumnResource", attachment)
}