diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 28044f9..dd3f5f6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,8 @@ leakcanary = "2.14" hilt = "2.59.1" ksp = "2.3.5" +metro = "0.10.2" + datastore = "1.2.0" spotless = "8.2.1" @@ -61,6 +63,8 @@ leak-canary = { module = "com.squareup.leakcanary:leakcanary-android", version.r dagger-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } dagger-hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +metro-runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" } + datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } @@ -86,6 +90,7 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +metro = { id = "dev.zacsweers.metro", version.ref = "metro" } [bundles] datastore = ["datastore", "datastore-preferences"] diff --git a/samples/service-metro/.gitignore b/samples/service-metro/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/samples/service-metro/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/service-metro/build.gradle.kts b/samples/service-metro/build.gradle.kts new file mode 100644 index 0000000..78ab51e --- /dev/null +++ b/samples/service-metro/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + id("android.application") + id("android.tests") + id("sample.common.deps") + + alias(libs.plugins.metro) +} + +android { + namespace = "io.github.arthurkun.service.metro" + + defaultConfig { + applicationId = "io.github.arthurkun.floating.window.metro" + versionCode = 1 + versionName = "1.0" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + signingConfig = signingConfigs.getByName("debug") + } + } +} + +dependencies { + implementation(project(":library")) + + debugImplementation(libs.compose.ui.tooling) + debugImplementation(libs.compose.ui.test.manifest) + + implementation(libs.metro.runtime) + + implementation(libs.bundles.datastore) +} diff --git a/samples/service-metro/proguard-rules.pro b/samples/service-metro/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/samples/service-metro/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/samples/service-metro/src/androidTest/java/io/github/arthurkun/service/metro/ExampleInstrumentedTest.kt b/samples/service-metro/src/androidTest/java/io/github/arthurkun/service/metro/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..b640728 --- /dev/null +++ b/samples/service-metro/src/androidTest/java/io/github/arthurkun/service/metro/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package io.github.arthurkun.service.metro + +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("io.github.arthurkun.floating.window.metro", appContext.packageName) + } +} diff --git a/samples/service-metro/src/main/AndroidManifest.xml b/samples/service-metro/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e5a4466 --- /dev/null +++ b/samples/service-metro/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/DialogPermission.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/DialogPermission.kt new file mode 100644 index 0000000..9d621bd --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/DialogPermission.kt @@ -0,0 +1,70 @@ +package io.github.arthurkun.service.metro + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.github.only52607.compose.core.checkOverlayPermission +import com.github.only52607.compose.core.requestOverlayPermission + +@Composable +fun DialogPermission( + onDismiss: () -> Unit = { }, +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(lifecycleOwner.lifecycle) { + val observer = object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + val overlayGranted = checkOverlayPermission(context) + if (overlayGranted) { + onDismiss() + } + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + AlertDialog( + icon = { + Icon( + Icons.Default.Warning, + contentDescription = stringResource(R.string.permission_required), + ) + }, + title = { + Text(text = stringResource(id = R.string.permission_required)) + }, + text = { + Text(text = stringResource(R.string.message_permission_to_draw_on_top_others_apps)) + }, + onDismissRequest = onDismiss, + confirmButton = { + TextButton( + onClick = { + requestOverlayPermission(context) + }, + ) { + Text(stringResource(R.string.grant_permission)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + }, + ) +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/FloatingApplication.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/FloatingApplication.kt new file mode 100644 index 0000000..f8a5a3a --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/FloatingApplication.kt @@ -0,0 +1,22 @@ +package io.github.arthurkun.service.metro + +import android.app.Application +import dev.zacsweers.metro.createGraphFactory +import io.github.arthurkun.service.metro.di.AppGraph + +class FloatingApplication : Application() { + + val appGraph: AppGraph by lazy { + createGraphFactory().create(this) + } + + companion object { + lateinit var instance: FloatingApplication + private set + } + + override fun onCreate() { + super.onCreate() + instance = this + } +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/MainActivity.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/MainActivity.kt new file mode 100644 index 0000000..10beaf2 --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/MainActivity.kt @@ -0,0 +1,114 @@ +package io.github.arthurkun.service.metro + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.github.only52607.compose.core.checkOverlayPermission +import io.github.arthurkun.service.metro.repository.UserPreferencesRepository +import io.github.arthurkun.service.metro.service.MyService +import io.github.arthurkun.service.metro.ui.theme.ComposeFloatingWindowTheme +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class MainActivity : AppCompatActivity() { + + private val userPreferencesRepository: UserPreferencesRepository by lazy { + FloatingApplication.instance.appGraph.userPreferencesRepository + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + userPreferencesRepository.darkModeFlow.collect { darkMode -> + withContext(Dispatchers.Main) { + AppCompatDelegate.setDefaultNightMode( + if (darkMode) { + AppCompatDelegate.MODE_NIGHT_YES + } else { + AppCompatDelegate.MODE_NIGHT_NO + }, + ) + } + } + } + } + + setContent { + ComposeFloatingWindowTheme { + var showDialogPermission by rememberSaveable { mutableStateOf(false) } + val isShowing by MyService.serviceStarted.collectAsStateWithLifecycle() + val context = LocalContext.current + + Scaffold( + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> + Column( + Modifier + .padding(innerPadding) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Button( + onClick = { + val overlayPermission = checkOverlayPermission(context) + if (overlayPermission) { + MyService.start(context) + } else { + showDialogPermission = true + } + }, + enabled = !isShowing, + ) { + Text("Show") + } + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = { + MyService.stop(context) + }, + enabled = isShowing, + ) { + Text("Hide") + } + } + } + + if (showDialogPermission) { + DialogPermission( + onDismiss = { + showDialogPermission = false + }, + ) + } + } + } + } +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/di/AppGraph.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/di/AppGraph.kt new file mode 100644 index 0000000..c8cf2d0 --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/di/AppGraph.kt @@ -0,0 +1,34 @@ +package io.github.arthurkun.service.metro.di + +import android.app.Application +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import io.github.arthurkun.service.metro.module.dataStore +import io.github.arthurkun.service.metro.repository.UserPreferencesRepository + +@DependencyGraph(AppScope::class) +interface AppGraph { + + val userPreferencesRepository: UserPreferencesRepository + + val serviceGraphFactory: ServiceGraph.Factory + + @Provides + fun provideApplicationContext(application: Application): Context = application + + @SingleIn(AppScope::class) + @Provides + fun provideDataStore(context: Context): DataStore { + return context.dataStore + } + + @DependencyGraph.Factory + fun interface Factory { + fun create(@Provides application: Application): AppGraph + } +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/di/ServiceGraph.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/di/ServiceGraph.kt new file mode 100644 index 0000000..2ab19bc --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/di/ServiceGraph.kt @@ -0,0 +1,27 @@ +package io.github.arthurkun.service.metro.di + +import android.content.Context +import dev.zacsweers.metro.GraphExtension +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import io.github.arthurkun.service.metro.repository.UserPreferencesRepository +import io.github.arthurkun.service.metro.service.ServiceOverlay + +@GraphExtension(ServiceScope::class) +@SingleIn(ServiceScope::class) +interface ServiceGraph { + + val serviceOverlay: ServiceOverlay + + @SingleIn(ServiceScope::class) + @Provides + fun provideServiceOverlay( + userPreferencesRepository: UserPreferencesRepository, + context: Context, + ): ServiceOverlay = ServiceOverlay(userPreferencesRepository, context) + + @GraphExtension.Factory + interface Factory { + fun create(): ServiceGraph + } +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/di/ServiceScope.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/di/ServiceScope.kt new file mode 100644 index 0000000..3cfbfaa --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/di/ServiceScope.kt @@ -0,0 +1,10 @@ +package io.github.arthurkun.service.metro.di + +import dev.zacsweers.metro.Scope + +/** + * Service-level scope for dependencies that live for the duration of a Service. + */ +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class ServiceScope diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/module/AppModule.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/module/AppModule.kt new file mode 100644 index 0000000..d617891 --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/module/AppModule.kt @@ -0,0 +1,10 @@ +package io.github.arthurkun.service.metro.module + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore + +private const val DATASTORE_NAME = "user_prefs" + +val Context.dataStore: DataStore by preferencesDataStore(name = DATASTORE_NAME) diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/repository/UserPreferencesRepository.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/repository/UserPreferencesRepository.kt new file mode 100644 index 0000000..7f890aa --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/repository/UserPreferencesRepository.kt @@ -0,0 +1,51 @@ +package io.github.arthurkun.service.metro.repository + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +@SingleIn(AppScope::class) +@Inject +class UserPreferencesRepository( + private val dataStore: DataStore, +) { + companion object { + private val DARK_MODE_KEY = booleanPreferencesKey("dark_mode") + private val LOCATION_X = intPreferencesKey("location_x") + private val LOCATION_Y = intPreferencesKey("location_y") + } + + val darkModeFlow: Flow = dataStore + .data + .map { preferences -> + preferences[DARK_MODE_KEY] ?: false + } + + suspend fun setDarkMode(enabled: Boolean) { + dataStore.edit { preferences -> + preferences[DARK_MODE_KEY] = enabled + } + } + + val locationFlow: Flow> = dataStore + .data + .map { preferences -> + val x = preferences[LOCATION_X] ?: 0 + val y = preferences[LOCATION_Y] ?: 0 + Pair(x, y) + } + + suspend fun setLocation(x: Int, y: Int) { + dataStore.edit { preferences -> + preferences[LOCATION_X] = x + preferences[LOCATION_Y] = y + } + } +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/service/MyService.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/service/MyService.kt new file mode 100644 index 0000000..749845f --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/service/MyService.kt @@ -0,0 +1,52 @@ +package io.github.arthurkun.service.metro.service + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.IBinder +import io.github.arthurkun.service.metro.FloatingApplication +import io.github.arthurkun.service.metro.di.ServiceGraph +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class MyService : Service() { + + companion object { + private var _serviceStarted = MutableStateFlow(false) + val serviceStarted: StateFlow + get() = _serviceStarted.asStateFlow() + + fun start(context: Context) { + val intent = Intent(context, MyService::class.java) + context.startService(intent) + } + + fun stop(context: Context) { + val intent = Intent(context, MyService::class.java) + context.stopService(intent) + } + } + + private var serviceGraph: ServiceGraph? = null + private val serviceOverlay: ServiceOverlay + get() = serviceGraph!!.serviceOverlay + + override fun onCreate() { + super.onCreate() + serviceGraph = FloatingApplication.instance.appGraph.serviceGraphFactory.create() + _serviceStarted.update { true } + serviceOverlay.show() + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onDestroy() { + _serviceStarted.update { false } + // Call close for cleanup and it will hide it in the process + serviceOverlay.close() + serviceGraph = null + super.onDestroy() + } +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/service/ServiceOverlay.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/service/ServiceOverlay.kt new file mode 100644 index 0000000..dd34369 --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/service/ServiceOverlay.kt @@ -0,0 +1,70 @@ +package io.github.arthurkun.service.metro.service + +import android.content.Context +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.github.only52607.compose.service.ComposeServiceFloatingWindow +import io.github.arthurkun.service.metro.repository.UserPreferencesRepository +import io.github.arthurkun.service.metro.ui.floating.FloatingScreen +import io.github.arthurkun.service.metro.ui.floating.FloatingViewModel +import kotlinx.coroutines.flow.first + +class ServiceOverlay( + private val userPreferencesRepository: UserPreferencesRepository, + private val context: Context, +) { + + private var viewModel: FloatingViewModel? = null + private var floatingWindow: ComposeServiceFloatingWindow? = null + + init { + floatingWindow = createFloatingWindow() + } + + private fun createFloatingWindow(): ComposeServiceFloatingWindow { + // Create ViewModel only when needed + viewModel = FloatingViewModel(userPreferencesRepository) + + return ComposeServiceFloatingWindow(context).apply { + setContent { + var initialization by remember { mutableStateOf(false) } + viewModel?.let { vm -> + LaunchedEffect(Unit) { + vm.location.first().let { location -> + windowParams.x = location.first + windowParams.y = location.second + update() + } + initialization = true + } + if (initialization) { + FloatingScreen(vm) + } + } + } + } + } + + fun show() { + if (floatingWindow == null) { + floatingWindow = createFloatingWindow() + } + floatingWindow?.let { window -> + if (!window.isShowing.value) { + window.show() + } + } + } + + fun close() { + floatingWindow?.let { window -> + window.hide() + window.close() + } + floatingWindow = null + viewModel = null + } +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/floating/FloatingScreen.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/floating/FloatingScreen.kt new file mode 100644 index 0000000..29dfd3e --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/floating/FloatingScreen.kt @@ -0,0 +1,65 @@ +package io.github.arthurkun.service.metro.ui.floating + +import androidx.compose.animation.AnimatedContent +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.SystemAlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.github.only52607.compose.service.dragServiceFloatingWindow +import io.github.arthurkun.service.metro.ui.theme.ComposeFloatingWindowTheme + +@Composable +fun FloatingScreen( + vm: FloatingViewModel = viewModel(), +) { + val showing by vm.dialogVisible.collectAsStateWithLifecycle() + + val darkMode by vm.darkMode.collectAsStateWithLifecycle(false) + + ComposeFloatingWindowTheme( + darkTheme = darkMode, + ) { + if (showing) { + SystemAlertDialog( + onDismissRequest = { vm.dismissDialog() }, + confirmButton = { + TextButton(onClick = { vm.dismissDialog() }) { + Text(text = "OK") + } + }, + text = { + Text(text = "This is a system dialog") + }, + ) + } + + FloatingActionButton( + modifier = Modifier + .dragServiceFloatingWindow( + onDrag = { x, y -> + vm.updateLocation(x, y) + }, + ), + onClick = { + vm.showDialog(!darkMode) + }, + ) { + AnimatedContent(showing) { isVisible -> + if (isVisible) { + Icon(Icons.Default.Close, contentDescription = "Hide Dialog") + } else { + Icon(Icons.Default.Done, contentDescription = "Show Dialog") + } + } + } + } +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/floating/FloatingViewModel.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/floating/FloatingViewModel.kt new file mode 100644 index 0000000..57886ae --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/floating/FloatingViewModel.kt @@ -0,0 +1,40 @@ +package io.github.arthurkun.service.metro.ui.floating + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.arthurkun.service.metro.repository.UserPreferencesRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class FloatingViewModel( + private val userPreferencesRepository: UserPreferencesRepository, +) : ViewModel() { + + val location: Flow> + get() = userPreferencesRepository.locationFlow + + val darkMode: Flow + get() = userPreferencesRepository.darkModeFlow + + private var _dialogVisible = MutableStateFlow(false) + val dialogVisible: StateFlow + get() = _dialogVisible.asStateFlow() + + fun showDialog(value: Boolean) = viewModelScope.launch { + _dialogVisible.update { true } + + userPreferencesRepository.setDarkMode(value) + } + + fun dismissDialog() { + _dialogVisible.update { false } + } + + fun updateLocation(x: Int, y: Int) = viewModelScope.launch { + userPreferencesRepository.setLocation(x, y) + } +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/theme/Color.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/theme/Color.kt new file mode 100644 index 0000000..5038199 --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package io.github.arthurkun.service.metro.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/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/theme/Theme.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/theme/Theme.kt new file mode 100644 index 0000000..874e0b6 --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package io.github.arthurkun.service.metro.ui.theme + +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.ui.platform.LocalContext + +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 ComposeFloatingWindowTheme( + 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 + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content, + ) +} diff --git a/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/theme/Type.kt b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/theme/Type.kt new file mode 100644 index 0000000..fba908a --- /dev/null +++ b/samples/service-metro/src/main/java/io/github/arthurkun/service/metro/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package io.github.arthurkun.service.metro.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/samples/service-metro/src/main/res/drawable/ic_launcher_background.xml b/samples/service-metro/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..6b753e2 --- /dev/null +++ b/samples/service-metro/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/service-metro/src/main/res/drawable/ic_launcher_foreground.xml b/samples/service-metro/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..0d0d981 --- /dev/null +++ b/samples/service-metro/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/samples/service-metro/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/service-metro/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..b3e26b4 --- /dev/null +++ b/samples/service-metro/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/service-metro/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/service-metro/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..b3e26b4 --- /dev/null +++ b/samples/service-metro/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/service-metro/src/main/res/mipmap-hdpi/ic_launcher.webp b/samples/service-metro/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/samples/service-metro/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/samples/service-metro/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/samples/service-metro/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/samples/service-metro/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/samples/service-metro/src/main/res/mipmap-mdpi/ic_launcher.webp b/samples/service-metro/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/samples/service-metro/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/samples/service-metro/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/samples/service-metro/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/samples/service-metro/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/samples/service-metro/src/main/res/mipmap-xhdpi/ic_launcher.webp b/samples/service-metro/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/samples/service-metro/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/samples/service-metro/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/samples/service-metro/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/samples/service-metro/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/samples/service-metro/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/samples/service-metro/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/samples/service-metro/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/samples/service-metro/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/samples/service-metro/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/samples/service-metro/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/samples/service-metro/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/samples/service-metro/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/samples/service-metro/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/samples/service-metro/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/samples/service-metro/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/samples/service-metro/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/samples/service-metro/src/main/res/values/colors.xml b/samples/service-metro/src/main/res/values/colors.xml new file mode 100644 index 0000000..ca1931b --- /dev/null +++ b/samples/service-metro/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + diff --git a/samples/service-metro/src/main/res/values/strings.xml b/samples/service-metro/src/main/res/values/strings.xml new file mode 100644 index 0000000..72ba866 --- /dev/null +++ b/samples/service-metro/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + ComposeFloatingWindow + + Permission required + Grant permission + …to show floating windows the app needs permissions to draw on top of other apps + diff --git a/samples/service-metro/src/main/res/values/themes.xml b/samples/service-metro/src/main/res/values/themes.xml new file mode 100644 index 0000000..15f3d39 --- /dev/null +++ b/samples/service-metro/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +