1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/CrashReporter.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,466 @@ 1.4 +/* -*- Mode: Java; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import java.util.HashMap; 1.12 +import java.util.Map; 1.13 +import java.io.BufferedReader; 1.14 +import java.io.File; 1.15 +import java.io.FileInputStream; 1.16 +import java.io.FileOutputStream; 1.17 +import java.io.FileReader; 1.18 +import java.io.InputStreamReader; 1.19 +import java.io.IOException; 1.20 +import java.io.OutputStream; 1.21 +import java.net.HttpURLConnection; 1.22 +import java.net.URL; 1.23 +import java.nio.channels.Channels; 1.24 +import java.nio.channels.FileChannel; 1.25 +import java.util.zip.GZIPOutputStream; 1.26 + 1.27 +import android.app.Activity; 1.28 +import android.app.AlertDialog; 1.29 +import android.app.ProgressDialog; 1.30 +import android.content.DialogInterface; 1.31 +import android.content.Intent; 1.32 +import android.content.SharedPreferences; 1.33 +import android.os.Build; 1.34 +import android.os.Bundle; 1.35 +import android.os.Handler; 1.36 +import android.text.TextUtils; 1.37 +import android.util.Log; 1.38 +import android.view.View; 1.39 +import android.widget.CheckBox; 1.40 +import android.widget.CompoundButton; 1.41 +import android.widget.EditText; 1.42 + 1.43 +public class CrashReporter extends Activity 1.44 +{ 1.45 + private static final String LOGTAG = "GeckoCrashReporter"; 1.46 + 1.47 + private static final String PASSED_MINI_DUMP_KEY = "minidumpPath"; 1.48 + private static final String MINI_DUMP_PATH_KEY = "upload_file_minidump"; 1.49 + private static final String PAGE_URL_KEY = "URL"; 1.50 + private static final String NOTES_KEY = "Notes"; 1.51 + private static final String SERVER_URL_KEY = "ServerURL"; 1.52 + 1.53 + private static final String CRASH_REPORT_SUFFIX = "/mozilla/Crash Reports/"; 1.54 + private static final String PENDING_SUFFIX = CRASH_REPORT_SUFFIX + "pending"; 1.55 + private static final String SUBMITTED_SUFFIX = CRASH_REPORT_SUFFIX + "submitted"; 1.56 + 1.57 + private static final String PREFS_SEND_REPORT = "sendReport"; 1.58 + private static final String PREFS_INCLUDE_URL = "includeUrl"; 1.59 + private static final String PREFS_ALLOW_CONTACT = "allowContact"; 1.60 + private static final String PREFS_CONTACT_EMAIL = "contactEmail"; 1.61 + 1.62 + private Handler mHandler; 1.63 + private ProgressDialog mProgressDialog; 1.64 + private File mPendingMinidumpFile; 1.65 + private File mPendingExtrasFile; 1.66 + private HashMap<String, String> mExtrasStringMap; 1.67 + 1.68 + private boolean moveFile(File inFile, File outFile) { 1.69 + Log.i(LOGTAG, "moving " + inFile + " to " + outFile); 1.70 + if (inFile.renameTo(outFile)) 1.71 + return true; 1.72 + try { 1.73 + outFile.createNewFile(); 1.74 + Log.i(LOGTAG, "couldn't rename minidump file"); 1.75 + // so copy it instead 1.76 + FileChannel inChannel = new FileInputStream(inFile).getChannel(); 1.77 + FileChannel outChannel = new FileOutputStream(outFile).getChannel(); 1.78 + long transferred = inChannel.transferTo(0, inChannel.size(), outChannel); 1.79 + inChannel.close(); 1.80 + outChannel.close(); 1.81 + 1.82 + if (transferred > 0) 1.83 + inFile.delete(); 1.84 + } catch (Exception e) { 1.85 + Log.e(LOGTAG, "exception while copying minidump file: ", e); 1.86 + return false; 1.87 + } 1.88 + return true; 1.89 + } 1.90 + 1.91 + private void doFinish() { 1.92 + if (mHandler != null) { 1.93 + mHandler.post(new Runnable() { 1.94 + @Override 1.95 + public void run() { 1.96 + finish(); 1.97 + } 1.98 + }); 1.99 + } 1.100 + } 1.101 + 1.102 + @Override 1.103 + public void finish() { 1.104 + try { 1.105 + if (mProgressDialog.isShowing()) { 1.106 + mProgressDialog.dismiss(); 1.107 + } 1.108 + } catch (Exception e) { 1.109 + Log.e(LOGTAG, "exception while closing progress dialog: ", e); 1.110 + } 1.111 + super.finish(); 1.112 + } 1.113 + 1.114 + @Override 1.115 + public void onCreate(Bundle savedInstanceState) { 1.116 + super.onCreate(savedInstanceState); 1.117 + // mHandler is created here so runnables can be run on the main thread 1.118 + mHandler = new Handler(); 1.119 + setContentView(R.layout.crash_reporter); 1.120 + mProgressDialog = new ProgressDialog(this); 1.121 + mProgressDialog.setMessage(getString(R.string.sending_crash_report)); 1.122 + 1.123 + String passedMinidumpPath = getIntent().getStringExtra(PASSED_MINI_DUMP_KEY); 1.124 + File passedMinidumpFile = new File(passedMinidumpPath); 1.125 + File pendingDir = new File(getFilesDir(), PENDING_SUFFIX); 1.126 + pendingDir.mkdirs(); 1.127 + mPendingMinidumpFile = new File(pendingDir, passedMinidumpFile.getName()); 1.128 + moveFile(passedMinidumpFile, mPendingMinidumpFile); 1.129 + 1.130 + File extrasFile = new File(passedMinidumpPath.replaceAll(".dmp", ".extra")); 1.131 + mPendingExtrasFile = new File(pendingDir, extrasFile.getName()); 1.132 + moveFile(extrasFile, mPendingExtrasFile); 1.133 + 1.134 + mExtrasStringMap = new HashMap<String, String>(); 1.135 + readStringsFromFile(mPendingExtrasFile.getPath(), mExtrasStringMap); 1.136 + 1.137 + // Set the flag that indicates we were stopped as expected, as 1.138 + // we will send a crash report, so it is not a silent OOM crash. 1.139 + SharedPreferences prefs = GeckoSharedPrefs.forApp(this); 1.140 + SharedPreferences.Editor editor = prefs.edit(); 1.141 + editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true); 1.142 + editor.putBoolean(GeckoApp.PREFS_CRASHED, true); 1.143 + editor.commit(); 1.144 + 1.145 + final CheckBox allowContactCheckBox = (CheckBox) findViewById(R.id.allow_contact); 1.146 + final CheckBox includeUrlCheckBox = (CheckBox) findViewById(R.id.include_url); 1.147 + final CheckBox sendReportCheckBox = (CheckBox) findViewById(R.id.send_report); 1.148 + final EditText commentsEditText = (EditText) findViewById(R.id.comment); 1.149 + final EditText emailEditText = (EditText) findViewById(R.id.email); 1.150 + 1.151 + // Load CrashReporter preferences to avoid redundant user input. 1.152 + final boolean sendReport = prefs.getBoolean(PREFS_SEND_REPORT, true); 1.153 + final boolean includeUrl = prefs.getBoolean(PREFS_INCLUDE_URL, false); 1.154 + final boolean allowContact = prefs.getBoolean(PREFS_ALLOW_CONTACT, false); 1.155 + final String contactEmail = prefs.getString(PREFS_CONTACT_EMAIL, ""); 1.156 + 1.157 + allowContactCheckBox.setChecked(allowContact); 1.158 + includeUrlCheckBox.setChecked(includeUrl); 1.159 + sendReportCheckBox.setChecked(sendReport); 1.160 + emailEditText.setText(contactEmail); 1.161 + 1.162 + sendReportCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() { 1.163 + @Override 1.164 + public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) { 1.165 + commentsEditText.setEnabled(isChecked); 1.166 + commentsEditText.requestFocus(); 1.167 + 1.168 + includeUrlCheckBox.setEnabled(isChecked); 1.169 + allowContactCheckBox.setEnabled(isChecked); 1.170 + emailEditText.setEnabled(isChecked && allowContactCheckBox.isChecked()); 1.171 + } 1.172 + }); 1.173 + 1.174 + allowContactCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() { 1.175 + @Override 1.176 + public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) { 1.177 + // We need to check isEnabled() here because this listener is 1.178 + // fired on rotation -- even when the checkbox is disabled. 1.179 + emailEditText.setEnabled(checkbox.isEnabled() && isChecked); 1.180 + emailEditText.requestFocus(); 1.181 + } 1.182 + }); 1.183 + 1.184 + emailEditText.setOnClickListener(new View.OnClickListener() { 1.185 + @Override 1.186 + public void onClick(View v) { 1.187 + // Even if the email EditText is disabled, allow it to be 1.188 + // clicked and focused. 1.189 + if (sendReportCheckBox.isChecked() && !v.isEnabled()) { 1.190 + allowContactCheckBox.setChecked(true); 1.191 + v.setEnabled(true); 1.192 + v.requestFocus(); 1.193 + } 1.194 + } 1.195 + }); 1.196 + } 1.197 + 1.198 + @Override 1.199 + public void onBackPressed() { 1.200 + AlertDialog.Builder builder = new AlertDialog.Builder(this); 1.201 + builder.setMessage(R.string.crash_closing_alert); 1.202 + builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { 1.203 + @Override 1.204 + public void onClick(DialogInterface dialog, int which) { 1.205 + dialog.dismiss(); 1.206 + } 1.207 + }); 1.208 + builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { 1.209 + @Override 1.210 + public void onClick(DialogInterface dialog, int which) { 1.211 + CrashReporter.this.finish(); 1.212 + } 1.213 + }); 1.214 + builder.show(); 1.215 + } 1.216 + 1.217 + private void backgroundSendReport() { 1.218 + final CheckBox sendReportCheckbox = (CheckBox) findViewById(R.id.send_report); 1.219 + if (!sendReportCheckbox.isChecked()) { 1.220 + doFinish(); 1.221 + return; 1.222 + } 1.223 + 1.224 + // Persist settings to avoid redundant user input. 1.225 + savePrefs(); 1.226 + 1.227 + mProgressDialog.show(); 1.228 + new Thread(new Runnable() { 1.229 + @Override 1.230 + public void run() { 1.231 + sendReport(mPendingMinidumpFile, mExtrasStringMap, mPendingExtrasFile); 1.232 + } 1.233 + }, "CrashReporter Thread").start(); 1.234 + } 1.235 + 1.236 + private void savePrefs() { 1.237 + SharedPreferences.Editor editor = GeckoSharedPrefs.forApp(this).edit(); 1.238 + 1.239 + final boolean allowContact = ((CheckBox) findViewById(R.id.allow_contact)).isChecked(); 1.240 + final boolean includeUrl = ((CheckBox) findViewById(R.id.include_url)).isChecked(); 1.241 + final boolean sendReport = ((CheckBox) findViewById(R.id.send_report)).isChecked(); 1.242 + final String contactEmail = ((EditText) findViewById(R.id.email)).getText().toString(); 1.243 + 1.244 + editor.putBoolean(PREFS_ALLOW_CONTACT, allowContact); 1.245 + editor.putBoolean(PREFS_INCLUDE_URL, includeUrl); 1.246 + editor.putBoolean(PREFS_SEND_REPORT, sendReport); 1.247 + editor.putString(PREFS_CONTACT_EMAIL, contactEmail); 1.248 + 1.249 + // A slight performance improvement via async apply() vs. blocking on commit(). 1.250 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { 1.251 + editor.commit(); 1.252 + } else { 1.253 + editor.apply(); 1.254 + } 1.255 + } 1.256 + 1.257 + public void onCloseClick(View v) { // bound via crash_reporter.xml 1.258 + backgroundSendReport(); 1.259 + } 1.260 + 1.261 + public void onRestartClick(View v) { // bound via crash_reporter.xml 1.262 + doRestart(); 1.263 + backgroundSendReport(); 1.264 + } 1.265 + 1.266 + private boolean readStringsFromFile(String filePath, Map<String, String> stringMap) { 1.267 + try { 1.268 + BufferedReader reader = new BufferedReader(new FileReader(filePath)); 1.269 + return readStringsFromReader(reader, stringMap); 1.270 + } catch (Exception e) { 1.271 + Log.e(LOGTAG, "exception while reading strings: ", e); 1.272 + return false; 1.273 + } 1.274 + } 1.275 + 1.276 + private boolean readStringsFromReader(BufferedReader reader, Map<String, String> stringMap) throws IOException { 1.277 + String line; 1.278 + while ((line = reader.readLine()) != null) { 1.279 + int equalsPos = -1; 1.280 + if ((equalsPos = line.indexOf('=')) != -1) { 1.281 + String key = line.substring(0, equalsPos); 1.282 + String val = unescape(line.substring(equalsPos + 1)); 1.283 + stringMap.put(key, val); 1.284 + } 1.285 + } 1.286 + reader.close(); 1.287 + return true; 1.288 + } 1.289 + 1.290 + private String generateBoundary() { 1.291 + // Generate some random numbers to fill out the boundary 1.292 + int r0 = (int)((double)Integer.MAX_VALUE * Math.random()); 1.293 + int r1 = (int)((double)Integer.MAX_VALUE * Math.random()); 1.294 + return String.format("---------------------------%08X%08X", r0, r1); 1.295 + } 1.296 + 1.297 + private void sendPart(OutputStream os, String boundary, String name, String data) { 1.298 + try { 1.299 + os.write(("--" + boundary + "\r\n" + 1.300 + "Content-Disposition: form-data; name=\"" + name + "\"\r\n" + 1.301 + "\r\n" + 1.302 + data + "\r\n" 1.303 + ).getBytes()); 1.304 + } catch (Exception ex) { 1.305 + Log.e(LOGTAG, "Exception when sending \"" + name + "\"", ex); 1.306 + } 1.307 + } 1.308 + 1.309 + private void sendFile(OutputStream os, String boundary, String name, File file) throws IOException { 1.310 + os.write(("--" + boundary + "\r\n" + 1.311 + "Content-Disposition: form-data; name=\"" + name + "\"; " + 1.312 + "filename=\"" + file.getName() + "\"\r\n" + 1.313 + "Content-Type: application/octet-stream\r\n" + 1.314 + "\r\n" 1.315 + ).getBytes()); 1.316 + FileChannel fc = new FileInputStream(file).getChannel(); 1.317 + fc.transferTo(0, fc.size(), Channels.newChannel(os)); 1.318 + fc.close(); 1.319 + } 1.320 + 1.321 + private String readLogcat() { 1.322 + BufferedReader br = null; 1.323 + try { 1.324 + // get the last 200 lines of logcat 1.325 + Process proc = Runtime.getRuntime().exec(new String[] { 1.326 + "logcat", "-v", "threadtime", "-t", "200", "-d", "*:D" 1.327 + }); 1.328 + StringBuilder sb = new StringBuilder(); 1.329 + br = new BufferedReader(new InputStreamReader(proc.getInputStream())); 1.330 + for (String s = br.readLine(); s != null; s = br.readLine()) { 1.331 + sb.append(s).append('\n'); 1.332 + } 1.333 + return sb.toString(); 1.334 + } catch (Exception e) { 1.335 + return "Unable to get logcat: " + e.toString(); 1.336 + } finally { 1.337 + if (br != null) { 1.338 + try { 1.339 + br.close(); 1.340 + } catch (Exception e) { 1.341 + // ignore 1.342 + } 1.343 + } 1.344 + } 1.345 + } 1.346 + 1.347 + private void sendReport(File minidumpFile, Map<String, String> extras, File extrasFile) { 1.348 + Log.i(LOGTAG, "sendReport: " + minidumpFile.getPath()); 1.349 + final CheckBox includeURLCheckbox = (CheckBox) findViewById(R.id.include_url); 1.350 + 1.351 + String spec = extras.get(SERVER_URL_KEY); 1.352 + if (spec == null) { 1.353 + doFinish(); 1.354 + return; 1.355 + } 1.356 + 1.357 + Log.i(LOGTAG, "server url: " + spec); 1.358 + try { 1.359 + URL url = new URL(spec); 1.360 + HttpURLConnection conn = (HttpURLConnection)url.openConnection(); 1.361 + conn.setRequestMethod("POST"); 1.362 + String boundary = generateBoundary(); 1.363 + conn.setDoOutput(true); 1.364 + conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); 1.365 + conn.setRequestProperty("Content-Encoding", "gzip"); 1.366 + 1.367 + OutputStream os = new GZIPOutputStream(conn.getOutputStream()); 1.368 + for (String key : extras.keySet()) { 1.369 + if (key.equals(PAGE_URL_KEY)) { 1.370 + if (includeURLCheckbox.isChecked()) 1.371 + sendPart(os, boundary, key, extras.get(key)); 1.372 + } else if (!key.equals(SERVER_URL_KEY) && !key.equals(NOTES_KEY)) { 1.373 + sendPart(os, boundary, key, extras.get(key)); 1.374 + } 1.375 + } 1.376 + 1.377 + // Add some extra information to notes so its displayed by 1.378 + // crash-stats.mozilla.org. Remove this when bug 607942 is fixed. 1.379 + StringBuilder sb = new StringBuilder(); 1.380 + sb.append(extras.containsKey(NOTES_KEY) ? extras.get(NOTES_KEY) + "\n" : ""); 1.381 + if (AppConstants.MOZ_MIN_CPU_VERSION < 7) { 1.382 + sb.append("nothumb Build\n"); 1.383 + } 1.384 + sb.append(Build.MANUFACTURER).append(' ') 1.385 + .append(Build.MODEL).append('\n') 1.386 + .append(Build.FINGERPRINT); 1.387 + sendPart(os, boundary, NOTES_KEY, sb.toString()); 1.388 + 1.389 + sendPart(os, boundary, "Min_ARM_Version", Integer.toString(AppConstants.MOZ_MIN_CPU_VERSION)); 1.390 + sendPart(os, boundary, "Android_Manufacturer", Build.MANUFACTURER); 1.391 + sendPart(os, boundary, "Android_Model", Build.MODEL); 1.392 + sendPart(os, boundary, "Android_Board", Build.BOARD); 1.393 + sendPart(os, boundary, "Android_Brand", Build.BRAND); 1.394 + sendPart(os, boundary, "Android_Device", Build.DEVICE); 1.395 + sendPart(os, boundary, "Android_Display", Build.DISPLAY); 1.396 + sendPart(os, boundary, "Android_Fingerprint", Build.FINGERPRINT); 1.397 + sendPart(os, boundary, "Android_CPU_ABI", Build.CPU_ABI); 1.398 + if (Build.VERSION.SDK_INT >= 8) { 1.399 + try { 1.400 + sendPart(os, boundary, "Android_CPU_ABI2", Build.CPU_ABI2); 1.401 + sendPart(os, boundary, "Android_Hardware", Build.HARDWARE); 1.402 + } catch (Exception ex) { 1.403 + Log.e(LOGTAG, "Exception while sending SDK version 8 keys", ex); 1.404 + } 1.405 + } 1.406 + sendPart(os, boundary, "Android_Version", Build.VERSION.SDK_INT + " (" + Build.VERSION.CODENAME + ")"); 1.407 + if (Build.VERSION.SDK_INT >= 16 && includeURLCheckbox.isChecked()) { 1.408 + sendPart(os, boundary, "Android_Logcat", readLogcat()); 1.409 + } 1.410 + 1.411 + String comment = ((EditText) findViewById(R.id.comment)).getText().toString(); 1.412 + if (!TextUtils.isEmpty(comment)) { 1.413 + sendPart(os, boundary, "Comments", comment); 1.414 + } 1.415 + 1.416 + if (((CheckBox) findViewById(R.id.allow_contact)).isChecked()) { 1.417 + String email = ((EditText) findViewById(R.id.email)).getText().toString(); 1.418 + sendPart(os, boundary, "Email", email); 1.419 + } 1.420 + 1.421 + sendFile(os, boundary, MINI_DUMP_PATH_KEY, minidumpFile); 1.422 + os.write(("\r\n--" + boundary + "--\r\n").getBytes()); 1.423 + os.flush(); 1.424 + os.close(); 1.425 + BufferedReader br = new BufferedReader( 1.426 + new InputStreamReader(conn.getInputStream())); 1.427 + HashMap<String, String> responseMap = new HashMap<String, String>(); 1.428 + readStringsFromReader(br, responseMap); 1.429 + 1.430 + if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { 1.431 + File submittedDir = new File(getFilesDir(), 1.432 + SUBMITTED_SUFFIX); 1.433 + submittedDir.mkdirs(); 1.434 + minidumpFile.delete(); 1.435 + extrasFile.delete(); 1.436 + String crashid = responseMap.get("CrashID"); 1.437 + File file = new File(submittedDir, crashid + ".txt"); 1.438 + FileOutputStream fos = new FileOutputStream(file); 1.439 + fos.write("Crash ID: ".getBytes()); 1.440 + fos.write(crashid.getBytes()); 1.441 + fos.close(); 1.442 + } else { 1.443 + Log.i(LOGTAG, "Received failure HTTP response code from server: " + conn.getResponseCode()); 1.444 + } 1.445 + } catch (IOException e) { 1.446 + Log.e(LOGTAG, "exception during send: ", e); 1.447 + } 1.448 + 1.449 + doFinish(); 1.450 + } 1.451 + 1.452 + private void doRestart() { 1.453 + try { 1.454 + String action = "android.intent.action.MAIN"; 1.455 + Intent intent = new Intent(action); 1.456 + intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, 1.457 + AppConstants.BROWSER_INTENT_CLASS_NAME); 1.458 + intent.putExtra("didRestart", true); 1.459 + Log.i(LOGTAG, intent.toString()); 1.460 + startActivity(intent); 1.461 + } catch (Exception e) { 1.462 + Log.e(LOGTAG, "error while trying to restart", e); 1.463 + } 1.464 + } 1.465 + 1.466 + private String unescape(String string) { 1.467 + return string.replaceAll("\\\\\\\\", "\\").replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t"); 1.468 + } 1.469 +}