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

266 lines
8.5 KiB
Kotlin
Raw Normal View History

2021-05-15 20:03:05 +00:00
package com.bugsnag.android
import android.annotation.SuppressLint
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
import android.provider.Settings
import java.io.File
import java.util.Date
import java.util.HashMap
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
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?,
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()
private val runtimeVersions: MutableMap<String, Any>
private val rootedFuture: Future<Boolean>?
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,
calculateTotalMemory(),
runtimeVersions.toMutableMap()
)
fun generateDeviceWithState(now: Long) = DeviceWithState(
buildInfo,
checkIsRooted(),
deviceId,
locale,
calculateTotalMemory(),
runtimeVersions.toMutableMap(),
calculateFreeDisk(),
calculateFreeMemory(),
2021-07-29 19:08:19 +00:00
getOrientationAsString(),
2021-05-15 20:03:05 +00:00
Date(now)
)
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 {
val cr = appContext.contentResolver
@Suppress("DEPRECATION") val providersAllowed =
Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)
return when {
providersAllowed != null && providersAllowed.isNotEmpty() -> "allowed"
else -> "disallowed"
}
} catch (exception: Exception) {
logger.w("Could not get locationStatus")
}
return null
}
/**
* 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
}
/**
* Get the amount of memory remaining that the VM can allocate
*/
private fun calculateFreeMemory(): Long {
val runtime = Runtime.getRuntime()
val maxMemory = runtime.maxMemory()
return if (maxMemory != Long.MAX_VALUE) {
maxMemory - runtime.totalMemory() + runtime.freeMemory()
} else {
runtime.freeMemory()
}
}
/**
* Get the total memory available on the current Android device, in bytes
*/
private fun calculateTotalMemory(): Long {
val runtime = Runtime.getRuntime()
val maxMemory = runtime.maxMemory()
return when {
maxMemory != Long.MAX_VALUE -> maxMemory
else -> runtime.totalMemory()
}
}
/**
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) {
runtimeVersions[key] = value
}
}