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>

Here is the activity to handle sending the log file. A text String is shown to ask the user to send email to us for reporting the problem. It provides a “cancel” and “compose email” button.
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();
            }
        }
    };

}

The layout of the new Activity.
<?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>

Retrieve the log file from app-specific storage

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

留言

此網誌的熱門文章

Use okhttp to download file and show progress bar

Download File into app specific storage with Retrofit

Unzipp file with Zip4j library