CrashRecoveryHandler.java revision 6c2e2f34718043f36488b4a94879dc2605aaac49
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 17package com.android.browser; 18 19import android.content.Context; 20import android.content.Intent; 21import android.content.SharedPreferences; 22import android.os.Bundle; 23import android.os.Handler; 24import android.os.Message; 25import android.os.Parcel; 26import android.util.Log; 27 28import java.io.ByteArrayOutputStream; 29import java.io.File; 30import java.io.FileInputStream; 31import java.io.FileNotFoundException; 32import java.io.FileOutputStream; 33import java.io.IOException; 34 35public 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 } catch (FileNotFoundException e) { 204 // No state to recover 205 state = null; 206 } catch (Throwable e) { 207 Log.w(LOGTAG, "Failed to recover state!", e); 208 state = null; 209 } finally { 210 parcel.recycle(); 211 if (fin != null) { 212 try { 213 fin.close(); 214 } catch (IOException e) { } 215 } 216 } 217 if (state != null && !state.isEmpty()) { 218 return state; 219 } 220 return null; 221 } 222 223 public void startRecovery(Intent intent) { 224 synchronized (CrashRecoveryHandler.this) { 225 while (mIsPreloading) { 226 try { 227 CrashRecoveryHandler.this.wait(); 228 } catch (InterruptedException e) {} 229 } 230 } 231 if (!mDidPreload) { 232 mRecoveryState = loadCrashState(); 233 } 234 updateLastRecovered(mRecoveryState != null 235 ? System.currentTimeMillis() : 0); 236 mController.doStart(mRecoveryState, intent); 237 mRecoveryState = null; 238 } 239 240 public void preloadCrashState() { 241 synchronized (CrashRecoveryHandler.this) { 242 if (mIsPreloading) { 243 return; 244 } 245 mIsPreloading = true; 246 } 247 mBackgroundHandler.sendEmptyMessage(MSG_PRELOAD_STATE); 248 } 249 250} 251