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 run {
67       if (Build.VERSION.SDK_INT <= 31)
68         Settings.Secure.getString(mContext.contentResolver, "bluetooth_name")
69       else
70         Settings.Global.getString(mContext.contentResolver, Settings.Global.DEVICE_NAME)
71     },
72   )
73 
74   private val deviceYearClass: Int
75     get() = YearClass.get(mContext)
76 
77   private val systemName: String
78     get() {
79       return if (Build.VERSION.SDK_INT < 23) {
80         "Android"
81       } else {
82         Build.VERSION.BASE_OS.takeIf { it.isNotEmpty() } ?: "Android"
83       }
84     }
85 
86   @ExpoMethod
87   fun getDeviceTypeAsync(promise: Promise) {
88     promise.resolve(getDeviceType(mContext).JSValue)
89   }
90 
91   @ExpoMethod
92   fun getUptimeAsync(promise: Promise) {
93     promise.resolve(SystemClock.uptimeMillis().toDouble())
94   }
95 
96   @ExpoMethod
97   fun getMaxMemoryAsync(promise: Promise) {
98     val maxMemory = Runtime.getRuntime().maxMemory()
99     promise.resolve(if (maxMemory != Long.MAX_VALUE) maxMemory.toDouble() else -1)
100   }
101 
102   @ExpoMethod
103   fun isRootedExperimentalAsync(promise: Promise) {
104     var isRooted = false
105     val isDevice = !isRunningOnEmulator
106 
107     try {
108       val buildTags = Build.TAGS
109       isRooted = if (isDevice && buildTags != null && buildTags.contains("test-keys")) {
110         true
111       } else {
112         if (File("/system/app/Superuser.apk").exists()) {
113           true
114         } else {
115           isDevice && File("/system/xbin/su").exists()
116         }
117       }
118     } catch (se: SecurityException) {
119       promise.reject(
120         "ERR_DEVICE_ROOT_DETECTION",
121         "Could not access the file system to determine if the device is rooted.",
122         se
123       )
124       return
125     }
126 
127     promise.resolve(isRooted)
128   }
129 
130   @ExpoMethod
131   fun isSideLoadingEnabledAsync(promise: Promise) {
132     val enabled: Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
133       Settings.Global.getInt(
134         mContext.applicationContext.contentResolver,
135         Settings.Global.INSTALL_NON_MARKET_APPS,
136         0
137       ) == 1
138     } else {
139       mContext.applicationContext.packageManager.canRequestPackageInstalls()
140     }
141 
142     promise.resolve(enabled)
143   }
144 
145   @ExpoMethod
146   fun getPlatformFeaturesAsync(promise: Promise) {
147     val allFeatures = mContext.applicationContext.packageManager.systemAvailableFeatures
148     val featureList = allFeatures.filterNotNull().map { it.name }
149     promise.resolve(featureList)
150   }
151 
152   @ExpoMethod
153   fun hasPlatformFeatureAsync(feature: String, promise: Promise) {
154     promise.resolve(mContext.applicationContext.packageManager.hasSystemFeature(feature))
155   }
156 
157   companion object {
158     private val TAG = DeviceModule::class.java.simpleName
159 
160     private val isRunningOnEmulator: Boolean
161       get() = EmulatorUtilities.isRunningOnEmulator()
162 
163     private fun getDeviceType(context: Context): DeviceType {
164       // Detect TVs via UI mode (Android TVs) or system features (Fire TV).
165       if (context.applicationContext.packageManager.hasSystemFeature("amazon.hardware.fire_tv")) {
166         return DeviceType.TV
167       }
168 
169       val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
170       if (uiManager != null && uiManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
171         return DeviceType.TV
172       }
173 
174       // Find the current window manager, if none is found we can't measure the device physical size.
175       val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager? ?: return DeviceType.UNKNOWN
176 
177       // Get display metrics to see if we can differentiate phones and tablets.
178       val metrics = DisplayMetrics()
179       windowManager.defaultDisplay.getMetrics(metrics)
180 
181       // Calculate physical size.
182       val widthInches = metrics.widthPixels / metrics.xdpi.toDouble()
183       val heightInches = metrics.heightPixels / metrics.ydpi.toDouble()
184       val diagonalSizeInches = sqrt(widthInches.pow(2.0) + heightInches.pow(2.0))
185       return if (diagonalSizeInches >= 3.0 && diagonalSizeInches <= 6.9) {
186         // Devices in a sane range for phones are considered to be phones.
187         DeviceType.PHONE
188       } else if (diagonalSizeInches > 6.9 && diagonalSizeInches <= 18.0) {
189         // Devices larger than a phone and in a sane range for tablets are tablets.
190         DeviceType.TABLET
191       } else {
192         // Otherwise, we don't know what device type we're on.
193         DeviceType.UNKNOWN
194       }
195     }
196   }
197 }
198