expo-rotation-module 1.0.1 → 1.0.3

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
@@ -49,7 +49,7 @@ import Rotation, {
49
49
 
50
50
  - `canWrite(): Promise<boolean>` — returns true if the app has `WRITE_SETTINGS` granted (Android M+). Returns `true` on older OS versions.
51
51
  - `requestWritePermission(): void` — opens the Android Settings screen where the user can grant the permission for your app.
52
- - `getRotationState(): Promise<'AUTOROTATE'|'PORTRAIT'|'LANDSCAPE'>` — reads the current global rotation state.
52
+ - `getRotationState(): Promise<'AUTOROTATE'|'PORTRAIT'|'LANDSCAPE'>` — reads the current global rotation state. The module reports the coarse axis (PORTRAIT or LANDSCAPE); within those axes the system sensor still allows regular or inverted orientations when auto-rotate is off.
53
53
  - `setRotationState(state): Promise<void>` — sets the rotation state. Rejects with an Error that may contain a `code` property (e.g. `E_PERMISSION`).
54
54
  - `getPackageName(): Promise<string>` — returns the package name the native module is using (useful for diagnostics).
55
55
 
@@ -2,13 +2,23 @@ package ktsierra.expo.rotationmodule
2
2
 
3
3
  import android.app.Activity
4
4
  import android.content.ContentResolver
5
+ import android.content.Context
5
6
  import android.content.Intent
6
7
  import android.net.Uri
7
8
  import android.os.Build
8
9
  import android.provider.Settings
9
10
  import android.util.Log
11
+ import android.content.pm.ActivityInfo
10
12
  import expo.modules.kotlin.modules.Module
11
13
  import expo.modules.kotlin.modules.ModuleDefinition
14
+ import android.hardware.Sensor
15
+ import android.hardware.SensorEvent
16
+ import android.hardware.SensorEventListener
17
+ import android.hardware.SensorManager
18
+ import kotlin.math.atan2
19
+ import kotlin.math.sqrt
20
+ import kotlin.math.abs
21
+
12
22
 
13
23
  class ExpoRotationModule : Module() {
14
24
  companion object {
@@ -16,6 +26,23 @@ class ExpoRotationModule : Module() {
16
26
  const val TAG = "ExpoRotationModule"
17
27
  }
18
28
 
29
+ private var orientationListener: android.view.OrientationEventListener? = null
30
+ private var desiredAxis: String? = null
31
+ private var lastWrittenRotation: Int = -1
32
+
33
+ // Sensor-related fields for tilt detection and debouncing
34
+ private var sensorManager: SensorManager? = null
35
+ private var accelSensor: Sensor? = null
36
+ private var accelListener: SensorEventListener? = null
37
+ // tilt threshold (degrees) below which device is considered "flat"
38
+ private val tiltThresholdDegrees = 20f
39
+ // debounce: must see same rot candidate this many times before writing
40
+ private var stableCount = 0
41
+ private val stableRequired = 3
42
+ private var lastRotCandidate = -1
43
+ // last measured inclination in degrees (0 = flat, 90 = upright)
44
+ private var lastInclinationDeg = 90f
45
+
19
46
  override fun definition() = ModuleDefinition {
20
47
  Name(NAME)
21
48
 
@@ -34,6 +61,36 @@ class ExpoRotationModule : Module() {
34
61
  }
35
62
  }
36
63
 
64
+ // Expose helpers (optional)
65
+ Function("startOrientationListener") {
66
+ val ctx = appContext.activityProvider?.currentActivity ?: appContext.reactContext!!
67
+ startOrientationListener(ctx)
68
+ return@Function null
69
+ }
70
+
71
+ Function("stopOrientationListener") {
72
+ orientationListener?.disable()
73
+ orientationListener = null
74
+ desiredAxis = null
75
+ lastWrittenRotation = -1
76
+ // unregister accelerometer listener if registered
77
+ try {
78
+ if (sensorManager != null && accelListener != null) {
79
+ sensorManager?.unregisterListener(accelListener)
80
+ }
81
+ } catch (e: Exception) {
82
+ Log.e(TAG, "stopOrientationListener sensor unregister error", e)
83
+ }
84
+ accelListener = null
85
+ accelSensor = null
86
+ sensorManager = null
87
+ // reset debounce
88
+ stableCount = 0
89
+ lastRotCandidate = -1
90
+ lastInclinationDeg = 90f
91
+ return@Function null
92
+ }
93
+
37
94
  Function("requestWritePermission") {
38
95
  try {
39
96
  val activity: Activity? = appContext.activityProvider?.currentActivity
@@ -86,9 +143,8 @@ class ExpoRotationModule : Module() {
86
143
  0
87
144
  }
88
145
  return@AsyncFunction when (rotation) {
89
- 0 -> "PORTRAIT"
90
- 1 -> "LANDSCAPE"
91
- 3 -> "LANDSCAPE"
146
+ 0, 2 -> "PORTRAIT"
147
+ 1, 3 -> "LANDSCAPE"
92
148
  else -> "PORTRAIT"
93
149
  }
94
150
  }
@@ -104,23 +160,36 @@ class ExpoRotationModule : Module() {
104
160
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.System.canWrite(ctx)) {
105
161
  throw Exception("E_PERMISSION: WRITE_SETTINGS not granted")
106
162
  }
107
- val resolver: ContentResolver = ctx.contentResolver
163
+
108
164
  when (state) {
109
165
  "AUTOROTATE" -> {
166
+ // Restore system auto-rotate
167
+ val resolver: ContentResolver = ctx.contentResolver
110
168
  Settings.System.putInt(resolver, Settings.System.ACCELEROMETER_ROTATION, 1)
169
+ // unregister listener if present
170
+ orientationListener?.disable()
171
+ orientationListener = null
172
+ desiredAxis = null
173
+ lastWrittenRotation = -1
111
174
  }
112
175
  "PORTRAIT" -> {
176
+ // disable system auto-rotate and enable axis-lock with sensor flips on portrait axis
177
+ val resolver: ContentResolver = ctx.contentResolver
113
178
  Settings.System.putInt(resolver, Settings.System.ACCELEROMETER_ROTATION, 0)
114
- Settings.System.putInt(resolver, Settings.System.USER_ROTATION, 0)
179
+ desiredAxis = "PORTRAIT"
180
+ startOrientationListener(ctx)
115
181
  }
116
182
  "LANDSCAPE" -> {
183
+ val resolver: ContentResolver = ctx.contentResolver
117
184
  Settings.System.putInt(resolver, Settings.System.ACCELEROMETER_ROTATION, 0)
118
- Settings.System.putInt(resolver, Settings.System.USER_ROTATION, 1)
185
+ desiredAxis = "LANDSCAPE"
186
+ startOrientationListener(ctx)
119
187
  }
120
188
  else -> {
121
189
  throw Exception("E_INVALID_STATE: Invalid rotation state: $state")
122
190
  }
123
191
  }
192
+
124
193
  return@AsyncFunction null
125
194
  } catch (e: Exception) {
126
195
  Log.e(TAG, "setRotationState error", e)
@@ -128,4 +197,94 @@ class ExpoRotationModule : Module() {
128
197
  }
129
198
  }
130
199
  }
200
+
201
+ private fun startOrientationListener(ctx: Context) {
202
+ // If listener already exists, do nothing
203
+ if (orientationListener != null) return
204
+
205
+ // set up accelerometer to measure inclination
206
+ sensorManager = ctx.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
207
+ accelSensor = sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
208
+ accelListener = object : SensorEventListener {
209
+ override fun onSensorChanged(event: SensorEvent) {
210
+ val x = event.values[0].toDouble()
211
+ val y = event.values[1].toDouble()
212
+ val z = event.values[2].toDouble()
213
+ val g = sqrt(x * x + y * y + z * z)
214
+ if (g > 0.0) {
215
+ var inclRad = atan2(z, sqrt(x * x + y * y))
216
+ var inclDeg = Math.toDegrees(inclRad).toFloat()
217
+ inclDeg = abs(inclDeg)
218
+ // clamp
219
+ if (inclDeg > 90f) inclDeg = 90f
220
+ lastInclinationDeg = inclDeg
221
+ }
222
+ }
223
+
224
+ override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
225
+ }
226
+ if (accelSensor != null) {
227
+ // Use UI delay for reasonable responsiveness
228
+ sensorManager?.registerListener(accelListener, accelSensor, SensorManager.SENSOR_DELAY_UI)
229
+ }
230
+
231
+ orientationListener = object : android.view.OrientationEventListener(ctx) {
232
+ override fun onOrientationChanged(orientation: Int) {
233
+ // orientation: 0..359 degrees, or ORIENTATION_UNKNOWN (-1)
234
+ if (orientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return
235
+ // Normalize to one of the 4 Android rotations: 0, 90, 180, 270
236
+ val rot = when {
237
+ orientation in 315..359 || orientation in 0..44 -> 0
238
+ orientation in 45..134 -> 1
239
+ orientation in 135..224 -> 2
240
+ else -> 3
241
+ }
242
+
243
+ try {
244
+ val resolver: ContentResolver = ctx.contentResolver
245
+
246
+ // If device is nearly flat, skip writing to avoid noise
247
+ if (lastInclinationDeg < tiltThresholdDegrees) {
248
+ lastRotCandidate = -1
249
+ stableCount = 0
250
+ return
251
+ }
252
+
253
+ // Debounce: require several consistent buckets
254
+ if (rot == lastRotCandidate) {
255
+ stableCount += 1
256
+ } else {
257
+ lastRotCandidate = rot
258
+ stableCount = 1
259
+ }
260
+ if (stableCount < stableRequired) return
261
+
262
+ // desiredAxis controls whether we want portrait or landscape axis.
263
+ when (desiredAxis) {
264
+ "PORTRAIT" -> {
265
+ // We want 0 or 2 depending on angle
266
+ val target = if (rot == 2) 2 else 0
267
+ if (lastWrittenRotation != target) {
268
+ Settings.System.putInt(resolver, Settings.System.USER_ROTATION, target)
269
+ lastWrittenRotation = target
270
+ }
271
+ }
272
+ "LANDSCAPE" -> {
273
+ // We want 1 or 3 depending on angle
274
+ val target = if (rot == 1) 3 else if (rot == 3) 1 else if (rot == 2) 1 else 3
275
+ // swapped mapping: 90 -> 3, 270 -> 1, fallback 180->1, 0->3
276
+ if (lastWrittenRotation != target) {
277
+ Settings.System.putInt(resolver, Settings.System.USER_ROTATION, target)
278
+ lastWrittenRotation = target
279
+ }
280
+ }
281
+ }
282
+ } catch (e: Exception) {
283
+ Log.e(TAG, "orientation listener write error", e)
284
+ }
285
+ }
286
+ }
287
+
288
+ orientationListener?.enable()
289
+ }
131
290
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-rotation-module",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Screen Orientation Native Module",
5
5
  "main": "index.js",
6
6
  "types": "src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type RotationState = "AUTOROTATE" | "PORTRAIT" | "LANDSCAPE";
1
+ export type RotationState = 'AUTOROTATE' | 'PORTRAIT' | 'LANDSCAPE';
2
2
 
3
3
  export function canWrite(): Promise<boolean>;
4
4
  export function requestWritePermission(): void;