<lambda>null1 package expo.modules.device
2
3 import android.app.ActivityManager
4 import android.app.UiModeManager
5 import android.content.Context
6 import android.content.res.Configuration
7 import android.os.Build
8 import android.os.SystemClock
9 import android.provider.Settings
10 import android.util.DisplayMetrics
11 import android.view.WindowManager
12 import com.facebook.device.yearclass.YearClass
13 import expo.modules.core.utilities.EmulatorUtilities
14 import expo.modules.kotlin.exception.Exceptions
15 import expo.modules.kotlin.modules.Module
16 import expo.modules.kotlin.modules.ModuleDefinition
17 import java.io.File
18 import kotlin.math.pow
19 import kotlin.math.sqrt
20
21 class DeviceModule : Module() {
22 // Keep this enum in sync with JavaScript
23 enum class DeviceType(val JSValue: Int) {
24 UNKNOWN(0),
25 PHONE(1),
26 TABLET(2),
27 DESKTOP(3),
28 TV(4);
29 }
30
31 private val context: Context
32 get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
33
34 override fun definition() = ModuleDefinition {
35 Name("ExpoDevice")
36
37 Constants {
38 return@Constants mapOf(
39 "isDevice" to !isRunningOnEmulator,
40 "brand" to Build.BRAND,
41 "manufacturer" to Build.MANUFACTURER,
42 "modelName" to Build.MODEL,
43 "designName" to Build.DEVICE,
44 "productName" to Build.DEVICE,
45 "deviceYearClass" to deviceYearClass,
46 "totalMemory" to run {
47 val memoryInfo = ActivityManager.MemoryInfo()
48 (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
49 memoryInfo.totalMem
50 },
51 "deviceType" to run {
52 getDeviceType(context).JSValue
53 },
54 "supportedCpuArchitectures" to Build.SUPPORTED_ABIS?.takeIf { it.isNotEmpty() },
55 "osName" to systemName,
56 "osVersion" to Build.VERSION.RELEASE,
57 "osBuildId" to Build.DISPLAY,
58 "osInternalBuildId" to Build.ID,
59 "osBuildFingerprint" to Build.FINGERPRINT,
60 "platformApiLevel" to Build.VERSION.SDK_INT,
61 "deviceName" to if (Build.VERSION.SDK_INT <= 31)
62 Settings.Secure.getString(context.contentResolver, "bluetooth_name")
63 else
64 Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME)
65 )
66 }
67
68 AsyncFunction("getDeviceTypeAsync") {
69 return@AsyncFunction getDeviceType(context).JSValue
70 }
71
72 AsyncFunction("getUptimeAsync") {
73 return@AsyncFunction SystemClock.uptimeMillis().toDouble()
74 }
75
76 AsyncFunction("getMaxMemoryAsync") {
77 val maxMemory = Runtime.getRuntime().maxMemory()
78 return@AsyncFunction if (maxMemory != Long.MAX_VALUE) maxMemory.toDouble() else -1
79 }
80
81 AsyncFunction("isRootedExperimentalAsync") {
82 val isRooted: Boolean
83 val isDevice = !isRunningOnEmulator
84
85 val buildTags = Build.TAGS
86 isRooted = if (isDevice && buildTags != null && buildTags.contains("test-keys")) {
87 true
88 } else {
89 if (File("/system/app/Superuser.apk").exists()) {
90 true
91 } else {
92 isDevice && File("/system/xbin/su").exists()
93 }
94 }
95
96 return@AsyncFunction isRooted
97 }
98
99 AsyncFunction("isSideLoadingEnabledAsync") {
100 return@AsyncFunction if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
101 Settings.Global.getInt(
102 context.applicationContext.contentResolver,
103 Settings.Global.INSTALL_NON_MARKET_APPS,
104 0
105 ) == 1
106 } else {
107 context.applicationContext.packageManager.canRequestPackageInstalls()
108 }
109 }
110
111 AsyncFunction("getPlatformFeaturesAsync") {
112 val allFeatures = context.applicationContext.packageManager.systemAvailableFeatures
113 return@AsyncFunction allFeatures.filterNotNull().map { it.name }
114 }
115
116 AsyncFunction("hasPlatformFeatureAsync") { feature: String ->
117 return@AsyncFunction context.applicationContext.packageManager.hasSystemFeature(feature)
118 }
119 }
120
121 private val deviceYearClass: Int
122 get() = YearClass.get(context)
123
124 private val systemName: String
125 get() {
126 return Build.VERSION.BASE_OS.takeIf { it.isNotEmpty() } ?: "Android"
127 }
128
129 companion object {
130 private val isRunningOnEmulator: Boolean
131 get() = EmulatorUtilities.isRunningOnEmulator()
132
133 private fun getDeviceType(context: Context): DeviceType {
134 // Detect TVs via UI mode (Android TVs) or system features (Fire TV).
135 if (context.applicationContext.packageManager.hasSystemFeature("amazon.hardware.fire_tv")) {
136 return DeviceType.TV
137 }
138
139 val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
140 if (uiManager != null && uiManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
141 return DeviceType.TV
142 }
143
144 val deviceTypeFromResourceConfiguration = getDeviceTypeFromResourceConfiguration(context)
145 return if (deviceTypeFromResourceConfiguration != DeviceType.UNKNOWN) {
146 deviceTypeFromResourceConfiguration
147 } else {
148 getDeviceTypeFromPhysicalSize(context)
149 }
150 }
151
152 // Device type based on the smallest screen width quantifier
153 // https://developer.android.com/guide/topics/resources/providing-resources#SmallestScreenWidthQualifier
154 private fun getDeviceTypeFromResourceConfiguration(context: Context): DeviceType {
155 val smallestScreenWidthDp = context.resources.configuration.smallestScreenWidthDp
156
157 return if (smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) {
158 DeviceType.UNKNOWN
159 } else if (smallestScreenWidthDp >= 600) {
160 DeviceType.TABLET
161 } else {
162 DeviceType.PHONE
163 }
164 }
165
166 private fun getDeviceTypeFromPhysicalSize(context: Context): DeviceType {
167 // Find the current window manager, if none is found we can't measure the device physical size.
168 val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager?
169 ?: return DeviceType.UNKNOWN
170
171 // Get display metrics to see if we can differentiate phones and tablets.
172 val widthInches: Double
173 val heightInches: Double
174
175 // windowManager.defaultDisplay was marked as deprecated in API level 30 (Android R) and above
176 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
177 val windowBounds = windowManager.currentWindowMetrics.bounds
178 val densityDpi = context.resources.configuration.densityDpi
179 widthInches = windowBounds.width() / densityDpi.toDouble()
180 heightInches = windowBounds.height() / densityDpi.toDouble()
181 } else {
182 val metrics = DisplayMetrics()
183 @Suppress("DEPRECATION")
184 windowManager.defaultDisplay.getRealMetrics(metrics)
185 widthInches = metrics.widthPixels / metrics.xdpi.toDouble()
186 heightInches = metrics.heightPixels / metrics.ydpi.toDouble()
187 }
188
189 // Calculate physical size.
190 val diagonalSizeInches = sqrt(widthInches.pow(2.0) + heightInches.pow(2.0))
191
192 return if (diagonalSizeInches in 3.0..6.9) {
193 // Devices in a sane range for phones are considered to be phones.
194 DeviceType.PHONE
195 } else if (diagonalSizeInches > 6.9 && diagonalSizeInches <= 18.0) {
196 // Devices larger than a phone and in a sane range for tablets are tablets.
197 DeviceType.TABLET
198 } else {
199 // Otherwise, we don't know what device type we're on.
200 DeviceType.UNKNOWN
201 }
202 }
203 }
204 }
205