Send Log file when app crashed by runtime exception
Although we have tested your app before releasing to others or the public, there still are hidden bugs that cause running exceptions we did not catch. Eventually, the app crashed. We want to know what has happened and try to fix it. Therefore, we need the app to send the log file from the installed device to us. Below is an example to send the log file stored in app-specific storage to a dedicated email address when the app crashed by the runtime exception.
For the method of saving log messages into files, please check the previous post Logging in Android.
Create a subclass of Application class to catch the unknown runtime exception
Application class is the first class to be invoked before any other class when the app starts because it is the base class of maintaining global application state. Our purpose is to add a concrete class of Thread.UncaughtExceptionHandler when the app starts.
The interface Thread.UncaughtExceptionHandler will be invoked when the app is terminated by uncaught exception. We implement the interface and override the method uncaughtException to add the custom action. Then we can add our code in the method uncaughtException.
To avoid affecting the default system handling, we called the original UncaughtExceptionHandler at the end of method uncaughtException.
public class MyApplication extends Application { @Override public void onCreate(){ super.onCreate(); Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(new LogUncaughtExceptionHandler(handler)); } public class LogUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{ private Thread.UncaughtExceptionHandler originalHandler; private Logger logger; public LogUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler){ originalHandler = handler; logger = LoggerFactory.getLogger(MainActivity.class); } @Override public void uncaughtException(@NonNull Thread t, @NonNull Throwable ex) { logger.error("Uncaught exception in thread: " + t.getName(), ex); // implement code here to send log file if(originalHandler != null) originalHandler.uncaughtException(t, ex); } } }
Then we specified the name of our subclass in the AndroidManifest.xml to let the system instantiate our subclass instead of the default Application class.
<application android:name=".MyApplication" ……> …… </application>
Create a new Activity class to inform the user to send log
In this example, I will start a new activity to send the log file before invoking the original UncaughtExceptionHandler.
@Override public void uncaughtException(@NonNull Thread t, @NonNull Throwable ex) { logger.error("Uncaught exception in thread: " + t.getName(), ex); //TODO send email with log file attached Intent intent = new Intent(); intent.setAction("com.example.test.SEND_LOG_AFTER_CRASH"); intent.setFlags((Intent.FLAG_ACTIVITY_NEW_TASK)); startActivity(intent); originalHandler.uncaughtException(t, ex); }
We add the activity in AndroidManifest.xml and define our customer action in intent-filter. Android can invoke our activity by this Action.
<activity android:name=".logger.SendLogActivity" android:theme="@style/Theme.AppCompat.Dialog" android:exported="false" android:windowSoftInputMode="stateHidden"> <intent-filter> <action android:name="com.example.test.SEND_LOG_AFTER_CRASH" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
public class SendLogActivity extends AppCompatActivity { Button composeMailButton, cancelButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_send_log); composeMailButton = (Button) findViewById(R.id.sendMailButton); cancelButton = (Button) findViewById(R.id.cancelButton); composeMailButton.setOnClickListener(buttonListener); cancelButton.setOnClickListener(buttonListener); currentTime = Calendar.getInstance(); } View.OnClickListener buttonListener = new View.OnClickListener() { @Override public void onClick(View v) { if(v.getId() == composeMailButton.getId()){ Intent emailIntent = new Intent(Intent.ACTION_SEND); emailIntent.setType("message/rfc822"); //prompts email client and social app only String[] recipients_email_address = new String[]{getString(R.string.recipients_email_address)}; emailIntent.putExtra(Intent.EXTRA_EMAIL, recipients_email_address); emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.mail_subject)); Uri attachment = getLogFile(); if(attachment != null) emailIntent.putExtra(Intent.EXTRA_STREAM, attachment); if(emailIntent.resolveActivity(getPackageManager()) != null){ startActivity(emailIntent); } finish(); }else if(v.getId() == cancelButton.getId()){ finish(); } } }; }
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".logger.SendLogActivity"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@id/cancelButton" android:layout_marginStart="@dimen/view_margin_small" android:layout_marginEnd="@dimen/view_margin_small" android:text="@string/ask_to_report_bug_message" /> <Button android:id="@+id/cancelButton" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/textView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/sendMailButton" android:text="@string/cancel" /> <Button android:id="@+id/sendMailButton" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/textView" app:layout_constraintStart_toEndOf="@id/cancelButton" app:layout_constraintEnd_toEndOf="parent" android:text="@string/compose_email" /> </androidx.constraintlayout.widget.ConstraintLayout>
The log file is stored in the app-specific storage space. We have to use FileProvider to retrieve the Uri of the log file so that other apps(email client) can access the file with this Uri. In this case, the log files are stored in the {app-specific folder}/log folder. The format of the filename is “logFile.yyyyMMdd.log”. For the details of saving the log file, please refer to the post Logging in the Android.
private static final String LOG_DIRECTORY_NAME = "log"; private static final String LOG_FILE_NAME_PREFIX = "logFile."; private static final String LOG_FILE_NAME_SUFFIX = ".log"; public Uri getLogFile(){ Uri logFileUri = null; Calendar currentTime = Calendar.getInstance(); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); String filename = LOG_FILE_NAME_PREFIX + format.format(currentTime.getTime()) + LOG_FILE_NAME_SUFFIX; File logFileDirectory = new File(getFilesDir(), LOG_DIRECTORY_NAME); if (logFileDirectory.exists()) { File logFile = new File(logFileDirectory, filename); if (logFile.exists()) { String authority = getPackageName() + ".fileprovider"; logFileUri = FileProvider.getUriForFile(getApplicationContext(), authority, logFile); } } return logFileUri; }
Then we add FileProvider element in the AndroidManifest.xml. To set granUriPermissions true gives the temporary right to access the file. FileProvider only can generate Uri for the files we specified in the “res/xml/filepaths.xml”.
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.example.test.fileprovider" android:grantUriPermissions="true" android:exported="false"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" /> </provider>
We create a xml file with name “filepaths.xml” in the directory “res/xml”. It is the relative path from the internal app-specific storage. Only the files in this directory can be shared.
<?xml version="1.0" encoding="utf-8"?> <paths> <files-path name="app name" path="log/" /> </paths>
Reference
handle uncaught exception and send log file
https://stackoverflow.com/a/19968400
Android Developer app-specific storage
https://developer.android.com/training/data-storage/app-specific
Android Intent for share file or sending email
https://developer.android.com/guide/components/intents-filters
留言
發佈留言