diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ae75f4b..a01cf1a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -16,6 +16,9 @@
+
+
+
@@ -126,6 +129,24 @@
android:name=".CallForegroundService"
android:foregroundServiceType="microphone|mediaProjection"
android:exported="false" />
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/ru/komet/app/FkmChannel.kt b/android/app/src/main/kotlin/ru/komet/app/FkmChannel.kt
new file mode 100644
index 0000000..d5f0e7c
--- /dev/null
+++ b/android/app/src/main/kotlin/ru/komet/app/FkmChannel.kt
@@ -0,0 +1,143 @@
+package ru.komet.app
+
+import android.Manifest
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.os.PowerManager
+import android.provider.Settings
+import android.util.Log
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationManagerCompat
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import java.util.concurrent.Executors
+
+object FkmChannel {
+ private const val NAME = "ru.komet.app/fkm"
+ const val NOTIF_PERMS_REQUEST = 7713
+
+ private val main = Handler(Looper.getMainLooper())
+ private val worker = Executors.newSingleThreadExecutor()
+
+ private var channel: MethodChannel? = null
+ private var permResult: MethodChannel.Result? = null
+
+ fun attach(engine: FlutterEngine, activity: Activity) {
+ val ctx = activity.applicationContext
+ FkmState.restore(ctx)
+ val ch = MethodChannel(engine.dartExecutor.binaryMessenger, NAME)
+ channel = ch
+ ch.setMethodCallHandler { call, result ->
+ when (call.method) {
+ "isEnabled" -> result.success(FkmState.enabled)
+
+ "setEnabled" -> {
+ val enabled = call.argument("enabled") ?: false
+ FkmState.applyEnabled(ctx, enabled)
+ if (enabled) FkmService.start(ctx) else FkmService.stop(ctx)
+ result.success(null)
+ }
+
+ "setConnected" -> {
+ FkmState.connected = call.argument("connected") ?: false
+ FkmService.refresh(ctx)
+ result.success(null)
+ }
+
+ "showMessage" -> result.success(deliver(ctx, call, "showMessage"))
+
+ "showCall" -> result.success(deliver(ctx, call, "showCall"))
+
+ "hasNotificationPermission" ->
+ result.success(NotificationManagerCompat.from(ctx).areNotificationsEnabled())
+
+ "requestNotificationPermission" -> {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
+ NotificationManagerCompat.from(ctx).areNotificationsEnabled()
+ ) {
+ result.success(
+ NotificationManagerCompat.from(ctx).areNotificationsEnabled(),
+ )
+ } else {
+ permResult?.success(false)
+ permResult = result
+ ActivityCompat.requestPermissions(
+ activity,
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ NOTIF_PERMS_REQUEST,
+ )
+ }
+ }
+
+ "isIgnoringBatteryOptimizations" -> {
+ val power = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
+ result.success(power.isIgnoringBatteryOptimizations(ctx.packageName))
+ }
+
+ "requestIgnoreBatteryOptimizations" -> {
+ try {
+ activity.startActivity(
+ Intent(
+ Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
+ Uri.parse("package:${ctx.packageName}"),
+ ),
+ )
+ } catch (e: Exception) {
+ Log.w("Fkm", "battery settings failed: ${e.message}")
+ }
+ result.success(null)
+ }
+
+ else -> result.notImplemented()
+ }
+ }
+ }
+
+ // Отрисовка тянет аватарку по сети — только не на главном потоке.
+ private fun deliver(ctx: Context, call: MethodCall, tag: String): Boolean {
+ val data = call.argument