1 package expo.modules.device
2 
3 import expo.modules.core.ExportedModule
4 import expo.modules.core.Promise
5 import expo.modules.core.interfaces.ExpoMethod
6 import expo.modules.core.utilities.EmulatorUtilities
7 
8 import com.facebook.device.yearclass.YearClass
9 
10 import android.app.ActivityManager
11 import android.app.UiModeManager
12 import android.content.Context
13 import android.content.res.Configuration
14 import android.os.Build
15 import android.os.SystemClock
16 import android.provider.Settings
17 import android.view.WindowManager
18 import android.util.DisplayMetrics
19 
20 import java.io.File
21 import kotlin.math.pow
22 import kotlin.math.sqrt
23 
24 private const val NAME = "ExpoDevice"
25 
26 class DeviceModule(private val mContext: Context) : ExportedModule(mContext) {
27   // Keep this enum in sync with JavaScript
28   enum class DeviceType(val JSValue: Int) {
29     UNKNOWN(0),
30     PHONE(1),
31     TABLET(2),
32     DESKTOP(3),
33     TV(4);
34   }
35 
36   override fun getName(): String {
37     return NAME
38   }
39 
40   override fun getConstants(): Map<String, Any> = 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       (mContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
51       memoryInfo.totalMem
52     },
53     "supportedCpuArchitectures" to run {
54       var supportedAbis = Build.SUPPORTED_ABIS
55       if (supportedAbis != null && supportedAbis.isEmpty()) {
56         supportedAbis = null
57       }
58       supportedAbis
59     },
60     "osName" to systemName,
61     "osVersion" to Build.VERSION.RELEASE,
62     "osBuildId" to Build.DISPLAY,
63     "osInternalBuildId" to Build.ID,
64     "osBuildFingerprint" to Build.FINGERPRINT,
65     "platformApiLevel" to Build.VERSION.SDK_INT,
66     "deviceName" to Settings.Secure.getString(mContext.contentResolver, "bluetooth_name")
67   )
68 
69   private val deviceYearClass: Int
70     get() = YearClass.get(mContext)
71 
72   private val systemName: String
73     get() {
74       return if (Build.VERSION.SDK_INT < 23) {
75         "Android"
76       } else {
77         Build.VERSION.BASE_OS.takeIf { it.isNotEmpty() } ?: "Android"
78       }
79     }
80 
81   @ExpoMethod
82   fun getDeviceTypeAsync(promise: Promise) {
83     promise.resolve(getDeviceType(mContext).JSValue)
84   }
85 
86   @ExpoMethod
87   fun getUptimeAsync(promise: Promise) {
88     promise.resolve(SystemClock.uptimeMillis().toDouble())
89   }
90 
91   @ExpoMethod
92   fun getMaxMemoryAsync(promise: Promise) {
93     val maxMemory = Runtime.getRuntime().maxMemory()
94     promise.resolve(if (maxMemory != Long.MAX_VALUE) maxMemory.toDouble() else -1)
95   }
96 
97   @ExpoMethod
98   fun isRootedExperimentalAsync(promise: Promise) {
99     var isRooted = false
100     val isDevice = !isRunningOnEmulator
101 
102     try {
103       val buildTags = Build.TAGS
104       isRooted = if (isDevice && buildTags != null && buildTags.contains("test-keys")) {
105         true
106       } else {
107         if (File("/system/app/Superuser.apk").exists()) {
108           true
109         } else {
110           isDevice && File("/system/xbin/su").exists()
111         }
112       }
113     } catch (se: SecurityException) {
114       promise.reject(
115         "ERR_DEVICE_ROOT_DETECTION",
116         "Could not access the file system to determine if the device is rooted.",
117         se
118       )
119       return
120     }
121 
122     promise.resolve(isRooted)
123   }
124 
125   @ExpoMethod
126   fun isSideLoadingEnabledAsync(promise: Promise) {
127     val enabled: Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
128       Settings.Global.getInt(
129         mContext.applicationContext.contentResolver,
130         Settings.Global.INSTALL_NON_MARKET_APPS,
131         0
132       ) == 1
133     } else {
134       mContext.applicationContext.packageManager.canRequestPackageInstalls()
135     }
136 
137     promise.resolve(enabled)
138   }
139 
140   @ExpoMethod
141   fun getPlatformFeaturesAsync(promise: Promise) {
142     val allFeatures = mContext.applicationContext.packageManager.systemAvailableFeatures
143     val featureList = allFeatures.filterNotNull().map { it.name }
144     promise.resolve(featureList)
145   }
146 
147   @ExpoMethod
148   fun hasPlatformFeatureAsync(feature: String, promise: Promise) {
149     promise.resolve(mContext.applicationContext.packageManager.hasSystemFeature(feature))
150   }
151 
152   companion object {
153     private val TAG = DeviceModule::class.java.simpleName
154 
155     private val isRunningOnEmulator: Boolean
156       get() = EmulatorUtilities.isRunningOnEmulator()
157 
158     private fun getDeviceType(context: Context): DeviceType {
159       // Detect TVs via UI mode (Android TVs) or system features (Fire TV).
160       if (context.applicationContext.packageManager.hasSystemFeature("amazon.hardware.fire_tv")) {
161         return DeviceType.TV
162       }
163 
164       val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
165       if (uiManager != null && uiManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
166         return DeviceType.TV
167       }
168 
169       // Find the current window manager, if none is found we can't measure the device physical size.
170       val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager? ?: return DeviceType.UNKNOWN
171 
172       // Get display metrics to see if we can differentiate phones and tablets.
173       val metrics = DisplayMetrics()
174       windowManager.defaultDisplay.getMetrics(metrics)
175 
176       // Calculate physical size.
177       val widthInches = metrics.widthPixels / metrics.xdpi.toDouble()
178       val heightInches = metrics.heightPixels / metrics.ydpi.toDouble()
179       val diagonalSizeInches = sqrt(widthInches.pow(2.0) + heightInches.pow(2.0))
180       return if (diagonalSizeInches >= 3.0 && diagonalSizeInches <= 6.9) {
181         // Devices in a sane range for phones are considered to be phones.
182         DeviceType.PHONE
183       } else if (diagonalSizeInches > 6.9 && diagonalSizeInches <= 18.0) {
184         // Devices larger than a phone and in a sane range for tablets are tablets.
185         DeviceType.TABLET
186       } else {
187         // Otherwise, we don't know what device type we're on.
188         DeviceType.UNKNOWN
189       }
190     }
191   }
192 }
193