Sie müssen nicht erfasste Ausnahmen behandeln und Protokolldateien senden

114

UPDATE: Bitte sehen Sie unten die "akzeptierte" Lösung

Wenn meine App eine nicht behandelte Ausnahme erstellt, anstatt sie einfach zu beenden, möchte ich dem Benutzer zunächst die Möglichkeit geben, eine Protokolldatei zu senden. Mir ist klar, dass es riskant ist, nach einer zufälligen Ausnahme mehr Arbeit zu erledigen, aber das Schlimmste ist, dass die App abstürzt und die Protokolldatei nicht gesendet wird. Das wird schwieriger als ich erwartet hatte :)

Was funktioniert: (1) Abfangen der nicht erfassten Ausnahme, (2) Extrahieren von Protokollinformationen und Schreiben in eine Datei.

Was noch nicht funktioniert: (3) Starten einer Aktivität zum Senden von E-Mails. Letztendlich werde ich noch eine weitere Aktivität haben, um den Benutzer um Erlaubnis zu bitten. Wenn die E-Mail-Aktivität funktioniert, erwarte ich für den anderen keine großen Probleme.

Der Kern des Problems besteht darin, dass die nicht behandelte Ausnahme in meiner Anwendungsklasse abgefangen wird. Da dies keine Aktivität ist, ist es nicht offensichtlich, wie eine Aktivität mit Intent.ACTION_SEND gestartet werden soll. Das heißt, normalerweise wird zum Starten einer Aktivität startActivity aufgerufen und mit onActivityResult fortgesetzt. Diese Methoden werden von Activity, jedoch nicht von Application unterstützt.

Irgendwelche Vorschläge dazu?

Hier sind einige Code-Schnipsel als Starthilfe:

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
quelle
4
Persönlich benutze ich nur ACRA , obwohl es Open Source ist, so dass Sie überprüfen können, wie sie es tun ...
David O'Meara

Antworten:

240

Hier ist die vollständige Lösung (fast: Ich habe das Layout der Benutzeroberfläche und die Handhabung der Schaltflächen weggelassen) - abgeleitet aus vielen Experimenten und verschiedenen Beiträgen von anderen, die sich auf Probleme bezogen, die auf dem Weg auftauchten.

Es gibt eine Reihe von Dingen, die Sie tun müssen:

  1. Behandeln Sie uncaughtException in Ihrer Application-Unterklasse.
  2. Starten Sie nach dem Abfangen einer Ausnahme eine neue Aktivität, um den Benutzer zum Senden eines Protokolls aufzufordern.
  3. Extrahieren Sie die Protokollinformationen aus den logcat-Dateien und schreiben Sie in Ihre eigene Datei.
  4. Starten Sie eine E-Mail-App und stellen Sie Ihre Datei als Anhang bereit.
  5. Manifest: Filtern Sie Ihre Aktivität, um von Ihrem Ausnahmebehandler erkannt zu werden.
  6. Richten Sie Proguard optional so ein, dass Log.d () und Log.v () entfernt werden.

Hier sind die Details:

(1 & 2) Behandeln Sie uncaughtException und starten Sie das Senden der Protokollaktivität:

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) Protokoll extrahieren (ich habe dies in meine SendLog-Aktivität eingefügt):

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) Starten Sie eine E-Mail-App (auch in meiner SendLog-Aktivität):

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) So sieht SendLog aus (Sie müssen jedoch die Benutzeroberfläche hinzufügen):

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

Ändern Sie in project.properties die Konfigurationszeile. Sie müssen angeben , „optimieren“ oder Proguard wird nicht entfernen Log.v () und Log.d () Anrufe.

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

Fügen Sie in proguard-project.txt Folgendes hinzu. Dies weist Proguard an, anzunehmen, dass Log.v und Log.d keine Nebenwirkungen haben (obwohl dies der Fall ist, da sie in die Protokolle schreiben) und daher während der Optimierung entfernt werden können:

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

Das ist es! Wenn Sie Verbesserungsvorschläge haben, lassen Sie es mich bitte wissen und ich kann dies aktualisieren.

Peri Hartman
quelle
10
Nur ein Hinweis: Rufen Sie niemals System.exit auf. Sie brechen die Kette der nicht erfassten Ausnahmebehandlungsroutinen. Leiten Sie es einfach zum nächsten weiter. Sie haben bereits "defaultUncaughtHandler" von getDefaultUncaughtExceptionHandler. Leiten Sie den Aufruf einfach weiter, wenn Sie fertig sind.
Gilm
2
@gilm, können Sie ein Beispiel dafür geben, wie Sie es an den nächsten weiterleiten und was sonst noch in der Handler-Kette passieren könnte? Es ist schon eine Weile her, aber ich habe eine Reihe von Szenarien getestet, und das Aufrufen von System.exit () schien die beste Lösung zu sein. Immerhin ist die App abgestürzt und muss beendet werden.
Peri Hartman
Ich denke, ich erinnere mich, was passiert, wenn Sie die nicht erfasste Ausnahme fortsetzen lassen: Das System gibt die Meldung "App beendet" aus. Normalerweise wäre das in Ordnung. Da die neue Aktivität (die E-Mails mit dem Protokoll sendet) bereits eine eigene Nachricht erstellt, ist es verwirrend, wenn das System eine andere Nachricht erstellt. Also zwinge ich es, mit System.exit () leise abzubrechen.
Peri Hartman
8
@PeriHartman sicher: Es gibt nur einen Standard-Handler. Bevor Sie setDefaultUncaughtExceptionHandler () aufrufen, müssen Sie getDefaultUncaughtExceptionHandler () aufrufen und diese Referenz beibehalten. Angenommen, Sie haben Bugsense, Crashlytics und Ihr Handler ist der zuletzt installierte. Das System ruft nur Ihren an. Es ist Ihre Aufgabe, die Referenz, die Sie über getDefaultUncaughtExceptionHandler () erhalten haben, aufzurufen und den Thread zu übergeben und an den nächsten in der Kette zu werfen. Wenn Sie nur System.exit () verwenden, werden die anderen nicht aufgerufen.
Gilm
4
Sie müssen auch Ihre Implementierung von Application im Manifest mit einem Attribut im Application-Tag registrieren, z. B.: <Application android: name = "com.mydomain.MyApplication" other attrs ... />
Matt
9

Heutzutage gibt es viele Crash-Reprting-Tools, die dies problemlos tun.

  1. Crashlytics - Ein kostenloses Crash-Reporting-Tool, das Ihnen grundlegende Berichte bietet. Vorteile: Kostenlos

  2. Gryphonet - Ein erweitertes Berichterstellungstool, für das eine Gebühr erforderlich ist. Vorteile: Einfache Wiederherstellung von Abstürzen, ANRs, Langsamkeit ...

Wenn Sie ein privater Entwickler sind, würde ich Crashlytics vorschlagen, aber wenn es eine große Organisation ist, würde ich mich für Gryphonet entscheiden.

Viel Glück!

Ariel Bell
quelle
5

Versuchen Sie stattdessen, ACRA zu verwenden. Hiermit werden der Stack-Trace sowie unzählige andere nützliche Debug-Informationen an Ihr Backend oder an das von Ihnen eingerichtete Google Text & Tabellen-Dokument gesendet.

https://github.com/ACRA/acra

Martin Konecny
quelle
5

Die Antwort von @ PeriHartman funktioniert gut, wenn der UI-Thread eine nicht erfasste Ausnahme auslöst. Ich habe einige Verbesserungen vorgenommen, wenn die nicht erfasste Ausnahme von einem Nicht-UI-Thread ausgelöst wird.

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
quelle
2

Schön erklärt. Aber eine Beobachtung hier, anstatt mit File Writer und Streaming in eine Datei zu schreiben, habe ich die Option logcat -f direkt verwendet. Hier ist der Code

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

Dies hat mir geholfen, die neuesten Pufferinformationen zu löschen. Die Verwendung von Datei-Streaming gab mir ein Problem, dass die neuesten Protokolle nicht aus dem Puffer gelöscht wurden. Trotzdem war dies eine sehr hilfreiche Anleitung. Danke dir.

schow
quelle
Ich glaube, ich habe das versucht (oder etwas Ähnliches). Wenn ich mich erinnere, stieß ich auf Berechtigungsprobleme. Tun Sie dies auf einem gerooteten Gerät?
Peri Hartman
Oh, es tut mir leid, wenn ich dich verwirrt habe, aber ich versuche es bisher mit meinen Emulatoren. Ich muss es noch auf ein Gerät portieren (aber ja, das Gerät, das ich zum Testen verwende, ist ein Root-Gerät).
schow
2

Umgang mit nicht erfassten Ausnahmen: Wie @gilm erklärt hat, tun Sie dies einfach (kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

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

Ich hoffe es hilft, es hat bei mir funktioniert .. (: y). In meinem Fall habe ich die Bibliothek 'com.microsoft.appcenter.crashes.Crashes' für die Fehlerverfolgung verwendet.

Kreshnik
quelle