From 0c9263b69f2bac90fa0f15204d08c7ff227e4a86 Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:16:23 +0530 Subject: [PATCH] feat: show notification and battery optimization warning --- i18n/en.json | 2 + .../app/alextran/immich/MainActivity.kt | 3 + .../immich/permission/PermissionApi.g.kt | 114 ++++++++++++++ .../immich/permission/PermissionApiImpl.kt | 19 +++ .../lib/pages/backup/drift_backup.page.dart | 147 +++++++++++++++++- mobile/lib/platform/permission_api.g.dart | 89 +++++++++++ .../providers/app_life_cycle.provider.dart | 2 +- .../infrastructure/platform.provider.dart | 5 +- ...provider.dart => permission.provider.dart} | 32 ++++ .../settings/notification_setting.dart | 2 +- mobile/mise.toml | 22 +-- mobile/pigeon/permission_api.dart | 17 ++ 12 files changed, 432 insertions(+), 22 deletions(-) create mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt create mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApiImpl.kt create mode 100644 mobile/lib/platform/permission_api.g.dart rename mobile/lib/providers/{notification_permission.provider.dart => permission.provider.dart} (54%) create mode 100644 mobile/pigeon/permission_api.dart diff --git a/i18n/en.json b/i18n/en.json index 6cf5bbeaee..fb9dafa68f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -693,6 +693,7 @@ "backup_settings_subtitle": "Manage upload settings", "backup_upload_details_page_more_details": "Tap for more details", "backward": "Backward", + "battery_optimization_backup_reliability": "Disabling battery optimizations can improve the reliability of background backup", "biometric_auth_enabled": "Biometric authentication enabled", "biometric_locked_out": "You are locked out of biometric authentication", "biometric_no_options": "No biometric options available", @@ -1667,6 +1668,7 @@ "not_selected": "Not selected", "notes": "Notes", "nothing_here_yet": "Nothing here yet", + "notification_backup_reliability": "Enable notifications to improve background backup reliability", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_list_tile_content": "Grant permission to enable notifications.", "notification_permission_list_tile_enable_button": "Enable Notifications", diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 2c80b8d2bd..127eb22cd5 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -17,6 +17,8 @@ import app.alextran.immich.images.LocalImageApi import app.alextran.immich.images.LocalImagesImpl import app.alextran.immich.images.RemoteImageApi import app.alextran.immich.images.RemoteImagesImpl +import app.alextran.immich.permission.PermissionApi +import app.alextran.immich.permission.PermissionApiImpl import app.alextran.immich.sync.NativeSyncApi import app.alextran.immich.sync.NativeSyncApiImpl26 import app.alextran.immich.sync.NativeSyncApiImpl30 @@ -50,6 +52,7 @@ class MainActivity : FlutterFragmentActivity() { BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx)) ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx)) + PermissionApi.setUp(messenger, PermissionApiImpl(ctx)) flutterEngine.plugins.add(backgroundEngineLockImpl) flutterEngine.plugins.add(nativeSyncApiImpl) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt new file mode 100644 index 0000000000..01762b4832 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt @@ -0,0 +1,114 @@ +// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package app.alextran.immich.permission + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object PermissionApiPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : RuntimeException() + +enum class PermissionStatus(val raw: Int) { + GRANTED(0), + DENIED(1), + PERMANENTLY_DENIED(2); + + companion object { + fun ofRaw(raw: Int): PermissionStatus? { + return values().firstOrNull { it.raw == raw } + } + } +} +private open class PermissionApiPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + PermissionStatus.ofRaw(it.toInt()) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is PermissionStatus -> { + stream.write(129) + writeValue(stream, value.raw.toLong()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface PermissionApi { + fun isIgnoringBatteryOptimizations(): PermissionStatus + + companion object { + /** The codec used by PermissionApi. */ + val codec: MessageCodec by lazy { + PermissionApiPigeonCodec() + } + /** Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.isIgnoringBatteryOptimizations()) + } catch (exception: Throwable) { + PermissionApiPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApiImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApiImpl.kt new file mode 100644 index 0000000000..822f0455fc --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApiImpl.kt @@ -0,0 +1,19 @@ +package app.alextran.immich.permission + +import android.content.Context +import android.os.PowerManager + +class PermissionApiImpl(context: Context) : PermissionApi { + private val ctx: Context = context.applicationContext + + private val powerManager = + ctx.getSystemService(Context.POWER_SERVICE) as PowerManager + + + override fun isIgnoringBatteryOptimizations(): PermissionStatus { + if (powerManager.isIgnoringBatteryOptimizations(ctx.packageName)) { + return PermissionStatus.GRANTED + } + return PermissionStatus.DENIED + } +} diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 2e18c3edc6..9736f5c8e1 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; @@ -15,11 +16,16 @@ import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.w import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/providers/permission.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; +import 'package:immich_ui/immich_ui.dart'; import 'package:logging/logging.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @RoutePage() @@ -162,11 +168,7 @@ class _DriftBackupPageState extends ConsumerState { ), ), }, - TextButton.icon( - icon: const Icon(Icons.info_outline_rounded), - onPressed: () => context.pushRoute(const DriftUploadDetailRoute()), - label: Text("view_details".t(context: context)), - ), + const _BackupFooter(), ], ], ), @@ -177,6 +179,137 @@ class _DriftBackupPageState extends ConsumerState { } } +class _BackupFooter extends ConsumerStatefulWidget { + const _BackupFooter(); + + @override + ConsumerState<_BackupFooter> createState() => _BackupFooterState(); +} + +class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (CurrentPlatform.isAndroid && state == AppLifecycleState.resumed && mounted) { + unawaited(ref.read(notificationPermissionProvider.notifier).getNotificationPermission()); + unawaited(ref.read(batteryOptimizationProvider.notifier).getBatteryOptimizationPermission()); + } + } + + void showPermissionsDialog() { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + content: Text(context.t.notification_permission_dialog_content), + actions: [ + ImmichTextButton( + labelText: context.t.cancel, + variant: .ghost, + expanded: false, + onPressed: () => ContextHelper(ctx).pop(), + ), + ImmichTextButton( + labelText: context.t.settings, + variant: .ghost, + expanded: false, + onPressed: () { + ContextHelper(context).pop(); + openAppSettings(); + }, + ), + ], + ), + ); + } + + void showBatteryOptimizationInfo() { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext ctx) { + return AlertDialog( + title: Text(context.t.backup_controller_page_background_battery_info_title), + content: SingleChildScrollView(child: Text(context.t.backup_controller_page_background_battery_info_message)), + actions: [ + ImmichTextButton( + labelText: context.t.backup_controller_page_background_battery_info_link, + variant: .ghost, + expanded: false, + onPressed: () => launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication), + ), + ImmichTextButton( + labelText: context.t.backup_controller_page_background_battery_info_ok, + variant: .ghost, + expanded: false, + onPressed: () => ContextHelper(ctx).pop(), + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final isBackupEnabled = ref.watch(_backupStatusProvider).valueOrNull ?? false; + final notificationStatus = ref.watch(notificationPermissionProvider); + final batteryOptimizationStatus = ref.watch(batteryOptimizationProvider).valueOrNull; + + return Column( + children: [ + if (CurrentPlatform.isAndroid && isBackupEnabled) ...[ + if (notificationStatus != PermissionStatus.granted) + TextButton.icon( + iconAlignment: .end, + icon: Icon(Icons.open_in_new_outlined, color: context.colorScheme.onSurfaceSecondary), + label: Text( + context.t.notification_backup_reliability, + textAlign: TextAlign.left, + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + onPressed: () { + ref.read(notificationPermissionProvider.notifier).requestNotificationPermission().then((p) { + if (p == PermissionStatus.permanentlyDenied) { + showPermissionsDialog(); + } + }); + }, + ), + if (notificationStatus != PermissionStatus.granted && batteryOptimizationStatus != PermissionStatus.granted) + const Divider(indent: 32, endIndent: 32), + if (batteryOptimizationStatus != PermissionStatus.granted) + TextButton.icon( + iconAlignment: .end, + icon: Icon(Icons.open_in_new_outlined, color: context.colorScheme.onSurfaceSecondary), + label: Text( + context.t.battery_optimization_backup_reliability, + textAlign: TextAlign.left, + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + onPressed: showBatteryOptimizationInfo, + ), + ], + TextButton.icon( + icon: const Icon(Icons.info_outline_rounded), + onPressed: () => context.pushRoute(const DriftUploadDetailRoute()), + label: Text(context.t.view_details), + ), + ], + ); + } +} + class _BackupAlbumSelectionCard extends ConsumerWidget { const _BackupAlbumSelectionCard(); @@ -527,3 +660,7 @@ class _PreparingStatusState extends ConsumerState { ); } } + +final _backupStatusProvider = StreamProvider.autoDispose((ref) async* { + yield* ref.watch(storeServiceProvider).watch(StoreKey.enableBackup); +}); diff --git a/mobile/lib/platform/permission_api.g.dart b/mobile/lib/platform/permission_api.g.dart new file mode 100644 index 0000000000..e1df657cfa --- /dev/null +++ b/mobile/lib/platform/permission_api.g.dart @@ -0,0 +1,89 @@ +// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; + +Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; +} + +enum PermissionStatus { granted, denied, permanentlyDenied } + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is PermissionStatus) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final value = readValue(buffer) as int?; + return value == null ? null : PermissionStatus.values[value]; + default: + return super.readValueOfType(type, buffer); + } + } +} + +class PermissionApi { + /// Constructor for [PermissionApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + PermissionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future isIgnoringBatteryOptimizations() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as PermissionStatus; + } +} diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 11e0dcd49c..f80781cf39 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/notification_permission.provider.dart'; +import 'package:immich_mobile/providers/permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; diff --git a/mobile/lib/providers/infrastructure/platform.provider.dart b/mobile/lib/providers/infrastructure/platform.provider.dart index 01d0f61d1c..a2bd1fd0d4 100644 --- a/mobile/lib/providers/infrastructure/platform.provider.dart +++ b/mobile/lib/providers/infrastructure/platform.provider.dart @@ -3,9 +3,10 @@ import 'package:immich_mobile/domain/services/background_worker.service.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/platform/connectivity_api.g.dart'; -import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/local_image_api.g.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/network_api.g.dart'; +import 'package:immich_mobile/platform/permission_api.g.dart'; import 'package:immich_mobile/platform/remote_image_api.g.dart'; final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi())); @@ -18,6 +19,8 @@ final nativeSyncApiProvider = Provider((_) => NativeSyncApi()); final connectivityApiProvider = Provider((_) => ConnectivityApi()); +final permissionApiProvider = Provider((_) => PermissionApi()); + final localImageApi = LocalImageApi(); final remoteImageApi = RemoteImageApi(); diff --git a/mobile/lib/providers/notification_permission.provider.dart b/mobile/lib/providers/permission.provider.dart similarity index 54% rename from mobile/lib/providers/notification_permission.provider.dart rename to mobile/lib/providers/permission.provider.dart index da0badd4ec..ffae87e679 100644 --- a/mobile/lib/providers/notification_permission.provider.dart +++ b/mobile/lib/providers/permission.provider.dart @@ -1,6 +1,9 @@ +import 'dart:async'; import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/platform/permission_api.g.dart' as pm; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:permission_handler/permission_handler.dart'; class NotificationPermissionNotifier extends StateNotifier { @@ -39,3 +42,32 @@ class NotificationPermissionNotifier extends StateNotifier { final notificationPermissionProvider = StateNotifierProvider( (ref) => NotificationPermissionNotifier(), ); + +final batteryOptimizationProvider = AsyncNotifierProvider( + BatteryOptimizationNotifier.new, +); + +class BatteryOptimizationNotifier extends AsyncNotifier { + Future getBatteryOptimizationPermission() async { + final PermissionStatus status; + final isIgnoring = await ref.read(permissionApiProvider).isIgnoringBatteryOptimizations().then((p) => p.toStatus()); + if (isIgnoring == PermissionStatus.granted) { + status = PermissionStatus.granted; + } else { + status = PermissionStatus.denied; + } + state = AsyncValue.data(status); + return status; + } + + @override + FutureOr build() => getBatteryOptimizationPermission(); +} + +extension on pm.PermissionStatus { + PermissionStatus toStatus() => switch (this) { + pm.PermissionStatus.granted => PermissionStatus.granted, + pm.PermissionStatus.denied => PermissionStatus.denied, + pm.PermissionStatus.permanentlyDenied => PermissionStatus.permanentlyDenied, + }; +} diff --git a/mobile/lib/widgets/settings/notification_setting.dart b/mobile/lib/widgets/settings/notification_setting.dart index 18a9749a71..49c4297d7b 100644 --- a/mobile/lib/widgets/settings/notification_setting.dart +++ b/mobile/lib/widgets/settings/notification_setting.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/notification_permission.provider.dart'; +import 'package:immich_mobile/providers/permission.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; diff --git a/mobile/mise.toml b/mobile/mise.toml index 89a9f0035c..7d35fe8fd3 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -30,14 +30,8 @@ run = "dart run build_runner watch --delete-conflicting-outputs" alias = "pigeon" description = "Generate pigeon platform code" run = [ - "dart run pigeon --input pigeon/native_sync_api.dart", - "dart run pigeon --input pigeon/local_image_api.dart", - "dart run pigeon --input pigeon/remote_image_api.dart", - "dart run pigeon --input pigeon/background_worker_api.dart", - "dart run pigeon --input pigeon/background_worker_lock_api.dart", - "dart run pigeon --input pigeon/connectivity_api.dart", - "dart run pigeon --input pigeon/network_api.dart", - "dart format lib/platform/native_sync_api.g.dart lib/platform/local_image_api.g.dart lib/platform/remote_image_api.g.dart lib/platform/background_worker_api.g.dart lib/platform/background_worker_lock_api.g.dart lib/platform/connectivity_api.g.dart lib/platform/network_api.g.dart", + "ls pigeon/*.dart | xargs -n1 -P4 -I{} dart run pigeon --input {}", + "dart format lib/platform/", ] [tasks."codegen:translation"] @@ -131,10 +125,10 @@ run = "dcm fix lib" [tasks.checklist] run = [ - {task = "codegen:pigeon" }, - {task = "codegen:dart" }, - {task = "codegen:translation" }, - {task = "analyze" }, - {task = "format" }, - {task = "test" }, + { task = "codegen:pigeon" }, + { task = "codegen:dart" }, + { task = "codegen:translation" }, + { task = "analyze" }, + { task = "format" }, + { task = "test" }, ] diff --git a/mobile/pigeon/permission_api.dart b/mobile/pigeon/permission_api.dart new file mode 100644 index 0000000000..873a7960d6 --- /dev/null +++ b/mobile/pigeon/permission_api.dart @@ -0,0 +1,17 @@ +import 'package:pigeon/pigeon.dart'; + +enum PermissionStatus { granted, denied, permanentlyDenied } + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/platform/permission_api.g.dart', + kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt', + kotlinOptions: KotlinOptions(package: 'app.alextran.immich.permission'), + dartOptions: DartOptions(), + dartPackageName: 'immich_mobile', + ), +) +@HostApi() +abstract class PermissionApi { + PermissionStatus isIgnoringBatteryOptimizations(); +}