@erkamyaman/capacitor-foldable 7.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/ErkamyamanCapacitorFoldable.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +28 -0
- package/README.md +457 -0
- package/android/build.gradle +71 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/kotlin/io/github/erkamyaman/foldable/DisplayModes.kt +115 -0
- package/android/src/main/kotlin/io/github/erkamyaman/foldable/Foldable.kt +189 -0
- package/android/src/main/kotlin/io/github/erkamyaman/foldable/FoldablePlugin.kt +333 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +584 -0
- package/dist/esm/css.d.ts +8 -0
- package/dist/esm/css.js +29 -0
- package/dist/esm/css.js.map +1 -0
- package/dist/esm/definitions.d.ts +267 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/polyfill.d.ts +19 -0
- package/dist/esm/polyfill.js +76 -0
- package/dist/esm/polyfill.js.map +1 -0
- package/dist/esm/segments.d.ts +8 -0
- package/dist/esm/segments.js +26 -0
- package/dist/esm/segments.js.map +1 -0
- package/dist/esm/size-class.d.ts +2 -0
- package/dist/esm/size-class.js +33 -0
- package/dist/esm/size-class.js.map +1 -0
- package/dist/esm/web.d.ts +21 -0
- package/dist/esm/web.js +46 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +222 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +225 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/FoldablePlugin/Foldable.swift +156 -0
- package/ios/Sources/FoldablePlugin/FoldablePlugin.swift +194 -0
- package/ios/Sources/FoldablePlugin/ReservedRegionFoldProvider.swift +69 -0
- package/ios/Sources/FoldablePlugin/UIKitFoldSource.swift +53 -0
- package/ios/Tests/FoldablePluginTests/FoldableTests.swift +104 -0
- package/ios/Tests/FoldablePluginTests/ReservedRegionFoldProviderTests.swift +143 -0
- package/package.json +96 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package io.github.erkamyaman.foldable
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.view.View
|
|
6
|
+
import androidx.core.content.ContextCompat
|
|
7
|
+
import androidx.window.area.WindowAreaCapability
|
|
8
|
+
import androidx.window.area.WindowAreaController
|
|
9
|
+
import androidx.window.area.WindowAreaInfo
|
|
10
|
+
import androidx.window.area.WindowAreaPresentationSessionCallback
|
|
11
|
+
import androidx.window.area.WindowAreaSession
|
|
12
|
+
import androidx.window.area.WindowAreaSessionCallback
|
|
13
|
+
import androidx.window.area.WindowAreaSessionPresenter
|
|
14
|
+
import androidx.window.core.ExperimentalWindowApi
|
|
15
|
+
import kotlinx.coroutines.flow.Flow
|
|
16
|
+
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
17
|
+
import kotlinx.coroutines.flow.map
|
|
18
|
+
import kotlinx.coroutines.flow.onEach
|
|
19
|
+
|
|
20
|
+
data class DisplayModeStatuses(val rearDisplay: String, val dualScreen: String) {
|
|
21
|
+
companion object {
|
|
22
|
+
val UNSUPPORTED = DisplayModeStatuses("unsupported", "unsupported")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@OptIn(ExperimentalWindowApi::class)
|
|
27
|
+
class DisplayModes(private val activity: Activity) {
|
|
28
|
+
|
|
29
|
+
private val controller = WindowAreaController.getOrCreate()
|
|
30
|
+
private val executor = ContextCompat.getMainExecutor(activity)
|
|
31
|
+
private var rearDisplayArea: WindowAreaInfo? = null
|
|
32
|
+
private var rearDisplaySession: WindowAreaSession? = null
|
|
33
|
+
private var dualScreenSession: WindowAreaSessionPresenter? = null
|
|
34
|
+
|
|
35
|
+
fun statuses(): Flow<DisplayModeStatuses> =
|
|
36
|
+
controller.windowAreaInfos
|
|
37
|
+
.map { areas -> areas.firstOrNull { it.type == WindowAreaInfo.Type.TYPE_REAR_FACING } }
|
|
38
|
+
.onEach { rearDisplayArea = it }
|
|
39
|
+
.map { area ->
|
|
40
|
+
DisplayModeStatuses(
|
|
41
|
+
rearDisplay = statusOf(area, WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA),
|
|
42
|
+
dualScreen = statusOf(area, WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA)
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
.distinctUntilChanged()
|
|
46
|
+
|
|
47
|
+
fun startRearDisplay(onStarted: () -> Unit, onEnded: (Throwable?) -> Unit) {
|
|
48
|
+
val area = rearDisplayArea ?: return onEnded(null)
|
|
49
|
+
|
|
50
|
+
controller.transferActivityToWindowArea(
|
|
51
|
+
area.token,
|
|
52
|
+
activity,
|
|
53
|
+
executor,
|
|
54
|
+
object : WindowAreaSessionCallback {
|
|
55
|
+
override fun onSessionStarted(session: WindowAreaSession) {
|
|
56
|
+
rearDisplaySession = session
|
|
57
|
+
onStarted()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun onSessionEnded(t: Throwable?) {
|
|
61
|
+
rearDisplaySession = null
|
|
62
|
+
onEnded(t)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun stopRearDisplay() {
|
|
69
|
+
rearDisplaySession?.close()
|
|
70
|
+
rearDisplaySession = null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fun startDualScreen(createContent: (Context) -> View, onStarted: () -> Unit, onEnded: (Throwable?) -> Unit) {
|
|
74
|
+
dualScreenSession?.let { session ->
|
|
75
|
+
session.setContentView(createContent(session.context))
|
|
76
|
+
onStarted()
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
val area = rearDisplayArea ?: return onEnded(null)
|
|
81
|
+
|
|
82
|
+
controller.presentContentOnWindowArea(
|
|
83
|
+
area.token,
|
|
84
|
+
activity,
|
|
85
|
+
executor,
|
|
86
|
+
object : WindowAreaPresentationSessionCallback {
|
|
87
|
+
override fun onSessionStarted(session: WindowAreaSessionPresenter) {
|
|
88
|
+
dualScreenSession = session
|
|
89
|
+
session.setContentView(createContent(session.context))
|
|
90
|
+
onStarted()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override fun onSessionEnded(t: Throwable?) {
|
|
94
|
+
dualScreenSession = null
|
|
95
|
+
onEnded(t)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun onContainerVisibilityChanged(isVisible: Boolean) = Unit
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fun stopDualScreen() {
|
|
104
|
+
dualScreenSession?.close()
|
|
105
|
+
dualScreenSession = null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private fun statusOf(area: WindowAreaInfo?, operation: WindowAreaCapability.Operation): String =
|
|
109
|
+
when (area?.getCapability(operation)?.status) {
|
|
110
|
+
WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> "active"
|
|
111
|
+
WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> "available"
|
|
112
|
+
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> "unavailable"
|
|
113
|
+
else -> "unsupported"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
package io.github.erkamyaman.foldable
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.graphics.Rect
|
|
7
|
+
import android.hardware.Sensor
|
|
8
|
+
import android.hardware.SensorEvent
|
|
9
|
+
import android.hardware.SensorEventListener
|
|
10
|
+
import android.hardware.SensorManager
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.view.View
|
|
13
|
+
import androidx.core.view.ViewCompat
|
|
14
|
+
import androidx.window.layout.FoldingFeature
|
|
15
|
+
import androidx.window.layout.SupportedPosture
|
|
16
|
+
import androidx.window.layout.WindowInfoTracker
|
|
17
|
+
import androidx.window.layout.WindowLayoutInfo
|
|
18
|
+
import androidx.window.layout.WindowMetricsCalculator
|
|
19
|
+
import kotlinx.coroutines.channels.awaitClose
|
|
20
|
+
import kotlinx.coroutines.flow.Flow
|
|
21
|
+
import kotlinx.coroutines.flow.callbackFlow
|
|
22
|
+
import kotlinx.coroutines.flow.combine
|
|
23
|
+
import kotlinx.coroutines.flow.conflate
|
|
24
|
+
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
25
|
+
import kotlinx.coroutines.flow.emptyFlow
|
|
26
|
+
import kotlin.math.roundToInt
|
|
27
|
+
|
|
28
|
+
data class FoldBounds(val x: Int, val y: Int, val width: Int, val height: Int)
|
|
29
|
+
|
|
30
|
+
data class FoldState(
|
|
31
|
+
val state: String,
|
|
32
|
+
val hingeOrientation: String? = null,
|
|
33
|
+
val hingeBounds: FoldBounds? = null,
|
|
34
|
+
val occludedBounds: FoldBounds? = null,
|
|
35
|
+
val isSeparating: Boolean = false,
|
|
36
|
+
val cameraBounds: List<FoldBounds> = emptyList(),
|
|
37
|
+
val posture: String = "flat"
|
|
38
|
+
) {
|
|
39
|
+
companion object {
|
|
40
|
+
val FLAT = FoldState("flat")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
data class SizeClass(
|
|
45
|
+
val horizontal: String,
|
|
46
|
+
val vertical: String,
|
|
47
|
+
val widthClass: String,
|
|
48
|
+
val heightClass: String
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
private const val MEDIUM_WIDTH_DP = 600f
|
|
52
|
+
private const val EXPANDED_WIDTH_DP = 840f
|
|
53
|
+
private const val LARGE_WIDTH_DP = 1200f
|
|
54
|
+
private const val EXTRA_LARGE_WIDTH_DP = 1600f
|
|
55
|
+
private const val MEDIUM_HEIGHT_DP = 480f
|
|
56
|
+
private const val EXPANDED_HEIGHT_DP = 900f
|
|
57
|
+
|
|
58
|
+
internal fun sizeClassOf(widthDp: Float, heightDp: Float) = SizeClass(
|
|
59
|
+
horizontal = if (widthDp >= MEDIUM_WIDTH_DP) "regular" else "compact",
|
|
60
|
+
vertical = if (heightDp >= MEDIUM_HEIGHT_DP) "regular" else "compact",
|
|
61
|
+
widthClass = when {
|
|
62
|
+
widthDp >= EXTRA_LARGE_WIDTH_DP -> "extraLarge"
|
|
63
|
+
widthDp >= LARGE_WIDTH_DP -> "large"
|
|
64
|
+
widthDp >= EXPANDED_WIDTH_DP -> "expanded"
|
|
65
|
+
widthDp >= MEDIUM_WIDTH_DP -> "medium"
|
|
66
|
+
else -> "compact"
|
|
67
|
+
},
|
|
68
|
+
heightClass = when {
|
|
69
|
+
heightDp >= EXPANDED_HEIGHT_DP -> "expanded"
|
|
70
|
+
heightDp >= MEDIUM_HEIGHT_DP -> "medium"
|
|
71
|
+
else -> "compact"
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
internal fun foldStateOf(
|
|
76
|
+
fold: FoldingFeature?,
|
|
77
|
+
density: Float,
|
|
78
|
+
offsetX: Int,
|
|
79
|
+
offsetY: Int,
|
|
80
|
+
cutouts: List<Rect> = emptyList()
|
|
81
|
+
): FoldState {
|
|
82
|
+
val cameraBounds = cutouts.map { toCssPixels(it, density, offsetX, offsetY) }
|
|
83
|
+
fold ?: return FoldState.FLAT.copy(cameraBounds = cameraBounds)
|
|
84
|
+
|
|
85
|
+
val state = when (fold.state) {
|
|
86
|
+
FoldingFeature.State.HALF_OPENED -> "half-opened"
|
|
87
|
+
else -> "flat"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
val orientation = when (fold.orientation) {
|
|
91
|
+
FoldingFeature.Orientation.HORIZONTAL -> "horizontal"
|
|
92
|
+
else -> "vertical"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
val bounds = toCssPixels(fold.bounds, density, offsetX, offsetY)
|
|
96
|
+
|
|
97
|
+
val occluded = if (fold.occlusionType == FoldingFeature.OcclusionType.FULL) {
|
|
98
|
+
bounds
|
|
99
|
+
} else {
|
|
100
|
+
null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
val posture = when {
|
|
104
|
+
fold.state != FoldingFeature.State.HALF_OPENED -> "flat"
|
|
105
|
+
fold.orientation == FoldingFeature.Orientation.HORIZONTAL -> "tabletop"
|
|
106
|
+
else -> "book"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return FoldState(state, orientation, bounds, occluded, fold.isSeparating, cameraBounds, posture)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private fun toCssPixels(rect: Rect, density: Float, offsetX: Int, offsetY: Int) = FoldBounds(
|
|
113
|
+
x = ((rect.left - offsetX) / density).roundToInt(),
|
|
114
|
+
y = ((rect.top - offsetY) / density).roundToInt(),
|
|
115
|
+
width = ((rect.right - rect.left) / density).roundToInt(),
|
|
116
|
+
height = ((rect.bottom - rect.top) / density).roundToInt()
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
class Foldable(private val activity: Activity, private val webView: View) {
|
|
120
|
+
|
|
121
|
+
private val tracker = WindowInfoTracker.getOrCreate(activity)
|
|
122
|
+
private val sensorManager = activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
|
123
|
+
private val hingeSensor: Sensor? =
|
|
124
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
125
|
+
sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
|
|
126
|
+
} else {
|
|
127
|
+
null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fun isDeviceFoldable(): Boolean {
|
|
131
|
+
if (supportedPostures().isNotEmpty()) return true
|
|
132
|
+
|
|
133
|
+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
|
134
|
+
activity.packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_HINGE_ANGLE)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fun supportsTabletop(): Boolean = SupportedPosture.TABLETOP in supportedPostures()
|
|
138
|
+
|
|
139
|
+
fun hasHingeSensor(): Boolean = hingeSensor != null
|
|
140
|
+
|
|
141
|
+
fun sizeClass(): SizeClass {
|
|
142
|
+
val bounds = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity).bounds
|
|
143
|
+
val density = activity.resources.displayMetrics.density
|
|
144
|
+
return sizeClassOf(bounds.width() / density, bounds.height() / density)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fun foldStates(): Flow<FoldState> =
|
|
148
|
+
combine(tracker.windowLayoutInfo(activity), webViewLayouts()) { info, _ -> toFoldState(info) }
|
|
149
|
+
.distinctUntilChanged()
|
|
150
|
+
|
|
151
|
+
private fun webViewLayouts(): Flow<Unit> = callbackFlow {
|
|
152
|
+
val listener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> trySend(Unit) }
|
|
153
|
+
trySend(Unit)
|
|
154
|
+
webView.addOnLayoutChangeListener(listener)
|
|
155
|
+
awaitClose { webView.removeOnLayoutChangeListener(listener) }
|
|
156
|
+
}.conflate()
|
|
157
|
+
|
|
158
|
+
fun hingeAngles(): Flow<Float> {
|
|
159
|
+
val sensor = hingeSensor ?: return emptyFlow()
|
|
160
|
+
|
|
161
|
+
return callbackFlow {
|
|
162
|
+
val listener = object : SensorEventListener {
|
|
163
|
+
override fun onSensorChanged(event: SensorEvent) {
|
|
164
|
+
if (event.values.isEmpty()) return
|
|
165
|
+
trySend(event.values[0].coerceIn(0f, 360f))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
|
|
169
|
+
}
|
|
170
|
+
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
|
|
171
|
+
awaitClose { sensorManager.unregisterListener(listener) }
|
|
172
|
+
}.distinctUntilChanged()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private fun supportedPostures(): List<SupportedPosture> =
|
|
176
|
+
runCatching { tracker.supportedPostures }.getOrDefault(emptyList())
|
|
177
|
+
|
|
178
|
+
private fun toFoldState(info: WindowLayoutInfo): FoldState {
|
|
179
|
+
val webViewOffset = IntArray(2).also(webView::getLocationInWindow)
|
|
180
|
+
val cutouts = ViewCompat.getRootWindowInsets(webView)?.displayCutout?.boundingRects.orEmpty()
|
|
181
|
+
return foldStateOf(
|
|
182
|
+
info.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull(),
|
|
183
|
+
activity.resources.displayMetrics.density,
|
|
184
|
+
webViewOffset[0],
|
|
185
|
+
webViewOffset[1],
|
|
186
|
+
cutouts
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
package io.github.erkamyaman.foldable
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import android.webkit.WebResourceRequest
|
|
7
|
+
import android.webkit.WebResourceResponse
|
|
8
|
+
import android.webkit.WebView
|
|
9
|
+
import android.webkit.WebViewClient
|
|
10
|
+
import androidx.lifecycle.Lifecycle
|
|
11
|
+
import androidx.lifecycle.LifecycleOwner
|
|
12
|
+
import androidx.lifecycle.repeatOnLifecycle
|
|
13
|
+
import com.getcapacitor.JSArray
|
|
14
|
+
import com.getcapacitor.JSObject
|
|
15
|
+
import com.getcapacitor.Plugin
|
|
16
|
+
import com.getcapacitor.PluginCall
|
|
17
|
+
import com.getcapacitor.PluginMethod
|
|
18
|
+
import com.getcapacitor.annotation.CapacitorPlugin
|
|
19
|
+
import kotlinx.coroutines.CoroutineScope
|
|
20
|
+
import kotlinx.coroutines.Dispatchers
|
|
21
|
+
import kotlinx.coroutines.Job
|
|
22
|
+
import kotlinx.coroutines.SupervisorJob
|
|
23
|
+
import kotlinx.coroutines.cancel
|
|
24
|
+
import kotlinx.coroutines.delay
|
|
25
|
+
import kotlinx.coroutines.flow.conflate
|
|
26
|
+
import kotlinx.coroutines.flow.first
|
|
27
|
+
import kotlinx.coroutines.flow.firstOrNull
|
|
28
|
+
import kotlinx.coroutines.launch
|
|
29
|
+
import kotlinx.coroutines.withTimeoutOrNull
|
|
30
|
+
import org.json.JSONObject
|
|
31
|
+
import java.net.URL
|
|
32
|
+
|
|
33
|
+
@CapacitorPlugin(name = "Foldable")
|
|
34
|
+
class FoldablePlugin : Plugin() {
|
|
35
|
+
|
|
36
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
|
37
|
+
|
|
38
|
+
private var implementation: Foldable? = null
|
|
39
|
+
private var displayModes: DisplayModes? = null
|
|
40
|
+
private var lastKnownState: FoldState? = null
|
|
41
|
+
private var hingeAngleJob: Job? = null
|
|
42
|
+
private var lastKnownAngle: Float? = null
|
|
43
|
+
private var lastSizeClass: SizeClass? = null
|
|
44
|
+
private var lastDisplayModes: DisplayModeStatuses? = null
|
|
45
|
+
|
|
46
|
+
override fun load() {
|
|
47
|
+
val activity = this.activity ?: return
|
|
48
|
+
val implementation = Foldable(activity, bridge.webView)
|
|
49
|
+
this.implementation = implementation
|
|
50
|
+
val displayModes = DisplayModes(activity)
|
|
51
|
+
this.displayModes = displayModes
|
|
52
|
+
|
|
53
|
+
lastSizeClass = implementation.sizeClass()
|
|
54
|
+
bridge.webView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> notifySizeClassIfChanged() }
|
|
55
|
+
|
|
56
|
+
val owner = activity as? LifecycleOwner ?: return
|
|
57
|
+
scope.launch {
|
|
58
|
+
owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
59
|
+
implementation.foldStates().collect { state ->
|
|
60
|
+
lastKnownState = state
|
|
61
|
+
notifyListeners("foldStateChange", state.toJSObject())
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
scope.launch {
|
|
66
|
+
owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
67
|
+
displayModes.statuses().collect { statuses ->
|
|
68
|
+
lastDisplayModes = statuses
|
|
69
|
+
notifyListeners(DISPLAY_MODE_CHANGE, statuses.toJSObject())
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override fun handleOnDestroy() {
|
|
76
|
+
displayModes?.stopRearDisplay()
|
|
77
|
+
displayModes?.stopDualScreen()
|
|
78
|
+
scope.cancel()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@PluginMethod(returnType = PluginMethod.RETURN_NONE)
|
|
82
|
+
override fun addListener(call: PluginCall) {
|
|
83
|
+
super.addListener(call)
|
|
84
|
+
scope.launch { updateHingeAngleUpdates() }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@PluginMethod(returnType = PluginMethod.RETURN_NONE)
|
|
88
|
+
override fun removeListener(call: PluginCall) {
|
|
89
|
+
super.removeListener(call)
|
|
90
|
+
scope.launch { updateHingeAngleUpdates() }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@PluginMethod
|
|
94
|
+
override fun removeAllListeners(call: PluginCall) {
|
|
95
|
+
super.removeAllListeners(call)
|
|
96
|
+
scope.launch { updateHingeAngleUpdates() }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@PluginMethod
|
|
100
|
+
fun isDeviceFoldable(call: PluginCall) {
|
|
101
|
+
val implementation = this.implementation
|
|
102
|
+
call.resolve(
|
|
103
|
+
JSObject()
|
|
104
|
+
.put("foldable", implementation?.isDeviceFoldable() ?: false)
|
|
105
|
+
.put("supportsTabletop", implementation?.supportsTabletop() ?: false)
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@PluginMethod
|
|
110
|
+
fun getFoldState(call: PluginCall) {
|
|
111
|
+
lastKnownState?.let {
|
|
112
|
+
call.resolve(it.toJSObject())
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
val implementation = this.implementation
|
|
117
|
+
if (implementation == null) {
|
|
118
|
+
call.resolve(FoldState.FLAT.toJSObject())
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
scope.launch {
|
|
123
|
+
val state = withTimeoutOrNull(FIRST_EMISSION_TIMEOUT_MS) {
|
|
124
|
+
implementation.foldStates().first()
|
|
125
|
+
} ?: FoldState.FLAT
|
|
126
|
+
lastKnownState = state
|
|
127
|
+
call.resolve(state.toJSObject())
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@PluginMethod
|
|
132
|
+
fun getHingeAngle(call: PluginCall) {
|
|
133
|
+
lastKnownAngle?.let {
|
|
134
|
+
call.resolve(angleResult(it))
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
val implementation = this.implementation
|
|
139
|
+
if (implementation == null || !implementation.hasHingeSensor()) {
|
|
140
|
+
call.resolve(angleResult(null))
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
scope.launch {
|
|
145
|
+
val angle = withTimeoutOrNull(FIRST_EMISSION_TIMEOUT_MS) {
|
|
146
|
+
implementation.hingeAngles().firstOrNull()
|
|
147
|
+
}
|
|
148
|
+
call.resolve(angleResult(angle))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@PluginMethod
|
|
153
|
+
fun getSizeClass(call: PluginCall) {
|
|
154
|
+
val sizeClass = implementation?.sizeClass() ?: sizeClassOf(0f, 0f)
|
|
155
|
+
call.resolve(sizeClass.toJSObject())
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@PluginMethod
|
|
159
|
+
fun getBarPlacement(call: PluginCall) {
|
|
160
|
+
call.resolve(JSObject().put("verticalBarEdge", JSONObject.NULL))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@PluginMethod
|
|
164
|
+
fun getDisplayModes(call: PluginCall) {
|
|
165
|
+
lastDisplayModes?.let {
|
|
166
|
+
call.resolve(it.toJSObject())
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
val displayModes = this.displayModes ?: return call.resolve(DisplayModeStatuses.UNSUPPORTED.toJSObject())
|
|
171
|
+
|
|
172
|
+
scope.launch {
|
|
173
|
+
val statuses = withTimeoutOrNull(FIRST_EMISSION_TIMEOUT_MS) {
|
|
174
|
+
displayModes.statuses().first()
|
|
175
|
+
} ?: DisplayModeStatuses.UNSUPPORTED
|
|
176
|
+
call.resolve(statuses.toJSObject())
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@PluginMethod
|
|
181
|
+
fun startRearDisplay(call: PluginCall) {
|
|
182
|
+
val displayModes = this.displayModes ?: return call.unavailable("Rear display mode needs an activity.")
|
|
183
|
+
|
|
184
|
+
scope.launch {
|
|
185
|
+
when (lastDisplayModes?.rearDisplay) {
|
|
186
|
+
"active" -> call.resolve()
|
|
187
|
+
"available" -> {
|
|
188
|
+
val settle = Settle(call, "Rear display mode")
|
|
189
|
+
displayModes.startRearDisplay(settle::started, settle::ended)
|
|
190
|
+
}
|
|
191
|
+
else -> call.unavailable("Rear display mode is not available on this device right now.")
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@PluginMethod
|
|
197
|
+
fun stopRearDisplay(call: PluginCall) {
|
|
198
|
+
scope.launch {
|
|
199
|
+
displayModes?.stopRearDisplay()
|
|
200
|
+
call.resolve()
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@PluginMethod
|
|
205
|
+
fun startDualScreen(call: PluginCall) {
|
|
206
|
+
val url = call.getString("url") ?: return call.reject("url is required.")
|
|
207
|
+
val displayModes = this.displayModes ?: return call.unavailable("Dual-screen mode needs an activity.")
|
|
208
|
+
|
|
209
|
+
scope.launch {
|
|
210
|
+
when (lastDisplayModes?.dualScreen) {
|
|
211
|
+
"available", "active" -> {
|
|
212
|
+
val settle = Settle(call, "Dual-screen mode")
|
|
213
|
+
displayModes.startDualScreen({ context -> dualScreenWebView(context, url) }, settle::started, settle::ended)
|
|
214
|
+
}
|
|
215
|
+
else -> call.unavailable("Dual-screen mode is not available on this device right now.")
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@PluginMethod
|
|
221
|
+
fun stopDualScreen(call: PluginCall) {
|
|
222
|
+
scope.launch {
|
|
223
|
+
displayModes?.stopDualScreen()
|
|
224
|
+
call.resolve()
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private fun notifySizeClassIfChanged() {
|
|
229
|
+
val sizeClass = implementation?.sizeClass() ?: return
|
|
230
|
+
if (sizeClass == lastSizeClass) return
|
|
231
|
+
|
|
232
|
+
lastSizeClass = sizeClass
|
|
233
|
+
notifyListeners(SIZE_CLASS_CHANGE, sizeClass.toJSObject())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private fun updateHingeAngleUpdates() {
|
|
237
|
+
if (!hasListeners(HINGE_ANGLE_CHANGE)) {
|
|
238
|
+
hingeAngleJob?.cancel()
|
|
239
|
+
hingeAngleJob = null
|
|
240
|
+
lastKnownAngle = null
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if (hingeAngleJob != null) return
|
|
244
|
+
|
|
245
|
+
val implementation = this.implementation ?: return
|
|
246
|
+
val owner = activity as? LifecycleOwner ?: return
|
|
247
|
+
hingeAngleJob = scope.launch {
|
|
248
|
+
owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
249
|
+
try {
|
|
250
|
+
implementation.hingeAngles().conflate().collect { angle ->
|
|
251
|
+
lastKnownAngle = angle
|
|
252
|
+
notifyListeners(HINGE_ANGLE_CHANGE, angleResult(angle))
|
|
253
|
+
delay(HINGE_ANGLE_FRAME_MS)
|
|
254
|
+
}
|
|
255
|
+
} finally {
|
|
256
|
+
lastKnownAngle = null
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@SuppressLint("SetJavaScriptEnabled")
|
|
263
|
+
private fun dualScreenWebView(context: Context, url: String): WebView = WebView(context).apply {
|
|
264
|
+
settings.javaScriptEnabled = true
|
|
265
|
+
settings.domStorageEnabled = true
|
|
266
|
+
webViewClient = object : WebViewClient() {
|
|
267
|
+
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? =
|
|
268
|
+
bridge.localServer.shouldInterceptRequest(request)
|
|
269
|
+
}
|
|
270
|
+
loadUrl(resolveUrl(url))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private fun resolveUrl(url: String): String =
|
|
274
|
+
if (Uri.parse(url).scheme != null) url else URL(URL(bridge.appUrl), url).toString()
|
|
275
|
+
|
|
276
|
+
private inner class Settle(private val call: PluginCall, private val mode: String) {
|
|
277
|
+
private var settled = false
|
|
278
|
+
|
|
279
|
+
fun started() {
|
|
280
|
+
if (settled) return
|
|
281
|
+
settled = true
|
|
282
|
+
call.resolve()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fun ended(error: Throwable?) {
|
|
286
|
+
if (settled) return
|
|
287
|
+
settled = true
|
|
288
|
+
call.reject(error?.message ?: "$mode ended before it started.")
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun angleResult(angle: Float?): JSObject =
|
|
293
|
+
JSObject().put("angle", angle?.toDouble() ?: JSONObject.NULL)
|
|
294
|
+
|
|
295
|
+
private fun FoldState.toJSObject(): JSObject = JSObject().apply {
|
|
296
|
+
put("state", state)
|
|
297
|
+
put("isSeparating", isSeparating)
|
|
298
|
+
put("posture", posture)
|
|
299
|
+
hingeOrientation?.let { put("hingeOrientation", it) }
|
|
300
|
+
hingeBounds?.let { put("hingeBounds", it.toJSObject()) }
|
|
301
|
+
occludedBounds?.let { put("occludedBounds", it.toJSObject()) }
|
|
302
|
+
if (cameraBounds.isNotEmpty()) {
|
|
303
|
+
put("cameraBounds", JSArray().apply { cameraBounds.forEach { put(it.toJSObject()) } })
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private fun SizeClass.toJSObject(): JSObject = JSObject().apply {
|
|
308
|
+
put("horizontal", horizontal)
|
|
309
|
+
put("vertical", vertical)
|
|
310
|
+
put("widthClass", widthClass)
|
|
311
|
+
put("heightClass", heightClass)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private fun DisplayModeStatuses.toJSObject(): JSObject = JSObject().apply {
|
|
315
|
+
put("rearDisplay", rearDisplay)
|
|
316
|
+
put("dualScreen", dualScreen)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private fun FoldBounds.toJSObject(): JSObject = JSObject().apply {
|
|
320
|
+
put("x", x)
|
|
321
|
+
put("y", y)
|
|
322
|
+
put("width", width)
|
|
323
|
+
put("height", height)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private companion object {
|
|
327
|
+
const val FIRST_EMISSION_TIMEOUT_MS = 1_000L
|
|
328
|
+
const val HINGE_ANGLE_CHANGE = "hingeAngleChange"
|
|
329
|
+
const val HINGE_ANGLE_FRAME_MS = 16L
|
|
330
|
+
const val SIZE_CLASS_CHANGE = "sizeClassChange"
|
|
331
|
+
const val DISPLAY_MODE_CHANGE = "displayModeChange"
|
|
332
|
+
}
|
|
333
|
+
}
|
|
File without changes
|