Perlu menangani pengecualian yang tidak tertangkap dan mengirim file log

114

PEMBARUAN: Silakan lihat solusi "diterima" di bawah

Saat aplikasi saya membuat pengecualian yang tidak tertangani, bukan hanya menghentikan, saya ingin memberikan kesempatan kepada pengguna untuk mengirim file log. Saya menyadari bahwa melakukan lebih banyak pekerjaan setelah mendapatkan pengecualian acak berisiko, tetapi, hei, yang terburuk adalah aplikasi selesai mogok dan file log tidak terkirim. Ini ternyata lebih rumit dari yang saya harapkan :)

Apa yang berhasil: (1) menjebak pengecualian yang tidak tertangkap, (2) mengekstrak info log dan menulis ke file.

Yang belum berhasil: (3) memulai kegiatan mengirim email. Akhirnya, saya akan memiliki aktivitas lain untuk meminta izin pengguna. Jika saya mengaktifkan aktivitas email, saya tidak berharap banyak masalah untuk yang lain.

Inti masalahnya adalah bahwa pengecualian yang tidak tertangani tertangkap di kelas Aplikasi saya. Karena itu bukan Aktivitas, cara memulai aktivitas dengan Intent.ACTION_SEND tidak jelas. Artinya, biasanya untuk memulai aktivitas, seseorang memanggil startActivity dan melanjutkan dengan onActivityResult. Metode ini didukung oleh Aktivitas tetapi tidak oleh Aplikasi.

Ada saran tentang cara untuk melakukan hal ini?

Berikut beberapa potongan kode sebagai panduan memulai:

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[] {"[email protected]"});
    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);
  }
}
Peri Hartman
sumber
4
Secara pribadi saya hanya menggunakan ACRA , meskipun ini open source sehingga Anda dapat memeriksa bagaimana mereka melakukannya ...
David O'Meara

Jawaban:

240

Inilah solusi lengkapnya (hampir: Saya menghilangkan tata letak UI dan penanganan tombol) - berasal dari banyak eksperimen dan berbagai posting dari orang lain yang terkait dengan masalah yang muncul di sepanjang jalan.

Ada beberapa hal yang perlu Anda lakukan:

  1. Tangani uncaughtException di subclass Application Anda.
  2. Setelah menangkap pengecualian, mulailah aktivitas baru untuk meminta pengguna mengirim log.
  3. Ekstrak info log dari file logcat dan tulis ke file Anda sendiri.
  4. Mulai aplikasi email, berikan file Anda sebagai lampiran.
  5. Manifes: filter aktivitas Anda agar dikenali oleh penangan pengecualian Anda.
  6. Secara opsional, atur Proguard untuk menghapus Log.d () dan Log.v ().

Nah, berikut detailnya:

(1 & 2) Tangani uncaughtException, mulai kirim aktivitas log:

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) Ekstrak log (saya taruh ini sebagai Aktivitas SendLog saya):

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) Mulai aplikasi email (juga di Aktivitas SendLog saya):

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[] {"[email protected]"});
  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) Inilah tampilan SendLog (Anda harus menambahkan UI, meskipun):

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) Manifes:

<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) Pengaturan Proguard:

Di project.properties, ubah baris config. Anda harus menentukan "optimalkan" atau Proguard tidak akan menghapus panggilan Log.v () dan Log.d ().

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

Di proguard-project.txt, tambahkan berikut ini. Ini memberitahu Proguard untuk menganggap Log.v dan Log.d tidak memiliki efek samping (meskipun mereka melakukannya sejak mereka menulis ke log) dan dengan demikian dapat dihapus selama pengoptimalan:

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

Itu dia! Jika Anda memiliki saran untuk perbaikan ini, beri tahu saya dan saya dapat memperbarui ini.

Peri Hartman
sumber
10
Sekadar catatan: jangan pernah menelepon System.exit. Anda akan memutuskan rantai penangan pengecualian yang tidak tertangkap. Teruskan saja ke yang berikutnya. Anda sudah memiliki "defaultUncaughtHandler" dari getDefaultUncaughtExceptionHandler, cukup teruskan panggilan ke sana setelah Anda selesai.
gilm
2
@gilm, dapatkah Anda memberikan contoh tentang cara "meneruskan ke berikutnya" dan apa lagi yang mungkin terjadi dalam rantai penangan? Sudah lama, tetapi saya menguji sejumlah skenario, dan memanggil System.exit () tampaknya menjadi solusi terbaik. Lagipula, aplikasi macet dan harus dihentikan.
Peri Hartman
Saya rasa saya ingat apa yang terjadi jika Anda membiarkan pengecualian yang tidak tertangkap berlanjut: sistem akan memasang pemberitahuan "aplikasi dihentikan". Biasanya, itu akan baik-baik saja. Tetapi karena aktivitas baru (yang mengirim email dengan log) sudah memasang pesannya sendiri, membuat sistem memasang yang lain akan membingungkan. Jadi saya memaksanya untuk membatalkan secara diam-diam dengan System.exit ().
Peri Hartman
8
@PeriHartman yakin: hanya ada satu penangan default. sebelum memanggil setDefaultUncaughtExceptionHandler (), Anda harus memanggil getDefaultUncaughtExceptionHandler () dan menyimpan referensi tersebut. Jadi, katakanlah Anda memiliki bugsense, crashlytics dan handler Anda adalah yang terakhir diinstal, sistem akan memanggil milik Anda saja. tugas Anda adalah memanggil referensi yang Anda terima melalui getDefaultUncaughtExceptionHandler () dan meneruskan utas serta melempar ke rantai berikutnya. jika Anda hanya System.exit (), yang lain tidak akan dipanggil.
gilm
4
Anda juga perlu mendaftarkan implementasi Aplikasi Anda dalam manifes dengan atribut di tag Aplikasi, misalnya: <application android: name = "com.mydomain.MyApplication" other attrs ... />
Matt
9

Saat ini ada banyak alat reprting kerusakan yang melakukan ini dengan mudah.

  1. crashlytics - Alat pelaporan kerusakan, gratis, tetapi memberi Anda laporan dasar. Keuntungan: Gratis

  2. Gryphonet - Alat pelaporan yang lebih canggih, memerlukan beberapa jenis biaya. Keuntungan: Kemacetan yang mudah, ANR, kelambatan ...

Jika Anda adalah pengembang pribadi, saya akan menyarankan Crashlytics, tetapi jika itu adalah organisasi besar, saya akan memilih Gryphonet.

Semoga berhasil!

Ariel Bell
sumber
5

Coba gunakan ACRA sebagai gantinya - ini menangani pengiriman pelacakan tumpukan serta banyak informasi debug berguna lainnya ke backend Anda, atau ke dokumen Google Docs yang telah Anda siapkan.

https://github.com/ACRA/acra

Martin Konecny
sumber
5

Jawaban @ PeriHartman berfungsi dengan baik saat utas UI menampilkan pengecualian yang tidak tertangkap. Saya membuat beberapa perbaikan ketika pengecualian yang tidak tertangkap dilemparkan oleh utas non 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
}
Jack Ruan
sumber
2

Dijelaskan dengan baik. Tapi satu pengamatan di sini, alih-alih menulis ke file menggunakan File Writer dan Streaming, saya memanfaatkan opsi logcat -f secara langsung. Ini kodenya

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();
        }

Ini membantu saya dalam membersihkan info buffer terbaru. Menggunakan File streaming memberi saya satu masalah yaitu tidak membersihkan log terbaru dari buffer. Tapi bagaimanapun, ini adalah panduan yang sangat membantu. Terima kasih.

Schow
sumber
Saya yakin saya sudah mencobanya (atau yang serupa). Jika saya ingat, saya mengalami masalah izin. Apakah Anda melakukan ini di perangkat yang di-rooting?
Peri Hartman
Oh, saya minta maaf jika saya membuat Anda bingung, tetapi sejauh ini saya mencoba dengan emulator saya. Saya belum mem-portingnya ke perangkat (tapi ya, perangkat yang saya gunakan untuk pengujian adalah yang sudah di-root).
Schow
2

Menangani Pengecualian yang tidak tertangkap: seperti yang dijelaskan @gilm, lakukan saja ini, (kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

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

Saya harap ini membantu, ini berhasil untuk saya .. (: y). Dalam kasus saya, saya telah menggunakan perpustakaan 'com.microsoft.appcenter.crashes.Crashes' untuk pelacakan kesalahan.

Kreshnik
sumber
0

Anda dapat menangani pengecualian yang tidak tertangkap menggunakan pustaka FireCrasher dan melakukan pemulihan darinya.

Anda dapat mengetahui lebih banyak tentang perpustakaan di Artikel Sedang ini

Osama Raddad
sumber