1 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 if (Build.VERSION.SDK_INT < 23) {
127         "Android"
128       } else {
129         Build.VERSION.BASE_OS.takeIf { it.isNotEmpty() } ?: "Android"
130       }
131     }
132 
133   companion object {
134     private val isRunningOnEmulator: Boolean
135       get() = EmulatorUtilities.isRunningOnEmulator()
136 
137     private fun getDeviceType(context: Context): DeviceType {
138       // Detect TVs via UI mode (Android TVs) or system features (Fire TV).
139       if (context.applicationContext.packageManager.hasSystemFeature("amazon.hardware.fire_tv")) {
140         return DeviceType.TV
141       }
142 
143       val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
144       if (uiManager != null && uiManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
145         return DeviceType.TV
146       }
147 
148       val deviceTypeFromResourceConfiguration = getDeviceTypeFromResourceConfiguration(context)
149       return if (deviceTypeFromResourceConfiguration != DeviceType.UNKNOWN) {
150         deviceTypeFromResourceConfiguration
151       } else {
152         getDeviceTypeFromPhysicalSize(context)
153       }
154     }
155 
156     // Device type based on the smallest screen width quantifier
157     // https://developer.android.com/guide/topics/resources/providing-resources#SmallestScreenWidthQualifier
158     private fun getDeviceTypeFromResourceConfiguration(context: Context): DeviceType {
159       val smallestScreenWidthDp = context.resources.configuration.smallestScreenWidthDp
160 
161       return if (smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) {
162         DeviceType.UNKNOWN
163       } else if (smallestScreenWidthDp >= 600) {
164         DeviceType.TABLET
165       } else {
166         DeviceType.PHONE
167       }
168     }
169 
170     private fun getDeviceTypeFromPhysicalSize(context: Context): DeviceType {
171       // Find the current window manager, if none is found we can't measure the device physical size.
172       val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager?
173         ?: return DeviceType.UNKNOWN
174 
175       // Get display metrics to see if we can differentiate phones and tablets.
176       val widthInches: Double
177       val heightInches: Double
178 
179       // windowManager.defaultDisplay was marked as deprecated in API level 30 (Android R) and above
180       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
181         val windowBounds = windowManager.currentWindowMetrics.bounds
182         val densityDpi = context.resources.configuration.densityDpi
183         widthInches = windowBounds.width() / densityDpi.toDouble()
184         heightInches = windowBounds.height() / densityDpi.toDouble()
185       } else {
186         val metrics = DisplayMetrics()
187         @Suppress("DEPRECATION")
188         windowManager.defaultDisplay.getRealMetrics(metrics)
189         widthInches = metrics.widthPixels / metrics.xdpi.toDouble()
190         heightInches = metrics.heightPixels / metrics.ydpi.toDouble()
191       }
192 
193       // Calculate physical size.
194       val diagonalSizeInches = sqrt(widthInches.pow(2.0) + heightInches.pow(2.0))
195 
196       return if (diagonalSizeInches in 3.0..6.9) {
197         // Devices in a sane range for phones are considered to be phones.
198         DeviceType.PHONE
199       } else if (diagonalSizeInches > 6.9 && diagonalSizeInches <= 18.0) {
200         // Devices larger than a phone and in a sane range for tablets are tablets.
201         DeviceType.TABLET
202       } else {
203         // Otherwise, we don't know what device type we're on.
204         DeviceType.UNKNOWN
205       }
206     }
207   }
208 }
209