Precisa lidar com exceção não detectada e enviar arquivo de log

114

ATUALIZAÇÃO: consulte a solução "aceita" abaixo

Quando meu aplicativo cria uma exceção não tratada, em vez de simplesmente encerrar, gostaria primeiro de dar ao usuário a oportunidade de enviar um arquivo de log. Percebo que fazer mais trabalho depois de obter uma exceção aleatória é arriscado, mas, ei, o pior é que o aplicativo pára de funcionar e o arquivo de log não é enviado. Isso está se tornando mais complicado do que eu esperava :)

O que funciona: (1) capturar a exceção não detectada, (2) extrair informações de log e gravar em um arquivo.

O que ainda não funciona: (3) iniciar uma atividade para enviar email. No final das contas, terei mais uma atividade para pedir a permissão do usuário. Se eu fizer com que a atividade de e-mail funcione, não espero muitos problemas para o outro.

O ponto crucial do problema é que a exceção não tratada é capturada na minha classe Application. Já que não é uma atividade, não é óbvio como iniciar uma atividade com Intent.ACTION_SEND. Ou seja, normalmente, para iniciar uma atividade, chama-se startActivity e continua com onActivityResult. Esses métodos são suportados por Activity, mas não por Application.

Alguma sugestão de como fazer isso?

Aqui estão alguns recortes de código como um guia 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);
  }
}
Peri Hartman
fonte
4
Pessoalmente, eu só uso o ACRA , embora seja de código aberto, então você pode verificar como eles fazem isso ...
David O'Meara

Respostas:

240

Aqui está a solução completa (quase: omiti o layout da IU e o manuseio dos botões) - derivada de muitos experimentos e vários posts de outros relacionados a problemas que surgiram ao longo do caminho.

Existem várias coisas que você precisa fazer:

  1. Manipule uncaughtException em sua subclasse de aplicativo.
  2. Depois de capturar uma exceção, inicie uma nova atividade para pedir ao usuário para enviar um log.
  3. Extraia as informações de log dos arquivos do logcat e grave em seu próprio arquivo.
  4. Inicie um aplicativo de e-mail, fornecendo seu arquivo como um anexo.
  5. Manifesto: filtre sua atividade para ser reconhecida por seu manipulador de exceções.
  6. Opcionalmente, configure o Proguard para remover Log.d () e Log.v ().

Agora, aqui estão os detalhes:

(1 e 2) Manipular uncaughtException, iniciar enviar atividade 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) Extrair log (coloquei isso em minha atividade 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 um aplicativo de e-mail (também em minha atividade 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 e 4) Esta é a aparência do SendLog (no entanto, você terá que adicionar a IU):

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

<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) Programa de configuração:

Em project.properties, altere a linha de configuração. Você deve especificar "otimizar" ou o Proguard não removerá as chamadas Log.v () e Log.d ().

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

Em proguard-project.txt, adicione o seguinte. Isso diz ao Proguard para assumir que Log.v e Log.d não têm efeitos colaterais (embora tenham, já que gravam nos logs) e, portanto, podem ser removidos durante a otimização:

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

É isso aí! Se você tiver alguma sugestão de melhorias para isso, por favor me avise e eu posso atualizar isto.

Peri Hartman
fonte
10
Apenas uma observação: nunca chame System.exit. Você quebrará a cadeia de manipuladores de exceções não detectadas. Apenas encaminhe para o próximo. Você já tem "defaultUncaughtHandler" de getDefaultUncaughtExceptionHandler, basta passar a chamada para ele quando terminar.
gilm
2
@gilm, você pode fornecer um exemplo de como "encaminhar para o próximo" e o que mais pode acontecer na cadeia de manipuladores? Já faz um tempo, mas testei vários cenários e chamar System.exit () parecia ser a melhor solução. Afinal, o aplicativo travou e precisa ser encerrado.
Peri Hartman
Acho que me lembro do que acontece se você deixar a exceção não detectada continuar: o sistema exibe o aviso de "aplicativo encerrado". Normalmente, isso seria bom. Mas como a nova atividade (que envia e-mail com o log) já está colocando sua própria mensagem, deixar o sistema colocando outra é confuso. Portanto, forço a interrupção silenciosa com System.exit ().
Peri Hartman,
8
@PeriHartman com certeza: há apenas um manipulador padrão. antes de chamar setDefaultUncaughtExceptionHandler (), você deve chamar getDefaultUncaughtExceptionHandler () e manter essa referência. Então, digamos que você tenha o bugsense, crashlytics e seu manipulador seja o último instalado, o sistema chamará apenas o seu. é sua tarefa chamar a referência que você recebeu por meio de getDefaultUncaughtExceptionHandler () e passar o thread e lançar para o próximo na cadeia. se você apenas System.exit (), os outros não serão chamados.
gilm
4
Você também precisa registrar sua implementação de Application no manifesto com um atributo na tag de Application, por exemplo: <application android: name = "com.mydomain.MyApplication" other attrs ... />
Matt
9

Hoje, existem muitas ferramentas de reprting de falha que fazem isso facilmente.

  1. crashlytics - Uma ferramenta de relatório de falhas, gratuita, mas oferece relatórios básicos Vantagens: Grátis

  2. Gryphonet - Uma ferramenta de relatório mais avançada, requer algum tipo de taxa. Vantagens: Fácil recriação de travamentos, ANR's, lentidão ...

Se você for um desenvolvedor privado, eu sugeriria o Crashlytics, mas se for uma grande organização, eu escolheria a Gryphonet.

Boa sorte!

Ariel Bell
fonte
5

Tente usar o ACRA - ele envia o rastreamento da pilha, bem como toneladas de outras informações úteis de depuração, para o seu back-end ou para o documento do Google Docs que você configurou.

https://github.com/ACRA/acra

Martin Konecny
fonte
5

A resposta de @PeriHartman funciona bem quando o thread de interface do usuário lança uma exceção não capturada. Fiz algumas melhorias para quando a exceção não capturada é lançada por um thread não IU.

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

Bem explicado. Mas uma observação aqui, em vez de gravar em arquivo usando File Writer and Streaming, usei a opção logcat -f diretamente. Aqui está o código

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

Isso me ajudou a liberar as informações mais recentes do buffer. Usar o streaming de arquivo me deu um problema: não estava liberando os logs mais recentes do buffer. Mas de qualquer maneira, este guia foi realmente útil. Obrigado.

Schow
fonte
Acho que tentei isso (ou algo semelhante). Se bem me lembro, tive problemas de permissão. Você está fazendo isso em um dispositivo com acesso root?
Peri Hartman,
Oh, desculpe se eu confundi você, mas até agora estou tentando com meus emuladores. Ainda estou para transferi-lo para um dispositivo (mas sim, o dispositivo que uso para teste é um enraizado).
Schow de
2

Lidando com exceções não capturadas: como @gilm explicou, faça isso, (kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

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

Espero que ajude, funcionou para mim .. (: y). No meu caso, usei a biblioteca 'com.microsoft.appcenter.crashes.Crashes' para rastreamento de erros.

Kreshnik
fonte
0

Você pode lidar com as exceções não detectadas usando a biblioteca FireCrasher e fazer uma recuperação a partir dela.

você pode saber mais sobre a biblioteca neste artigo médio

Osama Raddad
fonte