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