mirror of
https://github.com/dzeiocom/crashhandler.git
synced 2025-04-22 10:52:16 +00:00
feat: Add crash saving (#12)
This commit is contained in:
parent
1703479865
commit
acbb524a7e
37
.github/scripts/gradlew_recursive.sh
vendored
37
.github/scripts/gradlew_recursive.sh
vendored
@ -1,37 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Copyright (C) 2020 The Android Open Source Project
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
#
|
|
||||||
# https://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.
|
|
||||||
|
|
||||||
set -xe
|
|
||||||
|
|
||||||
# Default Gradle settings are not optimal for Android builds, override them
|
|
||||||
# here to make the most out of the GitHub Actions build servers
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS -Xms4g -Xmx4g"
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS -XX:+HeapDumpOnOutOfMemoryError"
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.daemon=false"
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.workers.max=2"
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.incremental=false"
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.compiler.execution.strategy=in-process"
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS -Dfile.encoding=UTF-8"
|
|
||||||
export GRADLE_OPTS
|
|
||||||
|
|
||||||
# Crawl all gradlew files which indicate an Android project
|
|
||||||
# You may edit this if your repo has a different project structure
|
|
||||||
for GRADLEW in `find . -name "gradlew"` ; do
|
|
||||||
SAMPLE=$(dirname "${GRADLEW}")
|
|
||||||
# Tell Gradle that this is a CI environment and disable parallel compilation
|
|
||||||
bash "$GRADLEW" -p "$SAMPLE" -Pci --no-parallel --stacktrace $@
|
|
||||||
done
|
|
||||||
|
|
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -27,16 +27,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: set up JDK 11
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'temurin'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: |
|
run: ./gradlew assembleDebug
|
||||||
chmod +x .github/scripts/gradlew_recursive.sh
|
|
||||||
.github/scripts/gradlew_recursive.sh assembleDebug
|
|
||||||
- name: Zip artifacts
|
- name: Zip artifacts
|
||||||
run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so'
|
run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so'
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
|
17
README.md
17
README.md
@ -33,22 +33,29 @@ _note: full featured example in the `sample` app_
|
|||||||
Create and add this to your Application.{kt,java}
|
Create and add this to your Application.{kt,java}
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
// create the Crash Handler
|
||||||
CrashHandler.Builder()
|
CrashHandler.Builder()
|
||||||
// need the application context to run
|
// need the application context to run
|
||||||
.withContext(this)
|
.withContext(this)
|
||||||
|
|
||||||
// every other items below are optionnal
|
// every other items below are optionnal
|
||||||
// define a custom activity to use
|
// define a custom activity to use
|
||||||
.withActivity(ErrorActivity::class.java)
|
.withActivity(ErrorActivity::class.java)
|
||||||
|
|
||||||
// define the preferenceManager to be able to handle crash in a custom Activity and to have the previous crash date in the logs
|
// define the preferenceManager to have the previous crash date in the logs
|
||||||
.withPrefs(prefs)
|
.withPrefs(prefs)
|
||||||
.withPrefsKey("com.dzeio.crashhandler.key")
|
.withPrefsKey("com.dzeio.crashhandler.key")
|
||||||
|
|
||||||
// a Prefix to add at the beggining the crash message
|
// a Prefix to add at the beginning the crash message
|
||||||
.withPrefix("Pouet :D")
|
.withPrefix("Prefix")
|
||||||
|
|
||||||
// a Suffic to add at the end of the crash message
|
// a Suffix to add at the end of the crash message
|
||||||
.withSuffix("WHYYYYY")
|
.withSuffix("Suffix")
|
||||||
|
|
||||||
|
// add a location where the crash logs are also exported (can be recovered as a zip ByteArray by calling {CrashHandler.getInstance().export()})
|
||||||
|
.withExportLocation(
|
||||||
|
File(this.getExternalFilesDir(null) ?: this.filesDir, "crash-logs")
|
||||||
|
)
|
||||||
|
|
||||||
// build & start the module
|
// build & start the module
|
||||||
.build().setup()
|
.build().setup()
|
||||||
|
@ -4,7 +4,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:7.4.2")
|
classpath("com.android.tools.build:gradle:8.1.1")
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
@ -12,8 +12,8 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "7.4.2" apply false
|
id("com.android.application") version "8.1.1" apply false
|
||||||
id("com.android.library") version "7.4.2" apply false
|
id("com.android.library") version "8.1.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.7.0" apply false
|
id("org.jetbrains.kotlin.android") version "1.7.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,17 +4,39 @@
|
|||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
#
|
#
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
#Tue Jul 19 18:42:00 CEST 2022
|
|
||||||
|
# enable non transitive R Classes
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
|
||||||
|
# use official kotlin style
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|
||||||
|
# use androidX
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
# Disable Jetifier
|
||||||
android.enableJetifier=false
|
android.enableJetifier=false
|
||||||
org.gradle.unsafe.configuration-cache=true
|
|
||||||
org.gradle.caching=true
|
# Disable configuration cache
|
||||||
|
org.gradle.unsafe.configuration-cache=false
|
||||||
|
|
||||||
|
# Enable Gradle Daemon
|
||||||
|
org.gradle.daemon=true
|
||||||
|
|
||||||
|
# Enable Configure on demand
|
||||||
org.gradle.configureondemand=true
|
org.gradle.configureondemand=true
|
||||||
|
|
||||||
|
# Enable gradle caching
|
||||||
|
org.gradle.caching=true
|
||||||
|
|
||||||
|
# Enabled Build config
|
||||||
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
|
|
||||||
|
# BREAK EVERYTHING
|
||||||
|
android.nonFinalResIds=false
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
#Sun Aug 07 22:38:24 CEST 2022
|
#Sun Aug 07 22:38:24 CEST 2022
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
0
gradlew.bat
vendored
Normal file → Executable file
0
gradlew.bat
vendored
Normal file → Executable file
@ -6,7 +6,7 @@ plugins {
|
|||||||
|
|
||||||
val artifact = "crashhandler"
|
val artifact = "crashhandler"
|
||||||
group = "com.dzeio"
|
group = "com.dzeio"
|
||||||
val projectVersion = project.findProperty("version") as String? ?: "1.0.0"
|
val projectVersion = project.findProperty("version") as String? ?: "1.1.0"
|
||||||
version = projectVersion
|
version = projectVersion
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
@ -37,6 +37,7 @@ android {
|
|||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
consumerProguardFiles("consumer-rules.pro")
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
buildConfigField("String", "VERSION", "\"$projectVersion\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
testFixtures {
|
testFixtures {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.dzeio.crashhandler
|
package com.dzeio.crashhandler
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@ -9,10 +10,16 @@ import android.os.Process
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.edit
|
||||||
import com.dzeio.crashhandler.CrashHandler.Builder
|
import com.dzeio.crashhandler.CrashHandler.Builder
|
||||||
import com.dzeio.crashhandler.ui.ErrorActivity
|
import com.dzeio.crashhandler.ui.ErrorActivity
|
||||||
|
import com.dzeio.crashhandler.utils.ZipFile
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import kotlin.system.exitProcess
|
import java.util.TimeZone
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the Crash Handler class, you can get an instance by using it's [Builder]
|
* the Crash Handler class, you can get an instance by using it's [Builder]
|
||||||
@ -25,11 +32,24 @@ class CrashHandler private constructor(
|
|||||||
@StringRes
|
@StringRes
|
||||||
private val errorReporterCrashKey: Int?,
|
private val errorReporterCrashKey: Int?,
|
||||||
private val prefix: String? = null,
|
private val prefix: String? = null,
|
||||||
private val suffix: String? = null
|
private val suffix: String? = null,
|
||||||
|
private val exportFolder: File? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private companion object {
|
companion object {
|
||||||
private const val TAG = "CrashHandler"
|
private const val TAG = "CrashHandler"
|
||||||
|
private var instance: CrashHandler? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the instance of the CrashHandler it will crash if it was not initialized previously
|
||||||
|
*/
|
||||||
|
fun getInstance(): CrashHandler {
|
||||||
|
if (this.instance == null) {
|
||||||
|
throw Exception("can't get CrashHandler instance as its not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance!!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,6 +63,7 @@ class CrashHandler private constructor(
|
|||||||
private var activity: Class<*>? = ErrorActivity::class.java
|
private var activity: Class<*>? = ErrorActivity::class.java
|
||||||
private var prefix: String? = null
|
private var prefix: String? = null
|
||||||
private var suffix: String? = null
|
private var suffix: String? = null
|
||||||
|
private var exportLocation: File? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the Crash activity to with your own
|
* Change the Crash activity to with your own
|
||||||
@ -124,6 +145,16 @@ class CrashHandler private constructor(
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a crash log export folder
|
||||||
|
*
|
||||||
|
* @param exportLocation the folder in which you want to export crash logs, it will be created if it does not exists
|
||||||
|
*/
|
||||||
|
fun withExportLocation(exportLocation: File): Builder {
|
||||||
|
this.exportLocation = exportLocation
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* build the Crash Handler
|
* build the Crash Handler
|
||||||
*/
|
*/
|
||||||
@ -135,11 +166,16 @@ class CrashHandler private constructor(
|
|||||||
prefsKey,
|
prefsKey,
|
||||||
errorReporterCrashKey,
|
errorReporterCrashKey,
|
||||||
prefix,
|
prefix,
|
||||||
suffix
|
suffix,
|
||||||
|
exportLocation
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
instance = this
|
||||||
|
}
|
||||||
|
|
||||||
private var oldHandler: Thread.UncaughtExceptionHandler? = null
|
private var oldHandler: Thread.UncaughtExceptionHandler? = null
|
||||||
|
|
||||||
fun setup() {
|
fun setup() {
|
||||||
@ -179,47 +215,15 @@ class CrashHandler private constructor(
|
|||||||
|
|
||||||
// get current time an date
|
// get current time an date
|
||||||
val now = Date().time
|
val now = Date().time
|
||||||
|
var previousCrash: Long? = null
|
||||||
// prepare to build debug string
|
|
||||||
var data = "${application.getString(R.string.crash_handler_crash_report)}\n\n"
|
|
||||||
|
|
||||||
data += prefix ?: ""
|
|
||||||
|
|
||||||
// add device informations
|
|
||||||
val deviceToReport =
|
|
||||||
if (Build.DEVICE.contains(Build.MANUFACTURER)) {
|
|
||||||
Build.DEVICE
|
|
||||||
} else {
|
|
||||||
"${Build.MANUFACTURER} ${Build.DEVICE}"
|
|
||||||
}
|
|
||||||
|
|
||||||
data += "\n\n${application.getString(
|
|
||||||
R.string.crash_handler_hard_soft_infos,
|
|
||||||
deviceToReport,
|
|
||||||
Build.MODEL,
|
|
||||||
Build.VERSION.RELEASE,
|
|
||||||
Build.VERSION.SDK_INT
|
|
||||||
)}"
|
|
||||||
|
|
||||||
// add the current time to it
|
|
||||||
data += "\n\n${application.getString(
|
|
||||||
R.string.crash_handler_crash_happened,
|
|
||||||
Date(now).toString()
|
|
||||||
)}"
|
|
||||||
|
|
||||||
// if lib as access to the preferences store
|
// if lib as access to the preferences store
|
||||||
if (prefs != null && prefsKey != null) {
|
if (prefs != null && prefsKey != null) {
|
||||||
// get the last Crash
|
// get the last Crash
|
||||||
val lastCrash = prefs.getLong(prefsKey, 0L)
|
previousCrash = prefs.getLong(prefsKey, 0L)
|
||||||
|
|
||||||
// then add it to the logs :D
|
|
||||||
data += "\n${application.getString(
|
|
||||||
R.string.crash_handler_previous_crash,
|
|
||||||
Date(lastCrash).toString()
|
|
||||||
)}"
|
|
||||||
|
|
||||||
// if a crash already happened just before it means the Error Activity crashed lul
|
// if a crash already happened just before it means the Error Activity crashed lul
|
||||||
if (lastCrash >= now - 1000) {
|
if (previousCrash >= now - 1000) {
|
||||||
// log it :D
|
// log it :D
|
||||||
Log.e(
|
Log.e(
|
||||||
TAG,
|
TAG,
|
||||||
@ -239,22 +243,23 @@ class CrashHandler private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update the store
|
// update the store
|
||||||
prefs.edit().putLong(prefsKey, now).commit()
|
prefs.edit(true) { putLong(prefsKey, now) }
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Collecting Error")
|
Log.i(TAG, "Collecting Error")
|
||||||
|
|
||||||
// get Thread name and ID
|
val data = this.buildData(
|
||||||
data += "\n\n${application.getString(
|
now,
|
||||||
R.string.crash_handler_thread_infos,
|
previousCrash,
|
||||||
paramThread.name,
|
paramThread,
|
||||||
paramThread.id
|
paramThrowable
|
||||||
)}"
|
)
|
||||||
|
|
||||||
// print exception backtrace
|
try {
|
||||||
data += "\n\n${application.getString(R.string.crash_handler_error)}\n${paramThrowable.stackTraceToString()}\n\n"
|
exportData(data, now)
|
||||||
|
} catch (e: IOException) {
|
||||||
data += suffix ?: ""
|
Log.e(TAG, "Could not export the data to file", e)
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Starting ${activity.name}")
|
Log.i(TAG, "Starting ${activity.name}")
|
||||||
|
|
||||||
@ -278,7 +283,127 @@ class CrashHandler private constructor(
|
|||||||
|
|
||||||
// Kill self
|
// Kill self
|
||||||
Process.killProcess(Process.myPid())
|
Process.killProcess(Process.myPid())
|
||||||
exitProcess(10)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun export(): ByteArray? {
|
||||||
|
if (exportFolder == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val output = ZipFile()
|
||||||
|
val files = exportFolder.listFiles()
|
||||||
|
for (file in files!!) {
|
||||||
|
output.addFile(file.name, file)
|
||||||
|
}
|
||||||
|
return output.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearExports() {
|
||||||
|
if (exportFolder == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val files = exportFolder.listFiles()
|
||||||
|
for (file in files!!) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exportData(data: String, now: Long) {
|
||||||
|
if (exportFolder == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@SuppressLint("SimpleDateFormat")
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss.SSS")
|
||||||
|
sdf.timeZone = TimeZone.getTimeZone("CET")
|
||||||
|
val filename = sdf.format(Date(now))
|
||||||
|
if (!exportFolder.exists()) {
|
||||||
|
exportFolder.mkdirs()
|
||||||
|
}
|
||||||
|
if (!exportFolder.isDirectory) {
|
||||||
|
Log.e(
|
||||||
|
"CrashHandler",
|
||||||
|
"Cannot export the crash logs to a file due to the folder not being a folder"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val out = File(exportFolder, "$filename.log")
|
||||||
|
out.writeText(data, Charsets.UTF_8)
|
||||||
|
Log.d("CrashHandler", "Saving file to ${out.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* build the data text
|
||||||
|
* @param now the date as of right now
|
||||||
|
* @param previousCrash the previous crash date
|
||||||
|
* @param thread the thread that crashed
|
||||||
|
* @param throwable the exception thrown
|
||||||
|
*
|
||||||
|
* @return the string that contains a nicely formatted list of informations about the device
|
||||||
|
*/
|
||||||
|
private fun buildData(
|
||||||
|
now: Long,
|
||||||
|
previousCrash: Long?,
|
||||||
|
thread: Thread,
|
||||||
|
throwable: Throwable
|
||||||
|
): String {
|
||||||
|
val app = application
|
||||||
|
|
||||||
|
if (app == null) {
|
||||||
|
return "Could not build data because the library is missing the context"
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare to build debug string
|
||||||
|
var data = "${app.getString(R.string.crash_handler_crash_report)}\n\n"
|
||||||
|
|
||||||
|
// add the user submitted prefix
|
||||||
|
data += prefix ?: ""
|
||||||
|
|
||||||
|
// add device informations
|
||||||
|
val deviceToReport =
|
||||||
|
if (Build.DEVICE.contains(Build.MANUFACTURER)) {
|
||||||
|
Build.DEVICE
|
||||||
|
} else {
|
||||||
|
"${Build.MANUFACTURER} ${Build.DEVICE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the device informations
|
||||||
|
data += "\n\n${app.getString(
|
||||||
|
R.string.crash_handler_hard_soft_infos,
|
||||||
|
deviceToReport,
|
||||||
|
Build.MODEL,
|
||||||
|
Build.VERSION.RELEASE,
|
||||||
|
Build.VERSION.SDK_INT
|
||||||
|
)}"
|
||||||
|
|
||||||
|
// add the current time to it
|
||||||
|
data += "\n\n${app.getString(
|
||||||
|
R.string.crash_handler_crash_happened,
|
||||||
|
Date(now).toString()
|
||||||
|
)}"
|
||||||
|
|
||||||
|
// add the previous crash date if available
|
||||||
|
if (previousCrash != null) {
|
||||||
|
data += "\n${app.getString(
|
||||||
|
R.string.crash_handler_previous_crash,
|
||||||
|
Date(previousCrash).toString()
|
||||||
|
)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// get Thread name and ID
|
||||||
|
data += "\n\n${app.getString(
|
||||||
|
R.string.crash_handler_thread_infos,
|
||||||
|
thread.name,
|
||||||
|
thread.id
|
||||||
|
)}"
|
||||||
|
|
||||||
|
// print exception backtrace
|
||||||
|
data += "\n\n${app.getString(R.string.crash_handler_error)}\n${throwable.stackTraceToString()}\n\n"
|
||||||
|
|
||||||
|
data += "Generated by Dzeio Crash Handler Version ${BuildConfig.VERSION}\n\n"
|
||||||
|
|
||||||
|
// add the user submitted suffix
|
||||||
|
data += suffix ?: ""
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
package com.dzeio.crashhandler.utils
|
||||||
|
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Wrapper around the Java zip implementation to make it easier to use
|
||||||
|
*/
|
||||||
|
class ZipFile {
|
||||||
|
|
||||||
|
private val stream = ByteArrayOutputStream()
|
||||||
|
private val output = ZipOutputStream(BufferedOutputStream(stream))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add a file to the zip with the [content] to the specified [path]
|
||||||
|
*
|
||||||
|
* @param path the path in the Zip
|
||||||
|
* @param content the content as a String
|
||||||
|
*/
|
||||||
|
fun addFile(path: String, content: String) = addFile(path, content.toByteArray())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add the [file] to the zip with at the specified [path]
|
||||||
|
*
|
||||||
|
* @param path the path in the Zip
|
||||||
|
* @param file the file to add to the zip
|
||||||
|
*/
|
||||||
|
fun addFile(path: String, file: File) {
|
||||||
|
// Read file
|
||||||
|
val data = file.inputStream()
|
||||||
|
val bytes = data.readBytes()
|
||||||
|
data.close()
|
||||||
|
|
||||||
|
return addFile(path, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add the [content] to the zip with at the specified [path]
|
||||||
|
*
|
||||||
|
* @param path the path in the Zip
|
||||||
|
* @param content the content of the file to add to the zip
|
||||||
|
*/
|
||||||
|
fun addFile(path: String, content: ByteArray) {
|
||||||
|
val entry = ZipEntry(path)
|
||||||
|
try {
|
||||||
|
output.putNextEntry(entry)
|
||||||
|
|
||||||
|
output.write(content)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
output.closeEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export the Zip file to a ByteArray
|
||||||
|
*
|
||||||
|
* **note: You can't write to the ZipFile after running this function**
|
||||||
|
*
|
||||||
|
* @return the Zip File as a [ByteArray]
|
||||||
|
*/
|
||||||
|
fun toByteArray(): ByteArray {
|
||||||
|
output.close()
|
||||||
|
return stream.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package com.dzeio.crashhandlertest
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.dzeio.crashhandler.CrashHandler
|
import com.dzeio.crashhandler.CrashHandler
|
||||||
import com.dzeio.crashhandlertest.ui.ErrorActivity
|
import com.dzeio.crashhandlertest.ui.ErrorActivity
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class Application : android.app.Application() {
|
class Application : android.app.Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@ -15,17 +16,20 @@ class Application : android.app.Application() {
|
|||||||
CrashHandler.Builder()
|
CrashHandler.Builder()
|
||||||
// need the application context to run
|
// need the application context to run
|
||||||
.withContext(this)
|
.withContext(this)
|
||||||
|
// every other items below are optionnal
|
||||||
// define a custom activity to use
|
// define a custom activity to use
|
||||||
.withActivity(ErrorActivity::class.java)
|
.withActivity(ErrorActivity::class.java)
|
||||||
// define the preferenceManager to be able to handle crash in a custom Activity and to have the previous crash date in the logs
|
// define the preferenceManager to have the previous crash date in the logs
|
||||||
.withPrefs(prefs)
|
.withPrefs(prefs)
|
||||||
.withPrefsKey("com.dzeio.crashhandler.key")
|
.withPrefsKey("com.dzeio.crashhandler.key")
|
||||||
// a Prefix to add at the beginning the crash message
|
// a Prefix to add at the beginning the crash message
|
||||||
.withPrefix(
|
.withPrefix("Prefix")
|
||||||
"Pokémon"
|
|
||||||
)
|
|
||||||
// a Suffix to add at the end of the crash message
|
// a Suffix to add at the end of the crash message
|
||||||
.withSuffix("Suffix")
|
.withSuffix("Suffix")
|
||||||
|
// add a location where the crash logs are also exported (can be recovered as a zip ByteArray by calling {CrashHandler.getInstance().export()})
|
||||||
|
.withExportLocation(
|
||||||
|
File(this.getExternalFilesDir(null) ?: this.filesDir, "crash-logs")
|
||||||
|
)
|
||||||
// build & start the module
|
// build & start the module
|
||||||
.build().setup()
|
.build().setup()
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,41 @@
|
|||||||
package com.dzeio.crashhandlertest.ui
|
package com.dzeio.crashhandlertest.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import com.dzeio.crashhandler.CrashHandler
|
||||||
import com.dzeio.crashhandlertest.R
|
import com.dzeio.crashhandlertest.R
|
||||||
import com.dzeio.crashhandlertest.databinding.ActivityMainBinding
|
import com.dzeio.crashhandlertest.databinding.ActivityMainBinding
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response to the export button event
|
||||||
|
*/
|
||||||
|
private val writeResult = registerForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("application/zip")
|
||||||
|
) {
|
||||||
|
|
||||||
|
val zipFile = CrashHandler.getInstance().export()
|
||||||
|
if (zipFile == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// write file to location
|
||||||
|
this.contentResolver.openOutputStream(it!!)?.apply {
|
||||||
|
write(zipFile)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
R.string.export_complete,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@ -24,5 +49,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// DIE
|
// DIE
|
||||||
throw Exception(getString(R.string.error_message))
|
throw Exception(getString(R.string.error_message))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.buttonExport.setOnClickListener {
|
||||||
|
// launch the popin to select where to save the file
|
||||||
|
writeResult.launch("output.zip")
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonClearExports.setOnClickListener {
|
||||||
|
// clear the handler exports
|
||||||
|
CrashHandler.getInstance().clearExports()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,35 @@
|
|||||||
tools:context=".ui.MainActivity"
|
tools:context=".ui.MainActivity"
|
||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/button_first"
|
android:id="@+id/button_first"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/crash_app"
|
android:text="@string/crash_app"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:layout_marginBottom="8dp" />
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
<Button
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
android:id="@+id/button_export"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/export_logs" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_clear_exports"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/clear_export_logs" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -9,4 +9,7 @@
|
|||||||
<string name="no_email_client_found">Aucun client E-Mail trouvé!</string>
|
<string name="no_email_client_found">Aucun client E-Mail trouvé!</string>
|
||||||
<string name="send_email_report">Envoyer le rapport de crash par E-Mail</string>
|
<string name="send_email_report">Envoyer le rapport de crash par E-Mail</string>
|
||||||
<string name="error_report_application_crash">Rapport d\'erreur pour le crash d\'application</string>
|
<string name="error_report_application_crash">Rapport d\'erreur pour le crash d\'application</string>
|
||||||
|
<string name="export_logs">Exporter les logs de crash sauvegardé</string>
|
||||||
|
<string name="export_complete">Logs de crash exporté!</string>
|
||||||
|
<string name="clear_export_logs">Nettoyer les crashs sauvegardé</string>
|
||||||
</resources>
|
</resources>
|
@ -10,4 +10,7 @@
|
|||||||
<string name="no_email_client_found">No E-Mail client found!</string>
|
<string name="no_email_client_found">No E-Mail client found!</string>
|
||||||
<string name="send_email_report">Send a Report E-Mail</string>
|
<string name="send_email_report">Send a Report E-Mail</string>
|
||||||
<string name="error_report_application_crash">Error report for the application crash</string>
|
<string name="error_report_application_crash">Error report for the application crash</string>
|
||||||
|
<string name="export_logs">Export saved crash logs</string>
|
||||||
|
<string name="export_complete">Exported crash logs!</string>
|
||||||
|
<string name="clear_export_logs">Cleanup saved crash logs</string>
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user