blob: 822e82a80f4382988973a0b60770c390c39b321a [file] [log] [blame]
/*
* 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();
}
}
}