This commit is contained in:
Assasinnys committed 2022-11-15 04:05:30 +03:00
commit 0500756be8
46 files changed
+1345

No files matched your search

+1
View File
@@ -0,0 +1 @@
/build
+71
View File
@@ -0,0 +1,71 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
namespace 'com.dmitryzenevich.remoteaudiocontrol'
compileSdk 33
defaultConfig {
applicationId "com.dmitryzenevich.remoteaudiocontrol"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.1.1'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose:1.6.1'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.compose.material3:material3:1.1.0-alpha02'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
implementation "com.squareup.moshi:moshi:1.14.0"
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.14.0"
}
+21
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
@@ -0,0 +1,24 @@
package com.dmitryzenevich.remoteaudiocontrol
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* 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("com.dmitryzenevich.remoteaudiocontrol", appContext.packageName)
}
}
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<application
android:name=".AudioControlApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RemoteAudioControl"
tools:targetApi="31">
<activity
android:name=".presentation.MainActivity"
android:exported="true"
android:theme="@style/Theme.RemoteAudioControl">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
</manifest>
@@ -0,0 +1,7 @@
package com.dmitryzenevich.remoteaudiocontrol
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class AudioControlApp : Application()
@@ -0,0 +1,67 @@
package com.dmitryzenevich.remoteaudiocontrol.data
import android.util.Log
import com.dmitryzenevich.remoteaudiocontrol.data.model.*
import com.dmitryzenevich.remoteaudiocontrol.domain.repository.SocketRepository
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import java.io.OutputStreamWriter
import javax.inject.Inject
class SocketRepositoryImpl @Inject constructor(
private val socketSource: SocketSource
) : SocketRepository {
private val moshi = Moshi.Builder().build()
override suspend fun openSocketConnection(address: String, port: Int): Boolean = withContext(Dispatchers.IO) {
try {
socketSource.openSocketConnection(address, port)
true
} catch (e: Exception) {
Log.e( javaClass.simpleName, "Socket connection error; address: $address, port: $port", e)
false
}
}
override suspend fun sendCommand(command: Command): Unit = withContext(Dispatchers.IO) {
val json = when(command) {
is VolumeIncrementCommand -> moshi.adapter(VolumeIncrementCommand::class.java).toJson(command)
is MuteToggleCommand -> moshi.adapter(MuteToggleCommand::class.java).toJson(command)
is SetVolumeCommand -> moshi.adapter(SetVolumeCommand::class.java).toJson(command)
}.plus("\n")
Log.i(javaClass.simpleName, "send: $json")
try {
socketSource.writer.send(json)
} catch (e: Exception) {
Log.e(javaClass.simpleName, "Can't send command: $command", e)
}
}
override fun bindSocketInput(): Flow<Event?> {
return socketSource.reader.lineSequence().asFlow().map {
Log.i(javaClass.simpleName, "json: $it")
it.toEvent(moshi)
}
}
}
fun OutputStreamWriter.send(str: String) {
write(str)
flush()
}
fun String.toEvent(moshi: Moshi): Event? {
return when {
contains(EventType.NEW_SESSION.event) -> moshi.adapter(NewSessionEvent::class.java).fromJson(this)
contains(EventType.SET_NAME.event) -> moshi.adapter(SetNameEvent::class.java).fromJson(this)
contains(EventType.MUTE_STATE_CHANGED.event) -> moshi.adapter(MuteStateChangedEvent::class.java).fromJson(this)
contains(EventType.STATE_CHANGED.event) -> moshi.adapter(StateChangedEvent::class.java).fromJson(this)
contains(EventType.VOLUME_CHANGED.event) -> moshi.adapter(VolumeChangedEvent::class.java).fromJson(this)
else -> UnknownEvent
}
}
@@ -0,0 +1,29 @@
package com.dmitryzenevich.remoteaudiocontrol.data
import java.io.Closeable
import java.net.Socket
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SocketSource @Inject constructor() : Closeable {
private var socket: Socket? = null
val writer
get() = socket().getOutputStream().writer()
val reader
get() = socket().getInputStream().bufferedReader()
private fun socket(): Socket = socket ?: throw IllegalStateException("Socket is not created")
fun openSocketConnection(address: String, port: Int) {
socket = Socket(address, port)
}
override fun close() {
socket?.close()
}
}
@@ -0,0 +1,25 @@
package com.dmitryzenevich.remoteaudiocontrol.data.model
import com.squareup.moshi.JsonClass
sealed interface Command
@JsonClass(generateAdapter = true)
data class VolumeIncrementCommand(
val PID: Long,
val increment: Int,
val event: String = "VolumeIncrement"
) : Command
@JsonClass(generateAdapter = true)
data class MuteToggleCommand(
val PID: Long,
val event: String = "MuteToggle"
) : Command
@JsonClass(generateAdapter = true)
data class SetVolumeCommand(
val PID: Long,
val volume: Int,
val event: String = "SetVolume"
) : Command
@@ -0,0 +1,48 @@
package com.dmitryzenevich.remoteaudiocontrol.data.model
import com.squareup.moshi.*
sealed interface Event
@JsonClass(generateAdapter = true)
data class NewSessionEvent(
val PID: Long
) : Event
@JsonClass(generateAdapter = true)
data class StateChangedEvent(
val PID: Long,
@field:Json(name = "is_active")
val isActive: Boolean
) : Event
@JsonClass(generateAdapter = true)
data class VolumeChangedEvent(
@field:Json(name = "new_volume")
val volume: Int,
val PID: Long
) : Event
@JsonClass(generateAdapter = true)
data class MuteStateChangedEvent(
@field:Json(name = "is_muted")
val isMuted: Boolean,
val PID: Long
) : Event
@JsonClass(generateAdapter = true)
data class SetNameEvent(
val name: String,
val PID: Long
) : Event
object UnknownEvent : Event
enum class EventType(val event: String) {
NEW_SESSION("NewSession"),
STATE_CHANGED("StateChanged"),
VOLUME_CHANGED("VolumeChanged"),
MUTE_STATE_CHANGED("MuteStateChanged"),
SET_NAME("SetName"),
SET_VOLUME("SetVolume");
}
@@ -0,0 +1,13 @@
package com.dmitryzenevich.remoteaudiocontrol.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class AppModule {
}
@@ -0,0 +1,15 @@
package com.dmitryzenevich.remoteaudiocontrol.domain.repository
import com.dmitryzenevich.remoteaudiocontrol.data.model.Command
import com.dmitryzenevich.remoteaudiocontrol.data.model.Event
import kotlinx.coroutines.flow.Flow
interface SocketRepository {
suspend fun openSocketConnection(address: String, port: Int): Boolean
suspend fun sendCommand(command: Command)
fun bindSocketInput(): Flow<Event?>
}
@@ -0,0 +1,5 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation
import kotlin.math.roundToInt
fun Float.classicRound(): Float = times(100).roundToInt().div(100f)
@@ -0,0 +1,105 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import com.dmitryzenevich.remoteaudiocontrol.presentation.theme.RemoteAudioControlTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.math.roundToInt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RemoteAudioControlTheme {
val uiState by viewModel.uiState.collectAsState()
LazyRow {
items(uiState.volumes) { itemState ->
VolumeItem(itemState, viewModel)
}
}
}
}
}
}
@Composable
fun VolumeItem(volumeItemState: VolumeItemState, viewModel: MainViewModel) {
var volume by remember { mutableStateOf(volumeItemState.volume) }
volume = volumeItemState.volume
val onValueChanged = { newValue: Float ->
Log.i("debug", "new value: $newValue")
viewModel.onVolumeChanged(volumeItemState, newValue.toInt())
volume = newValue.toInt()
}
Column(
modifier = Modifier
.padding(8.dp)
.wrapContentSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "${volumeItemState.name}\nVolume: ${volumeItemState.volume}"
)
VerticalSlider(
value = /*volumeItemState.volume.toFloat()*/volume.toFloat(),
onValueChanged = onValueChanged
)
}
}
@Composable
fun VerticalSlider(
value: Float,
onValueChanged: (Float) -> Unit
) {
Slider(
value = value,
valueRange = 0f..100f,
steps = 99,
onValueChange = onValueChanged,
modifier = Modifier
.graphicsLayer {
rotationZ = 270f
transformOrigin = TransformOrigin(0f, 0f)
}
.layout { measurable, constraints ->
val placeable = measurable.measure(
Constraints(
minWidth = constraints.minHeight,
maxWidth = constraints.maxHeight,
minHeight = constraints.minWidth,
maxHeight = constraints.maxHeight,
)
)
layout(placeable.height, placeable.width) {
placeable.place(-placeable.width, 0)
}
}
.padding(horizontal = 24.dp)
)
}
@@ -0,0 +1,143 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dmitryzenevich.remoteaudiocontrol.data.SocketRepositoryImpl
import com.dmitryzenevich.remoteaudiocontrol.data.model.*
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.inject.Inject
import kotlin.math.absoluteValue
@HiltViewModel
class MainViewModel @Inject constructor(
private val socketRepositoryImpl: SocketRepositoryImpl
) : ViewModel() {
private val _uiState = MutableStateFlow(MainScreenUiState())
val uiState = _uiState.asStateFlow()
private var commandJob: Job? = null
init {
viewModelScope.launch {
val isReady = socketRepositoryImpl.openSocketConnection("10.0.2.2", 54683)
if (isReady) {
socketRepositoryImpl.bindSocketInput()
.filterNotNull()
.onEach {
Log.i(javaClass.simpleName, "received: $it")
// TODO: test it
withContext(Dispatchers.Main) { proceedEvent(it) }
}
.catch { Log.e(javaClass.simpleName, "Fetch event error", it) }
.launchIn(CoroutineScope(Dispatchers.IO))
}
}
}
private fun proceedEvent(event: Event) {
val volumes = _uiState.value.volumes.toMutableList()
when(event) {
is NewSessionEvent -> {
// if (_uiState.value.volumes.find { it.pid == event.PID } == null) {
// _uiState.value = MainScreenUiState(volumes = _uiState.value.volumes + event.toVolumeItemState())
// }
}
is MuteStateChangedEvent -> {
volumes.find { event.PID == it.pid }?.let { item ->
val newItem = item.copy(isMuted = event.isMuted)
volumes.set(
index = volumes.indexOfFirst { it.pid == newItem.pid },
element = newItem
)
}
_uiState.value = _uiState.value.copy(volumes = volumes)
}
is SetNameEvent -> {
volumes.find { event.PID == it.pid }?.let { item ->
val newItem = item.copy(name = event.name)
volumes.set(
index = volumes.indexOfFirst { it.pid == newItem.pid },
element = newItem
)
}
_uiState.value = _uiState.value.copy(volumes = volumes)
}
is StateChangedEvent -> {
volumes.find { event.PID == it.pid }?.let { item ->
val newItem = item.copy(isActive = event.isActive)
volumes.set(
index = volumes.indexOfFirst { it.pid == newItem.pid },
element = newItem
)
}
_uiState.value = _uiState.value.copy(volumes = volumes)
}
is VolumeChangedEvent -> {
addIfNotExist(event)
val v = _uiState.value.volumes.toMutableList()
v.find { event.PID == it.pid }?.let { item ->
val newItem = item.copy(volume = event.volume)
v.set(
index = v.indexOfFirst { it.pid == newItem.pid },
element = newItem
)
}
_uiState.value = _uiState.value.copy(volumes = v)
}
UnknownEvent -> _uiState.value = _uiState.value.copy(isError = true)
}
}
fun onVolumeChanged(volumeItemState: VolumeItemState, newVolume: Int) {
Log.i(javaClass.simpleName, "old volume: ${volumeItemState.volume}, newVolume: $newVolume")
val increment = newVolume.minus(volumeItemState.volume)
Log.i(javaClass.simpleName, "increment: $increment")
// val volumes = _uiState.value.volumes.toMutableList()
// volumes.find { volumeItemState.pid == it.pid }?.let { item ->
// val newItem = item.copy(volume = newVolume)
// volumes.set(
// index = volumes.indexOfFirst { it.pid == newItem.pid },
// element = newItem
// )
// }
// _uiState.value = _uiState.value.copy(volumes = volumes)
sendCommand(SetVolumeCommand(volumeItemState.pid, newVolume))
}
private fun sendCommand(command: Command) {
commandJob?.cancel()
commandJob = viewModelScope.launch { socketRepositoryImpl.sendCommand(command) }
}
private fun addIfNotExist(event: VolumeChangedEvent) {
if (_uiState.value.volumes.find { it.pid == event.PID } == null) {
_uiState.value = MainScreenUiState(volumes = _uiState.value.volumes + event.toVolumeItemState())
}
}
}
data class MainScreenUiState(
val volumes: List<VolumeItemState> = emptyList(),
val isError: Boolean = false
)
data class VolumeItemState(
val pid: Long,
val name: String = "",
val volume: Int = 0,
val isMuted: Boolean = false,
val isActive: Boolean = false
)
fun NewSessionEvent.toVolumeItemState() = VolumeItemState(pid = PID)
fun VolumeChangedEvent.toVolumeItemState() = VolumeItemState(pid = PID, volume = volume)
@@ -0,0 +1,11 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation.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)
@@ -0,0 +1,68 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation.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.ViewCompat
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 RemoteAudioControlTheme(
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 {
(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,34 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation.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
)
*/
)
@@ -0,0 +1,30 @@
<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>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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" />
</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

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Remote Audio Control</string>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.RemoteAudioControl" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package com.dmitryzenevich.remoteaudiocontrol
import org.junit.Test
import org.junit.Assert.*
/**
* 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)
}
}