Musisz obsłużyć nieprzechwycony wyjątek i wysłać plik dziennika

114

AKTUALIZACJA: zobacz „zaakceptowane” rozwiązanie poniżej

Gdy moja aplikacja tworzy nieobsługiwany wyjątek, zamiast po prostu zakończyć działanie, chciałbym najpierw dać użytkownikowi możliwość wysłania pliku dziennika. Zdaję sobie sprawę, że robienie więcej pracy po uzyskaniu losowego wyjątku jest ryzykowne, ale hej, najgorsze jest to, że aplikacja kończy się zawieszać, a plik dziennika nie jest wysyłany. Okazuje się, że jest to trudniejsze niż się spodziewałem :)

Co działa: (1) przechwytywanie nieprzechwyconego wyjątku, (2) wyodrębnianie informacji dziennika i zapisywanie do pliku.

Co jeszcze nie działa: (3) rozpoczęcie działania polegającego na wysyłaniu wiadomości e-mail. Ostatecznie będę miał jeszcze jedną czynność, aby poprosić użytkownika o pozwolenie. Jeśli aktywność poczty e-mail działa, nie spodziewam się większych problemów dla drugiej osoby.

Sednem problemu jest to, że nieobsługiwany wyjątek jest przechwytywany w mojej klasie Application. Ponieważ to nie jest działanie, nie jest oczywiste, jak rozpocząć działanie z intencją.ACTION_SEND. Oznacza to, że zwykle w celu rozpoczęcia działania wywołuje się startActivity i wznawia za pomocą onActivityResult. Te metody są obsługiwane przez działanie, ale nie przez aplikację.

Jakieś sugestie, jak to zrobić?

Oto kilka wycinków kodu jako przewodnik początkowy:

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
źródło
4
Osobiście po prostu używam ACRA , chociaż jest to open source, więc możesz sprawdzić, jak to robią ...
David O'Meara

Odpowiedzi:

240

Oto kompletne rozwiązanie (prawie: pominąłem układ interfejsu użytkownika i obsługę przycisków) - pochodzące z wielu eksperymentów i różnych postów od innych osób związanych z problemami, które pojawiły się po drodze.

Jest kilka rzeczy, które musisz zrobić:

  1. Obsłuż wyjątek uncaughtException w podklasie aplikacji.
  2. Po złapaniu wyjątku rozpocznij nowe działanie, aby poprosić użytkownika o przesłanie dziennika.
  3. Wyodrębnij informacje dziennika z plików logcat i zapisz do własnego pliku.
  4. Uruchom aplikację e-mail, podając plik jako załącznik.
  5. Manifest: filtruj swoją aktywność, aby została rozpoznana przez moduł obsługi wyjątków.
  6. Opcjonalnie skonfiguruj Proguard, aby usunąć Log.d () i Log.v ().

A teraz szczegóły:

(1 i 2) Obsłuż wyjątek uncaughtException, rozpocznij działanie dziennika wysyłania:

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) Wyodrębnij dziennik (umieściłem to jako moje działanie 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) Uruchom aplikację e-mail (również w moim działaniu 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[] {"[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 i 4) Oto jak wygląda SendLog (musisz jednak dodać interfejs użytkownika):

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:

<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 konfiguracji:

W project.properties zmień wiersz konfiguracji. Musisz określić „optimize”, inaczej Proguard nie usunie wywołań Log.v () i Log.d ().

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

W proguard-project.txt dodaj następujący plik. To mówi Proguardowi, aby założył, że Log.v i Log.d nie mają żadnych skutków ubocznych (nawet jeśli robią to, ponieważ zapisują w dziennikach) i dlatego można je usunąć podczas optymalizacji:

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

Otóż ​​to! Jeśli masz jakieś sugestie dotyczące ulepszeń, daj mi znać, a mogę to zaktualizować.

Peri Hartman
źródło
10
Tylko uwaga: nigdy nie dzwoń do System.exit. Będziesz przerywał łańcuch nieprzechwyconych programów obsługi wyjątków. Po prostu przekaż to następnemu. Masz już "defaultUncaughtHandler" z getDefaultUncaughtExceptionHandler, po prostu przekaż do niego wywołanie, kiedy skończysz.
gilm
2
@gilm, czy możesz podać przykład, jak „przekazać to następnemu” i co jeszcze może się wydarzyć w łańcuchu obsługi? Minęło trochę czasu, ale przetestowałem wiele scenariuszy i wywołanie System.exit () wydawało się najlepszym rozwiązaniem. W końcu aplikacja uległa awarii i musi się zakończyć.
Peri Hartman
Myślę, że pamiętam, co się stanie, jeśli pozwolisz na kontynuację niepochwyconego wyjątku: system wyświetli powiadomienie o zakończeniu działania aplikacji. Normalnie byłoby dobrze. Ale ponieważ nowa aktywność (która wysyła e-maile z dziennikiem) już tworzy własną wiadomość, umieszczenie w systemie kolejnej jest mylące. Dlatego wymuszam ciche przerwanie pracy za pomocą System.exit ().
Peri Hartman
8
@PeriHartman jasne: jest tylko jeden domyślny program obsługi. przed wywołaniem setDefaultUncaughtExceptionHandler () należy wywołać metodę getDefaultUncaughtExceptionHandler () i zachować to odwołanie. Powiedzmy, że masz bugsense, crashlytics, a program obsługi jest zainstalowany jako ostatni, a system zadzwoni tylko do ciebie. Twoim zadaniem jest wywołanie odwołania otrzymanego za pośrednictwem metody getDefaultUncaughtExceptionHandler () i przekazanie wątku oraz przekazanie do następnego w łańcuchu. jeśli użyjesz tylko System.exit (), inne nie zostaną wywołane.
gilm
4
Musisz również zarejestrować swoją implementację aplikacji w manifeście z atrybutem w tagu Application, np .: <application android: name = "com.mydomain.MyApplication" inne atrybuty ... />
Matt
9

Obecnie istnieje wiele narzędzi do odtwarzania awaryjnego, które robią to łatwo.

  1. crashlytics - narzędzie do zgłaszania awarii, bezpłatne, ale zapewnia podstawowe raporty. Zalety: Bezpłatne

  2. Gryphonet - bardziej zaawansowane narzędzie do raportowania, wymaga pewnej opłaty. Zalety: łatwe odtwarzanie awarii, błędów ANR, spowolnienia ...

Jeśli jesteś prywatnym programistą, zasugerowałbym Crashlytics, ale jeśli jest to duża organizacja, wybrałbym Gryphonet.

Powodzenia!

Ariel Bell
źródło
5

Spróbuj zamiast tego użyć ACRA - obsługuje ona wysyłanie śladu stosu, a także wielu innych przydatnych informacji debugowania do zaplecza lub do skonfigurowanego dokumentu Dokumentów Google.

https://github.com/ACRA/acra

Martin Konecny
źródło
5

Odpowiedź @ PeriHartman działa dobrze, gdy wątek interfejsu użytkownika zgłasza nieprzechwycony wyjątek. Wprowadziłem pewne ulepszenia, gdy nieprzechwycony wyjątek jest generowany przez wątek spoza interfejsu użytkownika.

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
źródło
2

Ładnie wyjaśnione. Ale jedna uwaga tutaj, zamiast pisać do pliku za pomocą programu File Writer i Streaming, skorzystałem bezpośrednio z opcji logcat -f. Oto kod

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

Pomogło mi to w wyczyszczeniu najnowszych informacji o buforze. Korzystanie z przesyłania strumieniowego plików spowodowało, że jeden problem polegał na tym, że nie było to opróżnianie najnowszych dzienników z bufora. W każdym razie był to bardzo pomocny przewodnik. Dziękuję Ci.

schowek
źródło
Myślę, że próbowałem tego (lub czegoś podobnego). Jeśli dobrze pamiętam, napotkałem problemy z pozwoleniami. Czy robisz to na zrootowanym urządzeniu?
Peri Hartman
Och, przepraszam, jeśli cię pomyliłem, ale jak dotąd próbuję z moimi emulatorami. Nie mogę jeszcze przenieść go na urządzenie (ale tak, urządzenie, którego używam do testów, jest zrootowane).
wystawa
2

Obsługa nieprzechwyconych wyjątków: jak wyjaśnił @gilm, po prostu zrób to, (kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

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

Mam nadzieję, że to pomogło, zadziałało dla mnie ... (: y). W moim przypadku użyłem biblioteki „com.microsoft.appcenter.crashes.Crashes” do śledzenia błędów.

Kreshnik
źródło