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