diff --git a/.gitignore b/.gitignore index 7660d02..696ae82 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,19 @@ google-services.json hs_err_pid* replay_pid* +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..7bed07e --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..c224ad5 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..d843f34 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..5cc7160 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "ru.officialdakari.fmd" + compileSdk = 35 + + defaultConfig { + applicationId = "ru.officialdakari.fmd" + minSdk = 33 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/ru/officialdakari/fmd/ExampleInstrumentedTest.kt b/app/src/androidTest/java/ru/officialdakari/fmd/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..f720f48 --- /dev/null +++ b/app/src/androidTest/java/ru/officialdakari/fmd/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package ru.officialdakari.fmd + +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("ru.officialdakari.fmd", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f7a8431 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/CommandHandler.kt b/app/src/main/java/ru/officialdakari/fmd/CommandHandler.kt new file mode 100644 index 0000000..79aa860 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/CommandHandler.kt @@ -0,0 +1,42 @@ +package ru.officialdakari.fmd + +import android.content.Context +import android.content.Intent +import android.util.Log + +class CommandHandler(val context: Context, val lockIntent: Intent, val ringIntent: Intent) { + val locUtils: LocationUtils = LocationUtils(context) + public fun handleCommand(str: String, reply: (String) -> Unit) { + if (!locUtils.isStarted()) locUtils.start() + val args = str.split(" "); + if (args.count() < 3) { + Log.w("FMD", "Skipping, ${args.count()} < 3"); + } + if (!Utils.checkPin(context, args[1])) { + Log.w("FMD", "Skipping, wrong PIN"); + } + if (args[2].contentEquals("lock")) { + if (args.count() > 3) { + Utils.lockMessage = args.subList(3, args.count()).joinToString(" ") + } else { + Utils.lockMessage = "Locked via FMD." + } + Utils.lockDevice(context) + Utils.sleep(1000, { + context.startActivity(lockIntent) + }) + reply("Device locked") + } else if (args[2].contentEquals("ring")) { + context.startActivity(ringIntent) + reply("Ringing") + } else if (args[2].contentEquals("locate")) { + locUtils.getLocation({ loc -> + if (args.contains("--no-link")) { + reply("${loc.latitude} ${loc.longitude}") + } else { + reply("https://www.openstreetmap.org/#map=18/${loc.latitude}/${loc.longitude}") + } + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/LocationUtils.kt b/app/src/main/java/ru/officialdakari/fmd/LocationUtils.kt new file mode 100644 index 0000000..5bca333 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/LocationUtils.kt @@ -0,0 +1,47 @@ +package ru.officialdakari.fmd + +import android.app.Application +import android.content.Context +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.util.Log +import ru.officialdakari.fmd.executors.ThreadPerTaskExecutor +import java.util.concurrent.Executor + +class LocationUtils(val context: Context) { + private var locationManager: LocationManager? = null + private lateinit var loc: Location + public fun start() { + locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + } + + public fun isStarted(): Boolean { + return locationManager != null + } + + public fun getLocation(gotLocation: (Location) -> Unit) { + try { + locationManager!!.getCurrentLocation( + LocationManager.GPS_PROVIDER, + null, + ThreadPerTaskExecutor(), + { location -> + gotLocation(location) + } + ) + } catch (ex: SecurityException) { + + } + } + + private val locationListener: LocationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + this@LocationUtils.loc = location + } + override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {} + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/LockActivity.kt b/app/src/main/java/ru/officialdakari/fmd/LockActivity.kt new file mode 100644 index 0000000..14aef5f --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/LockActivity.kt @@ -0,0 +1,55 @@ +package ru.officialdakari.fmd + +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import ru.officialdakari.fmd.ui.theme.FindMyDeviceTheme + +class LockActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD) + window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + enableEdgeToEdge() + setContent { + FindMyDeviceTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + LockMessageScreen(Modifier.padding(innerPadding)) + } + } + } + } +} + +@Composable +fun LockMessageScreen(modifier: Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = "This device was locked by FindMyDevice!", + fontSize = 5.em + ) + Text( + text = Utils.lockMessage + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/MainActivity.kt b/app/src/main/java/ru/officialdakari/fmd/MainActivity.kt new file mode 100644 index 0000000..0498a07 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/MainActivity.kt @@ -0,0 +1,144 @@ +package ru.officialdakari.fmd + +import android.content.Context +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ru.officialdakari.fmd.dialogs.NewPinDialog +import ru.officialdakari.fmd.dialogs.SelectAppsDialog +import ru.officialdakari.fmd.ui.theme.FindMyDeviceTheme +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream + + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + FindMyDeviceTheme { + Scaffold(topBar = { Titlebar() }, modifier = Modifier.fillMaxSize()) { innerPadding -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(innerPadding), + verticalArrangement = Arrangement.Center, + ) { + SetPinButton(context = this@MainActivity) + SetAppsButton(context = this@MainActivity) + } + } + } + } + } +} + +@Composable +fun SetPinButton(context: Context) { + val deviceProtectedContext: Context = context.createDeviceProtectedStorageContext() + var showDialog by remember { mutableStateOf(false) } + + if (showDialog) { + NewPinDialog( + onDismissRequest = { + showDialog = false + }, + onConfirm = { pass -> + var pinFile = File("${deviceProtectedContext.filesDir.path}/pin.txt") + val pinInputStream = FileOutputStream(pinFile) + var hashedPin = Utils.sha256(pass) + pinInputStream.write(hashedPin.toByteArray(Charsets.US_ASCII)) + } + ) + } + + Text( + text = "FMD is operating through notifications and SMS. To prevent malicious usage, you need to set PIN and remember it. It is required to trigger FMD remotely.", + modifier = Modifier.padding(10.dp) + ) + Button( + onClick = { showDialog = true }, + modifier = Modifier.fillMaxWidth().padding(10.dp) + ) { + Text("Set PIN") + } +} + +@Composable +fun SetAppsButton(context: Context) { + var showDialog by remember { mutableStateOf(false) } + val initialState = mutableListOf>() + + for (app in Utils.getAllowedApps(context)) { + initialState.add(Pair(app, true)) + } + + if (showDialog) { + SelectAppsDialog( + onDismissRequest = { + showDialog = false + }, + onConfirm = { state -> + var l = mutableListOf() + for (pair in state) { + if (pair.second) { + l.add(pair.first) + } + } + Utils.setAllowedApps(context, l.toTypedArray()) + Toast.makeText(context, "App list saved", Toast.LENGTH_SHORT) + }, + initialState = initialState, + context = context, + ) + } + + Text( + text = "Choose which apps' notifications are allowed to control FMD.", + modifier = Modifier.padding(10.dp) + ) + Button( + onClick = { + showDialog = true + }, + modifier = Modifier.fillMaxWidth().padding(10.dp) + ) { + Text("Choose apps") + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Titlebar() { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text(text = "FindMyDevice") + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/PermissionsActivity.kt b/app/src/main/java/ru/officialdakari/fmd/PermissionsActivity.kt new file mode 100644 index 0000000..78ae3b6 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/PermissionsActivity.kt @@ -0,0 +1,247 @@ +package ru.officialdakari.fmd + +import android.Manifest +import android.app.admin.DevicePolicyManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import ru.officialdakari.fmd.receivers.FmdDeviceAdminReceiver +import ru.officialdakari.fmd.services.FmdNotificationListenerService +import ru.officialdakari.fmd.ui.theme.FindMyDeviceTheme +import androidx.core.net.toUri + + +class PermissionsActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + FindMyDeviceTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + PermissionsScreen( + modifier = Modifier.padding(innerPadding), + context = this, // Pass context here + ) + } + } + } + } + + internal fun moveToMainActivity() { + val intent = Intent(this, MainActivity::class.java) + finish() + startActivity(intent) + } + + internal fun isSmsPermissionGranted(): Boolean { + Log.w("FMD", "Receive SMS Permission: ${ContextCompat.checkSelfPermission(this, Manifest.permission.RECEIVE_SMS)}"); + Log.w("FMD", "Send SMS Permission: ${ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS)}"); + return ContextCompat.checkSelfPermission(this, Manifest.permission.RECEIVE_SMS) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED + } + + internal fun isNotificationPermissionGranted(): Boolean { + return ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + } + + internal fun isNotificationAccessGranted(): Boolean { + val cn: ComponentName = ComponentName(this, FmdNotificationListenerService::class.java) + val flat = Settings.Secure.getString( + this.getContentResolver(), + "enabled_notification_listeners" + ) + val enabled = flat != null && flat.contains(cn.flattenToString()) + return enabled; + } + + internal fun isDeviceAdminGranted(): Boolean { + val deviceManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val componentName = ComponentName(this, FmdDeviceAdminReceiver::class.java) + return deviceManager.isAdminActive(componentName) + } + + internal fun isLocationAccessGranted(): Boolean { + return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED + } + + internal fun requestLocationAccess() { + if (!isLocationAccessGranted()) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_LOCATION_PERMISSION) + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), REQUEST_BG_LOCATION_PERMISSION) + } + checkPermissionsGranted() + } + + internal fun requestNotificationPermission() { + if (!isNotificationPermissionGranted()) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQUEST_NOTIFICATION_PERMISSION); + } + checkPermissionsGranted() + } + + internal fun requestSmsPermission() { + if (!isSmsPermissionGranted()) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECEIVE_SMS, Manifest.permission.SEND_SMS), REQUEST_SMS_PERMISSION) + } + checkPermissionsGranted() + } + + internal fun requestNotificationAccess() { + if (!isNotificationAccessGranted()) { + val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) + startActivity(intent) + } + checkPermissionsGranted() + } + + internal fun requestDeviceAdminPermission() { + if (!isDeviceAdminGranted()) { + val componentName = ComponentName(this, FmdDeviceAdminReceiver::class.java) + val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN) + intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, componentName) + intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, "FindMyDevice requires device admin permissions to lock it remotely.") + startActivity(intent) + } + checkPermissionsGranted() + } + + internal fun isOverlaysGranted(): Boolean { + return Settings.canDrawOverlays(this); + } + + internal fun requestOverlays() { + if (!isOverlaysGranted()) { + val intent = Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + "package:ru.officialdakari.fmd".toUri() + ) + startActivityForResult(intent, REQUEST_OVERLAYS_PERMISSION) + } + checkPermissionsGranted() + } + + internal fun checkPermissionsGranted() { + if (isSmsPermissionGranted() + && isNotificationAccessGranted() + && isDeviceAdminGranted() + && isLocationAccessGranted() + && isNotificationPermissionGranted() + && isOverlaysGranted()) { + moveToMainActivity() + } + } +} + +@Composable +fun PermissionsScreen(modifier: Modifier = Modifier, context: Context) { + var smsPermissionGranted by remember { mutableStateOf(false) } + var notificationAccessGranted by remember { mutableStateOf(false) } + var deviceAdminGranted by remember { mutableStateOf(false) } + var locationAccessGranted by remember { mutableStateOf(false) } + var notificationPermissionGranted by remember { mutableStateOf(false) } + var overlaysGranted by remember { mutableStateOf(false) } + + var ctx = context as PermissionsActivity + + LaunchedEffect(Unit) { + smsPermissionGranted = context.isSmsPermissionGranted() + notificationAccessGranted = context.isNotificationAccessGranted() + deviceAdminGranted = context.isDeviceAdminGranted() + locationAccessGranted = context.isLocationAccessGranted() + notificationPermissionGranted = context.isNotificationPermissionGranted() + overlaysGranted = context.isOverlaysGranted() + + Log.w("FMD", "SMS state: $smsPermissionGranted | Notification State: $notificationAccessGranted | Device Admin State: $deviceAdminGranted"); + + context.checkPermissionsGranted() + } + + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "FindMyDevice needs permissions below to function correctly.") + + Button( + onClick = { context.requestOverlays() }, + enabled = !overlaysGranted + ) { + Text(text = "Request display over other apps") + } + + Button( + onClick = { context.requestSmsPermission() }, + enabled = !smsPermissionGranted + ) { + Text(text = "Request SMS Permission") + } + + Button( + onClick = { context.requestLocationAccess() }, + enabled = !locationAccessGranted + ) { + Text(text = "Request Location Access") + } + + Button( + onClick = { context.requestNotificationAccess() }, + enabled = !notificationAccessGranted + ) { + Text(text = "Request Notification Listener") + } + + Button( + onClick = { context.requestNotificationPermission() }, + enabled = !notificationPermissionGranted + ) { + Text(text = "Request Post Notifications") + } + + Button( + onClick = { context.requestDeviceAdminPermission() }, + enabled = !deviceAdminGranted + ) { + Text(text = "Request Device Admin") + } + } +} + +@Preview(showBackground = true) +@Composable +fun PermissionsScreenPreview() { + FindMyDeviceTheme { + // Provide a mock context or use PreviewParameterProvider for preview + PermissionsScreen(context = LocalContext.current) + } +} + +private const val REQUEST_SMS_PERMISSION = 1001 +private const val REQUEST_LOCATION_PERMISSION = 1002 +private const val REQUEST_NOTIFICATION_PERMISSION = 1003 +private const val REQUEST_OVERLAYS_PERMISSION = 1004 +private const val REQUEST_BG_LOCATION_PERMISSION = 1005 \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/RingActivity.kt b/app/src/main/java/ru/officialdakari/fmd/RingActivity.kt new file mode 100644 index 0000000..9b9d121 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/RingActivity.kt @@ -0,0 +1,112 @@ +package ru.officialdakari.fmd + +import android.app.KeyguardManager +import android.content.Context +import android.media.AudioAttributes +import android.media.MediaPlayer +import android.media.RingtoneManager +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +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.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import ru.officialdakari.fmd.ui.theme.FindMyDeviceTheme + +class RingActivity : ComponentActivity() { + private var mediaPlayer: MediaPlayer? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD) + window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + enableEdgeToEdge() + playAlarmSound() // Play the alarm sound + setContent { + FindMyDeviceTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + RingScreen(Modifier.padding(innerPadding), stopRinging = { stopRinging() }) + } + } + } + } + + private fun playAlarmSound() { + val alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM) + mediaPlayer = MediaPlayer.create(this, alarmUri) + mediaPlayer?.apply { + reset() + setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ALARM) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + ) + setDataSource(this@RingActivity, alarmUri) + isLooping = true + prepareAsync() + } + + mediaPlayer?.setOnPreparedListener { + it.start() + } + } + + override fun onStop() { + super.onStop() + mediaPlayer?.stop() + mediaPlayer?.release() + mediaPlayer = null + } + + override fun onDestroy() { + super.onDestroy() + mediaPlayer?.stop() + mediaPlayer?.release() + mediaPlayer = null + } + + private fun stopRinging() { + mediaPlayer?.stop() + mediaPlayer?.release() + mediaPlayer = null + finish() + } +} + +@Composable +fun RingScreen(modifier: Modifier, stopRinging: () -> Unit) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (Utils.lockMessage != null) { + Text( + text = "This device was locked by FindMyDevice!", + fontSize = 5.em + ) + Text( + text = Utils.lockMessage + ) + } + Button(onClick = { stopRinging() }) { + Text(text = "Stop ringing") + } + } +} diff --git a/app/src/main/java/ru/officialdakari/fmd/Utils.kt b/app/src/main/java/ru/officialdakari/fmd/Utils.kt new file mode 100644 index 0000000..d4d3395 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/Utils.kt @@ -0,0 +1,71 @@ +package ru.officialdakari.fmd + +import android.app.admin.DevicePolicyManager +import android.content.ComponentName +import kotlin.jvm.java +import android.content.Context +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import ru.officialdakari.fmd.receivers.FmdDeviceAdminReceiver +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.security.MessageDigest + +class Utils { + companion object { + var lockMessage: String = "Locked by FMD"; + + fun lockDevice(context: Context) { + val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val componentName = ComponentName(context, FmdDeviceAdminReceiver::class.java) + + if (dpm.isAdminActive(componentName)) { + dpm.lockNow() + } else { + // Handle the case where the device admin is not active + } + } + + fun sha256(input: String): String { + val bytes = input.toByteArray() + val md = MessageDigest.getInstance("SHA-256") + val digest = md.digest(bytes) + return digest.fold("") { str, it -> str + "%02x".format(it) } + } + + fun checkPin(context: Context, pin: String): Boolean { + val ctx: Context = context.createDeviceProtectedStorageContext() + val file = File("${ctx.filesDir.path}/pin.txt") + if (!file.exists()) return false; + val reader = FileInputStream(file) + val hash = reader.readAllBytes().toString(Charsets.US_ASCII) + return hash == sha256(pin) + } + + fun getAllowedApps(context: Context): Array { + val file = File("${context.filesDir.path}/apps.txt") + if (!file.exists()) return arrayOf() + val reader = FileInputStream(file) + val apps = reader.readAllBytes().toString(Charsets.UTF_8).split("\n") + return apps.toTypedArray() + } + + fun setAllowedApps(context: Context, apps: Array) { + val file = File("${context.filesDir.path}/apps.txt") + val writer = FileOutputStream(file) + val buff = apps.joinToString("\n").toByteArray(Charsets.UTF_8) + writer.write(buff) + } + + fun sleep(ms: Long, onComplete: () -> Unit) { + GlobalScope.launch(Dispatchers.Main) { + delay(ms) + onComplete() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/dialogs/NewPinDialog.kt b/app/src/main/java/ru/officialdakari/fmd/dialogs/NewPinDialog.kt new file mode 100644 index 0000000..aa9f9e3 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/dialogs/NewPinDialog.kt @@ -0,0 +1,65 @@ +package ru.officialdakari.fmd.dialogs + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType + +@Composable +fun NewPinDialog( + onDismissRequest: () -> Unit, + onConfirm: (String) -> Unit +) { + var password by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = "New PIN") + }, + text = { + Column { + Text(text = "Please enter new PIN") + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + label = @Composable { Text(text = "New PIN") } + ) + } + }, + confirmButton = { + Button( + onClick = { + onConfirm(password) + onDismissRequest() + }) @Composable { + Text("OK") + } + }, + dismissButton = { + Button( + onClick = onDismissRequest + ) @Composable { + Text("Cancel") + } + } + ) +} diff --git a/app/src/main/java/ru/officialdakari/fmd/dialogs/SelectAppsDialog.kt b/app/src/main/java/ru/officialdakari/fmd/dialogs/SelectAppsDialog.kt new file mode 100644 index 0000000..fc069c2 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/dialogs/SelectAppsDialog.kt @@ -0,0 +1,112 @@ +package ru.officialdakari.fmd.dialogs + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import androidx.compose.foundation.layout.Arrangement + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType + +fun getInstalledApps(context: Context): List> { + val packageManager: PackageManager = context.packageManager + val packages: List = packageManager.getInstalledPackages(PackageManager.GET_META_DATA) + + val apps = mutableListOf>() + for (packageInfo in packages) { + if (packageInfo?.applicationInfo != null && (packageInfo?.applicationInfo?.flags!! and ApplicationInfo.FLAG_SYSTEM == 0)) { + val appName = packageManager.getApplicationLabel(packageInfo.applicationInfo as ApplicationInfo).toString() + val packageName = packageInfo.packageName + apps.add(Pair(packageName, appName)) + } + } + return apps +} + + +@Composable +fun SelectAppsDialog( + context: Context, + initialState: List>, + onDismissRequest: () -> Unit, + onConfirm: (List>) -> Unit +) { + val items = getInstalledApps(context) + val state by remember { mutableStateOf(mutableListOf>()) } + for (pkg in initialState) { + state.add(Pair(pkg.first, true)) + } + for (pkg in items) { + if (state.find({ it.first == pkg.first }) == null) + state.add(Pair(pkg.first, false)) + } + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(text = "Apps") }, + text = { + Column { + Text(text = "Select apps") + Spacer(modifier = Modifier.height(8.dp)) + LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp)) { + items(state.size) { index -> + val currentItem = state[index] + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = currentItem.second, + onCheckedChange = { isChecked -> + state[index] = currentItem.copy(second = isChecked) + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = items.find({ currentItem.first == it.first })!!.second) + } + } + } + } + }, + confirmButton = { + Button( + onClick = { + onConfirm(state.toList()) + onDismissRequest() + } + ) { + Text("Save") + } + }, + dismissButton = { + Button(onClick = onDismissRequest) { + Text("Cancel") + } + } + ) +} diff --git a/app/src/main/java/ru/officialdakari/fmd/executors/ThreadPerTaskExecutor.kt b/app/src/main/java/ru/officialdakari/fmd/executors/ThreadPerTaskExecutor.kt new file mode 100644 index 0000000..71db75a --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/executors/ThreadPerTaskExecutor.kt @@ -0,0 +1,9 @@ +package ru.officialdakari.fmd.executors + +import java.util.concurrent.Executor + +class ThreadPerTaskExecutor: Executor { + override fun execute(r: Runnable?) { + Thread(r).start() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/receivers/FmdBootReceiver.kt b/app/src/main/java/ru/officialdakari/fmd/receivers/FmdBootReceiver.kt new file mode 100644 index 0000000..46b013e --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/receivers/FmdBootReceiver.kt @@ -0,0 +1,15 @@ +package ru.officialdakari.fmd.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import ru.officialdakari.fmd.services.FmdNotificationListenerService + +class FmdBootReceiver: BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/receivers/FmdDeviceAdminReceiver.kt b/app/src/main/java/ru/officialdakari/fmd/receivers/FmdDeviceAdminReceiver.kt new file mode 100644 index 0000000..4899c10 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/receivers/FmdDeviceAdminReceiver.kt @@ -0,0 +1,11 @@ +package ru.officialdakari.fmd.receivers + +import android.app.admin.DeviceAdminReceiver +import android.content.Context +import android.content.Intent + +class FmdDeviceAdminReceiver: DeviceAdminReceiver() { + override fun onEnabled(context: Context, intent: Intent) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/receivers/SmsReceiver.kt b/app/src/main/java/ru/officialdakari/fmd/receivers/SmsReceiver.kt new file mode 100644 index 0000000..bb76282 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/receivers/SmsReceiver.kt @@ -0,0 +1,76 @@ +package ru.officialdakari.fmd.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.telephony.SmsManager +import android.telephony.SmsMessage +import android.util.Log +import ru.officialdakari.fmd.CommandHandler +import ru.officialdakari.fmd.LockActivity +import ru.officialdakari.fmd.RingActivity + + +class SmsReceiver: BroadcastReceiver() { + private val preferences: SharedPreferences? = null + + var lockIntent: Intent? = null + var ringIntent: Intent? = null + + fun initIntents(ctx: Context) { + ringIntent = Intent(ctx, RingActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + addFlags(Intent.FLAG_FROM_BACKGROUND) + } + lockIntent = Intent(ctx, LockActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + addFlags(Intent.FLAG_FROM_BACKGROUND) + } + } + + override fun onReceive(context: Context, intent: Intent) { + if (intent.getAction() == "android.provider.Telephony.SMS_RECEIVED") { + if (lockIntent == null || ringIntent == null) { + initIntents(context) + } + val bundle = intent.getExtras() + var msgs: Array? = null + var msg_from: String? + if (bundle != null) { + try { + val pdus = bundle.get("pdus") as Array? + msgs = kotlin.arrayOfNulls(pdus!!.size) + for (i in msgs.indices) { + msgs[i] = SmsMessage.createFromPdu(pdus[i] as ByteArray?) + msg_from = msgs[i]?.getOriginatingAddress() + val msgBody: String? = msgs[i]?.getMessageBody() + if (msg_from != null && msgBody != null) { + if (msgBody.startsWith("fma ")) onFmaCommand(context, msg_from, msgBody); + } + } + } catch (e: Exception) { + Log.d("Exception caught",e.stackTraceToString()); + } + } + } + } + + + + private fun onFmaCommand(context: Context, msgFrom: String, msgBody: String) { + Log.w("FMD", "SMS from $msgFrom: $msgBody") + val args = msgBody.split(" ") + if (!args[0].contentEquals("fma")) return; + val handler = CommandHandler(context, lockIntent!!, ringIntent!!) + val smsManager = SmsManager.getDefault() + handler.handleCommand(msgBody, { repl -> + if (repl.length != 0) { + smsManager.sendTextMessage(msgFrom, null, repl, null, null) + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/services/FmdNotificationListenerService.kt b/app/src/main/java/ru/officialdakari/fmd/services/FmdNotificationListenerService.kt new file mode 100644 index 0000000..e67c247 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/services/FmdNotificationListenerService.kt @@ -0,0 +1,102 @@ +package ru.officialdakari.fmd.services + +import android.app.Notification +import android.app.NotificationChannel +import android.content.Intent +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.RemoteInput +import android.os.Bundle +import android.util.Log +import androidx.core.app.NotificationCompat +import ru.officialdakari.fmd.CommandHandler +import ru.officialdakari.fmd.LockActivity +import ru.officialdakari.fmd.RingActivity +import ru.officialdakari.fmd.Utils +import kotlin.jvm.java + +class FmdNotificationListenerService: NotificationListenerService() { + lateinit var ringIntent: Intent + lateinit var lockIntent: Intent + + override fun onCreate() { + super.onCreate() + startForegroundService() + ringIntent = Intent(this, RingActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + addFlags(Intent.FLAG_FROM_BACKGROUND) + } + lockIntent = Intent(this, LockActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + addFlags(Intent.FLAG_FROM_BACKGROUND) + } + } + + private fun startForegroundService() { + val channelId = "notification_listener_service" + val channel = NotificationChannel( + channelId, + "FMD Service", + NotificationManager.IMPORTANCE_LOW + ) + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + + val notification: Notification = NotificationCompat.Builder(this, channelId) + .setContentTitle("Service Running") + .setContentText("FMD is active") + .build() + + startForeground(1, notification) + } + + override fun onNotificationPosted(sbn: StatusBarNotification?) { + val notificationText = sbn?.notification?.extras?.getCharSequence("android.text")?.toString() + val handler = CommandHandler(this, lockIntent, ringIntent) + + if (notificationText == null) return; + + if (!Utils.getAllowedApps(this).contains(sbn.packageName)) return; + + if (notificationText.startsWith("fma") && sbn.notification != null) { + handler.handleCommand(notificationText, { res -> + var replyAction = getReplyAction(sbn.notification) + if (!res.contentEquals("") && replyAction != null) { + sendReply(sbn, replyAction, res) + } + }) + } + + } + + private fun getReplyAction(notification: Notification): NotificationCompat.Action? { + // Iterate through actions and look for RemoteInput + notification.actions?.forEach { action -> + if (action.remoteInputs != null) { + return action as NotificationCompat.Action + } + } + return null + } + + private fun sendReply(sbn: StatusBarNotification, action: NotificationCompat.Action, replyText: String) { + val remoteInput = action.remoteInputs?.firstOrNull() + if (action.remoteInputs != null && remoteInput != null) { + val remoteInputs = action.remoteInputs as Array + val replyIntent = Intent().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val replyBundle = Bundle() + replyBundle.putCharSequence(remoteInput.resultKey, replyText) + RemoteInput.addResultsToIntent(remoteInputs, replyIntent, replyBundle) + + try { + action.actionIntent.send(this, 0, replyIntent) + } catch (e: PendingIntent.CanceledException) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/ui/theme/Color.kt b/app/src/main/java/ru/officialdakari/fmd/ui/theme/Color.kt new file mode 100644 index 0000000..b15151a --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package ru.officialdakari.fmd.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) \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/ui/theme/Theme.kt b/app/src/main/java/ru/officialdakari/fmd/ui/theme/Theme.kt new file mode 100644 index 0000000..e8270ed --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package ru.officialdakari.fmd.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.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 FindMyDeviceTheme( + 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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/officialdakari/fmd/ui/theme/Type.kt b/app/src/main/java/ru/officialdakari/fmd/ui/theme/Type.kt new file mode 100644 index 0000000..eaeda92 --- /dev/null +++ b/app/src/main/java/ru/officialdakari/fmd/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package ru.officialdakari.fmd.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 + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_lock.xml b/app/src/main/res/layout/activity_lock.xml new file mode 100644 index 0000000..4a1ab1b --- /dev/null +++ b/app/src/main/res/layout/activity_lock.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..e439e7f --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + FindMyDevice + PermissionsActivity + RingActivity + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..27cbc26 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +