Thu, 22 Jan 2015 13:21:57 +0100
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 }