mobile/android/base/CrashReporter.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: Java; tab-width: 20; indent-tabs-mode: nil; -*-
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 package org.mozilla.gecko;
     8 import java.util.HashMap;
     9 import java.util.Map;
    10 import java.io.BufferedReader;
    11 import java.io.File;
    12 import java.io.FileInputStream;
    13 import java.io.FileOutputStream;
    14 import java.io.FileReader;
    15 import java.io.InputStreamReader;
    16 import java.io.IOException;
    17 import java.io.OutputStream;
    18 import java.net.HttpURLConnection;
    19 import java.net.URL;
    20 import java.nio.channels.Channels;
    21 import java.nio.channels.FileChannel;
    22 import java.util.zip.GZIPOutputStream;
    24 import android.app.Activity;
    25 import android.app.AlertDialog;
    26 import android.app.ProgressDialog;
    27 import android.content.DialogInterface;
    28 import android.content.Intent;
    29 import android.content.SharedPreferences;
    30 import android.os.Build;
    31 import android.os.Bundle;
    32 import android.os.Handler;
    33 import android.text.TextUtils;
    34 import android.util.Log;
    35 import android.view.View;
    36 import android.widget.CheckBox;
    37 import android.widget.CompoundButton;
    38 import android.widget.EditText;
    40 public class CrashReporter extends Activity
    41 {
    42     private static final String LOGTAG = "GeckoCrashReporter";
    44     private static final String PASSED_MINI_DUMP_KEY = "minidumpPath";
    45     private static final String MINI_DUMP_PATH_KEY = "upload_file_minidump";
    46     private static final String PAGE_URL_KEY = "URL";
    47     private static final String NOTES_KEY = "Notes";
    48     private static final String SERVER_URL_KEY = "ServerURL";
    50     private static final String CRASH_REPORT_SUFFIX = "/mozilla/Crash Reports/";
    51     private static final String PENDING_SUFFIX = CRASH_REPORT_SUFFIX + "pending";
    52     private static final String SUBMITTED_SUFFIX = CRASH_REPORT_SUFFIX + "submitted";
    54     private static final String PREFS_SEND_REPORT   = "sendReport";
    55     private static final String PREFS_INCLUDE_URL   = "includeUrl";
    56     private static final String PREFS_ALLOW_CONTACT = "allowContact";
    57     private static final String PREFS_CONTACT_EMAIL = "contactEmail";
    59     private Handler mHandler;
    60     private ProgressDialog mProgressDialog;
    61     private File mPendingMinidumpFile;
    62     private File mPendingExtrasFile;
    63     private HashMap<String, String> mExtrasStringMap;
    65     private boolean moveFile(File inFile, File outFile) {
    66         Log.i(LOGTAG, "moving " + inFile + " to " + outFile);
    67         if (inFile.renameTo(outFile))
    68             return true;
    69         try {
    70             outFile.createNewFile();
    71             Log.i(LOGTAG, "couldn't rename minidump file");
    72             // so copy it instead
    73             FileChannel inChannel = new FileInputStream(inFile).getChannel();
    74             FileChannel outChannel = new FileOutputStream(outFile).getChannel();
    75             long transferred = inChannel.transferTo(0, inChannel.size(), outChannel);
    76             inChannel.close();
    77             outChannel.close();
    79             if (transferred > 0)
    80                 inFile.delete();
    81         } catch (Exception e) {
    82             Log.e(LOGTAG, "exception while copying minidump file: ", e);
    83             return false;
    84         }
    85         return true;
    86     }
    88     private void doFinish() {
    89         if (mHandler != null) {
    90             mHandler.post(new Runnable() {
    91                 @Override
    92                 public void run() {
    93                     finish();
    94                 }
    95             });
    96         }
    97     }
    99     @Override
   100     public void finish() {
   101         try {
   102             if (mProgressDialog.isShowing()) {
   103                 mProgressDialog.dismiss();
   104             }
   105         } catch (Exception e) {
   106             Log.e(LOGTAG, "exception while closing progress dialog: ", e);
   107         }
   108         super.finish();
   109     }
   111     @Override
   112     public void onCreate(Bundle savedInstanceState) {
   113         super.onCreate(savedInstanceState);
   114         // mHandler is created here so runnables can be run on the main thread
   115         mHandler = new Handler();
   116         setContentView(R.layout.crash_reporter);
   117         mProgressDialog = new ProgressDialog(this);
   118         mProgressDialog.setMessage(getString(R.string.sending_crash_report));
   120         String passedMinidumpPath = getIntent().getStringExtra(PASSED_MINI_DUMP_KEY);
   121         File passedMinidumpFile = new File(passedMinidumpPath);
   122         File pendingDir = new File(getFilesDir(), PENDING_SUFFIX);
   123         pendingDir.mkdirs();
   124         mPendingMinidumpFile = new File(pendingDir, passedMinidumpFile.getName());
   125         moveFile(passedMinidumpFile, mPendingMinidumpFile);
   127         File extrasFile = new File(passedMinidumpPath.replaceAll(".dmp", ".extra"));
   128         mPendingExtrasFile = new File(pendingDir, extrasFile.getName());
   129         moveFile(extrasFile, mPendingExtrasFile);
   131         mExtrasStringMap = new HashMap<String, String>();
   132         readStringsFromFile(mPendingExtrasFile.getPath(), mExtrasStringMap);
   134         // Set the flag that indicates we were stopped as expected, as
   135         // we will send a crash report, so it is not a silent OOM crash.
   136         SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
   137         SharedPreferences.Editor editor = prefs.edit();
   138         editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
   139         editor.putBoolean(GeckoApp.PREFS_CRASHED, true);
   140         editor.commit();
   142         final CheckBox allowContactCheckBox = (CheckBox) findViewById(R.id.allow_contact);
   143         final CheckBox includeUrlCheckBox = (CheckBox) findViewById(R.id.include_url);
   144         final CheckBox sendReportCheckBox = (CheckBox) findViewById(R.id.send_report);
   145         final EditText commentsEditText = (EditText) findViewById(R.id.comment);
   146         final EditText emailEditText = (EditText) findViewById(R.id.email);
   148         // Load CrashReporter preferences to avoid redundant user input.
   149         final boolean sendReport   = prefs.getBoolean(PREFS_SEND_REPORT, true);
   150         final boolean includeUrl   = prefs.getBoolean(PREFS_INCLUDE_URL, false);
   151         final boolean allowContact = prefs.getBoolean(PREFS_ALLOW_CONTACT, false);
   152         final String contactEmail  = prefs.getString(PREFS_CONTACT_EMAIL, "");
   154         allowContactCheckBox.setChecked(allowContact);
   155         includeUrlCheckBox.setChecked(includeUrl);
   156         sendReportCheckBox.setChecked(sendReport);
   157         emailEditText.setText(contactEmail);
   159         sendReportCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() {
   160             @Override
   161             public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
   162                 commentsEditText.setEnabled(isChecked);
   163                 commentsEditText.requestFocus();
   165                 includeUrlCheckBox.setEnabled(isChecked);
   166                 allowContactCheckBox.setEnabled(isChecked);
   167                 emailEditText.setEnabled(isChecked && allowContactCheckBox.isChecked());
   168             }
   169         });
   171         allowContactCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() {
   172             @Override
   173             public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
   174                 // We need to check isEnabled() here because this listener is
   175                 // fired on rotation -- even when the checkbox is disabled.
   176                 emailEditText.setEnabled(checkbox.isEnabled() && isChecked);
   177                 emailEditText.requestFocus();
   178             }
   179         });
   181         emailEditText.setOnClickListener(new View.OnClickListener() {
   182             @Override
   183             public void onClick(View v) {
   184                 // Even if the email EditText is disabled, allow it to be
   185                 // clicked and focused.
   186                 if (sendReportCheckBox.isChecked() && !v.isEnabled()) {
   187                     allowContactCheckBox.setChecked(true);
   188                     v.setEnabled(true);
   189                     v.requestFocus();
   190                 }
   191             }
   192         });
   193     }
   195     @Override
   196     public void onBackPressed() {
   197         AlertDialog.Builder builder = new AlertDialog.Builder(this);
   198         builder.setMessage(R.string.crash_closing_alert);
   199         builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
   200             @Override
   201             public void onClick(DialogInterface dialog, int which) {
   202                 dialog.dismiss();
   203             }
   204         });
   205         builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
   206             @Override
   207             public void onClick(DialogInterface dialog, int which) {
   208                 CrashReporter.this.finish();
   209             }
   210         });
   211         builder.show();
   212     }
   214     private void backgroundSendReport() {
   215         final CheckBox sendReportCheckbox = (CheckBox) findViewById(R.id.send_report);
   216         if (!sendReportCheckbox.isChecked()) {
   217             doFinish();
   218             return;
   219         }
   221         // Persist settings to avoid redundant user input.
   222         savePrefs();
   224         mProgressDialog.show();
   225         new Thread(new Runnable() {
   226             @Override
   227             public void run() {
   228                 sendReport(mPendingMinidumpFile, mExtrasStringMap, mPendingExtrasFile);
   229             }
   230         }, "CrashReporter Thread").start();
   231     }
   233     private void savePrefs() {
   234         SharedPreferences.Editor editor = GeckoSharedPrefs.forApp(this).edit();
   236         final boolean allowContact = ((CheckBox) findViewById(R.id.allow_contact)).isChecked();
   237         final boolean includeUrl   = ((CheckBox) findViewById(R.id.include_url)).isChecked();
   238         final boolean sendReport   = ((CheckBox) findViewById(R.id.send_report)).isChecked();
   239         final String contactEmail  = ((EditText) findViewById(R.id.email)).getText().toString();
   241         editor.putBoolean(PREFS_ALLOW_CONTACT, allowContact);
   242         editor.putBoolean(PREFS_INCLUDE_URL, includeUrl);
   243         editor.putBoolean(PREFS_SEND_REPORT, sendReport);
   244         editor.putString(PREFS_CONTACT_EMAIL, contactEmail);
   246         // A slight performance improvement via async apply() vs. blocking on commit().
   247         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
   248             editor.commit();
   249         } else { 
   250             editor.apply();
   251         }
   252     }
   254     public void onCloseClick(View v) {  // bound via crash_reporter.xml
   255         backgroundSendReport();
   256     }
   258     public void onRestartClick(View v) {  // bound via crash_reporter.xml
   259         doRestart();
   260         backgroundSendReport();
   261     }
   263     private boolean readStringsFromFile(String filePath, Map<String, String> stringMap) {
   264         try {
   265             BufferedReader reader = new BufferedReader(new FileReader(filePath));
   266             return readStringsFromReader(reader, stringMap);
   267         } catch (Exception e) {
   268             Log.e(LOGTAG, "exception while reading strings: ", e);
   269             return false;
   270         }
   271     }
   273     private boolean readStringsFromReader(BufferedReader reader, Map<String, String> stringMap) throws IOException {
   274         String line;
   275         while ((line = reader.readLine()) != null) {
   276             int equalsPos = -1;
   277             if ((equalsPos = line.indexOf('=')) != -1) {
   278                 String key = line.substring(0, equalsPos);
   279                 String val = unescape(line.substring(equalsPos + 1));
   280                 stringMap.put(key, val);
   281             }
   282         }
   283         reader.close();
   284         return true;
   285     }
   287     private String generateBoundary() {
   288         // Generate some random numbers to fill out the boundary
   289         int r0 = (int)((double)Integer.MAX_VALUE * Math.random());
   290         int r1 = (int)((double)Integer.MAX_VALUE * Math.random());
   291         return String.format("---------------------------%08X%08X", r0, r1);
   292     }
   294     private void sendPart(OutputStream os, String boundary, String name, String data) {
   295         try {
   296             os.write(("--" + boundary + "\r\n" +
   297                       "Content-Disposition: form-data; name=\"" + name + "\"\r\n" +
   298                       "\r\n" +
   299                       data + "\r\n"
   300                      ).getBytes());
   301         } catch (Exception ex) {
   302             Log.e(LOGTAG, "Exception when sending \"" + name + "\"", ex);
   303         }
   304     }
   306     private void sendFile(OutputStream os, String boundary, String name, File file) throws IOException {
   307         os.write(("--" + boundary + "\r\n" +
   308                   "Content-Disposition: form-data; name=\"" + name + "\"; " +
   309                   "filename=\"" + file.getName() + "\"\r\n" +
   310                   "Content-Type: application/octet-stream\r\n" +
   311                   "\r\n"
   312                  ).getBytes());
   313         FileChannel fc = new FileInputStream(file).getChannel();
   314         fc.transferTo(0, fc.size(), Channels.newChannel(os));
   315         fc.close();
   316     }
   318     private String readLogcat() {
   319         BufferedReader br = null;
   320         try {
   321             // get the last 200 lines of logcat
   322             Process proc = Runtime.getRuntime().exec(new String[] {
   323                 "logcat", "-v", "threadtime", "-t", "200", "-d", "*:D"
   324             });
   325             StringBuilder sb = new StringBuilder();
   326             br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
   327             for (String s = br.readLine(); s != null; s = br.readLine()) {
   328                 sb.append(s).append('\n');
   329             }
   330             return sb.toString();
   331         } catch (Exception e) {
   332             return "Unable to get logcat: " + e.toString();
   333         } finally {
   334             if (br != null) {
   335                 try {
   336                     br.close();
   337                 } catch (Exception e) {
   338                     // ignore
   339                 }
   340             }
   341         }
   342     }
   344     private void sendReport(File minidumpFile, Map<String, String> extras, File extrasFile) {
   345         Log.i(LOGTAG, "sendReport: " + minidumpFile.getPath());
   346         final CheckBox includeURLCheckbox = (CheckBox) findViewById(R.id.include_url);
   348         String spec = extras.get(SERVER_URL_KEY);
   349         if (spec == null) {
   350             doFinish();
   351             return;
   352         }
   354         Log.i(LOGTAG, "server url: " + spec);
   355         try {
   356             URL url = new URL(spec);
   357             HttpURLConnection conn = (HttpURLConnection)url.openConnection();
   358             conn.setRequestMethod("POST");
   359             String boundary = generateBoundary();
   360             conn.setDoOutput(true);
   361             conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
   362             conn.setRequestProperty("Content-Encoding", "gzip");
   364             OutputStream os = new GZIPOutputStream(conn.getOutputStream());
   365             for (String key : extras.keySet()) {
   366                 if (key.equals(PAGE_URL_KEY)) {
   367                     if (includeURLCheckbox.isChecked())
   368                         sendPart(os, boundary, key, extras.get(key));
   369                 } else if (!key.equals(SERVER_URL_KEY) && !key.equals(NOTES_KEY)) {
   370                     sendPart(os, boundary, key, extras.get(key));
   371                 }
   372             }
   374             // Add some extra information to notes so its displayed by
   375             // crash-stats.mozilla.org. Remove this when bug 607942 is fixed.
   376             StringBuilder sb = new StringBuilder();
   377             sb.append(extras.containsKey(NOTES_KEY) ? extras.get(NOTES_KEY) + "\n" : "");
   378             if (AppConstants.MOZ_MIN_CPU_VERSION < 7) {
   379                 sb.append("nothumb Build\n");
   380             }
   381             sb.append(Build.MANUFACTURER).append(' ')
   382               .append(Build.MODEL).append('\n')
   383               .append(Build.FINGERPRINT);
   384             sendPart(os, boundary, NOTES_KEY, sb.toString());
   386             sendPart(os, boundary, "Min_ARM_Version", Integer.toString(AppConstants.MOZ_MIN_CPU_VERSION));
   387             sendPart(os, boundary, "Android_Manufacturer", Build.MANUFACTURER);
   388             sendPart(os, boundary, "Android_Model", Build.MODEL);
   389             sendPart(os, boundary, "Android_Board", Build.BOARD);
   390             sendPart(os, boundary, "Android_Brand", Build.BRAND);
   391             sendPart(os, boundary, "Android_Device", Build.DEVICE);
   392             sendPart(os, boundary, "Android_Display", Build.DISPLAY);
   393             sendPart(os, boundary, "Android_Fingerprint", Build.FINGERPRINT);
   394             sendPart(os, boundary, "Android_CPU_ABI", Build.CPU_ABI);
   395             if (Build.VERSION.SDK_INT >= 8) {
   396                 try {
   397                     sendPart(os, boundary, "Android_CPU_ABI2", Build.CPU_ABI2);
   398                     sendPart(os, boundary, "Android_Hardware", Build.HARDWARE);
   399                 } catch (Exception ex) {
   400                     Log.e(LOGTAG, "Exception while sending SDK version 8 keys", ex);
   401                 }
   402             }
   403             sendPart(os, boundary, "Android_Version",  Build.VERSION.SDK_INT + " (" + Build.VERSION.CODENAME + ")");
   404             if (Build.VERSION.SDK_INT >= 16 && includeURLCheckbox.isChecked()) {
   405                 sendPart(os, boundary, "Android_Logcat", readLogcat());
   406             }
   408             String comment = ((EditText) findViewById(R.id.comment)).getText().toString();
   409             if (!TextUtils.isEmpty(comment)) {
   410                 sendPart(os, boundary, "Comments", comment);
   411             }
   413             if (((CheckBox) findViewById(R.id.allow_contact)).isChecked()) {
   414                 String email = ((EditText) findViewById(R.id.email)).getText().toString();
   415                 sendPart(os, boundary, "Email", email);
   416             }
   418             sendFile(os, boundary, MINI_DUMP_PATH_KEY, minidumpFile);
   419             os.write(("\r\n--" + boundary + "--\r\n").getBytes());
   420             os.flush();
   421             os.close();
   422             BufferedReader br = new BufferedReader(
   423                 new InputStreamReader(conn.getInputStream()));
   424             HashMap<String, String>  responseMap = new HashMap<String, String>();
   425             readStringsFromReader(br, responseMap);
   427             if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
   428                 File submittedDir = new File(getFilesDir(),
   429                                              SUBMITTED_SUFFIX);
   430                 submittedDir.mkdirs();
   431                 minidumpFile.delete();
   432                 extrasFile.delete();
   433                 String crashid = responseMap.get("CrashID");
   434                 File file = new File(submittedDir, crashid + ".txt");
   435                 FileOutputStream fos = new FileOutputStream(file);
   436                 fos.write("Crash ID: ".getBytes());
   437                 fos.write(crashid.getBytes());
   438                 fos.close();
   439             } else {
   440                 Log.i(LOGTAG, "Received failure HTTP response code from server: " + conn.getResponseCode());
   441             }
   442         } catch (IOException e) {
   443             Log.e(LOGTAG, "exception during send: ", e);
   444         }
   446         doFinish();
   447     }
   449     private void doRestart() {
   450         try {
   451             String action = "android.intent.action.MAIN";
   452             Intent intent = new Intent(action);
   453             intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
   454                                 AppConstants.BROWSER_INTENT_CLASS_NAME);
   455             intent.putExtra("didRestart", true);
   456             Log.i(LOGTAG, intent.toString());
   457             startActivity(intent);
   458         } catch (Exception e) {
   459             Log.e(LOGTAG, "error while trying to restart", e);
   460         }
   461     }
   463     private String unescape(String string) {
   464         return string.replaceAll("\\\\\\\\", "\\").replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t");
   465     }
   466 }

mercurial