Skip to content
Snippets Groups Projects
Commit c5b0e649 authored by Adrien Béraud's avatar Adrien Béraud
Browse files

tombstone: parse last crash reason

Change-Id: I5097748eea936670d1d0a36f994c01159b17f442
parent 0f6cf118
Branches
Tags
No related merge requests found
......@@ -8,6 +8,7 @@ plugins {
kotlin("android")
kotlin("kapt")
id("dagger.hilt.android.plugin")
id("com.google.protobuf") version "0.9.3"
}
android {
......@@ -117,6 +118,7 @@ dependencies {
implementation ("org.osmdroid:osmdroid-android:6.1.16")
implementation ("io.noties.markwon:core:$markwon_version")
implementation ("io.noties.markwon:linkify:$markwon_version")
implementation ("com.google.protobuf:protobuf-javalite:3.23.2")
// ORM
implementation ("com.j256.ormlite:ormlite-android:5.7")
......@@ -157,6 +159,21 @@ dependencies {
"withUnifiedPushImplementation"("com.github.UnifiedPush:android-connector:2.1.0")
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.23.2"
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
}
}
}
}
kapt {
correctErrorTypes = true
}
......
......@@ -34,7 +34,6 @@ import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.util.Log
import android.view.WindowManager
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import com.bumptech.glide.Glide
import cx.ring.BuildConfig
......@@ -219,6 +218,7 @@ abstract class JamiApplication : Application() {
RxJavaPlugins.setErrorHandler { e -> Log.e(TAG, "Unhandled RxJava error", e) }
}
// Initialize the Android Telecom API if available
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getSystemService<TelecomManager>()?.let { telecomService ->
val componentName = ComponentName(this, ConnectionService::class.java)
......
......@@ -18,22 +18,29 @@
*/
package cx.ring.client
import android.content.Context
import android.app.ActivityManager
import android.app.ApplicationExitInfo
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.snackbar.Snackbar
import cx.ring.R
import cx.ring.application.JamiApplication
import cx.ring.databinding.ActivityLogsBinding
import cx.ring.databinding.CrashReportBinding
import cx.ring.utils.AndroidFileUtils
import cx.ring.utils.ContentUriHandler
import dagger.hilt.android.AndroidEntryPoint
......@@ -42,10 +49,13 @@ import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import net.jami.android.tombstone.TombstoneProtos.Tombstone
import net.jami.services.HardwareService
import net.jami.utils.StringUtils
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton
......@@ -68,7 +78,7 @@ class LogsActivity : AppCompatActivity() {
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
fileSaver = registerForActivityResult(ActivityResultContracts.CreateDocument()) { result: Uri? ->
fileSaver = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { result: Uri? ->
if (result != null)
compositeDisposable.add(AndroidFileUtils.copyFileToUri(contentResolver, mCurrentFile, result)
.observeOn(AndroidSchedulers.mainThread()).subscribe({
......@@ -80,6 +90,11 @@ class LogsActivity : AppCompatActivity() {
})
}
binding.fab.setOnClickListener { if (disposable == null) startLogging() else stopLogging() }
// Check for previous crash reasons, if any.
if (savedInstanceState == null)
showNativeCrashes()
if (mHardwareService.isLogging) startLogging()
}
......@@ -88,6 +103,28 @@ class LogsActivity : AppCompatActivity() {
return super.onCreateOptionsMenu(menu)
}
fun shareLogs(uriMaybe: Maybe<Uri>) {
compositeDisposable.add(uriMaybe
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ uri: Uri ->
Log.w(TAG, "saved logs to $uri")
val sendIntent = Intent(Intent.ACTION_SEND).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
setDataAndType(uri, contentResolver.getType(uri))
putExtra(Intent.EXTRA_STREAM, uri)
}
startActivity(Intent.createChooser(sendIntent, null))
}) { e: Throwable ->
Snackbar.make(binding.root, "Error sharing logs: " + e.localizedMessage, Snackbar.LENGTH_SHORT).show()
})
}
fun saveFile(fileMaybe: Maybe<File>) {
compositeDisposable.add(fileMaybe.subscribe { file: File ->
mCurrentFile = file
fileSaver.launch(file.name)
})
}
private val log: Maybe<String>
get() {
if (mHardwareService.isLogging)
......@@ -116,37 +153,108 @@ class LogsActivity : AppCompatActivity() {
return true
}
R.id.menu_log_share -> {
compositeDisposable.add(logUri.observeOn(AndroidSchedulers.mainThread())
.subscribe({ uri: Uri ->
Log.w(TAG, "saved logs to $uri")
val sendIntent = Intent(Intent.ACTION_SEND).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
setDataAndType(uri, contentResolver.getType(uri))
putExtra(Intent.EXTRA_STREAM, uri)
}
startActivity(Intent.createChooser(sendIntent, null))
}) { e: Throwable ->
Snackbar.make(binding.root, "Error sharing logs: " + e.localizedMessage, Snackbar.LENGTH_SHORT).show()
})
shareLogs(logUri)
return true
}
R.id.menu_log_save -> {
compositeDisposable.add(logFile.subscribe { file: File ->
mCurrentFile = file
fileSaver.launch(file.name)
})
saveFile(logFile)
return true
}
R.id.menu_log_crashes -> {
showNativeCrashes(true)
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
internal class CrashBottomSheet : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
= CrashReportBinding.inflate(inflater, container, false).apply {
crash.text = arguments?.getString("crash")
toolbar.menu.findItem(R.id.menu_log_crashes)?.isVisible = false
toolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_log_share -> {
(activity as LogsActivity).shareLogs(crashUri)
true
}
R.id.menu_log_save -> {
(activity as LogsActivity).saveFile(crashFile)
true
}
else -> super.onOptionsItemSelected(it)
}
}
}.root
private val crashFile: Maybe<File> by lazy {
val crashReport = arguments?.getString("crash")?.toByteArray() ?: return@lazy Maybe.empty()
val crashFile = AndroidFileUtils.createLogFile(requireContext())
FileOutputStream(crashFile).use { it.write(crashReport) }
return@lazy Maybe.just(crashFile)
}
private val crashUri: Maybe<Uri> by lazy {
crashFile.map { file: File ->
ContentUriHandler.getUriForFile(requireContext(), ContentUriHandler.AUTHORITY_FILES, file)
}
}
companion object {
const val TAG = "CrashBottomSheet"
}
}
private fun showNativeCrashes(userInitiated: Boolean = false) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return
val activityManager = getSystemService<ActivityManager>() ?: return
val exitReasons: MutableList<ApplicationExitInfo> =
activityManager.getHistoricalProcessExitReasons(/* packageName = */ null, /* pid = */0, /* maxNum = */5)
val stringStream = StringBuilder()
exitReasons.forEachIndexed { index, aei ->
if (aei.reason == ApplicationExitInfo.REASON_CRASH_NATIVE) {
try {
val trace: InputStream = aei.traceInputStream ?: return@forEachIndexed
val time = Instant.ofEpochMilli(aei.timestamp)
stringStream.append("Previous native crash #$index at ${time}: ${aei.description} ${aei.reason} ${aei.pid} ${aei.processName}\n")
val tombstone: Tombstone = Tombstone.parseFrom(trace)
stringStream.append("Tombstone ${tombstone.tid} ${tombstone.abortMessage}\n")
tombstone.causesList.forEachIndexed { i, cause ->
stringStream.append("Cause $i: ${cause.humanReadable}\n")
}
tombstone.threadsMap[tombstone.tid]?.currentBacktraceList?.forEachIndexed { index, frame ->
stringStream.append("\t#$index ${frame.fileName} ${frame.functionName}+${frame.functionOffset}\n")
}
// Enable to print all threads backtrace
/*tombstone.threadsMap.values.forEach { thread ->
Log.w(TAG, "Backstack for thread ${thread.id} ${thread.name}:")
thread.currentBacktraceOrBuilderList.forEachIndexed { index, frame ->
Log.w(TAG, "#$index ${frame.fileName} ${frame.functionName}+${frame.functionOffset}")
}
}*/
} catch (e: IOException) {
Log.e(TAG, "Failed to parse tombstone", e)
}
}
}
val crashText = stringStream.toString()
if (crashText.isNotEmpty()) {
CrashBottomSheet().apply {
arguments = Bundle().apply {
putString("crash", stringStream.toString())
}
}.show(supportFragmentManager, CrashBottomSheet.TAG)
} else if (userInitiated) {
Snackbar.make(binding.root, "No recent native crash", Snackbar.LENGTH_SHORT).show()
}
}
private fun startLogging() {
// Allows to start logging at application startup.
mHardwareService.mPreferenceService.isLogActive = true
binding.logView.text = ""
//disposable =
compositeDisposable.add(mHardwareService.startLogs()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ message: String ->
......
......@@ -73,10 +73,6 @@ object ContentUriHandler {
FileProvider.getUriForFile(context, authority, file, displayName)
} catch (e: IllegalArgumentException) {
if (HUAWEI_MANUFACTURER.equals(Build.MANUFACTURER, ignoreCase = true)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Log.w(TAG, "Returning Uri.fromFile to avoid Huawei 'external-files-path' bug for pre-N devices", e)
Uri.fromFile(file)
} else {
Log.w(TAG, "ANR Risk -- Copying the file the location cache to avoid Huawei 'external-files-path' bug for N+ devices", e)
// Note: Periodically clear this cache
val cacheFolder = File(context.cacheDir, HUAWEI_MANUFACTURER)
......@@ -90,7 +86,6 @@ object ContentUriHandler {
}
Log.e(TAG, "Failed to copy the Huawei file. Re-throwing exception")
throw IllegalArgumentException("Huawei devices are unsupported for Android N")
}
} else {
throw e
}
......
//
// Protobuf definition for Android tombstones.
//
// An app can get hold of these for any `REASON_CRASH_NATIVE` instance of
// `android.app.ApplicationExitInfo`.
//
// https://developer.android.com/reference/android/app/ApplicationExitInfo#getTraceInputStream()
//
syntax = "proto3";
option java_package = "net.jami.android.tombstone";
option java_outer_classname = "TombstoneProtos";
// NOTE TO OEMS:
// If you add custom fields to this proto, do not use numbers in the reserved range.
message Tombstone {
Architecture arch = 1;
string build_fingerprint = 2;
string revision = 3;
string timestamp = 4;
uint32 pid = 5;
uint32 tid = 6;
uint32 uid = 7;
string selinux_label = 8;
repeated string command_line = 9;
// Process uptime in seconds.
uint32 process_uptime = 20;
Signal signal_info = 10;
string abort_message = 14;
repeated Cause causes = 15;
map<uint32, Thread> threads = 16;
repeated MemoryMapping memory_mappings = 17;
repeated LogBuffer log_buffers = 18;
repeated FD open_fds = 19;
reserved 21 to 999;
}
enum Architecture {
ARM32 = 0;
ARM64 = 1;
X86 = 2;
X86_64 = 3;
RISCV64 = 4;
reserved 5 to 999;
}
message Signal {
int32 number = 1;
string name = 2;
int32 code = 3;
string code_name = 4;
bool has_sender = 5;
int32 sender_uid = 6;
int32 sender_pid = 7;
bool has_fault_address = 8;
uint64 fault_address = 9;
// Note, may or may not contain the dump of the actual memory contents. Currently, on arm64, we
// only include metadata, and not the contents.
MemoryDump fault_adjacent_metadata = 10;
reserved 11 to 999;
}
message HeapObject {
uint64 address = 1;
uint64 size = 2;
uint64 allocation_tid = 3;
repeated BacktraceFrame allocation_backtrace = 4;
uint64 deallocation_tid = 5;
repeated BacktraceFrame deallocation_backtrace = 6;
}
message MemoryError {
enum Tool {
GWP_ASAN = 0;
SCUDO = 1;
reserved 2 to 999;
}
Tool tool = 1;
enum Type {
UNKNOWN = 0;
USE_AFTER_FREE = 1;
DOUBLE_FREE = 2;
INVALID_FREE = 3;
BUFFER_OVERFLOW = 4;
BUFFER_UNDERFLOW = 5;
reserved 6 to 999;
}
Type type = 2;
oneof location {
HeapObject heap = 3;
}
reserved 4 to 999;
}
message Cause {
string human_readable = 1;
oneof details {
MemoryError memory_error = 2;
}
reserved 3 to 999;
}
message Register {
string name = 1;
uint64 u64 = 2;
reserved 3 to 999;
}
message Thread {
int32 id = 1;
string name = 2;
repeated Register registers = 3;
repeated string backtrace_note = 7;
repeated string unreadable_elf_files = 9;
repeated BacktraceFrame current_backtrace = 4;
repeated MemoryDump memory_dump = 5;
int64 tagged_addr_ctrl = 6;
int64 pac_enabled_keys = 8;
reserved 10 to 999;
}
message BacktraceFrame {
uint64 rel_pc = 1;
uint64 pc = 2;
uint64 sp = 3;
string function_name = 4;
uint64 function_offset = 5;
string file_name = 6;
uint64 file_map_offset = 7;
string build_id = 8;
reserved 9 to 999;
}
message ArmMTEMetadata {
// One memory tag per granule (e.g. every 16 bytes) of regular memory.
bytes memory_tags = 1;
reserved 2 to 999;
}
message MemoryDump {
string register_name = 1;
string mapping_name = 2;
uint64 begin_address = 3;
bytes memory = 4;
oneof metadata {
ArmMTEMetadata arm_mte_metadata = 6;
}
reserved 5, 7 to 999;
}
message MemoryMapping {
uint64 begin_address = 1;
uint64 end_address = 2;
uint64 offset = 3;
bool read = 4;
bool write = 5;
bool execute = 6;
string mapping_name = 7;
string build_id = 8;
uint64 load_bias = 9;
reserved 10 to 999;
}
message FD {
int32 fd = 1;
string path = 2;
string owner = 3;
uint64 tag = 4;
reserved 5 to 999;
}
message LogBuffer {
string name = 1;
repeated LogMessage logs = 2;
reserved 3 to 999;
}
message LogMessage {
string timestamp = 1;
uint32 pid = 2;
uint32 tid = 3;
uint32 priority = 4;
string tag = 5;
string message = 6;
reserved 7 to 999;
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5s-0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM16,12v3c0,0.22 -0.03,0.47 -0.07,0.7l-0.1,0.65 -0.37,0.65c-0.72,1.24 -2.04,2 -3.46,2s-2.74,-0.77 -3.46,-2l-0.37,-0.64 -0.1,-0.65C8.03,15.48 8,15.23 8,15v-4c0,-0.23 0.03,-0.48 0.07,-0.7l0.1,-0.65 0.37,-0.65c0.3,-0.52 0.72,-0.97 1.21,-1.31l0.57,-0.39 0.74,-0.18c0.31,-0.08 0.63,-0.12 0.94,-0.12 0.32,0 0.63,0.04 0.95,0.12l0.68,0.16 0.61,0.42c0.5,0.34 0.91,0.78 1.21,1.31l0.38,0.65 0.1,0.65c0.04,0.22 0.07,0.47 0.07,0.69v1zM10,14h4v2h-4zM10,10h4v2h-4z"/>
</vector>
......@@ -34,6 +34,8 @@
android:layout_height="match_parent"
android:padding="@dimen/padding_medium"
android:textIsSelectable="true"
android:breakStrategy="simple"
android:hyphenationFrequency="none"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:text="@tools:sample/lorem/random" />
</androidx.core.widget.NestedScrollView>
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.Material3.Toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="Last crash report"
app:menu="@menu/logs_menu"/>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/crash"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/padding_small"
android:textIsSelectable="true"
android:breakStrategy="simple"
android:hyphenationFrequency="none"
tools:text="@tools:sample/lorem/random"
/>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
\ No newline at end of file
......@@ -3,6 +3,11 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".client.HomeActivity">
<item
android:id="@+id/menu_log_crashes"
android:icon="@drawable/outline_bug_report_24"
android:title="Crashes"
app:showAsAction="always" />
<item
android:id="@+id/menu_log_share"
......@@ -13,7 +18,7 @@
<item
android:id="@+id/menu_log_save"
android:icon="@drawable/baseline_file_download_24"
android:title="@string/dial_number"
android:title="@string/menu_file_save"
app:showAsAction="always" />
</menu>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment