From 4557aebe6de87313d1184db7470435bf7bece76c Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Fri, 6 Mar 2026 18:37:06 +0530 Subject: [PATCH 1/2] ios [nfc]: Run `flutter build ios --config-only` Just running `flutter build ios --config-only` wasn't sufficient, there were no changes, I had to trigger Xcode's edits to the `project.pbxproj` first, by changing the order of build phases (moved "Compile Sources" phase above "Run Script") and then again reverted the order of those build phases. The Xcode's edits ended up being changing the `objectVersion = 54;` to `objectVersion = 70;`, and addition of couple of empty `inputPaths = ();` and `outputPaths = ();`. After Xcode's edits to `project.pbxproj`, I ran `flutter build ios --config-only` which resulted in this commit. This tells that this was probably missed in merge/rebase conflict in 509d88f46 (Patrol setup). Which changed the `objectVersion` entry to `objectVersion = 54;` but missed the change to `37D847A22F4CE7C20020EB99 /* RunnerUITests */ = {` block. So, on the next `flutter run/build`, it probably doesn't bother to verify or make changes to the `project.pbxproj` because `objectVersion` is already set to a value that `flutter run/build` likes. --- ios/Runner.xcodeproj/project.pbxproj | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a92edd5d73..e466a40deb 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -68,7 +68,17 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 37D847A22F4CE7C20020EB99 /* RunnerUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = RunnerUITests; sourceTree = ""; }; + 37D847A22F4CE7C20020EB99 /* RunnerUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = RunnerUITests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ From 1d03e575c1018920438feca86e561bbe9750ce1b Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Wed, 4 Mar 2026 01:13:27 +0530 Subject: [PATCH 2/2] store ios: Exclude the database file from iOS backup Fixes #2158. --- .gitattributes | 3 + ios/Runner.xcodeproj/project.pbxproj | 4 + ios/Runner/AppDelegate.swift | 12 +++ ios/Runner/IosNative.g.swift | 107 +++++++++++++++++++++++++++ lib/host/ios_native.g.dart | 79 ++++++++++++++++++++ lib/model/store.dart | 32 +++++++- pigeon/ios_native.dart | 22 ++++++ 7 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 ios/Runner/IosNative.g.swift create mode 100644 lib/host/ios_native.g.dart create mode 100644 pigeon/ios_native.dart diff --git a/.gitattributes b/.gitattributes index e2ad1d6edb..a70de9f0aa 100644 --- a/.gitattributes +++ b/.gitattributes @@ -18,6 +18,9 @@ test/model/schemas/*.json -diff # Kotlin files generated by, e.g., Pigeon: *.g.kt -diff +# Swift files generated by, e.g., Pigeon: +*.g.swift -diff + # On the other hand, keep diffs for pubspec.lock. It contains # information independent of any non-generated file in the tree. # And thankfully it's much less verbose than, say, a yarn.lock. diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e466a40deb..c5941134e6 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + B340EB382F5B092B007AD309 /* IosNative.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B340EB372F5B092B007AD309 /* IosNative.g.swift */; }; B34E9F092D776BEB0009AED2 /* Notifications.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34E9F082D776BEB0009AED2 /* Notifications.g.swift */; }; F311C174AF9C005CE4AADD72 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3EAE3F3F518B95B7BFEB4FE7 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -63,6 +64,7 @@ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B340EB372F5B092B007AD309 /* IosNative.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosNative.g.swift; sourceTree = ""; }; B34E9F082D776BEB0009AED2 /* Notifications.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.g.swift; sourceTree = ""; }; B3AF53A72CA20BD10039801D /* Zulip.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Zulip.xcconfig; path = Flutter/Zulip.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ @@ -150,6 +152,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + B340EB372F5B092B007AD309 /* IosNative.g.swift */, 3752899A2AF472D400475D9C /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -479,6 +482,7 @@ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, B34E9F092D776BEB0009AED2 /* Notifications.g.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + B340EB382F5B092B007AD309 /* IosNative.g.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 9a112e22e1..5918368222 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -17,6 +17,8 @@ import Flutter let controller = window?.rootViewController as! FlutterViewController + IosNativeHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: IosNativeHostApiImpl()) + // Retrieve the remote notification payload from launch options; // this will be null if the launch wasn't triggered by a notification. let notificationPayload = launchOptions?[.remoteNotification] as? [AnyHashable : Any] @@ -69,3 +71,13 @@ class NotificationTapEventListener: NotificationTapEventsStreamHandler { eventSink?.success(IosNotificationTapEvent(payload: payload)) } } + +private class IosNativeHostApiImpl: IosNativeHostApi { + func setExcludedFromBackup(filePath: String) throws { + var resourceValues = URLResourceValues() + resourceValues.isExcludedFromBackup = true + + var url = URL(fileURLWithPath: filePath, isDirectory: false) + try url.setResourceValues(resourceValues) + } +} diff --git a/ios/Runner/IosNative.g.swift b/ios/Runner/IosNative.g.swift new file mode 100644 index 0000000000..3c2b653a80 --- /dev/null +++ b/ios/Runner/IosNative.g.swift @@ -0,0 +1,107 @@ +// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + + +private class IosNativePigeonCodecReader: FlutterStandardReader { +} + +private class IosNativePigeonCodecWriter: FlutterStandardWriter { +} + +private class IosNativePigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return IosNativePigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return IosNativePigeonCodecWriter(data: data) + } +} + +class IosNativePigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = IosNativePigeonCodec(readerWriter: IosNativePigeonCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol IosNativeHostApi { + /// Sets UrlResourceValues.isExcludedFromBackup for the given file path. + /// + /// See doc: + /// https://developer.apple.com/documentation/foundation/urlresourcevalues/isexcludedfrombackup + /// https://developer.apple.com/documentation/foundation/optimizing-your-app-s-data-for-icloud-backup + func setExcludedFromBackup(filePath: String) throws +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class IosNativeHostApiSetup { + static var codec: FlutterStandardMessageCodec { IosNativePigeonCodec.shared } + /// Sets up an instance of `IosNativeHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: IosNativeHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + /// Sets UrlResourceValues.isExcludedFromBackup for the given file path. + /// + /// See doc: + /// https://developer.apple.com/documentation/foundation/urlresourcevalues/isexcludedfrombackup + /// https://developer.apple.com/documentation/foundation/optimizing-your-app-s-data-for-icloud-backup + let setExcludedFromBackupChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.zulip.IosNativeHostApi.setExcludedFromBackup\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setExcludedFromBackupChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let filePathArg = args[0] as! String + do { + try api.setExcludedFromBackup(filePath: filePathArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + setExcludedFromBackupChannel.setMessageHandler(nil) + } + } +} diff --git a/lib/host/ios_native.g.dart b/lib/host/ios_native.g.dart new file mode 100644 index 0000000000..82e97f6b17 --- /dev/null +++ b/lib/host/ios_native.g.dart @@ -0,0 +1,79 @@ +// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + +class IosNativeHostApi { + /// Constructor for [IosNativeHostApi]. 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. + IosNativeHostApi({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; + + /// Sets UrlResourceValues.isExcludedFromBackup for the given file path. + /// + /// See doc: + /// https://developer.apple.com/documentation/foundation/urlresourcevalues/isexcludedfrombackup + /// https://developer.apple.com/documentation/foundation/optimizing-your-app-s-data-for-icloud-backup + Future setExcludedFromBackup(String filePath) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.zulip.IosNativeHostApi.setExcludedFromBackup$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([filePath]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } +} diff --git a/lib/model/store.dart b/lib/model/store.dart index e0f9b588c2..1084a2f017 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -16,6 +16,7 @@ import '../api/model/model.dart'; import '../api/route/events.dart'; import '../api/backoff.dart'; import '../api/route/realm.dart'; +import '../host/ios_native.g.dart'; import '../log.dart'; import 'actions.dart'; import 'autocomplete.dart'; @@ -1098,7 +1099,8 @@ class LiveGlobalStore extends GlobalStore { // we'd invest in this area more. For example we'd try doing these // in parallel, or deferring some to be concurrent with loading server data. final stopwatch = Stopwatch()..start(); - final db = AppDatabase(NativeDatabase.createInBackground(await _dbFile())); + final file = await _dbFile(); + final db = AppDatabase(NativeDatabase.createInBackground(file)); final t1 = stopwatch.elapsed; final globalSettings = await db.getGlobalSettings(); final t2 = stopwatch.elapsed; @@ -1116,6 +1118,12 @@ class LiveGlobalStore extends GlobalStore { "${format(t4 - t3)} int-settings, ${format(t5 - t4)} accounts"); } + // Disable OS backups for the database file, see: + // https://github.com/zulip/zulip-flutter/issues/2158 + // This comes after the queries above, because it must come after + // the database file has been created on disk. + unawaited(_maybeDisableOsBackup(file)); // TODO(log) on error + return LiveGlobalStore._( backend: LiveGlobalStoreBackend._(db: db), globalSettings: globalSettings, @@ -1143,6 +1151,28 @@ class LiveGlobalStore extends GlobalStore { return File(p.join(dir.path, 'zulip.db')); } + /// Excludes the provided database file from OS backups. + /// + /// It is no-op on platforms other than iOS. + static Future _maybeDisableOsBackup(File databaseFile) async { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + await IosNativeHostApi().setExcludedFromBackup(databaseFile.path); + + case TargetPlatform.android: + // On Android, backups are disabled for all files. + // See: https://github.com/zulip/zulip-flutter/pull/2160. + break; + + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + // Does nothing on these platforms. + break; + } + } + final LiveGlobalStoreBackend _backend; // The methods that use this should probably all move to [GlobalStoreBackend] diff --git a/pigeon/ios_native.dart b/pigeon/ios_native.dart new file mode 100644 index 0000000000..40bbebe8d6 --- /dev/null +++ b/pigeon/ios_native.dart @@ -0,0 +1,22 @@ +import 'package:pigeon/pigeon.dart'; + +// To rebuild this pigeon's output after editing this file, +// run `tools/check pigeon --fix`. +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/host/ios_native.g.dart', + swiftOut: 'ios/Runner/IosNative.g.swift', + swiftOptions: SwiftOptions(includeErrorClass: false), +)) + +@HostApi() +abstract class IosNativeHostApi { + /// Sets UrlResourceValues.isExcludedFromBackup for the given file path. + /// + /// The file at this path must already exist, + /// and be a regular file (not a directory). + /// + /// See doc: + /// https://developer.apple.com/documentation/foundation/urlresourcevalues/isexcludedfrombackup + /// https://developer.apple.com/documentation/foundation/optimizing-your-app-s-data-for-icloud-backup + void setExcludedFromBackup(String filePath); +}