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