diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaNotificationProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaNotificationProvider.kt
index 8386958cc9..8b5e7aeaa0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaNotificationProvider.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaNotificationProvider.kt
@@ -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
diff --git a/dependencies.gradle b/dependencies.gradle
index f75271ef0a..8863ca5353 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -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')
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index a5d056b1ab..6ef566d045 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -362,6 +362,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -428,6 +436,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
@@ -476,6 +489,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
@@ -489,6 +507,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
@@ -528,6 +551,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
@@ -1278,6 +1309,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
+
+
+
+
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
index 9ce38b14af..3040346ef7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -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'
diff --git a/video-app/.gitignore b/video-app/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/video-app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/video-app/build.gradle.kts b/video-app/build.gradle.kts
new file mode 100644
index 0000000000..fc01414921
--- /dev/null
+++ b/video-app/build.gradle.kts
@@ -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)
+}
diff --git a/video-app/proguard-rules.pro b/video-app/proguard-rules.pro
new file mode 100644
index 0000000000..481bb43481
--- /dev/null
+++ b/video-app/proguard-rules.pro
@@ -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
\ No newline at end of file
diff --git a/video-app/src/androidTest/java/org/thoughtcrime/video/app/ExampleInstrumentedTest.kt b/video-app/src/androidTest/java/org/thoughtcrime/video/app/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000000..be3f148b04
--- /dev/null
+++ b/video-app/src/androidTest/java/org/thoughtcrime/video/app/ExampleInstrumentedTest.kt
@@ -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)
+ }
+}
diff --git a/video-app/src/main/AndroidManifest.xml b/video-app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..ad55630ca3
--- /dev/null
+++ b/video-app/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/video-app/src/main/java/org/thoughtcrime/video/app/MainActivity.kt b/video-app/src/main/java/org/thoughtcrime/video/app/MainActivity.kt
new file mode 100644
index 0000000000..267299b1f7
--- /dev/null
+++ b/video-app/src/main/java/org/thoughtcrime/video/app/MainActivity.kt
@@ -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") {}
+ }
+}
diff --git a/video-app/src/main/java/org/thoughtcrime/video/app/MainScreenViewModel.kt b/video-app/src/main/java/org/thoughtcrime/video/app/MainScreenViewModel.kt
new file mode 100644
index 0000000000..a91d6590e3
--- /dev/null
+++ b/video-app/src/main/java/org/thoughtcrime/video/app/MainScreenViewModel.kt
@@ -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("")
+ }
+}
diff --git a/video-app/src/main/java/org/thoughtcrime/video/app/SlowDataSource.kt b/video-app/src/main/java/org/thoughtcrime/video/app/SlowDataSource.kt
new file mode 100644
index 0000000000..5406e1e156
--- /dev/null
+++ b/video-app/src/main/java/org/thoughtcrime/video/app/SlowDataSource.kt
@@ -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)
+ }
+ }
+}
diff --git a/video-app/src/main/java/org/thoughtcrime/video/app/ui/theme/Color.kt b/video-app/src/main/java/org/thoughtcrime/video/app/ui/theme/Color.kt
new file mode 100644
index 0000000000..90fc2bd952
--- /dev/null
+++ b/video-app/src/main/java/org/thoughtcrime/video/app/ui/theme/Color.kt
@@ -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)
diff --git a/video-app/src/main/java/org/thoughtcrime/video/app/ui/theme/Theme.kt b/video-app/src/main/java/org/thoughtcrime/video/app/ui/theme/Theme.kt
new file mode 100644
index 0000000000..10c826c424
--- /dev/null
+++ b/video-app/src/main/java/org/thoughtcrime/video/app/ui/theme/Theme.kt
@@ -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
+ )
+}
diff --git a/video-app/src/main/java/org/thoughtcrime/video/app/ui/theme/Type.kt b/video-app/src/main/java/org/thoughtcrime/video/app/ui/theme/Type.kt
new file mode 100644
index 0000000000..726f149a58
--- /dev/null
+++ b/video-app/src/main/java/org/thoughtcrime/video/app/ui/theme/Type.kt
@@ -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
+ )
+ */
+)
diff --git a/video-app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/video-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..743651fa54
--- /dev/null
+++ b/video-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/video-app/src/main/res/drawable/ic_launcher_background.xml b/video-app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000..126c9713e0
--- /dev/null
+++ b/video-app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/video-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/video-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..80e53a5e47
--- /dev/null
+++ b/video-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/video-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/video-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..80e53a5e47
--- /dev/null
+++ b/video-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/video-app/src/main/res/mipmap-hdpi/ic_launcher.webp b/video-app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000000..c209e78ecd
Binary files /dev/null and b/video-app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/video-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/video-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..b2dfe3d1ba
Binary files /dev/null and b/video-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/video-app/src/main/res/mipmap-mdpi/ic_launcher.webp b/video-app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000000..4f0f1d64e5
Binary files /dev/null and b/video-app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/video-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/video-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..62b611da08
Binary files /dev/null and b/video-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/video-app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/video-app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..948a3070fe
Binary files /dev/null and b/video-app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/video-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/video-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..1b9a6956b3
Binary files /dev/null and b/video-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/video-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/video-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..28d4b77f9f
Binary files /dev/null and b/video-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/video-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/video-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..9287f50836
Binary files /dev/null and b/video-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/video-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/video-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..aa7d6427e6
Binary files /dev/null and b/video-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/video-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/video-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..9126ae37cb
Binary files /dev/null and b/video-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/video-app/src/main/res/values/colors.xml b/video-app/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..38279fc979
--- /dev/null
+++ b/video-app/src/main/res/values/colors.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/video-app/src/main/res/values/strings.xml b/video-app/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..06af607989
--- /dev/null
+++ b/video-app/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+
+
+
+ Video Player
+
\ No newline at end of file
diff --git a/video-app/src/main/res/values/themes.xml b/video-app/src/main/res/values/themes.xml
new file mode 100644
index 0000000000..35ed1d0bff
--- /dev/null
+++ b/video-app/src/main/res/values/themes.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/video-app/src/test/java/org/thoughtcrime/video/app/ExampleUnitTest.kt b/video-app/src/test/java/org/thoughtcrime/video/app/ExampleUnitTest.kt
new file mode 100644
index 0000000000..886b8306a0
--- /dev/null
+++ b/video-app/src/test/java/org/thoughtcrime/video/app/ExampleUnitTest.kt
@@ -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)
+ }
+}