È necessario gestire l'eccezione non rilevata e inviare il file di registro


114

AGGIORNAMENTO: vedere la soluzione "accettata" di seguito

Quando la mia app crea un'eccezione non gestita, invece di terminare semplicemente, vorrei prima dare all'utente l'opportunità di inviare un file di registro. Mi rendo conto che fare più lavoro dopo aver ottenuto un'eccezione casuale è rischioso ma, ehi, la cosa peggiore è che l'app finisce di bloccarsi e il file di registro non viene inviato. Questo si sta rivelando più complicato di quanto mi aspettassi :)

Cosa funziona: (1) intercettare l'eccezione non rilevata, (2) estrarre le informazioni di registro e scrivere su un file.

Cosa non funziona ancora: (3) avvio di un'attività per inviare e-mail. Infine, avrò ancora un'altra attività per chiedere il permesso dell'utente. Se riesco a far funzionare l'attività di posta elettronica, non mi aspetto molti problemi per l'altro.

Il nocciolo del problema è che l'eccezione non gestita viene catturata nella mia classe Application. Poiché questa non è un'attività, non è ovvio come avviare un'attività con Intent.ACTION_SEND. Cioè, normalmente per avviare un'attività si chiama startActivity e riprende con onActivityResult. Questi metodi sono supportati da Activity ma non da Application.

Qualche suggerimento su come fare questo?

Ecco alcuni ritagli di codice come guida iniziale:

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
Personalmente uso solo ACRA , anche se è open source, quindi puoi controllare come lo fanno ...
David O'Meara

Risposte:


240

Ecco la soluzione completa (quasi: ho omesso il layout dell'interfaccia utente e la gestione dei pulsanti), derivata da molte sperimentazioni e vari post di altri relativi a problemi emersi lungo il percorso.

Ci sono una serie di cose che devi fare:

  1. Gestisci uncaughtException nella tua sottoclasse Application.
  2. Dopo aver rilevato un'eccezione, avvia una nuova attività per chiedere all'utente di inviare un registro.
  3. Estrai le informazioni di registro dai file di logcat e scrivi nel tuo file.
  4. Avvia un'app di posta elettronica, fornendo il tuo file come allegato.
  5. Manifest: filtra la tua attività per essere riconosciuta dal gestore delle eccezioni.
  6. Facoltativamente, configurare Proguard per eliminare Log.d () e Log.v ().

Ora, ecco i dettagli:

(1 e 2) Gestisci uncaughtException, avvia l'invio dell'attività del registro:

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) Estrai il registro (lo metto nella mia attività 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) Avvia un'app di posta elettronica (anche nella mia attività 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 e 4) Ecco come appare SendLog (dovrai aggiungere l'interfaccia utente, però):

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

In project.properties, modifica la riga di configurazione. È necessario specificare "ottimizzare" o Proguard non rimuoverà le chiamate Log.v () e Log.d ().

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

In proguard-project.txt, aggiungi quanto segue. Questo dice a Proguard di presumere che Log.v e Log.d non abbiano effetti collaterali (anche se lo fanno dal momento che scrivono nei log) e quindi possono essere rimossi durante l'ottimizzazione:

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

Questo è tutto! Se hai suggerimenti per migliorare questo, faccelo sapere e potrei aggiornarlo.


10
Solo una nota: non chiamare mai System.exit. Interromperai la catena dei gestori di eccezioni non rilevate. Basta inoltrarlo al successivo. Hai già "defaultUncaughtHandler" da getDefaultUncaughtExceptionHandler, passaci la chiamata quando hai finito.
Gilm

2
@gilm, puoi fornire un esempio su come "inoltrarlo al prossimo" e cos'altro potrebbe accadere nella catena del gestore? È passato un po 'di tempo, ma ho testato una serie di scenari e chiamare System.exit () sembrava essere la soluzione migliore. Dopo tutto, l'app si è bloccata e deve essere terminata.
Peri Hartman

Penso di ricordare cosa succede se lasci che l'eccezione non rilevata continui: il sistema mette l'avviso "app terminata". Normalmente, andrebbe bene. Ma poiché la nuova attività (che invia e-mail con il log) sta già inserendo il proprio messaggio, avere il sistema ne ha un altro è fonte di confusione. Quindi lo costringo ad abortire silenziosamente con System.exit ().
Peri Hartman

8
@PeriHartman sicuro: c'è solo un gestore predefinito. prima di chiamare setDefaultUncaughtExceptionHandler (), è necessario chiamare getDefaultUncaughtExceptionHandler () e mantenere quel riferimento. Quindi, supponiamo che tu abbia bugsense, crashlytics e il tuo gestore sia l'ultimo installato, il sistema chiamerà solo il tuo. è compito tuo chiamare il riferimento che hai ricevuto tramite getDefaultUncaughtExceptionHandler () e passare il thread e lanciabile al successivo nella catena. se usi solo System.exit (), gli altri non verranno chiamati.
Gilm

4
Devi anche registrare la tua implementazione di Application nel manifest con un attributo nel tag Application, ad esempio: <application android: name = "com.mydomain.MyApplication" other attrs ... />
Matt

9

Oggi ci sono molti strumenti di riproduzione di crash che lo fanno facilmente.

  1. crashlytics - Uno strumento di segnalazione degli arresti anomali , gratuito ma che fornisce rapporti di base Vantaggi: gratuito

  2. Gryphonet - Uno strumento di reporting più avanzato, richiede una sorta di commissione. Vantaggi: facile ricreazione di arresti anomali, ANR, lentezza ...

Se sei uno sviluppatore privato suggerirei Crashlytics, ma se si tratta di una grande organizzazione, sceglierei Gryphonet.

In bocca al lupo!


5

Prova invece a utilizzare ACRA: gestisce l'invio della traccia dello stack e di tonnellate di altre utili informazioni di debug al tuo backend o al documento di Google Documenti che hai impostato.

https://github.com/ACRA/acra


5

La risposta di @ PeriHartman funziona bene quando il thread dell'interfaccia utente genera un'eccezione non rilevata. Ho apportato alcuni miglioramenti per quando l'eccezione non rilevata viene generata da un thread non dell'interfaccia utente.

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

Ben spiegato. Ma un'osservazione qui, invece di scrivere nel file utilizzando File Writer e Streaming, ho utilizzato direttamente l'opzione logcat -f. Ecco il codice

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

Questo mi ha aiutato a scaricare le ultime informazioni sul buffer. L'utilizzo dello streaming di file mi ha dato un problema che non scaricava gli ultimi registri dal buffer. Ma comunque, questa è stata una guida davvero utile. Grazie.


Credo di averlo provato (o qualcosa di simile). Se ricordo bene, ho riscontrato problemi di autorizzazione. Lo stai facendo su un dispositivo rooted?
Peri Hartman

Oh, mi dispiace se ti ho confuso, ma finora ci sto provando con i miei emulatori. Devo ancora portarlo su un dispositivo (ma sì, il dispositivo che uso per i test è rootato).
schow

2

Gestione delle eccezioni non rilevate : come spiegato da @gilm, fai questo, (kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

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

Spero che aiuti, ha funzionato per me .. (: y). Nel mio caso ho utilizzato la libreria "com.microsoft.appcenter.crashes.Crashes" per il monitoraggio degli errori.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.