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