fix: смена иконки через нативный MethodChannel — без плагина и без отключения MainActivity

This commit is contained in:
klockky
2026-05-28 05:13:14 +00:00
parent 73208bda49
commit c6b0cba08f
7 changed files with 101 additions and 21 deletions
+9 -4
View File
@@ -9,9 +9,6 @@
android:label="Komet"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<service
android:name="com.solusibejo.flutter_dynamic_icon_plus.FlutterDynamicIconPlusService"
android:stopWithTask="false"/>
<activity
android:name=".MainActivity"
android:exported="true"
@@ -29,11 +26,19 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
</activity>
<activity-alias
android:name=".DefaultIcon"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="Komet"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</activity-alias>
<activity-alias
android:name=".MinimalIcon"
android:enabled="false"
@@ -1,7 +1,9 @@
package ru.komet.app
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
@@ -20,11 +22,32 @@ import java.util.concurrent.atomic.AtomicBoolean
class MainActivity : FlutterActivity() {
private val channelName = "ru.komet.app/vpn_bypass"
private val iconAliases = listOf("DefaultIcon", "MinimalIcon")
private companion object {
const val LOG_TAG = "VpnBypass"
}
private fun applyIcon(name: String) {
val pm = packageManager
for (alias in iconAliases) {
val component = ComponentName(packageName, "$packageName.$alias")
val state = if (alias == name) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
pm.setComponentEnabledSetting(
component,
state,
PackageManager.DONT_KILL_APP,
)
}
Handler(Looper.getMainLooper()).postDelayed({
finishAndRemoveTask()
}, 250L)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
@@ -39,6 +62,28 @@ class MainActivity : FlutterActivity() {
}
}
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"ru.komet.app/app_icon",
).setMethodCallHandler { call, result ->
when (call.method) {
"setAppIcon" -> {
val name = call.argument<String>("name")
if (name == null || !iconAliases.contains(name)) {
result.error("INVALID_ICON", "Unknown icon: $name", null)
return@setMethodCallHandler
}
try {
applyIcon(name)
result.success(null)
} catch (e: Exception) {
result.error("APPLY_FAILED", e.message, null)
}
}
else -> result.notImplemented()
}
}
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"ru.komet.app/upload_service",
+37
View File
@@ -8,6 +8,43 @@ import UIKit
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let controller = window?.rootViewController as? FlutterViewController
if let messenger = controller?.binaryMessenger {
let channel = FlutterMethodChannel(
name: "ru.komet.app/app_icon",
binaryMessenger: messenger
)
channel.setMethodCallHandler { (call, result) in
guard call.method == "setAppIcon" else {
result(FlutterMethodNotImplemented)
return
}
let args = call.arguments as? [String: Any]
let name = args?["name"] as? String
let iconName: String? = (name == "DefaultIcon") ? nil : name
if !UIApplication.shared.supportsAlternateIcons {
result(FlutterError(
code: "UNSUPPORTED",
message: "Alternate icons are not supported",
details: nil
))
return
}
UIApplication.shared.setAlternateIconName(iconName) { error in
if let error = error {
result(FlutterError(
code: "APPLY_FAILED",
message: error.localizedDescription,
details: nil
))
} else {
result(nil)
}
}
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
+9 -7
View File
@@ -1,23 +1,25 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_dynamic_icon_plus/flutter_dynamic_icon_plus.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum AppIcon {
defaultIcon('default', 'Default', 'assets/komet_icon.png', null),
defaultIcon('default', 'Default', 'assets/komet_icon.png', 'DefaultIcon'),
minimal('minimal', 'Minimal', 'assets/meteor_icon.png', 'MinimalIcon');
final String id;
final String title;
final String previewAsset;
final String? platformName;
final String platformName;
const AppIcon(this.id, this.title, this.previewAsset, this.platformName);
}
class AppIconConfig {
static const prefKey = 'app_icon';
static const _channel = MethodChannel('ru.komet.app/app_icon');
static final ValueNotifier<AppIcon> current = ValueNotifier(
AppIcon.defaultIcon,
);
@@ -34,12 +36,12 @@ class AppIconConfig {
static Future<void> apply(AppIcon icon) async {
if (!isSupported) return;
if (current.value == icon) return;
await FlutterDynamicIconPlus.setAlternateIconName(
iconName: icon.platformName,
);
current.value = icon;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(prefKey, icon.id);
current.value = icon;
await _channel.invokeMethod<void>('setAppIcon', {
'name': icon.platformName,
});
}
static AppIcon _parse(String? val) {
@@ -74,7 +74,7 @@ class _AppIconScreenState extends State<AppIconScreen> {
const SizedBox(height: 4),
Text(
AppIconConfig.isSupported
? 'Изменение применится сразу — приложение может ненадолго закрыться'
? 'На Android приложение закроется — лаунчер подхватит новую иконку. На iOS — мгновенно с системным диалогом.'
: 'Доступно только на Android и iOS',
style: TextStyle(
color: cs.onSurfaceVariant,
-8
View File
@@ -318,14 +318,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_dynamic_icon_plus:
dependency: "direct main"
description:
name: flutter_dynamic_icon_plus
sha256: "7721ef2666691064dece879d968fa7e1c4d64aa743ea998e6b1e65a1b482f212"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
-1
View File
@@ -58,7 +58,6 @@ dependencies:
firebase_core: ^4.1.1
firebase_messaging: ^16.0.2
flutter_local_notifications: ^21.0.0
flutter_dynamic_icon_plus: ^1.4.0
dev_dependencies:
flutter_test: