@capgo/capacitor-screen-orientation 8.1.21 → 8.3.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.
@@ -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.21";
27
+ private final String pluginVersion = "8.3.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,101 @@ 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
+
188
+ @PluginMethod
189
+ public void getReservedRegions(PluginCall call) {
190
+ JSObject result = new JSObject();
191
+ result.put("regions", new org.json.JSONArray());
192
+ call.resolve(result);
193
+ }
194
+
195
+ @PluginMethod
196
+ public void getBarPlacement(PluginCall call) {
197
+ JSObject result = new JSObject();
198
+ result.put("verticalBarEdge", JSONObject.NULL);
199
+ result.put("inset", 0);
200
+ call.resolve(result);
201
+ }
202
+
203
+ @PluginMethod
204
+ public void setVerticalBarBehavior(PluginCall call) {
205
+ JSObject result = new JSObject();
206
+ result.put("applied", false);
207
+ call.resolve(result);
208
+ }
209
+
62
210
  @PluginMethod
63
211
  public void orientation(final PluginCall call) {
64
212
  try {
@@ -198,6 +346,68 @@ public class CapacitorScreenOrientationPlugin extends Plugin implements SensorEv
198
346
  }
199
347
  }
200
348
 
349
+ private JSObject currentSizeClass() {
350
+ if (foldLayout != null) {
351
+ return foldLayout.sizeClass();
352
+ }
353
+ return FoldLayout.sizeClassOf(0f, 0f);
354
+ }
355
+
356
+ private void notifySizeClassIfChanged() {
357
+ JSObject sizeClass = currentSizeClass();
358
+ String key = sizeClass.toString();
359
+ if (key.equals(lastSizeClassKey)) {
360
+ return;
361
+ }
362
+ lastSizeClassKey = key;
363
+ notifyListeners("sizeClassChange", sizeClass);
364
+ }
365
+
366
+ private void startHingeTracking() {
367
+ if (hingeRegistered || hingeSensor == null || sensorManager == null) {
368
+ return;
369
+ }
370
+ hingeRegistered = true;
371
+ sensorManager.registerListener(hingeListener, hingeSensor, SensorManager.SENSOR_DELAY_NORMAL, mainHandler);
372
+ }
373
+
374
+ private void stopHingeTracking() {
375
+ if (!hingeRegistered || sensorManager == null) {
376
+ return;
377
+ }
378
+ hingeRegistered = false;
379
+ sensorManager.unregisterListener(hingeListener);
380
+ }
381
+
382
+ private void resolvePendingHinge(JSObject payload) {
383
+ if (pendingHingeCalls.isEmpty()) {
384
+ return;
385
+ }
386
+ List<PluginCall> pending = new ArrayList<>(pendingHingeCalls);
387
+ pendingHingeCalls.clear();
388
+ for (PluginCall call : pending) {
389
+ call.resolve(payload);
390
+ }
391
+ }
392
+
393
+ private static JSObject hingePayload(Float angle) {
394
+ JSObject result = new JSObject();
395
+ if (angle == null) {
396
+ result.put("angle", JSONObject.NULL);
397
+ } else {
398
+ result.put("angle", angle.doubleValue());
399
+ }
400
+ return result;
401
+ }
402
+
403
+ private static JSObject flatFold() {
404
+ JSObject state = new JSObject();
405
+ state.put("state", "flat");
406
+ state.put("isSeparating", false);
407
+ state.put("posture", "flat");
408
+ return state;
409
+ }
410
+
201
411
  // Motion tracking methods
202
412
  private void startMotionTracking() {
203
413
  if (isTrackingMotion || accelerometer == null) {
@@ -0,0 +1,176 @@
1
+ package ee.forgr.capacitor.screenorientation;
2
+
3
+ import android.app.Activity;
4
+ import android.content.pm.PackageManager;
5
+ import android.graphics.Rect;
6
+ import android.os.Build;
7
+ import android.view.View;
8
+ import androidx.core.util.Consumer;
9
+ import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter;
10
+ import androidx.window.layout.DisplayFeature;
11
+ import androidx.window.layout.FoldingFeature;
12
+ import androidx.window.layout.SupportedPosture;
13
+ import androidx.window.layout.WindowInfoTracker;
14
+ import androidx.window.layout.WindowLayoutInfo;
15
+ import androidx.window.layout.WindowMetricsCalculator;
16
+ import com.getcapacitor.JSObject;
17
+ import java.util.concurrent.Executor;
18
+
19
+ /**
20
+ * Reads Jetpack WindowManager fold state for the current activity.
21
+ * The hinge sensor itself stays in the plugin so it can run only while a listener is registered.
22
+ */
23
+ final class FoldLayout implements Consumer<WindowLayoutInfo> {
24
+
25
+ interface Callback {
26
+ void onFoldState(JSObject state);
27
+ }
28
+
29
+ private final Activity activity;
30
+ private final View webView;
31
+ private final WindowInfoTrackerCallbackAdapter adapter;
32
+ private final Executor executor;
33
+ private final Callback callback;
34
+ private final boolean supportsTabletop;
35
+ private JSObject current = flatState();
36
+ private boolean sawFold = false;
37
+
38
+ FoldLayout(Activity activity, View webView, Executor executor, Callback callback) {
39
+ this.activity = activity;
40
+ this.webView = webView;
41
+ this.executor = executor;
42
+ this.callback = callback;
43
+ WindowInfoTracker tracker = WindowInfoTracker.getOrCreate(activity);
44
+ this.adapter = new WindowInfoTrackerCallbackAdapter(tracker);
45
+ this.supportsTabletop = supportsTabletop(tracker);
46
+ this.current = toFoldState(firstFold());
47
+ this.adapter.addWindowLayoutInfoListener(activity, executor, this);
48
+ }
49
+
50
+ private FoldingFeature firstFold() {
51
+ WindowLayoutInfo info;
52
+ try {
53
+ info = adapter.getCurrentWindowLayoutInfo(activity);
54
+ } catch (UnsupportedOperationException ignored) {
55
+ return null;
56
+ }
57
+ for (DisplayFeature feature : info.getDisplayFeatures()) {
58
+ if (feature instanceof FoldingFeature) {
59
+ sawFold = true;
60
+ return (FoldingFeature) feature;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ void stop() {
67
+ adapter.removeWindowLayoutInfoListener(this);
68
+ }
69
+
70
+ JSObject currentState() {
71
+ return current;
72
+ }
73
+
74
+ boolean isFoldable() {
75
+ if (sawFold || supportsTabletop) {
76
+ return true;
77
+ }
78
+ return (
79
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
80
+ activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_SENSOR_HINGE_ANGLE)
81
+ );
82
+ }
83
+
84
+ boolean supportsTabletop() {
85
+ return supportsTabletop;
86
+ }
87
+
88
+ JSObject sizeClass() {
89
+ Rect bounds = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity).getBounds();
90
+ float density = activity.getResources().getDisplayMetrics().density;
91
+ return sizeClassOf(bounds.width() / density, bounds.height() / density);
92
+ }
93
+
94
+ static JSObject sizeClassOf(float widthDp, float heightDp) {
95
+ JSObject result = new JSObject();
96
+ result.put("horizontal", widthDp >= 600f ? "regular" : "compact");
97
+ result.put("vertical", heightDp >= 480f ? "regular" : "compact");
98
+ result.put("widthClass", widthClass(widthDp));
99
+ result.put("heightClass", heightDp >= 900f ? "expanded" : heightDp >= 480f ? "medium" : "compact");
100
+ return result;
101
+ }
102
+
103
+ @Override
104
+ public void accept(WindowLayoutInfo info) {
105
+ FoldingFeature fold = null;
106
+ for (DisplayFeature feature : info.getDisplayFeatures()) {
107
+ if (feature instanceof FoldingFeature) {
108
+ fold = (FoldingFeature) feature;
109
+ break;
110
+ }
111
+ }
112
+ if (fold != null) {
113
+ sawFold = true;
114
+ }
115
+ current = toFoldState(fold);
116
+ callback.onFoldState(current);
117
+ }
118
+
119
+ private JSObject toFoldState(FoldingFeature fold) {
120
+ if (fold == null) {
121
+ return flatState();
122
+ }
123
+
124
+ float density = activity.getResources().getDisplayMetrics().density;
125
+ int[] offset = new int[2];
126
+ webView.getLocationInWindow(offset);
127
+ JSObject bounds = boundsOf(fold.getBounds(), density, offset[0], offset[1]);
128
+
129
+ boolean halfOpened = FoldingFeature.State.HALF_OPENED.equals(fold.getState());
130
+ boolean horizontal = FoldingFeature.Orientation.HORIZONTAL.equals(fold.getOrientation());
131
+
132
+ JSObject state = new JSObject();
133
+ state.put("state", halfOpened ? "half-opened" : "flat");
134
+ state.put("isSeparating", fold.isSeparating());
135
+ state.put("posture", !halfOpened ? "flat" : horizontal ? "tabletop" : "book");
136
+ state.put("hingeOrientation", horizontal ? "horizontal" : "vertical");
137
+ state.put("hingeBounds", bounds);
138
+ if (FoldingFeature.OcclusionType.FULL.equals(fold.getOcclusionType())) {
139
+ state.put("occludedBounds", bounds);
140
+ }
141
+ return state;
142
+ }
143
+
144
+ private static JSObject boundsOf(Rect rect, float density, int offsetX, int offsetY) {
145
+ JSObject bounds = new JSObject();
146
+ bounds.put("x", Math.round((rect.left - offsetX) / density));
147
+ bounds.put("y", Math.round((rect.top - offsetY) / density));
148
+ bounds.put("width", Math.round(rect.width() / density));
149
+ bounds.put("height", Math.round(rect.height() / density));
150
+ return bounds;
151
+ }
152
+
153
+ private static JSObject flatState() {
154
+ JSObject state = new JSObject();
155
+ state.put("state", "flat");
156
+ state.put("isSeparating", false);
157
+ state.put("posture", "flat");
158
+ return state;
159
+ }
160
+
161
+ private static String widthClass(float widthDp) {
162
+ if (widthDp >= 1600f) return "extraLarge";
163
+ if (widthDp >= 1200f) return "large";
164
+ if (widthDp >= 840f) return "expanded";
165
+ if (widthDp >= 600f) return "medium";
166
+ return "compact";
167
+ }
168
+
169
+ private static boolean supportsTabletop(WindowInfoTracker tracker) {
170
+ try {
171
+ return tracker.getSupportedPostures().contains(SupportedPosture.TABLETOP);
172
+ } catch (RuntimeException ignored) {
173
+ return false;
174
+ }
175
+ }
176
+ }