FairEmail/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt

320 lines
10 KiB
Kotlin
Raw Normal View History

2021-05-15 20:03:05 +00:00
package com.bugsnag.android
import android.annotation.SuppressLint
2021-09-11 18:36:58 +00:00
import android.app.ActivityManager
2021-05-15 20:03:05 +00:00
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.content.res.Resources
import android.os.BatteryManager
2021-09-11 18:36:58 +00:00
import android.os.Build
2021-05-15 20:03:05 +00:00
import android.provider.Settings
import java.io.File
import java.util.Date
import java.util.Locale
import java.util.concurrent.Callable
import java.util.concurrent.Future
import java.util.concurrent.RejectedExecutionException
2021-07-29 19:08:19 +00:00
import java.util.concurrent.atomic.AtomicInteger
2021-05-15 20:03:05 +00:00
import kotlin.math.max
import kotlin.math.min
2021-09-11 18:36:58 +00:00
import android.os.Process as AndroidProcess
2021-05-15 20:03:05 +00:00
internal class DeviceDataCollector(
private val connectivity: Connectivity,
private val appContext: Context,
2021-07-29 19:08:19 +00:00
resources: Resources,
2021-05-15 20:03:05 +00:00
private val deviceId: String?,
2022-06-23 18:23:15 +00:00
private val internalDeviceId: String?,
2021-05-15 20:03:05 +00:00
private val buildInfo: DeviceBuildInfo,
private val dataDirectory: File,
rootDetector: RootDetector,
2021-08-15 12:40:22 +00:00
private val bgTaskService: BackgroundTaskService,
2021-05-15 20:03:05 +00:00
private val logger: Logger
) {
2021-07-29 19:08:19 +00:00
private val displayMetrics = resources.displayMetrics
2021-05-15 20:03:05 +00:00
private val emulator = isEmulator()
private val screenDensity = getScreenDensity()
private val dpi = getScreenDensityDpi()
private val screenResolution = getScreenResolution()
private val locale = Locale.getDefault().toString()
private val cpuAbi = getCpuAbi()
2022-06-23 18:23:15 +00:00
private var runtimeVersions: MutableMap<String, Any>
2021-05-15 20:03:05 +00:00
private val rootedFuture: Future<Boolean>?
2021-09-11 18:36:58 +00:00
private val totalMemoryFuture: Future<Long?>? = retrieveTotalDeviceMemory()
2021-07-29 19:08:19 +00:00
private var orientation = AtomicInteger(resources.configuration.orientation)
2021-05-15 20:03:05 +00:00
init {
val map = mutableMapOf<String, Any>()
buildInfo.apiLevel?.let { map["androidApiLevel"] = it }
buildInfo.osBuild?.let { map["osBuild"] = it }
runtimeVersions = map
rootedFuture = try {
bgTaskService.submitTask(
TaskType.IO,
Callable {
rootDetector.isRooted()
}
)
} catch (exc: RejectedExecutionException) {
logger.w("Failed to perform root detection checks", exc)
null
}
}
fun generateDevice() = Device(
buildInfo,
cpuAbi,
checkIsRooted(),
deviceId,
locale,
2021-09-11 18:36:58 +00:00
totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
2021-05-15 20:03:05 +00:00
runtimeVersions.toMutableMap()
)
fun generateDeviceWithState(now: Long) = DeviceWithState(
buildInfo,
checkIsRooted(),
deviceId,
locale,
2021-09-11 18:36:58 +00:00
totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
2021-05-15 20:03:05 +00:00
runtimeVersions.toMutableMap(),
calculateFreeDisk(),
calculateFreeMemory(),
2021-07-29 19:08:19 +00:00
getOrientationAsString(),
2021-05-15 20:03:05 +00:00
Date(now)
)
2022-06-23 18:23:15 +00:00
fun generateInternalDeviceWithState(now: Long) = DeviceWithState(
buildInfo,
checkIsRooted(),
internalDeviceId,
locale,
totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
runtimeVersions.toMutableMap(),
calculateFreeDisk(),
calculateFreeMemory(),
getOrientationAsString(),
Date(now)
)
2021-05-15 20:03:05 +00:00
fun getDeviceMetadata(): Map<String, Any?> {
val map = HashMap<String, Any?>()
2021-06-01 05:34:31 +00:00
populateBatteryInfo(into = map)
2021-05-15 20:03:05 +00:00
map["locationStatus"] = getLocationStatus()
map["networkAccess"] = getNetworkAccess()
map["brand"] = buildInfo.brand
map["screenDensity"] = screenDensity
map["dpi"] = dpi
map["emulator"] = emulator
map["screenResolution"] = screenResolution
return map
}
private fun checkIsRooted(): Boolean {
return try {
rootedFuture != null && rootedFuture.get()
} catch (exc: Exception) {
false
}
}
/**
* Guesses whether the current device is an emulator or not, erring on the side of caution
*
* @return true if the current device is an emulator
*/
private // genymotion
fun isEmulator(): Boolean {
val fingerprint = buildInfo.fingerprint
return fingerprint != null && (
fingerprint.startsWith("unknown") ||
fingerprint.contains("generic") ||
fingerprint.contains("vbox")
)
}
/**
* The screen density of the current Android device in dpi, eg. 320
*/
private fun getScreenDensityDpi(): Int? = displayMetrics?.densityDpi
/**
2021-06-01 05:34:31 +00:00
* Populate the current Battery Info into the specified MutableMap
2021-05-15 20:03:05 +00:00
*/
2021-06-01 05:34:31 +00:00
private fun populateBatteryInfo(into: MutableMap<String, Any?>) {
2021-05-15 20:03:05 +00:00
try {
val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = appContext.registerReceiverSafe(null, ifilter, logger)
if (batteryStatus != null) {
2021-06-01 05:34:31 +00:00
val level = batteryStatus.getIntExtra("level", -1)
val scale = batteryStatus.getIntExtra("scale", -1)
2021-05-15 20:03:05 +00:00
2021-06-01 05:34:31 +00:00
if (level != -1 || scale != -1) {
val batteryLevel: Float = level.toFloat() / scale.toFloat()
into["batteryLevel"] = batteryLevel
}
2021-05-15 20:03:05 +00:00
val status = batteryStatus.getIntExtra("status", -1)
2021-06-01 05:34:31 +00:00
val charging =
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
into["charging"] = charging
2021-05-15 20:03:05 +00:00
}
} catch (exception: Exception) {
2021-06-01 05:34:31 +00:00
logger.w("Could not get battery status")
2021-05-15 20:03:05 +00:00
}
}
/**
* Get the current status of location services
*/
private fun getLocationStatus(): String? {
try {
2022-06-23 18:23:15 +00:00
return if (isLocationEnabled()) "allowed" else "disallowed"
2021-05-15 20:03:05 +00:00
} catch (exception: Exception) {
logger.w("Could not get locationStatus")
}
return null
}
2022-06-23 18:23:15 +00:00
private fun isLocationEnabled() = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
appContext.getLocationManager()?.isLocationEnabled == true
else -> {
val cr = appContext.contentResolver
@Suppress("DEPRECATION") val providersAllowed =
Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)
providersAllowed != null && providersAllowed.isNotEmpty()
}
}
2021-05-15 20:03:05 +00:00
/**
* Get the current status of network access, eg "cellular"
*/
private fun getNetworkAccess(): String = connectivity.retrieveNetworkAccessState()
/**
* The screen density scaling factor of the current Android device
*/
private fun getScreenDensity(): Float? = displayMetrics?.density
/**
* The screen resolution of the current Android device in px, eg. 1920x1080
*/
private fun getScreenResolution(): String? {
return if (displayMetrics != null) {
val max = max(displayMetrics.widthPixels, displayMetrics.heightPixels)
val min = min(displayMetrics.widthPixels, displayMetrics.heightPixels)
2021-07-29 19:08:19 +00:00
"${max}x$min"
2021-05-15 20:03:05 +00:00
} else {
null
}
}
/**
* Gets information about the CPU / API
*/
fun getCpuAbi(): Array<String> = buildInfo.cpuAbis ?: emptyArray()
/**
* Get the usable disk space on internal storage's data directory
*/
@SuppressLint("UsableSpace")
fun calculateFreeDisk(): Long {
// for this specific case we want the currently usable space, not
// StorageManager#allocatableBytes() as the UsableSpace lint inspection suggests
2021-08-15 12:40:22 +00:00
return runCatching {
bgTaskService.submitTask(
TaskType.IO,
Callable { dataDirectory.usableSpace }
).get()
}.getOrDefault(0L)
2021-05-15 20:03:05 +00:00
}
/**
2021-09-11 18:36:58 +00:00
* Get the amount of memory remaining on the device
2021-05-15 20:03:05 +00:00
*/
2021-09-11 18:36:58 +00:00
private fun calculateFreeMemory(): Long? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
val freeMemory = appContext.getActivityManager()
?.let { am -> ActivityManager.MemoryInfo().also { am.getMemoryInfo(it) } }
?.availMem
2021-05-15 20:03:05 +00:00
2021-09-11 18:36:58 +00:00
if (freeMemory != null) {
return freeMemory
}
2021-05-15 20:03:05 +00:00
}
2021-09-11 18:36:58 +00:00
return runCatching {
@Suppress("PrivateApi")
AndroidProcess::class.java.getDeclaredMethod("getFreeMemory").invoke(null) as Long?
}.getOrNull()
2021-05-15 20:03:05 +00:00
}
/**
2021-09-11 18:36:58 +00:00
* Attempt to retrieve the total amount of memory available on the device
2021-05-15 20:03:05 +00:00
*/
2021-09-11 18:36:58 +00:00
private fun retrieveTotalDeviceMemory(): Future<Long?>? {
return try {
bgTaskService.submitTask(
TaskType.DEFAULT,
Callable {
calculateTotalMemory()
}
)
} catch (exc: RejectedExecutionException) {
logger.w("Failed to lookup available device memory", exc)
null
2021-05-15 20:03:05 +00:00
}
}
2021-09-11 18:36:58 +00:00
private fun calculateTotalMemory(): Long? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
val totalMemory = appContext.getActivityManager()
?.let { am -> ActivityManager.MemoryInfo().also { am.getMemoryInfo(it) } }
?.totalMem
if (totalMemory != null) {
return totalMemory
}
}
// we try falling back to a reflective API
return runCatching {
@Suppress("PrivateApi")
AndroidProcess::class.java.getDeclaredMethod("getTotalMemory").invoke(null) as Long?
}.getOrNull()
}
2021-05-15 20:03:05 +00:00
/**
2021-07-29 19:08:19 +00:00
* Get the current device orientation, eg. "landscape"
2021-05-15 20:03:05 +00:00
*/
2021-07-29 19:08:19 +00:00
internal fun getOrientationAsString(): String? = when (orientation.get()) {
2021-05-15 20:03:05 +00:00
ORIENTATION_LANDSCAPE -> "landscape"
ORIENTATION_PORTRAIT -> "portrait"
else -> null
}
2021-07-29 19:08:19 +00:00
/**
* Called whenever the orientation is updated so that the device information is accurate.
* Currently this is only invoked by [ClientComponentCallbacks]. Returns true if the
* orientation has changed, otherwise false.
*/
internal fun updateOrientation(newOrientation: Int): Boolean {
return orientation.getAndSet(newOrientation) != newOrientation
}
2021-05-15 20:03:05 +00:00
fun addRuntimeVersionInfo(key: String, value: String) {
2022-06-23 18:23:15 +00:00
// Use copy-on-write to avoid a ConcurrentModificationException in generateDeviceWithState
val newRuntimeVersions = runtimeVersions.toMutableMap()
newRuntimeVersions[key] = value
runtimeVersions = newRuntimeVersions
2021-05-15 20:03:05 +00:00
}
}