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 a92edd5d73..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,12 +64,23 @@ 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 */ /* 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 */ @@ -140,6 +152,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + B340EB372F5B092B007AD309 /* IosNative.g.swift */, 3752899A2AF472D400475D9C /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -469,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); +}