<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