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