2010年10月5日火曜日

Android キャッチされなかった例外を処理する

Androidアプリでキャッチされなかった例外を処理する方法が下記のサイトに詳しく紹介されています。

throw Life - Androidアプリのバグ報告システムを考える
冬通りに消え行く制服ガールは、夢物語にリアルを求めない。 - Android でアプリケーションが強制終了したとき、エラーレポートを送るようにする

上記のリンクを参考に、例外のスタックトレースをメールで送信できるようにしてみます。

ボタンを押すと例外が発生し、アプリは強制終了します。

もう一度アプリを起動すると、バグレポートをメール送信するか確認メッセージを表示します。
Postボタンをタップすると、メールを送信します。



catchしなかった例外を補足するためにThread.UncaughtExceptionHandleを実装したCsUncaughtExceptionHandlerを作成します。
uncaughtException()メソッドで例外のスタックトレースをファイルに書き込みます。
SendBugReport()メソッドでファイルを読み込んでメールを送信します。
package my.study.android;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.PrintWriter;
import java.lang.Thread.UncaughtExceptionHandler;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;

public class CsUncaughtExceptionHandler implements java.lang.Thread.UncaughtExceptionHandler {
    private static Context sContext = null;
    private static final String BUG_FILE = "BugReport";
    private static final UncaughtExceptionHandler sDefaultHandler 
             = Thread.getDefaultUncaughtExceptionHandler();
    
    /**
     * コンストラクタ
     * @param context
     */
    public CsUncaughtExceptionHandler(Context context){
        sContext = context;
    }

    /**
     * キャッチされない例外によって指定されたスレッドが終了したときに呼び出されます
     * 例外スタックトレースの内容をファイルに出力します
     */
    public void uncaughtException(Thread thread, Throwable ex) { 
        PrintWriter pw = null;   
        try {
            pw = new PrintWriter(sContext.openFileOutput(BUG_FILE, Context.MODE_WORLD_READABLE));
            ex.printStackTrace(pw); 
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (pw != null) pw.close();      
        }
        sDefaultHandler.uncaughtException(thread, ex);
    }
    
    /**
     * バグレポートの内容をメールで送信します。
     * @param activity
     */
    public static void SendBugReport(final Activity activity) {
        //バグレポートがなければ以降の処理を行いません。
        final File bugfile = activity.getFileStreamPath(BUG_FILE);
        if (!bugfile.exists()) {
            return;
        }    
        //AlertDialogを表示します。
        AlertDialog.Builder alert = new AlertDialog.Builder(activity);
        alert.setTitle("ERROR");
        alert.setMessage("予期しないエラーが発生しました。開発元にエラーを送信してください。");
        alert.setPositiveButton("Post", new DialogInterface.OnClickListener(){
            @Override
            public void onClick(DialogInterface dialog, int which) {
                SendMail(activity,bugfile);
            }});
        alert.setNegativeButton("Cancel", null);
        alert.show();
    }
    
    /**
     * バグレポートの内容をメールで送信します。
     * @param activity
     * @param bugfile
     */
    private static void SendMail(final Activity activity,File bugfile){
        //バグレポートの内容を読み込みます。
        StringBuilder sb = new StringBuilder();
        try {
            BufferedReader br = new BufferedReader(new FileReader(bugfile));
            String str;      
            while((str = br.readLine()) != null){      
                sb.append(str +"\n");     
            }       
        } catch (Exception e) {
            e.printStackTrace();
        }
        //メールで送信します。
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_SENDTO);
        intent.setData(Uri.parse("mailto:" + "xxx@xxxx.xx.xx"));
        intent.putExtra(Intent.EXTRA_SUBJECT, "【BugReport】" + R.string.app_name );
        intent.putExtra(Intent.EXTRA_TEXT, sb.toString());
        activity.startActivity(intent);
        //バグレポートを削除します。
        bugfile.delete();
    }
    
}

スレッドでキャッチされなかった例外を捕捉するために、Thread.setDefaultUncaughtExceptionHandler()メソッドにCsUncaughtExceptionHandlerオブジェクトを設定します。
アプリで1度だけハンドラを設定すればいいので、一番最初に表示されるMainActivityのonCreate()メソッドで設定します。
CsUncaughtExceptionHandlerのコンストラクタの引数には、メモリリークが発生しないようgetApplicationContext()メソッドで取得したcontextを渡すようにします。
※アプリケーションで共通に使用するcontextに、現在のActivityを指定するとメモリが解放されない事があります。
こちらで詳しく書かれています。textdrop-Androidでのメモリリーク回避

MainActivityにはボタンを1つ配置し、タップするとArithmeticExceptionを発生します。

onStart()メソッドでは、CsUncaughtExceptionHandler.SendBugReport()メソッドを呼び出して、バグレポートがあればメールを送信するようにします。

package my.study.android;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends Activity {

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //レイアウトを読み込みます
        this.setContentView(R.layout.main);
       
        //アプリケーションで共通に利用するオブジェクトには、メモリリークが発生しないようにthisではなく
        //Context.getApplicationContext()を使用します。
        Context context = this.getApplicationContext(); 
        //キャッチされない例外により、スレッドが突然終了したときや、
        //このスレッドに対してほかにハンドラが定義されていないときに
        //呼び出されるデフォルトのハンドラを設定します。
        Thread.setDefaultUncaughtExceptionHandler(new CsUncaughtExceptionHandler(context));

        //ボタンにOnClickListenerを設定します。
        Button btn = (Button)this.findViewById(R.id.btnThrowEx);
        btn.setOnClickListener(new OnClickListener(){
            @Override
            public void onClick(View arg0) {
                int index = 5 / 0;  //ArithmeticExceptionを発生させる
            }});
    }

    @Override
    protected void onStart() {
        super.onStart();
        CsUncaughtExceptionHandler.SendBugReport(this);
    }
}

0 件のコメント: