キャッチされない例外を処理してログファイルを送信する必要がある


114

更新:以下の「承認された」ソリューションを参照してください

アプリが単に終了するのではなく、未処理の例外を作成する場合、まずユーザーにログファイルを送信する機会を与えたいと思います。ランダムな例外が発生した後にさらに作業を行うのは危険であると私は理解していますが、最悪の場合、アプリのクラッシュが終了し、ログファイルが送信されません。これは私が予想したよりもトリッキーであることが判明しています:)

機能:(1)キャッチされなかった例外をトラップし、(2)ログ情報を抽出してファイルに書き込みます。

まだ機能していないもの:(3)メールを送信するためのアクティビティを開始する。最終的に、ユーザーの許可を求める別のアクティビティがあります。メールアクティビティが正常に機能するようになれば、他のユーザーに問題が発生することはあまりありません。

問題の核心は、未処理の例外が私のApplicationクラスでキャッチされることです。これはアクティビティではないため、Intent.ACTION_SENDでアクティビティを開始する方法は明らかではありません。つまり、通常、アクティビティを開始するには、startActivityを呼び出し、onActivityResultで再開します。これらのメソッドは、アクティビティではサポートされていますが、アプリケーションではサポートされていません。

これを行う方法に関する提案はありますか?

以下は、開始ガイドとしてのコードの抜粋です。

public class MyApplication extends Application
{
  defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();
  public void onCreate ()
  {
    Thread.setDefaultUncaughtExceptionHandler (new Thread.UncaughtExceptionHandler()
    {
      @Override
      public void uncaughtException (Thread thread, Throwable e)
      {
        handleUncaughtException (thread, e);
      }
    });
  }

  private void handleUncaughtException (Thread thread, Throwable e)
  {
    String fullFileName = extractLogToFile(); // code not shown

    // The following shows what I'd like, though it won't work like this.
    Intent intent = new Intent (Intent.ACTION_SEND);
    intent.setType ("plain/text");
    intent.putExtra (Intent.EXTRA_EMAIL, new String[] {"me@mydomain.com"});
    intent.putExtra (Intent.EXTRA_SUBJECT, "log file");
    intent.putExtra (Intent.EXTRA_STREAM, Uri.parse ("file://" + fullFileName));
    startActivityForResult (intent, ACTIVITY_REQUEST_SEND_LOG);
  }

  public void onActivityResult (int requestCode, int resultCode, Intent data)
  {
    if (requestCode == ACTIVITY_REQUEST_SEND_LOG)
      System.exit(1);
  }
}

4
個人的に私はACRAを使用していますが、それはオープンソースなので、どのように動作するかを確認できます...
David O'Meara

回答:


240

ここに完全な解決策があります(ほとんど:私はUIレイアウトとボタン処理を省略しました)-多くの実験と、途中で発生した問題に関連する他の人からのさまざまな投稿から派生しました。

やらなければならないことがたくさんあります:

  1. アプリケーションサブクラスでuncaughtExceptionを処理します。
  2. 例外をキャッチした後、新しいアクティビティを開始して、ユーザーにログの送信を依頼します。
  3. logcatのファイルからログ情報を抽出し、独自のファイルに書き込みます。
  4. メールアプリを起動し、ファイルを添付ファイルとして提供します。
  5. マニフェスト:例外ハンドラーによって認識されるようにアクティビティをフィルタリングします。
  6. オプションで、Log.d()とLog.v()を取り除くようにProguardを設定します。

詳細は次のとおりです。

(1&2)uncaughtExceptionを処理し、ログ送信アクティビティを開始します。

public class MyApplication extends Application
{
  public void onCreate ()
  {
    // Setup handler for uncaught exceptions.
    Thread.setDefaultUncaughtExceptionHandler (new Thread.UncaughtExceptionHandler()
    {
      @Override
      public void uncaughtException (Thread thread, Throwable e)
      {
        handleUncaughtException (thread, e);
      }
    });
  }

  public void handleUncaughtException (Thread thread, Throwable e)
  {
    e.printStackTrace(); // not all Android versions will print the stack trace automatically

    Intent intent = new Intent ();
    intent.setAction ("com.mydomain.SEND_LOG"); // see step 5.
    intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); // required when starting from Application
    startActivity (intent);

    System.exit(1); // kill off the crashed app
  }
}

(3)ログを抽出します(これをSendLogアクティビティに追加しました):

private String extractLogToFile()
{
  PackageManager manager = this.getPackageManager();
  PackageInfo info = null;
  try {
    info = manager.getPackageInfo (this.getPackageName(), 0);
  } catch (NameNotFoundException e2) {
  }
  String model = Build.MODEL;
  if (!model.startsWith(Build.MANUFACTURER))
    model = Build.MANUFACTURER + " " + model;

  // Make file name - file must be saved to external storage or it wont be readable by
  // the email app.
  String path = Environment.getExternalStorageDirectory() + "/" + "MyApp/";
  String fullName = path + <some name>;

  // Extract to file.
  File file = new File (fullName);
  InputStreamReader reader = null;
  FileWriter writer = null;
  try
  {
    // For Android 4.0 and earlier, you will get all app's log output, so filter it to
    // mostly limit it to your app's output.  In later versions, the filtering isn't needed.
    String cmd = (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) ?
                  "logcat -d -v time MyApp:v dalvikvm:v System.err:v *:s" :
                  "logcat -d -v time";

    // get input stream
    Process process = Runtime.getRuntime().exec(cmd);
    reader = new InputStreamReader (process.getInputStream());

    // write output stream
    writer = new FileWriter (file);
    writer.write ("Android version: " +  Build.VERSION.SDK_INT + "\n");
    writer.write ("Device: " + model + "\n");
    writer.write ("App version: " + (info == null ? "(null)" : info.versionCode) + "\n");

    char[] buffer = new char[10000];
    do 
    {
      int n = reader.read (buffer, 0, buffer.length);
      if (n == -1)
        break;
      writer.write (buffer, 0, n);
    } while (true);

    reader.close();
    writer.close();
  }
  catch (IOException e)
  {
    if (writer != null)
      try {
        writer.close();
      } catch (IOException e1) {
      }
    if (reader != null)
      try {
        reader.close();
      } catch (IOException e1) {
      }

    // You might want to write a failure message to the log here.
    return null;
  }

  return fullName;
}

(4)メールアプリを起動します(これもSendLogアクティビティで):

private void sendLogFile ()
{
  String fullName = extractLogToFile();
  if (fullName == null)
    return;

  Intent intent = new Intent (Intent.ACTION_SEND);
  intent.setType ("plain/text");
  intent.putExtra (Intent.EXTRA_EMAIL, new String[] {"log@mydomain.com"});
  intent.putExtra (Intent.EXTRA_SUBJECT, "MyApp log file");
  intent.putExtra (Intent.EXTRA_STREAM, Uri.parse ("file://" + fullName));
  intent.putExtra (Intent.EXTRA_TEXT, "Log file attached."); // do this so some email clients don't complain about empty body.
  startActivity (intent);
}

(3&4)SendLogは次のようになります(ただし、UIを追加する必要があります)。

public class SendLog extends Activity implements OnClickListener
{
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    requestWindowFeature (Window.FEATURE_NO_TITLE); // make a dialog without a titlebar
    setFinishOnTouchOutside (false); // prevent users from dismissing the dialog by tapping outside
    setContentView (R.layout.send_log);
  }

  @Override
  public void onClick (View v) 
  {
    // respond to button clicks in your UI
  }

  private void sendLogFile ()
  {
    // method as shown above
  }

  private String extractLogToFile()
  {
    // method as shown above
  }
}

(5)マニフェスト:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ... >
    <!-- needed for Android 4.0.x and eariler -->
    <uses-permission android:name="android.permission.READ_LOGS" /> 

    <application ... >
        <activity
            android:name="com.mydomain.SendLog"
            android:theme="@android:style/Theme.Dialog"
            android:textAppearance="@android:style/TextAppearance.Large"
            android:windowSoftInputMode="stateHidden">
            <intent-filter>
              <action android:name="com.mydomain.SEND_LOG" />
              <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
     </application>
</manifest>

(6)Proguardのセットアップ:

project.propertiesで、config行を変更します。「optimize」を指定する必要があります。そうしないと、ProguardはLog.v()およびLog.d()呼び出しを削除しません

proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt

proguard-project.txtに以下を追加します。これは、ProguardにLog.vとLog.dには副作用がない(ただし、ログに書き込むので副作用がある)と想定しているため、最適化中に削除できます。

-assumenosideeffects class android.util.Log {
    public static int v(...);
    public static int d(...);
}

それでおしまい!これを改善するための提案がある場合は、私に知らせてください。これを更新する場合があります。


10
注:System.exitを呼び出さないでください。キャッチされなかった例外ハンドラチェーンを解除します。次へそれを転送します。getDefaultUncaughtExceptionHandlerからの「defaultUncaughtHandler」はすでにあります。完了したら、呼び出しを渡してください。
gilm 2014

2
@gilm、「それを次へ転送する」方法の例と、ハンドラチェーンで他に何が発生する可能性があるかを説明できますか?久しぶりですが、いくつかのシナリオをテストしましたが、System.exit()を呼び出すのが最善の解決策のようです。結局のところ、アプリはクラッシュし、終了する必要があります。
Peri Hartman

キャッチされない例外を継続させるとどうなるかを思い出します。システムが「アプリ終了」通知を表示します。通常、それで問題ありません。しかし、新しいアクティビティ(ログを含む電子メールを送信する)はすでに独自のメッセージを送信しているため、システムに別のメッセージを送信させると混乱を招きます。だから私はそれをSystem.exit()で静かに中止することを強制します。
Peri Hartman

8
@PeriHartman確かに:デフォルトのハンドラーは1つだけです。setDefaultUncaughtExceptionHandler()を呼び出す前に、getDefaultUncaughtExceptionHandler()を呼び出して、その参照を保持する必要があります。つまり、bugsense、crashlyticsがあり、ハンドラーが最後にインストールされたとすると、システムはあなたのものだけを呼び出します。getDefaultUncaughtExceptionHandler()を介して受け取った参照を呼び出し、スレッドとスロー可能オブジェクトをチェーン内の次のスレッドに渡すのはあなたの仕事です。System.exit()だけの場合、他は呼び出されません。
gilm 2014年

4
また、アプリケーションの実装をマニフェストにApplicationタグの属性で登録する必要があります。例:<application android:name = "com.mydomain.MyApplication" other attrs ... />
Matt

9

今日、これを簡単に行う多くのクラッシュreprtingツールがあります。

  1. crashlytics-クラッシュレポートツール、無料ですが、基本的なレポートを提供します利点:無料

  2. Gryphonet-より高度なレポートツールで、何らかの料金が必要です。利点:クラッシュ、ANR、スローの簡単な再現...

あなたが非公開の開発者であればCrashlyticsをお勧めしますが、大規模な組織であればGryphonetを選びます。

幸運を!



5

@PeriHartmanの回答は、UIスレッドがキャッチされない例外をスローしたときにうまく機能します。キャッチされなかった例外が非UIスレッドによってスローされる場合の改善を行いました。

public boolean isUIThread(){
    return Looper.getMainLooper().getThread() == Thread.currentThread();
}

public void handleUncaughtException(Thread thread, Throwable e) {
    e.printStackTrace(); // not all Android versions will print the stack trace automatically

    if(isUIThread()) {
        invokeLogActivity();
    }else{  //handle non UI thread throw uncaught exception

        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                invokeLogActivity();
            }
        });
    }
}

private void invokeLogActivity(){
    Intent intent = new Intent ();
    intent.setAction ("com.mydomain.SEND_LOG"); // see step 5.
    intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); // required when starting from Application
    startActivity (intent);

    System.exit(1); // kill off the crashed app
}

2

うまく説明しました。ただし、ここでは、ファイルライターとストリーミングを使用してファイルに書き込む代わりに、logcat -fオプションを直接使用しました。これがコードです

String[] cmd = new String[] {"logcat","-f",filePath,"-v","time","<MyTagName>:D","*:S"};
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

これは、最新のバッファ情報をフラッシュするのに役立ちました。ファイルストリーミングを使用すると、最新のログがバッファからフラッシュされないという問題が1つありました。とにかく、これは本当に役立つガイドでした。ありがとうございました。


私はそれを試したと思います(または同様の何か)。思い出すと、許可の問題に遭遇しました。ルート権限を取得したデバイスでこれを行っていますか?
Peri Hartman、2014

ああ、混乱してしまって申し訳ありませんが、今のところエミュレータを使ってみています。まだデバイスに移植していません(ただし、テストに使用するデバイスはルート化されたデバイスです)。
schow

2

キャッチされない例外の処理: @gilmが説明しているように、これを行うだけです(kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

override fun onCreate() {
  //...
    Thread.setDefaultUncaughtExceptionHandler { t, e ->
        Crashlytics.logException(e)
        defaultUncaughtHandler?.uncaughtException(t, e)
    }
}

私はそれが役に立てば幸いです、それは私のために働いた..(:y)。私の場合、エラー追跡に「com.microsoft.appcenter.crashes.Crashes」ライブラリを使用しました。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.