diff --git a/.github/scripts/gradlew_recursive.sh b/.github/scripts/gradlew_recursive.sh deleted file mode 100644 index d575c49..0000000 --- a/.github/scripts/gradlew_recursive.sh +++ /dev/null @@ -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 - diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef1c8c4..068c5a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,16 +27,14 @@ jobs: steps: - uses: actions/checkout@v3 - - name: set up JDK 11 + - name: set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '11' - distribution: 'adopt' + java-version: '17' + distribution: 'temurin' cache: gradle - name: Build project - run: | - chmod +x .github/scripts/gradlew_recursive.sh - .github/scripts/gradlew_recursive.sh assembleDebug + run: ./gradlew assembleDebug - name: Zip artifacts run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - name: Upload artifacts diff --git a/README.md b/README.md index 5f95cdb..42370c2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Lightweight & customizable crash android crash handler library Add to you dependencies (check the latest release for the version): - (Gradle Kotlin DSL) Add `implementation("com.dzeio:crashhandler:1.0.2")` -- (Gradle Groovy DSL) Add `implementation "com.dzeio:crashhandler:1.0.2" ` +- (Gradle Groovy DSL) Add `implementation "com.dzeio:crashhandler:1.0.2"` ## Usage @@ -33,25 +33,32 @@ _note: full featured example in the `sample` app_ Create and add this to your Application.{kt,java} ```kotlin +// create the Crash Handler CrashHandler.Builder() - // need the application context to run - .withContext(this) - // every other items below are optionnal - // define a custom activity to use - .withActivity(ErrorActivity::class.java) + // need the application context to run + .withContext(this) + + // every other items below are optionnal + // define a custom activity to use + .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 - .withPrefs(prefs) - .withPrefsKey("com.dzeio.crashhandler.key") + // define the preferenceManager to have the previous crash date in the logs + .withPrefs(prefs) + .withPrefsKey("com.dzeio.crashhandler.key") - // a Prefix to add at the beggining the crash message - .withPrefix("Pouet :D") + // a Prefix to add at the beginning the crash message + .withPrefix("Prefix") - // a Suffic to add at the end of the crash message - .withSuffix("WHYYYYY") + // a Suffix to add at the end of the crash message + .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().setup() + // build & start the module + .build().setup() ``` ## Build diff --git a/build.gradle.kts b/build.gradle.kts index 4394ff2..1a28748 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ buildscript { mavenCentral() } 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 // in the individual module build.gradle files @@ -12,8 +12,8 @@ buildscript { } plugins { - id("com.android.application") version "7.4.2" apply false - id("com.android.library") version "7.4.2" apply false + id("com.android.application") version "8.1.1" apply false + id("com.android.library") version "8.1.1" apply false id("org.jetbrains.kotlin.android") version "1.7.0" apply false } diff --git a/gradle.properties b/gradle.properties index b81a080..d4699b7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,17 +4,39 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # 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. # 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 org.gradle.parallel=true -#Tue Jul 19 18:42:00 CEST 2022 + +# enable non transitive R Classes android.nonTransitiveRClass=true + +# use official kotlin style kotlin.code.style=official + +# use androidX android.useAndroidX=true + +# Disable Jetifier android.enableJetifier=false -org.gradle.unsafe.configuration-cache=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 + +# Enable gradle caching org.gradle.caching=true -org.gradle.configureondemand=true \ No newline at end of file + +# Enabled Build config +android.defaults.buildfeatures.buildconfig=true + +# BREAK EVERYTHING +android.nonFinalResIds=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4cbbcaa..fa344aa 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Aug 07 22:38:24 CEST 2022 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 zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 8ece8ad..71a4c7d 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -6,7 +6,7 @@ plugins { val artifact = "crashhandler" 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 publishing { @@ -37,6 +37,7 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") + buildConfigField("String", "VERSION", "\"$projectVersion\"") } testFixtures { diff --git a/library/src/main/java/com/dzeio/crashhandler/CrashHandler.kt b/library/src/main/java/com/dzeio/crashhandler/CrashHandler.kt index ab45f96..6a3c2e5 100644 --- a/library/src/main/java/com/dzeio/crashhandler/CrashHandler.kt +++ b/library/src/main/java/com/dzeio/crashhandler/CrashHandler.kt @@ -1,5 +1,6 @@ package com.dzeio.crashhandler +import android.annotation.SuppressLint import android.app.Application import android.content.Context import android.content.Intent @@ -9,10 +10,16 @@ import android.os.Process import android.util.Log import android.widget.Toast import androidx.annotation.StringRes +import androidx.core.content.edit import com.dzeio.crashhandler.CrashHandler.Builder 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 kotlin.system.exitProcess +import java.util.TimeZone /** * the Crash Handler class, you can get an instance by using it's [Builder] @@ -25,11 +32,24 @@ class CrashHandler private constructor( @StringRes private val errorReporterCrashKey: Int?, 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 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 prefix: String? = null private var suffix: String? = null + private var exportLocation: File? = null /** * Change the Crash activity to with your own @@ -124,6 +145,16 @@ class CrashHandler private constructor( 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 */ @@ -135,11 +166,16 @@ class CrashHandler private constructor( prefsKey, errorReporterCrashKey, prefix, - suffix + suffix, + exportLocation ) } } + init { + instance = this + } + private var oldHandler: Thread.UncaughtExceptionHandler? = null fun setup() { @@ -179,47 +215,15 @@ class CrashHandler private constructor( // get current time an date val now = Date().time - - // 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() - )}" + var previousCrash: Long? = null // if lib as access to the preferences store if (prefs != null && prefsKey != null) { // get the last Crash - val lastCrash = prefs.getLong(prefsKey, 0L) - - // then add it to the logs :D - data += "\n${application.getString( - R.string.crash_handler_previous_crash, - Date(lastCrash).toString() - )}" + previousCrash = prefs.getLong(prefsKey, 0L) // 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.e( TAG, @@ -239,22 +243,23 @@ class CrashHandler private constructor( } // update the store - prefs.edit().putLong(prefsKey, now).commit() + prefs.edit(true) { putLong(prefsKey, now) } } Log.i(TAG, "Collecting Error") - // get Thread name and ID - data += "\n\n${application.getString( - R.string.crash_handler_thread_infos, - paramThread.name, - paramThread.id - )}" + val data = this.buildData( + now, + previousCrash, + paramThread, + paramThrowable + ) - // print exception backtrace - data += "\n\n${application.getString(R.string.crash_handler_error)}\n${paramThrowable.stackTraceToString()}\n\n" - - data += suffix ?: "" + try { + exportData(data, now) + } catch (e: IOException) { + Log.e(TAG, "Could not export the data to file", e) + } Log.i(TAG, "Starting ${activity.name}") @@ -278,7 +283,127 @@ class CrashHandler private constructor( // Kill self 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 + } } diff --git a/library/src/main/java/com/dzeio/crashhandler/utils/ZipFile.kt b/library/src/main/java/com/dzeio/crashhandler/utils/ZipFile.kt new file mode 100644 index 0000000..2c94fef --- /dev/null +++ b/library/src/main/java/com/dzeio/crashhandler/utils/ZipFile.kt @@ -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() + } +} diff --git a/sample/src/main/java/com/dzeio/crashhandlertest/Application.kt b/sample/src/main/java/com/dzeio/crashhandlertest/Application.kt index b6f9c45..182baca 100644 --- a/sample/src/main/java/com/dzeio/crashhandlertest/Application.kt +++ b/sample/src/main/java/com/dzeio/crashhandlertest/Application.kt @@ -3,6 +3,7 @@ package com.dzeio.crashhandlertest import androidx.preference.PreferenceManager import com.dzeio.crashhandler.CrashHandler import com.dzeio.crashhandlertest.ui.ErrorActivity +import java.io.File class Application : android.app.Application() { override fun onCreate() { @@ -15,17 +16,20 @@ class Application : android.app.Application() { CrashHandler.Builder() // need the application context to run .withContext(this) + // every other items below are optionnal // define a custom activity to use .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) .withPrefsKey("com.dzeio.crashhandler.key") // a Prefix to add at the beginning the crash message - .withPrefix( - "Pokémon" - ) + .withPrefix("Prefix") // a Suffix to add at the end of the crash message .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().setup() } diff --git a/sample/src/main/java/com/dzeio/crashhandlertest/ui/MainActivity.kt b/sample/src/main/java/com/dzeio/crashhandlertest/ui/MainActivity.kt index 3a30160..065cf92 100644 --- a/sample/src/main/java/com/dzeio/crashhandlertest/ui/MainActivity.kt +++ b/sample/src/main/java/com/dzeio/crashhandlertest/ui/MainActivity.kt @@ -1,16 +1,41 @@ package com.dzeio.crashhandlertest.ui import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat +import com.dzeio.crashhandler.CrashHandler import com.dzeio.crashhandlertest.R import com.dzeio.crashhandlertest.databinding.ActivityMainBinding -/** - * - */ 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 override fun onCreate(savedInstanceState: Bundle?) { @@ -24,5 +49,15 @@ class MainActivity : AppCompatActivity() { // DIE 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() + } } } diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index c4d5449..8df1670 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -8,14 +8,35 @@ tools:context=".ui.MainActivity" android:padding="16dp"> -