@rejourneyco/react-native 1.0.0
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/android/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
- package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
- package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
- package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
- package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
- package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
- package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
- package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
- package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
- package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
- package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
- package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
- package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
- package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
- package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
- package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
- package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Capture/RJANRHandler.h +42 -0
- package/ios/Capture/RJANRHandler.m +328 -0
- package/ios/Capture/RJCaptureEngine.h +275 -0
- package/ios/Capture/RJCaptureEngine.m +2062 -0
- package/ios/Capture/RJCaptureHeuristics.h +80 -0
- package/ios/Capture/RJCaptureHeuristics.m +903 -0
- package/ios/Capture/RJCrashHandler.h +46 -0
- package/ios/Capture/RJCrashHandler.m +313 -0
- package/ios/Capture/RJMotionEvent.h +183 -0
- package/ios/Capture/RJMotionEvent.m +183 -0
- package/ios/Capture/RJPerformanceManager.h +100 -0
- package/ios/Capture/RJPerformanceManager.m +373 -0
- package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
- package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
- package/ios/Capture/RJSegmentUploader.h +146 -0
- package/ios/Capture/RJSegmentUploader.m +778 -0
- package/ios/Capture/RJVideoEncoder.h +247 -0
- package/ios/Capture/RJVideoEncoder.m +1036 -0
- package/ios/Capture/RJViewControllerTracker.h +73 -0
- package/ios/Capture/RJViewControllerTracker.m +508 -0
- package/ios/Capture/RJViewHierarchyScanner.h +215 -0
- package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
- package/ios/Capture/RJViewSerializer.h +119 -0
- package/ios/Capture/RJViewSerializer.m +498 -0
- package/ios/Core/RJConstants.h +124 -0
- package/ios/Core/RJConstants.m +88 -0
- package/ios/Core/RJLifecycleManager.h +85 -0
- package/ios/Core/RJLifecycleManager.m +308 -0
- package/ios/Core/RJLogger.h +61 -0
- package/ios/Core/RJLogger.m +211 -0
- package/ios/Core/RJTypes.h +176 -0
- package/ios/Core/RJTypes.m +66 -0
- package/ios/Core/Rejourney.h +64 -0
- package/ios/Core/Rejourney.mm +2495 -0
- package/ios/Network/RJDeviceAuthManager.h +94 -0
- package/ios/Network/RJDeviceAuthManager.m +967 -0
- package/ios/Network/RJNetworkMonitor.h +68 -0
- package/ios/Network/RJNetworkMonitor.m +267 -0
- package/ios/Network/RJRetryManager.h +73 -0
- package/ios/Network/RJRetryManager.m +325 -0
- package/ios/Network/RJUploadManager.h +267 -0
- package/ios/Network/RJUploadManager.m +2296 -0
- package/ios/Privacy/RJPrivacyMask.h +163 -0
- package/ios/Privacy/RJPrivacyMask.m +922 -0
- package/ios/Rejourney.h +63 -0
- package/ios/Touch/RJGestureClassifier.h +130 -0
- package/ios/Touch/RJGestureClassifier.m +333 -0
- package/ios/Touch/RJTouchInterceptor.h +169 -0
- package/ios/Touch/RJTouchInterceptor.m +772 -0
- package/ios/Utils/RJEventBuffer.h +112 -0
- package/ios/Utils/RJEventBuffer.m +358 -0
- package/ios/Utils/RJGzipUtils.h +33 -0
- package/ios/Utils/RJGzipUtils.m +89 -0
- package/ios/Utils/RJKeychainManager.h +48 -0
- package/ios/Utils/RJKeychainManager.m +111 -0
- package/ios/Utils/RJPerfTiming.h +209 -0
- package/ios/Utils/RJPerfTiming.m +264 -0
- package/ios/Utils/RJTelemetry.h +92 -0
- package/ios/Utils/RJTelemetry.m +320 -0
- package/ios/Utils/RJWindowUtils.h +66 -0
- package/ios/Utils/RJWindowUtils.m +133 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +79 -0
- package/lib/commonjs/index.js +1381 -0
- package/lib/commonjs/sdk/autoTracking.js +1259 -0
- package/lib/commonjs/sdk/constants.js +151 -0
- package/lib/commonjs/sdk/errorTracking.js +199 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +204 -0
- package/lib/commonjs/sdk/navigation.js +151 -0
- package/lib/commonjs/sdk/networkInterceptor.js +412 -0
- package/lib/commonjs/sdk/utils.js +363 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +72 -0
- package/lib/module/index.js +1284 -0
- package/lib/module/sdk/autoTracking.js +1233 -0
- package/lib/module/sdk/constants.js +145 -0
- package/lib/module/sdk/errorTracking.js +189 -0
- package/lib/module/sdk/index.js +12 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +143 -0
- package/lib/module/sdk/networkInterceptor.js +401 -0
- package/lib/module/sdk/utils.js +342 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +147 -0
- package/lib/typescript/components/Mask.d.ts +39 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +204 -0
- package/lib/typescript/sdk/constants.d.ts +120 -0
- package/lib/typescript/sdk/errorTracking.d.ts +32 -0
- package/lib/typescript/sdk/index.d.ts +9 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
- package/lib/typescript/sdk/navigation.d.ts +33 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
- package/lib/typescript/sdk/utils.d.ts +148 -0
- package/lib/typescript/types/index.d.ts +624 -0
- package/package.json +102 -0
- package/rejourney.podspec +21 -0
- package/src/NativeRejourney.ts +165 -0
- package/src/components/Mask.tsx +80 -0
- package/src/index.ts +1459 -0
- package/src/sdk/autoTracking.ts +1373 -0
- package/src/sdk/constants.ts +134 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/metricsTracking.ts +232 -0
- package/src/sdk/navigation.ts +157 -0
- package/src/sdk/networkInterceptor.ts +440 -0
- package/src/sdk/utils.ts +369 -0
- package/src/types/expo-router.d.ts +7 -0
- package/src/types/index.ts +739 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJCaptureHeuristics.m
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Capture heuristics implementation.
|
|
6
|
+
//
|
|
7
|
+
// Copyright (c) 2026 Rejourney
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
#import "RJCaptureHeuristics.h"
|
|
11
|
+
#import "../Core/RJLogger.h"
|
|
12
|
+
#import <QuartzCore/QuartzCore.h>
|
|
13
|
+
|
|
14
|
+
static const NSTimeInterval kRJCaptureGraceSeconds = 0.9;
|
|
15
|
+
static const NSTimeInterval kRJPollIntervalSeconds = 0.08;
|
|
16
|
+
static const NSTimeInterval kRJMaxStaleSeconds = 5.0;
|
|
17
|
+
|
|
18
|
+
static const NSTimeInterval kRJQuietTouchSeconds = 0.12;
|
|
19
|
+
static const NSTimeInterval kRJQuietScrollSeconds = 0.2;
|
|
20
|
+
static const NSTimeInterval kRJQuietBounceSeconds = 0.2;
|
|
21
|
+
static const NSTimeInterval kRJQuietRefreshSeconds = 0.22;
|
|
22
|
+
static const NSTimeInterval kRJQuietMapSeconds = 0.55;
|
|
23
|
+
static const NSTimeInterval kRJQuietTransitionSeconds = 0.2;
|
|
24
|
+
static const NSTimeInterval kRJQuietKeyboardSeconds = 0.25;
|
|
25
|
+
static const NSTimeInterval kRJQuietAnimationSeconds = 0.25;
|
|
26
|
+
|
|
27
|
+
static const NSTimeInterval kRJMapSettleSeconds = 0.8;
|
|
28
|
+
|
|
29
|
+
static const NSTimeInterval kRJSignatureChurnWindowSeconds = 0.25;
|
|
30
|
+
|
|
31
|
+
static const NSTimeInterval kRJBonusScrollDelaySeconds = 0.12;
|
|
32
|
+
static const NSTimeInterval kRJBonusMapDelaySeconds = 0.35;
|
|
33
|
+
static const NSTimeInterval kRJBonusRefreshDelaySeconds = 0.2;
|
|
34
|
+
static const NSTimeInterval kRJBonusInteractionDelaySeconds = 0.15;
|
|
35
|
+
static const NSTimeInterval kRJBonusTransitionDelaySeconds = 0.2;
|
|
36
|
+
static const NSTimeInterval kRJBonusKeyboardDelaySeconds = 0.2;
|
|
37
|
+
static const NSTimeInterval kRJBonusAnimationDelaySeconds = 0.2;
|
|
38
|
+
|
|
39
|
+
static const CGFloat kRJAnimationSmallAreaAllowed = 0.03;
|
|
40
|
+
|
|
41
|
+
static const NSTimeInterval kRJKeyframeSpacingSeconds = 0.25;
|
|
42
|
+
static const NSUInteger kRJMaxPendingKeyframes = 3;
|
|
43
|
+
|
|
44
|
+
static const CGFloat kRJScrollEpsilon = 0.5;
|
|
45
|
+
static const CGFloat kRJZoomEpsilon = 0.01;
|
|
46
|
+
static const CGFloat kRJInsetEpsilon = 0.5;
|
|
47
|
+
|
|
48
|
+
typedef struct {
|
|
49
|
+
double latitude;
|
|
50
|
+
double longitude;
|
|
51
|
+
} RJCoordinate;
|
|
52
|
+
|
|
53
|
+
typedef struct {
|
|
54
|
+
double latitudeDelta;
|
|
55
|
+
double longitudeDelta;
|
|
56
|
+
} RJCoordinateSpan;
|
|
57
|
+
|
|
58
|
+
@interface RJScrollViewSample : NSObject
|
|
59
|
+
|
|
60
|
+
@property(nonatomic, assign) CGPoint contentOffset;
|
|
61
|
+
@property(nonatomic, assign) UIEdgeInsets contentInset;
|
|
62
|
+
@property(nonatomic, assign) CGFloat zoomScale;
|
|
63
|
+
|
|
64
|
+
@end
|
|
65
|
+
|
|
66
|
+
@implementation RJScrollViewSample
|
|
67
|
+
@end
|
|
68
|
+
|
|
69
|
+
@interface RJCaptureHeuristics ()
|
|
70
|
+
|
|
71
|
+
@property(nonatomic, assign) NSTimeInterval lastTouchTime;
|
|
72
|
+
@property(nonatomic, assign) NSTimeInterval lastScrollTime;
|
|
73
|
+
@property(nonatomic, assign) NSTimeInterval lastBounceTime;
|
|
74
|
+
@property(nonatomic, assign) NSTimeInterval lastRefreshTime;
|
|
75
|
+
@property(nonatomic, assign) NSTimeInterval lastMapTime;
|
|
76
|
+
@property(nonatomic, assign) NSTimeInterval lastTransitionTime;
|
|
77
|
+
@property(nonatomic, assign) NSTimeInterval lastKeyboardTime;
|
|
78
|
+
@property(nonatomic, assign) NSTimeInterval lastAnimationTime;
|
|
79
|
+
@property(nonatomic, assign) NSTimeInterval mapSettleUntil;
|
|
80
|
+
|
|
81
|
+
@property(nonatomic, assign) NSTimeInterval lastRenderedTime;
|
|
82
|
+
@property(nonatomic, copy, nullable) NSString *lastRenderedSignature;
|
|
83
|
+
|
|
84
|
+
@property(nonatomic, assign, readwrite) BOOL keyboardAnimating;
|
|
85
|
+
@property(nonatomic, assign, readwrite) BOOL scrollActive;
|
|
86
|
+
@property(nonatomic, assign, readwrite) BOOL refreshActive;
|
|
87
|
+
@property(nonatomic, assign, readwrite) BOOL mapActive;
|
|
88
|
+
@property(nonatomic, assign, readwrite) BOOL animationBlocking;
|
|
89
|
+
@property(nonatomic, assign) CGFloat lastAnimationAreaRatio;
|
|
90
|
+
|
|
91
|
+
@property(nonatomic, copy, nullable) NSString *lastObservedSignature;
|
|
92
|
+
@property(nonatomic, assign) NSTimeInterval lastObservedSignatureTime;
|
|
93
|
+
@property(nonatomic, assign) NSUInteger signatureChurnCount;
|
|
94
|
+
@property(nonatomic, assign) NSTimeInterval lastSignatureChurnTime;
|
|
95
|
+
@property(nonatomic, assign) BOOL churnBlocking;
|
|
96
|
+
|
|
97
|
+
@property(nonatomic, assign) BOOL hasVideoSurface;
|
|
98
|
+
@property(nonatomic, assign) BOOL hasWebSurface;
|
|
99
|
+
@property(nonatomic, assign) BOOL hasCameraSurface;
|
|
100
|
+
|
|
101
|
+
@property(nonatomic, weak) UIViewController *lastTopVC;
|
|
102
|
+
|
|
103
|
+
@property(nonatomic, assign) NSTimeInterval bonusCaptureTime;
|
|
104
|
+
@property(nonatomic, assign) NSUInteger pendingKeyframes;
|
|
105
|
+
@property(nonatomic, assign) NSTimeInterval lastKeyframeRenderTime;
|
|
106
|
+
|
|
107
|
+
@property(nonatomic, strong)
|
|
108
|
+
NSMapTable<UIScrollView *, RJScrollViewSample *> *scrollSamples;
|
|
109
|
+
@property(nonatomic, strong) NSHashTable<UIView *> *animatedViews;
|
|
110
|
+
@property(nonatomic, strong) NSHashTable<UIView *> *mapViews;
|
|
111
|
+
@property(nonatomic, strong) NSMapTable<UIView *, NSString *> *mapStates;
|
|
112
|
+
|
|
113
|
+
@end
|
|
114
|
+
|
|
115
|
+
@implementation RJCaptureHeuristicsDecision
|
|
116
|
+
@end
|
|
117
|
+
|
|
118
|
+
@implementation RJCaptureHeuristics
|
|
119
|
+
|
|
120
|
+
- (instancetype)init {
|
|
121
|
+
self = [super init];
|
|
122
|
+
if (self) {
|
|
123
|
+
_captureGraceSeconds = kRJCaptureGraceSeconds;
|
|
124
|
+
_pollIntervalSeconds = kRJPollIntervalSeconds;
|
|
125
|
+
_maxStaleSeconds = kRJMaxStaleSeconds;
|
|
126
|
+
_scrollSamples = [NSMapTable weakToStrongObjectsMapTable];
|
|
127
|
+
_animatedViews = [NSHashTable weakObjectsHashTable];
|
|
128
|
+
_mapViews = [NSHashTable weakObjectsHashTable];
|
|
129
|
+
_mapStates = [NSMapTable weakToStrongObjectsMapTable];
|
|
130
|
+
|
|
131
|
+
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
|
132
|
+
[center addObserver:self
|
|
133
|
+
selector:@selector(handleKeyboardWillChange:)
|
|
134
|
+
name:UIKeyboardWillShowNotification
|
|
135
|
+
object:nil];
|
|
136
|
+
[center addObserver:self
|
|
137
|
+
selector:@selector(handleKeyboardWillChange:)
|
|
138
|
+
name:UIKeyboardWillHideNotification
|
|
139
|
+
object:nil];
|
|
140
|
+
[center addObserver:self
|
|
141
|
+
selector:@selector(handleKeyboardWillChange:)
|
|
142
|
+
name:UIKeyboardWillChangeFrameNotification
|
|
143
|
+
object:nil];
|
|
144
|
+
[center addObserver:self
|
|
145
|
+
selector:@selector(handleKeyboardDidChange:)
|
|
146
|
+
name:UIKeyboardDidShowNotification
|
|
147
|
+
object:nil];
|
|
148
|
+
[center addObserver:self
|
|
149
|
+
selector:@selector(handleKeyboardDidChange:)
|
|
150
|
+
name:UIKeyboardDidHideNotification
|
|
151
|
+
object:nil];
|
|
152
|
+
[center addObserver:self
|
|
153
|
+
selector:@selector(handleKeyboardDidChange:)
|
|
154
|
+
name:UIKeyboardDidChangeFrameNotification
|
|
155
|
+
object:nil];
|
|
156
|
+
}
|
|
157
|
+
return self;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
- (void)dealloc {
|
|
161
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
- (void)reset {
|
|
165
|
+
self.lastTouchTime = 0;
|
|
166
|
+
self.lastScrollTime = 0;
|
|
167
|
+
self.lastBounceTime = 0;
|
|
168
|
+
self.lastRefreshTime = 0;
|
|
169
|
+
self.lastMapTime = 0;
|
|
170
|
+
self.lastTransitionTime = 0;
|
|
171
|
+
self.lastKeyboardTime = 0;
|
|
172
|
+
self.lastAnimationTime = 0;
|
|
173
|
+
self.mapSettleUntil = 0;
|
|
174
|
+
self.lastRenderedTime = 0;
|
|
175
|
+
self.lastRenderedSignature = nil;
|
|
176
|
+
self.lastObservedSignature = nil;
|
|
177
|
+
self.lastObservedSignatureTime = 0;
|
|
178
|
+
self.signatureChurnCount = 0;
|
|
179
|
+
self.lastSignatureChurnTime = 0;
|
|
180
|
+
self.churnBlocking = NO;
|
|
181
|
+
self.keyboardAnimating = NO;
|
|
182
|
+
self.scrollActive = NO;
|
|
183
|
+
self.refreshActive = NO;
|
|
184
|
+
self.mapActive = NO;
|
|
185
|
+
self.animationBlocking = NO;
|
|
186
|
+
self.lastAnimationAreaRatio = 0.0;
|
|
187
|
+
self.hasVideoSurface = NO;
|
|
188
|
+
self.hasWebSurface = NO;
|
|
189
|
+
self.hasCameraSurface = NO;
|
|
190
|
+
self.bonusCaptureTime = 0;
|
|
191
|
+
self.pendingKeyframes = 0;
|
|
192
|
+
self.lastKeyframeRenderTime = 0;
|
|
193
|
+
self.lastTopVC = nil;
|
|
194
|
+
[self.scrollSamples removeAllObjects];
|
|
195
|
+
[self.animatedViews removeAllObjects];
|
|
196
|
+
[self.mapViews removeAllObjects];
|
|
197
|
+
[self.mapStates removeAllObjects];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
- (void)invalidateSignature {
|
|
201
|
+
self.lastRenderedSignature = nil;
|
|
202
|
+
self.lastRenderedTime = 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
- (void)recordTouchEventAtTime:(NSTimeInterval)time {
|
|
206
|
+
self.lastTouchTime = time;
|
|
207
|
+
[self scheduleBonusCaptureAfterDelay:kRJBonusInteractionDelaySeconds now:time];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
- (void)recordInteractionEventAtTime:(NSTimeInterval)time {
|
|
211
|
+
[self recordTouchEventAtTime:time];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
- (void)recordMapInteractionAtTime:(NSTimeInterval)time {
|
|
215
|
+
self.lastMapTime = time;
|
|
216
|
+
NSTimeInterval candidate = time + kRJMapSettleSeconds;
|
|
217
|
+
if (candidate > self.mapSettleUntil) {
|
|
218
|
+
self.mapSettleUntil = candidate;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
- (void)recordNavigationEventAtTime:(NSTimeInterval)time {
|
|
223
|
+
self.lastTransitionTime = time;
|
|
224
|
+
[self scheduleBonusCaptureAfterDelay:kRJBonusTransitionDelaySeconds
|
|
225
|
+
now:time];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
- (void)recordRenderedSignature:(nullable NSString *)signature
|
|
229
|
+
atTime:(NSTimeInterval)time {
|
|
230
|
+
self.lastRenderedSignature = signature.length > 0 ? signature : nil;
|
|
231
|
+
self.lastRenderedTime = time;
|
|
232
|
+
if (self.pendingKeyframes > 0) {
|
|
233
|
+
self.pendingKeyframes -= 1;
|
|
234
|
+
self.lastKeyframeRenderTime = time;
|
|
235
|
+
if (self.pendingKeyframes > 0) {
|
|
236
|
+
self.bonusCaptureTime = time + kRJKeyframeSpacingSeconds;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
self.bonusCaptureTime = 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
- (void)updateWithScanResult:(RJViewHierarchyScanResult *)scanResult
|
|
244
|
+
window:(UIWindow *)window
|
|
245
|
+
now:(NSTimeInterval)now {
|
|
246
|
+
if (!scanResult) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
[self updateTouchStateAtTime:now];
|
|
251
|
+
[self updateTransitionStateForWindow:window atTime:now];
|
|
252
|
+
|
|
253
|
+
NSString *currentSignature = scanResult.layoutSignature ?: @"";
|
|
254
|
+
NSString *lastSignature = self.lastObservedSignature ?: @"";
|
|
255
|
+
BOOL signatureChanged = ![currentSignature isEqualToString:lastSignature];
|
|
256
|
+
if (signatureChanged) {
|
|
257
|
+
NSTimeInterval delta = now - self.lastObservedSignatureTime;
|
|
258
|
+
if (delta < kRJSignatureChurnWindowSeconds) {
|
|
259
|
+
self.signatureChurnCount += 1;
|
|
260
|
+
} else {
|
|
261
|
+
self.signatureChurnCount = 1;
|
|
262
|
+
}
|
|
263
|
+
self.lastObservedSignatureTime = now;
|
|
264
|
+
self.lastSignatureChurnTime = now;
|
|
265
|
+
self.lastObservedSignature = currentSignature.length > 0 ? currentSignature
|
|
266
|
+
: nil;
|
|
267
|
+
} else if (self.lastSignatureChurnTime > 0 &&
|
|
268
|
+
(now - self.lastSignatureChurnTime) >
|
|
269
|
+
kRJSignatureChurnWindowSeconds) {
|
|
270
|
+
self.signatureChurnCount = 0;
|
|
271
|
+
}
|
|
272
|
+
self.churnBlocking =
|
|
273
|
+
(self.signatureChurnCount >= 2 &&
|
|
274
|
+
(now - self.lastSignatureChurnTime) < kRJSignatureChurnWindowSeconds);
|
|
275
|
+
|
|
276
|
+
self.hasVideoSurface = (scanResult.videoFrames.count > 0);
|
|
277
|
+
self.hasWebSurface = (scanResult.webViewFrames.count > 0);
|
|
278
|
+
self.hasCameraSurface = (scanResult.cameraFrames.count > 0);
|
|
279
|
+
|
|
280
|
+
if (scanResult.scrollActive) {
|
|
281
|
+
// Active scroll is a strong blocker: rendering during drag or deceleration
|
|
282
|
+
// causes visible hitching.
|
|
283
|
+
self.lastScrollTime = now;
|
|
284
|
+
}
|
|
285
|
+
if (scanResult.bounceActive) {
|
|
286
|
+
// Rubber-band bounce and inset settling stutter easily, so delay capture.
|
|
287
|
+
self.lastBounceTime = now;
|
|
288
|
+
}
|
|
289
|
+
if (scanResult.refreshActive) {
|
|
290
|
+
// Pull-to-refresh animations and insets can hitch; wait for settle.
|
|
291
|
+
self.lastRefreshTime = now;
|
|
292
|
+
}
|
|
293
|
+
if (scanResult.mapActive) {
|
|
294
|
+
// Map camera or tile motion is expensive; never capture mid-motion.
|
|
295
|
+
[self recordMapInteractionAtTime:now];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
[self updateScrollActiveState:scanResult.scrollActive
|
|
299
|
+
refreshActive:scanResult.refreshActive
|
|
300
|
+
mapActive:scanResult.mapActive
|
|
301
|
+
now:now];
|
|
302
|
+
|
|
303
|
+
BOOL blockingAnimation = NO;
|
|
304
|
+
if (scanResult.hasAnyAnimations) {
|
|
305
|
+
self.lastAnimationAreaRatio = scanResult.animationAreaRatio;
|
|
306
|
+
blockingAnimation = (scanResult.animationAreaRatio >=
|
|
307
|
+
kRJAnimationSmallAreaAllowed);
|
|
308
|
+
} else {
|
|
309
|
+
self.lastAnimationAreaRatio = 0.0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
BOOL recentSignatureChange =
|
|
313
|
+
signatureChanged ||
|
|
314
|
+
(self.signatureChurnCount > 0 &&
|
|
315
|
+
(now - self.lastSignatureChurnTime) < kRJSignatureChurnWindowSeconds);
|
|
316
|
+
BOOL bailoutBlocking = (scanResult.didBailOutEarly && recentSignatureChange);
|
|
317
|
+
BOOL shouldBlock = blockingAnimation || self.churnBlocking || bailoutBlocking;
|
|
318
|
+
self.animationBlocking = shouldBlock;
|
|
319
|
+
if (shouldBlock) {
|
|
320
|
+
self.lastAnimationTime = now;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
[self updateTrackedScrollViews:scanResult.scrollViewPointers now:now];
|
|
324
|
+
[self updateTrackedMapViews:scanResult.mapViewPointers now:now];
|
|
325
|
+
[self updateTrackedAnimatedViews:scanResult.animatedViewPointers];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
- (void)updateWithStabilityProbeForWindow:(UIWindow *)window
|
|
329
|
+
now:(NSTimeInterval)now {
|
|
330
|
+
[self updateTouchStateAtTime:now];
|
|
331
|
+
[self updateTransitionStateForWindow:window atTime:now];
|
|
332
|
+
|
|
333
|
+
[self probeScrollViewsAtTime:now];
|
|
334
|
+
[self probeMapViewsAtTime:now];
|
|
335
|
+
[self probeAnimatedViewsAtTime:now];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
- (RJCaptureHeuristicsDecision *)decisionForSignature:
|
|
339
|
+
(nullable NSString *)signature
|
|
340
|
+
now:(NSTimeInterval)now
|
|
341
|
+
hasLastFrame:(BOOL)hasLastFrame {
|
|
342
|
+
RJCaptureHeuristicsDecision *decision =
|
|
343
|
+
[[RJCaptureHeuristicsDecision alloc] init];
|
|
344
|
+
|
|
345
|
+
NSTimeInterval earliestSafeTime = now;
|
|
346
|
+
RJCaptureHeuristicsReason blockerReason = RJCaptureHeuristicsReasonRenderNow;
|
|
347
|
+
|
|
348
|
+
// Active touch/gesture input should be avoided to keep input latency smooth.
|
|
349
|
+
[self considerBlockerSince:self.lastTouchTime
|
|
350
|
+
quietInterval:kRJQuietTouchSeconds
|
|
351
|
+
now:now
|
|
352
|
+
reason:RJCaptureHeuristicsReasonDeferTouch
|
|
353
|
+
earliestTime:&earliestSafeTime
|
|
354
|
+
chosenReason:&blockerReason];
|
|
355
|
+
|
|
356
|
+
// Scroll motion (dragging or deceleration) is a high-jank period.
|
|
357
|
+
[self considerBlockerSince:self.lastScrollTime
|
|
358
|
+
quietInterval:kRJQuietScrollSeconds
|
|
359
|
+
now:now
|
|
360
|
+
reason:RJCaptureHeuristicsReasonDeferScroll
|
|
361
|
+
earliestTime:&earliestSafeTime
|
|
362
|
+
chosenReason:&blockerReason];
|
|
363
|
+
|
|
364
|
+
// Rubber-band bounce and inset animations are visually sensitive.
|
|
365
|
+
[self considerBlockerSince:self.lastBounceTime
|
|
366
|
+
quietInterval:kRJQuietBounceSeconds
|
|
367
|
+
now:now
|
|
368
|
+
reason:RJCaptureHeuristicsReasonDeferBounce
|
|
369
|
+
earliestTime:&earliestSafeTime
|
|
370
|
+
chosenReason:&blockerReason];
|
|
371
|
+
|
|
372
|
+
// Pull-to-refresh animations should finish before rendering.
|
|
373
|
+
[self considerBlockerSince:self.lastRefreshTime
|
|
374
|
+
quietInterval:kRJQuietRefreshSeconds
|
|
375
|
+
now:now
|
|
376
|
+
reason:RJCaptureHeuristicsReasonDeferRefresh
|
|
377
|
+
earliestTime:&earliestSafeTime
|
|
378
|
+
chosenReason:&blockerReason];
|
|
379
|
+
|
|
380
|
+
// Interactive transitions (swipe back, drag-to-dismiss) are hitch-sensitive.
|
|
381
|
+
[self considerBlockerSince:self.lastTransitionTime
|
|
382
|
+
quietInterval:kRJQuietTransitionSeconds
|
|
383
|
+
now:now
|
|
384
|
+
reason:RJCaptureHeuristicsReasonDeferTransition
|
|
385
|
+
earliestTime:&earliestSafeTime
|
|
386
|
+
chosenReason:&blockerReason];
|
|
387
|
+
|
|
388
|
+
// Keyboard frame animations can stutter; wait for settle.
|
|
389
|
+
if (self.keyboardAnimating) {
|
|
390
|
+
self.lastKeyboardTime = now;
|
|
391
|
+
}
|
|
392
|
+
[self considerBlockerSince:self.lastKeyboardTime
|
|
393
|
+
quietInterval:kRJQuietKeyboardSeconds
|
|
394
|
+
now:now
|
|
395
|
+
reason:RJCaptureHeuristicsReasonDeferKeyboard
|
|
396
|
+
earliestTime:&earliestSafeTime
|
|
397
|
+
chosenReason:&blockerReason];
|
|
398
|
+
|
|
399
|
+
// Map camera or tile motion is visually obvious; avoid rendering mid-flight.
|
|
400
|
+
[self considerBlockerSince:self.lastMapTime
|
|
401
|
+
quietInterval:kRJQuietMapSeconds
|
|
402
|
+
now:now
|
|
403
|
+
reason:RJCaptureHeuristicsReasonDeferMap
|
|
404
|
+
earliestTime:&earliestSafeTime
|
|
405
|
+
chosenReason:&blockerReason];
|
|
406
|
+
|
|
407
|
+
if (self.mapSettleUntil > now && self.mapSettleUntil > earliestSafeTime) {
|
|
408
|
+
earliestSafeTime = self.mapSettleUntil;
|
|
409
|
+
blockerReason = RJCaptureHeuristicsReasonDeferMap;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Large-area animations (Lottie, shimmer, etc.) are very noticeable.
|
|
413
|
+
if (self.animationBlocking) {
|
|
414
|
+
[self considerBlockerSince:self.lastAnimationTime
|
|
415
|
+
quietInterval:kRJQuietAnimationSeconds
|
|
416
|
+
now:now
|
|
417
|
+
reason:RJCaptureHeuristicsReasonDeferBigAnimation
|
|
418
|
+
earliestTime:&earliestSafeTime
|
|
419
|
+
chosenReason:&blockerReason];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (earliestSafeTime > now) {
|
|
423
|
+
decision.action = RJCaptureHeuristicsActionDefer;
|
|
424
|
+
decision.reason = blockerReason;
|
|
425
|
+
decision.deferUntil = earliestSafeTime;
|
|
426
|
+
return decision;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
BOOL signatureChanged = (signature.length == 0 ||
|
|
430
|
+
![signature isEqualToString:self.lastRenderedSignature]);
|
|
431
|
+
BOOL stale = (self.lastRenderedTime <= 0 ||
|
|
432
|
+
(now - self.lastRenderedTime) > self.maxStaleSeconds);
|
|
433
|
+
BOOL bonusDue = (self.bonusCaptureTime > 0 && now >= self.bonusCaptureTime);
|
|
434
|
+
BOOL keyframeDue = bonusDue && self.pendingKeyframes > 0 &&
|
|
435
|
+
(now - self.lastKeyframeRenderTime) >=
|
|
436
|
+
kRJKeyframeSpacingSeconds;
|
|
437
|
+
BOOL staleOnly = stale && hasLastFrame && !signatureChanged && !keyframeDue;
|
|
438
|
+
BOOL suppressStaleRender =
|
|
439
|
+
staleOnly &&
|
|
440
|
+
(self.hasVideoSurface || self.hasWebSurface || self.hasCameraSurface);
|
|
441
|
+
|
|
442
|
+
if (suppressStaleRender) {
|
|
443
|
+
decision.action = RJCaptureHeuristicsActionReuseLast;
|
|
444
|
+
decision.reason = RJCaptureHeuristicsReasonReuseSignatureUnchanged;
|
|
445
|
+
return decision;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!hasLastFrame || signatureChanged || stale || keyframeDue) {
|
|
449
|
+
decision.action = RJCaptureHeuristicsActionRenderNow;
|
|
450
|
+
decision.reason = RJCaptureHeuristicsReasonRenderNow;
|
|
451
|
+
return decision;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
decision.action = RJCaptureHeuristicsActionReuseLast;
|
|
455
|
+
decision.reason = RJCaptureHeuristicsReasonReuseSignatureUnchanged;
|
|
456
|
+
return decision;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
+ (NSString *)stringForReason:(RJCaptureHeuristicsReason)reason {
|
|
460
|
+
switch (reason) {
|
|
461
|
+
case RJCaptureHeuristicsReasonRenderNow:
|
|
462
|
+
return @"RENDER_NOW";
|
|
463
|
+
case RJCaptureHeuristicsReasonDeferTouch:
|
|
464
|
+
return @"DEFER_TOUCH";
|
|
465
|
+
case RJCaptureHeuristicsReasonDeferScroll:
|
|
466
|
+
return @"DEFER_SCROLL";
|
|
467
|
+
case RJCaptureHeuristicsReasonDeferBounce:
|
|
468
|
+
return @"DEFER_BOUNCE";
|
|
469
|
+
case RJCaptureHeuristicsReasonDeferRefresh:
|
|
470
|
+
return @"DEFER_REFRESH";
|
|
471
|
+
case RJCaptureHeuristicsReasonDeferTransition:
|
|
472
|
+
return @"DEFER_TRANSITION";
|
|
473
|
+
case RJCaptureHeuristicsReasonDeferKeyboard:
|
|
474
|
+
return @"DEFER_KEYBOARD";
|
|
475
|
+
case RJCaptureHeuristicsReasonDeferMap:
|
|
476
|
+
return @"DEFER_MAP";
|
|
477
|
+
case RJCaptureHeuristicsReasonDeferBigAnimation:
|
|
478
|
+
return @"DEFER_BIG_ANIMATION";
|
|
479
|
+
case RJCaptureHeuristicsReasonReuseSignatureUnchanged:
|
|
480
|
+
return @"REUSE_SIGNATURE_UNCHANGED";
|
|
481
|
+
case RJCaptureHeuristicsReasonDeadlineExpired:
|
|
482
|
+
return @"REUSE_DEADLINE_EXPIRED";
|
|
483
|
+
case RJCaptureHeuristicsReasonRenderFailedReuse:
|
|
484
|
+
return @"RENDER_FAILED_REUSE";
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return @"";
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#pragma mark - Keyboard Tracking
|
|
491
|
+
|
|
492
|
+
- (void)handleKeyboardWillChange:(NSNotification *)notification {
|
|
493
|
+
self.keyboardAnimating = YES;
|
|
494
|
+
self.lastKeyboardTime = CACurrentMediaTime();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
- (void)handleKeyboardDidChange:(NSNotification *)notification {
|
|
498
|
+
self.keyboardAnimating = NO;
|
|
499
|
+
NSTimeInterval now = CACurrentMediaTime();
|
|
500
|
+
self.lastKeyboardTime = now;
|
|
501
|
+
[self scheduleBonusCaptureAfterDelay:kRJBonusKeyboardDelaySeconds now:now];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#pragma mark - Touch/Transition Tracking
|
|
505
|
+
|
|
506
|
+
- (void)updateTouchStateAtTime:(NSTimeInterval)now {
|
|
507
|
+
NSString *mode = [[NSRunLoop mainRunLoop] currentMode];
|
|
508
|
+
if ([mode isEqualToString:UITrackingRunLoopMode]) {
|
|
509
|
+
self.lastTouchTime = now;
|
|
510
|
+
self.lastScrollTime = now;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
- (void)updateTransitionStateForWindow:(UIWindow *)window
|
|
515
|
+
atTime:(NSTimeInterval)now {
|
|
516
|
+
UIViewController *topVC = [self topViewControllerForWindow:window];
|
|
517
|
+
if (topVC != self.lastTopVC) {
|
|
518
|
+
self.lastTransitionTime = now;
|
|
519
|
+
[self scheduleBonusCaptureAfterDelay:kRJBonusTransitionDelaySeconds now:now];
|
|
520
|
+
self.lastTopVC = topVC;
|
|
521
|
+
}
|
|
522
|
+
id<UIViewControllerTransitionCoordinator> coordinator =
|
|
523
|
+
topVC.transitionCoordinator;
|
|
524
|
+
if (coordinator && (coordinator.isInteractive || coordinator.isAnimated)) {
|
|
525
|
+
self.lastTransitionTime = now;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
- (UIViewController *)topViewControllerForWindow:(UIWindow *)window {
|
|
530
|
+
UIViewController *root = window.rootViewController;
|
|
531
|
+
if (!root) {
|
|
532
|
+
return nil;
|
|
533
|
+
}
|
|
534
|
+
return [self topViewControllerFrom:root];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
- (UIViewController *)topViewControllerFrom:(UIViewController *)viewController {
|
|
538
|
+
if (viewController.presentedViewController) {
|
|
539
|
+
return [self topViewControllerFrom:viewController.presentedViewController];
|
|
540
|
+
}
|
|
541
|
+
if ([viewController isKindOfClass:[UINavigationController class]]) {
|
|
542
|
+
UINavigationController *nav = (UINavigationController *)viewController;
|
|
543
|
+
return [self topViewControllerFrom:nav.visibleViewController];
|
|
544
|
+
}
|
|
545
|
+
if ([viewController isKindOfClass:[UITabBarController class]]) {
|
|
546
|
+
UITabBarController *tab = (UITabBarController *)viewController;
|
|
547
|
+
return [self topViewControllerFrom:tab.selectedViewController];
|
|
548
|
+
}
|
|
549
|
+
return viewController;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
#pragma mark - Scroll Tracking
|
|
553
|
+
|
|
554
|
+
- (void)updateTrackedScrollViews:(NSArray<NSValue *> *)scrollViewPointers
|
|
555
|
+
now:(NSTimeInterval)now {
|
|
556
|
+
if (!scrollViewPointers) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
for (NSValue *pointer in scrollViewPointers) {
|
|
561
|
+
UIScrollView *scrollView = [pointer nonretainedObjectValue];
|
|
562
|
+
if (![scrollView isKindOfClass:[UIScrollView class]]) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
[self evaluateScrollView:scrollView
|
|
566
|
+
now:now
|
|
567
|
+
scrollActive:NULL
|
|
568
|
+
refreshActive:NULL];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
- (void)probeScrollViewsAtTime:(NSTimeInterval)now {
|
|
574
|
+
BOOL anyScrollActive = NO;
|
|
575
|
+
BOOL anyRefreshActive = NO;
|
|
576
|
+
|
|
577
|
+
NSArray<UIScrollView *> *scrollViews = [[self.scrollSamples keyEnumerator] allObjects];
|
|
578
|
+
if (!scrollViews) {
|
|
579
|
+
scrollViews = @[];
|
|
580
|
+
}
|
|
581
|
+
for (UIScrollView *scrollView in scrollViews) {
|
|
582
|
+
if (![scrollView isKindOfClass:[UIScrollView class]]) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
[self evaluateScrollView:scrollView
|
|
586
|
+
now:now
|
|
587
|
+
scrollActive:&anyScrollActive
|
|
588
|
+
refreshActive:&anyRefreshActive];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
[self updateScrollActiveState:anyScrollActive
|
|
592
|
+
refreshActive:anyRefreshActive
|
|
593
|
+
mapActive:self.mapActive
|
|
594
|
+
now:now];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
- (void)evaluateScrollView:(UIScrollView *)scrollView
|
|
598
|
+
now:(NSTimeInterval)now
|
|
599
|
+
scrollActive:(BOOL *)scrollActive
|
|
600
|
+
refreshActive:(BOOL *)refreshActive {
|
|
601
|
+
RJScrollViewSample *sample = [self.scrollSamples objectForKey:scrollView];
|
|
602
|
+
if (!sample) {
|
|
603
|
+
sample = [[RJScrollViewSample alloc] init];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
CGPoint offset = scrollView.contentOffset;
|
|
607
|
+
UIEdgeInsets inset = scrollView.contentInset;
|
|
608
|
+
CGFloat zoomScale = scrollView.zoomScale;
|
|
609
|
+
|
|
610
|
+
BOOL tracking = scrollView.isTracking || scrollView.isDragging ||
|
|
611
|
+
scrollView.isDecelerating;
|
|
612
|
+
BOOL offsetMoved = (fabs(offset.x - sample.contentOffset.x) > kRJScrollEpsilon ||
|
|
613
|
+
fabs(offset.y - sample.contentOffset.y) > kRJScrollEpsilon);
|
|
614
|
+
BOOL zoomMoved = fabs(zoomScale - sample.zoomScale) > kRJZoomEpsilon;
|
|
615
|
+
BOOL isScrolling = tracking || offsetMoved || zoomMoved;
|
|
616
|
+
|
|
617
|
+
if (isScrolling) {
|
|
618
|
+
// Scroll movement, including momentum/deceleration.
|
|
619
|
+
self.lastScrollTime = now;
|
|
620
|
+
if (scrollActive) {
|
|
621
|
+
*scrollActive = YES;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
BOOL insetChanged =
|
|
626
|
+
(fabs(inset.top - sample.contentInset.top) > kRJInsetEpsilon ||
|
|
627
|
+
fabs(inset.bottom - sample.contentInset.bottom) > kRJInsetEpsilon ||
|
|
628
|
+
fabs(inset.left - sample.contentInset.left) > kRJInsetEpsilon ||
|
|
629
|
+
fabs(inset.right - sample.contentInset.right) > kRJInsetEpsilon);
|
|
630
|
+
|
|
631
|
+
BOOL isOverscrolled = [self isOverscrolling:scrollView offset:offset];
|
|
632
|
+
if (isOverscrolled || insetChanged) {
|
|
633
|
+
// Rubber-band bounce or inset settling.
|
|
634
|
+
self.lastBounceTime = now;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
BOOL refreshVisible = [self isRefreshActiveForScrollView:scrollView
|
|
638
|
+
offset:offset
|
|
639
|
+
inset:inset];
|
|
640
|
+
if (refreshVisible) {
|
|
641
|
+
// Pull-to-refresh is active or settling.
|
|
642
|
+
self.lastRefreshTime = now;
|
|
643
|
+
if (refreshActive) {
|
|
644
|
+
*refreshActive = YES;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
sample.contentOffset = offset;
|
|
649
|
+
sample.contentInset = inset;
|
|
650
|
+
sample.zoomScale = zoomScale;
|
|
651
|
+
[self.scrollSamples setObject:sample forKey:scrollView];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
- (BOOL)isOverscrolling:(UIScrollView *)scrollView offset:(CGPoint)offset {
|
|
655
|
+
UIEdgeInsets inset = UIEdgeInsetsZero;
|
|
656
|
+
@try {
|
|
657
|
+
inset = scrollView.adjustedContentInset;
|
|
658
|
+
} @catch (NSException *exception) {
|
|
659
|
+
inset = scrollView.contentInset;
|
|
660
|
+
}
|
|
661
|
+
CGFloat topLimit = -inset.top - kRJScrollEpsilon;
|
|
662
|
+
CGFloat bottomLimit = scrollView.contentSize.height -
|
|
663
|
+
scrollView.bounds.size.height +
|
|
664
|
+
inset.bottom + kRJScrollEpsilon;
|
|
665
|
+
|
|
666
|
+
if (offset.y < topLimit || offset.y > bottomLimit) {
|
|
667
|
+
return YES;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
CGFloat leftLimit = -inset.left - kRJScrollEpsilon;
|
|
671
|
+
CGFloat rightLimit = scrollView.contentSize.width -
|
|
672
|
+
scrollView.bounds.size.width +
|
|
673
|
+
inset.right + kRJScrollEpsilon;
|
|
674
|
+
if (offset.x < leftLimit || offset.x > rightLimit) {
|
|
675
|
+
return YES;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return NO;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
- (BOOL)isRefreshActiveForScrollView:(UIScrollView *)scrollView
|
|
682
|
+
offset:(CGPoint)offset
|
|
683
|
+
inset:(UIEdgeInsets)inset {
|
|
684
|
+
UIRefreshControl *refreshControl = scrollView.refreshControl;
|
|
685
|
+
if (!refreshControl) {
|
|
686
|
+
return NO;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (refreshControl.isRefreshing) {
|
|
690
|
+
return YES;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
CGFloat triggerOffset = -scrollView.adjustedContentInset.top -
|
|
694
|
+
kRJScrollEpsilon;
|
|
695
|
+
if (offset.y < triggerOffset) {
|
|
696
|
+
return YES;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
CGRect refreshFrame = refreshControl.frame;
|
|
700
|
+
if (refreshControl.superview) {
|
|
701
|
+
CGRect inScroll = [refreshControl.superview convertRect:refreshFrame
|
|
702
|
+
toView:scrollView];
|
|
703
|
+
if (CGRectIntersectsRect(inScroll, scrollView.bounds)) {
|
|
704
|
+
return YES;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return NO;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
- (void)updateScrollActiveState:(BOOL)scrollActive
|
|
712
|
+
refreshActive:(BOOL)refreshActive
|
|
713
|
+
mapActive:(BOOL)mapActive
|
|
714
|
+
now:(NSTimeInterval)now {
|
|
715
|
+
if (self.scrollActive && !scrollActive) {
|
|
716
|
+
[self scheduleBonusCaptureAfterDelay:kRJBonusScrollDelaySeconds now:now];
|
|
717
|
+
}
|
|
718
|
+
if (self.refreshActive && !refreshActive) {
|
|
719
|
+
[self scheduleBonusCaptureAfterDelay:kRJBonusRefreshDelaySeconds now:now];
|
|
720
|
+
}
|
|
721
|
+
if (self.mapActive && !mapActive) {
|
|
722
|
+
[self scheduleBonusCaptureAfterDelay:kRJBonusMapDelaySeconds now:now];
|
|
723
|
+
NSTimeInterval candidate = now + kRJMapSettleSeconds;
|
|
724
|
+
if (candidate > self.mapSettleUntil) {
|
|
725
|
+
self.mapSettleUntil = candidate;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
self.scrollActive = scrollActive;
|
|
730
|
+
self.refreshActive = refreshActive;
|
|
731
|
+
self.mapActive = mapActive;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
#pragma mark - Map Tracking
|
|
735
|
+
|
|
736
|
+
- (void)updateTrackedMapViews:(NSArray<NSValue *> *)mapViewPointers
|
|
737
|
+
now:(NSTimeInterval)now {
|
|
738
|
+
if (!mapViewPointers) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
for (NSValue *pointer in mapViewPointers) {
|
|
743
|
+
UIView *view = [pointer nonretainedObjectValue];
|
|
744
|
+
if (![view isKindOfClass:[UIView class]]) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
[self.mapViews addObject:view];
|
|
748
|
+
[self updateMapStateForView:view atTime:now];
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
- (void)probeMapViewsAtTime:(NSTimeInterval)now {
|
|
753
|
+
BOOL anyMapActive = NO;
|
|
754
|
+
for (UIView *view in self.mapViews) {
|
|
755
|
+
if (![view isKindOfClass:[UIView class]]) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if ([self updateMapStateForView:view atTime:now]) {
|
|
759
|
+
anyMapActive = YES;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (anyMapActive) {
|
|
763
|
+
self.lastMapTime = now;
|
|
764
|
+
}
|
|
765
|
+
[self updateScrollActiveState:self.scrollActive
|
|
766
|
+
refreshActive:self.refreshActive
|
|
767
|
+
mapActive:anyMapActive || self.mapActive
|
|
768
|
+
now:now];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
- (BOOL)updateMapStateForView:(UIView *)view atTime:(NSTimeInterval)now {
|
|
772
|
+
NSString *signature = [self mapSignatureForView:view];
|
|
773
|
+
if (signature.length == 0) {
|
|
774
|
+
return NO;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
NSString *previous = [self.mapStates objectForKey:view];
|
|
778
|
+
[self.mapStates setObject:signature forKey:view];
|
|
779
|
+
if (!previous) {
|
|
780
|
+
return NO;
|
|
781
|
+
}
|
|
782
|
+
if (![previous isEqualToString:signature]) {
|
|
783
|
+
[self recordMapInteractionAtTime:now];
|
|
784
|
+
return YES;
|
|
785
|
+
}
|
|
786
|
+
return NO;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
- (NSString *)mapSignatureForView:(UIView *)view {
|
|
790
|
+
@try {
|
|
791
|
+
NSValue *centerValue = [view valueForKey:@"centerCoordinate"];
|
|
792
|
+
NSValue *spanValue = [view valueForKeyPath:@"region.span"];
|
|
793
|
+
|
|
794
|
+
RJCoordinate center = {0, 0};
|
|
795
|
+
RJCoordinateSpan span = {0, 0};
|
|
796
|
+
if ([centerValue isKindOfClass:[NSValue class]]) {
|
|
797
|
+
[centerValue getValue:¢er];
|
|
798
|
+
}
|
|
799
|
+
if ([spanValue isKindOfClass:[NSValue class]]) {
|
|
800
|
+
[spanValue getValue:&span];
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
NSNumber *altitude = [view valueForKeyPath:@"camera.altitude"];
|
|
804
|
+
NSNumber *heading = [view valueForKeyPath:@"camera.heading"];
|
|
805
|
+
NSNumber *pitch = [view valueForKeyPath:@"camera.pitch"];
|
|
806
|
+
|
|
807
|
+
double altitudeValue = altitude ? altitude.doubleValue : 0;
|
|
808
|
+
double headingValue = heading ? heading.doubleValue : 0;
|
|
809
|
+
double pitchValue = pitch ? pitch.doubleValue : 0;
|
|
810
|
+
|
|
811
|
+
return [NSString stringWithFormat:@"%.5f:%.5f:%.5f:%.5f:%.1f:%.1f:%.1f",
|
|
812
|
+
center.latitude, center.longitude,
|
|
813
|
+
span.latitudeDelta, span.longitudeDelta,
|
|
814
|
+
altitudeValue, headingValue, pitchValue];
|
|
815
|
+
} @catch (NSException *exception) {
|
|
816
|
+
return @"";
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
#pragma mark - Animation Tracking
|
|
821
|
+
|
|
822
|
+
- (void)updateTrackedAnimatedViews:(NSArray<NSValue *> *)animatedViewPointers {
|
|
823
|
+
if (!animatedViewPointers) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
[self.animatedViews removeAllObjects];
|
|
828
|
+
for (NSValue *pointer in animatedViewPointers) {
|
|
829
|
+
UIView *view = [pointer nonretainedObjectValue];
|
|
830
|
+
if (![view isKindOfClass:[UIView class]]) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
[self.animatedViews addObject:view];
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
- (void)probeAnimatedViewsAtTime:(NSTimeInterval)now {
|
|
838
|
+
if (self.churnBlocking) {
|
|
839
|
+
if ((now - self.lastSignatureChurnTime) <
|
|
840
|
+
kRJSignatureChurnWindowSeconds) {
|
|
841
|
+
self.lastAnimationTime = now;
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
self.churnBlocking = NO;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (!self.animationBlocking) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
BOOL stillAnimating = NO;
|
|
852
|
+
for (UIView *view in self.animatedViews) {
|
|
853
|
+
if (![view isKindOfClass:[UIView class]]) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (view.layer.animationKeys.count > 0) {
|
|
857
|
+
stillAnimating = YES;
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (stillAnimating) {
|
|
863
|
+
self.lastAnimationTime = now;
|
|
864
|
+
} else {
|
|
865
|
+
self.animationBlocking = NO;
|
|
866
|
+
[self scheduleBonusCaptureAfterDelay:kRJBonusAnimationDelaySeconds
|
|
867
|
+
now:now];
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
#pragma mark - Bonus Capture
|
|
872
|
+
|
|
873
|
+
- (void)scheduleBonusCaptureAfterDelay:(NSTimeInterval)delay
|
|
874
|
+
now:(NSTimeInterval)now {
|
|
875
|
+
if (self.pendingKeyframes < kRJMaxPendingKeyframes) {
|
|
876
|
+
self.pendingKeyframes += 1;
|
|
877
|
+
}
|
|
878
|
+
NSTimeInterval candidate = now + delay;
|
|
879
|
+
if (self.bonusCaptureTime <= 0 || candidate < self.bonusCaptureTime) {
|
|
880
|
+
self.bonusCaptureTime = candidate;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
#pragma mark - Decision Helpers
|
|
885
|
+
|
|
886
|
+
- (void)considerBlockerSince:(NSTimeInterval)timestamp
|
|
887
|
+
quietInterval:(NSTimeInterval)quietInterval
|
|
888
|
+
now:(NSTimeInterval)now
|
|
889
|
+
reason:(RJCaptureHeuristicsReason)reason
|
|
890
|
+
earliestTime:(NSTimeInterval *)earliestTime
|
|
891
|
+
chosenReason:(RJCaptureHeuristicsReason *)chosenReason {
|
|
892
|
+
if (timestamp <= 0) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
NSTimeInterval readyTime = timestamp + quietInterval;
|
|
897
|
+
if (readyTime > now && readyTime > *earliestTime) {
|
|
898
|
+
*earliestTime = readyTime;
|
|
899
|
+
*chosenReason = reason;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
@end
|