diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml
new file mode 100644
index 0000000..3017132
--- /dev/null
+++ b/.github/actions/setup-rust/action.yml
@@ -0,0 +1,24 @@
+name: Setup Rust
+description: >
+ Install the Rust toolchain with platform targets and a build cache, so
+ cargokit can compile the native libraries during the Flutter build. kolibri
+ comes from pub.dev and builds inside the pub cache, so only the in-repo crate
+ is listed here; the shared cargo registry and git checkouts are cached anyway.
+
+inputs:
+ targets:
+ description: Comma-separated rustup targets for the platform being built.
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: ${{ inputs.targets }}
+
+ - name: Cache Rust build
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: native/komet_crypto/rust
diff --git a/.github/workflows/build-android-fcm.yml b/.github/workflows/build-android-fcm.yml
index fe613a2..3fef1cb 100644
--- a/.github/workflows/build-android-fcm.yml
+++ b/.github/workflows/build-android-fcm.yml
@@ -41,6 +41,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Get dependencies
run: flutter pub get
diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml
index c15b65f..390a215 100644
--- a/.github/workflows/build-android.yml
+++ b/.github/workflows/build-android.yml
@@ -41,6 +41,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Get dependencies
run: flutter pub get
diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml
index b216815..17c3560 100644
--- a/.github/workflows/build-ios.yml
+++ b/.github/workflows/build-ios.yml
@@ -37,6 +37,11 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios
+
- name: Get dependencies
run: flutter pub get
@@ -74,6 +79,9 @@ jobs:
get-task-allow
+ application-identifierru.komet.app
+ keychain-access-groups
+ ru.komet.app
PLIST
diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml
index da87ce4..d47216f 100644
--- a/.github/workflows/build-linux.yml
+++ b/.github/workflows/build-linux.yml
@@ -42,6 +42,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev libjsoncpp-dev libmpv-dev mpv libopus-dev libogg-dev
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-unknown-linux-gnu
+
- name: Get dependencies
run: flutter pub get
diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml
index 760e1e3..0c07ea5 100644
--- a/.github/workflows/build-macos.yml
+++ b/.github/workflows/build-macos.yml
@@ -37,6 +37,11 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-darwin,x86_64-apple-darwin
+
- name: Install create-dmg
run: brew install create-dmg
diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml
index 14f93d8..ede96ba 100644
--- a/.github/workflows/build-windows.yml
+++ b/.github/workflows/build-windows.yml
@@ -37,6 +37,11 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-pc-windows-msvc
+
- name: Get dependencies
run: flutter pub get
diff --git a/.github/workflows/flutter-dev.yml b/.github/workflows/flutter-dev.yml
index 6545875..92e7052 100644
--- a/.github/workflows/flutter-dev.yml
+++ b/.github/workflows/flutter-dev.yml
@@ -29,6 +29,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Install dependencies
run: flutter pub get
@@ -66,3 +71,32 @@ jobs:
- name: Build Android APK
run: flutter build apk --release --flavor komet
+
+ build-ios:
+ runs-on: macos-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ with:
+ submodules: recursive
+
+ - name: Set up Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ flutter-version: '3.44.3'
+ channel: 'stable'
+ cache: true
+
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios
+
+ - name: Install dependencies
+ run: flutter pub get
+
+ - name: Build iOS (no codesign)
+ run: |
+ flutter config --no-enable-swift-package-manager
+ flutter build ios --release --no-codesign
diff --git a/.github/workflows/flutter-main.yml b/.github/workflows/flutter-main.yml
index a863adb..3e065f9 100644
--- a/.github/workflows/flutter-main.yml
+++ b/.github/workflows/flutter-main.yml
@@ -28,6 +28,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Install dependencies
run: flutter pub get
@@ -66,7 +71,7 @@ jobs:
- name: Build Android APK
run: flutter build apk --release --flavor komet
- web-linux:
+ linux:
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -85,12 +90,14 @@ jobs:
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev libjsoncpp-dev
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-unknown-linux-gnu
+
- name: Install dependencies
run: flutter pub get
- - name: Build Web
- run: flutter build web --release
-
- name: Build Linux
run: flutter build linux --release
@@ -108,6 +115,11 @@ jobs:
flutter-version: '3.44.3'
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-pc-windows-msvc
+
- name: Install dependencies
run: flutter pub get
@@ -128,6 +140,11 @@ jobs:
flutter-version: '3.44.3'
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios,aarch64-apple-darwin,x86_64-apple-darwin
+
- name: Install dependencies
run: flutter pub get
diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml
index c646b02..573b309 100644
--- a/.github/workflows/release-dev.yml
+++ b/.github/workflows/release-dev.yml
@@ -42,6 +42,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Get dependencies
run: flutter pub get
@@ -110,6 +115,10 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-pc-windows-msvc
- name: Get dependencies
run: flutter pub get
- name: Build Windows
@@ -142,6 +151,10 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev libjsoncpp-dev libmpv-dev mpv libopus-dev libogg-dev
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-unknown-linux-gnu
- name: Get dependencies
run: flutter pub get
- name: Build Linux
@@ -169,6 +182,10 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-darwin,x86_64-apple-darwin
- name: Get dependencies
run: flutter pub get
- name: Fix opus/ogg libs for macOS
@@ -199,6 +216,10 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios
- name: Get dependencies
run: flutter pub get
- name: Build iOS (no codesign)
@@ -225,6 +246,9 @@ jobs:
platform-application
get-task-allow
+ application-identifierru.komet.app
+ keychain-access-groups
+ ru.komet.app
com.apple.private.security.no-container
diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml
index 1f9f247..d6ea1b4 100644
--- a/.github/workflows/release-main.yml
+++ b/.github/workflows/release-main.yml
@@ -37,6 +37,11 @@ jobs:
channel: 'stable'
cache: true
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
+
- name: Get dependencies
run: flutter pub get
@@ -105,6 +110,10 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-pc-windows-msvc
- name: Get dependencies
run: flutter pub get
- name: Build Windows
@@ -137,6 +146,10 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev libjsoncpp-dev
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: x86_64-unknown-linux-gnu
- name: Get dependencies
run: flutter pub get
- name: Build Linux
@@ -164,6 +177,10 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-darwin,x86_64-apple-darwin
- name: Get dependencies
run: flutter pub get
- name: Build macOS
@@ -192,6 +209,10 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
+ - name: Setup Rust
+ uses: ./.github/actions/setup-rust
+ with:
+ targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios
- name: Get dependencies
run: flutter pub get
- name: Build iOS (no codesign)
@@ -212,6 +233,9 @@ jobs:
platform-application
get-task-allow
+ application-identifierru.komet.app
+ keychain-access-groups
+ ru.komet.app
com.apple.private.security.no-container
diff --git a/.gitignore b/.gitignore
index eba31df..d108e96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -148,3 +148,9 @@ maxmint/
maxtun/
turnprobe/
test/live_server_probe_test.dart
+
+# local kolibri development: point the plugin at third_party/kolibri and the
+# Rust core at third_party/kolibri/kolibri-net instead of the published ones
+pubspec_overrides.yaml
+.cargo/
+third_party/kolibri/
diff --git a/AGENTS.md b/AGENTS.md
index c894244..ff1e7da 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -3,3 +3,6 @@
Лучше качество чем количество
Когда при исправления какой то ошибки/добавление новой возникает ситуация 50/50 где можно выбрать починить сейчас но костылём, или чинить долго, упорно, может даже вообще не починить и переписать пол приложения - выбирай долго и упорно.
ВМЕСТО СНЕКБАРОВ ИСПОЛЬЗУЙ НАШИ КАСТОМНЫЕ УВЕДОМЛЕНИЕ showCustomNotification(context, 'текст')
+Never leave real data in test files, including existing message contents or real IDs captured from requests. Use synthetic fixtures instead.
+Не пытайся собирать APK/AAB (flutter build apk, flutter build appbundle, gradle assemble) — сборку запускает пользователь, тебе достаточно flutter analyze
+Если делаешь кнопку, где иконка переключается перечёркнутая/неперечёркнутая (вспышка, микрофон, звук, уведомления) — она должна анимироваться lottie-иконкой, а не подменяться мгновенно: добавь спеку в SLASH_SPECS в tool/make_morph_icons.py (fill=1.0, если кнопка рисует Icon(..., fill: 1)), прогони python3 tool/make_morph_icons.py и выводи через LottieSlashIcon. Ассеты в assets/lottie/ руками не правь — только через генератор.
diff --git a/CLAUDE.md b/CLAUDE.md
index fa0a52d..4be5eb0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -29,6 +29,8 @@ flutter build windows --release
Android builds require **Java 17**. Gradle memory is configured to `-Xmx4096m`.
+**Never run APK/AAB builds yourself** (`flutter build apk`, `flutter build appbundle`, gradle assemble tasks) — they are slow and the user builds them. Verify changes with `flutter analyze`; the build commands above are documentation only.
+
## Build Flavors
| Flavor | App ID | Notes |
@@ -68,6 +70,21 @@ Incoming packets: transport → dispatcher → backend module → state → UI r
- **Use `showCustomNotification(context, 'text')`** for all user-facing notifications — never use SnackBars.
- When a fix can be done quickly with a hack or properly with a rewrite, **choose the proper rewrite**.
- Quality over quantity.
+- **Never leave real data in test files**, including existing message contents or real IDs captured from requests. Use synthetic fixtures instead.
+- **A button whose icon toggles between plain and slashed** (flash on/off, mic muted, sound, notifications) **must animate with a Lottie icon** — never swap two `Icon`s instantly. See *Animated icons* below.
+
+## Animated icons
+
+Everything in `assets/lottie/` is generated from the Material Symbols font by `tool/make_morph_icons.py` (stdlib-only Python, no deps). Never hand-edit the JSON — add a spec and re-run `python3 tool/make_morph_icons.py`.
+
+| Kind | Spec list | Widget |
+|------|-----------|--------|
+| Morph between two glyphs | `SPECS` | `ComposerMorphIcon` |
+| Plain ↔ slashed toggle | `SLASH_SPECS` | `LottieSlashIcon` |
+
+A slash spec takes the plain and slashed codepoints; the generator lays both glyphs out as static layers and sweeps a mask across the diagonal, so the slash looks drawn on top of the icon. Pass `fill=1.0` when the button renders `Icon(..., fill: 1)` — contours are then taken from the `FILL=1` instance of the variable font.
+
+`LottieSlashIcon` plays the asset forward when `slashed` turns true and backward when it turns false, so a single asset covers both directions. The older `AnimatedSlashIcon` (clip wipe over two glyphs) stays where it is already used; new buttons use the Lottie one.
## Localization
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 0d29021..bf8d421 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -7,6 +7,15 @@
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
+analyzer:
+ exclude:
+ - build/**
+ - android/**
+ - ios/**
+ - web/**
+ - windows/**
+ - macos/**
+ - linux/**
include: package:flutter_lints/flutter.yaml
linter:
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 6c621f0..0501773 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -15,6 +15,10 @@
+
+
+
+
@@ -23,6 +27,7 @@
+
@@ -64,6 +69,15 @@
+
+
+
+
+
+
+
+
+
@@ -71,6 +85,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/ru/komet/app/CallForegroundService.kt b/android/app/src/main/kotlin/ru/komet/app/CallForegroundService.kt
index 8700101..13a1ac5 100644
--- a/android/app/src/main/kotlin/ru/komet/app/CallForegroundService.kt
+++ b/android/app/src/main/kotlin/ru/komet/app/CallForegroundService.kt
@@ -8,7 +8,9 @@ import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
+import androidx.core.app.ServiceCompat
object CallState {
@Volatile
@@ -20,8 +22,34 @@ class CallForegroundService : Service() {
companion object {
const val ACTION_START = "ru.komet.app.CALL_ONGOING_START"
const val ACTION_STOP = "ru.komet.app.CALL_ONGOING_STOP"
+ const val ACTION_SCREEN_SHARE = "ru.komet.app.CALL_SCREEN_SHARE"
+ const val EXTRA_SCREEN_SHARE = "screenShare"
const val ONGOING_ID = 424243
+ @Volatile
+ var screenShare = false
+
+ @Volatile
+ private var running: CallForegroundService? = null
+
+ fun setScreenShare(ctx: Context, enabled: Boolean, caller: String) {
+ screenShare = enabled
+ val intent = Intent(ctx, CallForegroundService::class.java).apply {
+ action = ACTION_SCREEN_SHARE
+ putExtra(CallConst.EXTRA_CALLER, caller)
+ putExtra(EXTRA_SCREEN_SHARE, enabled)
+ }
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ ctx.startForegroundService(intent)
+ } else {
+ ctx.startService(intent)
+ }
+ } catch (e: Exception) {
+ Log.w("KometFcm", "screen share FGS update failed: ${e.message}")
+ }
+ }
+
fun start(ctx: Context, caller: String) {
CallState.inCall = true
val intent = Intent(ctx, CallForegroundService::class.java).apply {
@@ -41,6 +69,13 @@ class CallForegroundService : Service() {
fun stop(ctx: Context) {
CallState.inCall = false
+ screenShare = false
+ val service = running
+ if (service != null) {
+ service.shutdown()
+ return
+ }
+ NotificationManagerCompat.from(ctx).cancel(ONGOING_ID)
try {
ctx.startService(
Intent(ctx, CallForegroundService::class.java).apply {
@@ -54,17 +89,30 @@ class CallForegroundService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
+ override fun onCreate() {
+ super.onCreate()
+ running = this
+ }
+
override fun onDestroy() {
+ if (running === this) running = null
CallState.inCall = false
super.onDestroy()
}
+ private fun shutdown() {
+ ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ }
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
- ACTION_STOP -> {
- @Suppress("DEPRECATION")
- stopForeground(true)
- stopSelf()
+ ACTION_STOP -> shutdown()
+ ACTION_SCREEN_SHARE -> {
+ screenShare = intent.getBooleanExtra(EXTRA_SCREEN_SHARE, false)
+ val caller = intent.getStringExtra(CallConst.EXTRA_CALLER) ?: "Звонок"
+ CallNotifier.ensureChannel(this)
+ startAsForeground(caller)
}
else -> {
val caller = intent?.getStringExtra(CallConst.EXTRA_CALLER) ?: "Звонок"
@@ -103,15 +151,16 @@ class CallForegroundService : Service() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- startForeground(
- ONGOING_ID, notif,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE,
- )
+ var types = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
+ if (screenShare) {
+ types = types or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
+ }
+ startForeground(ONGOING_ID, notif, types)
} else {
startForeground(ONGOING_ID, notif)
}
} catch (e: Exception) {
- Log.w("KometFcm", "startForeground(mic) failed: ${e.message}")
+ Log.w("KometFcm", "startForeground failed: ${e.message}")
stopSelf()
}
}
diff --git a/android/app/src/main/kotlin/ru/komet/app/ChatNotifications.kt b/android/app/src/main/kotlin/ru/komet/app/ChatNotifications.kt
new file mode 100644
index 0000000..df3f5ac
--- /dev/null
+++ b/android/app/src/main/kotlin/ru/komet/app/ChatNotifications.kt
@@ -0,0 +1,22 @@
+package ru.komet.app
+
+import android.content.Intent
+import io.flutter.plugin.common.EventChannel
+
+object ChatNotifications {
+ const val EXTRA_CHAT = "komet_chat"
+
+ @Volatile
+ var activeChatId: Long = 0L
+
+ @Volatile
+ var sink: EventChannel.EventSink? = null
+
+ fun isDisplayed(chatId: Long): Boolean =
+ AppState.resumed && activeChatId == chatId
+
+ fun chatIdFrom(intent: Intent?): Long {
+ val id = intent?.getLongExtra(EXTRA_CHAT, 0L) ?: 0L
+ return if (id > 0L) id else 0L
+ }
+}
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..5638a48
--- /dev/null
+++ b/android/app/src/main/kotlin/ru/komet/app/FkmChannel.kt
@@ -0,0 +1,173 @@
+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"))
+
+ "editMessage" -> result.success(
+ update(ctx, call, "editMessage") { notifier, data ->
+ notifier.editMessage(data)
+ },
+ )
+
+ "removeMessage" -> result.success(
+ update(ctx, call, "removeMessage") { notifier, data ->
+ notifier.removeMessage(data)
+ },
+ )
+
+ "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