blob: 72df8ecf3912d7de73975fddbad22d624901b184 [file] [log] [blame]
// 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;
import android.os.Handler;
import android.util.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.util.StreamUtil;
import org.chromium.content.browser.crypto.CipherFactory;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
/**
* Object that contains the state of a tab, including its navigation history.
*/
public class TabState {
private static final String TAG = "TabState";
/**
* Version number of the format used to save the WebContents navigation history, as returned by
* nativeGetContentsStateAsByteBuffer(). Version labels:
* 0 - Chrome m18
* 1 - Chrome m25
* 2 - Chrome m26+
*/
public static final int CONTENTS_STATE_CURRENT_VERSION = 2;
/** Special value for mTimestampMillis. */
private static final long TIMESTAMP_NOT_SET = -1;
/** Checks if the TabState header is loaded properly. */
private static final long KEY_CHECKER = 0;
/** Overrides the Chrome channel/package name to test a variant channel-specific behaviour. */
@VisibleForTesting
static String sChannelNameOverrideForTest;
/** Contains the state for a WebContents. */
public static class WebContentsState {
public final ByteBuffer mBuffer;
private int mVersion;
public WebContentsState(ByteBuffer buffer) {
this.mBuffer = buffer;
}
public ByteBuffer buffer() {
return mBuffer;
}
public int version() {
return mVersion;
}
public void setVersion(int version) {
mVersion = version;
}
/**
* Creates a WebContents from the buffer.
* @param isHidden Whether or not the tab initially starts hidden.
* @return Pointer to the native WebContents.
*/
public long restoreContentsFromByteBuffer(boolean isHidden) {
return nativeRestoreContentsFromByteBuffer(mBuffer, mVersion, isHidden);
}
}
/** Deletes the native-side portion of the buffer. */
public static class WebContentsStateNative extends WebContentsState {
private final Handler mHandler;
public WebContentsStateNative(ByteBuffer buffer) {
super(buffer);
this.mHandler = new Handler();
}
@Override
protected void finalize() {
assert mHandler != null;
mHandler.post(new Runnable() {
@Override
public void run() {
nativeFreeWebContentsStateBuffer(mBuffer);
}
});
}
}
public long timestampMillis = TIMESTAMP_NOT_SET;
public WebContentsState contentsState; // Navigation history of the WebContents.
public int parentId = Tab.INVALID_TAB_ID;
public String openerAppId;
public boolean isIncognito;
public long syncId;
public boolean shouldPreserve;
/** @return Whether a Stable channel build of Chrome is being used. */
private static boolean isStableChannelBuild() {
if ("stable".equals(sChannelNameOverrideForTest)) return true;
return ChromeVersionInfo.isStableBuild();
}
/**
* Restores a particular TabState file from storage.
* @param tabFile Location of the TabState file.
* @param isIncognito Whether the Tab is incognito or not.
* @return TabState that has been restored, or null if it failed.
*/
public static TabState restoreTabState(File tabFile, boolean isIncognito) {
FileInputStream stream = null;
TabState tabState = null;
try {
stream = new FileInputStream(tabFile);
tabState = TabState.readState(stream, isIncognito);
} catch (FileNotFoundException exception) {
Log.e(TAG, "Failed to restore tab state for tab: " + tabFile);
} catch (IOException exception) {
Log.e(TAG, "Failed to restore tab state.", exception);
} finally {
StreamUtil.closeQuietly(stream);
}
return tabState;
}
/**
* Restores a particular TabState file from storage.
* @param input Location of the TabState file.
* @param encrypted Whether the file is encrypted or not.
* @return TabState that has been restored, or null if it failed.
*/
public static TabState readState(FileInputStream input, boolean encrypted) throws IOException {
return readStateInternal(input, encrypted);
}
private static TabState readStateInternal(FileInputStream input, boolean encrypted)
throws IOException {
DataInputStream stream = null;
if (encrypted) {
Cipher cipher = CipherFactory.getInstance().getCipher(Cipher.DECRYPT_MODE);
if (cipher != null) {
stream = new DataInputStream(new CipherInputStream(input, cipher));
}
}
if (stream == null) {
stream = new DataInputStream(input);
}
try {
if (encrypted && stream.readLong() != KEY_CHECKER) {
// Got the wrong key, skip the file
return null;
}
TabState tabState = new TabState();
tabState.timestampMillis = stream.readLong();
int size = stream.readInt();
if (encrypted) {
// If it's encrypted, we have to read the stream normally to apply the cipher.
byte[] state = new byte[size];
stream.readFully(state);
tabState.contentsState = new WebContentsState(ByteBuffer.allocateDirect(size));
tabState.contentsState.buffer().put(state);
} else {
// If not, we can mmap the file directly, saving time and copies into the java heap.
FileChannel channel = input.getChannel();
tabState.contentsState = new WebContentsState(
channel.map(MapMode.READ_ONLY, channel.position(), size));
// Skip ahead to avoid re-reading data that mmap'd.
long skipped = input.skip(size);
if (skipped != size) {
Log.e(TAG, "Only skipped " + skipped + " bytes when " + size + " should've "
+ "been skipped. Tab restore may fail.");
}
}
tabState.parentId = stream.readInt();
try {
tabState.openerAppId = stream.readUTF();
if ("".equals(tabState.openerAppId)) tabState.openerAppId = null;
} catch (EOFException eof) {
// Could happen if reading a version of a TabState that does not include the app id.
Log.w(TAG, "Failed to read opener app id state from tab state");
}
try {
tabState.contentsState.setVersion(stream.readInt());
} catch (EOFException eof) {
// On the stable channel, the first release is version 18. For all other channels,
// chrome 25 is the first release.
tabState.contentsState.setVersion(isStableChannelBuild() ? 0 : 1);
// Could happen if reading a version of a TabState that does not include the
// version id.
Log.w(TAG, "Failed to read saved state version id from tab state. Assuming "
+ "version " + tabState.contentsState.version());
}
try {
tabState.syncId = stream.readLong();
} catch (EOFException eof) {
tabState.syncId = 0;
// Could happen if reading a version of TabState without syncId.
Log.w(TAG, "Failed to read syncId from tab state. Assuming syncId is: 0");
}
try {
tabState.shouldPreserve = stream.readBoolean();
} catch (EOFException eof) {
// Could happen if reading a version of TabState without this flag set.
tabState.shouldPreserve = false;
Log.w(TAG, "Failed to read shouldPreserve flag from tab state. "
+ "Assuming shouldPreserve is false");
}
tabState.isIncognito = encrypted;
return tabState;
} finally {
stream.close();
}
}
/**
* Writes the TabState to disk. This method may be called on either the UI or background thread.
* @param foutput Stream to write the tab's state to.
* @param state State object obtained from from {@link ChromeTab#getState()}.
* @param encrypted Whether or not the TabState should be encrypted.
*/
public static void saveState(FileOutputStream foutput, TabState state, boolean encrypted)
throws IOException {
if (state == null || state.contentsState == null) {
return;
}
saveStateInternal(foutput, state, encrypted);
}
private static void saveStateInternal(FileOutputStream output, TabState state,
boolean encrypted) throws IOException {
DataOutputStream stream;
if (encrypted) {
Cipher cipher = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
if (cipher != null) {
stream = new DataOutputStream(new CipherOutputStream(output, cipher));
} else {
// If cipher is null, getRandomBytes failed, which means encryption is meaningless.
// Therefore, do not save anything. This will cause users to lose Incognito state in
// certain cases. That is annoying, but is better than failing to provide the
// guarantee of Incognito Mode.
return;
}
} else {
stream = new DataOutputStream(output);
}
try {
if (encrypted) {
stream.writeLong(KEY_CHECKER);
}
stream.writeLong(state.timestampMillis);
state.contentsState.buffer().rewind();
stream.writeInt(state.contentsState.buffer().remaining());
if (encrypted) {
byte[] bytes = new byte[state.contentsState.buffer().remaining()];
state.contentsState.buffer().get(bytes);
stream.write(bytes);
} else {
output.getChannel().write(state.contentsState.buffer());
}
stream.writeInt(state.parentId);
stream.writeUTF(state.openerAppId != null ? state.openerAppId : "");
stream.writeInt(state.contentsState.version());
stream.writeLong(state.syncId);
stream.writeBoolean(state.shouldPreserve);
} finally {
StreamUtil.closeQuietly(stream);
}
}
/** @return Title currently being displayed in the saved state's current entry. */
public String getDisplayTitleFromState() {
return nativeGetDisplayTitleFromByteBuffer(contentsState.buffer(), contentsState.version());
}
/** @return URL currently being displayed in the saved state's current entry. */
public String getVirtualUrlFromState() {
return nativeGetVirtualUrlFromByteBuffer(contentsState.buffer(), contentsState.version());
}
/**
* Creates a WebContentsState for a tab that will be loaded lazily.
* @param url URL that is pending.
* @param referrerUrl URL for the referrer.
* @param referrerPolicy Policy for the referrer.
* @param isIncognito Whether or not the state is meant to be incognito (e.g. encrypted).
* @return ByteBuffer that represents a state representing a single pending URL.
*/
public static ByteBuffer createSingleNavigationStateAsByteBuffer(
String url, String referrerUrl, int referrerPolicy, boolean isIncognito) {
return nativeCreateSingleNavigationStateAsByteBuffer(
url, referrerUrl, referrerPolicy, isIncognito);
}
/**
* Returns the WebContents' state as a ByteBuffer.
* @param tab Tab to pickle.
* @return ByteBuffer containing the state of the WebContents.
*/
public static ByteBuffer getContentsStateAsByteBuffer(Tab tab) {
return nativeGetContentsStateAsByteBuffer(tab);
}
/**
* Overrides the channel name for testing.
* @param name Channel to use.
*/
public static void setChannelNameOverrideForTest(String name) {
sChannelNameOverrideForTest = name;
}
private static native long nativeRestoreContentsFromByteBuffer(
ByteBuffer buffer, int savedStateVersion, boolean initiallyHidden);
private static native ByteBuffer nativeGetContentsStateAsByteBuffer(Tab tab);
private static native ByteBuffer nativeCreateSingleNavigationStateAsByteBuffer(
String url, String referrerUrl, int referrerPolicy, boolean isIncognito);
private static native String nativeGetDisplayTitleFromByteBuffer(
ByteBuffer state, int savedStateVersion);
private static native String nativeGetVirtualUrlFromByteBuffer(
ByteBuffer state, int savedStateVersion);
private static native void nativeFreeWebContentsStateBuffer(ByteBuffer buffer);
}