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