craft-native 0.0.396 → 0.0.408
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/android/templates/CraftBridge.kt.template +2 -0
- package/dist/android/templates/CraftNative.kt.template +7 -0
- package/dist/android/templates/CraftNativeServices.kt.template +2 -1
- package/dist/android/templates/CraftNativeShell.kt.template +12 -2
- package/dist/android/templates/LocationRecordingService.kt.template +125 -6
- package/dist/android/templates/MainActivityNative.kt.template +21 -1
- package/dist/api/mobile.d.ts +59 -2
- package/dist/cli.js +1 -1
- package/dist/index.cjs +54 -7
- package/dist/index.js +54 -7
- package/dist/ios/src/index.d.ts +7 -0
- package/dist/ios/templates/CraftApp.swift +268 -12
- package/dist/ios/templates/CraftHybrid.swift +1 -0
- package/dist/ios/templates/CraftNativeActions.swift +3 -1
- package/dist/ios/templates/CraftNativeScreen.swift +1 -0
- package/dist/ios/templates/CraftNativeUI.swift +89 -0
- package/dist/ios/templates/testing/DriverTests.swift.template +17 -1
- package/dist/macos/src/index.d.ts +14 -3
- package/dist/macos/src/index.js +124 -27
- package/dist/macos/src/toolchain.d.ts +56 -0
- package/dist/macos/templates/CraftMacApplication.swift +54 -9
- package/dist/macos/templates/CraftMacRenderer.swift +376 -18
- package/dist/macos/templates/CraftMacRuntime.swift +114 -1
- package/dist/macos/templates/CraftMacWebView.swift +416 -0
- package/dist/mobile.js +53 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
|
|
|
117
117
|
`);
|
|
118
118
|
}
|
|
119
119
|
// package.json
|
|
120
|
-
var version = "0.0.
|
|
120
|
+
var version = "0.0.408";
|
|
121
121
|
|
|
122
122
|
// src/sdk-version.ts
|
|
123
123
|
var SDK_VERSION = version;
|
|
@@ -5247,6 +5247,7 @@ var secureStorage = {
|
|
|
5247
5247
|
Object.keys(localStorage).filter((k) => k.startsWith("secure_")).forEach((k) => localStorage.removeItem(k));
|
|
5248
5248
|
}
|
|
5249
5249
|
};
|
|
5250
|
+
var RECORDING_POWER_MODES = ["default", "saver", "auto"];
|
|
5250
5251
|
function storageHost(method) {
|
|
5251
5252
|
const host = getCraftRoot()?.storage;
|
|
5252
5253
|
if (typeof host?.[method] !== "function")
|
|
@@ -5310,6 +5311,23 @@ function recordingIntentSupported(namespace) {
|
|
|
5310
5311
|
const advertised = Array.isArray(caps) ? caps.includes(flag) : caps?.[flag] === true;
|
|
5311
5312
|
return advertised && recordingOwnershipSupported(namespace) && typeof root?.[namespace]?.startRecording === "function";
|
|
5312
5313
|
}
|
|
5314
|
+
function recordingPowerModeSupported() {
|
|
5315
|
+
const root = getCraftRoot();
|
|
5316
|
+
const caps = root?.capabilities;
|
|
5317
|
+
const advertised = Array.isArray(caps) ? caps.includes("locationRecordingPowerMode") : caps?.locationRecordingPowerMode === true;
|
|
5318
|
+
return advertised && recordingOwnershipSupported("location") && typeof root?.location?.setRecordingPowerMode === "function";
|
|
5319
|
+
}
|
|
5320
|
+
function validatePowerMode(value) {
|
|
5321
|
+
if (typeof value !== "string" || !RECORDING_POWER_MODES.includes(value))
|
|
5322
|
+
throw Object.assign(new Error("powerMode must be 'default', 'saver' or 'auto'"), { code: "INVALID_ARGUMENT" });
|
|
5323
|
+
return value;
|
|
5324
|
+
}
|
|
5325
|
+
function validateRecordingPowerStart(options) {
|
|
5326
|
+
if (!Object.prototype.hasOwnProperty.call(options, "powerMode") || options.powerMode === undefined)
|
|
5327
|
+
return;
|
|
5328
|
+
if (validatePowerMode(options.powerMode) === "auto" && getCraftRoot() && !recordingPowerModeSupported())
|
|
5329
|
+
throw Object.assign(new Error("This app build cannot start a recording in powerMode 'auto'"), { code: "RECORDING_POWER_MODE_UNSUPPORTED" });
|
|
5330
|
+
}
|
|
5313
5331
|
function validateRecordingStart(namespace, options) {
|
|
5314
5332
|
if (!Object.prototype.hasOwnProperty.call(options, "recordingId"))
|
|
5315
5333
|
return;
|
|
@@ -5340,10 +5358,20 @@ var webLocationRecording = {
|
|
|
5340
5358
|
startedAt: null,
|
|
5341
5359
|
locations: []
|
|
5342
5360
|
};
|
|
5361
|
+
var webLocationPowerMode = "default";
|
|
5362
|
+
function webLocationRecordingState() {
|
|
5363
|
+
return {
|
|
5364
|
+
...webLocationRecording,
|
|
5365
|
+
sampleCount: webLocationRecording.locations.length,
|
|
5366
|
+
powerMode: webLocationPowerMode,
|
|
5367
|
+
effectivePowerMode: webLocationRecording.active && webLocationPowerMode === "saver" ? "saver" : "default"
|
|
5368
|
+
};
|
|
5369
|
+
}
|
|
5343
5370
|
var webLocationRecordingWatchId = null;
|
|
5344
5371
|
var location = {
|
|
5345
5372
|
supportsRecordingOwnership: () => recordingOwnershipSupported("location"),
|
|
5346
5373
|
supportsRecordingIntentIdentity: () => recordingIntentSupported("location"),
|
|
5374
|
+
supportsRecordingPowerMode: () => recordingPowerModeSupported(),
|
|
5347
5375
|
async getCurrentPosition(options = {}) {
|
|
5348
5376
|
const craft = getCraftMobile();
|
|
5349
5377
|
if (typeof window !== "undefined" && craft?.location) {
|
|
@@ -5393,6 +5421,7 @@ var location = {
|
|
|
5393
5421
|
},
|
|
5394
5422
|
async startRecording(options = {}) {
|
|
5395
5423
|
validateRecordingStart("location", options);
|
|
5424
|
+
validateRecordingPowerStart(options);
|
|
5396
5425
|
const craft = getCraftMobile();
|
|
5397
5426
|
if (typeof window !== "undefined" && craft?.location?.startRecording)
|
|
5398
5427
|
return verifyRecordingStart(options, await craft.location.startRecording(options));
|
|
@@ -5407,11 +5436,27 @@ var location = {
|
|
|
5407
5436
|
startedAt: Date.now(),
|
|
5408
5437
|
locations: []
|
|
5409
5438
|
};
|
|
5439
|
+
webLocationPowerMode = options.powerMode ?? "default";
|
|
5410
5440
|
webLocationRecordingWatchId = location.watchPosition((position) => {
|
|
5411
5441
|
if (!webLocationRecording.paused)
|
|
5412
5442
|
webLocationRecording.locations.push(position);
|
|
5413
5443
|
}, options);
|
|
5414
|
-
return
|
|
5444
|
+
return webLocationRecordingState();
|
|
5445
|
+
},
|
|
5446
|
+
async setRecordingPowerMode(options) {
|
|
5447
|
+
if (!options || !Object.prototype.hasOwnProperty.call(options, "expectedId"))
|
|
5448
|
+
throw Object.assign(new Error("setRecordingPowerMode needs expectedId"), { code: "INVALID_ARGUMENT" });
|
|
5449
|
+
validateRecordingOwnership("location", options);
|
|
5450
|
+
const powerMode = validatePowerMode(options.powerMode);
|
|
5451
|
+
if (getCraftRoot() && !recordingPowerModeSupported())
|
|
5452
|
+
throw Object.assign(new Error("This app build cannot change the recording power mode"), { code: "RECORDING_POWER_MODE_UNSUPPORTED" });
|
|
5453
|
+
const craft = getCraftMobile();
|
|
5454
|
+
if (typeof window !== "undefined" && craft?.location?.setRecordingPowerMode)
|
|
5455
|
+
return craft.location.setRecordingPowerMode({ expectedId: options.expectedId, powerMode });
|
|
5456
|
+
if (!webLocationRecording.active)
|
|
5457
|
+
throw Object.assign(new Error("No active location recording"), { code: "NO_ACTIVE_RECORDING" });
|
|
5458
|
+
webLocationPowerMode = powerMode;
|
|
5459
|
+
return webLocationRecordingState();
|
|
5415
5460
|
},
|
|
5416
5461
|
async pauseRecording(options) {
|
|
5417
5462
|
validateRecordingOwnership("location", options);
|
|
@@ -5419,7 +5464,7 @@ var location = {
|
|
|
5419
5464
|
if (typeof window !== "undefined" && craft?.location?.pauseRecording)
|
|
5420
5465
|
return craft.location.pauseRecording(options);
|
|
5421
5466
|
webLocationRecording.paused = true;
|
|
5422
|
-
return
|
|
5467
|
+
return webLocationRecordingState();
|
|
5423
5468
|
},
|
|
5424
5469
|
async resumeRecording(options) {
|
|
5425
5470
|
validateRecordingOwnership("location", options);
|
|
@@ -5427,7 +5472,7 @@ var location = {
|
|
|
5427
5472
|
if (typeof window !== "undefined" && craft?.location?.resumeRecording)
|
|
5428
5473
|
return craft.location.resumeRecording(options);
|
|
5429
5474
|
webLocationRecording.paused = false;
|
|
5430
|
-
return
|
|
5475
|
+
return webLocationRecordingState();
|
|
5431
5476
|
},
|
|
5432
5477
|
async stopRecording(options) {
|
|
5433
5478
|
validateRecordingOwnership("location", options);
|
|
@@ -5440,14 +5485,14 @@ var location = {
|
|
|
5440
5485
|
}
|
|
5441
5486
|
webLocationRecording.active = false;
|
|
5442
5487
|
webLocationRecording.paused = false;
|
|
5443
|
-
return { ...
|
|
5488
|
+
return { ...webLocationRecordingState(), locations: [...webLocationRecording.locations] };
|
|
5444
5489
|
},
|
|
5445
5490
|
async getRecordingState(options) {
|
|
5446
5491
|
validateRecordingOwnership("location", options);
|
|
5447
5492
|
const craft = getCraftMobile();
|
|
5448
5493
|
if (typeof window !== "undefined" && craft?.location?.getRecordingState)
|
|
5449
5494
|
return craft.location.getRecordingState(options);
|
|
5450
|
-
return
|
|
5495
|
+
return webLocationRecordingState();
|
|
5451
5496
|
},
|
|
5452
5497
|
async acknowledgeRecording(options) {
|
|
5453
5498
|
if (!options || !Object.prototype.hasOwnProperty.call(options, "expectedId"))
|
|
@@ -5459,6 +5504,7 @@ var location = {
|
|
|
5459
5504
|
if (webLocationRecording.active)
|
|
5460
5505
|
throw Object.assign(new Error("The recording is still active"), { code: "RECORDING_ALREADY_ACTIVE" });
|
|
5461
5506
|
webLocationRecording = { id: null, active: false, paused: false, startedAt: null, locations: [] };
|
|
5507
|
+
webLocationPowerMode = "default";
|
|
5462
5508
|
return true;
|
|
5463
5509
|
},
|
|
5464
5510
|
async readRecording(options) {
|
|
@@ -5466,7 +5512,8 @@ var location = {
|
|
|
5466
5512
|
const craft = getCraftMobile();
|
|
5467
5513
|
if (typeof window !== "undefined" && craft?.location?.readRecording)
|
|
5468
5514
|
return craft.location.readRecording(options);
|
|
5469
|
-
|
|
5515
|
+
const since = options?.since;
|
|
5516
|
+
return typeof since === "number" && Number.isFinite(since) ? webLocationRecording.locations.filter((location) => location.timestamp > since) : [...webLocationRecording.locations];
|
|
5470
5517
|
}
|
|
5471
5518
|
};
|
|
5472
5519
|
var share = {
|
package/dist/ios/src/index.d.ts
CHANGED
|
@@ -46,6 +46,13 @@ export interface CraftConfig {
|
|
|
46
46
|
backgroundColor?: string;
|
|
47
47
|
/** Default tint for native controls and selected tabs, as #RRGGBB. */
|
|
48
48
|
tintColor?: string;
|
|
49
|
+
/**
|
|
50
|
+
* CSS font stack for the native chrome's labels (tab bar, bar buttons); the
|
|
51
|
+
* first family packaged in `nativeFonts` wins, a generic one keeps the system face.
|
|
52
|
+
*/
|
|
53
|
+
fontFamily?: string;
|
|
54
|
+
/** CSS font stack for navigation titles; `fontFamily` when unset. */
|
|
55
|
+
headingFontFamily?: string;
|
|
49
56
|
/** The launch and webview background while the phone is in Dark Mode. */
|
|
50
57
|
backgroundColorDark?: string;
|
|
51
58
|
/** Edge-swipe back and forward through the page's history, pushState entries included. */
|
|
@@ -288,6 +288,7 @@ final class CraftAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificatio
|
|
|
288
288
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
|
289
289
|
let config = CraftConfig.bundled()
|
|
290
290
|
CraftInterfaceStyleController.configure(config)
|
|
291
|
+
CraftChromeFonts.configure(config)
|
|
291
292
|
// First, so a crash anywhere in the rest of launch is reported too.
|
|
292
293
|
CraftCrashReporter.shared.start(config)
|
|
293
294
|
UNUserNotificationCenter.current().delegate = self
|
|
@@ -1791,6 +1792,12 @@ struct CraftConfig: Codable {
|
|
|
1791
1792
|
var backgroundColor: String = "#1a1a2e"
|
|
1792
1793
|
/// Default native control and selected-tab tint, independent of page chrome.
|
|
1793
1794
|
var tintColor: String? = nil
|
|
1795
|
+
/// CSS font stacks for the app's own chrome (navigation titles, tab and
|
|
1796
|
+
/// bar button labels): the first family packaged with the app wins, and a
|
|
1797
|
+
/// generic one (`system-ui`, `sans-serif`) keeps the system face.
|
|
1798
|
+
var fontFamily: String? = nil
|
|
1799
|
+
/// Navigation titles; `fontFamily` when unset.
|
|
1800
|
+
var headingFontFamily: String? = nil
|
|
1794
1801
|
/// The background while the phone is in Dark Mode; the light one if unset.
|
|
1795
1802
|
var backgroundColorDark: String? = nil
|
|
1796
1803
|
/// Edge-swipe back and forward through the page's history.
|
|
@@ -2331,6 +2338,28 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2331
2338
|
private var isLocationRecordingPaused = false
|
|
2332
2339
|
private var locationRecordingId: String?
|
|
2333
2340
|
private var locationRecordingStartedAt: TimeInterval?
|
|
2341
|
+
/// The power mode the recording was asked for: `default`, `saver` (a
|
|
2342
|
+
/// long recording, an ultra, that trades fixes nobody needs for
|
|
2343
|
+
/// battery) or `auto` (a saver while the device itself is saving
|
|
2344
|
+
/// power). Set by `startRecording`, changed by `setRecordingPowerMode`
|
|
2345
|
+
/// and kept in the state file. See `applyLocationRecordingPower`.
|
|
2346
|
+
private var locationRecordingPowerMode = "default"
|
|
2347
|
+
/// What the recorder is using: the mode, with `auto` resolved from the
|
|
2348
|
+
/// device. Never true outside a recording.
|
|
2349
|
+
private var locationRecordingPowerSaver: Bool {
|
|
2350
|
+
guard isRecordingLocation else { return false }
|
|
2351
|
+
switch locationRecordingPowerMode {
|
|
2352
|
+
case "saver": return true
|
|
2353
|
+
case "auto": return Self.deviceIsSavingPower
|
|
2354
|
+
default: return false
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
/// The saver setting `applyLocationRecordingPower` last gave the manager.
|
|
2358
|
+
private var locationRecordingAppliedSaver = false
|
|
2359
|
+
private var locationRecordingPowerObservers: [NSObjectProtocol] = []
|
|
2360
|
+
/// Recent fix speeds while saving power; steady ones relax the accuracy.
|
|
2361
|
+
private var locationRecordingRecentSpeeds: [Double] = []
|
|
2362
|
+
private var locationRecordingSteady = false
|
|
2334
2363
|
private let recordingDirectory: URL?
|
|
2335
2364
|
private var locationRecordingRestoreError: Error?
|
|
2336
2365
|
|
|
@@ -2580,7 +2609,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2580
2609
|
/// `craft_ios_deliver_result` instead of evaluating JavaScript here —
|
|
2581
2610
|
/// one reply path, owned by whichever side received the page's message.
|
|
2582
2611
|
func dispatch(action: String, body: [String: Any], callbackId: String?) {
|
|
2583
|
-
if ["startLocationRecording", "pauseLocationRecording", "resumeLocationRecording", "stopLocationRecording", "getLocationRecordingState", "readLocationRecording", "acknowledgeLocationRecording"].contains(action),
|
|
2612
|
+
if ["startLocationRecording", "setLocationRecordingPowerMode", "pauseLocationRecording", "resumeLocationRecording", "stopLocationRecording", "getLocationRecordingState", "readLocationRecording", "acknowledgeLocationRecording"].contains(action),
|
|
2584
2613
|
locationRecordingRestoreError != nil {
|
|
2585
2614
|
rejectCallback(callbackId, error: "Unable to restore recording data", code: "RECORDING_STORAGE_ERROR")
|
|
2586
2615
|
return
|
|
@@ -2799,6 +2828,14 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2799
2828
|
} else {
|
|
2800
2829
|
rejectCallback(callbackId, error: "Geolocation is disabled", code: "CAPABILITY_DISABLED")
|
|
2801
2830
|
}
|
|
2831
|
+
case "setLocationRecordingPowerMode":
|
|
2832
|
+
if !CraftZigRuntime.supportsRecordingPowerMode {
|
|
2833
|
+
rejectCallback(callbackId, error: "The linked runtime's recorder has no power modes", code: "RECORDING_POWER_MODE_UNSUPPORTED")
|
|
2834
|
+
} else if config.enableGeolocation {
|
|
2835
|
+
if requireLocationRecordingOwner(body, callbackId: callbackId) { setLocationRecordingPowerMode(body: body, callbackId: callbackId) }
|
|
2836
|
+
} else {
|
|
2837
|
+
rejectCallback(callbackId, error: "Geolocation is disabled", code: "CAPABILITY_DISABLED")
|
|
2838
|
+
}
|
|
2802
2839
|
case "pauseLocationRecording":
|
|
2803
2840
|
if requireLocationRecordingOwner(body, callbackId: callbackId) { pauseLocationRecording(callbackId: callbackId) }
|
|
2804
2841
|
case "resumeLocationRecording":
|
|
@@ -2808,7 +2845,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2808
2845
|
case "getLocationRecordingState":
|
|
2809
2846
|
getLocationRecordingState(callbackId: callbackId)
|
|
2810
2847
|
case "readLocationRecording":
|
|
2811
|
-
if requireLocationRecordingOwner(body, callbackId: callbackId) { readLocationRecording(callbackId: callbackId) }
|
|
2848
|
+
if requireLocationRecordingOwner(body, callbackId: callbackId) { readLocationRecording(body: body, callbackId: callbackId) }
|
|
2812
2849
|
case "acknowledgeLocationRecording":
|
|
2813
2850
|
_ = archivedLocationAction(action, body: body, callbackId: callbackId)
|
|
2814
2851
|
// App Badge
|
|
@@ -4169,6 +4206,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
4169
4206
|
crypto: true,
|
|
4170
4207
|
locationRecordingOwnership: \(config.enableGeolocation && CraftZigRuntime.supportsRecordingOwnership),
|
|
4171
4208
|
locationRecordingIntentIdentity: \(config.enableGeolocation && CraftZigRuntime.supportsRecordingIntentIdentity),
|
|
4209
|
+
locationRecordingPowerMode: \(config.enableGeolocation && CraftZigRuntime.supportsRecordingPowerMode),
|
|
4172
4210
|
backgroundLocation: \(config.enableBackgroundLocation),
|
|
4173
4211
|
healthKit: \(config.enableHealthKit),
|
|
4174
4212
|
liveActivities: \(config.enableLiveActivities),
|
|
@@ -5772,7 +5810,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5772
5810
|
};
|
|
5773
5811
|
craft._invoke = function(action, payload) {
|
|
5774
5812
|
var recordingOptions = action === 'heartRate' && payload && ['startRecording', 'pauseRecording', 'resumeRecording', 'stopRecording', 'readRecording', 'getRecordingState', 'acknowledgeRecording'].indexOf(payload.method) !== -1
|
|
5775
|
-
? payload.options : ['startLocationRecording', 'pauseLocationRecording', 'resumeLocationRecording', 'stopLocationRecording', 'readLocationRecording', 'getLocationRecordingState', 'acknowledgeLocationRecording'].indexOf(action) !== -1 ? payload : null;
|
|
5813
|
+
? payload.options : ['startLocationRecording', 'setLocationRecordingPowerMode', 'pauseLocationRecording', 'resumeLocationRecording', 'stopLocationRecording', 'readLocationRecording', 'getLocationRecordingState', 'acknowledgeLocationRecording'].indexOf(action) !== -1 ? payload : null;
|
|
5776
5814
|
if (recordingOptions && Object.prototype.hasOwnProperty.call(recordingOptions, 'recordingId') &&
|
|
5777
5815
|
(typeof recordingOptions.recordingId !== 'string' || !recordingOptions.recordingId.trim())) {
|
|
5778
5816
|
var invalidStart = new Error('recordingId must be a nonempty recording ID');
|
|
@@ -5950,6 +5988,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5950
5988
|
void stopLocationWatchIfUnused().catch(function() {});
|
|
5951
5989
|
},
|
|
5952
5990
|
startRecording: function(options) { return craft._invoke('startLocationRecording', options || {}); },
|
|
5991
|
+
setRecordingPowerMode: function(options) { return craft._invoke('setLocationRecordingPowerMode', options || {}); },
|
|
5953
5992
|
pauseRecording: function(options) { return craft._invoke('pauseLocationRecording', options || {}); },
|
|
5954
5993
|
resumeRecording: function(options) { return craft._invoke('resumeLocationRecording', options || {}); },
|
|
5955
5994
|
stopRecording: function(options) { return craft._invoke('stopLocationRecording', options || {}); },
|
|
@@ -6906,9 +6945,13 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
6906
6945
|
return
|
|
6907
6946
|
}
|
|
6908
6947
|
manager.delegate = self
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6948
|
+
// A recording in progress owns the shared manager's accuracy: a
|
|
6949
|
+
// one-off fix must neither coarsen it nor undo its power saving.
|
|
6950
|
+
if !(isRecordingLocation && !isLocationRecordingPaused) {
|
|
6951
|
+
manager.desiredAccuracy = body["enableHighAccuracy"] as? Bool == true
|
|
6952
|
+
? kCLLocationAccuracyBest
|
|
6953
|
+
: kCLLocationAccuracyHundredMeters
|
|
6954
|
+
}
|
|
6912
6955
|
|
|
6913
6956
|
if let pendingCallbackId = singleLocationCallbackId {
|
|
6914
6957
|
finishSingleLocationRequest()
|
|
@@ -6974,7 +7017,113 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
6974
7017
|
singleLocationCallbackId = nil
|
|
6975
7018
|
singleLocationAcquisitionStarted = false
|
|
6976
7019
|
singleLocationAuthorizationRequested = false
|
|
6977
|
-
locationManager?.desiredAccuracy =
|
|
7020
|
+
locationManager?.desiredAccuracy = recordingDesiredAccuracy
|
|
7021
|
+
}
|
|
7022
|
+
|
|
7023
|
+
/// Fixes at least this far apart, metres, while saving power. A
|
|
7024
|
+
/// filter of d metres spaces fixes d/v seconds apart at v m/s; the
|
|
7025
|
+
/// route counts fixes more than 30 s apart as a pause, so d must stay
|
|
7026
|
+
/// under 30 s at the slowest speed that still counts as moving (0.2
|
|
7027
|
+
/// m/s on the server): 5 m is 25 s at 0.2 m/s.
|
|
7028
|
+
static let powerSaverDistanceFilterM: CLLocationDistance = 5
|
|
7029
|
+
|
|
7030
|
+
/// Best, except while a power-saving recording holds a steady speed.
|
|
7031
|
+
private var recordingDesiredAccuracy: CLLocationAccuracy {
|
|
7032
|
+
locationRecordingPowerSaver && locationRecordingSteady ? kCLLocationAccuracyNearestTenMeters : kCLLocationAccuracyBest
|
|
7033
|
+
}
|
|
7034
|
+
|
|
7035
|
+
/// The manager's filter and accuracy for the recording's power mode.
|
|
7036
|
+
private func applyLocationRecordingPower() {
|
|
7037
|
+
let saver = locationRecordingPowerSaver
|
|
7038
|
+
locationRecordingAppliedSaver = saver
|
|
7039
|
+
locationManager?.distanceFilter = saver ? Self.powerSaverDistanceFilterM : kCLDistanceFilterNone
|
|
7040
|
+
if singleLocationCallbackId == nil || (isRecordingLocation && !isLocationRecordingPaused) {
|
|
7041
|
+
locationManager?.desiredAccuracy = recordingDesiredAccuracy
|
|
7042
|
+
}
|
|
7043
|
+
}
|
|
7044
|
+
|
|
7045
|
+
/// A new mode, a resume or a change of the device's power state starts
|
|
7046
|
+
/// the steady-pace history over: speeds seen under the old setting say
|
|
7047
|
+
/// nothing about the new one.
|
|
7048
|
+
private func resetLocationRecordingPace() {
|
|
7049
|
+
locationRecordingRecentSpeeds.removeAll()
|
|
7050
|
+
locationRecordingSteady = false
|
|
7051
|
+
}
|
|
7052
|
+
|
|
7053
|
+
/// Under this charge, unplugged, `auto` saves power.
|
|
7054
|
+
static let autoPowerSaverBatteryLevel: Float = 0.2
|
|
7055
|
+
|
|
7056
|
+
/// The device wants power saved: Low Power Mode, or a battery under
|
|
7057
|
+
/// 20% that is not charging (`unplugged`; an unknown state, as in the
|
|
7058
|
+
/// simulator, does not count).
|
|
7059
|
+
static var deviceIsSavingPower: Bool {
|
|
7060
|
+
if ProcessInfo.processInfo.isLowPowerModeEnabled { return true }
|
|
7061
|
+
let device = UIDevice.current
|
|
7062
|
+
return device.isBatteryMonitoringEnabled && device.batteryState == .unplugged
|
|
7063
|
+
&& device.batteryLevel >= 0 && device.batteryLevel < autoPowerSaverBatteryLevel
|
|
7064
|
+
}
|
|
7065
|
+
|
|
7066
|
+
/// `auto` follows the platform's own change notifications, only while a
|
|
7067
|
+
/// recording runs in that mode, so no other recording pays for them.
|
|
7068
|
+
private func updateLocationRecordingPowerObservers() {
|
|
7069
|
+
let wanted = isRecordingLocation && locationRecordingPowerMode == "auto"
|
|
7070
|
+
guard wanted != !locationRecordingPowerObservers.isEmpty else { return }
|
|
7071
|
+
let center = NotificationCenter.default
|
|
7072
|
+
if !wanted {
|
|
7073
|
+
locationRecordingPowerObservers.forEach { center.removeObserver($0) }
|
|
7074
|
+
locationRecordingPowerObservers.removeAll()
|
|
7075
|
+
UIDevice.current.isBatteryMonitoringEnabled = false
|
|
7076
|
+
return
|
|
7077
|
+
}
|
|
7078
|
+
UIDevice.current.isBatteryMonitoringEnabled = true
|
|
7079
|
+
let names: [Notification.Name] = [
|
|
7080
|
+
.NSProcessInfoPowerStateDidChange,
|
|
7081
|
+
UIDevice.batteryLevelDidChangeNotification,
|
|
7082
|
+
UIDevice.batteryStateDidChangeNotification,
|
|
7083
|
+
]
|
|
7084
|
+
locationRecordingPowerObservers = names.map { name in
|
|
7085
|
+
center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
|
|
7086
|
+
self?.locationRecordingDeviceStateChanged()
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
}
|
|
7090
|
+
|
|
7091
|
+
/// The device's power state changed: re-resolve `auto`, and only touch
|
|
7092
|
+
/// the manager when the answer is different.
|
|
7093
|
+
private func locationRecordingDeviceStateChanged() {
|
|
7094
|
+
guard isRecordingLocation, locationRecordingPowerMode == "auto",
|
|
7095
|
+
locationRecordingPowerSaver != locationRecordingAppliedSaver else { return }
|
|
7096
|
+
resetLocationRecordingPace()
|
|
7097
|
+
applyLocationRecordingPower()
|
|
7098
|
+
}
|
|
7099
|
+
|
|
7100
|
+
static let recordingPowerModes = ["default", "saver", "auto"]
|
|
7101
|
+
|
|
7102
|
+
/// A mode the page may ask for; nil for anything else, `nil` included.
|
|
7103
|
+
private static func recordingPowerMode(_ value: Any?) -> String? {
|
|
7104
|
+
guard let mode = value as? String, recordingPowerModes.contains(mode) else { return nil }
|
|
7105
|
+
return mode
|
|
7106
|
+
}
|
|
7107
|
+
|
|
7108
|
+
/// Steady: the last ten fixes within a fifth of their mean speed, at
|
|
7109
|
+
/// least a slow walk, each accurate to 10 m. Anything else (a change
|
|
7110
|
+
/// of pace, a weak signal) goes straight back to Best.
|
|
7111
|
+
private func noteLocationRecordingSpeed(_ location: CLLocation) {
|
|
7112
|
+
guard locationRecordingPowerSaver else { return }
|
|
7113
|
+
let usable = location.speed >= 0 && location.horizontalAccuracy >= 0 && location.horizontalAccuracy <= 10
|
|
7114
|
+
if usable {
|
|
7115
|
+
locationRecordingRecentSpeeds.append(location.speed)
|
|
7116
|
+
if locationRecordingRecentSpeeds.count > 10 { locationRecordingRecentSpeeds.removeFirst(locationRecordingRecentSpeeds.count - 10) }
|
|
7117
|
+
} else if location.horizontalAccuracy > 20 || location.horizontalAccuracy < 0 {
|
|
7118
|
+
locationRecordingRecentSpeeds.removeAll()
|
|
7119
|
+
}
|
|
7120
|
+
let speeds = locationRecordingRecentSpeeds
|
|
7121
|
+
let mean = speeds.isEmpty ? 0 : speeds.reduce(0, +) / Double(speeds.count)
|
|
7122
|
+
let steady = speeds.count >= 10 && mean >= 0.8 && speeds.allSatisfy { abs($0 - mean) <= mean * 0.2 }
|
|
7123
|
+
if steady != locationRecordingSteady {
|
|
7124
|
+
locationRecordingSteady = steady
|
|
7125
|
+
applyLocationRecordingPower()
|
|
7126
|
+
}
|
|
6978
7127
|
}
|
|
6979
7128
|
|
|
6980
7129
|
private func locationData(_ location: CLLocation) -> [String: Any] {
|
|
@@ -7046,7 +7195,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7046
7195
|
}
|
|
7047
7196
|
|
|
7048
7197
|
private func persistLocationRecordingState() throws {
|
|
7049
|
-
let state: [String: Any] = ["active": isRecordingLocation, "paused": isLocationRecordingPaused, "id": locationRecordingId ?? NSNull(), "startedAt": locationRecordingStartedAt ?? NSNull()]
|
|
7198
|
+
let state: [String: Any] = ["active": isRecordingLocation, "paused": isLocationRecordingPaused, "id": locationRecordingId ?? NSNull(), "startedAt": locationRecordingStartedAt ?? NSNull(), "powerMode": locationRecordingPowerMode]
|
|
7050
7199
|
try CraftRecordingArchive.writeAtomically(JSONSerialization.data(withJSONObject: state), locationRecordingStateURL)
|
|
7051
7200
|
}
|
|
7052
7201
|
|
|
@@ -7067,6 +7216,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7067
7216
|
isLocationRecordingPaused = state["paused"] as? Bool ?? false
|
|
7068
7217
|
locationRecordingId = state["id"] as? String
|
|
7069
7218
|
locationRecordingStartedAt = state["startedAt"] as? TimeInterval
|
|
7219
|
+
locationRecordingPowerMode = Self.recordingPowerMode(state["powerMode"]) ?? "default"
|
|
7070
7220
|
if let id = locationRecordingId, try CraftRecordingArchive.isAcknowledged(recordingBaseDirectory, "location", id) {
|
|
7071
7221
|
try CraftRecordingArchive.acknowledge(recordingBaseDirectory, "location", id)
|
|
7072
7222
|
if FileManager.default.fileExists(atPath: locationRecordingTrackURL.path) { try FileManager.default.removeItem(at: locationRecordingTrackURL) }
|
|
@@ -7076,9 +7226,11 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7076
7226
|
isLocationRecordingPaused = false
|
|
7077
7227
|
try persistLocationRecordingState()
|
|
7078
7228
|
}
|
|
7229
|
+
updateLocationRecordingPowerObservers()
|
|
7079
7230
|
if isRecordingLocation && !isLocationRecordingPaused {
|
|
7080
7231
|
locationManager?.delegate = self
|
|
7081
7232
|
configureBackgroundLocationIfNeeded()
|
|
7233
|
+
applyLocationRecordingPower()
|
|
7082
7234
|
locationManager?.startUpdatingLocation()
|
|
7083
7235
|
}
|
|
7084
7236
|
|
|
@@ -7161,6 +7313,14 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7161
7313
|
rejectCallback(callbackId, error: "A location recording is already active", code: "RECORDING_ALREADY_ACTIVE")
|
|
7162
7314
|
return
|
|
7163
7315
|
}
|
|
7316
|
+
var powerMode = "default"
|
|
7317
|
+
if let requested = body["powerMode"], !(requested is NSNull) {
|
|
7318
|
+
guard let mode = Self.recordingPowerMode(requested) else {
|
|
7319
|
+
rejectCallback(callbackId, error: "powerMode must be 'default', 'saver' or 'auto'", code: "INVALID_ARGUMENT")
|
|
7320
|
+
return
|
|
7321
|
+
}
|
|
7322
|
+
powerMode = mode
|
|
7323
|
+
}
|
|
7164
7324
|
let directory = locationRecordingTrackURL.deletingLastPathComponent()
|
|
7165
7325
|
let id: String
|
|
7166
7326
|
do {
|
|
@@ -7176,20 +7336,24 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7176
7336
|
rejectCallback(callbackId, error: "Unable to create the location recording", code: "RECORDING_STORAGE_ERROR")
|
|
7177
7337
|
return
|
|
7178
7338
|
}
|
|
7179
|
-
let previous = (locationRecordingId, locationRecordingStartedAt, isRecordingLocation, isLocationRecordingPaused)
|
|
7339
|
+
let previous = (locationRecordingId, locationRecordingStartedAt, isRecordingLocation, isLocationRecordingPaused, locationRecordingPowerMode)
|
|
7180
7340
|
locationRecordingId = id
|
|
7181
7341
|
locationRecordingStartedAt = Date().timeIntervalSince1970 * 1000
|
|
7182
7342
|
isRecordingLocation = true
|
|
7183
7343
|
isLocationRecordingPaused = false
|
|
7344
|
+
locationRecordingPowerMode = powerMode
|
|
7345
|
+
resetLocationRecordingPace()
|
|
7184
7346
|
do { try persistLocationRecordingState() }
|
|
7185
7347
|
catch {
|
|
7186
|
-
(locationRecordingId, locationRecordingStartedAt, isRecordingLocation, isLocationRecordingPaused) = previous
|
|
7348
|
+
(locationRecordingId, locationRecordingStartedAt, isRecordingLocation, isLocationRecordingPaused, locationRecordingPowerMode) = previous
|
|
7187
7349
|
rejectCallback(callbackId, error: "Unable to persist the location recording", code: "RECORDING_STORAGE_ERROR")
|
|
7188
7350
|
return
|
|
7189
7351
|
}
|
|
7190
7352
|
locationManager?.delegate = self
|
|
7191
7353
|
requestLocationAuthorization()
|
|
7192
7354
|
configureBackgroundLocationIfNeeded()
|
|
7355
|
+
updateLocationRecordingPowerObservers()
|
|
7356
|
+
applyLocationRecordingPower()
|
|
7193
7357
|
locationManager?.startUpdatingLocation()
|
|
7194
7358
|
resolveCallback(callbackId, result: locationRecordingSummary())
|
|
7195
7359
|
|
|
@@ -7198,6 +7362,31 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7198
7362
|
#endif
|
|
7199
7363
|
}
|
|
7200
7364
|
|
|
7365
|
+
/// Change the running recording's power mode. The state file carries it,
|
|
7366
|
+
/// so a relaunch restores what was last asked for.
|
|
7367
|
+
private func setLocationRecordingPowerMode(body: [String: Any], callbackId: String?) {
|
|
7368
|
+
guard let mode = Self.recordingPowerMode(body["powerMode"]) else {
|
|
7369
|
+
rejectCallback(callbackId, error: "powerMode must be 'default', 'saver' or 'auto'", code: "INVALID_ARGUMENT")
|
|
7370
|
+
return
|
|
7371
|
+
}
|
|
7372
|
+
guard isRecordingLocation else {
|
|
7373
|
+
rejectCallback(callbackId, error: "No active location recording", code: "NO_ACTIVE_RECORDING")
|
|
7374
|
+
return
|
|
7375
|
+
}
|
|
7376
|
+
let previous = locationRecordingPowerMode
|
|
7377
|
+
locationRecordingPowerMode = mode
|
|
7378
|
+
do { try persistLocationRecordingState() }
|
|
7379
|
+
catch {
|
|
7380
|
+
locationRecordingPowerMode = previous
|
|
7381
|
+
rejectCallback(callbackId, error: "Unable to persist the location recording", code: "RECORDING_STORAGE_ERROR")
|
|
7382
|
+
return
|
|
7383
|
+
}
|
|
7384
|
+
resetLocationRecordingPace()
|
|
7385
|
+
updateLocationRecordingPowerObservers()
|
|
7386
|
+
applyLocationRecordingPower()
|
|
7387
|
+
resolveCallback(callbackId, result: locationRecordingSummary())
|
|
7388
|
+
}
|
|
7389
|
+
|
|
7201
7390
|
private func pauseLocationRecording(callbackId: String?) {
|
|
7202
7391
|
guard isRecordingLocation else {
|
|
7203
7392
|
rejectCallback(callbackId, error: "No active location recording", code: "NO_ACTIVE_RECORDING")
|
|
@@ -7235,6 +7424,11 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7235
7424
|
locationManager?.delegate = self
|
|
7236
7425
|
requestLocationAuthorization()
|
|
7237
7426
|
configureBackgroundLocationIfNeeded()
|
|
7427
|
+
// The manager forgets nothing across a pause, but the pace seen
|
|
7428
|
+
// before it says nothing about the pace after it, and a recording
|
|
7429
|
+
// restored while paused never had its filter set at all.
|
|
7430
|
+
resetLocationRecordingPace()
|
|
7431
|
+
applyLocationRecordingPower()
|
|
7238
7432
|
locationManager?.startUpdatingLocation()
|
|
7239
7433
|
resolveCallback(callbackId, result: locationRecordingSummary())
|
|
7240
7434
|
|
|
@@ -7255,6 +7449,9 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7255
7449
|
locationManager?.stopUpdatingLocation()
|
|
7256
7450
|
locationManager?.allowsBackgroundLocationUpdates = false
|
|
7257
7451
|
}
|
|
7452
|
+
resetLocationRecordingPace()
|
|
7453
|
+
updateLocationRecordingPowerObservers()
|
|
7454
|
+
applyLocationRecordingPower()
|
|
7258
7455
|
var summary = locationRecordingSummary()
|
|
7259
7456
|
summary["locations"] = locations
|
|
7260
7457
|
resolveCallback(callbackId, result: summary)
|
|
@@ -7267,8 +7464,18 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7267
7464
|
resolveCallback(callbackId, result: summary)
|
|
7268
7465
|
}
|
|
7269
7466
|
|
|
7270
|
-
|
|
7271
|
-
|
|
7467
|
+
/// The recording's fixes, or with `since` (epoch ms) only those recorded
|
|
7468
|
+
/// after it. A screen showing a long run reads every few seconds, and a
|
|
7469
|
+
/// 20-hour track is ~72,000 lines: `since` reads the file's tail, not all
|
|
7470
|
+
/// of it, so each read costs what the new fixes do.
|
|
7471
|
+
private func readLocationRecording(body: [String: Any], callbackId: String?) {
|
|
7472
|
+
do {
|
|
7473
|
+
if let since = (body["since"] as? NSNumber)?.doubleValue, since.isFinite {
|
|
7474
|
+
resolveCallback(callbackId, result: try loadRecordedLocationsStrict(after: since))
|
|
7475
|
+
} else {
|
|
7476
|
+
resolveCallback(callbackId, result: try loadRecordedLocationsStrict())
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7272
7479
|
catch { rejectCallback(callbackId, error: "Unable to read the location recording", code: "RECORDING_STORAGE_ERROR") }
|
|
7273
7480
|
}
|
|
7274
7481
|
|
|
@@ -7278,6 +7485,8 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7278
7485
|
"active": isRecordingLocation,
|
|
7279
7486
|
"paused": isLocationRecordingPaused,
|
|
7280
7487
|
"startedAt": locationRecordingStartedAt ?? NSNull(),
|
|
7488
|
+
"powerMode": locationRecordingPowerMode,
|
|
7489
|
+
"effectivePowerMode": locationRecordingPowerSaver ? "saver" : "default",
|
|
7281
7490
|
]
|
|
7282
7491
|
}
|
|
7283
7492
|
|
|
@@ -7306,6 +7515,39 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7306
7515
|
|
|
7307
7516
|
private func loadRecordedLocations() -> [[String: Any]] { (try? loadRecordedLocationsStrict()) ?? [] }
|
|
7308
7517
|
|
|
7518
|
+
/// The fixes recorded after `since` (epoch ms), oldest first, read from
|
|
7519
|
+
/// the end of the track backwards: a window of the file's last bytes,
|
|
7520
|
+
/// widened until it reaches a fix at or before `since` or the start.
|
|
7521
|
+
/// Fixes are appended in time order, one JSON object per line.
|
|
7522
|
+
private func loadRecordedLocationsStrict(after since: Double) throws -> [[String: Any]] {
|
|
7523
|
+
if locationRecordingId == nil { return [] }
|
|
7524
|
+
let handle = try FileHandle(forReadingFrom: locationRecordingTrackURL)
|
|
7525
|
+
defer { try? handle.close() }
|
|
7526
|
+
let size = try handle.seekToEnd()
|
|
7527
|
+
var window: UInt64 = 64 * 1024
|
|
7528
|
+
while true {
|
|
7529
|
+
let start = size > window ? size - window : 0
|
|
7530
|
+
try handle.seek(toOffset: start)
|
|
7531
|
+
let data = try handle.readToEnd() ?? Data()
|
|
7532
|
+
var lines = data.split(separator: 0x0A, omittingEmptySubsequences: true)
|
|
7533
|
+
// Mid-file, the window's first line may be cut: it is older than
|
|
7534
|
+
// every line after it, so it is never the one needed.
|
|
7535
|
+
if start > 0, !lines.isEmpty { lines.removeFirst() }
|
|
7536
|
+
var newer: [[String: Any]] = []
|
|
7537
|
+
var reachedOlder = start == 0
|
|
7538
|
+
for line in lines.reversed() {
|
|
7539
|
+
guard let fix = try? JSONSerialization.jsonObject(with: Data(line)) as? [String: Any] else { continue }
|
|
7540
|
+
if let time = (fix["timestamp"] as? NSNumber)?.doubleValue, time <= since {
|
|
7541
|
+
reachedOlder = true
|
|
7542
|
+
break
|
|
7543
|
+
}
|
|
7544
|
+
newer.append(fix)
|
|
7545
|
+
}
|
|
7546
|
+
if reachedOlder { return Array(newer.reversed()) }
|
|
7547
|
+
window *= 4
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7550
|
+
|
|
7309
7551
|
private func loadRecordedLocationsStrict() throws -> [[String: Any]] {
|
|
7310
7552
|
if locationRecordingId == nil { return [] }
|
|
7311
7553
|
let text = try String(contentsOf: locationRecordingTrackURL, encoding: .utf8)
|
|
@@ -7325,7 +7567,17 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7325
7567
|
}
|
|
7326
7568
|
|
|
7327
7569
|
if isRecordingLocation, let id = locationRecordingId { data["recordingId"] = id }
|
|
7570
|
+
// In the background Core Location can hand over several fixes at
|
|
7571
|
+
// once: the recording keeps each of them, not just the newest, or
|
|
7572
|
+
// the route would have holes a pause is made of.
|
|
7573
|
+
for earlier in locations.dropLast() {
|
|
7574
|
+
var sample = locationData(earlier)
|
|
7575
|
+
if isRecordingLocation, let id = locationRecordingId { sample["recordingId"] = id }
|
|
7576
|
+
appendRecordedLocation(sample)
|
|
7577
|
+
noteLocationRecordingSpeed(earlier)
|
|
7578
|
+
}
|
|
7328
7579
|
appendRecordedLocation(data)
|
|
7580
|
+
noteLocationRecordingSpeed(location)
|
|
7329
7581
|
if isWatchingLocation || isRecordingLocation {
|
|
7330
7582
|
sendToWeb("craftLocationUpdate", data: data)
|
|
7331
7583
|
}
|
|
@@ -9843,6 +10095,10 @@ final class CraftZigRuntime: NSObject {
|
|
|
9843
10095
|
return unsafeBitCast(symbol, to: RecordingOwnershipVersionFn.self)() >= 1
|
|
9844
10096
|
}
|
|
9845
10097
|
|
|
10098
|
+
/// Power modes belong to Swift's recorder. A linked Zig runtime owns the
|
|
10099
|
+
/// recorder instead and has no power mode, so it must not be advertised.
|
|
10100
|
+
static var supportsRecordingPowerMode: Bool { !isLinked }
|
|
10101
|
+
|
|
9846
10102
|
static var supportsRecordingIntentIdentity: Bool {
|
|
9847
10103
|
if !isLinked { return true }
|
|
9848
10104
|
guard performRecordingFn != nil,
|
|
@@ -1184,6 +1184,7 @@ final class CraftHybridController: UITabBarController, UITabBarControllerDelegat
|
|
|
1184
1184
|
#if compiler(>=6.2)
|
|
1185
1185
|
if #available(iOS 26.0, *) { tabBarMinimizeBehavior = .never }
|
|
1186
1186
|
#endif
|
|
1187
|
+
CraftChromeFonts.apply(to: tabBar)
|
|
1187
1188
|
accessoryButton.accessibilityIdentifier = "craft-tab-accessory"
|
|
1188
1189
|
accessoryButton.addTarget(self, action: #selector(openTabAccessory), for: .touchUpInside)
|
|
1189
1190
|
chromeSubscription = CraftChrome.shared.objectWillChange.sink { [weak self] in
|
|
@@ -79,6 +79,7 @@ enum CraftNativeActions {
|
|
|
79
79
|
values.append("geolocation")
|
|
80
80
|
if CraftZigRuntime.supportsRecordingOwnership { values.append("locationRecordingOwnership") }
|
|
81
81
|
if CraftZigRuntime.supportsRecordingIntentIdentity { values.append("locationRecordingIntentIdentity") }
|
|
82
|
+
if CraftZigRuntime.supportsRecordingPowerMode { values.append("locationRecordingPowerMode") }
|
|
82
83
|
}
|
|
83
84
|
// The Music app's song and transport are the coordinator's too.
|
|
84
85
|
if config.enableAppleMusic { values.append("music") }
|
|
@@ -494,7 +495,8 @@ enum CraftNativeActions {
|
|
|
494
495
|
}
|
|
495
496
|
let actions = [
|
|
496
497
|
"getCurrentPosition": "getCurrentPosition", "watchPosition": "watchPosition", "clearWatch": "clearWatch",
|
|
497
|
-
"startRecording": "startLocationRecording", "
|
|
498
|
+
"startRecording": "startLocationRecording", "setRecordingPowerMode": "setLocationRecordingPowerMode",
|
|
499
|
+
"pauseRecording": "pauseLocationRecording",
|
|
498
500
|
"resumeRecording": "resumeLocationRecording", "stopRecording": "stopLocationRecording",
|
|
499
501
|
"getRecordingState": "getLocationRecordingState", "readRecording": "readLocationRecording",
|
|
500
502
|
"acknowledgeRecording": "acknowledgeLocationRecording",
|