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 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}