diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3953f290..18602482 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -139,6 +139,9 @@ + + + diff --git a/android/app/src/main/kotlin/eu/twonly/MainActivity.kt b/android/app/src/main/kotlin/eu/twonly/MainActivity.kt index 945bb0db..207cfdc2 100644 --- a/android/app/src/main/kotlin/eu/twonly/MainActivity.kt +++ b/android/app/src/main/kotlin/eu/twonly/MainActivity.kt @@ -20,12 +20,16 @@ import io.flutter.plugin.common.MethodChannel import eu.twonly.notifications.NotificationTapChannel import eu.twonly.webxdc.WebxdcChannel import eu.twonly.widget.WidgetRuntimeChannel +import android.Manifest +import android.content.pm.PackageManager class MainActivity : FlutterFragmentActivity() { private val CHANNEL = "eu.twonly/photo_picker" private var pendingResult: MethodChannel.Result? = null + private var pendingLocationPermissionResult: MethodChannel.Result? = null private lateinit var pickMultipleMedia: ActivityResultLauncher + private lateinit var requestLocationPermissions: ActivityResultLauncher> override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() @@ -43,6 +47,14 @@ class MainActivity : FlutterFragmentActivity() { } pendingResult = null } + requestLocationPermissions = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions(), + ) { + pendingLocationPermissionResult?.success( + checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED, + ) + pendingLocationPermissionResult = null + } super.onCreate(savedInstanceState) } @@ -112,6 +124,28 @@ class MainActivity : FlutterFragmentActivity() { else -> result.notImplemented() } } + MethodChannel( + flutterEngine.dartExecutor.binaryMessenger, + "eu.twonly/location_metadata", + ).setMethodCallHandler { call, result -> + if (call.method != "requestPrecisePermission") { + result.notImplemented() + return@setMethodCallHandler + } + if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + result.success(true) + } else if (pendingLocationPermissionResult != null) { + result.error("request_in_progress", "A location permission request is already open.", null) + } else { + pendingLocationPermissionResult = result + requestLocationPermissions.launch( + arrayOf( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ), + ) + } + } } override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) { diff --git a/android/app/src/main/kotlin/eu/twonly/location/NativeLocation.kt b/android/app/src/main/kotlin/eu/twonly/location/NativeLocation.kt new file mode 100644 index 00000000..cf1e5310 --- /dev/null +++ b/android/app/src/main/kotlin/eu/twonly/location/NativeLocation.kt @@ -0,0 +1,60 @@ +package eu.twonly.location + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import eu.twonly.MyApplication +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + +/** One-shot precise location lookup called synchronously by Rust/JNI. */ +object NativeLocation { + private const val TARGET_ACCURACY_METERS = 50f + + @JvmStatic + @SuppressLint("MissingPermission") + fun current(timeoutMillis: Long): String? { + val context = MyApplication.instance + if (context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + return null + } + val manager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager ?: return null + val finished = CountDownLatch(1) + val best = AtomicReference(null) + val listener = object : LocationListener { + override fun onLocationChanged(location: Location) { + if (location.accuracy < 0) return + val previous = best.get() + if (previous == null || location.accuracy < previous.accuracy) best.set(location) + if (location.accuracy <= TARGET_ACCURACY_METERS) finished.countDown() + } + + @Deprecated("Deprecated in Android") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit + } + val main = Handler(Looper.getMainLooper()) + main.post { + var requested = false + for (provider in listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)) { + if (!runCatching { manager.isProviderEnabled(provider) }.getOrDefault(false)) continue + runCatching { + manager.requestLocationUpdates(provider, 0L, 0f, listener, Looper.getMainLooper()) + requested = true + } + } + if (!requested) finished.countDown() + } + finished.await(timeoutMillis.coerceAtLeast(0), TimeUnit.MILLISECONDS) + main.post { runCatching { manager.removeUpdates(listener) } } + val location = best.get() ?: return null + return "[${location.latitude},${location.longitude},${location.accuracy}]" + } +} diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index e633b03e..e2b0d3ee 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c704c950..6204f0c3 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ D3A100032F70000100D1A002 /* NativeImageCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100042F70000100D1A002 /* NativeImageCodec.swift */; }; D3A100052F70000100D1A003 /* NativeVideoCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100062F70000100D1A003 /* NativeVideoCodec.swift */; }; D3A100072F70000100D1A004 /* NativeGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100082F70000100D1A004 /* NativeGallery.swift */; }; + D3A100092F70000100D1A005 /* NativeLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A1000A2F70000100D1A005 /* NativeLocation.swift */; }; D3A1000B2F70000100D1A0FE /* WebxdcHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */; }; F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ @@ -135,6 +136,7 @@ D3A100042F70000100D1A002 /* NativeImageCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeImageCodec.swift; sourceTree = ""; }; D3A100062F70000100D1A003 /* NativeVideoCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeVideoCodec.swift; sourceTree = ""; }; D3A100082F70000100D1A004 /* NativeGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeGallery.swift; sourceTree = ""; }; + D3A1000A2F70000100D1A005 /* NativeLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeLocation.swift; sourceTree = ""; }; D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebxdcHost.swift; sourceTree = ""; }; DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E190E82D9973B318A389650B /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -305,6 +307,7 @@ D3A100042F70000100D1A002 /* NativeImageCodec.swift */, D3A100062F70000100D1A003 /* NativeVideoCodec.swift */, D3A100082F70000100D1A004 /* NativeGallery.swift */, + D3A1000A2F70000100D1A005 /* NativeLocation.swift */, D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */, D24E27CC2F38ABC10055D9D1 /* RunnerRelease.entitlements */, D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */, @@ -739,6 +742,7 @@ D3A100032F70000100D1A002 /* NativeImageCodec.swift in Sources */, D3A100052F70000100D1A003 /* NativeVideoCodec.swift in Sources */, D3A100072F70000100D1A004 /* NativeGallery.swift in Sources */, + D3A100092F70000100D1A005 /* NativeLocation.swift in Sources */, D3A1000B2F70000100D1A0FE /* WebxdcHost.swift in Sources */, D4A100022F81000100A10002 /* TwonlyWidgetShared.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3a5a88ff..864d7d77 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" + "revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902", + "version" : "11.3.1" } }, { @@ -23,17 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382", - "version" : "12.14.0" - } - }, - { - "identity" : "flutterfire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/flutterfire", - "state" : { - "revision" : "a10a4148e769fadb01b1ff8d6bb76e9137f35b81", - "version" : "4.6.0-firebase-core-swift" + "revision" : "346daa9f46316aa372b35b317e18224acc2e9063", + "version" : "12.18.0" } }, { @@ -41,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", "state" : { - "revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64", - "version" : "3.6.0" + "revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55", + "version" : "3.6.1" } }, { @@ -50,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "219e564a8510e983e675c94f77f7f7c50049f22d", - "version" : "12.14.0" + "revision" : "f04760d460296cc0fa430935a7be212e5bd67fc5", + "version" : "12.18.0" } }, { @@ -59,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", - "version" : "10.1.0" + "revision" : "ba3358d3c3dbae8ef230b58a46b97ad65e84e974", + "version" : "10.1.1" } }, { @@ -68,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", - "version" : "8.1.0" + "revision" : "92c8f6dc3ac375d6febdfcb3db68bc3d10633db3", + "version" : "8.1.3" } }, { @@ -86,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a", - "version" : "5.3.0" + "revision" : "724a52eea6329b7e12d3ad8300d76ca9f3895fcc", + "version" : "5.3.1" } }, { @@ -113,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/nanopb.git", "state" : { - "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", - "version" : "2.30910.0" + "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163", + "version" : "2.30910.1" } }, { @@ -122,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" + "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837", + "version" : "2.4.1" } }, { @@ -131,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae", - "version" : "8.58.0" + "revision" : "f196cbb98e0388381d57908f2f5a9bdc385cde68", + "version" : "8.58.4" } } ], diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index e6556149..864d7d77 100644 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,15 +27,6 @@ "version" : "12.18.0" } }, - { - "identity" : "flutterfire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/flutterfire", - "state" : { - "revision" : "a10a4148e769fadb01b1ff8d6bb76e9137f35b81", - "version" : "4.6.0-firebase-core-swift" - } - }, { "identity" : "google-ads-on-device-conversion-ios-sdk", "kind" : "remoteSourceControl", @@ -131,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae", - "version" : "8.58.0" + "revision" : "f196cbb98e0388381d57908f2f5a9bdc385cde68", + "version" : "8.58.4" } } ], diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index c6097586..84152847 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -64,6 +64,7 @@ import flutter_sharing_intent RuntimeStorageChannel.register(with: engineBridge.pluginRegistry) NativeNotificationChannel.register(with: engineBridge.pluginRegistry) WebxdcHostChannel.register(with: engineBridge.pluginRegistry) + NativeLocationPermission.register(with: engineBridge.pluginRegistry) } override func userNotificationCenter( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 4d8b0fe0..36c68595 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -62,7 +62,12 @@ NSPhotoLibraryAddUsageDescription twonly will save photos or videos to your library. NSLocationWhenInUseUsageDescription - This app does not use or store your location information. + If you enable location metadata, twonly uses your precise location to add it to Memories stored on this device and photos you export to your library. + NSLocationTemporaryUsageDescriptionDictionary + + MemoryLocation + Your precise location is needed to attach an accurate capture location to this Memory. + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/ios/Runner/NativeLocation.swift b/ios/Runner/NativeLocation.swift new file mode 100644 index 00000000..98c05806 --- /dev/null +++ b/ios/Runner/NativeLocation.swift @@ -0,0 +1,164 @@ +import CoreLocation +import Flutter +import Foundation + +private let memoryLocationPurpose = "MemoryLocation" + +/// Permission is requested only from the settings switch through this channel. +final class NativeLocationPermission: NSObject, CLLocationManagerDelegate { + private static var active: NativeLocationPermission? + private let manager = CLLocationManager() + private let completion: FlutterResult + + init(completion: @escaping FlutterResult) { + self.completion = completion + super.init() + manager.delegate = self + } + + static func register(with registry: FlutterPluginRegistry) { + guard let registrar = registry.registrar(forPlugin: "TwonlyLocationMetadata") else { return } + let channel = FlutterMethodChannel( + name: "eu.twonly/location_metadata", + binaryMessenger: registrar.messenger() + ) + channel.setMethodCallHandler { call, result in + guard call.method == "requestPrecisePermission" else { + result(FlutterMethodNotImplemented) + return + } + DispatchQueue.main.async { + guard active == nil else { + result(false) + return + } + let request = NativeLocationPermission(completion: result) + active = request + request.begin() + } + } + } + + private func begin() { + switch manager.authorizationStatus { + case .notDetermined: + manager.requestWhenInUseAuthorization() + case .authorizedAlways, .authorizedWhenInUse: + ensureFullAccuracy() + default: + finish(false) + } + } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + switch manager.authorizationStatus { + case .authorizedAlways, .authorizedWhenInUse: ensureFullAccuracy() + case .denied, .restricted: finish(false) + default: break + } + } + + private func ensureFullAccuracy() { + guard manager.accuracyAuthorization != .fullAccuracy else { + finish(true) + return + } + manager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: memoryLocationPurpose) { + [weak self] _ in + guard let self else { return } + self.finish(self.manager.accuracyAuthorization == .fullAccuracy) + } + } + + private func finish(_ granted: Bool) { + guard Self.active === self else { return } + Self.active = nil + completion(granted) + } +} + +private final class NativeLocationRequest: NSObject, CLLocationManagerDelegate { + private let manager = CLLocationManager() + private let completion: (CLLocation?) -> Void + private(set) var best: CLLocation? + private var finished = false + + init(completion: @escaping (CLLocation?) -> Void) { + self.completion = completion + super.init() + manager.delegate = self + manager.desiredAccuracy = kCLLocationAccuracyBest + manager.distanceFilter = kCLDistanceFilterNone + } + + func start() { + guard CLLocationManager.locationServicesEnabled(), + [.authorizedAlways, .authorizedWhenInUse].contains(manager.authorizationStatus), + manager.accuracyAuthorization == .fullAccuracy + else { + stop(returning: nil) + return + } + manager.startUpdatingLocation() + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + for location in locations where location.horizontalAccuracy >= 0 { + if best == nil || location.horizontalAccuracy < best!.horizontalAccuracy { + best = location + } + } + if let best, best.horizontalAccuracy <= 50 { + stop(returning: best) + } + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + if (error as? CLError)?.code != .locationUnknown { + stop(returning: best) + } + } + + func stop(returning location: CLLocation?) { + guard !finished else { return } + finished = true + manager.stopUpdatingLocation() + completion(location) + } +} + +private var activeNativeLocationRequest: NativeLocationRequest? + +/// Synchronous C ABI used from Rust's blocking location worker. +@_cdecl("twonly_current_location") +func twonlyCurrentLocation( + _ timeoutMillis: Int64, + _ latitude: UnsafeMutablePointer?, + _ longitude: UnsafeMutablePointer?, + _ accuracy: UnsafeMutablePointer? +) -> Bool { + guard let latitude, let longitude, let accuracy else { return false } + let finished = DispatchSemaphore(value: 0) + var found: CLLocation? + DispatchQueue.main.async { + let request = NativeLocationRequest { location in + found = location + activeNativeLocationRequest = nil + finished.signal() + } + activeNativeLocationRequest = request + request.start() + } + if finished.wait(timeout: .now() + .milliseconds(Int(max(0, timeoutMillis)))) == .timedOut { + DispatchQueue.main.sync { + found = activeNativeLocationRequest?.best + activeNativeLocationRequest?.stop(returning: found) + activeNativeLocationRequest = nil + } + } + guard let found else { return false } + latitude.pointee = found.coordinate.latitude + longitude.pointee = found.coordinate.longitude + accuracy.pointee = found.horizontalAccuracy + return true +} diff --git a/lib/core/bridge/api.dart b/lib/core/bridge/api.dart index 4676f9f7..56a38342 100644 --- a/lib/core/bridge/api.dart +++ b/lib/core/bridge/api.dart @@ -471,6 +471,13 @@ class RustApi { isDraftMedia: isDraftMedia, ); + /// Starts the optional location window for a Memory row created by the + /// local save button rather than the outgoing-media initializer. + static Future initializeMemoryLocation({required String mediaId}) => + RustLib.instance.api.crateBridgeApiRustApiInitializeMemoryLocation( + mediaId: mediaId, + ); + static Future insertAndSendAdditionalData({ required String groupId, required String messageType, @@ -568,6 +575,11 @@ class RustApi { .api .crateBridgeApiRustApiPrerenderMedia(mediaId: mediaId); + /// Begins the optional precise-location lookup when the capture camera is + /// opened, so a later photo usually has a fix immediately available. + static Future prewarmMemoryLocation() => + RustLib.instance.api.crateBridgeApiRustApiPrewarmMemoryLocation(); + /// Deletes temporary media whose messages are finished with it. static Future purgeMediaTempFolder() => RustLib.instance.api.crateBridgeApiRustApiPurgeMediaTempFolder(); diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index 315bd0c8..357ec571 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -87,7 +87,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => -1591152700; + int get rustContentHash => 101877534; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -333,6 +333,10 @@ abstract class RustLibApi extends BaseApi { required bool isDraftMedia, }); + Future crateBridgeApiRustApiInitializeMemoryLocation({ + required String mediaId, + }); + Future crateBridgeApiRustApiInsertAndSendAdditionalData({ required String groupId, required String messageType, @@ -385,6 +389,8 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeApiRustApiPrerenderMedia({required String mediaId}); + Future crateBridgeApiRustApiPrewarmMemoryLocation(); + Future crateBridgeApiRustApiPurgeMediaTempFolder(); Future crateBridgeApiRustApiPurgeWidgetMedia(); @@ -3061,6 +3067,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["mediaType", "displayLimitInMilliseconds", "isDraftMedia"], ); + @override + Future crateBridgeApiRustApiInitializeMemoryLocation({ + required String mediaId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(mediaId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 70, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeApiRustApiInitializeMemoryLocationConstMeta, + argValues: [mediaId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeApiRustApiInitializeMemoryLocationConstMeta => + const TaskConstMeta( + debugName: "rust_api_initialize_memory_location", + argNames: ["mediaId"], + ); + @override Future crateBridgeApiRustApiInsertAndSendAdditionalData({ required String groupId, @@ -3079,7 +3118,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 70, + funcId: 71, port: port_, ); }, @@ -3115,7 +3154,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 71, + funcId: 72, port: port_, ); }, @@ -3150,7 +3189,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 72, + funcId: 73, port: port_, ); }, @@ -3192,7 +3231,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 73, + funcId: 74, port: port_, ); }, @@ -3234,7 +3273,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 74, + funcId: 75, port: port_, ); }, @@ -3264,7 +3303,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 75, + funcId: 76, port: port_, ); }, @@ -3297,7 +3336,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 76, + funcId: 77, port: port_, ); }, @@ -3332,7 +3371,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 77, + funcId: 78, port: port_, ); }, @@ -3362,7 +3401,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 78, + funcId: 79, port: port_, ); }, @@ -3397,7 +3436,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 79, + funcId: 80, port: port_, ); }, @@ -3427,7 +3466,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 80, + funcId: 81, port: port_, ); }, @@ -3460,7 +3499,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 81, + funcId: 82, port: port_, ); }, @@ -3481,6 +3520,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["mediaId"], ); + @override + Future crateBridgeApiRustApiPrewarmMemoryLocation() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 83, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeApiRustApiPrewarmMemoryLocationConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeApiRustApiPrewarmMemoryLocationConstMeta => + const TaskConstMeta( + debugName: "rust_api_prewarm_memory_location", + argNames: [], + ); + @override Future crateBridgeApiRustApiPurgeMediaTempFolder() { return handler.executeNormal( @@ -3490,7 +3559,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 82, + funcId: 84, port: port_, ); }, @@ -3520,7 +3589,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 83, + funcId: 85, port: port_, ); }, @@ -3550,7 +3619,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 84, + funcId: 86, port: port_, ); }, @@ -3589,7 +3658,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 85, + funcId: 87, port: port_, ); }, @@ -3624,7 +3693,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 86, + funcId: 88, port: port_, ); }, @@ -3663,7 +3732,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 87, + funcId: 89, port: port_, ); }, @@ -3705,7 +3774,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 88, + funcId: 90, port: port_, ); }, @@ -3736,7 +3805,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 89, + funcId: 91, port: port_, ); }, @@ -3769,7 +3838,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 90, + funcId: 92, port: port_, ); }, @@ -3802,7 +3871,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 91, + funcId: 93, port: port_, ); }, @@ -3837,7 +3906,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 92, + funcId: 94, port: port_, ); }, @@ -3870,7 +3939,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 93, + funcId: 95, port: port_, ); }, @@ -3903,7 +3972,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 94, + funcId: 96, port: port_, ); }, @@ -3936,7 +4005,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 95, + funcId: 97, port: port_, ); }, @@ -3973,7 +4042,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 96, + funcId: 98, port: port_, ); }, @@ -4003,7 +4072,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 97, + funcId: 99, port: port_, ); }, @@ -4033,7 +4102,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 98, + funcId: 100, port: port_, ); }, @@ -4063,7 +4132,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 99, + funcId: 101, port: port_, ); }, @@ -4096,7 +4165,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 102, port: port_, ); }, @@ -4127,7 +4196,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 103, port: port_, ); }, @@ -4160,7 +4229,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 104, port: port_, ); }, @@ -4206,7 +4275,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 105, port: port_, ); }, @@ -4262,7 +4331,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 106, port: port_, ); }, @@ -4310,7 +4379,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 107, port: port_, ); }, @@ -4348,7 +4417,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 106, + funcId: 108, port: port_, ); }, @@ -4383,7 +4452,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 107, + funcId: 109, port: port_, ); }, @@ -4418,7 +4487,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 108, + funcId: 110, port: port_, ); }, @@ -4451,7 +4520,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 109, + funcId: 111, port: port_, ); }, @@ -4488,7 +4557,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 110, + funcId: 112, port: port_, ); }, @@ -4519,7 +4588,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 111, + funcId: 113, port: port_, ); }, @@ -4557,7 +4626,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 112, + funcId: 114, port: port_, ); }, @@ -4592,7 +4661,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 113, + funcId: 115, port: port_, ); }, @@ -4631,7 +4700,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 114, + funcId: 116, port: port_, ); }, @@ -4664,7 +4733,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 115, + funcId: 117, port: port_, ); }, @@ -4695,7 +4764,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 116, + funcId: 118, port: port_, ); }, @@ -4730,7 +4799,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 117, + funcId: 119, port: port_, ); }, @@ -4760,7 +4829,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 118, + funcId: 120, port: port_, ); }, @@ -4793,7 +4862,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 119, + funcId: 121, port: port_, ); }, @@ -4828,7 +4897,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 120, + funcId: 122, port: port_, ); }, @@ -4861,7 +4930,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 121, + funcId: 123, port: port_, ); }, @@ -4892,7 +4961,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 122, + funcId: 124, port: port_, ); }, @@ -4929,7 +4998,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 123, + funcId: 125, port: port_, ); }, @@ -4981,7 +5050,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 124, + funcId: 126, port: port_, ); }, @@ -5034,7 +5103,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 125, + funcId: 127, port: port_, ); }, @@ -5074,7 +5143,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 126, + funcId: 128, port: port_, ); }, @@ -5107,7 +5176,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 127, + funcId: 129, port: port_, ); }, @@ -5140,7 +5209,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 128, + funcId: 130, port: port_, ); }, @@ -5177,7 +5246,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 129, + funcId: 131, port: port_, ); }, @@ -5209,7 +5278,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 130, + funcId: 132, port: port_, ); }, @@ -5242,7 +5311,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 131, + funcId: 133, port: port_, ); }, @@ -5277,7 +5346,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 132, + funcId: 134, port: port_, ); }, @@ -5309,7 +5378,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 133, + funcId: 135, port: port_, ); }, @@ -5347,7 +5416,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 134, + funcId: 136, port: port_, ); }, @@ -5380,7 +5449,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 135, + funcId: 137, port: port_, ); }, @@ -5418,7 +5487,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 136, + funcId: 138, port: port_, ); }, @@ -5455,7 +5524,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 137, + funcId: 139, port: port_, ); }, @@ -5492,7 +5561,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 138, + funcId: 140, port: port_, ); }, @@ -5530,7 +5599,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 139, + funcId: 141, port: port_, ); }, @@ -5568,7 +5637,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 140, + funcId: 142, port: port_, ); }, @@ -5601,7 +5670,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 141, + funcId: 143, port: port_, ); }, @@ -5633,7 +5702,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 142, + funcId: 144, port: port_, ); }, @@ -5668,7 +5737,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 143, + funcId: 145, port: port_, ); }, @@ -5705,7 +5774,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 144, + funcId: 146, port: port_, ); }, @@ -5738,7 +5807,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 145, + funcId: 147, port: port_, ); }, @@ -5770,7 +5839,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 146, + funcId: 148, port: port_, ); }, @@ -5805,7 +5874,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 147, + funcId: 149, port: port_, ); }, @@ -5844,7 +5913,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 148, + funcId: 150, port: port_, ); }, @@ -5881,7 +5950,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 149, + funcId: 151, port: port_, ); }, @@ -5911,7 +5980,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 150, + funcId: 152, port: port_, ); }, @@ -5943,7 +6012,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 151, + funcId: 153, port: port_, ); }, @@ -5978,7 +6047,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 152, + funcId: 154, port: port_, ); }, @@ -6010,7 +6079,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 153, + funcId: 155, port: port_, ); }, @@ -6048,7 +6117,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 154, + funcId: 156, port: port_, ); }, @@ -6087,7 +6156,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 155, + funcId: 157, port: port_, ); }, @@ -6122,7 +6191,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 156, + funcId: 158, port: port_, ); }, @@ -6167,7 +6236,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 157, + funcId: 159, port: port_, ); }, @@ -6210,7 +6279,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 158, + funcId: 160, port: port_, ); }, @@ -6244,7 +6313,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 159, + funcId: 161, port: port_, ); }, @@ -6279,7 +6348,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 160, + funcId: 162, port: port_, ); }, @@ -6314,7 +6383,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 161, + funcId: 163, port: port_, ); }, @@ -6347,7 +6416,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 162, + funcId: 164, )!; }, codec: SseCodec( @@ -6387,7 +6456,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 163, + funcId: 165, port: port_, ); }, @@ -6432,7 +6501,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 164, + funcId: 166, port: port_, ); }, @@ -6462,7 +6531,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 165, + funcId: 167, port: port_, ); }, @@ -6495,7 +6564,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 166, + funcId: 168, port: port_, ); }, @@ -6530,7 +6599,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 167, + funcId: 169, port: port_, ); }, @@ -6569,7 +6638,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 168, + funcId: 170, )!; }, codec: SseCodec( @@ -7577,8 +7646,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { UserConfig dco_decode_user_config(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 61) - throw Exception('unexpected arr length: expect 61 but see ${arr.length}'); + if (arr.length != 62) + throw Exception('unexpected arr length: expect 62 but see ${arr.length}'); return UserConfig( userId: dco_decode_i_64(arr[0]), username: dco_decode_String(arr[1]), @@ -7608,44 +7677,45 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { preSelectedEmojies: dco_decode_opt_list_String(arr[25]), autoDownloadOptions: dco_decode_opt_Map_String_list_String_None(arr[26]), storeMediaFilesInGallery: dco_decode_bool(arr[27]), - autoStoreAllSendUnlimitedMediaFiles: dco_decode_bool(arr[28]), - typingIndicators: dco_decode_bool(arr[29]), - showRestoreFlame: dco_decode_bool(arr[30]), - myBestFriendGroupId: dco_decode_opt_String(arr[31]), + storeLocationInMemories: dco_decode_bool(arr[28]), + autoStoreAllSendUnlimitedMediaFiles: dco_decode_bool(arr[29]), + typingIndicators: dco_decode_bool(arr[30]), + showRestoreFlame: dco_decode_bool(arr[31]), + myBestFriendGroupId: dco_decode_opt_String(arr[32]), signalLastSignedPreKeyUpdated: dco_decode_opt_box_autoadd_Chrono_Utc( - arr[32], - ), - signalLastPqcPreKeysUploaded: dco_decode_opt_box_autoadd_Chrono_Utc( arr[33], ), - allowErrorTrackingViaSentry: dco_decode_bool(arr[34]), - screenLockEnabled: dco_decode_bool(arr[35]), - isCloudBackupEnabled: dco_decode_bool(arr[36]), - isUserDiscoveryEnabled: dco_decode_bool(arr[37]), - requiredSendImages: dco_decode_i_64(arr[38]), - userDiscoveryThreshold: dco_decode_u_8(arr[39]), - userDiscoveryRequiresManualApproval: dco_decode_bool(arr[40]), - userDiscoverySharePromotion: dco_decode_bool(arr[41]), - userDiscoveryInitializationError: dco_decode_bool(arr[42]), - askForFriendPromotions: dco_decode_bool(arr[43]), - currentPreKeyIndexStart: dco_decode_i_64(arr[44]), - currentSignedPreKeyIndexStart: dco_decode_i_64(arr[45]), - lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[46]), - hideChangeLog: dco_decode_bool(arr[47]), - hideMemoriesBackupPromo: dco_decode_bool(arr[48]), - hideWidgetShareExplainer: dco_decode_bool(arr[49]), - updateFcmToken: dco_decode_bool(arr[50]), - canUseLoginTokenForAuth: dco_decode_bool(arr[51]), - twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[52]), - isBackupEnabled: dco_decode_bool(arr[53]), + signalLastPqcPreKeysUploaded: dco_decode_opt_box_autoadd_Chrono_Utc( + arr[34], + ), + allowErrorTrackingViaSentry: dco_decode_bool(arr[35]), + screenLockEnabled: dco_decode_bool(arr[36]), + isCloudBackupEnabled: dco_decode_bool(arr[37]), + isUserDiscoveryEnabled: dco_decode_bool(arr[38]), + requiredSendImages: dco_decode_i_64(arr[39]), + userDiscoveryThreshold: dco_decode_u_8(arr[40]), + userDiscoveryRequiresManualApproval: dco_decode_bool(arr[41]), + userDiscoverySharePromotion: dco_decode_bool(arr[42]), + userDiscoveryInitializationError: dco_decode_bool(arr[43]), + askForFriendPromotions: dco_decode_bool(arr[44]), + currentPreKeyIndexStart: dco_decode_i_64(arr[45]), + currentSignedPreKeyIndexStart: dco_decode_i_64(arr[46]), + lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[47]), + hideChangeLog: dco_decode_bool(arr[48]), + hideMemoriesBackupPromo: dco_decode_bool(arr[49]), + hideWidgetShareExplainer: dco_decode_bool(arr[50]), + updateFcmToken: dco_decode_bool(arr[51]), + canUseLoginTokenForAuth: dco_decode_bool(arr[52]), + twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[53]), + isBackupEnabled: dco_decode_bool(arr[54]), passwordLessRecovery: - dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[54]), - fcmToken: dco_decode_opt_String(arr[55]), - lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[56]), - lastServerMessageAt: dco_decode_opt_box_autoadd_i_64(arr[57]), - currentSetupPage: dco_decode_opt_String(arr[58]), - skipSetupPages: dco_decode_bool(arr[59]), - hasZoomed: dco_decode_bool(arr[60]), + dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[55]), + fcmToken: dco_decode_opt_String(arr[56]), + lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[57]), + lastServerMessageAt: dco_decode_opt_box_autoadd_i_64(arr[58]), + currentSetupPage: dco_decode_opt_String(arr[59]), + skipSetupPages: dco_decode_bool(arr[60]), + hasZoomed: dco_decode_bool(arr[61]), ); } @@ -8880,6 +8950,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { deserializer, ); var var_storeMediaFilesInGallery = sse_decode_bool(deserializer); + var var_storeLocationInMemories = sse_decode_bool(deserializer); var var_autoStoreAllSendUnlimitedMediaFiles = sse_decode_bool(deserializer); var var_typingIndicators = sse_decode_bool(deserializer); var var_showRestoreFlame = sse_decode_bool(deserializer); @@ -8950,6 +9021,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { preSelectedEmojies: var_preSelectedEmojies, autoDownloadOptions: var_autoDownloadOptions, storeMediaFilesInGallery: var_storeMediaFilesInGallery, + storeLocationInMemories: var_storeLocationInMemories, autoStoreAllSendUnlimitedMediaFiles: var_autoStoreAllSendUnlimitedMediaFiles, typingIndicators: var_typingIndicators, @@ -10198,6 +10270,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_bool(self.storeMediaFilesInGallery, serializer); + sse_encode_bool(self.storeLocationInMemories, serializer); sse_encode_bool(self.autoStoreAllSendUnlimitedMediaFiles, serializer); sse_encode_bool(self.typingIndicators, serializer); sse_encode_bool(self.showRestoreFlame, serializer); diff --git a/lib/core/user_config.dart b/lib/core/user_config.dart index 6af0319f..8a0b14c3 100644 --- a/lib/core/user_config.dart +++ b/lib/core/user_config.dart @@ -131,6 +131,10 @@ class UserConfig { List? preSelectedEmojies; Map>? autoDownloadOptions; bool storeMediaFilesInGallery; + + /// Adds the capture location to locally stored Memories and gallery + /// exports. Disabled by default and never added to outgoing media. + bool storeLocationInMemories; bool autoStoreAllSendUnlimitedMediaFiles; bool typingIndicators; bool showRestoreFlame; @@ -202,6 +206,7 @@ class UserConfig { this.preSelectedEmojies, this.autoDownloadOptions, required this.storeMediaFilesInGallery, + required this.storeLocationInMemories, required this.autoStoreAllSendUnlimitedMediaFiles, required this.typingIndicators, required this.showRestoreFlame, @@ -267,6 +272,7 @@ class UserConfig { preSelectedEmojies.hashCode ^ autoDownloadOptions.hashCode ^ storeMediaFilesInGallery.hashCode ^ + storeLocationInMemories.hashCode ^ autoStoreAllSendUnlimitedMediaFiles.hashCode ^ typingIndicators.hashCode ^ showRestoreFlame.hashCode ^ @@ -336,6 +342,7 @@ class UserConfig { preSelectedEmojies == other.preSelectedEmojies && autoDownloadOptions == other.autoDownloadOptions && storeMediaFilesInGallery == other.storeMediaFilesInGallery && + storeLocationInMemories == other.storeLocationInMemories && autoStoreAllSendUnlimitedMediaFiles == other.autoStoreAllSendUnlimitedMediaFiles && typingIndicators == other.typingIndicators && diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index f4e212a7..5fe0f731 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -91,6 +91,14 @@ class MediaFiles extends Table { IntColumn get sizeInBytes => integer().nullable()(); + RealColumn get locationLatitude => real().nullable()(); + RealColumn get locationLongitude => real().nullable()(); + RealColumn get locationAccuracy => real().nullable()(); + TextColumn get locationStatus => text().nullable()(); + IntColumn get locationDeadlineAt => integer().nullable()(); + BoolColumn get galleryExportPending => + boolean().withDefault(const Constant(false))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); TextColumn get createdAtMonth => text().nullable()(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 48d99d8f..23366fed 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -3536,6 +3536,75 @@ class $MediaFilesTable extends MediaFiles type: DriftSqlType.int, requiredDuringInsert: false, ); + static const VerificationMeta _locationLatitudeMeta = const VerificationMeta( + 'locationLatitude', + ); + @override + late final GeneratedColumn locationLatitude = GeneratedColumn( + 'location_latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + static const VerificationMeta _locationLongitudeMeta = const VerificationMeta( + 'locationLongitude', + ); + @override + late final GeneratedColumn locationLongitude = + GeneratedColumn( + 'location_longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + static const VerificationMeta _locationAccuracyMeta = const VerificationMeta( + 'locationAccuracy', + ); + @override + late final GeneratedColumn locationAccuracy = GeneratedColumn( + 'location_accuracy', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + static const VerificationMeta _locationStatusMeta = const VerificationMeta( + 'locationStatus', + ); + @override + late final GeneratedColumn locationStatus = GeneratedColumn( + 'location_status', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _locationDeadlineAtMeta = + const VerificationMeta('locationDeadlineAt'); + @override + late final GeneratedColumn locationDeadlineAt = GeneratedColumn( + 'location_deadline_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _galleryExportPendingMeta = + const VerificationMeta('galleryExportPending'); + @override + late final GeneratedColumn galleryExportPending = GeneratedColumn( + 'gallery_export_pending', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("gallery_export_pending" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); static const VerificationMeta _createdAtMeta = const VerificationMeta( 'createdAt', ); @@ -3586,6 +3655,12 @@ class $MediaFilesTable extends MediaFiles storedFileHash, hasThumbnail, sizeInBytes, + locationLatitude, + locationLongitude, + locationAccuracy, + locationStatus, + locationDeadlineAt, + galleryExportPending, createdAt, createdAtMonth, ]; @@ -3768,6 +3843,60 @@ class $MediaFilesTable extends MediaFiles ), ); } + if (data.containsKey('location_latitude')) { + context.handle( + _locationLatitudeMeta, + locationLatitude.isAcceptableOrUnknown( + data['location_latitude']!, + _locationLatitudeMeta, + ), + ); + } + if (data.containsKey('location_longitude')) { + context.handle( + _locationLongitudeMeta, + locationLongitude.isAcceptableOrUnknown( + data['location_longitude']!, + _locationLongitudeMeta, + ), + ); + } + if (data.containsKey('location_accuracy')) { + context.handle( + _locationAccuracyMeta, + locationAccuracy.isAcceptableOrUnknown( + data['location_accuracy']!, + _locationAccuracyMeta, + ), + ); + } + if (data.containsKey('location_status')) { + context.handle( + _locationStatusMeta, + locationStatus.isAcceptableOrUnknown( + data['location_status']!, + _locationStatusMeta, + ), + ); + } + if (data.containsKey('location_deadline_at')) { + context.handle( + _locationDeadlineAtMeta, + locationDeadlineAt.isAcceptableOrUnknown( + data['location_deadline_at']!, + _locationDeadlineAtMeta, + ), + ); + } + if (data.containsKey('gallery_export_pending')) { + context.handle( + _galleryExportPendingMeta, + galleryExportPending.isAcceptableOrUnknown( + data['gallery_export_pending']!, + _galleryExportPendingMeta, + ), + ); + } if (data.containsKey('created_at')) { context.handle( _createdAtMeta, @@ -3903,6 +4032,30 @@ class $MediaFilesTable extends MediaFiles DriftSqlType.int, data['${effectivePrefix}size_in_bytes'], ), + locationLatitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}location_latitude'], + ), + locationLongitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}location_longitude'], + ), + locationAccuracy: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}location_accuracy'], + ), + locationStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}location_status'], + ), + locationDeadlineAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}location_deadline_at'], + ), + galleryExportPending: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}gallery_export_pending'], + )!, createdAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}created_at'], @@ -3972,6 +4125,12 @@ class MediaFile extends DataClass implements Insertable { final Uint8List? storedFileHash; final bool hasThumbnail; final int? sizeInBytes; + final double? locationLatitude; + final double? locationLongitude; + final double? locationAccuracy; + final String? locationStatus; + final int? locationDeadlineAt; + final bool galleryExportPending; final DateTime createdAt; final String? createdAtMonth; const MediaFile({ @@ -4000,6 +4159,12 @@ class MediaFile extends DataClass implements Insertable { this.storedFileHash, required this.hasThumbnail, this.sizeInBytes, + this.locationLatitude, + this.locationLongitude, + this.locationAccuracy, + this.locationStatus, + this.locationDeadlineAt, + required this.galleryExportPending, required this.createdAt, this.createdAtMonth, }); @@ -4079,6 +4244,22 @@ class MediaFile extends DataClass implements Insertable { if (!nullToAbsent || sizeInBytes != null) { map['size_in_bytes'] = Variable(sizeInBytes); } + if (!nullToAbsent || locationLatitude != null) { + map['location_latitude'] = Variable(locationLatitude); + } + if (!nullToAbsent || locationLongitude != null) { + map['location_longitude'] = Variable(locationLongitude); + } + if (!nullToAbsent || locationAccuracy != null) { + map['location_accuracy'] = Variable(locationAccuracy); + } + if (!nullToAbsent || locationStatus != null) { + map['location_status'] = Variable(locationStatus); + } + if (!nullToAbsent || locationDeadlineAt != null) { + map['location_deadline_at'] = Variable(locationDeadlineAt); + } + map['gallery_export_pending'] = Variable(galleryExportPending); map['created_at'] = Variable(createdAt); if (!nullToAbsent || createdAtMonth != null) { map['created_at_month'] = Variable(createdAtMonth); @@ -4144,6 +4325,22 @@ class MediaFile extends DataClass implements Insertable { sizeInBytes: sizeInBytes == null && nullToAbsent ? const Value.absent() : Value(sizeInBytes), + locationLatitude: locationLatitude == null && nullToAbsent + ? const Value.absent() + : Value(locationLatitude), + locationLongitude: locationLongitude == null && nullToAbsent + ? const Value.absent() + : Value(locationLongitude), + locationAccuracy: locationAccuracy == null && nullToAbsent + ? const Value.absent() + : Value(locationAccuracy), + locationStatus: locationStatus == null && nullToAbsent + ? const Value.absent() + : Value(locationStatus), + locationDeadlineAt: locationDeadlineAt == null && nullToAbsent + ? const Value.absent() + : Value(locationDeadlineAt), + galleryExportPending: Value(galleryExportPending), createdAt: Value(createdAt), createdAtMonth: createdAtMonth == null && nullToAbsent ? const Value.absent() @@ -4198,6 +4395,16 @@ class MediaFile extends DataClass implements Insertable { storedFileHash: serializer.fromJson(json['storedFileHash']), hasThumbnail: serializer.fromJson(json['hasThumbnail']), sizeInBytes: serializer.fromJson(json['sizeInBytes']), + locationLatitude: serializer.fromJson(json['locationLatitude']), + locationLongitude: serializer.fromJson( + json['locationLongitude'], + ), + locationAccuracy: serializer.fromJson(json['locationAccuracy']), + locationStatus: serializer.fromJson(json['locationStatus']), + locationDeadlineAt: serializer.fromJson(json['locationDeadlineAt']), + galleryExportPending: serializer.fromJson( + json['galleryExportPending'], + ), createdAt: serializer.fromJson(json['createdAt']), createdAtMonth: serializer.fromJson(json['createdAtMonth']), ); @@ -4241,6 +4448,12 @@ class MediaFile extends DataClass implements Insertable { 'storedFileHash': serializer.toJson(storedFileHash), 'hasThumbnail': serializer.toJson(hasThumbnail), 'sizeInBytes': serializer.toJson(sizeInBytes), + 'locationLatitude': serializer.toJson(locationLatitude), + 'locationLongitude': serializer.toJson(locationLongitude), + 'locationAccuracy': serializer.toJson(locationAccuracy), + 'locationStatus': serializer.toJson(locationStatus), + 'locationDeadlineAt': serializer.toJson(locationDeadlineAt), + 'galleryExportPending': serializer.toJson(galleryExportPending), 'createdAt': serializer.toJson(createdAt), 'createdAtMonth': serializer.toJson(createdAtMonth), }; @@ -4272,6 +4485,12 @@ class MediaFile extends DataClass implements Insertable { Value storedFileHash = const Value.absent(), bool? hasThumbnail, Value sizeInBytes = const Value.absent(), + Value locationLatitude = const Value.absent(), + Value locationLongitude = const Value.absent(), + Value locationAccuracy = const Value.absent(), + Value locationStatus = const Value.absent(), + Value locationDeadlineAt = const Value.absent(), + bool? galleryExportPending, DateTime? createdAt, Value createdAtMonth = const Value.absent(), }) => MediaFile( @@ -4319,6 +4538,22 @@ class MediaFile extends DataClass implements Insertable { : this.storedFileHash, hasThumbnail: hasThumbnail ?? this.hasThumbnail, sizeInBytes: sizeInBytes.present ? sizeInBytes.value : this.sizeInBytes, + locationLatitude: locationLatitude.present + ? locationLatitude.value + : this.locationLatitude, + locationLongitude: locationLongitude.present + ? locationLongitude.value + : this.locationLongitude, + locationAccuracy: locationAccuracy.present + ? locationAccuracy.value + : this.locationAccuracy, + locationStatus: locationStatus.present + ? locationStatus.value + : this.locationStatus, + locationDeadlineAt: locationDeadlineAt.present + ? locationDeadlineAt.value + : this.locationDeadlineAt, + galleryExportPending: galleryExportPending ?? this.galleryExportPending, createdAt: createdAt ?? this.createdAt, createdAtMonth: createdAtMonth.present ? createdAtMonth.value @@ -4391,6 +4626,24 @@ class MediaFile extends DataClass implements Insertable { sizeInBytes: data.sizeInBytes.present ? data.sizeInBytes.value : this.sizeInBytes, + locationLatitude: data.locationLatitude.present + ? data.locationLatitude.value + : this.locationLatitude, + locationLongitude: data.locationLongitude.present + ? data.locationLongitude.value + : this.locationLongitude, + locationAccuracy: data.locationAccuracy.present + ? data.locationAccuracy.value + : this.locationAccuracy, + locationStatus: data.locationStatus.present + ? data.locationStatus.value + : this.locationStatus, + locationDeadlineAt: data.locationDeadlineAt.present + ? data.locationDeadlineAt.value + : this.locationDeadlineAt, + galleryExportPending: data.galleryExportPending.present + ? data.galleryExportPending.value + : this.galleryExportPending, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, createdAtMonth: data.createdAtMonth.present ? data.createdAtMonth.value @@ -4426,6 +4679,12 @@ class MediaFile extends DataClass implements Insertable { ..write('storedFileHash: $storedFileHash, ') ..write('hasThumbnail: $hasThumbnail, ') ..write('sizeInBytes: $sizeInBytes, ') + ..write('locationLatitude: $locationLatitude, ') + ..write('locationLongitude: $locationLongitude, ') + ..write('locationAccuracy: $locationAccuracy, ') + ..write('locationStatus: $locationStatus, ') + ..write('locationDeadlineAt: $locationDeadlineAt, ') + ..write('galleryExportPending: $galleryExportPending, ') ..write('createdAt: $createdAt, ') ..write('createdAtMonth: $createdAtMonth') ..write(')')) @@ -4459,6 +4718,12 @@ class MediaFile extends DataClass implements Insertable { $driftBlobEquality.hash(storedFileHash), hasThumbnail, sizeInBytes, + locationLatitude, + locationLongitude, + locationAccuracy, + locationStatus, + locationDeadlineAt, + galleryExportPending, createdAt, createdAtMonth, ]); @@ -4497,6 +4762,12 @@ class MediaFile extends DataClass implements Insertable { ) && other.hasThumbnail == this.hasThumbnail && other.sizeInBytes == this.sizeInBytes && + other.locationLatitude == this.locationLatitude && + other.locationLongitude == this.locationLongitude && + other.locationAccuracy == this.locationAccuracy && + other.locationStatus == this.locationStatus && + other.locationDeadlineAt == this.locationDeadlineAt && + other.galleryExportPending == this.galleryExportPending && other.createdAt == this.createdAt && other.createdAtMonth == this.createdAtMonth); } @@ -4527,6 +4798,12 @@ class MediaFilesCompanion extends UpdateCompanion { final Value storedFileHash; final Value hasThumbnail; final Value sizeInBytes; + final Value locationLatitude; + final Value locationLongitude; + final Value locationAccuracy; + final Value locationStatus; + final Value locationDeadlineAt; + final Value galleryExportPending; final Value createdAt; final Value createdAtMonth; final Value rowid; @@ -4556,6 +4833,12 @@ class MediaFilesCompanion extends UpdateCompanion { this.storedFileHash = const Value.absent(), this.hasThumbnail = const Value.absent(), this.sizeInBytes = const Value.absent(), + this.locationLatitude = const Value.absent(), + this.locationLongitude = const Value.absent(), + this.locationAccuracy = const Value.absent(), + this.locationStatus = const Value.absent(), + this.locationDeadlineAt = const Value.absent(), + this.galleryExportPending = const Value.absent(), this.createdAt = const Value.absent(), this.createdAtMonth = const Value.absent(), this.rowid = const Value.absent(), @@ -4586,6 +4869,12 @@ class MediaFilesCompanion extends UpdateCompanion { this.storedFileHash = const Value.absent(), this.hasThumbnail = const Value.absent(), this.sizeInBytes = const Value.absent(), + this.locationLatitude = const Value.absent(), + this.locationLongitude = const Value.absent(), + this.locationAccuracy = const Value.absent(), + this.locationStatus = const Value.absent(), + this.locationDeadlineAt = const Value.absent(), + this.galleryExportPending = const Value.absent(), this.createdAt = const Value.absent(), this.createdAtMonth = const Value.absent(), this.rowid = const Value.absent(), @@ -4617,6 +4906,12 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? storedFileHash, Expression? hasThumbnail, Expression? sizeInBytes, + Expression? locationLatitude, + Expression? locationLongitude, + Expression? locationAccuracy, + Expression? locationStatus, + Expression? locationDeadlineAt, + Expression? galleryExportPending, Expression? createdAt, Expression? createdAtMonth, Expression? rowid, @@ -4651,6 +4946,14 @@ class MediaFilesCompanion extends UpdateCompanion { if (storedFileHash != null) 'stored_file_hash': storedFileHash, if (hasThumbnail != null) 'has_thumbnail': hasThumbnail, if (sizeInBytes != null) 'size_in_bytes': sizeInBytes, + if (locationLatitude != null) 'location_latitude': locationLatitude, + if (locationLongitude != null) 'location_longitude': locationLongitude, + if (locationAccuracy != null) 'location_accuracy': locationAccuracy, + if (locationStatus != null) 'location_status': locationStatus, + if (locationDeadlineAt != null) + 'location_deadline_at': locationDeadlineAt, + if (galleryExportPending != null) + 'gallery_export_pending': galleryExportPending, if (createdAt != null) 'created_at': createdAt, if (createdAtMonth != null) 'created_at_month': createdAtMonth, if (rowid != null) 'rowid': rowid, @@ -4683,6 +4986,12 @@ class MediaFilesCompanion extends UpdateCompanion { Value? storedFileHash, Value? hasThumbnail, Value? sizeInBytes, + Value? locationLatitude, + Value? locationLongitude, + Value? locationAccuracy, + Value? locationStatus, + Value? locationDeadlineAt, + Value? galleryExportPending, Value? createdAt, Value? createdAtMonth, Value? rowid, @@ -4716,6 +5025,12 @@ class MediaFilesCompanion extends UpdateCompanion { storedFileHash: storedFileHash ?? this.storedFileHash, hasThumbnail: hasThumbnail ?? this.hasThumbnail, sizeInBytes: sizeInBytes ?? this.sizeInBytes, + locationLatitude: locationLatitude ?? this.locationLatitude, + locationLongitude: locationLongitude ?? this.locationLongitude, + locationAccuracy: locationAccuracy ?? this.locationAccuracy, + locationStatus: locationStatus ?? this.locationStatus, + locationDeadlineAt: locationDeadlineAt ?? this.locationDeadlineAt, + galleryExportPending: galleryExportPending ?? this.galleryExportPending, createdAt: createdAt ?? this.createdAt, createdAtMonth: createdAtMonth ?? this.createdAtMonth, rowid: rowid ?? this.rowid, @@ -4818,6 +5133,26 @@ class MediaFilesCompanion extends UpdateCompanion { if (sizeInBytes.present) { map['size_in_bytes'] = Variable(sizeInBytes.value); } + if (locationLatitude.present) { + map['location_latitude'] = Variable(locationLatitude.value); + } + if (locationLongitude.present) { + map['location_longitude'] = Variable(locationLongitude.value); + } + if (locationAccuracy.present) { + map['location_accuracy'] = Variable(locationAccuracy.value); + } + if (locationStatus.present) { + map['location_status'] = Variable(locationStatus.value); + } + if (locationDeadlineAt.present) { + map['location_deadline_at'] = Variable(locationDeadlineAt.value); + } + if (galleryExportPending.present) { + map['gallery_export_pending'] = Variable( + galleryExportPending.value, + ); + } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } @@ -4858,6 +5193,12 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('storedFileHash: $storedFileHash, ') ..write('hasThumbnail: $hasThumbnail, ') ..write('sizeInBytes: $sizeInBytes, ') + ..write('locationLatitude: $locationLatitude, ') + ..write('locationLongitude: $locationLongitude, ') + ..write('locationAccuracy: $locationAccuracy, ') + ..write('locationStatus: $locationStatus, ') + ..write('locationDeadlineAt: $locationDeadlineAt, ') + ..write('galleryExportPending: $galleryExportPending, ') ..write('createdAt: $createdAt, ') ..write('createdAtMonth: $createdAtMonth, ') ..write('rowid: $rowid') @@ -17839,6 +18180,12 @@ typedef $$MediaFilesTableCreateCompanionBuilder = Value storedFileHash, Value hasThumbnail, Value sizeInBytes, + Value locationLatitude, + Value locationLongitude, + Value locationAccuracy, + Value locationStatus, + Value locationDeadlineAt, + Value galleryExportPending, Value createdAt, Value createdAtMonth, Value rowid, @@ -17870,6 +18217,12 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = Value storedFileHash, Value hasThumbnail, Value sizeInBytes, + Value locationLatitude, + Value locationLongitude, + Value locationAccuracy, + Value locationStatus, + Value locationDeadlineAt, + Value galleryExportPending, Value createdAt, Value createdAtMonth, Value rowid, @@ -18037,6 +18390,36 @@ class $$MediaFilesTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get locationLatitude => $composableBuilder( + column: $table.locationLatitude, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get locationLongitude => $composableBuilder( + column: $table.locationLongitude, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get locationAccuracy => $composableBuilder( + column: $table.locationAccuracy, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get locationStatus => $composableBuilder( + column: $table.locationStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get locationDeadlineAt => $composableBuilder( + column: $table.locationDeadlineAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get galleryExportPending => $composableBuilder( + column: $table.galleryExportPending, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column), @@ -18207,6 +18590,36 @@ class $$MediaFilesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get locationLatitude => $composableBuilder( + column: $table.locationLatitude, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get locationLongitude => $composableBuilder( + column: $table.locationLongitude, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get locationAccuracy => $composableBuilder( + column: $table.locationAccuracy, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get locationStatus => $composableBuilder( + column: $table.locationStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get locationDeadlineAt => $composableBuilder( + column: $table.locationDeadlineAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get galleryExportPending => $composableBuilder( + column: $table.galleryExportPending, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column), @@ -18346,6 +18759,36 @@ class $$MediaFilesTableAnnotationComposer builder: (column) => column, ); + GeneratedColumn get locationLatitude => $composableBuilder( + column: $table.locationLatitude, + builder: (column) => column, + ); + + GeneratedColumn get locationLongitude => $composableBuilder( + column: $table.locationLongitude, + builder: (column) => column, + ); + + GeneratedColumn get locationAccuracy => $composableBuilder( + column: $table.locationAccuracy, + builder: (column) => column, + ); + + GeneratedColumn get locationStatus => $composableBuilder( + column: $table.locationStatus, + builder: (column) => column, + ); + + GeneratedColumn get locationDeadlineAt => $composableBuilder( + column: $table.locationDeadlineAt, + builder: (column) => column, + ); + + GeneratedColumn get galleryExportPending => $composableBuilder( + column: $table.galleryExportPending, + builder: (column) => column, + ); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); @@ -18433,6 +18876,12 @@ class $$MediaFilesTableTableManager Value storedFileHash = const Value.absent(), Value hasThumbnail = const Value.absent(), Value sizeInBytes = const Value.absent(), + Value locationLatitude = const Value.absent(), + Value locationLongitude = const Value.absent(), + Value locationAccuracy = const Value.absent(), + Value locationStatus = const Value.absent(), + Value locationDeadlineAt = const Value.absent(), + Value galleryExportPending = const Value.absent(), Value createdAt = const Value.absent(), Value createdAtMonth = const Value.absent(), Value rowid = const Value.absent(), @@ -18462,6 +18911,12 @@ class $$MediaFilesTableTableManager storedFileHash: storedFileHash, hasThumbnail: hasThumbnail, sizeInBytes: sizeInBytes, + locationLatitude: locationLatitude, + locationLongitude: locationLongitude, + locationAccuracy: locationAccuracy, + locationStatus: locationStatus, + locationDeadlineAt: locationDeadlineAt, + galleryExportPending: galleryExportPending, createdAt: createdAt, createdAtMonth: createdAtMonth, rowid: rowid, @@ -18493,6 +18948,12 @@ class $$MediaFilesTableTableManager Value storedFileHash = const Value.absent(), Value hasThumbnail = const Value.absent(), Value sizeInBytes = const Value.absent(), + Value locationLatitude = const Value.absent(), + Value locationLongitude = const Value.absent(), + Value locationAccuracy = const Value.absent(), + Value locationStatus = const Value.absent(), + Value locationDeadlineAt = const Value.absent(), + Value galleryExportPending = const Value.absent(), Value createdAt = const Value.absent(), Value createdAtMonth = const Value.absent(), Value rowid = const Value.absent(), @@ -18522,6 +18983,12 @@ class $$MediaFilesTableTableManager storedFileHash: storedFileHash, hasThumbnail: hasThumbnail, sizeInBytes: sizeInBytes, + locationLatitude: locationLatitude, + locationLongitude: locationLongitude, + locationAccuracy: locationAccuracy, + locationStatus: locationStatus, + locationDeadlineAt: locationDeadlineAt, + galleryExportPending: galleryExportPending, createdAt: createdAt, createdAtMonth: createdAtMonth, rowid: rowid, diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index b4ab479f..453a9384 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -521,9 +521,27 @@ abstract class AppLocalizations { /// No description provided for @settingsStorageData. /// /// In en, this message translates to: - /// **'Data and storage'** + /// **'Memories & Storage'** String get settingsStorageData; + /// No description provided for @memoryLocationTitle. + /// + /// In en, this message translates to: + /// **'Store location in Memories'** + String get memoryLocationTitle; + + /// No description provided for @memoryLocationSubtitle. + /// + /// In en, this message translates to: + /// **'Location for Memories and gallery exports. Never sent to contacts.'** + String get memoryLocationSubtitle; + + /// No description provided for @memoryLocationPermissionDenied. + /// + /// In en, this message translates to: + /// **'Precise location access was not granted. The setting remains disabled.'** + String get memoryLocationPermissionDenied; + /// No description provided for @settingsStorageDataStoreInGTitle. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index cca01180..e7e94630 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -231,7 +231,18 @@ class AppLocalizationsDe extends AppLocalizations { String get settingsProfile => 'Profil'; @override - String get settingsStorageData => 'Daten und Speicher'; + String get settingsStorageData => 'Memories & Speicher'; + + @override + String get memoryLocationTitle => 'Standort in Memories speichern'; + + @override + String get memoryLocationSubtitle => + 'Standort für Memories und Galerie-Exporte. Wird nicht an Kontakte gesendet.'; + + @override + String get memoryLocationPermissionDenied => + 'Der genaue Standortzugriff wurde nicht erlaubt. Die Einstellung bleibt deaktiviert.'; @override String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index fe195e8e..d1fa98b5 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -229,7 +229,18 @@ class AppLocalizationsEn extends AppLocalizations { String get settingsProfile => 'Profile'; @override - String get settingsStorageData => 'Data and storage'; + String get settingsStorageData => 'Memories & Storage'; + + @override + String get memoryLocationTitle => 'Store location in Memories'; + + @override + String get memoryLocationSubtitle => + 'Location for Memories and gallery exports. Never sent to contacts.'; + + @override + String get memoryLocationPermissionDenied => + 'Precise location access was not granted. The setting remains disabled.'; @override String get settingsStorageDataStoreInGTitle => 'Store in Gallery'; diff --git a/lib/src/localization/translations/de.arb b/lib/src/localization/translations/de.arb index 3b3f5b85..f0751d51 100644 --- a/lib/src/localization/translations/de.arb +++ b/lib/src/localization/translations/de.arb @@ -74,7 +74,10 @@ "imageEditorDrawOk": "Zeichnung machen", "settingsTitle": "Einstellungen", "settingsChats": "Chats", - "settingsStorageData": "Daten und Speicher", + "settingsStorageData": "Memories & Speicher", + "memoryLocationTitle": "Standort in Memories speichern", + "memoryLocationSubtitle": "Standort für Memories und Galerie-Exporte. Wird nicht an Kontakte gesendet.", + "memoryLocationPermissionDenied": "Der genaue Standortzugriff wurde nicht erlaubt. Die Einstellung bleibt deaktiviert.", "settingsStorageDataStoreInGTitle": "In der Galerie speichern", "settingsStorageDataMediaAutoDownload": "Automatischer Mediendownload", "settingsStorageDataAutoDownMobile": "Bei Nutzung mobiler Daten", diff --git a/lib/src/localization/translations/en.arb b/lib/src/localization/translations/en.arb index 517e8ec3..3b07dcbd 100644 --- a/lib/src/localization/translations/en.arb +++ b/lib/src/localization/translations/en.arb @@ -70,7 +70,10 @@ "settingsAutomaticallyMarkEqualMediaFilesAsOpenedSubtitle": "If you receive the same media in multiple chats, opening one marks all others as opened.", "settingsPreSelectedReactionsError": "A maximum of 12 reactions can be selected.", "settingsProfile": "Profile", - "settingsStorageData": "Data and storage", + "settingsStorageData": "Memories & Storage", + "memoryLocationTitle": "Store location in Memories", + "memoryLocationSubtitle": "Location for Memories and gallery exports. Never sent to contacts.", + "memoryLocationPermissionDenied": "Precise location access was not granted. The setting remains disabled.", "settingsStorageDataStoreInGTitle": "Store in Gallery", "settingsStorageDataMediaAutoDownload": "Media auto-download", "settingsStorageDataAutoDownMobile": "When using mobile data", diff --git a/lib/src/providers/purchases.provider.dart b/lib/src/providers/purchases.provider.dart index 706aae68..c3a36e31 100644 --- a/lib/src/providers/purchases.provider.dart +++ b/lib/src/providers/purchases.provider.dart @@ -108,11 +108,14 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin { isPayingUser( planFromString(userService.currentUser.subscriptionPlan), )) { - globalForceIpaCheck = Timer(const Duration(seconds: 5), () async { + globalForceIpaCheck = Timer(const Duration(seconds: 5), () { Log.info( 'Force Ipa check was not stopped. Requesting forced check...', ); - await RustApi.forceIpaCheck(); + unawaitedRustCall( + RustApi.forceIpaCheck(), + 'Forced IAP check', + ); }); } diff --git a/lib/src/services/location_metadata.service.dart b/lib/src/services/location_metadata.service.dart new file mode 100644 index 00000000..97eb7b87 --- /dev/null +++ b/lib/src/services/location_metadata.service.dart @@ -0,0 +1,13 @@ +import 'package:flutter/services.dart'; + +class LocationMetadataService { + static const _channel = MethodChannel('eu.twonly/location_metadata'); + + /// Opens the native permission sheet only after the user enables the switch. + /// Approximate/reduced access is rejected because this feature promises a + /// precise capture location. + static Future requestPrecisePermission() async { + return await _channel.invokeMethod('requestPrecisePermission') ?? + false; + } +} diff --git a/lib/src/services/memories/memories.service.dart b/lib/src/services/memories/memories.service.dart index 22131068..4398fd5e 100644 --- a/lib/src/services/memories/memories.service.dart +++ b/lib/src/services/memories/memories.service.dart @@ -384,6 +384,7 @@ class MemoriesService { final mediaFile = item.mediaService.mediaFile; if (!mediaFile.hasThumbnail && mediaFile.type != MediaType.audio && + item.mediaService.storedPath.existsSync() && _thumbnailRequests.add(mediaFile.mediaId)) { unawaited(item.mediaService.createThumbnail()); } diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart index ef1e41c8..65d12e7c 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -169,6 +169,7 @@ class _CameraPreviewViewState extends State { initVolumeControl(); initAsync(); _checkAndInitCamera(); + _prewarmMemoryLocation(); } @override @@ -178,12 +179,24 @@ class _CameraPreviewViewState extends State { if (widget.isVisible) { initVolumeControl(); _checkAndInitCamera(); + _prewarmMemoryLocation(); } else { _deInitVolumeControl(); } } } + void _prewarmMemoryLocation() { + // QR scanning deliberately does not touch location. A normal camera view + // has controllers and can create a Memory, so start looking as soon as it + // becomes visible instead of waiting for the shutter. + if (!widget.isVisible || widget.hideControllers) return; + unawaitedRustCall( + RustApi.prewarmMemoryLocation(), + 'prewarmMemoryLocation', + ); + } + void _checkAndInitCamera() { if (widget.isVisible && mc.cameraController == null && diff --git a/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart index e4b4916d..4a251ea2 100644 --- a/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart @@ -52,13 +52,19 @@ class SaveToGalleryButtonState extends State { MediaFilesCompanion( type: Value(widget.mediaService.mediaFile.type), createdAt: Value(clock.now()), - stored: const Value(true), + // Rust publishes the Memory as stored only after the + // plaintext and its thumbnail have been written. + stored: const Value(false), ), ); if (newMediaFile != null) { final newService = MediaFileService(newMediaFile); + await RustApi.initializeMemoryLocation( + mediaId: newMediaFile.mediaId, + ); + if (widget.mediaService.tempPath.existsSync()) { widget.mediaService.tempPath.copySync( newService.tempPath.path, diff --git a/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart b/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart index 0f3fde6e..c703ba40 100644 --- a/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart +++ b/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart @@ -142,8 +142,13 @@ class _MemoriesThumbnailCompState extends State { @override void didUpdateWidget(covariant MemoriesThumbnailComp oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.galleryItem.mediaService.mediaFile.mediaId != - widget.galleryItem.mediaService.mediaFile.mediaId) { + final oldMedia = oldWidget.galleryItem.mediaService.mediaFile; + final media = widget.galleryItem.mediaService.mediaFile; + final changedMedia = oldMedia.mediaId != media.mediaId; + final sourceBecameAvailable = + !oldMedia.stored && media.stored || + !oldMedia.hasThumbnail && media.hasThumbnail; + if (changedMedia || sourceBecameAvailable) { _imageStream?.removeListener(_listener); _imageStream = null; _imageProvider = null; diff --git a/lib/src/visual/views/settings/data_and_storage.view.dart b/lib/src/visual/views/settings/data_and_storage.view.dart index 5259e59c..d1e4aa51 100644 --- a/lib/src/visual/views/settings/data_and_storage.view.dart +++ b/lib/src/visual/views/settings/data_and_storage.view.dart @@ -5,8 +5,10 @@ import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/services/location_metadata.service.dart'; import 'package:twonly/src/services/mediafiles/media_download_policy.dart'; import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; class DataAndStorageView extends StatefulWidget { @@ -18,6 +20,7 @@ class DataAndStorageView extends StatefulWidget { class _DataAndStorageViewState extends State { late Future> _storageStatsFuture; + bool _isUpdatingLocationSetting = false; @override void initState() { @@ -86,6 +89,46 @@ class _DataAndStorageViewState extends State { }); } + Future toggleLocationInMemories() async { + if (_isUpdatingLocationSetting) return; + setState(() => _isUpdatingLocationSetting = true); + final currentlyEnabled = userService.currentUser.storeLocationInMemories; + try { + if (!currentlyEnabled) { + final granted = + await LocationMetadataService.requestPrecisePermission(); + if (!granted) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.lang.memoryLocationPermissionDenied), + ), + ); + } + return; + } + } + await UserService.update((user) { + user.storeLocationInMemories = !currentlyEnabled; + }); + } catch (error, stackTrace) { + Log.error( + 'Could not update the Memory location setting', + error: error, + stackTrace: stackTrace, + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.lang.memoryLocationPermissionDenied), + ), + ); + } + } finally { + if (mounted) setState(() => _isUpdatingLocationSetting = false); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -124,6 +167,22 @@ class _DataAndStorageViewState extends State { onChanged: (a) => toggleStoreInGallery(), ), ), + ListTile( + title: Text(context.lang.memoryLocationTitle), + subtitle: Text( + context.lang.memoryLocationSubtitle, + style: const TextStyle(fontSize: 12), + ), + onTap: _isUpdatingLocationSetting + ? null + : toggleLocationInMemories, + trailing: Switch.adaptive( + value: userService.currentUser.storeLocationInMemories, + onChanged: _isUpdatingLocationSetting + ? null + : (_) => toggleLocationInMemories(), + ), + ), ListTile( title: Text(context.lang.autoStoreAllSendUnlimitedMediaFiles), subtitle: Text( diff --git a/lib/src/visual/views/settings/developer/developer.view.dart b/lib/src/visual/views/settings/developer/developer.view.dart index f97ff891..8a3d96ce 100644 --- a/lib/src/visual/views/settings/developer/developer.view.dart +++ b/lib/src/visual/views/settings/developer/developer.view.dart @@ -225,6 +225,7 @@ class _DeveloperSettingsViewState extends State { isFavorite: false, hasCropAnalyzed: false, hasThumbnail: false, + galleryExportPending: false, cloudState: CloudState.none, createdAt: now, ); diff --git a/pubspec.lock b/pubspec.lock index 7fabdd8b..ff256e62 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "1b0e6a07425a3e460666e88bf1c949ccc7bb0116ad562ce94a1eca60fe820725" + sha256: fe18c7e37d5acb3b43224fa255281dec007970d6fd01f6d76fcd88ed3777a117 url: "https://pub.dev" source: hosted - version: "103.0.0" + version: "107.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: f698de6eb8a0dd7a9a931bbfe13568e8b77e702eb2deb13dd83480c5373e7746 + sha256: f6966125633a34f82d9be2ee895b755cff3554087b1d00a9eb884e5947f4bca9 url: "https://pub.dev" source: hosted - version: "1.3.68" + version: "1.3.77" analyzer: dependency: transitive description: name: analyzer - sha256: "61c04d0c1bfed555c681ea079519933f071a5a026578ff73c4ff0df2d3462e5e" + sha256: f2dde9e50c23fc95b846b4ef334e98c3ae8c0effd5d2453b02fc9fb3f8939791 url: "https://pub.dev" source: hosted - version: "13.3.0" + version: "14.3.0" analyzer_buffer: dependency: transitive description: name: analyzer_buffer - sha256: "445b77e2054fa3e8c8a8ef1b5e9e6b23bb8028fffd34b5e60eaef315b7750674" + sha256: f890a2e708e38fbf4238ad2f9fd3d506538acdddb0cd8eb2c3e8c7bd17784b57 url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.4" app_links: dependency: "direct main" description: name: app_links - sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc" + sha256: f8db46d2ea9ff6f3a37191a7fd5b7813da1253ace8c32c1b9eadace41d1188ea url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.2.1" app_links_linux: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: app_links_platform_interface - sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + sha256: "7546f09a6e93f4a2df2fe2bd40a5c6c64310ac461b036d82b43033be7a59f809" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.4" app_links_web: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: archive - sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + sha256: ace891da0862b0e4cabbb064ee3fd87b2728b898949fdb366d83fe98342c9f19 url: "https://pub.dev" source: hosted - version: "4.0.9" + version: "4.2.0" args: dependency: transitive description: @@ -122,10 +122,10 @@ packages: dependency: transitive description: name: build - sha256: b94f5da9ed3d081fd4ecc426c260998b5f4f4eb2f6d89b7e5472edef0b2b2a1b + sha256: "8a5c5761af8e31748bba3c82f68925ace40f3225c3eea25be9beb57eca7cd7a8" url: "https://pub.dev" source: hosted - version: "4.0.10" + version: "4.0.11" build_cli_annotations: dependency: transitive description: @@ -138,26 +138,26 @@ packages: dependency: transitive description: name: build_config - sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + sha256: d466ed2dc9c6cd1d169948879b84ee061eb5e22c64a7c6089879c6296d272a8d url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.3" build_daemon: dependency: transitive description: name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + sha256: e1d40ef3f7934986d5da2271b1ba07794921ce263e44d622fb6c406d76589e33 url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.6" build_runner: dependency: "direct dev" description: name: build_runner - sha256: be46e59dcfefd6c6c7127df9f1be6c1f89219fee22ae02a923463e0f286ce652 + sha256: "894c243f6bc32015fec466ce30a6925bd537a77a426ee5bf481120477eb3de67" url: "https://pub.dev" source: hosted - version: "2.15.2" + version: "2.16.1" built_collection: dependency: transitive description: @@ -170,18 +170,18 @@ packages: dependency: transitive description: name: built_value - sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" + sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7 url: "https://pub.dev" source: hosted - version: "8.12.5" + version: "8.13.0" camera: dependency: "direct main" description: name: camera - sha256: "558230d6ce6ccea856b32d390db7e7b557adf4d9320aa614481bd3f2f608953f" + sha256: "3f30ca0ff376f91534f23afa2a7aea06ebb6c889fd9e260642437b37e3d9f753" url: "https://pub.dev" source: hosted - version: "0.12.0+2" + version: "0.12.1" camera_android_camerax: dependency: "direct main" description: @@ -193,10 +193,10 @@ packages: dependency: transitive description: name: camera_avfoundation - sha256: "90e4cc3fde331581a3b2d35d83be41dbb7393af0ab857eb27b732174289cb96d" + sha256: "843247230a583e04742ea1434ce3f91e536760922fab67c72ce46aa34fac3e9d" url: "https://pub.dev" source: hosted - version: "0.10.1" + version: "0.10.3" camera_platform_interface: dependency: transitive description: @@ -209,10 +209,10 @@ packages: dependency: transitive description: name: camera_web - sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7" + sha256: "5aecf1ac4dd4a949c5a7820e590020acb2bc35425384ab87a030e1739bfe12e2" url: "https://pub.dev" source: hosted - version: "0.3.5+3" + version: "0.3.5+6" characters: dependency: transitive description: @@ -249,18 +249,18 @@ packages: dependency: "direct main" description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" code_assets: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: cfd4f5f575a49c5f10ca856e9846073f1e6c3ee94912377eea5f6cefc5272941 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0" collection: dependency: "direct main" description: @@ -273,10 +273,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: b8fe52979ff12432ecf8f0abf6ff70410b1bb734be1c9e4f2f86807ad7166c79 + sha256: "762c99f890ca8bf87f7337236f99edd42793843bc6c3631da294a76653a54bd0" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.3.1" connectivity_plus_platform_interface: dependency: transitive description: @@ -297,10 +297,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6 url: "https://pub.dev" source: hosted - version: "0.3.5+2" + version: "0.3.5+5" crypto: dependency: "direct main" description: @@ -345,18 +345,18 @@ packages: dependency: transitive description: name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + sha256: a48d5da28e89bd02196e80d81ed8d7954923d00a0f4a68cc20b575038f023383 url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.15" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: "6a642e1daa10190af89ba6cb6386c0df7d071a3592080bfe1e44faa63ae1df65" + sha256: "0891702f96b2e465fe567b7ec448380e6b1c14f60af552a8536d9f583b6b8442" url: "https://pub.dev" source: hosted - version: "13.1.0" + version: "13.2.0" device_info_plus_platform_interface: dependency: transitive description: @@ -376,26 +376,26 @@ packages: dependency: "direct main" description: name: drift - sha256: c21b9af62e78f86837638dda6c33506bd9444ca8a32cad2b99c07369cf9e2462 + sha256: e3b6352aa02e60bed6ffc3cc9894c421578dad5fbef21b1f7f57cdc0e4edabcb url: "https://pub.dev" source: hosted - version: "2.34.1" + version: "2.35.0" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: "735aad3c34215805c66bd518c8812fa4f83b5534b2c13c4092c970c04a7e9983" + sha256: fbad5c1e3a73820cdb1f32161eb2f40bb5e5f03c0c1a3d236595725aec7efc6d url: "https://pub.dev" source: hosted - version: "2.34.5" + version: "2.35.0" drift_flutter: dependency: "direct main" description: name: drift_flutter - sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166" + sha256: "91acf4bee7c3c84467cba46455aa70e5292a3b889f4582645d74f2e5a8c106f2" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1" emoji_picker_flutter: dependency: "direct main" description: @@ -446,18 +446,18 @@ packages: dependency: transitive description: name: file_selector_linux - sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + sha256: da76400e7872ce7637ffdce12749ec24169c25f6195c28372208e65a24bcd2ab url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.9.4+1" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + sha256: d57c62362766b5e7ae739448650b66c6aab7a68ba7ecc65e04018652645ae0f4 url: "https://pub.dev" source: hosted - version: "0.9.5" + version: "0.9.5+1" file_selector_platform_interface: dependency: transitive description: @@ -470,82 +470,82 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + sha256: fbefc5fb92c6d3cbe8d284a2cd971b593bb07d2cd6da8557b81a862250b4acec url: "https://pub.dev" source: hosted - version: "0.9.3+5" + version: "0.9.3+6" firebase_app_installations: dependency: "direct main" description: name: firebase_app_installations - sha256: "9103cac19ec40561b49a023e8e583f007f77a499c2058f5a1d82ba480c2367d7" + sha256: "168d55d7b80769a884f933e48ee69b98212a27a9704efae8f04d6c0219c58ccc" url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.3" firebase_app_installations_platform_interface: dependency: transitive description: name: firebase_app_installations_platform_interface - sha256: "0811d37b91c992cc0c98200cca79652d0375a16c58b364a4db5601571c198ee5" + sha256: "17e69460110d8d373b41ae88465aa306effffe1d344464b2ac5968f54d89e770" url: "https://pub.dev" source: hosted - version: "0.1.4+67" + version: "0.1.5" firebase_app_installations_web: dependency: transitive description: name: firebase_app_installations_web - sha256: "02f2a96e85581bd1f78319b503dc92c80afa1527cbc299c7c921995b75595bbd" + sha256: b9f91f9ff49bc07837f5389db405eb171bf9fbbe5d4cbef478b89336318085db url: "https://pub.dev" source: hosted - version: "0.1.7+4" + version: "0.1.7+13" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "2f988dab915efde3b3105268dbd69efce0e8570d767a218ccd914afd0c10c8cc" + sha256: "2343710d8a164e3157a5e2fbb00663503c669be4aa06185311df48626760fdf1" url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.14.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce" + sha256: "9bfbc85faca09346471ac56fbcee00757ebcfd85887c6f602764eff0001902d8" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "8.1.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "1399ab1f0ac3b503d8a9be64a4c997fc066bbf33f701f42866e5569f26205ebe" + sha256: de1e678209a0c974d8aa4cff39f17d61d2dc263aa64993168360f9be4efb8e91 url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "3.11.0" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "8dc372085b1647f05e3ec1b8bc1dada87c0062f93b2a6976f620eb85edc44f97" + sha256: "31feb5db1fcbdececefd0feb5520013b7cb5d362018008c2889e7ae7e0da4dcb" url: "https://pub.dev" source: hosted - version: "16.1.3" + version: "16.6.0" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "6ea10f7df747542b17679d5939213c09163aab9c301b2f9b858cb55f38efdb54" + sha256: f9707e185b8ce1155d58e7e29348245072d99981f899fa8d3214abb2652c4ccb url: "https://pub.dev" source: hosted - version: "4.7.8" + version: "4.10.0" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "1f9798c8021ccf22b7e43e7fba81becd42252cb168228379fcabb7c2ef7dd638" + sha256: "311b16c75fc645c7a70c9ade38908abb12310dfce449fc3d3cee1771f196351e" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.2.5" fixnum: dependency: "direct main" description: @@ -684,10 +684,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" url: "https://pub.dev" source: hosted - version: "2.0.34" + version: "2.0.35" flutter_rust_bridge: dependency: "direct main" description: @@ -707,10 +707,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -741,10 +741,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "27585a6c2b9ac28ffb7b45d87fd1502332af1eb90e1c9f5eb1acb24c71d26e17" + sha256: "541a82723c84298f3ca46a6c2186935aa21ad529ad1f1423d67af18040494f51" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" freezed_annotation: dependency: transitive description: @@ -770,18 +770,18 @@ packages: dependency: transitive description: name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" go_router: dependency: "direct main" description: name: go_router - sha256: "48fb2f42ad057476fa4b733cb95e9f9ea7b0b010bb349ea491dca7dbdb18ffc4" + sha256: d7a3576cb312649eaa51f2356450aed686085fb58fcdebda5b359aa951eef7ea url: "https://pub.dev" source: hosted - version: "17.2.0" + version: "17.5.0" google_mlkit_barcode_scanning: dependency: "direct main" description: @@ -818,10 +818,10 @@ packages: dependency: transitive description: name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" hand_signature: dependency: "direct main" description: @@ -847,18 +847,18 @@ packages: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.2.0" html: dependency: "direct main" description: name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699" url: "https://pub.dev" source: hosted - version: "0.15.6" + version: "0.15.7" http: dependency: "direct main" description: @@ -894,18 +894,18 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" + sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" + sha256: "0a55d645a670d6ae11efa948f955ae904afa472f1344fc15a7ddacc20c1e7219" url: "https://pub.dev" source: hosted - version: "0.8.13+16" + version: "0.8.13+23" image_picker_for_web: dependency: transitive description: @@ -918,10 +918,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + sha256: ee3885b6fcd71958fbc79770dd194c63371439d536d69c47b279171a486482ae url: "https://pub.dev" source: hosted - version: "0.8.13+6" + version: "0.8.13+7" image_picker_linux: dependency: transitive description: @@ -966,26 +966,26 @@ packages: dependency: transitive description: name: in_app_purchase_android - sha256: c04e2cad0470fc868cb0ea06477648f003220b1b6612909db83528ca83ac0905 + sha256: f7327b5fd70d8dc1b419fb4cd5c17520175c4059441afd985e8a9c3fa2cefed9 url: "https://pub.dev" source: hosted - version: "0.5.2" + version: "0.5.3" in_app_purchase_platform_interface: dependency: "direct dev" description: name: in_app_purchase_platform_interface - sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c" + sha256: "0b0076cac8ce4fa7048f01e76af8b123aeb6a7c4e0dea2a5206d6664454f3e36" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" in_app_purchase_storekit: dependency: transitive description: name: in_app_purchase_storekit - sha256: "1d512809edd9f12ff88fce4596a13a18134e2499013f4d6a8894b04699363c93" + sha256: bb87b9003fd77da6f23f82c09b1891b28f7bf2520cfa1881a266a9601e675bf0 url: "https://pub.dev" source: hosted - version: "0.4.8+1" + version: "0.4.13" integration_test: dependency: "direct dev" description: flutter @@ -1010,10 +1010,10 @@ packages: dependency: transitive description: name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.1.0" jni: dependency: transitive description: @@ -1026,18 +1026,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.12.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: "2c15e78e1cc6e62aadecf59f81566fd56829713d96a8c4177699e2b2e17f20db" + sha256: e45aefa0324f08c683caafbb94b72837aa6193c61822799c916e45f4a263113d url: "https://pub.dev" source: hosted - version: "6.13.2" + version: "6.14.1" leak_tracker: dependency: transitive description: @@ -1074,26 +1074,26 @@ packages: dependency: "direct main" description: name: local_auth - sha256: ae6f382f638108c6becd134318d7c3f0a93875383a54010f61d7c97ac05d5137 + sha256: ecf24edf2283c509ecd217e3595f6f71034b68888d28ad1dae6bfa0857b816ac url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" local_auth_android: dependency: transitive description: name: local_auth_android - sha256: b41970749c2d43791790724b76917eeee1e90de76e6b0eec3edca03a329bf44c + sha256: "2de619ed7196c136ec4579539c0446668e794afeec1e1815786a3158a478d04b" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.10" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: a8c3d4e17454111f7fd31ff72a31222359f6059f7fe956c2dcfe0f88f49826d4 + sha256: b1b2d938cc3511512e552d92e782e055b8dc29c83e21c59e6ecfc6bd79ab0744 url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" local_auth_platform_interface: dependency: transitive description: @@ -1106,10 +1106,10 @@ packages: dependency: transitive description: name: local_auth_windows - sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16 + sha256: d72b8a441e5e6a679902f87c51529fcce65f442d77598484cf35454c3d733eda url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" logging: dependency: "direct main" description: @@ -1169,10 +1169,10 @@ packages: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" mutex: dependency: "direct main" description: @@ -1184,10 +1184,10 @@ packages: dependency: transitive description: name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + sha256: "9d233b6f2d9c52e1a2b5fbe70451d2c10ac674d3bb419d0ec8de14989d437c26" url: "https://pub.dev" source: hosted - version: "0.17.6" + version: "0.19.4" nested: dependency: transitive description: @@ -1208,10 +1208,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: ad56fd53a78ff6b1472fa59ff2a4e8b8ccabafc586fc263a1dfad0b99b5553e3 url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.6.0" package_config: dependency: transitive description: @@ -1224,10 +1224,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852" + sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481" url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.2.1" package_info_plus_platform_interface: dependency: transitive description: @@ -1256,10 +1256,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" path_provider_android: dependency: transitive description: @@ -1280,18 +1280,18 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_windows: dependency: transitive description: @@ -1304,10 +1304,10 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6 url: "https://pub.dev" source: hosted - version: "12.0.1" + version: "12.0.3" permission_handler_android: dependency: transitive description: @@ -1320,34 +1320,34 @@ packages: dependency: transitive description: name: permission_handler_apple - sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8 url: "https://pub.dev" source: hosted - version: "9.4.7" + version: "9.6.1" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac" url: "https://pub.dev" source: hosted - version: "0.1.3+5" + version: "0.1.4+1" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + sha256: ed86a61c190258fdd65de395ea0632822e3415c1faec38eae0c31b479c28a531 url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.4.1" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.2" petitparser: dependency: transitive description: @@ -1360,10 +1360,10 @@ packages: dependency: "direct main" description: name: photo_manager - sha256: fb3bc8ea653370f88742b3baa304700107c83d12748aa58b2b9f2ed3ef15e6c2 + sha256: "2d00a785b501bf1e5f6180bbef772846e767be11d45d11112b63092ef54a419e" url: "https://pub.dev" source: hosted - version: "3.9.0" + version: "3.12.0" photo_view: dependency: "direct main" description: @@ -1375,10 +1375,10 @@ packages: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.2.0" plugin_platform_interface: dependency: transitive description: @@ -1391,34 +1391,34 @@ packages: dependency: transitive description: name: pool - sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + sha256: "4177f68c237ea2128d1bee66ac17b2ce05ba3dbaafcbdd54c5d40a39d0b6b11c" url: "https://pub.dev" source: hosted - version: "1.5.2" + version: "1.5.3" posix: dependency: transitive description: name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.5.2" process: dependency: transitive description: name: process - sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + sha256: "4242ba3508d37e01808bdf71ad1d5bb93a8d671bf2e7450e6b1b353fb0808891" url: "https://pub.dev" source: hosted - version: "5.0.5" + version: "5.0.6" protobuf: dependency: "direct main" description: name: protobuf - sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" + sha256: "13b6717366aa7ec3792dd62c19c7d33a785708224365f2db23c5ca01f1c1a0f2" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" provider: dependency: "direct main" description: @@ -1431,18 +1431,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: c38b81cbf34450b67e0265d73433569d12e34782e30ed769c9cc99c9d5f2e796 url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" qr: dependency: "direct main" description: @@ -1465,6 +1465,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2" + url: "https://pub.dev" + source: hosted + version: "1.1.1" restart_app: dependency: "direct main" description: @@ -1498,34 +1506,34 @@ packages: dependency: transitive description: name: sentry - sha256: "288aee3d35f252ac0dc3a4b0accbbe7212fa2867604027f2cc5bc65334afd743" + sha256: bf80a571e370aad3f68b5c26d43f31cc230a084c250fb3138a83da6032ba0c01 url: "https://pub.dev" source: hosted - version: "9.16.0" + version: "9.30.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: f9e87d5895cc437902aa2b081727ee7e46524fe7cc2e1910f535480a3eeb8bed + sha256: da36fbb1581c0f66a0c8fc76f8aadb62d9ea567ba950c97eb326e959482c342a url: "https://pub.dev" source: hosted - version: "9.16.0" + version: "9.30.0" share_plus: dependency: "direct main" description: name: share_plus - sha256: a857d8b1479250aff6b57a51b2c02d31ca05848d441817c43f1640c885c286c0 + sha256: "34f00f9becd2743c1fb05363d624f9f70d37f7ccdcdda47450bc0b8c9d327b8c" url: "https://pub.dev" source: hosted - version: "13.1.0" + version: "13.3.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9" + sha256: "365ef7379fc22507256adda3385152942ffce08935452bc972c2e52a0bebae41" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.0" shared_preferences: dependency: transitive description: @@ -1538,18 +1546,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + sha256: "1e12aafe408aa50da80edfd679a2a6bf63ba7ab37c7fa98286da459a757b3399" url: "https://pub.dev" source: hosted - version: "2.4.23" + version: "2.4.28" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.5.7" shared_preferences_linux: dependency: transitive description: @@ -1607,10 +1615,10 @@ packages: dependency: transitive description: name: source_gen - sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5 + sha256: "11b6047da8e4eb6c643ccac3d0fda3f9c9e11651695f7da24a85ade8aff15a44" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "4.3.0" source_helper: dependency: transitive description: @@ -1646,10 +1654,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5" + sha256: "4c7fe79840389aaeaf05fd093f795b631b5a98e2bd28d54e555c100f4a9c7a1c" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.5.2" sqlite3_flutter_libs: dependency: transitive description: @@ -1670,10 +1678,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.12.2" stream_channel: dependency: transitive description: @@ -1686,10 +1694,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -1750,34 +1758,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610" url: "https://pub.dev" source: hosted - version: "6.3.29" + version: "6.3.33" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.4.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201" url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "3.2.6" url_launcher_platform_interface: dependency: transitive description: @@ -1790,34 +1798,34 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" uuid: dependency: transitive description: name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.6.0" vector_graphics: dependency: "direct main" description: name: vector_graphics - sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373" + sha256: "9d0e3b9cb16542ad660daee871e726a10d13a93b7b5391677c3160e8f5e83935" url: "https://pub.dev" source: hosted - version: "1.1.21" + version: "1.2.3" vector_graphics_codec: dependency: transitive description: @@ -1830,10 +1838,10 @@ packages: dependency: "direct dev" description: name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + sha256: "4dca4feb77dc3ec7f6e27e49c53241eb8217f55e4f9b12599a27f8903bca5682" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" vector_math: dependency: transitive description: @@ -1846,10 +1854,10 @@ packages: dependency: "direct dev" description: name: very_good_analysis - sha256: d1cb1d66a5aae2c702d68caca6c8347306d35e728fd94555fa21fa0448a972e0 + sha256: "481af67ab5877af20325251dc215a4ebac7666a1c8cf09198ffd457bc612b33d" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.3.0" video_player: dependency: "direct main" description: @@ -1891,10 +1899,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.3.0" watcher: dependency: transitive description: @@ -1931,18 +1939,18 @@ packages: dependency: transitive description: name: webdriver - sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + sha256: "28b82ec894fed45dd71c23ba62d1af973ed97dd59a4f5790a4d38b0b13e5657e" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" win32: dependency: transitive description: name: win32 - sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 + sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.4.0" win32_registry: dependency: transitive description: @@ -1971,10 +1979,18 @@ packages: dependency: transitive description: name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488" + url: "https://pub.dev" + source: hosted + version: "2.2.4" sdks: dart: ">=3.13.0 <4.0.0" flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index fa679d0a..03ece3d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,7 +71,9 @@ dependencies: permission_handler: ^12.0.0+1 # 2 mio # Not yet checked - cryptography_flutter_plus: ^3.0.0 + # 3.1.0 has a broken AGP 9 build script: it skips the Kotlin plugin but still + # configures the missing `kotlin` extension. + cryptography_flutter_plus: 3.0.0 cryptography_plus: ^3.0.0 flutter_android_volume_keydown: ^1.0.1 flutter_volume_controller: ^1.3.4 diff --git a/rust/src/bridge/api.rs b/rust/src/bridge/api.rs index d8af538c..17d94f88 100644 --- a/rust/src/bridge/api.rs +++ b/rust/src/bridge/api.rs @@ -209,6 +209,20 @@ impl RustApi { .await } + /// Starts the optional location window for a Memory row created by the + /// local save button rather than the outgoing-media initializer. + pub async fn initialize_memory_location(media_id: String) -> Result<()> { + let ctx = Context::get_static()?; + crate::services::location_metadata::initialize_for_media(ctx, &media_id).await + } + + /// Begins the optional precise-location lookup when the capture camera is + /// opened, so a later photo usually has a fix immediately available. + pub async fn prewarm_memory_location() -> Result<()> { + let ctx = Context::get_static()?; + crate::services::location_metadata::prewarm(ctx) + } + /// Creates one outgoing message per selected group and starts the upload. pub async fn send_media_to_groups( media_id: String, diff --git a/rust/src/bridge/user_config.rs b/rust/src/bridge/user_config.rs index 28ebb0b5..caa51e68 100644 --- a/rust/src/bridge/user_config.rs +++ b/rust/src/bridge/user_config.rs @@ -51,12 +51,17 @@ impl UserConfigApi { pub async fn update(base: UserConfig, config: UserConfig) -> Result { let ctx = Context::get_static()?; + let location_was_disabled = + base.store_location_in_memories && !config.store_location_in_memories; let normalized = UserConfig::update_json( ctx, &serde_json::to_string(&base)?, &serde_json::to_string(&config)?, )?; let config: UserConfig = serde_json::from_str(&normalized)?; + if location_was_disabled { + crate::services::location_metadata::disable_pending(ctx).await?; + } if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { (callbacks.api.user_config_changed)(config.clone()).await; } diff --git a/rust/src/database/app/migrations/0023_memory_locations.sql b/rust/src/database/app/migrations/0023_memory_locations.sql new file mode 100644 index 00000000..057e3e2a --- /dev/null +++ b/rust/src/database/app/migrations/0023_memory_locations.sql @@ -0,0 +1,16 @@ +-- Optional capture location for a locally stored Memory. Coordinates never +-- leave the device through the messaging path; they are only written to a +-- gallery export after the user explicitly enables the feature. +ALTER TABLE media_files ADD COLUMN location_latitude REAL; +ALTER TABLE media_files ADD COLUMN location_longitude REAL; +ALTER TABLE media_files ADD COLUMN location_accuracy REAL; +ALTER TABLE media_files ADD COLUMN location_status TEXT + CHECK (location_status IN ('pending', 'resolved', 'timedOut')); +ALTER TABLE media_files ADD COLUMN location_deadline_at INTEGER; +ALTER TABLE media_files ADD COLUMN gallery_export_pending INTEGER NOT NULL DEFAULT 0 + CHECK (gallery_export_pending IN (0, 1)); + +CREATE INDEX idx_media_files_pending_location + ON media_files(location_status, location_deadline_at) + WHERE location_status = 'pending'; + diff --git a/rust/src/database/app/mod.rs b/rust/src/database/app/mod.rs index d57eb100..b211306c 100644 --- a/rust/src/database/app/mod.rs +++ b/rust/src/database/app/mod.rs @@ -16,7 +16,7 @@ mod legacy_import; pub mod tables; pub const APP_DATABASE_FILE: &str = "app_db.sqlite"; -pub const APP_SCHEMA_VERSION: i64 = 8; +pub const APP_SCHEMA_VERSION: i64 = 9; /// User-owned application tables in the current Rust schema. Rust-only outbox /// tables are deliberately absent because they are reconstructed locally. diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 9303aa76..c02bd4f3 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1591152700; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 101877534; // Section: executor @@ -2568,6 +2568,44 @@ fn wire__crate__bridge__api__rust_api_initialize_media_upload_impl( }, ) } +fn wire__crate__bridge__api__rust_api_initialize_memory_location_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "rust_api_initialize_memory_location", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_media_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::api::RustApi::initialize_memory_location(api_media_id) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -3050,6 +3088,42 @@ fn wire__crate__bridge__api__rust_api_prerender_media_impl( }, ) } +fn wire__crate__bridge__api__rust_api_prewarm_memory_location_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "rust_api_prewarm_memory_location", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::api::RustApi::prewarm_memory_location().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -7320,6 +7394,7 @@ impl SseDecode for crate::user_config::UserConfig { let mut var_autoDownloadOptions = >>>::sse_decode(deserializer); let mut var_storeMediaFilesInGallery = ::sse_decode(deserializer); + let mut var_storeLocationInMemories = ::sse_decode(deserializer); let mut var_autoStoreAllSendUnlimitedMediaFiles = ::sse_decode(deserializer); let mut var_typingIndicators = ::sse_decode(deserializer); let mut var_showRestoreFlame = ::sse_decode(deserializer); @@ -7387,6 +7462,7 @@ impl SseDecode for crate::user_config::UserConfig { pre_selected_emojies: var_preSelectedEmojies, auto_download_options: var_autoDownloadOptions, store_media_files_in_gallery: var_storeMediaFilesInGallery, + store_location_in_memories: var_storeLocationInMemories, auto_store_all_send_unlimited_media_files: var_autoStoreAllSendUnlimitedMediaFiles, typing_indicators: var_typingIndicators, show_restore_flame: var_showRestoreFlame, @@ -7617,103 +7693,105 @@ fn pde_ffi_dispatcher_primary_impl( 67 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), 68 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len), 69 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len), -70 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), -71 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), -72 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), -73 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), -74 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), -75 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), -76 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), -77 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), -78 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), -79 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), -80 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), -81 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len), -82 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), -83 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len), -84 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len), -85 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), -86 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len), -87 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), -88 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), -89 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), -90 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), -91 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), -92 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), -93 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), -94 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), -95 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), -96 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), -97 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), -98 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), -99 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), -100 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), -101 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), -102 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), -103 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), -104 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), -105 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), -106 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), -107 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), -108 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), -109 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), -110 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len), -111 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), -112 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), -113 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), -114 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len), -115 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), -116 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), -117 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), -118 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len), -119 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), -120 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), -121 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len), -122 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), -123 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), -124 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), -125 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), -126 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), -127 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), -128 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), -129 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), -130 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), -131 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), -132 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), -133 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), -134 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -135 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), -136 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -137 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), -138 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -139 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -140 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -141 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), -142 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), -143 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), -144 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), -145 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len), -146 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), -147 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), -148 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), -149 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), -150 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), -151 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), -152 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), -153 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), -154 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), -155 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), -156 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), -157 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len), -158 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len), -159 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), -160 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), -161 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len), -163 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), -164 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), -165 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), -166 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), -167 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), +70 => wire__crate__bridge__api__rust_api_initialize_memory_location_impl(port, ptr, rust_vec_len, data_len), +71 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), +72 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), +73 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), +74 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), +75 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), +76 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), +77 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), +78 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), +79 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), +80 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), +81 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), +82 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len), +83 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len), +84 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), +85 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len), +86 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len), +87 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), +88 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len), +89 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), +90 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), +91 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), +92 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), +93 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), +94 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), +95 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), +96 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), +97 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), +98 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), +99 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), +100 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), +101 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), +102 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), +103 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), +104 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), +105 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), +106 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), +107 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), +108 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), +109 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), +110 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), +111 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), +112 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len), +113 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), +114 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), +115 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), +116 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len), +117 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), +118 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), +119 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), +120 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len), +121 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), +122 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), +123 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len), +124 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), +125 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), +126 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), +127 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), +128 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), +129 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), +130 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), +131 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), +132 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), +133 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), +134 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), +135 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), +136 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +137 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), +138 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +139 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), +140 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +141 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +142 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +143 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), +144 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), +145 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), +146 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), +147 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len), +148 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), +149 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), +150 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), +151 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), +152 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), +153 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), +154 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), +155 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), +156 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), +157 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), +158 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), +159 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len), +160 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len), +161 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), +162 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), +163 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len), +165 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), +166 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), +167 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), +168 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), +169 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -7731,12 +7809,12 @@ fn pde_ffi_dispatcher_sync_impl( 47 => { wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len) } - 162 => wire__crate__bridge__user_config__user_config_api_clone_impl( + 164 => wire__crate__bridge__user_config__user_config_api_clone_impl( ptr, rust_vec_len, data_len, ), - 168 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), + 170 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -8571,6 +8649,7 @@ impl flutter_rust_bridge::IntoDart for crate::user_config::UserConfig { self.store_media_files_in_gallery .into_into_dart() .into_dart(), + self.store_location_in_memories.into_into_dart().into_dart(), self.auto_store_all_send_unlimited_media_files .into_into_dart() .into_dart(), @@ -9688,6 +9767,7 @@ impl SseEncode for crate::user_config::UserConfig { serializer, ); ::sse_encode(self.store_media_files_in_gallery, serializer); + ::sse_encode(self.store_location_in_memories, serializer); ::sse_encode(self.auto_store_all_send_unlimited_media_files, serializer); ::sse_encode(self.typing_indicators, serializer); ::sse_encode(self.show_restore_flame, serializer); diff --git a/rust/src/native/location.rs b/rust/src/native/location.rs new file mode 100644 index 00000000..11352a55 --- /dev/null +++ b/rust/src/native/location.rs @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026, Tobias Müller git@tsmr.eu + * + */ + +//! One-shot, precise foreground location access implemented by the host app. + +use crate::error::Result; +#[cfg(any(target_os = "android", target_os = "ios"))] +use crate::error::TwonlyError; +use std::time::Duration; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct LocationFix { + pub latitude: f64, + pub longitude: f64, + pub accuracy: f64, +} + +#[cfg(target_os = "ios")] +pub(crate) fn current(timeout: Duration) -> Result> { + type CurrentLocation = unsafe extern "C" fn(i64, *mut f64, *mut f64, *mut f64) -> bool; + + let callback = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"twonly_current_location".as_ptr()) }; + if callback.is_null() { + return Err(TwonlyError::Generic( + "iOS location service is unavailable".into(), + )); + } + // SAFETY: the Swift @_cdecl declaration has this exact stable C ABI signature. + let callback: CurrentLocation = unsafe { std::mem::transmute(callback) }; + let mut latitude = 0.0; + let mut longitude = 0.0; + let mut accuracy = 0.0; + // SAFETY: all output pointers remain valid until the synchronous call returns. + let found = unsafe { + callback( + timeout.as_millis().min(i64::MAX as u128) as i64, + &mut latitude, + &mut longitude, + &mut accuracy, + ) + }; + Ok(found.then_some(LocationFix { + latitude, + longitude, + accuracy, + })) +} + +#[cfg(target_os = "android")] +pub(crate) fn current(timeout: Duration) -> Result> { + use crate::native::transfer::android::{jni_env, location_class}; + use jni::objects::JString; + + let class = location_class()?; + let mut env = jni_env()?; + let value = env + .call_static_method( + class, + "current", + "(J)Ljava/lang/String;", + &[jni::objects::JValue::Long( + timeout.as_millis().min(i64::MAX as u128) as i64, + )], + ) + .and_then(|value| value.l()); + if env.exception_check().unwrap_or(false) { + let _ = env.exception_describe(); + let _ = env.exception_clear(); + } + let value = value.map_err(|error| TwonlyError::Generic(error.to_string()))?; + if value.is_null() { + return Ok(None); + } + let json: String = env + .get_string(&JString::from(value)) + .map_err(|error| TwonlyError::Generic(error.to_string()))? + .into(); + let values: Vec = serde_json::from_str(&json)?; + if values.len() != 3 { + return Err(TwonlyError::Generic( + "Android returned an invalid location".into(), + )); + } + Ok(Some(LocationFix { + latitude: values[0], + longitude: values[1], + accuracy: values[2], + })) +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub(crate) fn current(_timeout: Duration) -> Result> { + Ok(None) +} diff --git a/rust/src/native/mod.rs b/rust/src/native/mod.rs index e89578b5..fd6170a3 100644 --- a/rust/src/native/mod.rs +++ b/rust/src/native/mod.rs @@ -13,6 +13,7 @@ pub(crate) mod background; pub(crate) mod gallery; pub(crate) mod image; +pub(crate) mod location; pub(crate) mod notifications; pub(crate) mod prepare; pub(crate) mod transfer; diff --git a/rust/src/native/transfer.rs b/rust/src/native/transfer.rs index fb1624c2..4ef3a834 100644 --- a/rust/src/native/transfer.rs +++ b/rust/src/native/transfer.rs @@ -44,6 +44,7 @@ pub(crate) mod android { const MEDIA_CODEC_CLASS: &str = "eu/twonly/media/NativeImageCodec"; const VIDEO_CODEC_CLASS: &str = "eu/twonly/media/NativeVideoCodec"; const GALLERY_CLASS: &str = "eu/twonly/media/NativeGallery"; + const LOCATION_CLASS: &str = "eu/twonly/location/NativeLocation"; static JAVA_VM: OnceLock = OnceLock::new(); static TRANSFER: OnceLock = OnceLock::new(); @@ -51,6 +52,7 @@ pub(crate) mod android { static MEDIA_CODEC: OnceLock = OnceLock::new(); static VIDEO_CODEC: OnceLock = OnceLock::new(); static GALLERY: OnceLock = OnceLock::new(); + static LOCATION: OnceLock = OnceLock::new(); /// `JNI_OnLoad` runs before Rust logging exists, so failures there are /// recorded rather than logged, and reported by the first call that needs /// the class. @@ -71,6 +73,7 @@ pub(crate) mod android { (MEDIA_CODEC_CLASS, &MEDIA_CODEC), (VIDEO_CODEC_CLASS, &VIDEO_CODEC), (GALLERY_CLASS, &GALLERY), + (LOCATION_CLASS, &LOCATION), ] { match env .find_class(name) @@ -123,6 +126,10 @@ pub(crate) mod android { cached_class(&GALLERY, GALLERY_CLASS) } + pub(crate) fn location_class() -> Result<&'static GlobalRef> { + cached_class(&LOCATION, LOCATION_CLASS) + } + pub(crate) fn jni_env() -> Result> { JAVA_VM .get() diff --git a/rust/src/services/location_metadata.rs b/rust/src/services/location_metadata.rs new file mode 100644 index 00000000..6a7b78eb --- /dev/null +++ b/rust/src/services/location_metadata.rs @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2026, Tobias Müller git@tsmr.eu + * + */ + +//! Optional, device-local location metadata for Memories. + +use crate::context::Context; +use crate::error::{Result, TwonlyError}; +use crate::native::location::{self, LocationFix}; +use crate::services::media_upload::MediaUploadService; +use crate::user_config::UserConfig; +use sqlx::FromRow; +use std::collections::HashSet; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Mutex as SyncMutex; +use std::sync::{Arc, LazyLock}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tokio::sync::Mutex; + +pub(crate) const LOCATION_WINDOW_SECONDS: i64 = 3 * 60; + +static LOCATION_REQUEST: LazyLock>> = + LazyLock::new(|| Mutex::new(None)); +static LOCATION_IN_FLIGHT: LazyLock>> = LazyLock::new(Default::default); +static CAMERA_WARMUP_IN_FLIGHT: AtomicBool = AtomicBool::new(false); + +struct CameraWarmup; + +impl Drop for CameraWarmup { + fn drop(&mut self) { + CAMERA_WARMUP_IN_FLIGHT.store(false, Ordering::Release); + } +} + +struct InFlight(String); + +impl Drop for InFlight { + fn drop(&mut self) { + if let Ok(mut active) = LOCATION_IN_FLIGHT.lock() { + active.remove(&self.0); + } + } +} + +#[derive(FromRow)] +struct PendingLocation { + media_id: String, + location_deadline_at: i64, +} + +/// Starts acquiring a fix as soon as the user opens a real capture camera. +/// Nothing is persisted until a media row exists; the result only warms the +/// in-process cache consumed by `initialize_for_media`. +pub(crate) fn prewarm(ctx: &Arc) -> Result<()> { + if !UserConfig::load_required_from(ctx)?.store_location_in_memories + || CAMERA_WARMUP_IN_FLIGHT.swap(true, Ordering::AcqRel) + { + return Ok(()); + } + tokio::spawn(async move { + let _warmup = CameraWarmup; + let mut request = LOCATION_REQUEST.lock().await; + if request.as_ref().is_some_and(|(captured_at, _)| { + captured_at.elapsed().unwrap_or(Duration::MAX) + < Duration::from_secs(LOCATION_WINDOW_SECONDS as u64) + }) { + return; + } + let timeout = Duration::from_secs(LOCATION_WINDOW_SECONDS as u64); + match tokio::task::spawn_blocking(move || location::current(timeout)).await { + Ok(Ok(Some(location))) => { + *request = Some((SystemTime::now(), location)); + } + Ok(Ok(None)) => {} + Ok(Err(error)) => tracing::warn!(%error, "camera location warm-up failed"), + Err(error) => tracing::warn!(%error, "camera location worker failed"), + } + }); + Ok(()) +} + +pub(crate) async fn initialize_for_media(ctx: &Arc, media_id: &str) -> Result<()> { + let config = UserConfig::load_required_from(ctx)?; + if !config.store_location_in_memories { + return Ok(()); + } + let deadline = chrono::Utc::now().timestamp() + LOCATION_WINDOW_SECONDS; + let database = ctx.app_db.read().await.clone(); + sqlx::query( + r#"UPDATE media_files + SET location_status = 'pending', location_deadline_at = ?, + location_latitude = NULL, location_longitude = NULL, + location_accuracy = NULL + WHERE media_id = ?"#, + ) + .bind(deadline) + .bind(media_id) + .execute(&database.pool) + .await?; + drop(database); + schedule(ctx, media_id.to_owned(), deadline); + Ok(()) +} + +pub(crate) fn schedule(ctx: &Arc, media_id: String, deadline: i64) { + let Ok(mut active) = LOCATION_IN_FLIGHT.lock() else { + return; + }; + if !active.insert(media_id.clone()) { + return; + } + drop(active); + let ctx = ctx.clone(); + tokio::spawn(async move { + let _in_flight = InFlight(media_id.clone()); + if let Err(error) = capture(&ctx, &media_id, deadline).await { + tracing::warn!(media_id, %error, "could not capture Memory location"); + } + }); +} + +async fn capture(ctx: &Arc, media_id: &str, deadline: i64) -> Result<()> { + // One native request serves captures made close together. Besides saving + // battery, this avoids multiple CLLocationManager/LocationManager sessions + // racing for identical coordinates. + let mut request = LOCATION_REQUEST.lock().await; + if let Some((captured_at, location)) = *request { + let captured_before_deadline = captured_at + .duration_since(UNIX_EPOCH) + .map(|value| value.as_secs() <= deadline.max(0) as u64) + .unwrap_or(false); + if captured_before_deadline + && captured_at.elapsed().unwrap_or(Duration::MAX) + < Duration::from_secs(LOCATION_WINDOW_SECONDS as u64) + { + drop(request); + finish(ctx, media_id, Some(location)).await?; + return Ok(()); + } + } + // Recalculate only after waiting for another Memory's request. Otherwise + // that wait would accidentally extend this Memory's three-minute window. + let remaining = deadline.saturating_sub(chrono::Utc::now().timestamp()); + if remaining <= 0 { + drop(request); + finish(ctx, media_id, None).await?; + return Ok(()); + } + let timeout = Duration::from_secs(remaining as u64); + let location = tokio::task::spawn_blocking(move || location::current(timeout)) + .await + .map_err(|error| TwonlyError::Generic(format!("location worker failed: {error}")))??; + if let Some(location) = location { + *request = Some((SystemTime::now(), location)); + } + drop(request); + finish(ctx, media_id, location).await +} + +async fn finish(ctx: &Arc, media_id: &str, location: Option) -> Result<()> { + let database = ctx.app_db.read().await.clone(); + if let Some(location) = location { + sqlx::query( + r#"UPDATE media_files + SET location_status = 'resolved', location_latitude = ?, + location_longitude = ?, location_accuracy = ? + WHERE media_id = ? AND location_status = 'pending'"#, + ) + .bind(location.latitude) + .bind(location.longitude) + .bind(location.accuracy) + .bind(media_id) + .execute(&database.pool) + .await?; + } else { + sqlx::query( + "UPDATE media_files SET location_status = 'timedOut' WHERE media_id = ? AND location_status = 'pending'", + ) + .bind(media_id) + .execute(&database.pool) + .await?; + } + drop(database); + MediaUploadService::new(ctx) + .finish_pending_gallery_export(media_id) + .await +} + +/// Resumes pending foreground requests and releases exports whose three-minute +/// deadline elapsed while the app was not running. +pub(crate) async fn resume(ctx: &Arc) -> Result<()> { + let database = ctx.app_db.read().await.clone(); + let pending = sqlx::query_as::<_, PendingLocation>( + r#"SELECT media_id, location_deadline_at + FROM media_files WHERE location_status = 'pending'"#, + ) + .fetch_all(&database.pool) + .await?; + drop(database); + let now = chrono::Utc::now().timestamp(); + for item in pending { + if item.location_deadline_at <= now { + finish(ctx, &item.media_id, None).await?; + } else if ctx.is_flutter_runtime() { + schedule(ctx, item.media_id, item.location_deadline_at); + } + } + Ok(()) +} + +/// Turning the option off must not leave already requested gallery exports in +/// limbo. Existing coordinates remain attached to their Memories. +pub(crate) async fn disable_pending(ctx: &Arc) -> Result<()> { + let database = ctx.app_db.read().await.clone(); + let media_ids = sqlx::query_scalar::<_, String>( + "SELECT media_id FROM media_files WHERE location_status = 'pending'", + ) + .fetch_all(&database.pool) + .await?; + sqlx::query( + "UPDATE media_files SET location_status = 'timedOut' WHERE location_status = 'pending'", + ) + .execute(&database.pool) + .await?; + drop(database); + for media_id in media_ids { + MediaUploadService::new(ctx) + .finish_pending_gallery_export(&media_id) + .await?; + } + Ok(()) +} diff --git a/rust/src/services/media_exif.rs b/rust/src/services/media_exif.rs index 7cc90452..13a8c2f6 100644 --- a/rust/src/services/media_exif.rs +++ b/rust/src/services/media_exif.rs @@ -21,10 +21,19 @@ const DATE_FORMAT: &str = "%Y:%m:%d %H:%M:%S"; const TAG_DATE_TIME: u16 = 0x0132; const TAG_EXIF_IFD: u16 = 0x8769; +const TAG_GPS_IFD: u16 = 0x8825; const TAG_DATE_TIME_ORIGINAL: u16 = 0x9003; const TAG_DATE_TIME_DIGITIZED: u16 = 0x9004; +const TAG_GPS_VERSION: u16 = 0x0000; +const TAG_GPS_LATITUDE_REF: u16 = 0x0001; +const TAG_GPS_LATITUDE: u16 = 0x0002; +const TAG_GPS_LONGITUDE_REF: u16 = 0x0003; +const TAG_GPS_LONGITUDE: u16 = 0x0004; +const TAG_GPS_HORIZONTAL_ERROR: u16 = 0x001f; +const TYPE_BYTE: u16 = 1; const TYPE_ASCII: u16 = 2; const TYPE_LONG: u16 = 4; +const TYPE_RATIONAL: u16 = 5; /// The `VP8X` flag bit that says an `EXIF` chunk is present. const FLAG_EXIF: u8 = 0b0000_1000; @@ -34,14 +43,56 @@ const FLAG_ALPHA: u8 = 0b0001_0000; #[derive(Default)] pub(crate) struct ExifMetadata { pub created_at: Option>, + pub latitude: Option, + pub longitude: Option, + pub accuracy: Option, } impl ExifMetadata { fn is_empty(&self) -> bool { - self.created_at.is_none() + self.created_at.is_none() && (self.latitude.is_none() || self.longitude.is_none()) } } +/// Removes the complete EXIF block from the WebP upload copy without decoding +/// or recompressing its pixels. +pub(crate) fn remove_exif_from_file(path: &std::path::Path) -> Result<()> { + let bytes = std::fs::read(path)?; + let stripped = without_exif(&bytes)?; + if stripped != bytes { + std::fs::write(path, stripped)?; + } + Ok(()) +} + +fn without_exif(webp: &[u8]) -> Result> { + let chunks = parse_chunks(webp)?; + if !chunks.iter().any(|chunk| chunk.id == *b"EXIF") { + return Ok(webp.to_vec()); + } + let mut body = Vec::new(); + for chunk in chunks { + if chunk.id == *b"EXIF" { + continue; + } + if chunk.id == *b"VP8X" { + let mut header = chunk.payload.to_vec(); + if let Some(flags) = header.first_mut() { + *flags &= !FLAG_EXIF; + } + write_chunk(&mut body, &chunk.id, &header); + } else { + write_chunk(&mut body, &chunk.id, chunk.payload); + } + } + let mut output = Vec::with_capacity(body.len() + 12); + output.extend_from_slice(b"RIFF"); + output.extend_from_slice(&((body.len() + 4) as u32).to_le_bytes()); + output.extend_from_slice(b"WEBP"); + output.extend_from_slice(&body); + Ok(output) +} + /// Returns the WebP bytes with an `EXIF` chunk attached, converting a simple /// file to the extended layout when needed. pub(crate) fn with_exif(webp: &[u8], metadata: &ExifMetadata) -> Result> { @@ -169,69 +220,120 @@ fn write_chunk(output: &mut Vec, id: &[u8; 4], payload: &[u8]) { } } -/// Builds a little-endian TIFF header with one IFD0 and one Exif sub-IFD. +/// Builds a little-endian TIFF header with Exif and GPS sub-IFDs. fn tiff_block(metadata: &ExifMetadata) -> Vec { let mut ifd0: Vec<(u16, u16, Vec)> = Vec::new(); let mut exif: Vec<(u16, u16, Vec)> = Vec::new(); + let mut gps: Vec<(u16, u16, Vec)> = Vec::new(); if let Some(created_at) = metadata.created_at { let text = format!("{}\0", created_at.format(DATE_FORMAT)).into_bytes(); ifd0.push((TAG_DATE_TIME, TYPE_ASCII, text.clone())); exif.push((TAG_DATE_TIME_ORIGINAL, TYPE_ASCII, text.clone())); exif.push((TAG_DATE_TIME_DIGITIZED, TYPE_ASCII, text)); } + if let (Some(latitude), Some(longitude)) = (metadata.latitude, metadata.longitude) { + gps.push((TAG_GPS_VERSION, TYPE_BYTE, vec![2, 3, 0, 0])); + gps.push(( + TAG_GPS_LATITUDE_REF, + TYPE_ASCII, + vec![if latitude < 0.0 { b'S' } else { b'N' }, 0], + )); + gps.push((TAG_GPS_LATITUDE, TYPE_RATIONAL, coordinate(latitude))); + gps.push(( + TAG_GPS_LONGITUDE_REF, + TYPE_ASCII, + vec![if longitude < 0.0 { b'W' } else { b'E' }, 0], + )); + gps.push((TAG_GPS_LONGITUDE, TYPE_RATIONAL, coordinate(longitude))); + if let Some(accuracy) = metadata + .accuracy + .filter(|value| value.is_finite() && *value >= 0.0) + { + gps.push((TAG_GPS_HORIZONTAL_ERROR, TYPE_RATIONAL, rational(accuracy))); + } + } let mut output = Vec::new(); output.extend_from_slice(b"II\x2a\x00"); output.extend_from_slice(&8_u32.to_le_bytes()); - // IFD0 gains a pointer entry to the Exif sub-IFD, so reserve room for it. - let ifd0_entries = ifd0.len() + usize::from(!exif.is_empty()); - let ifd0_start = 8; - let ifd0_end = ifd0_start + 2 + ifd0_entries * 12 + 4; - let mut values = Vec::new(); - let mut entries = Vec::new(); - let mut value_offset = ifd0_end; - - for (tag, kind, data) in &ifd0 { - entries.push(entry(*tag, *kind, data, &mut values, &mut value_offset)); - } - // The Exif sub-IFD starts after IFD0 and its value block. - let exif_ifd_offset = ifd0_end + values.len(); + let pointer_count = usize::from(!exif.is_empty()) + usize::from(!gps.is_empty()); + let ifd0_size = ifd_size(&ifd0, pointer_count); + let exif_ifd_offset = 8 + ifd0_size; + let gps_ifd_offset = exif_ifd_offset + ifd_size(&exif, 0); if !exif.is_empty() { - entries.push(entry_inline( + ifd0.push(( TAG_EXIF_IFD, TYPE_LONG, - 1, - (exif_ifd_offset as u32).to_le_bytes(), + (exif_ifd_offset as u32).to_le_bytes().to_vec(), )); } - - output.extend_from_slice(&(ifd0_entries as u16).to_le_bytes()); - for bytes in &entries { - output.extend_from_slice(bytes); - } - output.extend_from_slice(&0_u32.to_le_bytes()); - output.extend_from_slice(&values); - - if !exif.is_empty() { - let sub_start = exif_ifd_offset; - let sub_end = sub_start + 2 + exif.len() * 12 + 4; - let mut sub_values = Vec::new(); - let mut sub_entries = Vec::new(); - let mut sub_offset = sub_end; - for (tag, kind, data) in &exif { - sub_entries.push(entry(*tag, *kind, data, &mut sub_values, &mut sub_offset)); - } - output.extend_from_slice(&(exif.len() as u16).to_le_bytes()); - for bytes in &sub_entries { - output.extend_from_slice(bytes); - } - output.extend_from_slice(&0_u32.to_le_bytes()); - output.extend_from_slice(&sub_values); + if !gps.is_empty() { + ifd0.push(( + TAG_GPS_IFD, + TYPE_LONG, + (gps_ifd_offset as u32).to_le_bytes().to_vec(), + )); } + output.extend_from_slice(&serialize_ifd(&ifd0, 8)); + output.extend_from_slice(&serialize_ifd(&exif, exif_ifd_offset)); + output.extend_from_slice(&serialize_ifd(&gps, gps_ifd_offset)); output } +fn ifd_size(entries: &[(u16, u16, Vec)], extra_entries: usize) -> usize { + if entries.is_empty() && extra_entries == 0 { + return 0; + } + 2 + (entries.len() + extra_entries) * 12 + + 4 + + entries + .iter() + .filter(|(_, _, data)| data.len() > 4) + .map(|(_, _, data)| data.len() + (data.len() & 1)) + .sum::() +} + +fn serialize_ifd(entries: &[(u16, u16, Vec)], start: usize) -> Vec { + if entries.is_empty() { + return Vec::new(); + } + let mut values = Vec::new(); + let mut encoded = Vec::new(); + let mut value_offset = start + 2 + entries.len() * 12 + 4; + encoded.extend_from_slice(&(entries.len() as u16).to_le_bytes()); + for (tag, kind, data) in entries { + encoded.extend_from_slice(&entry(*tag, *kind, data, &mut values, &mut value_offset)); + } + encoded.extend_from_slice(&0_u32.to_le_bytes()); + encoded.extend_from_slice(&values); + encoded +} + +fn coordinate(value: f64) -> Vec { + let value = value.abs(); + let degrees = value.floor(); + let minutes_full = (value - degrees) * 60.0; + let minutes = minutes_full.floor(); + let seconds = (minutes_full - minutes) * 60.0; + let mut bytes = Vec::with_capacity(24); + bytes.extend_from_slice(&rational(degrees)); + bytes.extend_from_slice(&rational(minutes)); + bytes.extend_from_slice(&rational(seconds)); + bytes +} + +fn rational(value: f64) -> Vec { + const DENOMINATOR: u32 = 1_000_000; + let numerator = (value * f64::from(DENOMINATOR)) + .round() + .clamp(0.0, f64::from(u32::MAX)) as u32; + let mut bytes = Vec::with_capacity(8); + bytes.extend_from_slice(&numerator.to_le_bytes()); + bytes.extend_from_slice(&DENOMINATOR.to_le_bytes()); + bytes +} + /// One 12-byte IFD entry. Values longer than four bytes live in the value block /// and the entry stores their offset instead. fn entry( @@ -244,7 +346,7 @@ fn entry( if data.len() <= 4 { let mut inline = [0_u8; 4]; inline[..data.len()].copy_from_slice(data); - return entry_inline(tag, kind, data.len() as u32, inline); + return entry_inline(tag, kind, component_count(kind, data), inline); } let offset = *value_offset; values.extend_from_slice(data); @@ -253,7 +355,20 @@ fn entry( *value_offset += 1; } *value_offset += data.len(); - entry_inline(tag, kind, data.len() as u32, (offset as u32).to_le_bytes()) + entry_inline( + tag, + kind, + component_count(kind, data), + (offset as u32).to_le_bytes(), + ) +} + +fn component_count(kind: u16, data: &[u8]) -> u32 { + match kind { + TYPE_LONG => (data.len() / 4) as u32, + TYPE_RATIONAL => (data.len() / 8) as u32, + _ => data.len() as u32, + } } fn entry_inline(tag: u16, kind: u16, count: u32, value: [u8; 4]) -> [u8; 12] { @@ -295,6 +410,7 @@ mod tests { let webp = sample_webp(); let metadata = ExifMetadata { created_at: Some(Utc.with_ymd_and_hms(2026, 8, 30, 12, 34, 56).unwrap()), + ..Default::default() }; let written = with_exif(&webp, &metadata).unwrap(); @@ -325,6 +441,7 @@ mod tests { fn writing_twice_replaces_rather_than_appends() { let metadata = ExifMetadata { created_at: Some(Utc.with_ymd_and_hms(2026, 1, 2, 3, 4, 5).unwrap()), + ..Default::default() }; let once = with_exif(&sample_webp(), &metadata).unwrap(); let twice = with_exif(&once, &metadata).unwrap(); @@ -333,4 +450,34 @@ mod tests { assert_eq!(chunks.iter().filter(|c| c.id == *b"EXIF").count(), 1); assert_eq!(chunks.iter().filter(|c| c.id == *b"VP8X").count(), 1); } + + #[test] + fn writes_gps_metadata_and_removes_it_from_the_send_copy() { + let metadata = ExifMetadata { + created_at: Some(Utc.with_ymd_and_hms(2026, 9, 10, 12, 0, 0).unwrap()), + latitude: Some(52.520_008), + longitude: Some(13.404_954), + accuracy: Some(8.5), + }; + let exported = with_exif(&sample_webp(), &metadata).unwrap(); + let chunks = parse_chunks(&exported).unwrap(); + let exif = chunks + .iter() + .find(|chunk| chunk.id == *b"EXIF") + .expect("EXIF chunk"); + assert!( + exif.payload.windows(2).any(|value| value == [0x25, 0x88]), + "IFD0 must point at a GPS IFD" + ); + assert!( + exif.payload.windows(2).any(|value| value == [0x02, 0x00]), + "GPS IFD must contain latitude" + ); + + let upload = without_exif(&exported).unwrap(); + let upload_chunks = parse_chunks(&upload).unwrap(); + assert!(!upload_chunks.iter().any(|chunk| chunk.id == *b"EXIF")); + assert_eq!(upload_chunks[0].payload[0] & FLAG_EXIF, 0); + image::load_from_memory(&upload).expect("stripped image remains decodable"); + } } diff --git a/rust/src/services/media_upload.rs b/rust/src/services/media_upload.rs index 1b5e28f6..a8f67b0e 100644 --- a/rust/src/services/media_upload.rs +++ b/rust/src/services/media_upload.rs @@ -78,6 +78,16 @@ struct MediaRow { created_at: i64, } +#[derive(FromRow)] +struct GalleryLocationRow { + location_status: Option, + location_deadline_at: Option, + location_latitude: Option, + location_longitude: Option, + location_accuracy: Option, + gallery_export_pending: i64, +} + /// Why a media send stopped at `fileLimitReached`, for the chat entry to show. pub struct MediaSizeReport { /// The encoded media as it sits on disk, while it is still there. @@ -147,6 +157,7 @@ impl MediaUploadService { .execute(&mut *transaction) .await?; transaction.commit().await?; + crate::services::location_metadata::initialize_for_media(&self.ctx, &media_id).await?; Ok(media_id) } @@ -400,6 +411,14 @@ impl MediaUploadService { self.store(media_id).await?; } + // The stored copy is device-local and may later gain EXIF metadata. + // The upload copy is a separate file and is always scrubbed before + // encryption, including when it originated from an imported Memory. + if media.media_type == "image" { + let path = temp_path.clone(); + blocking(move || media_exif::remove_exif_from_file(&path)).await?; + } + match DirectMediaUploadService::new(&self.ctx) .prepare_and_schedule(media_id) .await @@ -439,6 +458,7 @@ impl MediaUploadService { /// settles the transfers the server has meanwhile accepted or rejected. pub async fn finish_started_uploads(&self) -> Result<()> { let _guard = self.ctx.media_preprocessing.lock().await; + crate::services::location_metadata::resume(&self.ctx).await?; let direct = DirectMediaUploadService::new(&self.ctx); if let Err(error) = direct.reconcile().await { tracing::warn!(%error, "direct media reconciliation failed"); @@ -647,13 +667,6 @@ impl MediaUploadService { let Some(media) = self.load(media_id).await? else { return Ok(()); }; - let database = self.ctx.app_db.read().await.clone(); - sqlx::query("UPDATE media_files SET stored = 1 WHERE media_id = ?") - .bind(media_id) - .execute(&database.pool) - .await?; - drop(database); - let files = MediaFileService::new(&self.ctx); let temp_path = files.temp_path(media_id, &media.media_type); let stored_path = files.stored_path(media_id, &media.media_type); @@ -676,14 +689,13 @@ impl MediaUploadService { self.create_thumbnail(&media).await?; // Trust the filesystem rather than what the encoder reported: a // thumbnail that was not written must not be advertised to the UI. - if files.thumbnail_path(media_id).exists() { - self.update_media( - "UPDATE media_files SET has_thumbnail = ? WHERE media_id = ?", - 1_i64, - media_id, - ) - .await?; - } + let has_thumbnail = i64::from(files.thumbnail_path(media_id).exists()); + self.update_media( + "UPDATE media_files SET stored = 1, has_thumbnail = ? WHERE media_id = ?", + has_thumbnail, + media_id, + ) + .await?; self.refresh_stored_metadata(media_id, &stored_path).await } @@ -1265,7 +1277,7 @@ impl MediaUploadService { Ok(()) } - /// Renders the preview shown in chat lists and the gallery. /// Renders the preview shown in chat lists and the gallery. + /// Renders the preview shown in chat lists and the gallery. async fn create_thumbnail(&self, media: &MediaRow) -> Result<()> { let files = MediaFileService::new(&self.ctx); let stored = files.stored_path(&media.media_id, &media.media_type); @@ -1295,9 +1307,83 @@ impl MediaUploadService { Ok(()) } - /// Exports a stored media file to the user's photo library, stamping the - /// capture time into the file so it survives being copied elsewhere. + /// Exports a stored media file, or durably queues it while its optional + /// location is still being resolved. pub async fn save_to_gallery(&self, media_id: &str) -> Result<()> { + let database = self.ctx.app_db.read().await.clone(); + let location = sqlx::query_as::<_, GalleryLocationRow>( + r#"SELECT location_status, location_deadline_at, location_latitude, + location_longitude, location_accuracy, gallery_export_pending + FROM media_files WHERE media_id = ?"#, + ) + .bind(media_id) + .fetch_optional(&database.pool) + .await?; + let Some(location) = location else { + return Ok(()); + }; + if location.location_status.as_deref() == Some("pending") + && location.location_deadline_at.unwrap_or_default() > chrono::Utc::now().timestamp() + { + sqlx::query("UPDATE media_files SET gallery_export_pending = 1 WHERE media_id = ?") + .bind(media_id) + .execute(&database.pool) + .await?; + drop(database); + if let Some(deadline) = location.location_deadline_at { + crate::services::location_metadata::schedule( + &self.ctx, + media_id.to_owned(), + deadline, + ); + } + return Ok(()); + } + if location.location_status.as_deref() == Some("pending") { + sqlx::query("UPDATE media_files SET location_status = 'timedOut' WHERE media_id = ?") + .bind(media_id) + .execute(&database.pool) + .await?; + } + drop(database); + self.export_to_gallery_now(media_id, location).await + } + + pub(crate) async fn finish_pending_gallery_export(&self, media_id: &str) -> Result<()> { + let database = self.ctx.app_db.read().await.clone(); + let location = sqlx::query_as::<_, GalleryLocationRow>( + r#"SELECT location_status, location_deadline_at, location_latitude, + location_longitude, location_accuracy, gallery_export_pending + FROM media_files WHERE media_id = ?"#, + ) + .bind(media_id) + .fetch_optional(&database.pool) + .await?; + let Some(location) = location else { + return Ok(()); + }; + if location.gallery_export_pending == 0 + || location.location_status.as_deref() == Some("pending") + { + return Ok(()); + } + // This flag represents waiting for location, not a general gallery + // retry queue. Clear it before handing the export to the platform. + sqlx::query("UPDATE media_files SET gallery_export_pending = 0 WHERE media_id = ?") + .bind(media_id) + .execute(&database.pool) + .await?; + drop(database); + self.export_to_gallery_now(media_id, location).await + } + + /// Stamps capture time and the locally stored location into a temporary + /// export. The Memory itself remains untouched. + async fn export_to_gallery_now( + &self, + media_id: &str, + location: GalleryLocationRow, + ) -> Result<()> { let Some(media) = self.load(media_id).await? else { return Ok(()); }; @@ -1319,6 +1405,9 @@ impl MediaUploadService { } else { let metadata = media_exif::ExifMetadata { created_at: chrono::DateTime::from_timestamp_millis(created_at_millis), + latitude: location.location_latitude, + longitude: location.location_longitude, + accuracy: location.location_accuracy, }; let bytes = std::fs::read(&stored)?; match media_exif::with_exif(&bytes, &metadata) { @@ -1491,6 +1580,46 @@ mod tests { Ok(()) } + /// A freshly saved Memory used to become visible to Flutter before its + /// source file existed. The one-shot repair added for the Memories viewer + /// then consumed its attempt too early and left the row without a preview. + #[tokio::test] + async fn storing_a_memory_publishes_it_with_its_thumbnail() -> anyhow::Result<()> { + let temp = tempfile::tempdir()?; + let data_dir = temp.path().join("data"); + std::fs::create_dir_all(data_dir.join("keyvalue"))?; + std::fs::write( + data_dir.join("keyvalue/user.json"), + serde_json::to_vec(&UserConfig::default())?, + )?; + let ctx = Context::init_for_testing(temp.path().join("database"), data_dir).await?; + + let media_id = "new-memory"; + let database = ctx.app_db.read().await.clone(); + sqlx::query("INSERT INTO media_files(media_id, type, stored) VALUES (?, 'image', 0)") + .bind(media_id) + .execute(&database.pool) + .await?; + drop(database); + + let files = MediaFileService::new(&ctx); + let source = files.temp_path(media_id, "image"); + MediaFileService::ensure_parent(&source)?; + image::DynamicImage::ImageRgb8(image::RgbImage::new(900, 1600)).save(&source)?; + + MediaUploadService::new(&ctx).store(media_id).await?; + + assert!(files.thumbnail_path(media_id).exists()); + let database = ctx.app_db.read().await.clone(); + let state: (i64, i64) = + sqlx::query_as("SELECT stored, has_thumbnail FROM media_files WHERE media_id = ?") + .bind(media_id) + .fetch_one(&database.pool) + .await?; + assert_eq!(state, (1, 1)); + Ok(()) + } + #[test] fn media_lock_is_shared_per_media_id() { let first = media_lock("media-a"); diff --git a/rust/src/services/mod.rs b/rust/src/services/mod.rs index 3adf7b13..b98dac3c 100644 --- a/rust/src/services/mod.rs +++ b/rust/src/services/mod.rs @@ -9,6 +9,7 @@ pub mod contacts; pub mod direct_media_upload; pub mod groups; pub mod home_widget; +pub mod location_metadata; pub mod media_codec; pub mod media_exif; pub mod media_upload; diff --git a/rust/src/user_config.rs b/rust/src/user_config.rs index 87bde4a2..2d12f211 100644 --- a/rust/src/user_config.rs +++ b/rust/src/user_config.rs @@ -242,6 +242,11 @@ pub struct UserConfig { #[serde(default = "defaults::true_value")] #[frb(non_final)] pub store_media_files_in_gallery: bool, + /// Adds the capture location to locally stored Memories and gallery + /// exports. Disabled by default and never added to outgoing media. + #[serde(default)] + #[frb(non_final)] + pub store_location_in_memories: bool, #[serde(default)] #[frb(non_final)] pub auto_store_all_send_unlimited_media_files: bool, diff --git a/test/mocks/user_config.dart b/test/mocks/user_config.dart index 303a8703..c36c2ee9 100644 --- a/test/mocks/user_config.dart +++ b/test/mocks/user_config.dart @@ -27,6 +27,7 @@ UserConfig testUserConfig({ showShowImagePreviewWhenSending: false, startWithCameraOpen: true, storeMediaFilesInGallery: true, + storeLocationInMemories: false, autoStoreAllSendUnlimitedMediaFiles: false, typingIndicators: true, showRestoreFlame: true,