Android error log capture

Posted by duk on Tue, 01 Feb 2022 10:06:38 +0100

Recently, I sorted out the code of error log capture. Although there are many online, I added some ideas! The following code:

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;

public class CrashHandler implements UncaughtExceptionHandler {

    //Single case
    private volatile static CrashHandler instance;

    //Exception log path
    private File mLogPath;

    //Equipment information
    private String mDeviceInfo;

    //Default main thread exception handler
    private UncaughtExceptionHandler mDefaultHandler;

    //Initialize in application
    public static void init(Context context) {
        CrashHandler crashHandler = getInstance();
        crashHandler.mLogPath = getCrashCacheDir(context);
        crashHandler.mDeviceInfo = CrashHandler.getDeviceInfo(context);
        crashHandler.mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        //Set the main thread processor as a custom processor
        Thread.setDefaultUncaughtExceptionHandler(crashHandler);
    }

    //Get some device information
    private static String getDeviceInfo(Context context) {
        StringBuilder sb = new StringBuilder();
        try {
            PackageManager pkgMgr = context.getPackageManager();
            PackageInfo pkgInfo = pkgMgr.getPackageInfo(context.getPackageName(), 0);
            sb.append("packageName: ").append(pkgInfo.packageName).append("\n");
            sb.append("versionCode: ").append(pkgInfo.versionCode).append("\n");
            sb.append("versionName: ").append(pkgInfo.versionName).append("\n");
        } catch (Exception e) {
            e.printStackTrace();
        }

        //Manufacturers, such as Xiaomi, Meizu, Huawei, etc.
        sb.append("brand: ").append(Build.BRAND).append("\n");
        //Full name of product model, e.g. meizu_mx3
        sb.append("product: ").append(Build.PRODUCT).append("\n");
        //Product model name, e.g. mx3
        sb.append("device: ").append(Build.DEVICE).append("\n");
        //Android version name, e.g. 4.4.4
        sb.append("androidVersionName: ").append(Build.VERSION.RELEASE).append("\n");
        //Android API version, e.g. 19
        sb.append("androidApiVersion: ").append(Build.VERSION.SDK_INT).append("\n");

        //Record all data of Build through reflection
//        Field[] fields = Build.class.getDeclaredFields();
//        for (Field field : fields) {
//            try {
//                field.setAccessible(true);
//                infos.put(field.getName(), field.get(null).toString());
//                Log.d(TAG, field.getName() + " : " + field.get(null));
//            } catch (Exception e) {
//                Log.e(TAG, "an error occured when collect crash info", e);
//            }
//        }
        return sb.toString();
    }

    //Get the log cache directory without permission
    private static File getCrashCacheDir(Context context) {
        String cachePath;
        if (context.getExternalCacheDir() != null) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getFilesDir().getPath();
        }

        File dir = new File(cachePath + File.separator + "log");
        if (!dir.exists()) {
            //noinspection ResultOfMethodCallIgnored
            dir.mkdirs();
        }
        return dir;
    }

    //DCL double check
    public static CrashHandler getInstance() {
        if (instance == null) {
            synchronized (CrashHandler.class) {
                if (instance == null) {
                    instance = new CrashHandler();
                }
            }
        }
        return instance;
    }


    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        //Self handling exception
        handleException(ex);
        //Handle it by yourself and then hand it over to the default processor
        if (null != mDefaultHandler) {
            mDefaultHandler.uncaughtException(thread, ex);
        }
    }

    //Handling exceptions
    private void handleException(final Throwable throwable) {
        StringBuilder sb = new StringBuilder();
        //Get exception information
        getExceptionInfo(sb, throwable);
        //Additional device information
        sb.append(mDeviceInfo);
        //Save to file
        new Thread(()->saveCrash2File(sb.toString())).start();
    }

    //Get exception information
    private void getExceptionInfo(StringBuilder sb, Throwable throwable) {
        Writer writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        throwable.printStackTrace(printWriter);
        //Get the exception reason of the package exception in the loop
        Throwable cause = throwable.getCause();
        while (cause != null) {
            cause.printStackTrace(printWriter);
            cause = cause.getCause();
        }
        printWriter.close();
        String exStr = writer.toString();
        sb.append(exStr).append("\n");
    }


    //Save to file
    private void saveCrash2File(String crashInfo) {
        try {
            // Used to format the date as part of the log file name
            DateFormat formatter = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA);
            String time = formatter.format(new Date());
            String fileName = "crash_" + time + ".log";
            String filePath = mLogPath.getAbsolutePath() + File.separator + fileName;
            FileOutputStream fos = new FileOutputStream(filePath);
            fos.write(crashInfo.getBytes());
            fos.flush();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    //Get all log files
    public static List<File> getCrashLogs() {
        CrashHandler crashHandler = getInstance();
        File crashDir = crashHandler.mLogPath;
        //Avoid using the asList method directly, and the generated view cannot be modified
        return new ArrayList<>(Arrays.asList(crashDir.listFiles()));
    }

    //Read the contents of the crash log file and return
    public static List<String> getCrashLogsStrings() {
        List<String> logs = new ArrayList<>();
        List<File> files = getCrashLogs();

        FileInputStream inputStream;
        for (File file : files) {
            try {
                inputStream = new FileInputStream(file);
                int len = 0;
                byte[] temp = new byte[1024];
                StringBuilder sb = new StringBuilder("");
                while ((len = inputStream.read(temp)) > 0){
                    sb.append(new String(temp, 0, len));
                }
                inputStream.close();
                logs.add(sb.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return logs;
    }

    //Delete log file
    public static void removeCrashLogs(List<File> files) {
        for (File file : files) {
            //noinspection ResultOfMethodCallIgnored
            file.delete();
        }
    }
}

Let's briefly talk about some things in it.

Error log capture

In fact, the program crash is the crash of the main Thread. The program exit is actually that we did not catch the exception from the main Thread, and the Java Thread class contains a defaultUncaughtExceptionHandler variable, which will handle the exception of the Thread and print it to the default output, That is, when we debug, there is a problem. We can find the reason of the error log in the log.

// null unless explicitly set
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

...
    
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
    defaultUncaughtExceptionHandler = eh;
}

Therefore, to capture the error log, we only need to implement the UncaughtExceptionHandler interface and set the processor of the main Thread to be customized. UncaughtExceptionHandler is also defined in the Thread class, as follows:

    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

Get device information

Most of the time, the programs we have debugged run without problems, but once we arrive at the customer, it goes from one thing to another. This involves some equipment data, and our test is unlikely to cover all models. When there is a problem, we hope to submit the log and equipment information together.

Below are some device information. PackageManager can get the software version number and version name. Of course, filling in other flag s can get more other information. We won't elaborate here. The Build class contains some equipment information. I've selected some of them for use. You can also get all the data through reflection as in the comments.

    //Get some device information
    private static String getDeviceInfo(Context context) {
        StringBuilder sb = new StringBuilder();
        try {
            PackageManager pkgMgr = context.getPackageManager();
            PackageInfo pkgInfo = pkgMgr.getPackageInfo(context.getPackageName(), 0);
            sb.append("packageName: ").append(pkgInfo.packageName).append("\n");
            sb.append("versionCode: ").append(pkgInfo.versionCode).append("\n");
            sb.append("versionName: ").append(pkgInfo.versionName).append("\n");
        } catch (Exception e) {
            e.printStackTrace();
        }

        //Manufacturers, such as Xiaomi, Meizu, Huawei, etc.
        sb.append("brand: ").append(Build.BRAND).append("\n");
        //Full name of product model, e.g. meizu_mx3
        sb.append("product: ").append(Build.PRODUCT).append("\n");
        //Product model name, e.g. mx3
        sb.append("device: ").append(Build.DEVICE).append("\n");
        //Android version name, e.g. 4.4.4
        sb.append("androidVersionName: ").append(Build.VERSION.RELEASE).append("\n");
        //Android API version, e.g. 19
        sb.append("androidApiVersion: ").append(Build.VERSION.SDK_INT).append("\n");

        //Record all data of Build through reflection
//        Field[] fields = Build.class.getDeclaredFields();
//        for (Field field : fields) {
//            try {
//                field.setAccessible(true);
//                infos.put(field.getName(), field.get(null).toString());
//                Log.d(TAG, field.getName() + " : " + field.get(null));
//            } catch (Exception e) {
//                Log.e(TAG, "an error occured when collect crash info", e);
//            }
//        }
        return sb.toString();
    }

Get exception information

    //Get exception information
    private void getExceptionInfo(StringBuilder sb, Throwable throwable) {
        Writer writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        throwable.printStackTrace(printWriter);
        //Get the exception reason of the package exception in the loop
        Throwable cause = throwable.getCause();
        while (cause != null) {
            cause.printStackTrace(printWriter);
            cause = cause.getCause();
        }
        printWriter.close();
        String exStr = writer.toString();
        sb.append(exStr).append("\n");
    }

We need to use the Throwable class to get exceptions here, because the exceptions we mentioned may include Error and Exception, which need to be caught.

Since the exception may contain packaged case s, we need to cycle once to get the most original exception information.

For more information about exceptions, see my other blog:

https://blog.csdn.net/lfq88/article/details/107173268

Save log

    //Get the log cache directory without permission
    private static File getCrashCacheDir(Context context) {
        String cachePath;
        if (context.getExternalCacheDir() != null) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getFilesDir().getPath();
        }

        File dir = new File(cachePath + File.separator + "log");
        if (!dir.exists()) {
            //noinspection ResultOfMethodCallIgnored
            dir.mkdirs();
        }
        return dir;
    }

Log upload

I don't want to write about the log upload here, but I prefer to process it after getting the log data externally. The following provides a method to obtain abnormal data and a deletion method, which is used to clear the saved drop-down files after the log is uploaded successfully.

    //Get all log files
    public static List<File> getCrashLogs() {
        CrashHandler crashHandler = getInstance();
        File crashDir = crashHandler.mLogPath;
        //Avoid using the asList method directly, and the generated view cannot be modified
        return new ArrayList<>(Arrays.asList(crashDir.listFiles()));
    }

    //Read the contents of the crash log file and return
    public static List<String> getCrashLogsStrings() {
        List<String> logs = new ArrayList<>();
        List<File> files = getCrashLogs();

        FileInputStream inputStream;
        for (File file : files) {
            try {
                inputStream = new FileInputStream(file);
                int len = 0;
                byte[] temp = new byte[1024];
                StringBuilder sb = new StringBuilder("");
                while ((len = inputStream.read(temp)) > 0){
                    sb.append(new String(temp, 0, len));
                }
                inputStream.close();
                logs.add(sb.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return logs;
    }

    //Delete log file
    public static void removeCrashLogs(List<File> files) {
        for (File file : files) {
            //noinspection ResultOfMethodCallIgnored
            file.delete();
        }
    }

epilogue

I don't write much. I hope it will be helpful to readers. I also record it myself.

end

Topics: Android