feat: Add crash saving (#12)

This commit is contained in:
Florian Bouillon 2023-08-24 15:54:55 +02:00 committed by GitHub
parent 1703479865
commit acbb524a7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 383 additions and 130 deletions

View File

@ -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

View File

@ -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

View File

@ -24,7 +24,7 @@ Lightweight & customizable crash android crash handler library
Add to you dependencies (check the latest release for the version): Add to you dependencies (check the latest release for the version):
- (Gradle Kotlin DSL) Add `implementation("com.dzeio:crashhandler:1.0.2")` - (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 ## Usage
@ -33,25 +33,32 @@ _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
// define a custom activity to use // every other items below are optionnal
.withActivity(ErrorActivity::class.java) // 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) .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()
``` ```
## Build ## Build

View File

@ -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
} }

View File

@ -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
# 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.caching=true
org.gradle.configureondemand=true
# Enabled Build config
android.defaults.buildfeatures.buildconfig=true
# BREAK EVERYTHING
android.nonFinalResIds=false

View File

@ -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 vendored Normal file → Executable file
View File

0
gradlew.bat vendored Normal file → Executable file
View File

View 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 {

View File

@ -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
}
} }

View File

@ -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()
}
}

View File

@ -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()
} }

View File

@ -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()
}
} }
} }

View File

@ -8,14 +8,35 @@
tools:context=".ui.MainActivity" tools:context=".ui.MainActivity"
android:padding="16dp"> android:padding="16dp">
<Button <LinearLayout
android:id="@+id/button_first" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/crash_app" android:orientation="vertical"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/button_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/crash_app"
android:layout_marginBottom="8dp" />
<Button
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>

View File

@ -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>

View File

@ -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>