@capgo/capacitor-screen-orientation 8.1.20 → 8.2.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/README.md CHANGED
@@ -20,6 +20,7 @@ Screen orientation plugin with support for detecting true physical device orient
20
20
  - 🔒 **Detect if orientation lock is enabled** by comparing physical vs UI orientation
21
21
  - 🔄 Real-time orientation change detection
22
22
  - 🎯 Lock orientation to specific modes
23
+ - 📖 **Foldable posture** on Android: flat, tabletop, or book, plus hinge angle and window size class
23
24
  - 🌐 Web platform support
24
25
 
25
26
  **Important Note:** This plugin can detect the physical orientation of the device using motion sensors, but it **cannot bypass the UI orientation lock**. The screen will still respect the user's orientation lock setting. This is useful for knowing how the device is physically held vs. how the UI is displayed.
@@ -131,6 +132,10 @@ await ScreenOrientation.stopOrientationTracking();
131
132
 
132
133
  // Remove listener
133
134
  await listener.remove();
135
+
136
+ // Foldables: flat, tabletop, or book. Flat on phones that do not fold.
137
+ const { posture } = await ScreenOrientation.getFoldState();
138
+ const { widthClass } = await ScreenOrientation.getSizeClass();
134
139
  ```
135
140
 
136
141
  ## API
@@ -143,7 +148,14 @@ await listener.remove();
143
148
  * [`startOrientationTracking(...)`](#startorientationtracking)
144
149
  * [`stopOrientationTracking()`](#stoporientationtracking)
145
150
  * [`isOrientationLocked()`](#isorientationlocked)
151
+ * [`isDeviceFoldable()`](#isdevicefoldable)
152
+ * [`getFoldState()`](#getfoldstate)
153
+ * [`getHingeAngle()`](#gethingeangle)
154
+ * [`getSizeClass()`](#getsizeclass)
146
155
  * [`addListener('screenOrientationChange', ...)`](#addlistenerscreenorientationchange-)
156
+ * [`addListener('foldStateChange', ...)`](#addlistenerfoldstatechange-)
157
+ * [`addListener('hingeAngleChange', ...)`](#addlistenerhingeanglechange-)
158
+ * [`addListener('sizeClassChange', ...)`](#addlistenersizeclasschange-)
147
159
  * [`removeAllListeners()`](#removealllisteners)
148
160
  * [`getPluginVersion()`](#getpluginversion)
149
161
  * [Interfaces](#interfaces)
@@ -275,26 +287,150 @@ Works on both iOS (Core Motion) and Android (Accelerometer).
275
287
  --------------------
276
288
 
277
289
 
290
+ ### isDeviceFoldable()
291
+
292
+ ```typescript
293
+ isDeviceFoldable() => Promise<DeviceFoldableResult>
294
+ ```
295
+
296
+ Whether this device folds, and whether it can stand half-open like a laptop.
297
+
298
+ Both flags are `false` on web and iOS.
299
+
300
+ **Returns:** <code>Promise&lt;<a href="#devicefoldableresult">DeviceFoldableResult</a>&gt;</code>
301
+
302
+ **Since:** 8.2.0
303
+
304
+ --------------------
305
+
306
+
307
+ ### getFoldState()
308
+
309
+ ```typescript
310
+ getFoldState() => Promise<FoldState>
311
+ ```
312
+
313
+ Read the current fold.
314
+
315
+ Resolves to a flat state when the device has no fold.
316
+
317
+ **Returns:** <code>Promise&lt;<a href="#foldstate">FoldState</a>&gt;</code>
318
+
319
+ **Since:** 8.2.0
320
+
321
+ --------------------
322
+
323
+
324
+ ### getHingeAngle()
325
+
326
+ ```typescript
327
+ getHingeAngle() => Promise<HingeAngleResult>
328
+ ```
329
+
330
+ Read the hinge angle in degrees.
331
+
332
+ `0` is closed and `180` is flat. `angle` is `null` without a hinge sensor.
333
+
334
+ **Returns:** <code>Promise&lt;<a href="#hingeangleresult">HingeAngleResult</a>&gt;</code>
335
+
336
+ **Since:** 8.2.0
337
+
338
+ --------------------
339
+
340
+
341
+ ### getSizeClass()
342
+
343
+ ```typescript
344
+ getSizeClass() => Promise<SizeClass>
345
+ ```
346
+
347
+ Read the window size classes.
348
+
349
+ **Returns:** <code>Promise&lt;<a href="#sizeclass">SizeClass</a>&gt;</code>
350
+
351
+ **Since:** 8.2.0
352
+
353
+ --------------------
354
+
355
+
278
356
  ### addListener('screenOrientationChange', ...)
279
357
 
280
358
  ```typescript
281
359
  addListener(eventName: 'screenOrientationChange', listenerFunc: (result: ScreenOrientationResult) => void) => Promise<PluginListenerHandle>
282
360
  ```
283
361
 
284
- Listen for screen orientation changes.
362
+ | Param | Type |
363
+ | ------------------ | ------------------------------------------------------------------------------------------------ |
364
+ | **`eventName`** | <code>'screenOrientationChange'</code> |
365
+ | **`listenerFunc`** | <code>(result: <a href="#screenorientationresult">ScreenOrientationResult</a>) =&gt; void</code> |
285
366
 
286
- Registers a listener that will be called whenever the screen orientation changes.
287
- If motion-based tracking is enabled, this will also fire for orientation changes
288
- detected by motion sensors even when orientation lock is enabled.
367
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
289
368
 
290
- | Param | Type | Description |
291
- | ------------------ | ------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
292
- | **`eventName`** | <code>'screenOrientationChange'</code> | The event name. Must be 'screenOrientationChange'. |
293
- | **`listenerFunc`** | <code>(result: <a href="#screenorientationresult">ScreenOrientationResult</a>) =&gt; void</code> | Callback function invoked when orientation changes. |
369
+ --------------------
370
+
371
+
372
+ ### addListener('foldStateChange', ...)
373
+
374
+ ```typescript
375
+ addListener(eventName: 'foldStateChange', listenerFunc: (state: FoldState) => void) => Promise<PluginListenerHandle>
376
+ ```
377
+
378
+ Listen for fold changes.
379
+
380
+ On a foldable this also fires when the device rotates, because the hinge
381
+ bounds rotate with the window.
382
+
383
+ | Param | Type | Description |
384
+ | ------------------ | ------------------------------------------------------------------- | ------------------------------------------ |
385
+ | **`eventName`** | <code>'foldStateChange'</code> | The event name. Must be 'foldStateChange'. |
386
+ | **`listenerFunc`** | <code>(state: <a href="#foldstate">FoldState</a>) =&gt; void</code> | Callback invoked with the new fold state. |
294
387
 
295
388
  **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
296
389
 
297
- **Since:** 1.0.0
390
+ **Since:** 8.2.0
391
+
392
+ --------------------
393
+
394
+
395
+ ### addListener('hingeAngleChange', ...)
396
+
397
+ ```typescript
398
+ addListener(eventName: 'hingeAngleChange', listenerFunc: (event: HingeAngleResult) => void) => Promise<PluginListenerHandle>
399
+ ```
400
+
401
+ Listen for hinge angle changes.
402
+
403
+ On Android the hinge sensor runs only while at least one listener is registered.
404
+ Never fires on web or iOS.
405
+
406
+ | Param | Type | Description |
407
+ | ------------------ | --------------------------------------------------------------------------------- | ------------------------------------------- |
408
+ | **`eventName`** | <code>'hingeAngleChange'</code> | The event name. Must be 'hingeAngleChange'. |
409
+ | **`listenerFunc`** | <code>(event: <a href="#hingeangleresult">HingeAngleResult</a>) =&gt; void</code> | Callback invoked with the angle in degrees. |
410
+
411
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
412
+
413
+ **Since:** 8.2.0
414
+
415
+ --------------------
416
+
417
+
418
+ ### addListener('sizeClassChange', ...)
419
+
420
+ ```typescript
421
+ addListener(eventName: 'sizeClassChange', listenerFunc: (sizeClass: SizeClass) => void) => Promise<PluginListenerHandle>
422
+ ```
423
+
424
+ Listen for size class changes, such as unfolding, rotating, or resizing.
425
+
426
+ | Param | Type | Description |
427
+ | ------------------ | ----------------------------------------------------------------------- | ------------------------------------------ |
428
+ | **`eventName`** | <code>'sizeClassChange'</code> | The event name. Must be 'sizeClassChange'. |
429
+ | **`listenerFunc`** | <code>(sizeClass: <a href="#sizeclass">SizeClass</a>) =&gt; void</code> | Callback invoked with the new size class. |
430
+
431
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
432
+
433
+ **Since:** 8.2.0
298
434
 
299
435
  --------------------
300
436
 
@@ -373,6 +509,71 @@ Result returned by the isOrientationLocked() method.
373
509
  | **`uiOrientation`** | <code><a href="#orientationtype">OrientationType</a></code> | The current UI orientation reported by the system. | 1.0.0 |
374
510
 
375
511
 
512
+ #### DeviceFoldableResult
513
+
514
+ Whether this device can fold.
515
+
516
+ | Prop | Type | Description | Since |
517
+ | ---------------------- | -------------------- | --------------------------------------------- | ----- |
518
+ | **`foldable`** | <code>boolean</code> | Whether the device has a fold. | 8.2.0 |
519
+ | **`supportsTabletop`** | <code>boolean</code> | Whether it can stand half-open like a laptop. | 8.2.0 |
520
+
521
+
522
+ #### FoldState
523
+
524
+ Current fold of the window.
525
+
526
+ Phones that do not fold, iOS, and web resolve to a flat state.
527
+
528
+ | Prop | Type | Description | Since |
529
+ | ---------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------ | ----- |
530
+ | **`state`** | <code><a href="#foldstatevalue">FoldStateValue</a></code> | Posture of the fold. | 8.2.0 |
531
+ | **`isSeparating`** | <code>boolean</code> | Whether the fold splits the web view into two areas. | 8.2.0 |
532
+ | **`posture`** | <code><a href="#foldposture">FoldPosture</a></code> | How the device is held. | 8.2.0 |
533
+ | **`hingeOrientation`** | <code><a href="#hingeorientation">HingeOrientation</a></code> | Direction of the hinge. Omitted when there is no fold. | 8.2.0 |
534
+ | **`hingeBounds`** | <code><a href="#foldbounds">FoldBounds</a></code> | Position of the fold in CSS pixels. Omitted when there is no fold. | 8.2.0 |
535
+ | **`occludedBounds`** | <code><a href="#foldbounds">FoldBounds</a></code> | Area the hinge covers. Present when the hinge has a physical gap. | 8.2.0 |
536
+
537
+
538
+ #### FoldBounds
539
+
540
+ A rectangle in CSS pixels, relative to the web view.
541
+
542
+ | Prop | Type |
543
+ | ------------ | ------------------- |
544
+ | **`x`** | <code>number</code> |
545
+ | **`y`** | <code>number</code> |
546
+ | **`width`** | <code>number</code> |
547
+ | **`height`** | <code>number</code> |
548
+
549
+
550
+ #### HingeAngleResult
551
+
552
+ Angle between the two halves of a foldable, in degrees.
553
+
554
+ `0` is closed and `180` is flat. `angle` is `null` when the device has no hinge sensor.
555
+
556
+ | Prop | Type | Description | Since |
557
+ | ----------- | --------------------------- | -------------------------------------------------------------- | ----- |
558
+ | **`angle`** | <code>number \| null</code> | Hinge angle in degrees, or `null` when no sensor is available. | 8.2.0 |
559
+
560
+
561
+ #### SizeClass
562
+
563
+ Window size classes.
564
+
565
+ `horizontal` and `vertical` follow Apple's compact/regular split.
566
+ On Android and web, `regular` starts at 600 CSS pixels wide and 480 tall.
567
+ `widthClass` and `heightClass` follow Material window size classes.
568
+
569
+ | Prop | Type | Description | Since |
570
+ | ----------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----- |
571
+ | **`horizontal`** | <code>'compact' \| 'regular'</code> | Width size class. `compact` on a phone, `regular` on the inner display of a foldable, a tablet, or a window at least 600 CSS pixels wide. | 8.2.0 |
572
+ | **`vertical`** | <code>'compact' \| 'regular'</code> | Height size class. `regular` starts at 480 CSS pixels. | 8.2.0 |
573
+ | **`widthClass`** | <code>'compact' \| 'medium' \| 'expanded' \| 'large' \| 'extraLarge'</code> | Material window width class. | 8.2.0 |
574
+ | **`heightClass`** | <code>'compact' \| 'medium' \| 'expanded'</code> | Material window height class. | 8.2.0 |
575
+
576
+
376
577
  #### PluginListenerHandle
377
578
 
378
579
  | Prop | Type |
@@ -396,4 +597,29 @@ Orientation lock type that can be used to lock the device orientation.
396
597
 
397
598
  <code>'any' | 'natural' | 'landscape' | 'portrait' | 'portrait-primary' | 'portrait-secondary' | 'landscape-primary' | 'landscape-secondary'</code>
398
599
 
600
+
601
+ #### FoldStateValue
602
+
603
+ How open a foldable is.
604
+
605
+ `closed` is reserved. A device shut onto its cover display reports `flat`.
606
+
607
+ <code>'flat' | 'half-opened'</code>
608
+
609
+
610
+ #### FoldPosture
611
+
612
+ How a half-open foldable is held.
613
+
614
+ `tabletop` is a horizontal hinge, like a laptop. `book` is a vertical hinge.
615
+
616
+ <code>'flat' | 'tabletop' | 'book'</code>
617
+
618
+
619
+ #### HingeOrientation
620
+
621
+ Direction of the hinge relative to the window.
622
+
623
+ <code>'horizontal' | 'vertical'</code>
624
+
399
625
  </docgen-api>
@@ -3,6 +3,7 @@ ext {
3
3
  androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
4
4
  androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
5
5
  androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
6
+ androidxWindowVersion = project.hasProperty('androidxWindowVersion') ? rootProject.ext.androidxWindowVersion : '1.5.1'
6
7
  }
7
8
 
8
9
  buildscript {
@@ -52,6 +53,7 @@ dependencies {
52
53
  implementation fileTree(dir: 'libs', include: ['*.jar'])
53
54
  implementation project(':capacitor-android')
54
55
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
56
+ implementation "androidx.window:window-java:$androidxWindowVersion"
55
57
  testImplementation "junit:junit:$junitVersion"
56
58
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
57
59
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -7,23 +7,60 @@ import android.hardware.Sensor;
7
7
  import android.hardware.SensorEvent;
8
8
  import android.hardware.SensorEventListener;
9
9
  import android.hardware.SensorManager;
10
+ import android.os.Build;
11
+ import android.os.Handler;
12
+ import android.os.Looper;
10
13
  import android.view.Surface;
11
14
  import com.getcapacitor.JSObject;
12
15
  import com.getcapacitor.Plugin;
13
16
  import com.getcapacitor.PluginCall;
14
17
  import com.getcapacitor.PluginMethod;
15
18
  import com.getcapacitor.annotation.CapacitorPlugin;
19
+ import java.util.ArrayList;
20
+ import java.util.List;
21
+ import java.util.concurrent.Executor;
22
+ import org.json.JSONObject;
16
23
 
17
24
  @CapacitorPlugin(name = "CapacitorScreenOrientation")
18
25
  public class CapacitorScreenOrientationPlugin extends Plugin implements SensorEventListener {
19
26
 
20
- private final String pluginVersion = "8.1.20";
27
+ private final String pluginVersion = "8.2.0";
21
28
  private int currentOrientation;
22
29
  private SensorManager sensorManager;
23
30
  private Sensor accelerometer;
24
31
  private boolean isTrackingMotion = false;
25
32
  private String currentPhysicalOrientation = "portrait-primary";
26
33
  private String lastNotifiedOrientation = null;
34
+ private FoldLayout foldLayout;
35
+ private Sensor hingeSensor;
36
+ private boolean hingeRegistered = false;
37
+ private Float lastHingeAngle = null;
38
+ private String lastSizeClassKey = null;
39
+ private final List<PluginCall> pendingHingeCalls = new ArrayList<>();
40
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
41
+ private final SensorEventListener hingeListener = new SensorEventListener() {
42
+ @Override
43
+ public void onSensorChanged(SensorEvent event) {
44
+ if (event.values.length == 0) {
45
+ return;
46
+ }
47
+ float angle = Math.max(0f, Math.min(360f, event.values[0]));
48
+ boolean unchanged = lastHingeAngle != null && Math.abs(lastHingeAngle - angle) < 0.5f;
49
+ lastHingeAngle = angle;
50
+ if (unchanged && pendingHingeCalls.isEmpty()) {
51
+ return;
52
+ }
53
+ JSObject payload = hingePayload(angle);
54
+ notifyListeners("hingeAngleChange", payload);
55
+ resolvePendingHinge(payload);
56
+ if (!hasListeners("hingeAngleChange")) {
57
+ stopHingeTracking();
58
+ }
59
+ }
60
+
61
+ @Override
62
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
63
+ };
27
64
 
28
65
  @Override
29
66
  public void load() {
@@ -34,12 +71,26 @@ public class CapacitorScreenOrientationPlugin extends Plugin implements SensorEv
34
71
  sensorManager = (SensorManager) getActivity().getSystemService(Context.SENSOR_SERVICE);
35
72
  if (sensorManager != null) {
36
73
  accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
74
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
75
+ hingeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE);
76
+ }
37
77
  }
78
+
79
+ Executor executor = mainHandler::post;
80
+ foldLayout = new FoldLayout(getActivity(), bridge.getWebView(), executor, (state) -> {
81
+ notifyListeners("foldStateChange", state);
82
+ notifySizeClassIfChanged();
83
+ });
84
+ notifySizeClassIfChanged();
38
85
  }
39
86
 
40
87
  @Override
41
88
  protected void handleOnDestroy() {
42
89
  stopMotionTracking();
90
+ stopHingeTracking();
91
+ if (foldLayout != null) {
92
+ foldLayout.stop();
93
+ }
43
94
  super.handleOnDestroy();
44
95
  }
45
96
 
@@ -47,6 +98,8 @@ public class CapacitorScreenOrientationPlugin extends Plugin implements SensorEv
47
98
  protected void handleOnConfigurationChanged(Configuration newConfig) {
48
99
  super.handleOnConfigurationChanged(newConfig);
49
100
 
101
+ notifySizeClassIfChanged();
102
+
50
103
  // Skip system orientation changes when motion tracking is active
51
104
  // to avoid duplicate events
52
105
  if (isTrackingMotion) {
@@ -59,6 +112,79 @@ public class CapacitorScreenOrientationPlugin extends Plugin implements SensorEv
59
112
  }
60
113
  }
61
114
 
115
+ @PluginMethod(returnType = PluginMethod.RETURN_NONE)
116
+ @Override
117
+ public void addListener(PluginCall call) {
118
+ super.addListener(call);
119
+ String eventName = call.getString("eventName");
120
+ if ("hingeAngleChange".equals(eventName)) {
121
+ mainHandler.post(this::startHingeTracking);
122
+ }
123
+ }
124
+
125
+ @PluginMethod(returnType = PluginMethod.RETURN_NONE)
126
+ @Override
127
+ public void removeListener(PluginCall call) {
128
+ super.removeListener(call);
129
+ if (!hasListeners("hingeAngleChange")) {
130
+ mainHandler.post(this::stopHingeTracking);
131
+ }
132
+ }
133
+
134
+ @PluginMethod
135
+ @Override
136
+ public void removeAllListeners(PluginCall call) {
137
+ super.removeAllListeners(call);
138
+ mainHandler.post(this::stopHingeTracking);
139
+ }
140
+
141
+ @PluginMethod
142
+ public void isDeviceFoldable(PluginCall call) {
143
+ JSObject result = new JSObject();
144
+ result.put("foldable", foldLayout != null && foldLayout.isFoldable());
145
+ result.put("supportsTabletop", foldLayout != null && foldLayout.supportsTabletop());
146
+ call.resolve(result);
147
+ }
148
+
149
+ @PluginMethod
150
+ public void getFoldState(PluginCall call) {
151
+ call.resolve(foldLayout == null ? flatFold() : foldLayout.currentState());
152
+ }
153
+
154
+ @PluginMethod
155
+ public void getHingeAngle(PluginCall call) {
156
+ mainHandler.post(() -> {
157
+ if (hingeRegistered && lastHingeAngle != null) {
158
+ call.resolve(hingePayload(lastHingeAngle));
159
+ return;
160
+ }
161
+ if (hingeSensor == null) {
162
+ call.resolve(hingePayload(null));
163
+ return;
164
+ }
165
+ pendingHingeCalls.add(call);
166
+ startHingeTracking();
167
+ mainHandler.postDelayed(
168
+ () -> {
169
+ if (!pendingHingeCalls.contains(call)) {
170
+ return;
171
+ }
172
+ pendingHingeCalls.remove(call);
173
+ call.resolve(hingePayload(null));
174
+ if (!hasListeners("hingeAngleChange") && pendingHingeCalls.isEmpty()) {
175
+ stopHingeTracking();
176
+ }
177
+ },
178
+ 800
179
+ );
180
+ });
181
+ }
182
+
183
+ @PluginMethod
184
+ public void getSizeClass(PluginCall call) {
185
+ call.resolve(currentSizeClass());
186
+ }
187
+
62
188
  @PluginMethod
63
189
  public void orientation(final PluginCall call) {
64
190
  try {
@@ -198,6 +324,68 @@ public class CapacitorScreenOrientationPlugin extends Plugin implements SensorEv
198
324
  }
199
325
  }
200
326
 
327
+ private JSObject currentSizeClass() {
328
+ if (foldLayout != null) {
329
+ return foldLayout.sizeClass();
330
+ }
331
+ return FoldLayout.sizeClassOf(0f, 0f);
332
+ }
333
+
334
+ private void notifySizeClassIfChanged() {
335
+ JSObject sizeClass = currentSizeClass();
336
+ String key = sizeClass.toString();
337
+ if (key.equals(lastSizeClassKey)) {
338
+ return;
339
+ }
340
+ lastSizeClassKey = key;
341
+ notifyListeners("sizeClassChange", sizeClass);
342
+ }
343
+
344
+ private void startHingeTracking() {
345
+ if (hingeRegistered || hingeSensor == null || sensorManager == null) {
346
+ return;
347
+ }
348
+ hingeRegistered = true;
349
+ sensorManager.registerListener(hingeListener, hingeSensor, SensorManager.SENSOR_DELAY_NORMAL, mainHandler);
350
+ }
351
+
352
+ private void stopHingeTracking() {
353
+ if (!hingeRegistered || sensorManager == null) {
354
+ return;
355
+ }
356
+ hingeRegistered = false;
357
+ sensorManager.unregisterListener(hingeListener);
358
+ }
359
+
360
+ private void resolvePendingHinge(JSObject payload) {
361
+ if (pendingHingeCalls.isEmpty()) {
362
+ return;
363
+ }
364
+ List<PluginCall> pending = new ArrayList<>(pendingHingeCalls);
365
+ pendingHingeCalls.clear();
366
+ for (PluginCall call : pending) {
367
+ call.resolve(payload);
368
+ }
369
+ }
370
+
371
+ private static JSObject hingePayload(Float angle) {
372
+ JSObject result = new JSObject();
373
+ if (angle == null) {
374
+ result.put("angle", JSONObject.NULL);
375
+ } else {
376
+ result.put("angle", angle.doubleValue());
377
+ }
378
+ return result;
379
+ }
380
+
381
+ private static JSObject flatFold() {
382
+ JSObject state = new JSObject();
383
+ state.put("state", "flat");
384
+ state.put("isSeparating", false);
385
+ state.put("posture", "flat");
386
+ return state;
387
+ }
388
+
201
389
  // Motion tracking methods
202
390
  private void startMotionTracking() {
203
391
  if (isTrackingMotion || accelerometer == null) {