Extract settings and harden geofence automation
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<application
|
||||
android:label="ignis_app"
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.BackoffPolicy
|
||||
@@ -22,6 +23,7 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
data class StoredGeofenceConfig(
|
||||
val homeId: String,
|
||||
val homeName: String,
|
||||
val baseUrl: String,
|
||||
val apiKey: String,
|
||||
val latitude: Double,
|
||||
@@ -45,6 +47,7 @@ class GeofenceNativeStore(context: Context) {
|
||||
val json = JSONObject(raw)
|
||||
StoredGeofenceConfig(
|
||||
homeId = json.getString("homeId"),
|
||||
homeName = json.optString("homeName"),
|
||||
baseUrl = json.getString("baseUrl"),
|
||||
apiKey = json.getString("apiKey"),
|
||||
latitude = json.getDouble("latitude"),
|
||||
@@ -59,6 +62,7 @@ class GeofenceNativeStore(context: Context) {
|
||||
val json =
|
||||
JSONObject()
|
||||
.put("homeId", config.homeId)
|
||||
.put("homeName", config.homeName)
|
||||
.put("baseUrl", config.baseUrl)
|
||||
.put("apiKey", config.apiKey)
|
||||
.put("latitude", config.latitude)
|
||||
@@ -110,8 +114,10 @@ object GeofenceAutomationManager {
|
||||
val client = LocationServices.getGeofencingClient(context)
|
||||
val pendingIntent = buildPendingIntent(context)
|
||||
|
||||
client.removeGeofences(pendingIntent).addOnCompleteListener {
|
||||
registerGeofence(context, client, pendingIntent, config, onComplete)
|
||||
seedPresenceState(context, config) {
|
||||
client.removeGeofences(pendingIntent).addOnCompleteListener {
|
||||
registerGeofence(context, client, pendingIntent, config, onComplete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,12 +146,18 @@ object GeofenceAutomationManager {
|
||||
if (requestId != geofenceRequestId) return
|
||||
|
||||
val store = GeofenceNativeStore(context)
|
||||
if (store.getState() != GeofencePresenceState.INSIDE) {
|
||||
return
|
||||
when (store.getState()) {
|
||||
GeofencePresenceState.OUTSIDE,
|
||||
GeofencePresenceState.TRIGGERED,
|
||||
-> return
|
||||
GeofencePresenceState.UNKNOWN,
|
||||
GeofencePresenceState.INSIDE,
|
||||
-> Unit
|
||||
}
|
||||
|
||||
val config = store.loadConfig() ?: return
|
||||
store.setState(GeofencePresenceState.OUTSIDE)
|
||||
scheduleExitWorker(context, store.loadConfig()?.homeId ?: return)
|
||||
scheduleExitWorker(context, config.homeId)
|
||||
}
|
||||
|
||||
fun markTriggered(context: Context) {
|
||||
@@ -181,6 +193,49 @@ object GeofenceAutomationManager {
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun seedPresenceState(
|
||||
context: Context,
|
||||
config: StoredGeofenceConfig,
|
||||
onComplete: () -> Unit,
|
||||
) {
|
||||
if (!hasRequiredLocationPermission(context)) {
|
||||
GeofenceNativeStore(context).setState(GeofencePresenceState.UNKNOWN)
|
||||
onComplete()
|
||||
return
|
||||
}
|
||||
|
||||
LocationServices.getFusedLocationProviderClient(context).lastLocation
|
||||
.addOnSuccessListener { location ->
|
||||
val state =
|
||||
when {
|
||||
location == null -> GeofencePresenceState.UNKNOWN
|
||||
isInsideFence(location, config) -> GeofencePresenceState.INSIDE
|
||||
else -> GeofencePresenceState.OUTSIDE
|
||||
}
|
||||
GeofenceNativeStore(context).setState(state)
|
||||
onComplete()
|
||||
}.addOnFailureListener {
|
||||
GeofenceNativeStore(context).setState(GeofencePresenceState.UNKNOWN)
|
||||
onComplete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInsideFence(
|
||||
location: Location,
|
||||
config: StoredGeofenceConfig,
|
||||
): Boolean {
|
||||
val distance = FloatArray(1)
|
||||
Location.distanceBetween(
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
config.latitude,
|
||||
config.longitude,
|
||||
distance,
|
||||
)
|
||||
return distance[0] <= config.radiusMeters
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun registerGeofence(
|
||||
context: Context,
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package ru.akokos.ignis_app
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
object GeofenceAutomationNotifier {
|
||||
private const val channelId = "ignis_geofence_automation"
|
||||
private const val channelName = "Geofence automation"
|
||||
private const val notificationId = 4101
|
||||
|
||||
fun showExitProcessed(
|
||||
context: Context,
|
||||
homeName: String?,
|
||||
turnedOffGroups: Int,
|
||||
) {
|
||||
val title = if (turnedOffGroups > 0) "Ignis выключил свет" else "Ignis проверил свет"
|
||||
val body =
|
||||
if (turnedOffGroups > 0) {
|
||||
buildString {
|
||||
append("Geofence подтвердил уход из дома")
|
||||
homeName?.takeIf { it.isNotBlank() }?.let { append(" \"$it\"") }
|
||||
append(" и выключил ")
|
||||
append(turnedOffGroups)
|
||||
append(if (turnedOffGroups == 1) " группу." else " групп(ы).")
|
||||
}
|
||||
} else {
|
||||
buildString {
|
||||
append("Geofence подтвердил уход из дома")
|
||||
homeName?.takeIf { it.isNotBlank() }?.let { append(" \"$it\"") }
|
||||
append(", но включённых групп не нашёл.")
|
||||
}
|
||||
}
|
||||
|
||||
show(context, title, body)
|
||||
}
|
||||
|
||||
private fun show(
|
||||
context: Context,
|
||||
title: String,
|
||||
body: String,
|
||||
) {
|
||||
if (!canNotify(context)) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureChannel(context)
|
||||
|
||||
val launchIntent =
|
||||
context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
|
||||
val notification =
|
||||
NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
||||
.setAutoCancel(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(
|
||||
launchIntent?.let {
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
it,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
|
||||
NotificationManagerCompat.from(context).notify(notificationId, notification)
|
||||
}
|
||||
|
||||
private fun canNotify(context: Context): Boolean {
|
||||
val permissionGranted =
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
return permissionGranted && NotificationManagerCompat.from(context).areNotificationsEnabled()
|
||||
}
|
||||
|
||||
private fun ensureChannel(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return
|
||||
}
|
||||
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
).apply {
|
||||
description = "Уведомления о срабатывании geofence-автоматизации Ignis"
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,11 @@ class GeofenceExitWorker(
|
||||
}
|
||||
|
||||
GeofenceAutomationManager.markTriggered(applicationContext)
|
||||
GeofenceAutomationNotifier.showExitProcessed(
|
||||
context = applicationContext,
|
||||
homeName = config.homeName,
|
||||
turnedOffGroups = activeGroupIds.size,
|
||||
)
|
||||
Result.success()
|
||||
}
|
||||
.getOrElse { Result.retry() }
|
||||
|
||||
@@ -15,6 +15,7 @@ class MainActivity : FlutterActivity() {
|
||||
when (call.method) {
|
||||
"armGeofence" -> {
|
||||
val homeId = call.argument<String>("homeId")
|
||||
val homeName = call.argument<String>("homeName")
|
||||
val baseUrl = call.argument<String>("baseUrl")
|
||||
val apiKey = call.argument<String>("apiKey")
|
||||
val latitude = call.argument<Double>("latitude")
|
||||
@@ -42,6 +43,7 @@ class MainActivity : FlutterActivity() {
|
||||
config =
|
||||
StoredGeofenceConfig(
|
||||
homeId = homeId,
|
||||
homeName = homeName ?: "",
|
||||
baseUrl = baseUrl,
|
||||
apiKey = apiKey,
|
||||
latitude = latitude,
|
||||
|
||||
Reference in New Issue
Block a user