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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/src/index.d.ts
CHANGED