mirror of https://github.com/M66B/FairEmail.git
parent
0c697d9c31
commit
245baa3b13
@ -0,0 +1,121 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.net.TrafficStats
|
||||
import com.bugsnag.android.internal.JsonHelper
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
internal class DefaultDelivery(
|
||||
private val connectivity: Connectivity?,
|
||||
private val logger: Logger
|
||||
) : Delivery {
|
||||
|
||||
override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||
val status = deliver(
|
||||
deliveryParams.endpoint,
|
||||
JsonHelper.serialize(payload),
|
||||
payload.integrityToken,
|
||||
deliveryParams.headers
|
||||
)
|
||||
logger.i("Session API request finished with status $status")
|
||||
return status
|
||||
}
|
||||
|
||||
override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||
val json = payload.trimToSize().toByteArray()
|
||||
val status = deliver(deliveryParams.endpoint, json, payload.integrityToken, deliveryParams.headers)
|
||||
logger.i("Error API request finished with status $status")
|
||||
return status
|
||||
}
|
||||
|
||||
fun deliver(
|
||||
urlString: String,
|
||||
json: ByteArray,
|
||||
integrity: String?,
|
||||
headers: Map<String, String?>
|
||||
): DeliveryStatus {
|
||||
|
||||
TrafficStats.setThreadStatsTag(1)
|
||||
if (connectivity != null && !connectivity.hasNetworkConnection()) {
|
||||
return DeliveryStatus.UNDELIVERED
|
||||
}
|
||||
var conn: HttpURLConnection? = null
|
||||
|
||||
try {
|
||||
conn = makeRequest(URL(urlString), json, integrity, headers)
|
||||
|
||||
// End the request, get the response code
|
||||
val responseCode = conn.responseCode
|
||||
val status = DeliveryStatus.forHttpResponseCode(responseCode)
|
||||
logRequestInfo(responseCode, conn, status)
|
||||
return status
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
// attempt to persist the payload on disk. This approach uses streams to write to a
|
||||
// file, which takes less memory than serializing the payload into a ByteArray, and
|
||||
// therefore has a reasonable chance of retaining the payload for future delivery.
|
||||
logger.w("Encountered OOM delivering payload, falling back to persist on disk", oom)
|
||||
return DeliveryStatus.UNDELIVERED
|
||||
} catch (exception: IOException) {
|
||||
logger.w("IOException encountered in request", exception)
|
||||
return DeliveryStatus.UNDELIVERED
|
||||
} catch (exception: Exception) {
|
||||
logger.w("Unexpected error delivering payload", exception)
|
||||
return DeliveryStatus.FAILURE
|
||||
} finally {
|
||||
conn?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeRequest(
|
||||
url: URL,
|
||||
json: ByteArray,
|
||||
integrity: String?,
|
||||
headers: Map<String, String?>
|
||||
): HttpURLConnection {
|
||||
val conn = url.openConnection() as HttpURLConnection
|
||||
conn.doOutput = true
|
||||
|
||||
// avoids creating a buffer within HttpUrlConnection, see
|
||||
// https://developer.android.com/reference/java/net/HttpURLConnection
|
||||
conn.setFixedLengthStreamingMode(json.size)
|
||||
|
||||
integrity?.let { digest ->
|
||||
conn.addRequestProperty(HEADER_BUGSNAG_INTEGRITY, digest)
|
||||
}
|
||||
headers.forEach { (key, value) ->
|
||||
if (value != null) {
|
||||
conn.addRequestProperty(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// write the JSON payload
|
||||
conn.outputStream.use {
|
||||
it.write(json)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
private fun logRequestInfo(code: Int, conn: HttpURLConnection, status: DeliveryStatus) {
|
||||
runCatching {
|
||||
logger.i(
|
||||
"Request completed with code $code, " +
|
||||
"message: ${conn.responseMessage}, " +
|
||||
"headers: ${conn.headerFields}"
|
||||
)
|
||||
}
|
||||
runCatching {
|
||||
conn.inputStream.bufferedReader().use {
|
||||
logger.d("Received request response: ${it.readText()}")
|
||||
}
|
||||
}
|
||||
|
||||
runCatching {
|
||||
if (status != DeliveryStatus.DELIVERED) {
|
||||
conn.errorStream.bufferedReader().use {
|
||||
logger.w("Request error details: ${it.readText()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import java.io.IOException
|
||||
import java.security.DigestOutputStream
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Denotes objects that are expected to be delivered over a network.
|
||||
*/
|
||||
interface Deliverable {
|
||||
/**
|
||||
* Return the byte representation of this `Deliverable`.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun toByteArray(): ByteArray
|
||||
|
||||
/**
|
||||
* The value of the "Bugsnag-Integrity" HTTP header returned as a String. This value is used
|
||||
* to validate the payload and is expected by the standard BugSnag servers.
|
||||
*/
|
||||
val integrityToken: String?
|
||||
get() {
|
||||
runCatching {
|
||||
val shaDigest = MessageDigest.getInstance("SHA-1")
|
||||
val builder = StringBuilder("sha1 ")
|
||||
|
||||
// Pipe the object through a no-op output stream
|
||||
DigestOutputStream(NullOutputStream(), shaDigest).use { stream ->
|
||||
stream.buffered().use { writer ->
|
||||
writer.write(toByteArray())
|
||||
}
|
||||
shaDigest.digest().forEach { byte ->
|
||||
builder.append(String.format("%02x", byte))
|
||||
}
|
||||
}
|
||||
return builder.toString()
|
||||
}.getOrElse { return null }
|
||||
}
|
||||
}
|
@ -1,56 +1,73 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.content.Context
|
||||
import com.bugsnag.android.internal.BackgroundTaskService
|
||||
import com.bugsnag.android.internal.BugsnagStoreMigrator.migrateLegacyFiles
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import com.bugsnag.android.internal.dag.DependencyModule
|
||||
import com.bugsnag.android.internal.TaskType
|
||||
import com.bugsnag.android.internal.dag.BackgroundDependencyModule
|
||||
import com.bugsnag.android.internal.dag.Provider
|
||||
|
||||
/**
|
||||
* A dependency module which constructs the objects that store information to disk in Bugsnag.
|
||||
*/
|
||||
internal class StorageModule(
|
||||
appContext: Context,
|
||||
immutableConfig: ImmutableConfig,
|
||||
logger: Logger
|
||||
) : DependencyModule() {
|
||||
private val immutableConfig: ImmutableConfig,
|
||||
bgTaskService: BackgroundTaskService
|
||||
) : BackgroundDependencyModule(bgTaskService, TaskType.IO) {
|
||||
|
||||
val sharedPrefMigrator by future { SharedPrefMigrator(appContext) }
|
||||
val bugsnagDir = provider {
|
||||
migrateLegacyFiles(immutableConfig.persistenceDirectory)
|
||||
}
|
||||
|
||||
val sharedPrefMigrator = provider {
|
||||
SharedPrefMigrator(appContext)
|
||||
}
|
||||
|
||||
private val deviceIdStore by future {
|
||||
val deviceIdStore = provider {
|
||||
DeviceIdStore(
|
||||
appContext,
|
||||
sharedPrefMigrator = sharedPrefMigrator,
|
||||
logger = logger,
|
||||
logger = immutableConfig.logger,
|
||||
config = immutableConfig
|
||||
)
|
||||
}
|
||||
|
||||
val deviceId by future { deviceIdStore.loadDeviceId() }
|
||||
|
||||
val internalDeviceId by future { deviceIdStore.loadInternalDeviceId() }
|
||||
|
||||
val userStore by future {
|
||||
val userStore = provider {
|
||||
UserStore(
|
||||
immutableConfig,
|
||||
deviceId,
|
||||
immutableConfig.persistUser,
|
||||
bugsnagDir,
|
||||
deviceIdStore.map { it.load() },
|
||||
sharedPrefMigrator = sharedPrefMigrator,
|
||||
logger = logger
|
||||
logger = immutableConfig.logger
|
||||
)
|
||||
}
|
||||
|
||||
val lastRunInfoStore by future { LastRunInfoStore(immutableConfig) }
|
||||
val lastRunInfoStore = provider {
|
||||
LastRunInfoStore(immutableConfig)
|
||||
}
|
||||
|
||||
val sessionStore by future {
|
||||
val sessionStore = provider {
|
||||
SessionStore(
|
||||
immutableConfig,
|
||||
logger,
|
||||
bugsnagDir.get(),
|
||||
immutableConfig.maxPersistedSessions,
|
||||
immutableConfig.apiKey,
|
||||
immutableConfig.logger,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
val lastRunInfo by future {
|
||||
val lastRunInfo = lastRunInfoStore.map { lastRunInfoStore ->
|
||||
val info = lastRunInfoStore.load()
|
||||
val currentRunInfo = LastRunInfo(0, crashed = false, crashedDuringLaunch = false)
|
||||
lastRunInfoStore.persist(currentRunInfo)
|
||||
info
|
||||
return@map info
|
||||
}
|
||||
|
||||
fun loadUser(initialUser: User): Provider<UserState> = provider {
|
||||
val userState = userStore.get().load(initialUser)
|
||||
sharedPrefMigrator.getOrNull()?.deleteLegacyPrefs()
|
||||
return@provider userState
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,181 @@
|
||||
package com.bugsnag.android.internal.dag
|
||||
|
||||
import android.os.Looper
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* A lightweight abstraction similar to `Lazy` or `Future` allowing values to be calculated on
|
||||
* separate threads, or to be pre-computed.
|
||||
*/
|
||||
interface Provider<E> {
|
||||
/**
|
||||
* Same as [get] but will return `null` instead of throwing an exception if the value could
|
||||
* not be computed.
|
||||
*/
|
||||
fun getOrNull(): E?
|
||||
|
||||
/**
|
||||
* Return the value sourced from this provider, throwing an exception if the provider failed
|
||||
* to calculate a value. Anything thrown from here will have been captured when attempting
|
||||
* to calculate the value.
|
||||
*/
|
||||
fun get(): E
|
||||
}
|
||||
|
||||
/**
|
||||
* The primary implementation of [Provider], usually created using the
|
||||
* [BackgroundDependencyModule.provider] function. Similar conceptually to
|
||||
* [java.util.concurrent.FutureTask] but with a more compact implementation. The implementation
|
||||
* of [RunnableProvider.get] is special because it behaves more like [Lazy.value] in that getting
|
||||
* a value that is still pending will cause it to be run on the current thread instead of waiting
|
||||
* for it to be run "sometime in the future". This makes RunnableProviders less bug-prone when
|
||||
* dealing with single-thread executors (such as those in [BackgroundTaskService]). RunnableProvider
|
||||
* also has special handling for the main-thread, ensuring no computational work (such as IO) is
|
||||
* done on the main thread.
|
||||
*/
|
||||
abstract class RunnableProvider<E> : Provider<E>, Runnable {
|
||||
private val state = AtomicInteger(TASK_STATE_PENDING)
|
||||
|
||||
@Volatile
|
||||
private var value: Any? = null
|
||||
|
||||
/**
|
||||
* Calculate the value of this [Provider]. This function will be called at-most once by [run].
|
||||
* Do not call this function directly, instead use [get] and [getOrNull] which implement the
|
||||
* correct threading behaviour and will reuse the value if it has been previously calculated.
|
||||
*/
|
||||
abstract operator fun invoke(): E
|
||||
|
||||
override fun getOrNull(): E? {
|
||||
return getOr { return null }
|
||||
}
|
||||
|
||||
override fun get(): E {
|
||||
return getOr { throw value as Throwable }
|
||||
}
|
||||
|
||||
private inline fun getOr(failureHandler: () -> E): E {
|
||||
while (true) {
|
||||
when (state.get()) {
|
||||
TASK_STATE_RUNNING -> awaitResult()
|
||||
TASK_STATE_PENDING -> {
|
||||
if (isMainThread()) {
|
||||
// When the calling thread is the 'main' thread, we *always* wait for the
|
||||
// background workers to [invoke] this Provider, assuming that the Provider
|
||||
// is performing some kind of IO that should be kept away from the main
|
||||
// thread. Ideally this doesn't happen, but this behaviour avoids the
|
||||
// need for complicated callback mechanisms.
|
||||
awaitResult()
|
||||
} else {
|
||||
// If the Provider has yet to be computed, we will try and run it on the
|
||||
// current thread. This potentially causes run() to happen on a different
|
||||
// Thread to the expected worker (TaskType), effectively like work-stealing.
|
||||
run()
|
||||
}
|
||||
}
|
||||
|
||||
TASK_STATE_COMPLETE -> @Suppress("UNCHECKED_CAST") return value as E
|
||||
TASK_STATE_FAILED -> failureHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isMainThread(): Boolean {
|
||||
return Thread.currentThread() === mainThread
|
||||
}
|
||||
|
||||
/**
|
||||
* Cause the current thread to wait (block) until this `Provider` [isComplete]. Upon returning
|
||||
* the [isComplete] function will return `true`.
|
||||
*/
|
||||
private fun awaitResult() {
|
||||
synchronized(this) {
|
||||
while (!isComplete()) {
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
(this as Object).wait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isComplete() = when (state.get()) {
|
||||
TASK_STATE_PENDING, TASK_STATE_RUNNING -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
/**
|
||||
* The main entry point for a provider, typically called by a worker thread from
|
||||
* [BackgroundTaskService]. If [run] has already been called this will be a no-op (including
|
||||
* a reentrant thread), as such the task state *must* be checked after calling this.
|
||||
*
|
||||
* This should not be called, and instead [get] or [getOrNull] should be used to obtain the
|
||||
* value produced by [invoke].
|
||||
*/
|
||||
final override fun run() {
|
||||
if (state.compareAndSet(TASK_STATE_PENDING, TASK_STATE_RUNNING)) {
|
||||
try {
|
||||
value = invoke()
|
||||
state.set(TASK_STATE_COMPLETE)
|
||||
} catch (ex: Throwable) {
|
||||
value = ex
|
||||
state.set(TASK_STATE_FAILED)
|
||||
} finally {
|
||||
synchronized(this) {
|
||||
// wakeup any waiting threads
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
(this as Object).notifyAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal companion object {
|
||||
/**
|
||||
* The `Provider` task state before the provider has started actually running. This state
|
||||
* indicates that the task has been constructed, has typically been scheduled but has
|
||||
* not actually started running yet.
|
||||
*/
|
||||
private const val TASK_STATE_PENDING = 0
|
||||
|
||||
/**
|
||||
* The `Provider` task state when running. Once the [run] function returns the state will
|
||||
* be either [TASK_STATE_COMPLETE] or [TASK_STATE_FAILED].
|
||||
*/
|
||||
private const val TASK_STATE_RUNNING = 1
|
||||
|
||||
/**
|
||||
* The `Provider` state of a successfully completed task. When this is the state the
|
||||
* provider value can be obtained immediately without error.
|
||||
*/
|
||||
private const val TASK_STATE_COMPLETE = 2
|
||||
|
||||
/**
|
||||
* The `Provider` state of a task where [invoke] failed with an error or exception.
|
||||
*/
|
||||
private const val TASK_STATE_FAILED = 999
|
||||
|
||||
/**
|
||||
* We cache the main thread to avoid any locks within [Looper.getMainLooper]. This is
|
||||
* settable for unit tests, so that there doesn't have to be a valid Looper when they run.
|
||||
*
|
||||
* Actually access is done via the [mainThread] property.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@Suppress("ObjectPropertyNaming") // backing property from 'mainThread'
|
||||
internal var _mainThread: Thread? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
field = Looper.getMainLooper().thread
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
internal val mainThread: Thread get() = _mainThread!!
|
||||
}
|
||||
}
|
||||
|
||||
data class ValueProvider<T>(private val value: T) : Provider<T> {
|
||||
override fun getOrNull(): T? = get()
|
||||
override fun get(): T = value
|
||||
}
|
Loading…
Reference in new issue