Merge pull request #222 from vector-im/feature/bma/networkModule
Add network module
This commit is contained in:
commit
7465614bb1
25 changed files with 621 additions and 313 deletions
|
|
@ -219,6 +219,9 @@ dependencies {
|
|||
implementation(libs.androidx.startup)
|
||||
implementation(libs.coil)
|
||||
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||
|
||||
implementation(libs.dagger)
|
||||
kapt(libs.dagger.compiler)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,15 +21,19 @@ import com.squareup.anvil.annotations.ContributesTo
|
|||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.x.BuildConfig
|
||||
import io.element.android.x.R
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.plus
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
|
@ -48,6 +52,22 @@ object AppModule {
|
|||
return MainScope() + CoroutineName("ElementX Scope")
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesBuildMeta(@ApplicationContext context: Context) = BuildMeta(
|
||||
isDebug = BuildConfig.DEBUG,
|
||||
applicationName = context.getString(R.string.app_name),
|
||||
applicationId = BuildConfig.APPLICATION_ID,
|
||||
lowPrivacyLoggingEnabled = false, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
|
||||
versionName = BuildConfig.VERSION_NAME,
|
||||
gitRevision = "TODO", // BuildConfig.GIT_REVISION,
|
||||
gitRevisionDate = "TODO", // BuildConfig.GIT_REVISION_DATE,
|
||||
gitBranchName = "TODO", // BuildConfig.GIT_BRANCH_NAME,
|
||||
flavorDescription = "TODO", // BuildConfig.FLAVOR_DESCRIPTION,
|
||||
flavorShortDescription = "TODO", // BuildConfig.SHORT_FLAVOR_DESCRIPTION,
|
||||
okHttpLoggingLevel = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesCoroutineDispatchers(): CoroutineDispatchers {
|
||||
|
|
|
|||
|
|
@ -16,15 +16,10 @@
|
|||
|
||||
package io.element.android.features.rageshake.api.reporter
|
||||
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporterListener
|
||||
import io.element.android.features.rageshake.api.reporter.ReportType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface BugReporter {
|
||||
/**
|
||||
* Send a bug report.
|
||||
*
|
||||
* @param coroutineScope The coroutine scope
|
||||
* @param reportType The report type (bug, suggestion, feedback)
|
||||
* @param withDevicesLogs true to include the device log
|
||||
* @param withCrashLogs true to include the crash logs
|
||||
|
|
@ -36,8 +31,7 @@ interface BugReporter {
|
|||
* @param customFields fields which will be sent with the report
|
||||
* @param listener the listener
|
||||
*/
|
||||
fun sendBugReport(
|
||||
coroutineScope: CoroutineScope,
|
||||
suspend fun sendBugReport(
|
||||
reportType: ReportType,
|
||||
withDevicesLogs: Boolean,
|
||||
withCrashLogs: Boolean,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ dependencies {
|
|||
api(libs.squareup.seismic)
|
||||
api(projects.features.rageshake.api)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation("com.squareup.okhttp3:okhttp")
|
||||
implementation(libs.coil)
|
||||
implementation(libs.coil.compose)
|
||||
ksp(libs.showkase.processor)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ package io.element.android.features.rageshake.impl.bugreport
|
|||
sealed interface BugReportEvents {
|
||||
object SendBugReport : BugReportEvents
|
||||
object ResetAll : BugReportEvents
|
||||
object ClearError : BugReportEvents
|
||||
|
||||
data class SetDescription(val description: String) : BugReportEvents
|
||||
data class SetSendLog(val sendLog: Boolean) : BugReportEvents
|
||||
data class SetSendCrashLog(val sendCrashlog: Boolean) : BugReportEvents
|
||||
|
|
|
|||
|
|
@ -109,6 +109,10 @@ class BugReportPresenter @Inject constructor(
|
|||
is BugReportEvents.SetSendScreenshot -> updateFormState(formState) {
|
||||
copy(sendScreenshot = event.sendScreenshot)
|
||||
}
|
||||
BugReportEvents.ClearError -> {
|
||||
sendingProgress.value = 0f
|
||||
sendingAction.value = Async.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +136,6 @@ class BugReportPresenter @Inject constructor(
|
|||
listener: BugReporterListener,
|
||||
) = launch {
|
||||
bugReporter.sendBugReport(
|
||||
coroutineScope = this,
|
||||
reportType = ReportType.BUG_REPORT,
|
||||
withDevicesLogs = formState.sendLogs,
|
||||
withCrashLogs = hasCrashLogs && formState.sendCrashLogs,
|
||||
|
|
|
|||
|
|
@ -197,13 +197,14 @@ fun BugReportView(
|
|||
}
|
||||
when (state.sending) {
|
||||
is Async.Loading -> {
|
||||
// Indeterminate indicator, to avoid the freeze effect if the connection takes time to initialize.
|
||||
CircularProgressIndicator(
|
||||
progress = state.sendingProgress,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
is Async.Failure -> ErrorDialog(
|
||||
content = state.sending.error.toString(),
|
||||
onDismiss = { state.eventSink(BugReportEvents.ClearError) }
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ import android.os.Build
|
|||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporterListener
|
||||
import io.element.android.features.rageshake.api.reporter.ReportType
|
||||
import io.element.android.features.rageshake.impl.R
|
||||
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.impl.logs.VectorFileLogger
|
||||
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
||||
import io.element.android.features.rageshake.impl.R
|
||||
import io.element.android.features.rageshake.impl.logs.VectorFileLogger
|
||||
import io.element.android.libraries.androidutils.file.compressFile
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
|
@ -35,9 +35,7 @@ import io.element.android.libraries.core.extensions.toOnOff
|
|||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Call
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
|
|
@ -64,6 +62,7 @@ class DefaultBugReporter @Inject constructor(
|
|||
private val screenshotHolder: ScreenshotHolder,
|
||||
private val crashDataStore: CrashDataStore,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
/*
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val versionProvider: VersionProvider,
|
||||
|
|
@ -88,9 +87,6 @@ class DefaultBugReporter @Inject constructor(
|
|||
private const val BUFFER_SIZE = 1024 * 1024 * 50
|
||||
}
|
||||
|
||||
// the http client
|
||||
private val mOkHttpClient = OkHttpClient()
|
||||
|
||||
// the pending bug report call
|
||||
private var mBugReportCall: Call? = null
|
||||
|
||||
|
|
@ -118,7 +114,6 @@ class DefaultBugReporter @Inject constructor(
|
|||
/**
|
||||
* Send a bug report.
|
||||
*
|
||||
* @param coroutineScope The coroutine scope
|
||||
* @param reportType The report type (bug, suggestion, feedback)
|
||||
* @param withDevicesLogs true to include the device log
|
||||
* @param withCrashLogs true to include the crash logs
|
||||
|
|
@ -130,8 +125,7 @@ class DefaultBugReporter @Inject constructor(
|
|||
* @param customFields fields which will be sent with the report
|
||||
* @param listener the listener
|
||||
*/
|
||||
override fun sendBugReport(
|
||||
coroutineScope: CoroutineScope,
|
||||
override suspend fun sendBugReport(
|
||||
reportType: ReportType,
|
||||
withDevicesLogs: Boolean,
|
||||
withCrashLogs: Boolean,
|
||||
|
|
@ -146,282 +140,280 @@ class DefaultBugReporter @Inject constructor(
|
|||
// enumerate files to delete
|
||||
val mBugReportFiles: MutableList<File> = ArrayList()
|
||||
|
||||
coroutineScope.launch {
|
||||
var serverError: String? = null
|
||||
var reportURL: String? = null
|
||||
withContext(coroutineDispatchers.io) {
|
||||
var bugDescription = theBugDescription
|
||||
val crashCallStack = crashDataStore.crashInfo().first()
|
||||
var serverError: String? = null
|
||||
var reportURL: String? = null
|
||||
withContext(coroutineDispatchers.io) {
|
||||
var bugDescription = theBugDescription
|
||||
val crashCallStack = crashDataStore.crashInfo().first()
|
||||
|
||||
if (crashCallStack.isNotEmpty() && withCrashLogs) {
|
||||
bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n"
|
||||
bugDescription += crashCallStack
|
||||
}
|
||||
if (crashCallStack.isNotEmpty() && withCrashLogs) {
|
||||
bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n"
|
||||
bugDescription += crashCallStack
|
||||
}
|
||||
|
||||
val gzippedFiles = ArrayList<File>()
|
||||
val gzippedFiles = ArrayList<File>()
|
||||
|
||||
val vectorFileLogger = VectorFileLogger.getFromTimber()
|
||||
if (withDevicesLogs && vectorFileLogger != null) {
|
||||
val files = vectorFileLogger.getLogFiles()
|
||||
files.mapNotNullTo(gzippedFiles) { f ->
|
||||
if (!mIsCancelled) {
|
||||
compressFile(f)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) {
|
||||
val gzippedLogcat = saveLogCat(false)
|
||||
|
||||
if (null != gzippedLogcat) {
|
||||
if (gzippedFiles.size == 0) {
|
||||
gzippedFiles.add(gzippedLogcat)
|
||||
} else {
|
||||
gzippedFiles.add(0, gzippedLogcat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
activeSessionHolder.getSafeActiveSession()
|
||||
?.takeIf { !mIsCancelled && withKeyRequestHistory }
|
||||
?.cryptoService()
|
||||
?.getGossipingEvents()
|
||||
?.let { GossipingEventsSerializer().serialize(it) }
|
||||
?.toByteArray()
|
||||
?.let { rawByteArray ->
|
||||
File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME)
|
||||
.also {
|
||||
it.outputStream()
|
||||
.use { os -> os.write(rawByteArray) }
|
||||
}
|
||||
}
|
||||
?.let { compressFile(it) }
|
||||
?.let { gzippedFiles.add(it) }
|
||||
*/
|
||||
|
||||
var deviceId = "undefined"
|
||||
var userId = "undefined"
|
||||
var olmVersion = "undefined"
|
||||
|
||||
/*
|
||||
activeSessionHolder.getSafeActiveSession()?.let { session ->
|
||||
userId = session.myUserId
|
||||
deviceId = session.sessionParams.deviceId ?: "undefined"
|
||||
olmVersion = session.cryptoService().getCryptoVersion(context, true)
|
||||
}
|
||||
*/
|
||||
|
||||
if (!mIsCancelled) {
|
||||
val text = when (reportType) {
|
||||
ReportType.BUG_REPORT -> bugDescription
|
||||
ReportType.SUGGESTION -> "[Suggestion] $bugDescription"
|
||||
ReportType.SPACE_BETA_FEEDBACK -> "[spaces-feedback] $bugDescription"
|
||||
ReportType.THREADS_BETA_FEEDBACK -> "[threads-feedback] $bugDescription"
|
||||
ReportType.AUTO_UISI_SENDER,
|
||||
ReportType.AUTO_UISI -> bugDescription
|
||||
}
|
||||
|
||||
// build the multi part request
|
||||
val builder = BugReporterMultipartBody.Builder()
|
||||
.addFormDataPart("text", text)
|
||||
.addFormDataPart("app", rageShakeAppNameForReport(reportType))
|
||||
// .addFormDataPart("user_agent", matrix.getUserAgent())
|
||||
.addFormDataPart("user_id", userId)
|
||||
.addFormDataPart("can_contact", canContact.toString())
|
||||
.addFormDataPart("device_id", deviceId)
|
||||
// .addFormDataPart("version", versionProvider.getVersion(longFormat = true))
|
||||
// .addFormDataPart("branch_name", buildMeta.gitBranchName)
|
||||
// .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion())
|
||||
.addFormDataPart("olm_version", olmVersion)
|
||||
.addFormDataPart("device", Build.MODEL.trim())
|
||||
// .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff())
|
||||
.addFormDataPart("multi_window", inMultiWindowMode.toOnOff())
|
||||
// .addFormDataPart(
|
||||
// "os", Build.VERSION.RELEASE + " (API " + sdkIntProvider.get() + ") " +
|
||||
// Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME
|
||||
// )
|
||||
.addFormDataPart("locale", Locale.getDefault().toString())
|
||||
// .addFormDataPart("app_language", vectorLocale.applicationLocale.toString())
|
||||
// .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString())
|
||||
// .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
|
||||
.addFormDataPart("server_version", serverVersion)
|
||||
.apply {
|
||||
customFields?.forEach { (name, value) ->
|
||||
addFormDataPart(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
// add the gzipped files
|
||||
for (file in gzippedFiles) {
|
||||
builder.addFormDataPart("compressed-log", file.name, file.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull()))
|
||||
}
|
||||
|
||||
mBugReportFiles.addAll(gzippedFiles)
|
||||
|
||||
if (withScreenshot) {
|
||||
screenshotHolder.getFileUri()
|
||||
?.toUri()
|
||||
?.toFile()
|
||||
?.let { screenshotFile ->
|
||||
try {
|
||||
builder.addFormDataPart(
|
||||
"file",
|
||||
screenshotFile.name, screenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## sendBugReport() : fail to write screenshot")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add some github labels
|
||||
// builder.addFormDataPart("label", buildMeta.versionName)
|
||||
// builder.addFormDataPart("label", buildMeta.flavorDescription)
|
||||
// builder.addFormDataPart("label", buildMeta.gitBranchName)
|
||||
|
||||
// Possible values for BuildConfig.BUILD_TYPE: "debug", "nightly", "release".
|
||||
// builder.addFormDataPart("label", BuildConfig.BUILD_TYPE)
|
||||
|
||||
when (reportType) {
|
||||
ReportType.BUG_REPORT -> {
|
||||
/* nop */
|
||||
}
|
||||
ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]")
|
||||
ReportType.SPACE_BETA_FEEDBACK -> builder.addFormDataPart("label", "spaces-feedback")
|
||||
ReportType.THREADS_BETA_FEEDBACK -> builder.addFormDataPart("label", "threads-feedback")
|
||||
ReportType.AUTO_UISI -> {
|
||||
builder.addFormDataPart("label", "Z-UISI")
|
||||
builder.addFormDataPart("label", "android")
|
||||
builder.addFormDataPart("label", "uisi-recipient")
|
||||
}
|
||||
ReportType.AUTO_UISI_SENDER -> {
|
||||
builder.addFormDataPart("label", "Z-UISI")
|
||||
builder.addFormDataPart("label", "android")
|
||||
builder.addFormDataPart("label", "uisi-sender")
|
||||
}
|
||||
}
|
||||
|
||||
if (crashCallStack.isNotEmpty() && withCrashLogs) {
|
||||
builder.addFormDataPart("label", "crash")
|
||||
}
|
||||
|
||||
val requestBody = builder.build()
|
||||
|
||||
// add a progress listener
|
||||
requestBody.setWriteListener { totalWritten, contentLength ->
|
||||
val percentage = if (-1L != contentLength) {
|
||||
if (totalWritten > contentLength) {
|
||||
100
|
||||
} else {
|
||||
(totalWritten * 100 / contentLength).toInt()
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
if (mIsCancelled && null != mBugReportCall) {
|
||||
mBugReportCall!!.cancel()
|
||||
}
|
||||
|
||||
Timber.v("## onWrite() : $percentage%")
|
||||
try {
|
||||
listener?.onProgress(percentage)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onProgress() : failed")
|
||||
}
|
||||
}
|
||||
|
||||
// build the request
|
||||
val request = Request.Builder()
|
||||
.url(context.getString(R.string.bug_report_url))
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
var responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR
|
||||
var response: Response? = null
|
||||
var errorMessage: String? = null
|
||||
|
||||
// trigger the request
|
||||
try {
|
||||
mBugReportCall = mOkHttpClient.newCall(request)
|
||||
response = mBugReportCall!!.execute()
|
||||
responseCode = response.code
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "response")
|
||||
errorMessage = e.localizedMessage
|
||||
}
|
||||
|
||||
// if the upload failed, try to retrieve the reason
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
if (null != errorMessage) {
|
||||
serverError = "Failed with error $errorMessage"
|
||||
} else if (response?.body == null) {
|
||||
serverError = "Failed with error $responseCode"
|
||||
} else {
|
||||
try {
|
||||
val inputStream = response.body!!.byteStream()
|
||||
|
||||
serverError = inputStream.use {
|
||||
buildString {
|
||||
var ch = it.read()
|
||||
while (ch != -1) {
|
||||
append(ch.toChar())
|
||||
ch = it.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the error message
|
||||
serverError?.let {
|
||||
try {
|
||||
val responseJSON = JSONObject(it)
|
||||
serverError = responseJSON.getString("error")
|
||||
} catch (e: JSONException) {
|
||||
Timber.e(e, "doInBackground ; Json conversion failed")
|
||||
}
|
||||
}
|
||||
|
||||
// should never happen
|
||||
if (null == serverError) {
|
||||
serverError = "Failed with error $responseCode"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## sendBugReport() : failed to parse error")
|
||||
}
|
||||
}
|
||||
val vectorFileLogger = VectorFileLogger.getFromTimber()
|
||||
if (withDevicesLogs && vectorFileLogger != null) {
|
||||
val files = vectorFileLogger.getLogFiles()
|
||||
files.mapNotNullTo(gzippedFiles) { f ->
|
||||
if (!mIsCancelled) {
|
||||
compressFile(f)
|
||||
} else {
|
||||
/*
|
||||
reportURL = response?.body?.string()?.let { stringBody ->
|
||||
adapter.fromJson(stringBody)?.get("report_url")?.toString()
|
||||
}
|
||||
*/
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withContext(coroutineDispatchers.main) {
|
||||
mBugReportCall = null
|
||||
if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) {
|
||||
val gzippedLogcat = saveLogCat(false)
|
||||
|
||||
// delete when the bug report has been successfully sent
|
||||
for (file in mBugReportFiles) {
|
||||
file.safeDelete()
|
||||
if (null != gzippedLogcat) {
|
||||
if (gzippedFiles.size == 0) {
|
||||
gzippedFiles.add(gzippedLogcat)
|
||||
} else {
|
||||
gzippedFiles.add(0, gzippedLogcat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
activeSessionHolder.getSafeActiveSession()
|
||||
?.takeIf { !mIsCancelled && withKeyRequestHistory }
|
||||
?.cryptoService()
|
||||
?.getGossipingEvents()
|
||||
?.let { GossipingEventsSerializer().serialize(it) }
|
||||
?.toByteArray()
|
||||
?.let { rawByteArray ->
|
||||
File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME)
|
||||
.also {
|
||||
it.outputStream()
|
||||
.use { os -> os.write(rawByteArray) }
|
||||
}
|
||||
}
|
||||
?.let { compressFile(it) }
|
||||
?.let { gzippedFiles.add(it) }
|
||||
*/
|
||||
|
||||
var deviceId = "undefined"
|
||||
var userId = "undefined"
|
||||
var olmVersion = "undefined"
|
||||
|
||||
/*
|
||||
activeSessionHolder.getSafeActiveSession()?.let { session ->
|
||||
userId = session.myUserId
|
||||
deviceId = session.sessionParams.deviceId ?: "undefined"
|
||||
olmVersion = session.cryptoService().getCryptoVersion(context, true)
|
||||
}
|
||||
*/
|
||||
|
||||
if (!mIsCancelled) {
|
||||
val text = when (reportType) {
|
||||
ReportType.BUG_REPORT -> bugDescription
|
||||
ReportType.SUGGESTION -> "[Suggestion] $bugDescription"
|
||||
ReportType.SPACE_BETA_FEEDBACK -> "[spaces-feedback] $bugDescription"
|
||||
ReportType.THREADS_BETA_FEEDBACK -> "[threads-feedback] $bugDescription"
|
||||
ReportType.AUTO_UISI_SENDER,
|
||||
ReportType.AUTO_UISI -> bugDescription
|
||||
}
|
||||
|
||||
if (null != listener) {
|
||||
try {
|
||||
if (mIsCancelled) {
|
||||
listener.onUploadCancelled()
|
||||
} else if (null == serverError) {
|
||||
listener.onUploadSucceed(reportURL)
|
||||
} else {
|
||||
listener.onUploadFailed(serverError)
|
||||
// build the multi part request
|
||||
val builder = BugReporterMultipartBody.Builder()
|
||||
.addFormDataPart("text", text)
|
||||
.addFormDataPart("app", rageShakeAppNameForReport(reportType))
|
||||
// .addFormDataPart("user_agent", matrix.getUserAgent())
|
||||
.addFormDataPart("user_id", userId)
|
||||
.addFormDataPart("can_contact", canContact.toString())
|
||||
.addFormDataPart("device_id", deviceId)
|
||||
// .addFormDataPart("version", versionProvider.getVersion(longFormat = true))
|
||||
// .addFormDataPart("branch_name", buildMeta.gitBranchName)
|
||||
// .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion())
|
||||
.addFormDataPart("olm_version", olmVersion)
|
||||
.addFormDataPart("device", Build.MODEL.trim())
|
||||
// .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff())
|
||||
.addFormDataPart("multi_window", inMultiWindowMode.toOnOff())
|
||||
// .addFormDataPart(
|
||||
// "os", Build.VERSION.RELEASE + " (API " + sdkIntProvider.get() + ") " +
|
||||
// Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME
|
||||
// )
|
||||
.addFormDataPart("locale", Locale.getDefault().toString())
|
||||
// .addFormDataPart("app_language", vectorLocale.applicationLocale.toString())
|
||||
// .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString())
|
||||
// .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
|
||||
.addFormDataPart("server_version", serverVersion)
|
||||
.apply {
|
||||
customFields?.forEach { (name, value) ->
|
||||
addFormDataPart(name, value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onPostExecute() : failed")
|
||||
}
|
||||
|
||||
// add the gzipped files
|
||||
for (file in gzippedFiles) {
|
||||
builder.addFormDataPart("compressed-log", file.name, file.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull()))
|
||||
}
|
||||
|
||||
mBugReportFiles.addAll(gzippedFiles)
|
||||
|
||||
if (withScreenshot) {
|
||||
screenshotHolder.getFileUri()
|
||||
?.toUri()
|
||||
?.toFile()
|
||||
?.let { screenshotFile ->
|
||||
try {
|
||||
builder.addFormDataPart(
|
||||
"file",
|
||||
screenshotFile.name, screenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## sendBugReport() : fail to write screenshot")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add some github labels
|
||||
// builder.addFormDataPart("label", buildMeta.versionName)
|
||||
// builder.addFormDataPart("label", buildMeta.flavorDescription)
|
||||
// builder.addFormDataPart("label", buildMeta.gitBranchName)
|
||||
|
||||
// Possible values for BuildConfig.BUILD_TYPE: "debug", "nightly", "release".
|
||||
// builder.addFormDataPart("label", BuildConfig.BUILD_TYPE)
|
||||
|
||||
when (reportType) {
|
||||
ReportType.BUG_REPORT -> {
|
||||
/* nop */
|
||||
}
|
||||
ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]")
|
||||
ReportType.SPACE_BETA_FEEDBACK -> builder.addFormDataPart("label", "spaces-feedback")
|
||||
ReportType.THREADS_BETA_FEEDBACK -> builder.addFormDataPart("label", "threads-feedback")
|
||||
ReportType.AUTO_UISI -> {
|
||||
builder.addFormDataPart("label", "Z-UISI")
|
||||
builder.addFormDataPart("label", "android")
|
||||
builder.addFormDataPart("label", "uisi-recipient")
|
||||
}
|
||||
ReportType.AUTO_UISI_SENDER -> {
|
||||
builder.addFormDataPart("label", "Z-UISI")
|
||||
builder.addFormDataPart("label", "android")
|
||||
builder.addFormDataPart("label", "uisi-sender")
|
||||
}
|
||||
}
|
||||
|
||||
if (crashCallStack.isNotEmpty() && withCrashLogs) {
|
||||
builder.addFormDataPart("label", "crash")
|
||||
}
|
||||
|
||||
val requestBody = builder.build()
|
||||
|
||||
// add a progress listener
|
||||
requestBody.setWriteListener { totalWritten, contentLength ->
|
||||
val percentage = if (-1L != contentLength) {
|
||||
if (totalWritten > contentLength) {
|
||||
100
|
||||
} else {
|
||||
(totalWritten * 100 / contentLength).toInt()
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
if (mIsCancelled && null != mBugReportCall) {
|
||||
mBugReportCall!!.cancel()
|
||||
}
|
||||
|
||||
Timber.v("## onWrite() : $percentage%")
|
||||
try {
|
||||
listener?.onProgress(percentage)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onProgress() : failed")
|
||||
}
|
||||
}
|
||||
|
||||
// build the request
|
||||
val request = Request.Builder()
|
||||
.url(context.getString(R.string.bug_report_url))
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
var responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR
|
||||
var response: Response? = null
|
||||
var errorMessage: String? = null
|
||||
|
||||
// trigger the request
|
||||
try {
|
||||
mBugReportCall = okHttpClient.newCall(request)
|
||||
response = mBugReportCall!!.execute()
|
||||
responseCode = response.code
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "response")
|
||||
errorMessage = e.localizedMessage
|
||||
}
|
||||
|
||||
// if the upload failed, try to retrieve the reason
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
if (null != errorMessage) {
|
||||
serverError = "Failed with error $errorMessage"
|
||||
} else if (response?.body == null) {
|
||||
serverError = "Failed with error $responseCode"
|
||||
} else {
|
||||
try {
|
||||
val inputStream = response.body!!.byteStream()
|
||||
|
||||
serverError = inputStream.use {
|
||||
buildString {
|
||||
var ch = it.read()
|
||||
while (ch != -1) {
|
||||
append(ch.toChar())
|
||||
ch = it.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the error message
|
||||
serverError?.let {
|
||||
try {
|
||||
val responseJSON = JSONObject(it)
|
||||
serverError = responseJSON.getString("error")
|
||||
} catch (e: JSONException) {
|
||||
Timber.e(e, "doInBackground ; Json conversion failed")
|
||||
}
|
||||
}
|
||||
|
||||
// should never happen
|
||||
if (null == serverError) {
|
||||
serverError = "Failed with error $responseCode"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## sendBugReport() : failed to parse error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
reportURL = response?.body?.string()?.let { stringBody ->
|
||||
adapter.fromJson(stringBody)?.get("report_url")?.toString()
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withContext(coroutineDispatchers.main) {
|
||||
mBugReportCall = null
|
||||
|
||||
// delete when the bug report has been successfully sent
|
||||
for (file in mBugReportFiles) {
|
||||
file.safeDelete()
|
||||
}
|
||||
|
||||
if (null != listener) {
|
||||
try {
|
||||
if (mIsCancelled) {
|
||||
listener.onUploadCancelled()
|
||||
} else if (null == serverError) {
|
||||
listener.onUploadSucceed(reportURL)
|
||||
} else {
|
||||
listener.onUploadFailed(serverError)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onPostExecute() : failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -492,9 +484,9 @@ class DefaultBugReporter @Inject constructor(
|
|||
|
||||
return compressFile(logCatErrFile)
|
||||
} catch (error: OutOfMemoryError) {
|
||||
Timber.e(error, "## saveLogCat() : fail to write logcat$error")
|
||||
Timber.e(error, "## saveLogCat() : fail to write logcat OOM")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## saveLogCat() : fail to write logcat$e")
|
||||
Timber.e(e, "## saveLogCat() : fail to write logcat")
|
||||
}
|
||||
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -222,6 +222,11 @@ class BugReportPresenterTest {
|
|||
// Failure
|
||||
assertThat(awaitItem().sendingProgress).isEqualTo(0f)
|
||||
assertThat((awaitItem().sending as Async.Failure).error.message).isEqualTo(A_FAILURE_REASON)
|
||||
// Reset failure
|
||||
initialState.eventSink.invoke(BugReportEvents.ClearError)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.sendingProgress).isEqualTo(0f)
|
||||
assertThat(lastItem.sending).isInstanceOf(Async.Uninitialized::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,10 @@ import io.element.android.features.rageshake.api.reporter.BugReporter
|
|||
import io.element.android.features.rageshake.api.reporter.BugReporterListener
|
||||
import io.element.android.features.rageshake.api.reporter.ReportType
|
||||
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter {
|
||||
override fun sendBugReport(
|
||||
coroutineScope: CoroutineScope,
|
||||
override suspend fun sendBugReport(
|
||||
reportType: ReportType,
|
||||
withDevicesLogs: Boolean,
|
||||
withCrashLogs: Boolean,
|
||||
|
|
@ -38,27 +35,25 @@ class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Succes
|
|||
customFields: Map<String, String>?,
|
||||
listener: BugReporterListener?,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
delay(100)
|
||||
listener?.onProgress(0)
|
||||
delay(100)
|
||||
listener?.onProgress(50)
|
||||
delay(100)
|
||||
when (mode) {
|
||||
FakeBugReporterMode.Success -> Unit
|
||||
FakeBugReporterMode.Failure -> {
|
||||
listener?.onUploadFailed(A_FAILURE_REASON)
|
||||
return@launch
|
||||
}
|
||||
FakeBugReporterMode.Cancel -> {
|
||||
listener?.onUploadCancelled()
|
||||
return@launch
|
||||
}
|
||||
delay(100)
|
||||
listener?.onProgress(0)
|
||||
delay(100)
|
||||
listener?.onProgress(50)
|
||||
delay(100)
|
||||
when (mode) {
|
||||
FakeBugReporterMode.Success -> Unit
|
||||
FakeBugReporterMode.Failure -> {
|
||||
listener?.onUploadFailed(A_FAILURE_REASON)
|
||||
return
|
||||
}
|
||||
FakeBugReporterMode.Cancel -> {
|
||||
listener?.onUploadCancelled()
|
||||
return
|
||||
}
|
||||
listener?.onProgress(100)
|
||||
delay(100)
|
||||
listener?.onUploadSucceed(null)
|
||||
}
|
||||
listener?.onProgress(100)
|
||||
delay(100)
|
||||
listener?.onUploadSucceed(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,11 @@ accompanist_flowlayout = { module = "com.google.accompanist:accompanist-flowlayo
|
|||
# Libraries
|
||||
squareup_seismic = "com.squareup:seismic:1.0.3"
|
||||
|
||||
# network
|
||||
network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.10.0"
|
||||
network_retrofit = "com.squareup.retrofit2:retrofit:2.9.0"
|
||||
network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
|
||||
|
||||
# Test
|
||||
test_core = { module = "androidx.test:core", version.ref = "test_core" }
|
||||
test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
|
|
@ -29,6 +28,8 @@ java {
|
|||
|
||||
dependencies {
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.meta
|
||||
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
||||
data class BuildMeta(
|
||||
val isDebug: Boolean,
|
||||
val applicationName: String,
|
||||
val applicationId: String,
|
||||
val lowPrivacyLoggingEnabled: Boolean,
|
||||
val versionName: String,
|
||||
val gitRevision: String,
|
||||
val gitRevisionDate: String,
|
||||
val gitBranchName: String,
|
||||
val flavorDescription: String,
|
||||
val flavorShortDescription: String,
|
||||
val okHttpLoggingLevel: HttpLoggingInterceptor.Level,
|
||||
)
|
||||
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.progressSemantics
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -23,8 +25,11 @@ import androidx.compose.material3.ProgressIndicatorDefaults
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
||||
@Composable
|
||||
fun CircularProgressIndicator(
|
||||
|
|
@ -54,6 +59,27 @@ fun CircularProgressIndicator(
|
|||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun CircularProgressIndicatorLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun CircularProgressIndicatorDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
// Indeterminate progress
|
||||
CircularProgressIndicator(
|
||||
)
|
||||
// Fixed progress
|
||||
CircularProgressIndicator(
|
||||
progress = 0.75F
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ButtonCircularProgressIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -21,15 +21,18 @@ import coil.ImageLoader
|
|||
import coil.ImageLoaderFactory
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoggedInImageLoaderFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
) : ImageLoaderFactory {
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader
|
||||
.Builder(context)
|
||||
.okHttpClient(okHttpClient)
|
||||
.components {
|
||||
add(AvatarKeyer())
|
||||
add(MediaKeyer())
|
||||
|
|
@ -42,10 +45,12 @@ class LoggedInImageLoaderFactory @Inject constructor(
|
|||
|
||||
class NotLoggedInImageLoaderFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
) : ImageLoaderFactory {
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader
|
||||
.Builder(context)
|
||||
.okHttpClient(okHttpClient)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
41
libraries/network/build.gradle.kts
Normal file
41
libraries/network/build.gradle.kts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.network"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation("com.squareup.okhttp3:okhttp")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||
|
||||
implementation(libs.network.retrofit)
|
||||
implementation(libs.network.retrofit.converter.serialization)
|
||||
implementation(libs.serialization.json)
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger
|
||||
import java.util.concurrent.TimeUnit
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@JvmStatic
|
||||
fun providesHttpLoggingInterceptor(buildMeta: BuildMeta): HttpLoggingInterceptor {
|
||||
val logger = FormattedJsonHttpLogger(buildMeta.okHttpLoggingLevel)
|
||||
val interceptor = HttpLoggingInterceptor(logger)
|
||||
interceptor.level = buildMeta.okHttpLoggingLevel
|
||||
return interceptor
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesOkHttpClient(
|
||||
httpLoggingInterceptor: HttpLoggingInterceptor,
|
||||
): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
// workaround for #4669
|
||||
.protocols(listOf(Protocol.HTTP_1_1))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.addInterceptor(httpLoggingInterceptor)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import dagger.Lazy
|
||||
import io.element.android.libraries.core.uri.ensureTrailingSlash
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import javax.inject.Inject
|
||||
|
||||
class RetrofitFactory @Inject constructor(
|
||||
private val okHttpClient: Lazy<OkHttpClient>,
|
||||
) {
|
||||
fun create(baseUrl: String): Retrofit {
|
||||
val contentType = "application/json".toMediaType()
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl.ensureTrailingSlash())
|
||||
.addConverterFactory(Json.asConverterFactory(contentType))
|
||||
.callFactory { request -> okHttpClient.get().newCall(request) }
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network.interceptors
|
||||
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
|
||||
internal class FormattedJsonHttpLogger(
|
||||
private val level: HttpLoggingInterceptor.Level
|
||||
) : HttpLoggingInterceptor.Logger {
|
||||
|
||||
companion object {
|
||||
private const val INDENT_SPACE = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the message and try to log it again as a JSON formatted string.
|
||||
* Note: it can consume a lot of memory but it is only in DEBUG mode.
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
@Synchronized
|
||||
override fun log(message: String) {
|
||||
Timber.v(message)
|
||||
|
||||
// Try to log formatted Json only if there is a chance that [message] contains Json.
|
||||
// It can be only the case if we log the bodies of Http requests.
|
||||
if (level != HttpLoggingInterceptor.Level.BODY) return
|
||||
|
||||
if (message.startsWith("{")) {
|
||||
// JSON Detected
|
||||
try {
|
||||
val o = JSONObject(message)
|
||||
logJson(o.toString(INDENT_SPACE))
|
||||
} catch (e: JSONException) {
|
||||
// Finally this is not a JSON string...
|
||||
Timber.e(e)
|
||||
}
|
||||
} else if (message.startsWith("[")) {
|
||||
// JSON Array detected
|
||||
try {
|
||||
val o = JSONArray(message)
|
||||
logJson(o.toString(INDENT_SPACE))
|
||||
} catch (e: JSONException) {
|
||||
// Finally not JSON...
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
// Else not a json string to log
|
||||
}
|
||||
|
||||
private fun logJson(formattedJson: String) {
|
||||
formattedJson
|
||||
.lines()
|
||||
.dropLastWhile { it.isEmpty() }
|
||||
.forEach { Timber.v(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
|||
implementation(project(":libraries:designsystem"))
|
||||
implementation(project(":libraries:matrix:impl"))
|
||||
implementation(project(":libraries:matrixui"))
|
||||
implementation(project(":libraries:network"))
|
||||
implementation(project(":libraries:core"))
|
||||
implementation(project(":libraries:architecture"))
|
||||
implementation(project(":libraries:dateformatter:impl"))
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ include(":libraries:dateformatter:api")
|
|||
include(":libraries:dateformatter:impl")
|
||||
include(":libraries:dateformatter:test")
|
||||
include(":libraries:elementresources")
|
||||
include(":libraries:network")
|
||||
include(":libraries:ui-strings")
|
||||
include(":libraries:testtags")
|
||||
include(":libraries:designsystem")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5c34404a33be8eb234025442da98fc85e29236769c200bd0e40cc2cf8f9db3e4
|
||||
size 52604
|
||||
oid sha256:9b432f300926890ddc7dcc59051b3b31a2a1185ba1d527d776378547433905d9
|
||||
size 52789
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e628e65234a8632350cdc9ab014b36f3c2bd9e35977bc6d919314a32c2f8a1cf
|
||||
size 50965
|
||||
oid sha256:08cb1e38ee31bf4931d3835d471e61e5cc51d8b6ca5fca3d3044ba85686777ea
|
||||
size 51075
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a100c468dce7b2f7d568dcf0890f680c8f46c063fe0f03f20aed90ed0ad12565
|
||||
size 6630
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d7e7e2d22e590304ff9921505bce7eca67846299838a6345dc1d27047f2db33c
|
||||
size 6518
|
||||
Loading…
Add table
Add a link
Reference in a new issue