| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.browser; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.os.Parcel; |
| import android.util.Log; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| |
| public class CrashRecoveryHandler { |
| |
| private static final boolean LOGV_ENABLED = Browser.LOGV_ENABLED; |
| private static final String LOGTAG = "BrowserCrashRecovery"; |
| private static final String STATE_FILE = "browser_state.parcel"; |
| private static final int BUFFER_SIZE = 4096; |
| private static final long BACKUP_DELAY = 500; // 500ms between writes |
| /* This is the duration for which we will prompt to restore |
| * instead of automatically restoring. The first time the browser crashes, |
| * we will automatically restore. If we then crash again within XX minutes, |
| * we will prompt instead of automatically restoring. |
| */ |
| private static final long PROMPT_INTERVAL = 5 * 60 * 1000; // 5 minutes |
| |
| private static final int MSG_WRITE_STATE = 1; |
| private static final int MSG_CLEAR_STATE = 2; |
| private static final int MSG_PRELOAD_STATE = 3; |
| |
| private static CrashRecoveryHandler sInstance; |
| |
| private Controller mController; |
| private Context mContext; |
| private Handler mForegroundHandler; |
| private Handler mBackgroundHandler; |
| private boolean mIsPreloading = false; |
| private boolean mDidPreload = false; |
| private Bundle mRecoveryState = null; |
| |
| public static CrashRecoveryHandler initialize(Controller controller) { |
| if (sInstance == null) { |
| sInstance = new CrashRecoveryHandler(controller); |
| } else { |
| sInstance.mController = controller; |
| } |
| return sInstance; |
| } |
| |
| public static CrashRecoveryHandler getInstance() { |
| return sInstance; |
| } |
| |
| private CrashRecoveryHandler(Controller controller) { |
| mController = controller; |
| mContext = mController.getActivity().getApplicationContext(); |
| mForegroundHandler = new Handler(); |
| mBackgroundHandler = new Handler(BackgroundHandler.getLooper()) { |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_WRITE_STATE: |
| Bundle saveState = (Bundle) msg.obj; |
| writeState(saveState); |
| break; |
| case MSG_CLEAR_STATE: |
| if (LOGV_ENABLED) { |
| Log.v(LOGTAG, "Clearing crash recovery state"); |
| } |
| File state = new File(mContext.getCacheDir(), STATE_FILE); |
| if (state.exists()) { |
| state.delete(); |
| } |
| break; |
| case MSG_PRELOAD_STATE: |
| mRecoveryState = loadCrashState(); |
| synchronized (CrashRecoveryHandler.this) { |
| mIsPreloading = false; |
| mDidPreload = true; |
| CrashRecoveryHandler.this.notifyAll(); |
| } |
| break; |
| } |
| } |
| }; |
| } |
| |
| public void backupState() { |
| mForegroundHandler.postDelayed(mCreateState, BACKUP_DELAY); |
| } |
| |
| private Runnable mCreateState = new Runnable() { |
| |
| @Override |
| public void run() { |
| try { |
| final Bundle state = mController.createSaveState(); |
| Message.obtain(mBackgroundHandler, MSG_WRITE_STATE, state) |
| .sendToTarget(); |
| // Remove any queued up saves |
| mForegroundHandler.removeCallbacks(mCreateState); |
| } catch (Throwable t) { |
| Log.w(LOGTAG, "Failed to save state", t); |
| return; |
| } |
| } |
| |
| }; |
| |
| public void clearState() { |
| mBackgroundHandler.sendEmptyMessage(MSG_CLEAR_STATE); |
| updateLastRecovered(0); |
| } |
| |
| private boolean shouldRestore() { |
| BrowserSettings browserSettings = BrowserSettings.getInstance(); |
| long lastRecovered = browserSettings.getLastRecovered(); |
| long timeSinceLastRecover = System.currentTimeMillis() - lastRecovered; |
| return (timeSinceLastRecover > PROMPT_INTERVAL) |
| || browserSettings.wasLastRunPaused(); |
| } |
| |
| private void updateLastRecovered(long time) { |
| BrowserSettings browserSettings = BrowserSettings.getInstance(); |
| browserSettings.setLastRecovered(time); |
| } |
| |
| synchronized private Bundle loadCrashState() { |
| if (!shouldRestore()) { |
| return null; |
| } |
| BrowserSettings browserSettings = BrowserSettings.getInstance(); |
| browserSettings.setLastRunPaused(false); |
| Bundle state = null; |
| Parcel parcel = Parcel.obtain(); |
| FileInputStream fin = null; |
| try { |
| File stateFile = new File(mContext.getCacheDir(), STATE_FILE); |
| fin = new FileInputStream(stateFile); |
| ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); |
| byte[] buffer = new byte[BUFFER_SIZE]; |
| int read; |
| while ((read = fin.read(buffer)) > 0) { |
| dataStream.write(buffer, 0, read); |
| } |
| byte[] data = dataStream.toByteArray(); |
| parcel.unmarshall(data, 0, data.length); |
| parcel.setDataPosition(0); |
| state = parcel.readBundle(); |
| if (state != null && !state.isEmpty()) { |
| return state; |
| } |
| } catch (FileNotFoundException e) { |
| // No state to recover |
| } catch (Throwable e) { |
| Log.w(LOGTAG, "Failed to recover state!", e); |
| } finally { |
| parcel.recycle(); |
| if (fin != null) { |
| try { |
| fin.close(); |
| } catch (IOException e) { } |
| } |
| } |
| return null; |
| } |
| |
| public void startRecovery(Intent intent) { |
| synchronized (CrashRecoveryHandler.this) { |
| while (mIsPreloading) { |
| try { |
| CrashRecoveryHandler.this.wait(); |
| } catch (InterruptedException e) {} |
| } |
| } |
| if (!mDidPreload) { |
| mRecoveryState = loadCrashState(); |
| } |
| updateLastRecovered(mRecoveryState != null |
| ? System.currentTimeMillis() : 0); |
| mController.doStart(mRecoveryState, intent); |
| mRecoveryState = null; |
| } |
| |
| public void preloadCrashState() { |
| synchronized (CrashRecoveryHandler.this) { |
| if (mIsPreloading) { |
| return; |
| } |
| mIsPreloading = true; |
| } |
| mBackgroundHandler.sendEmptyMessage(MSG_PRELOAD_STATE); |
| } |
| |
| /** |
| * Writes the crash recovery state to a file synchronously. |
| * Errors are swallowed, but logged. |
| * @param state The state to write out |
| */ |
| synchronized void writeState(Bundle state) { |
| if (LOGV_ENABLED) { |
| Log.v(LOGTAG, "Saving crash recovery state"); |
| } |
| Parcel p = Parcel.obtain(); |
| try { |
| state.writeToParcel(p, 0); |
| File stateJournal = new File(mContext.getCacheDir(), |
| STATE_FILE + ".journal"); |
| FileOutputStream fout = new FileOutputStream(stateJournal); |
| fout.write(p.marshall()); |
| fout.close(); |
| File stateFile = new File(mContext.getCacheDir(), |
| STATE_FILE); |
| if (!stateJournal.renameTo(stateFile)) { |
| // Failed to rename, try deleting the existing |
| // file and try again |
| stateFile.delete(); |
| stateJournal.renameTo(stateFile); |
| } |
| } catch (Throwable e) { |
| Log.i(LOGTAG, "Failed to save persistent state", e); |
| } finally { |
| p.recycle(); |
| } |
| } |
| } |