// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.tabmodel.document;
import android.content.Context;
import android.os.AsyncTask;
import android.util.SparseArray;
import com.google.protobuf.nano.MessageNano;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StreamUtil;
import org.chromium.chrome.browser.TabState;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabPersister;
import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel.Entry;
import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelInfo.DocumentEntry;
import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelInfo.DocumentList;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
/**
* Contains functions for interacting with the file system.
*/
public class StorageDelegate extends TabPersister {
private static final String TAG = "StorageDelegate";
/** Filename to use for the DocumentTabModel that stores regular tabs. */
private static final String REGULAR_FILE_NAME = "chrome_document_activity.store";
/** Directory to store TabState files in. */
private static final String STATE_DIRECTORY = "ChromeDocumentActivity";
/** The buffer size to use when reading the DocumentTabModel file, set to 4k bytes. */
private static final int BUF_SIZE = 0x1000;
/** Cached base state directory to prevent main-thread filesystem access in getStateDirectory().
*/
private static AsyncTask<Void, Void, File> sBaseStateDirectoryFetchTask;
public StorageDelegate() {
// Warm up the state directory to prevent it from using filesystem on main thread in the
// future
preloadStateDirectory();
}
/**
* Reads the file containing the minimum info required to restore the state of the
* {@link DocumentTabModel}.
* @param encrypted Whether or not the file corresponds to an Incognito TabModel.
* @return Byte buffer containing the task file's data, or null if it wasn't read.
*/
protected byte[] readMetadataFileBytes(boolean encrypted) {
// Incognito mode doesn't save its state out.
if (encrypted) return null;
// Read in the file.
byte[] bytes = null;
FileInputStream streamIn = null;
try {
String filename = getFilename(encrypted);
streamIn = ContextUtils.getApplicationContext().openFileInput(filename);
// Read the file from the file into the out stream.
ByteArrayOutputStream streamOut = new ByteArrayOutputStream();
byte[] buf = new byte[BUF_SIZE];
int r;
while ((r = streamIn.read(buf)) != -1) {
streamOut.write(buf, 0, r);
}
bytes = streamOut.toByteArray();
} catch (FileNotFoundException e) {
Log.e(TAG, "DocumentTabModel file not found.");
} catch (IOException e) {
Log.e(TAG, "I/O exception", e);
} finally {
StreamUtil.closeQuietly(streamIn);
}
return bytes;
}
/**
* Writes the file containing the minimum info required to restore the state of the
* {@link DocumentTabModel}.
* @param encrypted Whether the TabModel is incognito.
* @param bytes Byte buffer containing the tab's data.
*/
public void writeTaskFileBytes(boolean encrypted, byte[] bytes) {
// Incognito mode doesn't save its state out.
if (encrypted) return;
FileOutputStream outputStream = null;
try {
outputStream = ContextUtils.getApplicationContext().openFileOutput(
getFilename(encrypted), Context.MODE_PRIVATE);
outputStream.write(bytes);
} catch (FileNotFoundException e) {
Log.e(TAG, "DocumentTabModel file not found", e);
} catch (IOException e) {
Log.e(TAG, "I/O exception", e);
} finally {
StreamUtil.closeQuietly(outputStream);
}
}
private void preloadStateDirectory() {
if (sBaseStateDirectoryFetchTask != null) return;
sBaseStateDirectoryFetchTask = new AsyncTask<Void, Void, File>() {
@Override
protected File doInBackground(Void... params) {
return ContextUtils.getApplicationContext().getDir(
STATE_DIRECTORY, Context.MODE_PRIVATE);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/** @return The directory that stores the TabState files. */
@Override
public File getStateDirectory() {
try {
return sBaseStateDirectoryFetchTask.get();
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
// If the AsyncTask failed for some reason, we have no choice but to fall back to
// main-thread disk access.
return ContextUtils.getApplicationContext().getDir(STATE_DIRECTORY, Context.MODE_PRIVATE);
}
/**
* Restores the TabState with the given ID.
* @param tabId ID of the Tab.
* @return TabState for the Tab.
*/
public TabState restoreTabState(int tabId, boolean encrypted) {
return TabState.restoreTabState(getTabStateFile(tabId, encrypted), encrypted);
}
/**
* Return the filename of the persisted TabModel state.
* @param encrypted Whether or not the state belongs to an IncognitoDocumentTabModel.
* @return String pointing at the TabModel's persisted state.
*/
private String getFilename(boolean encrypted) {
return encrypted ? null : REGULAR_FILE_NAME;
}
/**
* Update tab entries based on metadata.
* @param metadataBytes Metadata from last time Chrome was alive.
* @param entryMap Map to fill with {@link DocumentTabModel.Entry}s about Tabs.
* @param recentlyClosedTabIdList List to fill with IDs of recently closed tabs.
*/
private void updateTabEntriesFromMetadata(byte[] metadataBytes, SparseArray<Entry> entryMap,
List<Integer> recentlyClosedTabIdList) {
if (metadataBytes != null) {
DocumentList list = null;
try {
list = MessageNano.mergeFrom(new DocumentList(), metadataBytes);
} catch (IOException e) {
Log.e(TAG, "I/O exception", e);
}
if (list == null) return;
for (int i = 0; i < list.entries.length; i++) {
DocumentEntry savedEntry = list.entries[i];
int tabId = savedEntry.tabId;
// If the tab ID isn't in the list, it must have been closed after Chrome died.
if (entryMap.indexOfKey(tabId) < 0) {
recentlyClosedTabIdList.add(tabId);
continue;
}
// Restore information about the Tab.
entryMap.get(tabId).canGoBack = savedEntry.canGoBack;
}
}
}
/**
* Constructs the DocumentTabModel's entries by combining the tasks currently listed in Android
* with information stored out in a metadata file.
* @param isIncognito Whether to build an Incognito tab list.
* @param activityDelegate Interacts with the Activitymanager.
* @param entryMap Map to fill with {@link DocumentTabModel.Entry}s about Tabs.
* @param tabIdList List to fill with live Tab IDs.
* @param recentlyClosedTabIdList List to fill with IDs of recently closed tabs.
*/
public void restoreTabEntries(final boolean isIncognito, ActivityDelegate activityDelegate,
final SparseArray<Entry> entryMap, List<Integer> tabIdList,
final List<Integer> recentlyClosedTabIdList) {
assert entryMap.size() == 0;
assert tabIdList.isEmpty();
assert recentlyClosedTabIdList.isEmpty();
// Run through Android's Overview to see what Chrome tabs are still listed.
List<Entry> entries = activityDelegate.getTasksFromRecents(isIncognito);
for (Entry entry : entries) {
int tabId = entry.tabId;
if (tabId != Tab.INVALID_TAB_ID) {
if (!tabIdList.contains(tabId)) tabIdList.add(tabId);
entryMap.put(tabId, entry);
}
// Prevent these tabs from being retargeted until we have had the opportunity to load
// more information about them.
entry.canGoBack = true;
}
new AsyncTask<Void, Void, byte[]>() {
@Override
protected byte[] doInBackground(Void... params) {
return readMetadataFileBytes(isIncognito);
}
@Override
protected void onPostExecute(byte[] metadataBytes) {
updateTabEntriesFromMetadata(metadataBytes, entryMap, recentlyClosedTabIdList);
}
// Run on serial executor to ensure that this is done before other start-up tasks.
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
}
}