mobile/android/base/CrashReporter.java

changeset 0
6474c204b198
     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 +}

mercurial