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 @@
+
+
+
+
+
diff --git a/samples/service-metro/src/test/java/io/github/arthurkun/service/metro/ExampleUnitTest.kt b/samples/service-metro/src/test/java/io/github/arthurkun/service/metro/ExampleUnitTest.kt
new file mode 100644
index 0000000..c0e4dd9
--- /dev/null
+++ b/samples/service-metro/src/test/java/io/github/arthurkun/service/metro/ExampleUnitTest.kt
@@ -0,0 +1,16 @@
+package io.github.arthurkun.service.metro
+
+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)
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 1744a0c..e7b0bc7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -33,5 +33,6 @@ rootProject.name = "ComposeFloatingWindow"
include(":library")
include(":samples:app-activity")
include(":samples:service-hilt")
+include(":samples:service-metro")
include(":samples:fullscreen-dialog")
include(":samples:keyboard-usage")