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 private const val NAME = "ExpoDevice"
22 
23 class DeviceModule : Module() {
24   // Keep this enum in sync with JavaScript
25   enum class DeviceType(val JSValue: Int) {
26     UNKNOWN(0),
27     PHONE(1),
28     TABLET(2),
29     DESKTOP(3),
30     TV(4);
31   }
32 
33   private val context: Context
34     get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
35 
36   override fun definition() = ModuleDefinition {
37     Name("ExpoDevice")
38 
39     Constants {
40       return@Constants mapOf(
41         "isDevice" to !isRunningOnEmulator,
42         "brand" to Build.BRAND,
43         "manufacturer" to Build.MANUFACTURER,
44         "modelName" to Build.MODEL,
45         "designName" to Build.DEVICE,
46         "productName" to Build.DEVICE,
47         "deviceYearClass" to deviceYearClass,
48         "totalMemory" to run {
49           val memoryInfo = ActivityManager.MemoryInfo()
50           (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
51           memoryInfo.totalMem
52         },
53         "supportedCpuArchitectures" to Build.SUPPORTED_ABIS?.takeIf { it.isNotEmpty() },
54         "osName" to systemName,
55         "osVersion" to Build.VERSION.RELEASE,
56         "osBuildId" to Build.DISPLAY,
57         "osInternalBuildId" to Build.ID,
58         "osBuildFingerprint" to Build.FINGERPRINT,
59         "platformApiLevel" to Build.VERSION.SDK_INT,
60         "deviceName" to if (Build.VERSION.SDK_INT <= 31)
61           Settings.Secure.getString(context.contentResolver, "bluetooth_name")
62         else
63           Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME)
64       )
65     }
66 
67     AsyncFunction("getDeviceTypeAsync") {
68       return@AsyncFunction getDeviceType(context).JSValue
69     }
70 
71     AsyncFunction("getUptimeAsync") {
72       return@AsyncFunction SystemClock.uptimeMillis().toDouble()
73     }
74 
75     AsyncFunction("getMaxMemoryAsync") {
76       val maxMemory = Runtime.getRuntime().maxMemory()
77       return@AsyncFunction if (maxMemory != Long.MAX_VALUE) maxMemory.toDouble() else -1
78     }
79 
80     AsyncFunction("isRootedExperimentalAsync") {
81       val isRooted: Boolean
82       val isDevice = !isRunningOnEmulator
83 
84       val buildTags = Build.TAGS
85       isRooted = if (isDevice && buildTags != null && buildTags.contains("test-keys")) {
86         true
87       } else {
88         if (File("/system/app/Superuser.apk").exists()) {
89           true
90         } else {
91           isDevice && File("/system/xbin/su").exists()
92         }
93       }
94 
95       return@AsyncFunction isRooted
96     }
97 
98     AsyncFunction("isSideLoadingEnabledAsync") {
99       return@AsyncFunction if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
100         Settings.Global.getInt(
101           context.applicationContext.contentResolver,
102           Settings.Global.INSTALL_NON_MARKET_APPS,
103           0
104         ) == 1
105       } else {
106         context.applicationContext.packageManager.canRequestPackageInstalls()
107       }
108     }
109 
110     AsyncFunction("getPlatformFeaturesAsync") {
111       val allFeatures = context.applicationContext.packageManager.systemAvailableFeatures
112       return@AsyncFunction allFeatures.filterNotNull().map { it.name }
113     }
114 
115     AsyncFunction("hasPlatformFeatureAsync") { feature: String ->
116       return@AsyncFunction context.applicationContext.packageManager.hasSystemFeature(feature)
117     }
118   }
119 
120   private val deviceYearClass: Int
121     get() = YearClass.get(context)
122 
123   private val systemName: String
124     get() {
125       return if (Build.VERSION.SDK_INT < 23) {
126         "Android"
127       } else {
128         Build.VERSION.BASE_OS.takeIf { it.isNotEmpty() } ?: "Android"
129       }
130     }
131 
132   companion object {
133     private val TAG = DeviceModule::class.java.simpleName
134 
135     private val isRunningOnEmulator: Boolean
136       get() = EmulatorUtilities.isRunningOnEmulator()
137 
138     private fun getDeviceType(context: Context): DeviceType {
139       // Detect TVs via UI mode (Android TVs) or system features (Fire TV).
140       if (context.applicationContext.packageManager.hasSystemFeature("amazon.hardware.fire_tv")) {
141         return DeviceType.TV
142       }
143 
144       val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
145       if (uiManager != null && uiManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
146         return DeviceType.TV
147       }
148 
149       // Find the current window manager, if none is found we can't measure the device physical size.
150       val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager?
151         ?: return DeviceType.UNKNOWN
152 
153       // Get display metrics to see if we can differentiate phones and tablets.
154       val metrics = DisplayMetrics()
155       windowManager.defaultDisplay.getMetrics(metrics)
156 
157       // Calculate physical size.
158       val widthInches = metrics.widthPixels / metrics.xdpi.toDouble()
159       val heightInches = metrics.heightPixels / metrics.ydpi.toDouble()
160       val diagonalSizeInches = sqrt(widthInches.pow(2.0) + heightInches.pow(2.0))
161       return if (diagonalSizeInches in 3.0..6.9) {
162         // Devices in a sane range for phones are considered to be phones.
163         DeviceType.PHONE
164       } else if (diagonalSizeInches > 6.9 && diagonalSizeInches <= 18.0) {
165         // Devices larger than a phone and in a sane range for tablets are tablets.
166         DeviceType.TABLET
167       } else {
168         // Otherwise, we don't know what device type we're on.
169         DeviceType.UNKNOWN
170       }
171     }
172   }
173 }
174