final things
This commit is contained in:
parent
e1d79d2faa
commit
7bc5f72309
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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"}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ class SmsReceiver: BroadcastReceiver() {
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d("Exception caught",e.stackTraceToString());
|
||||
Log.d("Exception caught FMD",e.stackTraceToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Reference in New Issue