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         "supportedCpuArchitectures" to Build.SUPPORTED_ABIS?.takeIf { it.isNotEmpty() },
52         "osName" to systemName,
53         "osVersion" to Build.VERSION.RELEASE,
54         "osBuildId" to Build.DISPLAY,
55         "osInternalBuildId" to Build.ID,
56         "osBuildFingerprint" to Build.FINGERPRINT,
57         "platformApiLevel" to Build.VERSION.SDK_INT,
58         "deviceName" to if (Build.VERSION.SDK_INT <= 31)
59           Settings.Secure.getString(context.contentResolver, "bluetooth_name")
60         else
61           Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME)
62       )
63     }
64 
65     AsyncFunction("getDeviceTypeAsync") {
66       return@AsyncFunction getDeviceType(context).JSValue
67     }
68 
69     AsyncFunction("getUptimeAsync") {
70       return@AsyncFunction SystemClock.uptimeMillis().toDouble()
71     }
72 
73     AsyncFunction("getMaxMemoryAsync") {
74       val maxMemory = Runtime.getRuntime().maxMemory()
75       return@AsyncFunction if (maxMemory != Long.MAX_VALUE) maxMemory.toDouble() else -1
76     }
77 
78     AsyncFunction("isRootedExperimentalAsync") {
79       val isRooted: Boolean
80       val isDevice = !isRunningOnEmulator
81 
82       val buildTags = Build.TAGS
83       isRooted = if (isDevice && buildTags != null && buildTags.contains("test-keys")) {
84         true
85       } else {
86         if (File("/system/app/Superuser.apk").exists()) {
87           true
88         } else {
89           isDevice && File("/system/xbin/su").exists()
90         }
91       }
92 
93       return@AsyncFunction isRooted
94     }
95 
96     AsyncFunction("isSideLoadingEnabledAsync") {
97       return@AsyncFunction if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
98         Settings.Global.getInt(
99           context.applicationContext.contentResolver,
100           Settings.Global.INSTALL_NON_MARKET_APPS,
101           0
102         ) == 1
103       } else {
104         context.applicationContext.packageManager.canRequestPackageInstalls()
105       }
106     }
107 
108     AsyncFunction("getPlatformFeaturesAsync") {
109       val allFeatures = context.applicationContext.packageManager.systemAvailableFeatures
110       return@AsyncFunction allFeatures.filterNotNull().map { it.name }
111     }
112 
113     AsyncFunction("hasPlatformFeatureAsync") { feature: String ->
114       return@AsyncFunction context.applicationContext.packageManager.hasSystemFeature(feature)
115     }
116   }
117 
118   private val deviceYearClass: Int
119     get() = YearClass.get(context)
120 
121   private val systemName: String
122     get() {
123       return if (Build.VERSION.SDK_INT < 23) {
124         "Android"
125       } else {
126         Build.VERSION.BASE_OS.takeIf { it.isNotEmpty() } ?: "Android"
127       }
128     }
129 
130   companion object {
131     private val isRunningOnEmulator: Boolean
132       get() = EmulatorUtilities.isRunningOnEmulator()
133 
134     private fun getDeviceType(context: Context): DeviceType {
135       // Detect TVs via UI mode (Android TVs) or system features (Fire TV).
136       if (context.applicationContext.packageManager.hasSystemFeature("amazon.hardware.fire_tv")) {
137         return DeviceType.TV
138       }
139 
140       val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
141       if (uiManager != null && uiManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
142         return DeviceType.TV
143       }
144 
145       val deviceTypeFromResourceConfiguration = getDeviceTypeFromResourceConfiguration(context)
146       return if (deviceTypeFromResourceConfiguration != DeviceType.UNKNOWN) {
147         deviceTypeFromResourceConfiguration
148       } else {
149         getDeviceTypeFromPhysicalSize(context)
150       }
151     }
152 
153     // Device type based on the smallest screen width quantifier
154     // https://developer.android.com/guide/topics/resources/providing-resources#SmallestScreenWidthQualifier
155     private fun getDeviceTypeFromResourceConfiguration(context: Context): DeviceType {
156       val smallestScreenWidthDp = context.resources.configuration.smallestScreenWidthDp
157 
158       return if (smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) {
159         DeviceType.UNKNOWN
160       } else if (smallestScreenWidthDp >= 600) {
161         DeviceType.TABLET
162       } else {
163         DeviceType.PHONE
164       }
165     }
166 
167     private fun getDeviceTypeFromPhysicalSize(context: Context): DeviceType {
168       // Find the current window manager, if none is found we can't measure the device physical size.
169       val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager?
170         ?: return DeviceType.UNKNOWN
171 
172       // Get display metrics to see if we can differentiate phones and tablets.
173       val widthInches: Double
174       val heightInches: Double
175 
176       // windowManager.defaultDisplay was marked as deprecated in API level 30 (Android R) and above
177       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
178         val windowBounds = windowManager.currentWindowMetrics.bounds
179         val densityDpi = context.resources.configuration.densityDpi
180         widthInches = windowBounds.width() / densityDpi.toDouble()
181         heightInches = windowBounds.height() / densityDpi.toDouble()
182       } else {
183         val metrics = DisplayMetrics()
184         @Suppress("DEPRECATION")
185         windowManager.defaultDisplay.getRealMetrics(metrics)
186         widthInches = metrics.widthPixels / metrics.xdpi.toDouble()
187         heightInches = metrics.heightPixels / metrics.ydpi.toDouble()
188       }
189 
190       // Calculate physical size.
191       val diagonalSizeInches = sqrt(widthInches.pow(2.0) + heightInches.pow(2.0))
192 
193       return if (diagonalSizeInches in 3.0..6.9) {
194         // Devices in a sane range for phones are considered to be phones.
195         DeviceType.PHONE
196       } else if (diagonalSizeInches > 6.9 && diagonalSizeInches <= 18.0) {
197         // Devices larger than a phone and in a sane range for tablets are tablets.
198         DeviceType.TABLET
199       } else {
200         // Otherwise, we don't know what device type we're on.
201         DeviceType.UNKNOWN
202       }
203     }
204   }
205 }
206