michael@0: /* -*- Mode: Java; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import java.util.HashMap; michael@0: import java.util.Map; michael@0: import java.io.BufferedReader; michael@0: import java.io.File; michael@0: import java.io.FileInputStream; michael@0: import java.io.FileOutputStream; michael@0: import java.io.FileReader; michael@0: import java.io.InputStreamReader; michael@0: import java.io.IOException; michael@0: import java.io.OutputStream; michael@0: import java.net.HttpURLConnection; michael@0: import java.net.URL; michael@0: import java.nio.channels.Channels; michael@0: import java.nio.channels.FileChannel; michael@0: import java.util.zip.GZIPOutputStream; michael@0: michael@0: import android.app.Activity; michael@0: import android.app.AlertDialog; michael@0: import android.app.ProgressDialog; michael@0: import android.content.DialogInterface; michael@0: import android.content.Intent; michael@0: import android.content.SharedPreferences; michael@0: import android.os.Build; michael@0: import android.os.Bundle; michael@0: import android.os.Handler; michael@0: import android.text.TextUtils; michael@0: import android.util.Log; michael@0: import android.view.View; michael@0: import android.widget.CheckBox; michael@0: import android.widget.CompoundButton; michael@0: import android.widget.EditText; michael@0: michael@0: public class CrashReporter extends Activity michael@0: { michael@0: private static final String LOGTAG = "GeckoCrashReporter"; michael@0: michael@0: private static final String PASSED_MINI_DUMP_KEY = "minidumpPath"; michael@0: private static final String MINI_DUMP_PATH_KEY = "upload_file_minidump"; michael@0: private static final String PAGE_URL_KEY = "URL"; michael@0: private static final String NOTES_KEY = "Notes"; michael@0: private static final String SERVER_URL_KEY = "ServerURL"; michael@0: michael@0: private static final String CRASH_REPORT_SUFFIX = "/mozilla/Crash Reports/"; michael@0: private static final String PENDING_SUFFIX = CRASH_REPORT_SUFFIX + "pending"; michael@0: private static final String SUBMITTED_SUFFIX = CRASH_REPORT_SUFFIX + "submitted"; michael@0: michael@0: private static final String PREFS_SEND_REPORT = "sendReport"; michael@0: private static final String PREFS_INCLUDE_URL = "includeUrl"; michael@0: private static final String PREFS_ALLOW_CONTACT = "allowContact"; michael@0: private static final String PREFS_CONTACT_EMAIL = "contactEmail"; michael@0: michael@0: private Handler mHandler; michael@0: private ProgressDialog mProgressDialog; michael@0: private File mPendingMinidumpFile; michael@0: private File mPendingExtrasFile; michael@0: private HashMap mExtrasStringMap; michael@0: michael@0: private boolean moveFile(File inFile, File outFile) { michael@0: Log.i(LOGTAG, "moving " + inFile + " to " + outFile); michael@0: if (inFile.renameTo(outFile)) michael@0: return true; michael@0: try { michael@0: outFile.createNewFile(); michael@0: Log.i(LOGTAG, "couldn't rename minidump file"); michael@0: // so copy it instead michael@0: FileChannel inChannel = new FileInputStream(inFile).getChannel(); michael@0: FileChannel outChannel = new FileOutputStream(outFile).getChannel(); michael@0: long transferred = inChannel.transferTo(0, inChannel.size(), outChannel); michael@0: inChannel.close(); michael@0: outChannel.close(); michael@0: michael@0: if (transferred > 0) michael@0: inFile.delete(); michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "exception while copying minidump file: ", e); michael@0: return false; michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: private void doFinish() { michael@0: if (mHandler != null) { michael@0: mHandler.post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: finish(); michael@0: } michael@0: }); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void finish() { michael@0: try { michael@0: if (mProgressDialog.isShowing()) { michael@0: mProgressDialog.dismiss(); michael@0: } michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "exception while closing progress dialog: ", e); michael@0: } michael@0: super.finish(); michael@0: } michael@0: michael@0: @Override michael@0: public void onCreate(Bundle savedInstanceState) { michael@0: super.onCreate(savedInstanceState); michael@0: // mHandler is created here so runnables can be run on the main thread michael@0: mHandler = new Handler(); michael@0: setContentView(R.layout.crash_reporter); michael@0: mProgressDialog = new ProgressDialog(this); michael@0: mProgressDialog.setMessage(getString(R.string.sending_crash_report)); michael@0: michael@0: String passedMinidumpPath = getIntent().getStringExtra(PASSED_MINI_DUMP_KEY); michael@0: File passedMinidumpFile = new File(passedMinidumpPath); michael@0: File pendingDir = new File(getFilesDir(), PENDING_SUFFIX); michael@0: pendingDir.mkdirs(); michael@0: mPendingMinidumpFile = new File(pendingDir, passedMinidumpFile.getName()); michael@0: moveFile(passedMinidumpFile, mPendingMinidumpFile); michael@0: michael@0: File extrasFile = new File(passedMinidumpPath.replaceAll(".dmp", ".extra")); michael@0: mPendingExtrasFile = new File(pendingDir, extrasFile.getName()); michael@0: moveFile(extrasFile, mPendingExtrasFile); michael@0: michael@0: mExtrasStringMap = new HashMap(); michael@0: readStringsFromFile(mPendingExtrasFile.getPath(), mExtrasStringMap); michael@0: michael@0: // Set the flag that indicates we were stopped as expected, as michael@0: // we will send a crash report, so it is not a silent OOM crash. michael@0: SharedPreferences prefs = GeckoSharedPrefs.forApp(this); michael@0: SharedPreferences.Editor editor = prefs.edit(); michael@0: editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true); michael@0: editor.putBoolean(GeckoApp.PREFS_CRASHED, true); michael@0: editor.commit(); michael@0: michael@0: final CheckBox allowContactCheckBox = (CheckBox) findViewById(R.id.allow_contact); michael@0: final CheckBox includeUrlCheckBox = (CheckBox) findViewById(R.id.include_url); michael@0: final CheckBox sendReportCheckBox = (CheckBox) findViewById(R.id.send_report); michael@0: final EditText commentsEditText = (EditText) findViewById(R.id.comment); michael@0: final EditText emailEditText = (EditText) findViewById(R.id.email); michael@0: michael@0: // Load CrashReporter preferences to avoid redundant user input. michael@0: final boolean sendReport = prefs.getBoolean(PREFS_SEND_REPORT, true); michael@0: final boolean includeUrl = prefs.getBoolean(PREFS_INCLUDE_URL, false); michael@0: final boolean allowContact = prefs.getBoolean(PREFS_ALLOW_CONTACT, false); michael@0: final String contactEmail = prefs.getString(PREFS_CONTACT_EMAIL, ""); michael@0: michael@0: allowContactCheckBox.setChecked(allowContact); michael@0: includeUrlCheckBox.setChecked(includeUrl); michael@0: sendReportCheckBox.setChecked(sendReport); michael@0: emailEditText.setText(contactEmail); michael@0: michael@0: sendReportCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() { michael@0: @Override michael@0: public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) { michael@0: commentsEditText.setEnabled(isChecked); michael@0: commentsEditText.requestFocus(); michael@0: michael@0: includeUrlCheckBox.setEnabled(isChecked); michael@0: allowContactCheckBox.setEnabled(isChecked); michael@0: emailEditText.setEnabled(isChecked && allowContactCheckBox.isChecked()); michael@0: } michael@0: }); michael@0: michael@0: allowContactCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() { michael@0: @Override michael@0: public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) { michael@0: // We need to check isEnabled() here because this listener is michael@0: // fired on rotation -- even when the checkbox is disabled. michael@0: emailEditText.setEnabled(checkbox.isEnabled() && isChecked); michael@0: emailEditText.requestFocus(); michael@0: } michael@0: }); michael@0: michael@0: emailEditText.setOnClickListener(new View.OnClickListener() { michael@0: @Override michael@0: public void onClick(View v) { michael@0: // Even if the email EditText is disabled, allow it to be michael@0: // clicked and focused. michael@0: if (sendReportCheckBox.isChecked() && !v.isEnabled()) { michael@0: allowContactCheckBox.setChecked(true); michael@0: v.setEnabled(true); michael@0: v.requestFocus(); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: @Override michael@0: public void onBackPressed() { michael@0: AlertDialog.Builder builder = new AlertDialog.Builder(this); michael@0: builder.setMessage(R.string.crash_closing_alert); michael@0: builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { michael@0: @Override michael@0: public void onClick(DialogInterface dialog, int which) { michael@0: dialog.dismiss(); michael@0: } michael@0: }); michael@0: builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { michael@0: @Override michael@0: public void onClick(DialogInterface dialog, int which) { michael@0: CrashReporter.this.finish(); michael@0: } michael@0: }); michael@0: builder.show(); michael@0: } michael@0: michael@0: private void backgroundSendReport() { michael@0: final CheckBox sendReportCheckbox = (CheckBox) findViewById(R.id.send_report); michael@0: if (!sendReportCheckbox.isChecked()) { michael@0: doFinish(); michael@0: return; michael@0: } michael@0: michael@0: // Persist settings to avoid redundant user input. michael@0: savePrefs(); michael@0: michael@0: mProgressDialog.show(); michael@0: new Thread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: sendReport(mPendingMinidumpFile, mExtrasStringMap, mPendingExtrasFile); michael@0: } michael@0: }, "CrashReporter Thread").start(); michael@0: } michael@0: michael@0: private void savePrefs() { michael@0: SharedPreferences.Editor editor = GeckoSharedPrefs.forApp(this).edit(); michael@0: michael@0: final boolean allowContact = ((CheckBox) findViewById(R.id.allow_contact)).isChecked(); michael@0: final boolean includeUrl = ((CheckBox) findViewById(R.id.include_url)).isChecked(); michael@0: final boolean sendReport = ((CheckBox) findViewById(R.id.send_report)).isChecked(); michael@0: final String contactEmail = ((EditText) findViewById(R.id.email)).getText().toString(); michael@0: michael@0: editor.putBoolean(PREFS_ALLOW_CONTACT, allowContact); michael@0: editor.putBoolean(PREFS_INCLUDE_URL, includeUrl); michael@0: editor.putBoolean(PREFS_SEND_REPORT, sendReport); michael@0: editor.putString(PREFS_CONTACT_EMAIL, contactEmail); michael@0: michael@0: // A slight performance improvement via async apply() vs. blocking on commit(). michael@0: if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { michael@0: editor.commit(); michael@0: } else { michael@0: editor.apply(); michael@0: } michael@0: } michael@0: michael@0: public void onCloseClick(View v) { // bound via crash_reporter.xml michael@0: backgroundSendReport(); michael@0: } michael@0: michael@0: public void onRestartClick(View v) { // bound via crash_reporter.xml michael@0: doRestart(); michael@0: backgroundSendReport(); michael@0: } michael@0: michael@0: private boolean readStringsFromFile(String filePath, Map stringMap) { michael@0: try { michael@0: BufferedReader reader = new BufferedReader(new FileReader(filePath)); michael@0: return readStringsFromReader(reader, stringMap); michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "exception while reading strings: ", e); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: private boolean readStringsFromReader(BufferedReader reader, Map stringMap) throws IOException { michael@0: String line; michael@0: while ((line = reader.readLine()) != null) { michael@0: int equalsPos = -1; michael@0: if ((equalsPos = line.indexOf('=')) != -1) { michael@0: String key = line.substring(0, equalsPos); michael@0: String val = unescape(line.substring(equalsPos + 1)); michael@0: stringMap.put(key, val); michael@0: } michael@0: } michael@0: reader.close(); michael@0: return true; michael@0: } michael@0: michael@0: private String generateBoundary() { michael@0: // Generate some random numbers to fill out the boundary michael@0: int r0 = (int)((double)Integer.MAX_VALUE * Math.random()); michael@0: int r1 = (int)((double)Integer.MAX_VALUE * Math.random()); michael@0: return String.format("---------------------------%08X%08X", r0, r1); michael@0: } michael@0: michael@0: private void sendPart(OutputStream os, String boundary, String name, String data) { michael@0: try { michael@0: os.write(("--" + boundary + "\r\n" + michael@0: "Content-Disposition: form-data; name=\"" + name + "\"\r\n" + michael@0: "\r\n" + michael@0: data + "\r\n" michael@0: ).getBytes()); michael@0: } catch (Exception ex) { michael@0: Log.e(LOGTAG, "Exception when sending \"" + name + "\"", ex); michael@0: } michael@0: } michael@0: michael@0: private void sendFile(OutputStream os, String boundary, String name, File file) throws IOException { michael@0: os.write(("--" + boundary + "\r\n" + michael@0: "Content-Disposition: form-data; name=\"" + name + "\"; " + michael@0: "filename=\"" + file.getName() + "\"\r\n" + michael@0: "Content-Type: application/octet-stream\r\n" + michael@0: "\r\n" michael@0: ).getBytes()); michael@0: FileChannel fc = new FileInputStream(file).getChannel(); michael@0: fc.transferTo(0, fc.size(), Channels.newChannel(os)); michael@0: fc.close(); michael@0: } michael@0: michael@0: private String readLogcat() { michael@0: BufferedReader br = null; michael@0: try { michael@0: // get the last 200 lines of logcat michael@0: Process proc = Runtime.getRuntime().exec(new String[] { michael@0: "logcat", "-v", "threadtime", "-t", "200", "-d", "*:D" michael@0: }); michael@0: StringBuilder sb = new StringBuilder(); michael@0: br = new BufferedReader(new InputStreamReader(proc.getInputStream())); michael@0: for (String s = br.readLine(); s != null; s = br.readLine()) { michael@0: sb.append(s).append('\n'); michael@0: } michael@0: return sb.toString(); michael@0: } catch (Exception e) { michael@0: return "Unable to get logcat: " + e.toString(); michael@0: } finally { michael@0: if (br != null) { michael@0: try { michael@0: br.close(); michael@0: } catch (Exception e) { michael@0: // ignore michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: private void sendReport(File minidumpFile, Map extras, File extrasFile) { michael@0: Log.i(LOGTAG, "sendReport: " + minidumpFile.getPath()); michael@0: final CheckBox includeURLCheckbox = (CheckBox) findViewById(R.id.include_url); michael@0: michael@0: String spec = extras.get(SERVER_URL_KEY); michael@0: if (spec == null) { michael@0: doFinish(); michael@0: return; michael@0: } michael@0: michael@0: Log.i(LOGTAG, "server url: " + spec); michael@0: try { michael@0: URL url = new URL(spec); michael@0: HttpURLConnection conn = (HttpURLConnection)url.openConnection(); michael@0: conn.setRequestMethod("POST"); michael@0: String boundary = generateBoundary(); michael@0: conn.setDoOutput(true); michael@0: conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); michael@0: conn.setRequestProperty("Content-Encoding", "gzip"); michael@0: michael@0: OutputStream os = new GZIPOutputStream(conn.getOutputStream()); michael@0: for (String key : extras.keySet()) { michael@0: if (key.equals(PAGE_URL_KEY)) { michael@0: if (includeURLCheckbox.isChecked()) michael@0: sendPart(os, boundary, key, extras.get(key)); michael@0: } else if (!key.equals(SERVER_URL_KEY) && !key.equals(NOTES_KEY)) { michael@0: sendPart(os, boundary, key, extras.get(key)); michael@0: } michael@0: } michael@0: michael@0: // Add some extra information to notes so its displayed by michael@0: // crash-stats.mozilla.org. Remove this when bug 607942 is fixed. michael@0: StringBuilder sb = new StringBuilder(); michael@0: sb.append(extras.containsKey(NOTES_KEY) ? extras.get(NOTES_KEY) + "\n" : ""); michael@0: if (AppConstants.MOZ_MIN_CPU_VERSION < 7) { michael@0: sb.append("nothumb Build\n"); michael@0: } michael@0: sb.append(Build.MANUFACTURER).append(' ') michael@0: .append(Build.MODEL).append('\n') michael@0: .append(Build.FINGERPRINT); michael@0: sendPart(os, boundary, NOTES_KEY, sb.toString()); michael@0: michael@0: sendPart(os, boundary, "Min_ARM_Version", Integer.toString(AppConstants.MOZ_MIN_CPU_VERSION)); michael@0: sendPart(os, boundary, "Android_Manufacturer", Build.MANUFACTURER); michael@0: sendPart(os, boundary, "Android_Model", Build.MODEL); michael@0: sendPart(os, boundary, "Android_Board", Build.BOARD); michael@0: sendPart(os, boundary, "Android_Brand", Build.BRAND); michael@0: sendPart(os, boundary, "Android_Device", Build.DEVICE); michael@0: sendPart(os, boundary, "Android_Display", Build.DISPLAY); michael@0: sendPart(os, boundary, "Android_Fingerprint", Build.FINGERPRINT); michael@0: sendPart(os, boundary, "Android_CPU_ABI", Build.CPU_ABI); michael@0: if (Build.VERSION.SDK_INT >= 8) { michael@0: try { michael@0: sendPart(os, boundary, "Android_CPU_ABI2", Build.CPU_ABI2); michael@0: sendPart(os, boundary, "Android_Hardware", Build.HARDWARE); michael@0: } catch (Exception ex) { michael@0: Log.e(LOGTAG, "Exception while sending SDK version 8 keys", ex); michael@0: } michael@0: } michael@0: sendPart(os, boundary, "Android_Version", Build.VERSION.SDK_INT + " (" + Build.VERSION.CODENAME + ")"); michael@0: if (Build.VERSION.SDK_INT >= 16 && includeURLCheckbox.isChecked()) { michael@0: sendPart(os, boundary, "Android_Logcat", readLogcat()); michael@0: } michael@0: michael@0: String comment = ((EditText) findViewById(R.id.comment)).getText().toString(); michael@0: if (!TextUtils.isEmpty(comment)) { michael@0: sendPart(os, boundary, "Comments", comment); michael@0: } michael@0: michael@0: if (((CheckBox) findViewById(R.id.allow_contact)).isChecked()) { michael@0: String email = ((EditText) findViewById(R.id.email)).getText().toString(); michael@0: sendPart(os, boundary, "Email", email); michael@0: } michael@0: michael@0: sendFile(os, boundary, MINI_DUMP_PATH_KEY, minidumpFile); michael@0: os.write(("\r\n--" + boundary + "--\r\n").getBytes()); michael@0: os.flush(); michael@0: os.close(); michael@0: BufferedReader br = new BufferedReader( michael@0: new InputStreamReader(conn.getInputStream())); michael@0: HashMap responseMap = new HashMap(); michael@0: readStringsFromReader(br, responseMap); michael@0: michael@0: if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { michael@0: File submittedDir = new File(getFilesDir(), michael@0: SUBMITTED_SUFFIX); michael@0: submittedDir.mkdirs(); michael@0: minidumpFile.delete(); michael@0: extrasFile.delete(); michael@0: String crashid = responseMap.get("CrashID"); michael@0: File file = new File(submittedDir, crashid + ".txt"); michael@0: FileOutputStream fos = new FileOutputStream(file); michael@0: fos.write("Crash ID: ".getBytes()); michael@0: fos.write(crashid.getBytes()); michael@0: fos.close(); michael@0: } else { michael@0: Log.i(LOGTAG, "Received failure HTTP response code from server: " + conn.getResponseCode()); michael@0: } michael@0: } catch (IOException e) { michael@0: Log.e(LOGTAG, "exception during send: ", e); michael@0: } michael@0: michael@0: doFinish(); michael@0: } michael@0: michael@0: private void doRestart() { michael@0: try { michael@0: String action = "android.intent.action.MAIN"; michael@0: Intent intent = new Intent(action); michael@0: intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, michael@0: AppConstants.BROWSER_INTENT_CLASS_NAME); michael@0: intent.putExtra("didRestart", true); michael@0: Log.i(LOGTAG, intent.toString()); michael@0: startActivity(intent); michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "error while trying to restart", e); michael@0: } michael@0: } michael@0: michael@0: private String unescape(String string) { michael@0: return string.replaceAll("\\\\\\\\", "\\").replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t"); michael@0: } michael@0: }