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 }