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