Необходимо обработать неперехваченное исключение и отправить файл журнала

115

ОБНОВЛЕНИЕ: см. "Принятое" решение ниже

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

Что работает: (1) перехват неперехваченного исключения, (2) извлечение информации журнала и запись в файл.

Что пока не работает: (3) начало действия по отправке электронной почты. В конце концов, у меня будет еще одно действие, чтобы спросить разрешения пользователя. Если у меня работает электронная почта, я не ожидаю особых проблем для другого.

Суть проблемы в том, что необработанное исключение попадает в мой класс Application. Поскольку это не Activity, не очевидно, как начать действие с Intent.ACTION_SEND. То есть обычно для начала действия вызывается startActivity и возобновляется с onActivityResult. Эти методы поддерживаются Activity, но не Application.

Есть предложения, как это сделать?

Вот несколько фрагментов кода в качестве руководства:

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 , хотя это открытый исходный код, так что вы можете проверить, как они это делают ...
Дэвид О'Мира

Ответы:

241

Вот полное решение (почти: я пропустил макет пользовательского интерфейса и обработку кнопок), полученное в результате множества экспериментов и различных сообщений других людей, связанных с проблемами, которые возникли на этом пути.

Вам нужно сделать следующее:

  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) Настройка Proguard:

В 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 () оказался лучшим решением. В конце концов, приложение разбилось, и его нужно прекратить.
Peri Hartman
Думаю, я припоминаю, что произойдет, если вы позволите неперехваченному исключению продолжаться: система выдает уведомление «приложение завершено». Обычно это было бы хорошо. Но поскольку новое действие (которое отправляет электронное письмо с журналом) уже создает собственное сообщение, создание системой другого сообщения сбивает с толку. Поэтому я заставляю его тихо прервать работу с помощью System.exit ().
Peri Hartman
8
@PeriHartman уверен: есть только один обработчик по умолчанию. перед вызовом setDefaultUncaughtExceptionHandler () вы должны вызвать getDefaultUncaughtExceptionHandler () и сохранить эту ссылку. Итак, допустим, у вас есть bugsense, crashlytics и ваш обработчик установлен последним, система вызовет только ваш. ваша задача - вызвать ссылку, которую вы получили через getDefaultUncaughtExceptionHandler (), и передать поток и передать его следующему в цепочке. если вы просто System.exit (), остальные не будут вызываться.
gilm
4
Вам также необходимо зарегистрировать свою реализацию Application в манифесте с атрибутом в теге Application, например: <application android: name = "com.mydomain.MyApplication" other attrs ... />
Мэтт
9

Сегодня существует множество инструментов для перепечатки сбоев, с которыми это легко сделать.

  1. crashlytics - бесплатный инструмент для отчетов о сбоях , но с базовыми отчетами. Преимущества: бесплатно.

  2. Gryphonet - более продвинутый инструмент отчетности, требует некоторой платы. Преимущества: Легкое воссоздание сбоев, ANR, медлительности ...

Если вы частный разработчик, я бы посоветовал Crashlytics, но если это большая организация, я бы выбрал Gryphonet.

Удачи!

Ариэль Белл
источник
5

Попробуйте вместо этого использовать ACRA - он обрабатывает отправку трассировки стека, а также множество другой полезной отладочной информации на ваш сервер или в настроенный вами документ Google Docs.

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

Это помогло мне очистить последнюю информацию о буфере. Использование потоковой передачи файлов дало мне одну проблему, заключающуюся в том, что не удалялись последние журналы из буфера. Но в любом случае это было действительно полезное руководство. Спасибо.

schow
источник
Кажется, я пробовал это (или что-то подобное). Если я помню, у меня возникли проблемы с разрешением. Вы делаете это на рутированном устройстве?
Peri Hartman
Ой, извините, если я запутал вас, но я пока пытаюсь со своими эмуляторами. Я еще не перенес его на устройство (но да, устройство, которое я использую для тестирования, является корневым).
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 для отслеживания ошибок.

Kreshnik
источник