Besoin de gérer les exceptions non interceptées et d'envoyer un fichier journal

114

MISE À JOUR: veuillez consulter la solution «acceptée» ci-dessous

Lorsque mon application crée une exception non gérée, plutôt que de simplement se terminer, je voudrais d'abord donner à l'utilisateur la possibilité d'envoyer un fichier journal. Je me rends compte qu'il est risqué de faire plus de travail après avoir obtenu une exception aléatoire, mais le pire est que l'application se bloque et que le fichier journal n'est pas envoyé. Cela s'avère plus compliqué que prévu :)

Ce qui fonctionne: (1) intercepter l'exception non interceptée, (2) extraire les informations du journal et écrire dans un fichier.

Ce qui ne fonctionne pas encore: (3) démarrer une activité pour envoyer des e-mails. En fin de compte, j'aurai encore une autre activité pour demander la permission de l'utilisateur. Si je fais fonctionner l'activité de messagerie, je ne m'attends pas à beaucoup de problèmes pour l'autre.

Le nœud du problème est que l'exception non gérée est interceptée dans ma classe Application. Étant donné qu'il ne s'agit pas d'une activité, il n'est pas évident de savoir comment démarrer une activité avec Intent.ACTION_SEND. Autrement dit, normalement pour démarrer une activité, on appelle startActivity et reprend avec onActivityResult. Ces méthodes sont prises en charge par Activity mais pas par Application.

Des suggestions sur la façon de procéder?

Voici quelques extraits de code comme guide de démarrage:

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
la source
4
Personnellement, j'utilise juste ACRA , bien qu'il soit open source afin que vous puissiez vérifier comment ils le font ...
David O'Meara

Réponses:

240

Voici la solution complète (presque: j'ai omis la disposition de l'interface utilisateur et la gestion des boutons) - dérivée de beaucoup d'expérimentation et de divers messages d'autres personnes liées à des problèmes survenus en cours de route.

Vous devez faire un certain nombre de choses:

  1. Gérez uncaughtException dans votre sous-classe Application.
  2. Après avoir détecté une exception, démarrez une nouvelle activité pour demander à l'utilisateur d'envoyer un journal.
  3. Extrayez les informations du journal des fichiers de logcat et écrivez dans votre propre fichier.
  4. Démarrez une application de messagerie, en fournissant votre fichier en pièce jointe.
  5. Manifest: filtrez votre activité pour qu'elle soit reconnue par votre gestionnaire d'exceptions.
  6. En option, configurez Proguard pour supprimer Log.d () et Log.v ().

Maintenant, voici les détails:

(1 & 2) Gérez uncaughtException, lancez l'activité du journal d'envoi:

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) Extraire le journal (je mets ceci dans mon activité 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) Démarrez une application de messagerie (également dans mon activité 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 & 4) Voici à quoi ressemble SendLog (vous devrez cependant ajouter l'interface utilisateur):

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) Manifeste:

<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) Programme d'installation:

Dans project.properties, modifiez la ligne de configuration. Vous devez spécifier «optimiser» ou Proguard ne supprimera pas les appels Log.v () et Log.d ().

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

Dans proguard-project.txt, ajoutez ce qui suit. Cela indique à Proguard de supposer que Log.v et Log.d n'ont aucun effet secondaire (même s'ils le font puisqu'ils écrivent dans les journaux) et peuvent donc être supprimés pendant l'optimisation:

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

C'est tout! Si vous avez des suggestions pour améliorer ceci, faites-le moi savoir et je pourrais mettre à jour ceci.

Peri Hartman
la source
10
Juste une note: n'appelez jamais System.exit. Vous briserez la chaîne des gestionnaires d'exceptions non interceptés. Envoyez-le simplement au suivant. Vous avez déjà "defaultUncaughtHandler" de getDefaultUncaughtExceptionHandler, passez-lui simplement l'appel lorsque vous avez terminé.
gilm
2
@gilm, pouvez-vous donner un exemple sur la façon de "le transmettre au suivant" et que pourrait-il se passer d'autre dans la chaîne du gestionnaire? Cela fait un moment, mais j'ai testé un certain nombre de scénarios, et appeler System.exit () m'a semblé être la meilleure solution. Après tout, l'application est tombée en panne et doit s'arrêter.
Peri Hartman
Je pense que je me souviens de ce qui se passe si vous laissez l'exception non interceptée se poursuivre: le système affiche la notification «application terminée». Normalement, ce serait bien. Mais comme la nouvelle activité (qui envoie un e-mail avec le journal) met déjà en place son propre message, le fait d'en avoir un autre par le système est déroutant. Je le force donc à abandonner tranquillement avec System.exit ().
Peri Hartman
8
@PeriHartman bien sûr: il n'y a qu'un seul gestionnaire par défaut. avant d'appeler setDefaultUncaughtExceptionHandler (), vous devez appeler getDefaultUncaughtExceptionHandler () et conserver cette référence. Donc, disons que vous avez bugsense, crashlytics et que votre gestionnaire est le dernier installé, le système n'appellera que le vôtre. c'est votre tâche d'appeler la référence que vous avez reçue via getDefaultUncaughtExceptionHandler () et de passer le thread et le jetable au suivant dans la chaîne. si vous venez de System.exit (), les autres ne seront pas appelés.
gilm
4
Vous devez également enregistrer votre implémentation d'Application dans le manifeste avec un attribut dans la balise Application, par exemple: <application android: name = "com.mydomain.MyApplication" other attrs ... />
Matt
9

Aujourd'hui, il existe de nombreux outils de reprise après incident qui le font facilement.

  1. crashlytics - Un outil de rapport de plantage, gratuit mais qui vous donne des rapports de base Avantages: gratuit

  2. Gryphonet - Un outil de reporting plus avancé, nécessite une sorte de frais. Avantages: Recréation facile des accidents, des ANR, de la lenteur ...

Si vous êtes un développeur privé, je suggérerais Crashlytics, mais si c'est une grande organisation, je choisirais Gryphonet.

Bonne chance!

Ariel Bell
la source
5

Essayez plutôt d'utiliser ACRA - il gère l'envoi de la trace de la pile ainsi que de tonnes d'autres informations de débogage utiles à votre backend ou au document Google Docs que vous avez configuré.

https://github.com/ACRA/acra

Martin Konecny
la source
5

La réponse de @ PeriHartman fonctionne bien lorsque le thread d'interface utilisateur lance une exception non interceptée. J'ai apporté quelques améliorations lorsque l'exception non interceptée est levée par un thread non UI.

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
la source
2

Bien expliqué. Mais une observation ici, au lieu d'écrire dans un fichier en utilisant File Writer et Streaming, j'ai utilisé directement l'option logcat -f. Voici le 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();
        }

Cela m'a aidé à vider les dernières informations de tampon. L'utilisation de la diffusion en continu de fichiers m'a donné un problème: il ne vidait pas les derniers journaux de la mémoire tampon. Mais de toute façon, c'était un guide vraiment utile. Je vous remercie.

schow
la source
Je crois que j'ai essayé ça (ou quelque chose de similaire). Si je me souviens bien, j'ai rencontré des problèmes de permission. Faites-vous cela sur un appareil enraciné?
Peri Hartman
Oh, je suis désolé si je vous ai confondu, mais j'essaye jusqu'à présent avec mes émulateurs. Je ne l'ai pas encore porté sur un périphérique (mais oui, le périphérique que j'utilise pour les tests est enraciné).
schow
2

Gérer les exceptions non interceptées : comme @gilm l'a expliqué, faites simplement ceci, (kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

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

J'espère que cela aide, cela a fonctionné pour moi .. (: y). Dans mon cas, j'ai utilisé la bibliothèque 'com.microsoft.appcenter.crashes.Crashes' pour le suivi des erreurs.

Kreshnik
la source