final things

This commit is contained in:
OfficialDakari 2025-06-03 11:45:43 +05:00
parent e1d79d2faa
commit 7bc5f72309
12 changed files with 330 additions and 9 deletions

View File

@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-29T12:15:47.909492410Z">
<DropdownSelection timestamp="2025-06-01T19:10:27.800446242Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=c3a188c5" />

View File

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings" defaultProject="true" />
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -53,6 +53,7 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.socket.socket.io.client)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@ -15,6 +15,7 @@
android:name="android.hardware.location.network"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
@ -28,6 +29,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application
android:allowBackup="true"
@ -77,18 +79,34 @@
</intent-filter>
</service>
<service
android:name=".services.WebSocketService"
android:directBootAware="true"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="specialUse">
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</service>
<receiver
android:name=".receivers.SmsReceiver"
android:exported="true"
android:directBootAware="true">
android:directBootAware="true"
android:permission="android.permission.BROADCAST_SMS">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".receivers.FmdBootReceiver"
android:directBootAware="true"
android:exported="false">
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<!-- Handle direct boot actions -->

View File

@ -37,6 +37,8 @@ class CommandHandler(val context: Context, val lockIntent: Intent, val ringInten
reply("https://www.openstreetmap.org/#map=18/${loc.latitude}/${loc.longitude}")
}
})
} else if (args[2].contentEquals("battery")) {
reply("${Utils.getBatteryLevel(context)}% ${if (Utils.getBatteryCharging(context)) "charging" else "discharging"}")
}
}
}

View File

@ -1,6 +1,7 @@
package ru.officialdakari.fmd
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
@ -27,6 +28,8 @@ 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.dialogs.ServerSetupDialog
import ru.officialdakari.fmd.services.WebSocketService
import ru.officialdakari.fmd.ui.theme.FindMyDeviceTheme
import java.io.File
import java.io.FileInputStream
@ -48,10 +51,13 @@ class MainActivity : ComponentActivity() {
) {
SetPinButton(context = this@MainActivity)
SetAppsButton(context = this@MainActivity)
ServerSetupDialog(context = this@MainActivity)
}
}
}
}
val intent = Intent(this, WebSocketService::class.java)
startService(intent)
}
}
@ -86,6 +92,37 @@ fun SetPinButton(context: Context) {
}
}
@Composable
fun ServerSetupDialog(context: Context) {
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
ServerSetupDialog(
context = context,
initialState = Utils.getServerData(context),
onDismissRequest = {
showDialog = false
},
onConfirm = {
Utils.setServerData(context, it.first, it.second)
}
)
}
Text(
text = "Setup controlling FMA via website.",
modifier = Modifier.padding(10.dp)
)
Button(
onClick = {
showDialog = true
},
modifier = Modifier.fillMaxWidth().padding(10.dp)
) {
Text("Setup server")
}
}
@Composable
fun SetAppsButton(context: Context) {
var showDialog by remember { mutableStateOf(false) }

View File

@ -4,6 +4,7 @@ import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import kotlin.jvm.java
import android.content.Context
import android.os.BatteryManager
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@ -12,8 +13,10 @@ import kotlinx.coroutines.launch
import ru.officialdakari.fmd.receivers.FmdDeviceAdminReceiver
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.security.MessageDigest
import kotlin.random.Random
class Utils {
companion object {
@ -61,11 +64,50 @@ class Utils {
writer.write(buff)
}
fun setServerData(context: Context, url: String, devKey: String) {
val ctx: Context = context.createDeviceProtectedStorageContext()
val writer = ctx.openFileOutput("server.txt", Context.MODE_PRIVATE)
val buff = "$url\n$devKey".toByteArray(Charsets.UTF_8)
writer.write(buff)
}
fun getServerData(context: Context): Pair<String, String>? {
try {
val ctx: Context = context.createDeviceProtectedStorageContext()
val reader = ctx.openFileInput("server.txt")
val data = reader.readAllBytes().toString(Charsets.UTF_8).split("\n")
return Pair(data[0], data[1])
} catch (ex: FileNotFoundException) {
Log.w("FMD", ex.stackTraceToString())
return null
}
}
fun sleep(ms: Long, onComplete: () -> Unit) {
GlobalScope.launch(Dispatchers.Main) {
delay(ms)
onComplete()
}
}
fun getBatteryLevel(context: Context): Int {
val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
fun getBatteryCharging(context: Context): Boolean {
val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return batteryManager.isCharging
}
fun randomDevKey(): String {
var devKey = ""
for (i in 0..18) {
devKey += ALPHABET[Random.nextInt(0, ALPHABET.length)];
}
return devKey
}
}
}
}
private const val ALPHABET: String = "ABCDEF1234567890"

View File

@ -0,0 +1,91 @@
package ru.officialdakari.fmd.dialogs
import android.content.Context
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.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import ru.officialdakari.fmd.Utils
@Composable
fun ServerSetupDialog(
context: Context,
initialState: Pair<String, String>?,
onDismissRequest: () -> Unit,
onConfirm: (Pair<String, String>) -> Unit
) {
var serverUrl by remember { mutableStateOf(if (initialState != null) initialState!!.first else "https://fma.officialdakari.ru/") }
var devKey by remember { mutableStateOf(if (initialState != null) initialState!!.second else Utils.randomDevKey() ) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = "Server connection") },
text = {
Column {
Text(text = "Choose FMA Server instance and device key.")
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = serverUrl,
onValueChange = { it ->
serverUrl = it
},
label = {
Text("Server URL")
}
)
OutlinedTextField(
value = devKey,
onValueChange = {},
readOnly = true,
label = {
Text("Device key")
}
)
Text(text = "Disclaimer: Device key grants full FMA access, treat it like a password.")
OutlinedButton(
onClick = {
devKey = Utils.randomDevKey()
}
) {
Text("Random key")
}
}
},
confirmButton = {
Button(
onClick = {
onConfirm(Pair(serverUrl, devKey))
onDismissRequest()
}
) {
Text("Save")
}
},
dismissButton = {
Button(onClick = onDismissRequest) {
Text("Cancel")
}
}
)
}

View File

@ -3,13 +3,16 @@ package ru.officialdakari.fmd.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import ru.officialdakari.fmd.services.FmdNotificationListenerService
import ru.officialdakari.fmd.services.WebSocketService
class FmdBootReceiver: BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
Log.w("FMD BootReceiver", "Received ${intent.action}")
if (intent.action == Intent.ACTION_LOCKED_BOOT_COMPLETED) {
val intent = Intent(ctx, WebSocketService::class.java)
ctx.startService(intent)
}
}
}

View File

@ -53,7 +53,7 @@ class SmsReceiver: BroadcastReceiver() {
}
}
} catch (e: Exception) {
Log.d("Exception caught",e.stackTraceToString());
Log.d("Exception caught FMD",e.stackTraceToString());
}
}
}

View File

@ -0,0 +1,121 @@
package ru.officialdakari.fmd.services
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import ru.officialdakari.fmd.LockActivity
import ru.officialdakari.fmd.RingActivity
import ru.officialdakari.fmd.Utils
import io.socket.client.IO
import io.socket.client.Socket
import io.socket.engineio.client.transports.PollingXHR
import io.socket.engineio.client.transports.WebSocket
import ru.officialdakari.fmd.LocationUtils
class WebSocketService: Service() {
var ringIntent: Intent? = null
var lockIntent: Intent? = null
var loc: LocationUtils = LocationUtils(this)
var mSocket: Socket? = null
private fun initIntents(context: Context) {
ringIntent = Intent(context, RingActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
addFlags(Intent.FLAG_FROM_BACKGROUND)
}
lockIntent = Intent(context, LockActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
addFlags(Intent.FLAG_FROM_BACKGROUND)
}
}
private fun startWebsocket(context: Context) {
try {
val connData = Utils.getServerData(context)
Log.w("FMD Service", "Connecting IO")
if (connData == null) {
Log.w("FMD Service", "connData == null, returning")
return
}
val opts = IO.Options()
opts.transports = arrayOf(WebSocket.NAME, PollingXHR.NAME)
mSocket = IO.socket(connData.first, opts)
Log.w("FMD Service", "Connecting to ${connData.first}")
mSocket!!.on(Socket.EVENT_CONNECT) {
Log.d("FMD Service", "Connected to server")
mSocket!!.emit("auth", connData.second, "${Build.BRAND} ${Build.MODEL}")
}.on(Socket.EVENT_CONNECT_ERROR) { args ->
Log.e("FMD Service", "Connection Error: ${(args[0] as Throwable).stackTraceToString()}")
}.on(Socket.EVENT_DISCONNECT) {
Log.d("FMD Service", "Disconnected from server")
Utils.sleep(15000) {
startWebsocket(context)
}
}.on("ring") {
ring(context)
}.on("lock") {
val message = it[0] as String
lock(context, message)
}.on("locate") {
loc.getLocation({
mSocket!!.emit("location", it.latitude, it.longitude)
})
}
mSocket!!.connect()
} catch (ex: Throwable) {
ex.printStackTrace()
Toast.makeText(this, "Failed to connect to server, see logcat", Toast.LENGTH_LONG).show()
}
}
private fun ring(context: Context) {
if (ringIntent != null) {
context.startActivity(ringIntent)
}
}
private fun lock(context: Context, message: String) {
if (lockIntent != null) {
Utils.lockMessage = message
Utils.lockDevice(context)
Utils.sleep(1000) {
startActivity(lockIntent)
}
}
}
override fun onCreate() {
super.onCreate()
if (!loc.isStarted()) loc.start()
if (lockIntent == null || ringIntent == null) {
initIntents(this)
}
startWebsocket(this)
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
// override fun onReceive(context: Context, intent: Intent) {
// if (intent.action == "android.intent.action.LOCKED_BOOT_COMPLETED"
// || intent.action == "android.intent.action.BOOT_COMPLETED") {
// if (lockIntent == null || ringIntent == null) {
// initIntents(context)
// }
// startWebsocket(context)
// }
// }
}

View File

@ -12,6 +12,8 @@ appcompat = "1.6.1"
material = "1.10.0"
activity = "1.10.1"
constraintlayout = "2.1.4"
socketIoClient = "0.6.0"
socketIoClientVersion = "2.1.2"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -32,6 +34,8 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
socket-io-client = { group = "com.github.nkzawa", name = "socket.io-client", version.ref = "socketIoClient" }
socket-socket-io-client = { group = "io.socket", name = "socket.io-client", version.ref = "socketIoClientVersion" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }