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 String RECOVERY_PREFERENCES = "browser_recovery_prefs";
     41     private static final String KEY_LAST_RECOVERED = "last_recovered";
     42     private static final int BUFFER_SIZE = 4096;
     43     private static final long BACKUP_DELAY = 500; // 500ms between writes
     44     /* This is the duration for which we will prompt to restore
     45      * instead of automatically restoring. The first time the browser crashes,
     46      * we will automatically restore. If we then crash again within XX minutes,
     47      * we will prompt instead of automatically restoring.
     48      */
     49     private static final long PROMPT_INTERVAL = 5 * 60 * 1000; // 5 minutes
     50 
     51     private static final int MSG_WRITE_STATE = 1;
     52     private static final int MSG_CLEAR_STATE = 2;
     53     private static final int MSG_PRELOAD_STATE = 3;
     54 
     55     private static CrashRecoveryHandler sInstance;
     56 
     57     private Controller mController;
     58     private Context mContext;
     59     private Handler mForegroundHandler;
     60     private Handler mBackgroundHandler;
     61     private boolean mIsPreloading = false;
     62     private boolean mDidPreload = false;
     63     private Bundle mRecoveryState = null;
     64 
     65     public static CrashRecoveryHandler initialize(Controller controller) {
     66         if (sInstance == null) {
     67             sInstance = new CrashRecoveryHandler(controller);
     68         } else {
     69             sInstance.mController = controller;
     70         }
     71         return sInstance;
     72     }
     73 
     74     public static CrashRecoveryHandler getInstance() {
     75         return sInstance;
     76     }
     77 
     78     private CrashRecoveryHandler(Controller controller) {
     79         mController = controller;
     80         mContext = mController.getActivity().getApplicationContext();
     81         mForegroundHandler = new Handler();
     82         mBackgroundHandler = new Handler(BackgroundHandler.getLooper()) {
     83 
     84             @Override
     85             public void handleMessage(Message msg) {
     86                 switch (msg.what) {
     87                 case MSG_WRITE_STATE:
     88                     if (LOGV_ENABLED) {
     89                         Log.v(LOGTAG, "Saving crash recovery state");
     90                     }
     91                     Parcel p = Parcel.obtain();
     92                     try {
     93                         Bundle state = (Bundle) msg.obj;
     94                         state.writeToParcel(p, 0);
     95                         File stateJournal = new File(mContext.getCacheDir(),
     96                                 STATE_FILE + ".journal");
     97                         FileOutputStream fout = new FileOutputStream(stateJournal);
     98                         fout.write(p.marshall());
     99                         fout.close();
    100                         File stateFile = new File(mContext.getCacheDir(),
    101                                 STATE_FILE);
    102                         if (!stateJournal.renameTo(stateFile)) {
    103                             // Failed to rename, try deleting the existing
    104                             // file and try again
    105                             stateFile.delete();
    106                             stateJournal.renameTo(stateFile);
    107                         }
    108                     } catch (Throwable e) {
    109                         Log.i(LOGTAG, "Failed to save persistent state", e);
    110                     } finally {
    111                         p.recycle();
    112                     }
    113                     break;
    114                 case MSG_CLEAR_STATE:
    115                     if (LOGV_ENABLED) {
    116                         Log.v(LOGTAG, "Clearing crash recovery state");
    117                     }
    118                     File state = new File(mContext.getCacheDir(), STATE_FILE);
    119                     if (state.exists()) {
    120                         state.delete();
    121                     }
    122                     break;
    123                 case MSG_PRELOAD_STATE:
    124                     mRecoveryState = loadCrashState();
    125                     synchronized (CrashRecoveryHandler.this) {
    126                         mIsPreloading = false;
    127                         mDidPreload = true;
    128                         CrashRecoveryHandler.this.notifyAll();
    129                     }
    130                     break;
    131                 }
    132             }
    133         };
    134     }
    135 
    136     public void backupState() {
    137         mForegroundHandler.postDelayed(mCreateState, BACKUP_DELAY);
    138     }
    139 
    140     private Runnable mCreateState = new Runnable() {
    141 
    142         @Override
    143         public void run() {
    144             try {
    145                 final Bundle state = new Bundle();
    146                 mController.onSaveInstanceState(state);
    147                 Message.obtain(mBackgroundHandler, MSG_WRITE_STATE, state)
    148                         .sendToTarget();
    149                 // Remove any queued up saves
    150                 mForegroundHandler.removeCallbacks(mCreateState);
    151             } catch (Throwable t) {
    152                 Log.w(LOGTAG, "Failed to save state", t);
    153                 return;
    154             }
    155         }
    156 
    157     };
    158 
    159     public void clearState() {
    160         mBackgroundHandler.sendEmptyMessage(MSG_CLEAR_STATE);
    161         updateLastRecovered(0);
    162     }
    163 
    164     private boolean shouldRestore() {
    165         SharedPreferences prefs = mContext.getSharedPreferences(
    166                 RECOVERY_PREFERENCES, Context.MODE_PRIVATE);
    167         long lastRecovered = prefs.getLong(KEY_LAST_RECOVERED, 0);
    168         long timeSinceLastRecover = System.currentTimeMillis() - lastRecovered;
    169         if (timeSinceLastRecover > PROMPT_INTERVAL) {
    170             return true;
    171         }
    172         return false;
    173     }
    174 
    175     private void updateLastRecovered(long time) {
    176         SharedPreferences prefs = mContext.getSharedPreferences(
    177                 RECOVERY_PREFERENCES, Context.MODE_PRIVATE);
    178         prefs.edit()
    179             .putLong(KEY_LAST_RECOVERED, time)
    180             .apply();
    181     }
    182 
    183     private Bundle loadCrashState() {
    184         if (!shouldRestore()) {
    185             return null;
    186         }
    187         Bundle state = null;
    188         Parcel parcel = Parcel.obtain();
    189         FileInputStream fin = null;
    190         try {
    191             File stateFile = new File(mContext.getCacheDir(), STATE_FILE);
    192             fin = new FileInputStream(stateFile);
    193             ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
    194             byte[] buffer = new byte[BUFFER_SIZE];
    195             int read;
    196             while ((read = fin.read(buffer)) > 0) {
    197                 dataStream.write(buffer, 0, read);
    198             }
    199             byte[] data = dataStream.toByteArray();
    200             parcel.unmarshall(data, 0, data.length);
    201             parcel.setDataPosition(0);
    202             state = parcel.readBundle();
    203             if (state != null && !state.isEmpty()) {
    204                 return state;
    205             }
    206         } catch (FileNotFoundException e) {
    207             // No state to recover
    208         } catch (Throwable e) {
    209             Log.w(LOGTAG, "Failed to recover state!", e);
    210         } finally {
    211             parcel.recycle();
    212             if (fin != null) {
    213                 try {
    214                     fin.close();
    215                 } catch (IOException e) { }
    216             }
    217         }
    218         return null;
    219     }
    220 
    221     public void startRecovery(Intent intent) {
    222         synchronized (CrashRecoveryHandler.this) {
    223             while (mIsPreloading) {
    224                 try {
    225                     CrashRecoveryHandler.this.wait();
    226                 } catch (InterruptedException e) {}
    227             }
    228         }
    229         if (!mDidPreload) {
    230             mRecoveryState = loadCrashState();
    231         }
    232         updateLastRecovered(mRecoveryState != null
    233                 ? System.currentTimeMillis() : 0);
    234         mController.doStart(mRecoveryState, intent, true);
    235         mRecoveryState = null;
    236     }
    237 
    238     public void preloadCrashState() {
    239         synchronized (CrashRecoveryHandler.this) {
    240             if (mIsPreloading) {
    241                 return;
    242             }
    243             mIsPreloading = true;
    244         }
    245         mBackgroundHandler.sendEmptyMessage(MSG_PRELOAD_STATE);
    246     }
    247 
    248 }
    249