今天的這個功能是一個我在工作上遇到的一個大坑的解決方案

然後也不知道為什麼這個功能居然在我沒寫Android之後的快一年後才想到要發ORZ

而且在下文章標題的時候還一時不知道要怎麼下,真是崩潰(・-・。)

好啦,姑且就如同標題所述,本文就是在寫「如何在程式閃退之後自動輸出錯誤報告」的方法

 

可能很多有實際上工過的工程師朋友都知道,因為Android手機廠牌不同、版本不同等等一堆亂七八糟的原因,因此可能在A機能跑的功能B機可能會閃退

或者在做開發與測試環節都沒問題,在客人那裡卻總是問題百出

如果是測試單位的手機還好,還可以請他們來接一下USB看錯誤報告

或者還可以像這篇文章介紹的方法,去下載人家手機的錯誤報告來看

-> 今天,我教会了测试查崩溃日志

 

不過客人的手機,我們總不能去叫人家來一趟接電腦吧?

於是總是要想個方法來取得他手機的錯誤報告,那麼該怎麼做呢?

於是,我要提供的就是完成這個步驟的方法

那麼,總之看個功能吧

 

還有Github

OK,開始吧!

 


 

1. 專案結構

首先來聊一下這次的檔案設置,如下

截圖 2023-01-01 下午2.45.03

這次的專案中,我多設置了一個Java Class以便之後我自己複製使用

如果各位有想要直接拿去使用的話,也可以直接將CrashHandlr.java複製過去使用即可

不過既然是文章,那麼還是會盡義務地講解講解一下內容(笑)

那麼讓我們繼續吧!

 


 

2. 加入權限與權限設置

首先,由於我寫的程式是要在程式爆掉(閃退)後,自動匯出錯誤報告到手機儲存裝置(SD卡)的系統

因此在這邊我們需要設置一些關於寫資料到儲存裝置的設置

特別注意的是,繼這篇文章

->碼農日常-『Android studio』檔案輸出至手機資料夾(File export)

之後,Google 在Android 11之後又加強了針對權限的要求(⇀‸↼‶)

OS: 靠有夠煩..........

OS: 需要加強的是資安觀念好嗎

OS: 一定又是某大國又在各種偷個資

O國央視:一切都是米果人搞得鬼

 

吐槽結束( ಠ ͜ʖ ಠ)

反正人家都制定了新規則我們也只能順從囉(攤手)

那麼就來看看這是新增了什麼吧

首先看看AndroidManifest.xml檔案

 

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission
        android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        android:minSdkVersion="30"
        tools:ignore="ScopedStorage" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.CatchErrorRepoter"
        android:requestLegacyExternalStorage="true"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

 

注意粉底白字的部份,基本上前幾行都跟前面差不多

不過在Android 11 之後,Google方面強制要求加入這行

<uses-permission
    android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
    android:minSdkVersion="30"
    tools:ignore="ScopedStorage" />

其功能基本就是會要求我們必須撰寫一點條件式要求使用者跳轉到權限設置畫面來開啟權限

後面會有畫面,稍後會再次提到

 

完成這部分的功能之後,接著來到MainActivity.java的部分

在onCreate的部分加入以下程式碼

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btBoom = findViewById(R.id.button);
        btBoom.setOnClickListener(v->{
            throw new RuntimeException();
        });
        //API版本30以上需要用這種方式取得權限
        if (Build.VERSION.SDK_INT >= 30){
            if (!Environment.isExternalStorageManager()) {
                Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                startActivity(intent);
            }
        }
        else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
                ActivityCompat.checkSelfPermission(this,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED)
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0);

    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == 0)
        {
            for (int grantResult : grantResults)
            {
                if (grantResult == PackageManager.PERMISSION_DENIED)
                {
                    Toast.makeText(this, "給個權限吧大哥", Toast.LENGTH_SHORT).show();
                }
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

集中看一下這個程式碼

if (Build.VERSION.SDK_INT >= 30){
    if (!Environment.isExternalStorageManager()) {
        Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
        startActivity(intent);
    }
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
        ActivityCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED)
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);

基本上要先判斷使用者手機的版本,如果SDK30以上(大概Android 11),就會進新版的權限給予程序

其程序會跳轉至如下圖

 

Screenshot_20230101_152954

在這邊使用者操作確認給予後,便是開啟了權限功能

剩下的部分都跟之前的一樣,詳情可以參考這篇文章

->碼農日常-『Android studio』檔案輸出至手機資料夾(File export)

權限功能完成後可以試著執行一下,基本上沒問題的話就進入下一步囉!

 


 

3. 撰寫報錯系統

 

接下來進入重頭戲了,我們要來完成報錯組件的設置

首先創建一個class,叫他CrashHandler

然後在初始設置中,使用工具Thread.UncaughtExceptionHandler

接著就會變成這樣

public class CrashHandler implements Thread.UncaughtExceptionHandler{

    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
        
    }
}

 

其中的概念也很簡單,其實Google的工程師們早就想到會有需要這個功能了

因此他們早在Thread函式庫底下幫你寫好工具方法了XD

接著我們需要加入一些基本宣告設置

public class CrashHandler implements Thread.UncaughtExceptionHandler{
    private static final String pathSD = Environment.getExternalStorageDirectory().getAbsolutePath();
    private static final String  fileNameSuffix = ".txt";
    private final static CrashHandler instance = new CrashHandler();
    private Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
    private Context mContext;
    public static final String TAG = CrashHandler.class.getSimpleName();

    public static CrashHandler getInstance(){
        return instance;
    }
    public void init(Context context){
        uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
        mContext = context.getApplicationContext();
    }
    

    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
        

    }
}

這部分在寫什麼....感覺講了也像是在講廢話XD

寫過一點程式的人大概也可以明白一二,這部分我就不多廢話

 

接著我們要在寫入跟寫資料相關的程式,請在上面         的部分加入以下程式

private String appendPhoneInfo() throws PackageManager.NameNotFoundException
{
    PackageManager pm = mContext.getPackageManager();
    PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
    StringBuilder stringBuilder = new StringBuilder();

    stringBuilder.append("APP Version Name: ");
    stringBuilder.append(pi.versionName).append("\n");

    stringBuilder.append("APP Version Code: ");
    stringBuilder.append(pi.versionCode).append("\n");

    stringBuilder.append("Mobile SDK: ");
    stringBuilder.append(Build.VERSION.SDK_INT).append("\n");

    stringBuilder.append("Mobile  Factory: ");
    stringBuilder.append(Build.MANUFACTURER).append("\n");

    stringBuilder.append("Type: ");
    stringBuilder.append(Build.MODEL).append("\n");

    stringBuilder.append("CPU: ");
    stringBuilder.append(Arrays.toString(Build.SUPPORTED_ABIS)).append("\n\n");
    stringBuilder.append("Error Code: ");
    return stringBuilder.toString();
}

@SuppressLint("SimpleDateFormat")
private void exportReport(Throwable throwable){
    @SuppressLint("SimpleDateFormat")
    String time = new SimpleDateFormat("yyyy-MM-dd HHmmss").format(new Date());
    //坑:Android 11以上不允許檔案名稱有":"
    //參考:https://stackoverflow.com/questions/61406818/filenotfoundexception-open-failed-eperm-operation-not-permitted-during-saving
    if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
    {
        Log.e(TAG, "SD卡不存在");
        return;
    }
    File file = new File(pathSD+ File.separator
            + time +mContext.getString(R.string.app_name)+ fileNameSuffix);

    try{
        PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter(file)));
        printWriter.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        printWriter.println(appendPhoneInfo());
        printWriter.println(throwable);
        printWriter.close();
    }catch (Exception e){
        e.printStackTrace();
        Log.d(TAG, "exportReport: 寫出失敗"+e);
    }
}

這部分有一個坑,我在註解裡有寫__φ(..;)

當時發現的時候也是一個意外,因為我通常在寫這種不需要連接硬體的程式都時候都會使用模擬器執行,力求讀者看程式的時候可以直接載來用執行

而模擬器我也基本都用最新版的模擬器,不過當時不知道為什麼在我實體手機(SDK29)能執行的東西在模擬器卻無法執行...

後來我查了很多資料(抱歉資料來源不小心給我關掉了),發現原來

Android 11在輸出檔案時的檔案名稱不能有冒號

Android 11在輸出檔案時的檔案名稱不能有冒號

Android 11在輸出檔案時的檔案名稱不能有冒號

殺小啦

Google!!!你幹嘛啦!!!!(╯°Д°)╯ ┻━┻

 

真是莫名其妙....

好啦,總之這裡有這個坑,大家要注意啦!

最後,我們在剛剛uncaughtException的部分加入執行就完成囉

像這樣

@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
    try{
        exportReport(throwable);
    }catch (Exception e){
        e.printStackTrace();
    }
    throwable.printStackTrace();
    if (uncaughtExceptionHandler != null)
    {
        uncaughtExceptionHandler.uncaughtException(thread, throwable);
    } else
    {
        Process.killProcess(Process.myPid());
    }

}

這部分的程式一言以蔽之就是會在程式Crash的時候被執行,簡單吧XD

 

最後是這部分的所有程式碼

CrashHandler.java

public class CrashHandler implements Thread.UncaughtExceptionHandler{
    private static final String pathSD = Environment.getExternalStorageDirectory().getAbsolutePath();
    private static final String  fileNameSuffix = ".txt";
    private final static CrashHandler instance = new CrashHandler();
    private Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
    private Context mContext;
    public static final String TAG = CrashHandler.class.getSimpleName();

    public static CrashHandler getInstance(){
        return instance;
    }
    public void init(Context context){
        uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
        mContext = context.getApplicationContext();
    }
    private String appendPhoneInfo() throws PackageManager.NameNotFoundException
    {
        PackageManager pm = mContext.getPackageManager();
        PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
        StringBuilder stringBuilder = new StringBuilder();

        stringBuilder.append("APP Version Name: ");
        stringBuilder.append(pi.versionName).append("\n");

        stringBuilder.append("APP Version Code: ");
        stringBuilder.append(pi.versionCode).append("\n");

        stringBuilder.append("Mobile SDK: ");
        stringBuilder.append(Build.VERSION.SDK_INT).append("\n");

        stringBuilder.append("Mobile  Factory: ");
        stringBuilder.append(Build.MANUFACTURER).append("\n");

        stringBuilder.append("Type: ");
        stringBuilder.append(Build.MODEL).append("\n");

        stringBuilder.append("CPU: ");
        stringBuilder.append(Arrays.toString(Build.SUPPORTED_ABIS)).append("\n\n");
        stringBuilder.append("Error Code: ");
        return stringBuilder.toString();
    }

    @SuppressLint("SimpleDateFormat")
    private void exportReport(Throwable throwable){
        @SuppressLint("SimpleDateFormat")
        String time = new SimpleDateFormat("yyyy-MM-dd HHmmss").format(new Date());
        //坑:Android 11以上不允許檔案名稱有":"
        //參考:https://stackoverflow.com/questions/61406818/filenotfoundexception-open-failed-eperm-operation-not-permitted-during-saving
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
        {
            Log.e(TAG, "SD卡不存在");
            return;
        }
        File file = new File(pathSD+ File.separator
                + time +mContext.getString(R.string.app_name)+ fileNameSuffix);

        try{
            PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter(file)));
            printWriter.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            printWriter.println(appendPhoneInfo());
            printWriter.println(throwable);
            printWriter.close();
        }catch (Exception e){
            e.printStackTrace();
            Log.d(TAG, "exportReport: 寫出失敗"+e);
        }
    }

    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
        try{
            exportReport(throwable);
        }catch (Exception e){
            e.printStackTrace();
        }
        throwable.printStackTrace();
        if (uncaughtExceptionHandler != null)
        {
            uncaughtExceptionHandler.uncaughtException(thread, throwable);
        } else
        {
            Process.killProcess(Process.myPid());
        }

    }
}

 

 


 

4. 加入報錯系統並完成程式

 

好了,把模組寫完之後,最後就是加入到主程式內了

其實非常簡單,就一行

直接看完整程式吧

 

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CrashHandler.getInstance().init(this);
        Button btBoom = findViewById(R.id.button);
        btBoom.setOnClickListener(v->{
            throw new RuntimeException();
        });
        //API版本30以上需要用這種方式取得權限
        if (Build.VERSION.SDK_INT >= 30){
            if (!Environment.isExternalStorageManager()) {
                Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                startActivity(intent);
            }
        }
        else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
                ActivityCompat.checkSelfPermission(this,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED)
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);

    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == 0)
        {
            for (int grantResult : grantResults)
            {
                if (grantResult == PackageManager.PERMISSION_DENIED)
                {
                    Toast.makeText(this, "給個權限吧大哥", Toast.LENGTH_SHORT).show();
                }
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

加入粉底白字的部分就完成囉!

 


就像開頭說的,這篇文章我早在之前還在工作時就想發了,不過寫完Code之後就一直放在電腦裡忘了他的存在QAQ

後來是最近在整理桌面的時候才發現「阿靠,這個忘記寫了....」

根本就像從家裡的垃圾堆翻出寶物啊哈哈哈哈

好啦,寫這很辛苦;大家不吝的話~

TK

arrow
arrow
    創作者介紹
    創作者 碼農日常 的頭像
    碼農日常

    碼農日常大小事

    碼農日常 發表在 痞客邦 留言(0) 人氣()