Necesidad de manejar la excepción no detectada y enviar el archivo de registro
Cuando mi aplicación crea una excepción no controlada, en lugar de simplemente finalizar, primero me gustaría darle al usuario la oportunidad de enviar un archivo de registro. Me doy cuenta de que trabajar más después de recibir una excepción aleatoria es arriesgado pero, bueno, lo peor es que la aplicación termina fallando y el archivo de registro no se envía. Esto está resultando más complicado de lo que esperaba :)
Qué funciona: (1) atrapar la excepción no detectada, (2) extraer información de registro y escribir en un archivo.
Lo que aún 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 logro 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 está detectada en mi clase de Aplicación. Como 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 startActivity y se reanuda con onActivityResult. Estos métodos son compatibles con la Actividad pero no con la Aplicación.
¿Alguna sugerencia sobre cómo hacer esto?
A continuación se muestran algunos fragmentos de código como guía inicial:
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);
}
}
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 debes hacer:
- Maneje una excepción no detectada en su subclase de Aplicación.
- Después de detectar una excepción, inicie una nueva actividad para pedirle al usuario que envíe un registro.
- Extraiga la información de registro de los archivos de logcat y escríbala en su propio archivo.
- Inicie una aplicación de correo electrónico y proporcione su archivo como archivo adjunto.
- Manifiesto: filtra tu actividad para que tu controlador de excepciones la reconozca.
- Opcionalmente, configure Proguard para eliminar
Log.d()
yLog.v()
.
Ahora, aquí están los detalles:
(1 y 2) Manejar una excepción no detectada, iniciar la actividad de envío de 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) Extraer registro (pongo 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 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ás 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á Log.v()
ni Log.d()
llamará.
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 Log.v
y Log.d
no tenga efectos secundarios (aunque los tenga, ya que escriben en los registros) y, por lo tanto, se puede eliminar durante la optimización:
-assumenosideeffects class android.util.Log {
public static int v(...);
public static int d(...);
}
¡Eso es todo! Si tiene alguna sugerencia para mejorar esto, hágamelo saber y puedo actualizarlo.
Hoy en día existen muchas herramientas de informes de fallos que hacen esto fácilmente.
crashlytics : una herramienta de informes de fallos, gratuita pero que ofrece informes básicos Ventajas: Gratis
Gryphonet : una herramienta de informes más avanzada que requiere algún tipo de tarifa. Ventajas: Fácil recreación de fallos, ANR, lentitud...
Si eres un desarrollador privado, te sugeriría Crashlytics, pero si es una organización grande, elegiría Gryphonet.
¡Buena suerte!
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
}