| /* |
| * Copyright (c) 2014, The Linux Foundation. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * * Neither the name of The Linux Foundation nor the names of its |
| * contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR |
| * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE |
| * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN |
| * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package com.android.browser; |
| |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.content.Context; |
| import android.os.Build; |
| import android.os.Build.VERSION; |
| import android.os.SystemClock; |
| import android.net.http.AndroidHttpClient; |
| import android.util.Log; |
| import android.os.FileObserver; |
| import android.os.Handler; |
| |
| import org.codeaurora.swe.BrowserCommandLine; |
| |
| import org.apache.http.HttpEntity; |
| import org.apache.http.HttpResponse; |
| import org.apache.http.client.methods.HttpPost; |
| import org.apache.http.entity.StringEntity; |
| import org.apache.http.client.ClientProtocolException; |
| import org.apache.http.client.HttpClient; |
| import org.apache.http.entity.InputStreamEntity; |
| import org.apache.http.impl.client.DefaultHttpClient; |
| import org.apache.http.entity.AbstractHttpEntity; |
| import org.apache.http.entity.ByteArrayEntity; |
| |
| |
| import org.json.JSONArray; |
| import org.json.JSONObject; |
| import org.json.JSONException; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.BufferedReader; |
| import java.io.InputStreamReader; |
| import java.io.ByteArrayOutputStream; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.lang.Integer; |
| import java.lang.StringBuilder; |
| import java.lang.System; |
| import java.lang.Thread.UncaughtExceptionHandler; |
| import java.util.Calendar; |
| import java.util.zip.GZIPOutputStream; |
| |
| public class CrashLogExceptionHandler implements Thread.UncaughtExceptionHandler { |
| |
| private static final String CRASH_LOG_FILE = "crash.log"; |
| private static final String CRASH_LOG_MAX_FILE_SIZE_CMD = "crash-log-max-file-size"; |
| private static final String CRASH_REPORT_DIR = "Crash Reports"; |
| |
| private final static String LOGTAG = "CrashLog"; |
| |
| private Context mAppContext = null; |
| |
| private UncaughtExceptionHandler mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); |
| |
| private String mLogServer = new String(); |
| |
| private boolean mOverrideHandler = false; |
| |
| private int mMaxLogFileSize = 1024 * 1024; |
| // To avoid increasing startup time an upload delay is used |
| private static final int UPLOAD_DELAY = 3000; |
| |
| private static FileObserver crashObserver; |
| |
| private final Handler mCrashReportHandler = new Handler(); |
| |
| public CrashLogExceptionHandler(Context ctx) { |
| mAppContext = ctx; |
| if (BrowserCommandLine.hasSwitch(BrowserSwitches.CRASH_LOG_SERVER_CMD)) { |
| initNativeReporter(ctx); |
| mLogServer = BrowserCommandLine.getSwitchValue(BrowserSwitches.CRASH_LOG_SERVER_CMD); |
| if (mLogServer != null) { |
| uploadPastCrashLog(); |
| mOverrideHandler = true; |
| } |
| } |
| |
| try { |
| int size = Integer.parseInt( |
| BrowserCommandLine.getSwitchValue(CRASH_LOG_MAX_FILE_SIZE_CMD, |
| Integer.toString(mMaxLogFileSize))); |
| mMaxLogFileSize = size; |
| } catch (NumberFormatException nfe) { |
| Log.e(LOGTAG,"Max log file size is not configured properly. Using default: " |
| + mMaxLogFileSize); |
| } |
| |
| } |
| |
| private void initNativeReporter(Context ctx){ |
| final File crashReports = new File(ctx.getCacheDir(),CRASH_REPORT_DIR); |
| // On fresh installs, make the directory before registering an observer |
| if (!crashReports.isDirectory()) { |
| crashReports.mkdir(); |
| } |
| // Implement FileObserver for crashReports that don't bring the system down |
| crashObserver = new FileObserver(crashReports.getAbsolutePath()) { |
| @Override |
| public void onEvent(int event, String path){ |
| if ((event == FileObserver.CREATE) || (event == FileObserver.MOVED_TO)){ |
| Log.w(LOGTAG, "A crash report was generated"); |
| checkNativeCrash(crashReports); |
| } |
| } |
| }; |
| // Native Crash reporting if commandline is set |
| mCrashReportHandler.postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| checkNativeCrash(crashReports); |
| } |
| }, UPLOAD_DELAY); |
| // start watching the crash reports folder |
| crashObserver.startWatching(); |
| } |
| |
| private void saveCrashLog(String crashLog) { |
| // Check if log file exists and it's current size |
| try { |
| File file = new File(mAppContext.getFilesDir(), CRASH_LOG_FILE); |
| if (file.exists()) { |
| if (file.length() > mMaxLogFileSize) { |
| Log.e(LOGTAG,"CRASH Log file size(" + file.length() |
| + ") exceeded max log file size(" |
| + mMaxLogFileSize + ")"); |
| return; |
| } |
| } |
| } catch (NullPointerException npe) { |
| Log.e(LOGTAG,"Exception while checking file size: " + npe); |
| } |
| |
| FileOutputStream crashLogFile = null; |
| try { |
| crashLogFile = mAppContext.openFileOutput(CRASH_LOG_FILE, Context.MODE_APPEND); |
| crashLogFile.write(crashLog.getBytes()); |
| } catch(IOException ioe) { |
| Log.e(LOGTAG,"Exception while writing file: " + ioe); |
| } finally { |
| if (crashLogFile != null) { |
| try { |
| crashLogFile.close(); |
| } catch (IOException ignore) { |
| } |
| } |
| } |
| } |
| |
| private void uploadPastCrashLog() { |
| FileInputStream crashLogFile = null; |
| BufferedReader reader = null; |
| try { |
| crashLogFile = mAppContext.openFileInput(CRASH_LOG_FILE); |
| |
| reader = new BufferedReader(new InputStreamReader(crashLogFile)); |
| StringBuilder crashLog = new StringBuilder(); |
| String line = reader.readLine(); |
| if (line != null) { |
| crashLog.append(line); |
| } |
| |
| // Typically there's only one line (JSON string) in the crash |
| // log file. This loop would not be executed. |
| while ((line = reader.readLine()) != null) { |
| crashLog.append("\n").append(line); |
| } |
| |
| uploadCrashLog(crashLog.toString(), UPLOAD_DELAY); |
| } catch(FileNotFoundException fnfe) { |
| Log.v(LOGTAG,"No previous crash found"); |
| } catch(IOException ioe) { |
| Log.e(LOGTAG,"Exception while reading crash file: " + ioe); |
| } finally { |
| if (crashLogFile != null) { |
| try { |
| crashLogFile.close(); |
| } catch (IOException ignore) { |
| } |
| } |
| if (reader != null) { |
| try { |
| reader.close(); |
| } catch (IOException ignore) { |
| } |
| } |
| } |
| } |
| |
| private void uploadCrashLog(String data, int after) { |
| final String crashLog = data; |
| final int waitFor = after; |
| new Thread(new Runnable() { |
| public void run(){ |
| try { |
| SystemClock.sleep(waitFor); |
| AndroidHttpClient httpClient = AndroidHttpClient.newInstance("Android");; |
| HttpPost httpPost = new HttpPost(mLogServer); |
| HttpEntity se = new StringEntity(crashLog); |
| httpPost.setEntity(se); |
| HttpResponse response = httpClient.execute(httpPost); |
| |
| File crashLogFile = new File(mAppContext.getFilesDir(), |
| CRASH_LOG_FILE); |
| if (crashLogFile != null) { |
| crashLogFile.delete(); |
| } else { |
| Log.e(LOGTAG,"crash log file could not be opened for deletion"); |
| } |
| } catch (ClientProtocolException pe) { |
| Log.e(LOGTAG,"Exception while sending http post: " + pe); |
| } catch (IOException ioe1) { |
| Log.e(LOGTAG,"Exception while sending http post: " + ioe1); |
| } |
| } |
| }).start(); |
| } |
| |
| public void uncaughtException(Thread t, Throwable e) { |
| if (!mOverrideHandler) { |
| mDefaultHandler.uncaughtException(t, e); |
| return; |
| } |
| |
| String crashLog = new String(); |
| |
| try { |
| Calendar calendar = Calendar.getInstance(); |
| JSONObject jsonBackTraceObj = new JSONObject(); |
| String date = calendar.getTime().toString(); |
| String aboutSWE = mAppContext.getResources().getString(R.string.about_text); |
| String sweVer = findValueFromAboutText(aboutSWE, "Version: "); |
| String sweHash = findValueFromAboutText(aboutSWE, "Hash: "); |
| String sweBuildDate = findValueFromAboutText(aboutSWE, "Built: "); |
| |
| jsonBackTraceObj.put("date", date); |
| jsonBackTraceObj.put("android-model", android.os.Build.MODEL); |
| jsonBackTraceObj.put("android-device", android.os.Build.DEVICE); |
| jsonBackTraceObj.put("android-ver", android.os.Build.VERSION.RELEASE); |
| jsonBackTraceObj.put("browser-ver", sweVer); |
| jsonBackTraceObj.put("browser-hash", sweHash); |
| jsonBackTraceObj.put("browser-build-date", sweBuildDate); |
| jsonBackTraceObj.put("thread", t.toString()); |
| jsonBackTraceObj.put("format", "crashmon-1"); |
| jsonBackTraceObj.put("monkey-test", ActivityManager.isUserAMonkey()); |
| |
| JSONArray jsonStackArray = new JSONArray(); |
| |
| Throwable throwable = e; |
| String stackTag = "Exception thrown while running"; |
| while (throwable != null) { |
| JSONObject jsonStackObj = new JSONObject(); |
| StackTraceElement[] arr = throwable.getStackTrace(); |
| JSONArray jsonStack = new JSONArray(arr); |
| |
| jsonStackObj.put("cause", throwable.getCause()); |
| jsonStackObj.put("message", throwable.getMessage()); |
| jsonStackObj.put(stackTag, jsonStack); |
| |
| jsonStackArray.put(jsonStackObj); |
| |
| stackTag = "stack"; |
| throwable = throwable.getCause(); |
| } |
| jsonBackTraceObj.put("exceptions", jsonStackArray); |
| |
| JSONObject jsonMainObj = new JSONObject(); |
| jsonMainObj.put("backtraces", jsonBackTraceObj); |
| |
| Log.e(LOGTAG, "Exception: " + jsonMainObj.toString(4)); |
| crashLog = jsonMainObj.toString(); |
| |
| } catch (JSONException je) { |
| Log.e(LOGTAG, "Failed in JSON encoding: " + je); |
| } |
| |
| saveCrashLog(crashLog); |
| |
| uploadCrashLog(crashLog, 0); |
| |
| mDefaultHandler.uncaughtException(t, e); |
| } |
| |
| private String findValueFromAboutText(String aboutText, String aboutKey) { |
| int start = aboutText.indexOf(aboutKey); |
| int end = aboutText.indexOf("\n", start); |
| String value = ""; |
| |
| if (start != -1 && end != -1) { |
| start += aboutKey.length(); |
| value = aboutText.substring(start, end); |
| } |
| return value; |
| } |
| |
| private void checkNativeCrash(final File crashReportsDir) { |
| // Search cache/Crash Reports/ for any crashes |
| if (crashReportsDir.exists()) { |
| new Thread(new Runnable() { |
| @Override |
| public void run() { |
| for (File f : crashReportsDir.listFiles()) { |
| uploadNativeCrashReport(f); |
| } |
| } |
| }).start(); |
| } |
| } |
| |
| private void uploadNativeCrashReport(final File report) { |
| Log.w(LOGTAG, "Preparing Crash Report for upload " + report.getName()); |
| // get server url from commandline |
| String server = BrowserCommandLine.getSwitchValue(BrowserSwitches.CRASH_LOG_SERVER_CMD); |
| try { |
| HttpClient httpClient = new DefaultHttpClient(); |
| HttpPost httpPost = new HttpPost(server); |
| |
| // Compress the data |
| ByteArrayOutputStream arrayStream = new ByteArrayOutputStream(); |
| OutputStream gzipData = new GZIPOutputStream(arrayStream); |
| InputStream inputStream = new FileInputStream(report); |
| long length = report.length(); |
| byte[] data = new byte[(int)length]; |
| |
| // Read in the bytes |
| int offset = 0; |
| int numRead = 0; |
| while (offset < data.length |
| && (numRead=inputStream.read(data, offset, data.length-offset)) >= 0) { |
| offset += numRead; |
| } |
| gzipData.write(data); |
| gzipData.close(); |
| |
| AbstractHttpEntity entity = new ByteArrayEntity(arrayStream.toByteArray()); |
| |
| // Send the report as a compressed Binary |
| entity.setContentType("binary/octet-stream"); |
| entity.setContentEncoding("gzip"); |
| entity.setChunked(false); |
| httpPost.setEntity(entity); |
| |
| HttpResponse response = httpClient.execute(httpPost); |
| int status = response.getStatusLine().getStatusCode(); |
| if (status == 200) |
| report.delete(); |
| else Log.w(LOGTAG, "Upload Failure. Will try again next time- " + status); |
| |
| } catch (Exception e) { |
| Log.w(LOGTAG, "Crash Report failed to upload, will try again next time " + e); |
| } |
| } |
| |
| } |