Home | History | Annotate | Download | only in browser
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.browser;
     18 
     19 import android.content.Context;
     20 import android.content.Intent;
     21 import android.content.SharedPreferences;
     22 import android.os.Bundle;
     23 import android.os.Handler;
     24 import android.os.Message;
     25 import android.os.Parcel;
     26 import android.util.Log;
     27 
     28 import java.io.ByteArrayOutputStream;
     29 import java.io.File;
     30 import java.io.FileInputStream;
     31 import java.io.FileNotFoundException;
     32 import java.io.FileOutputStream;
     33 import java.io.IOException;
     34 
     35 public class CrashRecoveryHandler {
     36 
     37     private static final boolean LOGV_ENABLED = Browser.LOGV_ENABLED;
     38     private static final String LOGTAG = "BrowserCrashRecovery";
     39     private static final String STATE_FILE = "browser_state.parcel";
     40     private static final int BUFFER_SIZE = 4096;
     41     private static final long BACKUP_DELAY = 500; // 500ms between writes
     42     /* This is the duration for which we will prompt to restore
     43      * instead of automatically restoring. The first time the browser crashes,
     44      * we will automatically restore. If we then crash again within XX minutes,
     45      * we will prompt instead of automatically restoring.
     46      */
     47     private static final long PROMPT_INTERVAL = 5 * 60 * 1000; // 5 minutes
     48 
     49     private static final int MSG_WRITE_STATE = 1;
     50     private static final int MSG_CLEAR_STATE = 2;
     51     private static final int MSG_PRELOAD_STATE = 3;
     52 
     53     private static CrashRecoveryHandler sInstance;
     54 
     55     private Controller mController;
     56     private Context mContext;
     57     private Handler mForegroundHandler;
     58     private Handler mBackgroundHandler;
     59     private boolean mIsPreloading = false;
     60     private boolean mDidPreload = false;
     61     private Bundle mRecoveryState = null;
     62 
     63     public static CrashRecoveryHandler initialize(Controller controller) {
     64         if (sInstance == null) {
     65             sInstance = new CrashRecoveryHandler(controller);
     66         } else {
     67             sInstance.mController = controller;
     68         }
     69         return sInstance;
     70     }
     71 
     72     public static CrashRecoveryHandler getInstance() {
     73         return sInstance;
     74     }
     75 
     76     private CrashRecoveryHandler(Controller controller) {
     77         mController = controller;
     78         mContext = mController.getActivity().getApplicationContext();
     79         mForegroundHandler = new Handler();
     80         mBackgroundHandler = new Handler(BackgroundHandler.getLooper()) {
     81 
     82             @Override
     83             public void handleMessage(Message msg) {
     84                 switch (msg.what) {
     85                 case MSG_WRITE_STATE:
     86                     Bundle saveState = (Bundle) msg.obj;
     87                     writeState(saveState);
     88                     break;
     89                 case MSG_CLEAR_STATE:
     90                     if (LOGV_ENABLED) {
     91                         Log.v(LOGTAG, "Clearing crash recovery state");
     92                     }
     93                     File state = new File(mContext.getCacheDir(), STATE_FILE);
     94                     if (state.exists()) {
     95                         state.delete();
     96                     }
     97                     break;
     98                 case MSG_PRELOAD_STATE:
     99                     mRecoveryState = loadCrashState();
    100                     synchronized (CrashRecoveryHandler.this) {
    101                         mIsPreloading = false;
    102                         mDidPreload = true;
    103                         CrashRecoveryHandler.this.notifyAll();
    104                     }
    105                     break;
    106                 }
    107             }
    108         };
    109     }
    110 
    111     public void backupState() {
    112         mForegroundHandler.postDelayed(mCreateState, BACKUP_DELAY);
    113     }
    114 
    115     private Runnable mCreateState = new Runnable() {
    116 
    117         @Override
    118         public void run() {
    119             try {
    120                 final Bundle state = mController.createSaveState();
    121                 Message.obtain(mBackgroundHandler, MSG_WRITE_STATE, state)
    122                         .sendToTarget();
    123                 // Remove any queued up saves
    124                 mForegroundHandler.removeCallbacks(mCreateState);
    125             } catch (Throwable t) {
    126                 Log.w(LOGTAG, "Failed to save state", t);
    127                 return;
    128             }
    129         }
    130 
    131     };
    132 
    133     public void clearState() {
    134         mBackgroundHandler.sendEmptyMessage(MSG_CLEAR_STATE);
    135         updateLastRecovered(0);
    136     }
    137 
    138     private boolean shouldRestore() {
    139         BrowserSettings browserSettings = BrowserSettings.getInstance();
    140         long lastRecovered = browserSettings.getLastRecovered();
    141         long timeSinceLastRecover = System.currentTimeMillis() - lastRecovered;
    142         return (timeSinceLastRecover > PROMPT_INTERVAL)
    143                 || browserSettings.wasLastRunPaused();
    144     }
    145 
    146     private void updateLastRecovered(long time) {
    147         BrowserSettings browserSettings = BrowserSettings.getInstance();
    148         browserSettings.setLastRecovered(time);
    149     }
    150 
    151     synchronized private Bundle loadCrashState() {
    152         if (!shouldRestore()) {
    153             return null;
    154         }
    155         BrowserSettings browserSettings = BrowserSettings.getInstance();
    156         browserSettings.setLastRunPaused(false);
    157         Bundle state = null;
    158         Parcel parcel = Parcel.obtain();
    159         FileInputStream fin = null;
    160         try {
    161             File stateFile = new File(mContext.getCacheDir(), STATE_FILE);
    162             fin = new FileInputStream(stateFile);
    163             ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
    164             byte[] buffer = new byte[BUFFER_SIZE];
    165             int read;
    166             while ((read = fin.read(buffer)) > 0) {
    167                 dataStream.write(buffer, 0, read);
    168             }
    169             byte[] data = dataStream.toByteArray();
    170             parcel.unmarshall(data, 0, data.length);
    171             parcel.setDataPosition(0);
    172             state = parcel.readBundle();
    173             if (state != null && !state.isEmpty()) {
    174                 return state;
    175             }
    176         } catch (FileNotFoundException e) {
    177             // No state to recover
    178         } catch (Throwable e) {
    179             Log.w(LOGTAG, "Failed to recover state!", e);
    180         } finally {
    181             parcel.recycle();
    182             if (fin != null) {
    183                 try {
    184                     fin.close();
    185                 } catch (IOException e) { }
    186             }
    187         }
    188         return null;
    189     }
    190 
    191     public void startRecovery(Intent intent) {
    192         synchronized (CrashRecoveryHandler.this) {
    193             while (mIsPreloading) {
    194                 try {
    195                     CrashRecoveryHandler.this.wait();
    196                 } catch (InterruptedException e) {}
    197             }
    198         }
    199         if (!mDidPreload) {
    200             mRecoveryState = loadCrashState();
    201         }
    202         updateLastRecovered(mRecoveryState != null
    203                 ? System.currentTimeMillis() : 0);
    204         mController.doStart(mRecoveryState, intent);
    205         mRecoveryState = null;
    206     }
    207 
    208     public void preloadCrashState() {
    209         synchronized (CrashRecoveryHandler.this) {
    210             if (mIsPreloading) {
    211                 return;
    212             }
    213             mIsPreloading = true;
    214         }
    215         mBackgroundHandler.sendEmptyMessage(MSG_PRELOAD_STATE);
    216     }
    217 
    218     /**
    219      * Writes the crash recovery state to a file synchronously.
    220      * Errors are swallowed, but logged.
    221      * @param state The state to write out
    222      */
    223     synchronized void writeState(Bundle state) {
    224         if (LOGV_ENABLED) {
    225             Log.v(LOGTAG, "Saving crash recovery state");
    226         }
    227         Parcel p = Parcel.obtain();
    228         try {
    229             state.writeToParcel(p, 0);
    230             File stateJournal = new File(mContext.getCacheDir(),
    231                     STATE_FILE + ".journal");
    232             FileOutputStream fout = new FileOutputStream(stateJournal);
    233             fout.write(p.marshall());
    234             fout.close();
    235             File stateFile = new File(mContext.getCacheDir(),
    236                     STATE_FILE);
    237             if (!stateJournal.renameTo(stateFile)) {
    238                 // Failed to rename, try deleting the existing
    239                 // file and try again
    240                 stateFile.delete();
    241                 stateJournal.renameTo(stateFile);
    242             }
    243         } catch (Throwable e) {
    244             Log.i(LOGTAG, "Failed to save persistent state", e);
    245         } finally {
    246             p.recycle();
    247         }
    248     }
    249 }