|
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/. */ |
|
5 |
|
6 package org.mozilla.gecko; |
|
7 |
|
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; |
|
23 |
|
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; |
|
39 |
|
40 public class CrashReporter extends Activity |
|
41 { |
|
42 private static final String LOGTAG = "GeckoCrashReporter"; |
|
43 |
|
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"; |
|
49 |
|
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"; |
|
53 |
|
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"; |
|
58 |
|
59 private Handler mHandler; |
|
60 private ProgressDialog mProgressDialog; |
|
61 private File mPendingMinidumpFile; |
|
62 private File mPendingExtrasFile; |
|
63 private HashMap<String, String> mExtrasStringMap; |
|
64 |
|
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(); |
|
78 |
|
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 } |
|
87 |
|
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 } |
|
98 |
|
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 } |
|
110 |
|
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)); |
|
119 |
|
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); |
|
126 |
|
127 File extrasFile = new File(passedMinidumpPath.replaceAll(".dmp", ".extra")); |
|
128 mPendingExtrasFile = new File(pendingDir, extrasFile.getName()); |
|
129 moveFile(extrasFile, mPendingExtrasFile); |
|
130 |
|
131 mExtrasStringMap = new HashMap<String, String>(); |
|
132 readStringsFromFile(mPendingExtrasFile.getPath(), mExtrasStringMap); |
|
133 |
|
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(); |
|
141 |
|
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); |
|
147 |
|
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, ""); |
|
153 |
|
154 allowContactCheckBox.setChecked(allowContact); |
|
155 includeUrlCheckBox.setChecked(includeUrl); |
|
156 sendReportCheckBox.setChecked(sendReport); |
|
157 emailEditText.setText(contactEmail); |
|
158 |
|
159 sendReportCheckBox.setOnCheckedChangeListener(new CheckBox.OnCheckedChangeListener() { |
|
160 @Override |
|
161 public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) { |
|
162 commentsEditText.setEnabled(isChecked); |
|
163 commentsEditText.requestFocus(); |
|
164 |
|
165 includeUrlCheckBox.setEnabled(isChecked); |
|
166 allowContactCheckBox.setEnabled(isChecked); |
|
167 emailEditText.setEnabled(isChecked && allowContactCheckBox.isChecked()); |
|
168 } |
|
169 }); |
|
170 |
|
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 }); |
|
180 |
|
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 } |
|
194 |
|
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 } |
|
213 |
|
214 private void backgroundSendReport() { |
|
215 final CheckBox sendReportCheckbox = (CheckBox) findViewById(R.id.send_report); |
|
216 if (!sendReportCheckbox.isChecked()) { |
|
217 doFinish(); |
|
218 return; |
|
219 } |
|
220 |
|
221 // Persist settings to avoid redundant user input. |
|
222 savePrefs(); |
|
223 |
|
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 } |
|
232 |
|
233 private void savePrefs() { |
|
234 SharedPreferences.Editor editor = GeckoSharedPrefs.forApp(this).edit(); |
|
235 |
|
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(); |
|
240 |
|
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); |
|
245 |
|
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 } |
|
253 |
|
254 public void onCloseClick(View v) { // bound via crash_reporter.xml |
|
255 backgroundSendReport(); |
|
256 } |
|
257 |
|
258 public void onRestartClick(View v) { // bound via crash_reporter.xml |
|
259 doRestart(); |
|
260 backgroundSendReport(); |
|
261 } |
|
262 |
|
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 } |
|
272 |
|
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 } |
|
286 |
|
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 } |
|
293 |
|
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 } |
|
305 |
|
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 } |
|
317 |
|
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 } |
|
343 |
|
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); |
|
347 |
|
348 String spec = extras.get(SERVER_URL_KEY); |
|
349 if (spec == null) { |
|
350 doFinish(); |
|
351 return; |
|
352 } |
|
353 |
|
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"); |
|
363 |
|
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 } |
|
373 |
|
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()); |
|
385 |
|
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 } |
|
407 |
|
408 String comment = ((EditText) findViewById(R.id.comment)).getText().toString(); |
|
409 if (!TextUtils.isEmpty(comment)) { |
|
410 sendPart(os, boundary, "Comments", comment); |
|
411 } |
|
412 |
|
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 } |
|
417 |
|
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); |
|
426 |
|
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 } |
|
445 |
|
446 doFinish(); |
|
447 } |
|
448 |
|
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 } |
|
462 |
|
463 private String unescape(String string) { |
|
464 return string.replaceAll("\\\\\\\\", "\\").replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t"); |
|
465 } |
|
466 } |