Necesita manejar una excepción no detectada y enviar un archivo de registro

114

ACTUALIZACIÓN: consulte la solución "aceptada" a continuación

Cuando mi aplicación crea una excepción no controlada, en lugar de simplemente terminar, primero me gustaría darle al usuario la oportunidad de enviar un archivo de registro. Me doy cuenta de que hacer más trabajo después de obtener una excepción aleatoria es arriesgado pero, oye, lo peor es que la aplicación termina fallando y el archivo de registro no se envía. Esto está resultando ser más complicado de lo que esperaba :)

Qué funciona: (1) capturando la excepción no detectada, (2) extrayendo información de registro y escribiendo en un archivo.

Lo que todavía no funciona: (3) iniciar una actividad para enviar correo electrónico. En última instancia, tendré otra actividad más para pedir permiso al usuario. Si consigo que la actividad del correo electrónico funcione, no espero muchos problemas para el otro.

El quid del problema es que la excepción no controlada se detecta en mi clase de aplicación. Dado que no es una actividad, no es obvio cómo iniciar una actividad con Intent.ACTION_SEND. Es decir, normalmente para iniciar una actividad, se llama a startActivity y se reanuda con onActivityResult. Estos métodos son compatibles con Activity pero no con Application.

¿Alguna sugerencia sobre cómo hacer esto?

Aquí hay algunos recortes de código como guía de inicio:

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
fuente
4
Personalmente solo uso ACRA , aunque es de código abierto, por lo que puedes comprobar cómo lo hacen ...
David O'Meara

Respuestas:

240

Aquí está la solución completa (casi: omití el diseño de la interfaz de usuario y el manejo de los botones), derivada de mucha experimentación y varias publicaciones de otros relacionados con problemas que surgieron en el camino.

Hay una serie de cosas que debe hacer:

  1. Maneje uncaughtException en su subclase de aplicación.
  2. Después de detectar una excepción, inicie una nueva actividad para pedirle al usuario que envíe un registro.
  3. Extraiga la información de registro de los archivos de logcat y escríbala en su propio archivo.
  4. Inicie una aplicación de correo electrónico, proporcionando su archivo como archivo adjunto.
  5. Manifiesto: filtre su actividad para que sea reconocida por su manejador de excepciones.
  6. Opcionalmente, configure Proguard para eliminar Log.d () y Log.v ().

Ahora, aquí están los detalles:

(1 y 2) Manejar uncaughtException, iniciar actividad de registro de envío:

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) Extraer registro (puse esto en mi actividad 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) Inicie una aplicación de correo electrónico (también en mi Actividad de 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 y 4) Así es como se ve SendLog (aunque tendrá que agregar la interfaz de usuario):

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

<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) Configuración de Proguard:

En project.properties, cambie la línea de configuración. Debe especificar "optimizar" o Proguard no eliminará las llamadas Log.v () y Log.d ().

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

En proguard-project.txt, agregue lo siguiente. Esto le dice a Proguard que asuma que Log.v y Log.d no tienen efectos secundarios (aunque los tienen desde que escriben en los registros) y, por lo tanto, se pueden eliminar durante la optimización:

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

¡Eso es! Si tiene alguna sugerencia para mejorar esto, hágamelo saber y puedo actualizarlo.

Peri Hartman
fuente
10
Solo una nota: nunca llame a System.exit. Romperá la cadena de controladores de excepciones no detectados. Simplemente reenvíalo al siguiente. Ya tienes "defaultUncaughtHandler" de getDefaultUncaughtExceptionHandler, simplemente pásale la llamada cuando hayas terminado.
Gilm
2
@gilm, ¿puede proporcionar un ejemplo sobre cómo "reenviarlo al siguiente" y qué más podría suceder en la cadena del controlador? Ha pasado un tiempo, pero probé varios escenarios y llamar a System.exit () parecía ser la mejor solución. Después de todo, la aplicación se ha bloqueado y debe cerrarse.
Peri Hartman
Creo recordar lo que sucede si deja que continúe la excepción no detectada: el sistema muestra el aviso de "aplicación terminada". Normalmente, eso estaría bien. Pero dado que la nueva actividad (que envía correo electrónico con el registro) ya está poniendo su propio mensaje, hacer que el sistema coloque otro es confuso. Así que lo obligo a abortar silenciosamente con System.exit ().
Peri Hartman
8
@PeriHartman seguro: solo hay un controlador predeterminado. antes de llamar a setDefaultUncaughtExceptionHandler (), debe llamar a getDefaultUncaughtExceptionHandler () y mantener esa referencia. Entonces, digamos que tiene bugsense, crashlytics y su controlador es el último instalado, el sistema llamará solo al suyo. es su tarea llamar a la referencia que recibió a través de getDefaultUncaughtExceptionHandler () y pasar el hilo y lanzarlo al siguiente en la cadena. si solo usa System.exit (), los demás no serán llamados.
Gilm
4
También debe registrar su implementación de Aplicación en el manifiesto con un atributo en la etiqueta de Aplicación, por ejemplo: <aplicación android: name = "com.mydomain.MyApplication" otros atributos ... />
Matt
9

Hoy en día existen muchas herramientas de notificación de fallos que hacen esto fácilmente.

  1. crashlytics : una herramienta de informes de fallos , gratuita, pero que ofrece informes básicos Ventajas: Gratis

  2. Gryphonet : una herramienta de informes más avanzada que requiere algún tipo de tarifa. Ventajas: Fácil recreación de choques, ANR's, lentitud ...

Si eres un desarrollador privado, te sugeriría Crashlytics, pero si es una gran organización, elegiría Gryphonet.

¡Buena suerte!

Ariel Bell
fuente
5

En su lugar, intente usar ACRA: se encarga de enviar el seguimiento de la pila, así como toneladas de otra información de depuración útil a su backend o al documento de Google Docs que haya configurado.

https://github.com/ACRA/acra

Martin Konecny
fuente
5

La respuesta de @ PeriHartman funciona bien cuando el hilo de la interfaz de usuario arroja una excepción no detectada. Hice algunas mejoras para cuando la excepción no detectada es lanzada por un hilo que no es de 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
fuente
2

Bien explicado. Pero una observación aquí, en lugar de escribir en un archivo usando File Writer y Streaming, hice uso de la opción logcat -f directamente. Aqui esta el codigo

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

Esto me ayudó a vaciar la última información del búfer. El uso de la transmisión de archivos me dio un problema: no estaba limpiando los últimos registros del búfer. Pero de todos modos, esta fue una guía realmente útil. Gracias.

schow
fuente
Creo que intenté eso (o algo similar). Si mal no recuerdo, me encontré con problemas de permisos. ¿Estás haciendo esto en un dispositivo rooteado?
Peri Hartman
Oh, lo siento si te confundí, pero hasta ahora estoy intentando con mis emuladores. Todavía tengo que portarlo a un dispositivo (pero sí, el dispositivo que uso para probar es uno rooteado).
schow
2

Manejo de excepciones no detectadas : como explicó @gilm, simplemente haga esto, (kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

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

Espero que ayude, funcionó para mí .. (: y). En mi caso, he usado la biblioteca 'com.microsoft.appcenter.crashes.Crashes' para el seguimiento de errores.

Kreshnik
fuente