Video streaming sample app.

This commit is contained in:
Nicholas 2023-08-15 14:33:02 -04:00 committed by Cody Henthorne
parent 11cfe5ee82
commit a9c45f7e78
33 changed files with 865 additions and 2 deletions

View file

@ -18,7 +18,6 @@ import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.Assertions
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.session.CommandButton
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaNotification

View file

@ -29,6 +29,7 @@ dependencyResolutionManagement {
library('androidx-compose-material3', 'androidx.compose.material3', 'material3').withoutVersion()
library('androidx-compose-ui-tooling-preview', 'androidx.compose.ui', 'ui-tooling-preview').withoutVersion()
library('androidx-compose-ui-tooling-core', 'androidx.compose.ui', 'ui-tooling').withoutVersion()
library('androidx-compose-ui-test-manifest', 'androidx.compose.ui', 'ui-test-manifest').withoutVersion()
library('androidx-compose-runtime-livedata', 'androidx.compose.runtime', 'runtime-livedata').withoutVersion()
library('androidx-compose-rxjava3', 'androidx.compose.runtime:runtime-rxjava3:1.4.2')
library('ktlint-twitter-compose', 'com.twitter.compose.rules:ktlint:0.0.26')
@ -47,6 +48,7 @@ dependencyResolutionManagement {
// Android X
library('androidx-activity-ktx', 'androidx.activity', 'activity-ktx').versionRef('androidx-activity')
library('androidx-activity-compose', 'androidx.activity', 'activity-compose').versionRef('androidx-activity')
library('androidx-appcompat', 'androidx.appcompat', 'appcompat').versionRef('androidx-appcompat')
library('androidx-core-ktx', 'androidx.core:core-ktx:1.10.0')
library('androidx-fragment-ktx', 'androidx.fragment', 'fragment-ktx').versionRef('androidx-fragment')

View file

@ -362,6 +362,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="80512d30da688904b645029f039cedc3e25be99de3851170e253315c55839aa6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.compiler" name="compiler" version="1.4.3">
<artifact name="compiler-1.4.3.jar">
<sha256 value="43fcad86835221accdc8c4043fa52d2dfcc8d66442498d298c6404376d18d17c" origin="Generated by Gradle"/>
</artifact>
<artifact name="compiler-1.4.3.module">
<sha256 value="966361be4253a8488efc74132ff6cbdd0cbe89ecbde18db2639d189a8f29b92e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.compiler" name="compiler" version="1.4.4">
<artifact name="compiler-1.4.4.jar">
<sha256 value="501521d2776e656e1b80ca0a3023960628b6a1f28c4b91f3178ac60b4cb33714" origin="Generated by Gradle"/>
@ -428,6 +436,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="d591498ced23a207db2849dba1c7dcc4aa41b8f9167d1f46bad50ca97c2cbf75" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime" version="1.0.1">
<artifact name="runtime-1.0.1.module">
<sha256 value="2543a8c7edc16bde91f140286b4fd3773d7204a283a4ec99f6e5e286aa92c0c3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime" version="1.4.2">
<artifact name="runtime-1.4.2.aar">
<sha256 value="41ff5a9fbbcb8a7403345b5426e454d73278878d469b088893ecb917cc7fd84c" origin="Generated by Gradle"/>
@ -476,6 +489,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="c435d7c87b6b7fa40c1affc7a4bcd2698c181d5ac31eec8069a177b000b42eda" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable" version="1.0.1">
<artifact name="runtime-saveable-1.0.1.module">
<sha256 value="c0d6f142542d8d74f65481ef6526d2be265f01f812a112948fcde87a458f4fb6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable" version="1.4.2">
<artifact name="runtime-saveable-1.4.2.module">
<sha256 value="3677ee9c263f671944711b36e2455b4cde9e83c9266508cd2a45d9dc01a3abc6" origin="Generated by Gradle"/>
@ -489,6 +507,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="0200f91f504e138b8c4af2c7da19e510efe1400409903b97af047e9295ea8347" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui" version="1.0.1">
<artifact name="ui-1.0.1.module">
<sha256 value="57031a6ac9b60e5b56792ebf5cde6e16812ff566ed9190cbd188b00b46c13779" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui" version="1.2.1">
<artifact name="ui-1.2.1.module">
<sha256 value="f068eba19a90f039b626afe78ef74f352f124d2834fde52c3bf1a0ea1726b041" origin="Generated by Gradle"/>
@ -528,6 +551,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="145368f666b9881a571884690a1a480856908e1393d197e2c694e9070c9f9054" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-test-manifest" version="1.4.3">
<artifact name="ui-test-manifest-1.4.3.aar">
<sha256 value="f8f33961de762aab99f325961e249b646f231c014edb2564204b36ca9dfc698a" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-test-manifest-1.4.3.module">
<sha256 value="a66cab504a46f4768fbd744a0f1d42bc3edbf5881be9d882469faf11521c0397" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-text" version="1.0.0">
<artifact name="ui-text-1.0.0.module">
<sha256 value="a85fde2fe3c50e9e956daa449f27e7a10c13fe4a1cafe7f41b478ed9ecb6166b" origin="Generated by Gradle"/>
@ -1278,6 +1309,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="23542c8c85cc58fafe0ae8cba201e6c9e01b4c6799223340a2d6a51d7784828c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.recyclerview" name="recyclerview" version="1.3.0">
<artifact name="recyclerview-1.3.0.aar">
<sha256 value="d65928a00f63589a49e21925412e0f48852f89254b07b03c030d560f91effc88" origin="Generated by Gradle"/>
</artifact>
<artifact name="recyclerview-1.3.0.module">
<sha256 value="7fa22bf1ab1a8d1544622076e2ad8454e2bef1402b3298b5b4079ab732e38845" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.recyclerview" name="recyclerview" version="1.3.1">
<artifact name="recyclerview-1.3.1.aar">
<sha256 value="4cfed42bdcc196d11e9b10da68c1f96cd4bda4cd8521e7285f62442c0c11de08" origin="Generated by Gradle"/>

View file

@ -55,6 +55,8 @@ include ':sticky-header-grid'
include ':photoview'
include ':core-ui'
include ':benchmark'
include ':microbenchmark'
include ':video-app'
project(':app').name = 'Signal-Android'
project(':paging').projectDir = file('paging/lib')
@ -86,4 +88,3 @@ project(':qr-app').projectDir = file('qr/app')
rootProject.name='Signal'
apply from: 'dependencies.gradle'
include ':microbenchmark'

1
video-app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,67 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
plugins {
id("signal-sample-app")
}
val signalBuildToolsVersion: String by extra
val signalCompileSdkVersion: String by extra
val signalTargetSdkVersion: Int by extra
val signalMinSdkVersion: Int by extra
val signalJavaVersion: JavaVersion by extra
android {
namespace = "org.thoughtcrime.video.app"
compileSdkVersion = signalCompileSdkVersion
defaultConfig {
applicationId = "org.thoughtcrime.video.app"
minSdk = signalMinSdkVersion
targetSdk = signalTargetSdkVersion
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = signalJavaVersion
targetCompatibility = signalJavaVersion
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.3"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3)
implementation(libs.bundles.media3)
debugImplementation(libs.androidx.compose.ui.tooling.core)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}

21
video-app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,27 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.video.app
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.thoughtcrime.video.app", appContext.packageName)
}
}

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Signal">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Signal">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,118 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.video.app
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.ui.PlayerView
import org.thoughtcrime.video.app.ui.theme.SignalTheme
/**
* Main activity for this sample app.
*/
class MainActivity : ComponentActivity() {
private val viewModel: MainScreenViewModel by viewModels()
private lateinit var exoPlayer: ExoPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.initialize(this)
exoPlayer = ExoPlayer.Builder(this).build()
setContent {
SignalTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
val videoUri = viewModel.selectedVideo
if (videoUri == null) {
LabeledButton("Select Video") { pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly)) }
} else {
LabeledButton("Play Video") { viewModel.updateMediaSource(this@MainActivity) }
LabeledButton("Play Video with slow download") { viewModel.updateMediaSourceTrickle(this@MainActivity) }
ExoVideoView(source = viewModel.mediaSource, exoPlayer = exoPlayer)
}
}
}
}
}
}
override fun onPause() {
super.onPause()
exoPlayer.pause()
}
override fun onDestroy() {
super.onDestroy()
viewModel.releaseCache()
exoPlayer.stop()
exoPlayer.release()
}
/**
* This launches the system media picker and stores the resulting URI.
*/
private val pickMedia = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri != null) {
Log.d("PhotoPicker", "Selected URI: $uri")
viewModel.selectedVideo = uri
viewModel.updateMediaSource(this)
} else {
Log.d("PhotoPicker", "No media selected")
}
}
}
@Composable
fun LabeledButton(buttonLabel: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
Button(onClick = onClick, modifier = modifier) {
Text(buttonLabel)
}
}
@OptIn(UnstableApi::class)
@Composable
fun ExoVideoView(source: MediaSource, exoPlayer: ExoPlayer, modifier: Modifier = Modifier) {
exoPlayer.playWhenReady = false
exoPlayer.setMediaSource(source)
exoPlayer.prepare()
AndroidView(factory = { context ->
PlayerView(context).apply {
player = exoPlayer
}
}, modifier = modifier)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
SignalTheme {
LabeledButton("Preview Render") {}
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.video.app
import android.content.Context
import android.net.Uri
import androidx.annotation.OptIn
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.cache.Cache
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.NoOpCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.source.SilenceMediaSource
import java.io.File
/**
* Main screen view model for the video sample app.
*/
@OptIn(UnstableApi::class)
class MainScreenViewModel : ViewModel() {
// Initialize an silent media source before the user selects a video. This is the closest I could find to an "empty" media source while still being nullsafe.
private val value by lazy {
val factory = SilenceMediaSource.Factory()
factory.setDurationUs(1000)
factory.createMediaSource()
}
private lateinit var cache: Cache
var selectedVideo: Uri? by mutableStateOf(null)
var mediaSource: MediaSource by mutableStateOf(value)
private set
/**
* Initialize the backing cache. This is a file in the app's cache directory that has a random suffix to ensure you get cache misses on a new app launch.
*
* @param context required to get the file path of the cache directory.
*/
fun initialize(context: Context) {
val cacheDir = File(context.cacheDir.absolutePath)
cache = SimpleCache(File(cacheDir, getRandomString(12)), NoOpCacheEvictor())
}
fun updateMediaSource(context: Context) {
selectedVideo?.let {
mediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context)).createMediaSource(MediaItem.fromUri(it))
}
}
/**
* Replaces the media source with one that has a latency to each read from the media source, simulating network latency.
* It stores the result in a cache (that does not have a penalty) to better mimic real-world performance:
* once a chunk is downloaded from the network, it will not have to be re-fetched.
*
* @param context
*/
fun updateMediaSourceTrickle(context: Context) {
selectedVideo?.let {
val cacheFactory = CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(SlowDataSource.Factory(context, 10))
mediaSource = ProgressiveMediaSource.Factory(cacheFactory).createMediaSource(MediaItem.fromUri(it))
}
}
fun releaseCache() {
cache.release()
}
/**
* Get random string. Will always return at least one character.
*
* @param length length of the returned string.
* @return a string composed of random alphanumeric characters of the specified length (minimum of 1).
*/
private fun getRandomString(length: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length.coerceAtLeast(1))
.map { allowedChars.random() }
.joinToString("")
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.video.app
import android.content.Context
import android.net.Uri
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.TransferListener
/**
* This wraps a [DefaultDataSource] and adds [latency] to each read. This is intended to approximate a slow/shoddy network connection that drip-feeds in data.
*
* @property latency the amount, in milliseconds, that each read should be delayed. A good proxy for network ping.
* @constructor
*
* @param context used to initialize the underlying [DefaultDataSource.Factory]
*/
@OptIn(UnstableApi::class)
class SlowDataSource(context: Context, private val latency: Long) : DataSource {
private val internalDataSource: DataSource = DefaultDataSource.Factory(context).createDataSource()
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
Thread.sleep(latency)
return internalDataSource.read(buffer, offset, length)
}
override fun addTransferListener(transferListener: TransferListener) {
internalDataSource.addTransferListener(transferListener)
}
override fun open(dataSpec: DataSpec): Long {
return internalDataSource.open(dataSpec)
}
override fun getUri(): Uri? {
return internalDataSource.uri
}
override fun close() {
return internalDataSource.close()
}
class Factory(private val context: Context, private val latency: Long) : DataSource.Factory {
override fun createDataSource(): DataSource {
return SlowDataSource(context, latency)
}
}
}

View file

@ -0,0 +1,11 @@
package org.thoughtcrime.video.app.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View file

@ -0,0 +1,75 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.video.app.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun SignalTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.video.app.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View file

@ -0,0 +1,35 @@
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View file

@ -0,0 +1,8 @@
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<resources>
<string name="app_name">Video Player</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<resources>
<style name="Theme.Signal" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View file

@ -0,0 +1,21 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.video.app
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}