Потрібно обробити невиконаний виняток та надіслати файл журналу


114

ОНОВЛЕННЯ: Будь ласка, див. "Прийняте" рішення нижче

Коли мій додаток створює необроблений виняток, а не просто його завершення, я спершу даю користувачеві можливість надіслати файл журналу. Я розумію, що робити більше роботи після отримання випадкового винятку ризиковано, але, ей, найгірше, що додаток закінчується збоєм, і файл журналу не надсилається. Це виявляється складніше, ніж я очікував :)

Що працює: (1) вилучення невловимого винятку, (2) вилучення інформації журналу та запис у файл.

Що ще не працює: (3) розпочати діяльність з надсилання електронної пошти. Зрештою, у мене буде ще одна діяльність, щоб запитати дозволу користувача. Якщо я працюю над електронною поштою, я не чекаю особливих проблем для інших.

Суть проблеми полягає в тому, що необроблений виняток потрапляє до мого класу Application. Оскільки це не діяльність, не очевидно, як розпочати діяльність з наміром.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

Ось повне рішення (майже: я опустив макет інтерфейсу та керування кнопками) - отриманий з безлічі експериментів та різних публікацій інших, пов’язаних із проблемами, які виникали на цьому шляху.

Вам потрібно зробити кілька речей:

  1. Обробіть uncaughtException у своєму підкласі Application.
  2. Уловивши виняток, почніть нову діяльність, щоб попросити користувача надіслати журнал.
  3. Витягніть інформацію журналу з файлів logcat і запишіть у свій власний файл.
  4. Запустіть додаток електронної пошти, надавши файл у якості вкладення.
  5. Маніфест: відфільтруйте свою діяльність, щоб її визнав ваш обробник винятків.
  6. За бажанням встановіть Proguard для викреслення Log.d () та Log.v ().

Тепер ось деталі:

(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 (вам потрібно буде додати інтерфейс користувача):

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) Засіб налаштування:

У project.properties змініть рядок конфігурації. Ви повинні вказати "оптимізувати", або 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. Ви будете ламати ланцюжок обробників винятків винятків. Просто перенесіть його до наступного. У вас уже є "defaultUncaughtHandler" від getDefaultUncaughtExceptionHandler, просто передайте йому виклик, коли закінчите.
gilm

2
@gilm, чи можете ви навести приклад того, як "переслати його на наступне" і що ще може статися в ланцюзі обробників? Минув час, але я перевірив ряд сценаріїв, і виклик System.exit () видався найкращим рішенням. Зрештою, додаток вийшов з ладу, і його потрібно припинити.
Пері Хартман

Я думаю, що я пригадую, що станеться, якщо ви дозволите продовження невиконаного винятку: система виставляє сповіщення "додаток припинено". Зазвичай це було б добре. Але оскільки нова активність (яка надсилає електронний лист із журналом) вже надсилає власне повідомлення, то система, яка виставила іншу, заплутує. Тому я змушую її спокійно переривати роботу з System.exit ().
Пері Хартман

8
@PeriHartman впевнений: є лише один обробник за замовчуванням. перед викликом setDefaultUncaughtExceptionHandler (), ви повинні зателефонувати getDefaultUncaughtExceptionHandler () і зберегти це посилання. Отже, скажіть, у вас є помилка, крашлітики, і ваш обробник останній встановлений, система зателефонує лише вам. ваше завдання викликати посилання, яке ви отримали через getDefaultUncaughtExceptionHandler (), і передавати нитку та передачу в наступну ланцюжок. якщо ви просто System.exit (), інші не будуть викликані.
gilm

4
Вам також потрібно зареєструвати свою реалізацію Application в маніфесті з атрибутом у тезі Application, наприклад: <application android: name = "com.mydomain.MyApplication" інші attrs ... />
Matt

9

Сьогодні існує безліч інструментів для перезавантаження крашів, які роблять це легко.

  1. крашлітики - засіб звітування про аварії, безкоштовний, але дає основні звіти Переваги: ​​Безкоштовно

  2. Gryphonet - більш досконалий інструмент звітності, вимагає певної плати. Переваги: ​​Легке відтворення аварій, аварійних ситуацій, повільність ...

Якщо ви приватний розробник, я б запропонував Crashlytics, але якщо це велика організація, я б пішов на Gryphonet.

Щасти!


5

Спробуйте скористатись ACRA замість цього - він обробляє надсилання сліду стека, а також тонни іншої корисної інформації про налагодження у ваш бекенд або в документ, який ви створили.

https://github.com/ACRA/acra


5

@ Відповідь PeriHartman добре працює, коли нитка користувальницького інтерфейсу викидає невиконаний виняток. Я вніс декілька вдосконалень, коли невдале виняток перекидається потоком, що не використовується інтерфейсом користувача

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

Приємно пояснив. Але одне спостереження тут, замість того, щоб записувати у файл за допомогою File Writer та Streaming, я безпосередньо використав варіант 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();
        }

Це допомогло мені очистити останню інформацію про буфер. Використання потокової передачі файлів дало мені одну проблему, що вона не змиває останні журнали з буфера. Але все одно, це було справді корисним посібником. Дякую.


Я вважаю, що я спробував це (або щось подібне). Якщо я пригадую, у мене виникли проблеми з дозволом. Ви робите це на укоріненому пристрої?
Пері Хартман

О, вибачте, якщо я вас плутав, але я поки що намагаюся зі своїми емуляторами. Я ще повинен перенести його на пристрій (але так, пристрій, який я використовую для тестування, є кореневим).
шоу

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' для відстеження помилок.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.