Browse Source
Cannot use `@ContributesBinding(AppScope::class)`, so provide the implementation in AppModule.kittykat-patch-1
Benoit Marty
2 years ago
5 changed files with 596 additions and 505 deletions
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* 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.features.rageshake.reporter |
||||
|
||||
/** |
||||
* Bug report upload listener. |
||||
*/ |
||||
interface BugReporterListener { |
||||
/** |
||||
* The bug report has been cancelled. |
||||
*/ |
||||
fun onUploadCancelled() |
||||
|
||||
/** |
||||
* The bug report upload failed. |
||||
* |
||||
* @param reason the failure reason |
||||
*/ |
||||
fun onUploadFailed(reason: String?) |
||||
|
||||
/** |
||||
* The upload progress (in percent). |
||||
* |
||||
* @param progress the upload progress |
||||
*/ |
||||
fun onProgress(progress: Int) |
||||
|
||||
/** |
||||
* The bug report upload succeeded. |
||||
*/ |
||||
fun onUploadSucceed(reportUrl: String?) |
||||
} |
@ -0,0 +1,520 @@
@@ -0,0 +1,520 @@
|
||||
/* |
||||
* 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.features.rageshake.reporter |
||||
|
||||
import android.content.Context |
||||
import android.os.Build |
||||
import io.element.android.features.rageshake.R |
||||
import io.element.android.features.rageshake.crash.CrashDataStore |
||||
import io.element.android.features.rageshake.logs.VectorFileLogger |
||||
import io.element.android.features.rageshake.screenshot.ScreenshotHolder |
||||
import io.element.android.libraries.androidutils.file.compressFile |
||||
import io.element.android.libraries.androidutils.file.safeDelete |
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers |
||||
import io.element.android.libraries.core.extensions.toOnOff |
||||
import io.element.android.libraries.core.mimetype.MimeTypes |
||||
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 |
||||
import okhttp3.OkHttpClient |
||||
import okhttp3.Request |
||||
import okhttp3.RequestBody.Companion.asRequestBody |
||||
import okhttp3.Response |
||||
import org.json.JSONException |
||||
import org.json.JSONObject |
||||
import timber.log.Timber |
||||
import java.io.File |
||||
import java.io.IOException |
||||
import java.io.OutputStreamWriter |
||||
import java.net.HttpURLConnection |
||||
import java.util.Locale |
||||
import javax.inject.Inject |
||||
|
||||
/** |
||||
* BugReporter creates and sends the bug reports. |
||||
*/ |
||||
class DefaultBugReporter @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
private val screenshotHolder: ScreenshotHolder, |
||||
private val crashDataStore: CrashDataStore, |
||||
private val coroutineDispatchers: CoroutineDispatchers, |
||||
/* |
||||
private val activeSessionHolder: ActiveSessionHolder, |
||||
private val versionProvider: VersionProvider, |
||||
private val vectorPreferences: VectorPreferences, |
||||
private val vectorFileLogger: VectorFileLogger, |
||||
private val systemLocaleProvider: SystemLocaleProvider, |
||||
private val matrix: Matrix, |
||||
private val buildMeta: BuildMeta, |
||||
private val processInfo: ProcessInfo, |
||||
private val sdkIntProvider: BuildVersionSdkIntProvider, |
||||
private val vectorLocale: VectorLocaleProvider, |
||||
*/ |
||||
) : BugReporter { |
||||
var inMultiWindowMode = false |
||||
|
||||
companion object { |
||||
// filenames |
||||
private const val LOG_CAT_ERROR_FILENAME = "logcatError.log" |
||||
private const val LOG_CAT_FILENAME = "logcat.log" |
||||
private const val KEY_REQUESTS_FILENAME = "keyRequests.log" |
||||
|
||||
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 |
||||
|
||||
// boolean to cancel the bug report |
||||
private val mIsCancelled = false |
||||
|
||||
/* |
||||
val adapter = MatrixJsonParser.getMoshi() |
||||
.adapter<JsonDict>(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java)) |
||||
*/ |
||||
|
||||
private val LOGCAT_CMD_ERROR = arrayOf( |
||||
"logcat", // /< Run 'logcat' command |
||||
"-d", // /< Dump the log rather than continue outputting it |
||||
"-v", // formatting |
||||
"threadtime", // include timestamps |
||||
"AndroidRuntime:E " + // /< Pick all AndroidRuntime errors (such as uncaught exceptions)"communicatorjni:V " + ///< All communicatorjni logging |
||||
"libcommunicator:V " + // /< All libcommunicator logging |
||||
"DEBUG:V " + // /< All DEBUG logging - which includes native land crashes (seg faults, etc) |
||||
"*:S" // /< Everything else silent, so don't pick it.. |
||||
) |
||||
|
||||
private val LOGCAT_CMD_DEBUG = arrayOf("logcat", "-d", "-v", "threadtime", "*:*") |
||||
|
||||
/** |
||||
* 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 |
||||
* @param withKeyRequestHistory true to include the crash logs |
||||
* @param withScreenshot true to include the screenshot |
||||
* @param theBugDescription the bug description |
||||
* @param serverVersion version of the server |
||||
* @param canContact true if the user opt in to be contacted directly |
||||
* @param customFields fields which will be sent with the report |
||||
* @param listener the listener |
||||
*/ |
||||
override fun sendBugReport( |
||||
coroutineScope: CoroutineScope, |
||||
reportType: ReportType, |
||||
withDevicesLogs: Boolean, |
||||
withCrashLogs: Boolean, |
||||
withKeyRequestHistory: Boolean, |
||||
withScreenshot: Boolean, |
||||
theBugDescription: String, |
||||
serverVersion: String, |
||||
canContact: Boolean, |
||||
customFields: Map<String, String>?, |
||||
listener: BugReporterListener? |
||||
) { |
||||
// 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() |
||||
|
||||
if (crashCallStack.isNotEmpty() && withCrashLogs) { |
||||
bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n" |
||||
bugDescription += crashCallStack |
||||
} |
||||
|
||||
val gzippedFiles = ArrayList<File>() |
||||
|
||||
val vectorFileLogger = VectorFileLogger.getFromTimber() |
||||
if (withDevicesLogs) { |
||||
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.getFile()?.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") |
||||
} |
||||
} |
||||
} 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") |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Send a bug report either with email or with Vector. |
||||
*/ |
||||
/* TODO Remove |
||||
fun openBugReportScreen(activity: FragmentActivity, reportType: ReportType = ReportType.BUG_REPORT) { |
||||
screenshot = takeScreenshot(activity) |
||||
logDbInfo() |
||||
logProcessInfo() |
||||
logOtherInfo() |
||||
activity.startActivity(BugReportActivity.intent(activity, reportType)) |
||||
} |
||||
*/ |
||||
|
||||
// private fun logOtherInfo() { |
||||
// Timber.i("SyncThread state: " + activeSessionHolder.getSafeActiveSession()?.syncService()?.getSyncState()) |
||||
// } |
||||
|
||||
// private fun logDbInfo() { |
||||
// val dbInfo = matrix.debugService().getDbUsageInfo() |
||||
// Timber.i(dbInfo) |
||||
// } |
||||
|
||||
// private fun logProcessInfo() { |
||||
// val pInfo = processInfo.getInfo() |
||||
// Timber.i(pInfo) |
||||
// } |
||||
|
||||
private fun rageShakeAppNameForReport(reportType: ReportType): String { |
||||
// As per https://github.com/matrix-org/rageshake |
||||
// app: Identifier for the application (eg 'riot-web'). |
||||
// Should correspond to a mapping configured in the configuration file for github issue reporting to work. |
||||
// (see R.string.bug_report_url for configured RS server) |
||||
return context.getString( |
||||
when (reportType) { |
||||
ReportType.AUTO_UISI_SENDER, |
||||
ReportType.AUTO_UISI -> R.string.bug_report_auto_uisi_app_name |
||||
else -> R.string.bug_report_app_name |
||||
} |
||||
) |
||||
} |
||||
|
||||
// ============================================================================================================== |
||||
// Logcat management |
||||
// ============================================================================================================== |
||||
|
||||
/** |
||||
* Save the logcat. |
||||
* |
||||
* @param isErrorLogcat true to save the error logcat |
||||
* @return the file if the operation succeeds |
||||
*/ |
||||
private fun saveLogCat(isErrorLogcat: Boolean): File? { |
||||
val logCatErrFile = File(context.cacheDir.absolutePath, if (isErrorLogcat) LOG_CAT_ERROR_FILENAME else LOG_CAT_FILENAME) |
||||
|
||||
if (logCatErrFile.exists()) { |
||||
logCatErrFile.safeDelete() |
||||
} |
||||
|
||||
try { |
||||
logCatErrFile.writer().use { |
||||
getLogCatError(it, isErrorLogcat) |
||||
} |
||||
|
||||
return compressFile(logCatErrFile) |
||||
} catch (error: OutOfMemoryError) { |
||||
Timber.e(error, "## saveLogCat() : fail to write logcat$error") |
||||
} catch (e: Exception) { |
||||
Timber.e(e, "## saveLogCat() : fail to write logcat$e") |
||||
} |
||||
|
||||
return null |
||||
} |
||||
|
||||
/** |
||||
* Retrieves the logs. |
||||
* |
||||
* @param streamWriter the stream writer |
||||
* @param isErrorLogCat true to save the error logs |
||||
*/ |
||||
private fun getLogCatError(streamWriter: OutputStreamWriter, isErrorLogCat: Boolean) { |
||||
val logcatProc: Process |
||||
|
||||
try { |
||||
logcatProc = Runtime.getRuntime().exec(if (isErrorLogCat) LOGCAT_CMD_ERROR else LOGCAT_CMD_DEBUG) |
||||
} catch (e1: IOException) { |
||||
return |
||||
} |
||||
|
||||
try { |
||||
val separator = System.getProperty("line.separator") |
||||
logcatProc.inputStream |
||||
.reader() |
||||
.buffered(BUFFER_SIZE) |
||||
.forEachLine { line -> |
||||
streamWriter.append(line) |
||||
streamWriter.append(separator) |
||||
} |
||||
} catch (e: IOException) { |
||||
Timber.e(e, "getLog fails") |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue