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