mirror of https://github.com/M66B/FairEmail.git
parent
dd8bd36712
commit
f5604d6ede
@ -0,0 +1,163 @@
|
|||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.lang.Thread
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import java.nio.channels.FileLock
|
||||||
|
import java.nio.channels.OverlappingFileLockException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for persisting and retrieving a device ID to a file.
|
||||||
|
*
|
||||||
|
* This class is made multi-process safe through the use of a [FileLock], and thread safe
|
||||||
|
* through the use of a [ReadWriteLock] in [SynchronizedStreamableStore].
|
||||||
|
*/
|
||||||
|
class DeviceIdFilePersistence(
|
||||||
|
private val file: File,
|
||||||
|
private val deviceIdGenerator: () -> UUID,
|
||||||
|
private val logger: Logger
|
||||||
|
) : DeviceIdPersistence {
|
||||||
|
private val synchronizedStreamableStore: SynchronizedStreamableStore<DeviceId>
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
file.createNewFile()
|
||||||
|
} catch (exc: Throwable) {
|
||||||
|
logger.w("Failed to created device ID file", exc)
|
||||||
|
}
|
||||||
|
this.synchronizedStreamableStore = SynchronizedStreamableStore(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the device ID from its file system location.
|
||||||
|
* If no value is present then a UUID will be generated and persisted.
|
||||||
|
*/
|
||||||
|
override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean): String? {
|
||||||
|
return try {
|
||||||
|
// optimistically read device ID without a lock - the majority of the time
|
||||||
|
// the device ID will already be present so no synchronization is required.
|
||||||
|
val deviceId = loadDeviceIdInternal()
|
||||||
|
|
||||||
|
if (deviceId?.id != null) {
|
||||||
|
deviceId.id
|
||||||
|
} else {
|
||||||
|
return if (requestCreateIfDoesNotExist) persistNewDeviceUuid(deviceIdGenerator()) else null
|
||||||
|
}
|
||||||
|
} catch (exc: Throwable) {
|
||||||
|
logger.w("Failed to load device ID", exc)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the device ID from the file.
|
||||||
|
*
|
||||||
|
* If the file has zero length it can't contain device ID, so reading will be skipped.
|
||||||
|
*/
|
||||||
|
private fun loadDeviceIdInternal(): DeviceId? {
|
||||||
|
if (file.length() > 0) {
|
||||||
|
try {
|
||||||
|
return synchronizedStreamableStore.load(DeviceId.Companion::fromReader)
|
||||||
|
} catch (exc: Throwable) { // catch AssertionError which can be thrown by JsonReader
|
||||||
|
// on Android 8.0/8.1. see https://issuetracker.google.com/issues/79920590
|
||||||
|
logger.w("Failed to load device ID", exc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a new Device ID to the file.
|
||||||
|
*/
|
||||||
|
private fun persistNewDeviceUuid(uuid: UUID): String? {
|
||||||
|
return try {
|
||||||
|
// acquire a FileLock to prevent Clients in different processes writing
|
||||||
|
// to the same file concurrently
|
||||||
|
file.outputStream().channel.use { channel ->
|
||||||
|
persistNewDeviceIdWithLock(channel, uuid)
|
||||||
|
}
|
||||||
|
} catch (exc: IOException) {
|
||||||
|
logger.w("Failed to persist device ID", exc)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persistNewDeviceIdWithLock(
|
||||||
|
channel: FileChannel,
|
||||||
|
uuid: UUID
|
||||||
|
): String? {
|
||||||
|
val lock = waitForFileLock(channel) ?: return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// read the device ID again as it could have changed
|
||||||
|
// between the last read and when the lock was acquired
|
||||||
|
val deviceId = loadDeviceIdInternal()
|
||||||
|
|
||||||
|
if (deviceId?.id != null) {
|
||||||
|
// the device ID changed between the last read
|
||||||
|
// and acquiring the lock, so return the generated value
|
||||||
|
deviceId.id
|
||||||
|
} else {
|
||||||
|
// generate a new device ID and persist it
|
||||||
|
val newId = DeviceId(uuid.toString())
|
||||||
|
synchronizedStreamableStore.persist(newId)
|
||||||
|
newId.id
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to acquire a file lock. If [OverlappingFileLockException] is thrown
|
||||||
|
* then the method will wait for 50ms then try again, for a maximum of 10 attempts.
|
||||||
|
*/
|
||||||
|
private fun waitForFileLock(channel: FileChannel): FileLock? {
|
||||||
|
repeat(MAX_FILE_LOCK_ATTEMPTS) {
|
||||||
|
try {
|
||||||
|
return channel.tryLock()
|
||||||
|
} catch (exc: OverlappingFileLockException) {
|
||||||
|
Thread.sleep(FILE_LOCK_WAIT_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_FILE_LOCK_ATTEMPTS = 20
|
||||||
|
private const val FILE_LOCK_WAIT_MS = 25L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes and deserializes the device ID to/from JSON.
|
||||||
|
*/
|
||||||
|
private class DeviceId(val id: String?) : JsonStream.Streamable {
|
||||||
|
|
||||||
|
override fun toStream(stream: JsonStream) {
|
||||||
|
with(stream) {
|
||||||
|
beginObject()
|
||||||
|
name(KEY_ID)
|
||||||
|
value(id)
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : JsonReadable<DeviceId> {
|
||||||
|
private const val KEY_ID = "id"
|
||||||
|
|
||||||
|
override fun fromReader(reader: JsonReader): DeviceId {
|
||||||
|
var id: String? = null
|
||||||
|
with(reader) {
|
||||||
|
beginObject()
|
||||||
|
if (hasNext() && KEY_ID == nextName()) {
|
||||||
|
id = nextString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DeviceId(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
interface DeviceIdPersistence {
|
||||||
|
/**
|
||||||
|
* Loads the device ID from storage.
|
||||||
|
*
|
||||||
|
* Device IDs are UUIDs which are persisted on a per-install basis.
|
||||||
|
*
|
||||||
|
* This method must be thread-safe and multi-process safe.
|
||||||
|
*
|
||||||
|
* Note: requestCreateIfDoesNotExist is only a request; an implementation may still refuse to create a new ID.
|
||||||
|
*/
|
||||||
|
fun loadDeviceId(requestCreateIfDoesNotExist: Boolean): String?
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents important information about a session filename.
|
||||||
|
* Currently the following information is encoded:
|
||||||
|
*
|
||||||
|
* uuid - to disambiguate stored error reports
|
||||||
|
* timestamp - to sort error reports by time of capture
|
||||||
|
*/
|
||||||
|
internal data class SessionFilenameInfo(
|
||||||
|
val timestamp: Long,
|
||||||
|
val uuid: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun encode(): String {
|
||||||
|
return toFilename(timestamp, uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal companion object {
|
||||||
|
|
||||||
|
const val uuidLength = 36
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a filename for the session in the format
|
||||||
|
* "[UUID][timestamp]_v2.json"
|
||||||
|
*/
|
||||||
|
fun toFilename(timestamp: Long, uuid: String): String {
|
||||||
|
return "${uuid}${timestamp}_v2.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun defaultFilename(): String {
|
||||||
|
return toFilename(System.currentTimeMillis(), UUID.randomUUID().toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromFile(file: File): SessionFilenameInfo {
|
||||||
|
return SessionFilenameInfo(
|
||||||
|
findTimestampInFilename(file),
|
||||||
|
findUuidInFilename(file)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findUuidInFilename(file: File): String {
|
||||||
|
return file.name.substring(0, uuidLength - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun findTimestampInFilename(file: File): Long {
|
||||||
|
return file.name.substring(uuidLength, file.name.indexOf("_")).toLongOrNull() ?: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of telemetry that may be sent to Bugsnag for product improvement purposes.
|
||||||
|
*/
|
||||||
|
enum class Telemetry {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Errors within the Bugsnag SDK.
|
||||||
|
*/
|
||||||
|
INTERNAL_ERRORS;
|
||||||
|
|
||||||
|
internal companion object {
|
||||||
|
fun fromString(str: String) = values().find { it.name == str } ?: INTERNAL_ERRORS
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
import com.bugsnag.android.BugsnagEventMapper
|
||||||
|
import com.bugsnag.android.Event
|
||||||
|
import com.bugsnag.android.JsonStream
|
||||||
|
import com.bugsnag.android.Logger
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import com.bugsnag.android.Error as BugsnagError
|
||||||
|
|
||||||
|
class BugsnagMapper(logger: Logger) {
|
||||||
|
private val eventMapper = BugsnagEventMapper(logger)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given `Map` of data to an `Event` object
|
||||||
|
*/
|
||||||
|
fun convertToEvent(data: Map<in String, Any?>, fallbackApiKey: String): Event {
|
||||||
|
return eventMapper.convertToEvent(data, fallbackApiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given `Map` of data to an `Error` object
|
||||||
|
*/
|
||||||
|
fun convertToError(data: Map<in String, Any?>): BugsnagError {
|
||||||
|
return eventMapper.convertError(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a given `Event` object to a `Map<String, Any?>`
|
||||||
|
*/
|
||||||
|
fun convertToMap(event: Event): Map<in String, Any?> {
|
||||||
|
val byteStream = ByteArrayOutputStream()
|
||||||
|
byteStream.writer().use { writer -> JsonStream(writer).value(event) }
|
||||||
|
return JsonHelper.deserialize(byteStream.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a given `Error` object to a `Map<String, Any?>`
|
||||||
|
*/
|
||||||
|
fun convertToMap(error: BugsnagError): Map<in String, Any?> {
|
||||||
|
val byteStream = ByteArrayOutputStream()
|
||||||
|
byteStream.writer().use { writer -> JsonStream(writer).value(error) }
|
||||||
|
return JsonHelper.deserialize(byteStream.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue