// Copyright 2020 The Chromium Authors
// 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.tabpersistence;
import android.os.SystemClock;
import android.util.AtomicFile;
import android.util.Pair;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.StreamUtil;
import org.chromium.base.ThreadUtils;
import org.chromium.base.Token;
import org.chromium.base.cached_flags.BooleanCachedFieldTrialParameter;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.version_info.VersionInfo;
import org.chromium.chrome.browser.crypto.CipherFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabState;
import org.chromium.chrome.browser.tab.TabUserAgent;
import org.chromium.chrome.browser.tab.WebContentsState;
import java.io.BufferedOutputStream;
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.FilenameFilter;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.channels.WritableByteChannel;
import java.util.Locale;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
/** Saves and restores {@link TabState} to and from files. */
public class TabStateFileManager {
// Different variants will be experimented with and each variant will have
// a different prefix.
private static final String FLATBUFFER_PREFIX = "flatbufferv1_";
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static final String SAVED_TAB_STATE_FILE_PREFIX = "tab";
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static final String SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO = "cryptonito";
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static final String FLATBUFFER_SAVED_TAB_STATE_FILE_PREFIX =
FLATBUFFER_PREFIX + SAVED_TAB_STATE_FILE_PREFIX;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static final String FLATBUFFER_SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO =
FLATBUFFER_PREFIX + SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO;
private static final String TAG = "TabState";
/** Checks if the TabState header is loaded properly. */
protected static final long KEY_CHECKER = 0;
/** Overrides the Chrome channel/package name to test a variant channel-specific behaviour. */
private static String sChannelNameOverrideForTest;
private static final long NO_TAB_GROUP_ID = 0L;
public static final BooleanCachedFieldTrialParameter MIGRATE_STALE_TABS_CACHED_PARAM =
ChromeFeatureList.newBooleanCachedFieldTrialParameter(
ChromeFeatureList.TAB_STATE_FLAT_BUFFER, "migrate_stale_tabs", false);
/** Enum representing the exception that occurred during {@link restoreTabState}. */
@IntDef({
RestoreTabStateException.FILE_NOT_FOUND_EXCEPTION,
RestoreTabStateException.CLOSED_BY_INTERRUPT_EXCEPTION,
RestoreTabStateException.IO_EXCEPTION,
RestoreTabStateException.NUM_ENTRIES
})
@Retention(RetentionPolicy.SOURCE)
public @interface RestoreTabStateException {
int FILE_NOT_FOUND_EXCEPTION = 0;
int CLOSED_BY_INTERRUPT_EXCEPTION = 1;
int IO_EXCEPTION = 2;
int NUM_ENTRIES = 3;
}
@IntDef({
TabStateRestoreMethod.FLATBUFFER,
TabStateRestoreMethod.LEGACY_HAND_WRITTEN,
TabStateRestoreMethod.FAILED,
TabStateRestoreMethod.NUM_ENTRIES,
})
@Retention(RetentionPolicy.SOURCE)
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public @interface TabStateRestoreMethod {
/** TabState restored using FlatBuffer schema */
int FLATBUFFER = 0;
/** TabState restored using Legacy Handwritten schema */
int LEGACY_HAND_WRITTEN = 1;
/** TabState failed to be restored. */
int FAILED = 2;
int NUM_ENTRIES = 3;
}
/**
* @param stateFolder folder {@link TabState} files are stored in
* @param id {@link Tab} identifier
* @return {@link TabState} corresponding to Tab with id
*/
public static TabState restoreTabState(File stateFolder, int id) {
// If the FlatBuffer schema is enabled, try to restore using that. There are no guarantees,
// however - for example if the flag was just turned on there won't have been the
// opportunity to save any FlatBuffer based {@link TabState} files yet. So we
// always have a fallback to regular hand-written based TabState.
if (isFlatBufferSchemaEnabled()) {
TabState tabState = null;
try {
tabState = restoreTabState(stateFolder, id, true);
} catch (Exception e) {
// TODO(crbug.com/341122002) Add in metrics
Log.d(TAG, "Error restoring TabState using FlatBuffer", e);
}
if (tabState != null) {
RecordHistogram.recordEnumeratedHistogram(
"Tabs.TabState.RestoreMethod",
TabStateRestoreMethod.FLATBUFFER,
TabStateRestoreMethod.NUM_ENTRIES);
return tabState;
}
}
// Flatbuffer flag is off or we couldn't restore the TabState using a FlatBuffer based
// file e.g. file doesn't exist for the Tab or is corrupt.
TabState tabState = restoreTabState(stateFolder, id, false);
if (tabState == null) {
RecordHistogram.recordEnumeratedHistogram(
"Tabs.TabState.RestoreMethod",
TabStateRestoreMethod.FAILED,
TabStateRestoreMethod.NUM_ENTRIES);
} else {
RecordHistogram.recordEnumeratedHistogram(
"Tabs.TabState.RestoreMethod",
TabStateRestoreMethod.LEGACY_HAND_WRITTEN,
TabStateRestoreMethod.NUM_ENTRIES);
}
return tabState;
}
/**
* Restore a TabState file for a particular Tab. Checks if the Tab exists as a regular tab
* before searching for an encrypted version.
*
* @param stateFolder Folder containing the TabState files.
* @param id ID of the Tab to restore.
* @param useFlatBuffer whether to restore using the FlatBuffer based TabState file or not.
* @return TabState that has been restored, or null if it failed.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static TabState restoreTabState(File stateFolder, int id, boolean useFlatBuffer) {
// First try finding an unencrypted file.
boolean encrypted = false;
File file = getTabStateFile(stateFolder, id, encrypted, useFlatBuffer);
// If that fails, try finding the encrypted version.
if (!file.exists()) {
encrypted = true;
file = getTabStateFile(stateFolder, id, encrypted, useFlatBuffer);
}
// If they both failed, there's nothing to read.
if (!file.exists()) return null;
// If one of them passed, open the file input stream and read the state contents.
long startTime = SystemClock.elapsedRealtime();
TabState tabState = restoreTabStateInternal(file, encrypted);
if (tabState != null) {
RecordHistogram.recordTimesHistogram(
"Tabs.TabState.LoadTime", SystemClock.elapsedRealtime() - startTime);
}
return tabState;
}
/**
* Restores a particular TabState file from storage.
*
* @param tabFile Location of the TabState file.
* @param isEncrypted Whether the Tab state is encrypted or not.
* @return TabState that has been restored, or null if it failed.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static TabState restoreTabStateInternal(File tabFile, boolean isEncrypted) {
TabState tabState = null;
try {
// TODO(b/307795775) investigate what strongly typed exceptions the FlatBuffer
// code might throw and log metrics.
tabState = readState(tabFile, isEncrypted);
} catch (FileNotFoundException exception) {
Log.e(TAG, "Failed to restore tab state for tab: " + tabFile);
recordRestoreTabStateException(RestoreTabStateException.FILE_NOT_FOUND_EXCEPTION);
} catch (ClosedByInterruptException exception) {
Log.e(TAG, "Failed to restore tab state.", exception);
recordRestoreTabStateException(RestoreTabStateException.CLOSED_BY_INTERRUPT_EXCEPTION);
} catch (IOException exception) {
Log.e(TAG, "Failed to restore tab state.", exception);
recordRestoreTabStateException(RestoreTabStateException.IO_EXCEPTION);
} catch (Throwable e) {
if (tabFile.getName().startsWith(FLATBUFFER_PREFIX)) {
// TODO(b/307597013) Record number of FlatBuffer Tab restoration success/failure
// as well as hand-written based success/failure.
String message =
String.format(
Locale.getDefault(),
"Error reading TabState file %s",
tabFile.getName());
Log.i(TAG, message, e);
assert false : message;
// Catch all. FlatBuffer approach failed for some reason. Caller will know
// to fall back to legacy TabState.
return null;
} else {
throw e;
}
}
return tabState;
}
private static void recordRestoreTabStateException(
@RestoreTabStateException int restoreTabStateException) {
RecordHistogram.recordEnumeratedHistogram(
"Tabs.RestoreTabStateException",
restoreTabStateException,
RestoreTabStateException.NUM_ENTRIES);
}
/**
* Restores a particular TabState file from storage.
*
* @param file file with serialized {@link TabState}
* @param encrypted Whether the file is encrypted or not.
* @return TabState that has been restored, or null if it failed.
*/
private static TabState readState(File file, boolean encrypted)
throws IOException, FileNotFoundException {
if (file.getName().startsWith(FLATBUFFER_PREFIX)) {
return readStateFlatBuffer(file, encrypted);
}
FileInputStream input = new FileInputStream(file);
DataInputStream stream = null;
try {
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);
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 {
// Skip obsolete sync ID.
stream.readLong();
} catch (EOFException eof) {
}
try {
boolean shouldPreserveNotUsed = stream.readBoolean();
} catch (EOFException eof) {
// Could happen if reading a version of TabState without this flag set.
Log.w(
TAG,
"Failed to read shouldPreserve flag from tab state. "
+ "Assuming shouldPreserve is false");
}
tabState.isIncognito = encrypted;
try {
tabState.themeColor = stream.readInt();
} catch (EOFException eof) {
// Could happen if reading a version of TabState without a theme color.
tabState.themeColor = TabState.UNSPECIFIED_THEME_COLOR;
Log.w(
TAG,
"Failed to read theme color from tab state. "
+ "Assuming theme color is TabState#UNSPECIFIED_THEME_COLOR");
}
try {
tabState.tabLaunchTypeAtCreation = stream.readInt();
if (tabState.tabLaunchTypeAtCreation < 0
|| tabState.tabLaunchTypeAtCreation >= TabLaunchType.SIZE) {
tabState.tabLaunchTypeAtCreation = null;
}
} catch (EOFException eof) {
tabState.tabLaunchTypeAtCreation = null;
Log.w(
TAG,
"Failed to read tab launch type at creation from tab state. "
+ "Assuming tab launch type is null");
}
try {
tabState.rootId = stream.readInt();
} catch (EOFException eof) {
tabState.rootId = Tab.INVALID_TAB_ID;
Log.w(
TAG,
"Failed to read tab root id from tab state. "
+ "Assuming root id is Tab.INVALID_TAB_ID");
}
try {
tabState.userAgent = stream.readInt();
} catch (EOFException eof) {
tabState.userAgent = TabUserAgent.UNSET;
Log.w(
TAG,
"Failed to read tab user agent from tab state. "
+ "Assuming user agent is TabUserAgent.UNSET");
}
try {
tabState.lastNavigationCommittedTimestampMillis = stream.readLong();
} catch (EOFException eof) {
tabState.lastNavigationCommittedTimestampMillis = TabState.TIMESTAMP_NOT_SET;
Log.w(
TAG,
"Failed to read last navigation committed timestamp from tab state."
+ " Assuming last navigation committed timestamp is"
+ " TabState.TIMESTAMP_NOT_SET");
}
try {
long tokenHigh = stream.readLong();
long tokenLow = stream.readLong();
Token tabGroupId = new Token(tokenHigh, tokenLow);
tabState.tabGroupId = tabGroupId.isZero() ? null : tabGroupId;
} catch (EOFException eof) {
tabState.tabGroupId = null;
Log.w(
TAG,
"Failed to read tabGroupId token from tab state."
+ " Assuming tabGroupId is null");
}
// If TabState was restored using legacy format and the FlatBuffer flag is on, that
// indicates the TabState hasn't been migrated yet and should be.
if (isMigrateStaleTabsToFlatBufferEnabled()) {
tabState.shouldMigrate = true;
}
return tabState;
} finally {
StreamUtil.closeQuietly(stream);
StreamUtil.closeQuietly(input);
}
}
private static TabState readStateFlatBuffer(File file, boolean encrypted) throws IOException {
FileInputStream fileInputStream = null;
CipherInputStream cipherInputStream = null;
DataInputStream dataInputStream = null;
try {
fileInputStream = new FileInputStream(file);
FlatBufferTabStateSerializer serializer = new FlatBufferTabStateSerializer(encrypted);
if (encrypted) {
Cipher cipher = CipherFactory.getInstance().getCipher(Cipher.DECRYPT_MODE);
if (cipher == null) {
Log.e(
TAG,
"Cannot restore encrypted TabState FlatBuffer file because cipher is"
+ " null");
return null;
}
cipherInputStream = new CipherInputStream(fileInputStream, cipher);
dataInputStream = new DataInputStream(cipherInputStream);
if (dataInputStream.readLong() != KEY_CHECKER) {
Log.i(TAG, "Encryption key has changed, cannot restore incognito TabState");
return null;
}
int size = dataInputStream.readInt();
byte[] res = new byte[size];
dataInputStream.readFully(res);
return serializer.deserialize(ByteBuffer.wrap(res));
} else {
FileChannel channel = fileInputStream.getChannel();
ByteBuffer res = channel.map(MapMode.READ_ONLY, channel.position(), channel.size());
return serializer.deserialize(res);
}
} finally {
StreamUtil.closeQuietly(dataInputStream);
StreamUtil.closeQuietly(cipherInputStream);
StreamUtil.closeQuietly(fileInputStream);
}
}
public static byte[] getContentStateByteArray(final ByteBuffer buffer) {
byte[] contentsStateBytes = new byte[buffer.limit()];
buffer.rewind();
buffer.get(contentsStateBytes);
return contentsStateBytes;
}
/**
* @param directory directory TabState files are stored in
* @param tabState TabState to store in a file
* @param tabId identifier for the Tab
* @param isEncrypted whether the stored Tab is encrypted or not
*/
public static void saveState(
File directory, TabState tabState, int tabId, boolean isEncrypted) {
// Save regular hand-written based TabState file when the FlatBuffer flag is both on and
// off.
// We must always have a safe fallback to hand-written based TabState to be able to roll out
// FlatBuffers safely.
saveStateInternal(
getTabStateFile(directory, tabId, isEncrypted, false), tabState, isEncrypted);
}
/**
* Migrate TabState to new FlatBuffer based format
*
* @param directory directory TabState files are stored in
* @param tabState TabState to store in a file
* @param tabId identifier for the Tab
* @param isEncrypted whether the stored Tab is encrypted or not
* @return true if migration was successful.
*/
public static boolean migrateTabState(
File directory, TabState tabState, int tabId, boolean isEncrypted) {
try {
saveStateInternal(
getTabStateFile(directory, tabId, isEncrypted, true), tabState, isEncrypted);
return true;
} catch (Exception e) {
// TODO(crbug.com/341122002) Add in metrics
Log.d(TAG, "Error saving TabState FlatBuffer file", e);
}
return false;
}
/**
* @param directory directory TabState files are stored in
* @param tabId identifier for the {@link Tab}
* @param isEncrypted true if the {@link Tab} is incognito. Otherwise false.
* @return true if a {@link Tab} is migrated to the new FlatBuffer format.
*/
public static boolean isMigrated(File directory, int tabId, boolean isEncrypted) {
File file = getTabStateFile(directory, tabId, isEncrypted, /* isFlatbuffer= */ true);
return file != null && file.exists();
}
/**
* Writes the TabState to disk. This method may be called on either the UI or background thread.
*
* @param file File to write the tab's state to.
* @param state State object obtained from from {@link Tab#getState()}.
* @param encrypted Whether or not the TabState should be encrypted.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static void saveStateInternal(File file, TabState state, boolean encrypted) {
if (state == null || state.contentsState == null) return;
long startTime = SystemClock.elapsedRealtime();
// Create the byte array from contentsState before opening the FileOutputStream, in case
// contentsState.buffer is an instance of MappedByteBuffer that is mapped to
// the tab state file.
// Use local ByteBuffer (backed by same byte[] to mitigate crbug.com/1297894)
byte[] contentsStateBytes =
getContentStateByteArray(state.contentsState.buffer().asReadOnlyBuffer());
DataOutputStream dataOutputStream = null;
FileOutputStream fileOutputStream = null;
try {
if (file.getName().startsWith(FLATBUFFER_PREFIX)) {
saveStateFlatBuffer(file, state, encrypted, contentsStateBytes, startTime);
return;
}
fileOutputStream = new FileOutputStream(file);
if (encrypted) {
Cipher cipher = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
if (cipher != null) {
dataOutputStream =
new DataOutputStream(
new BufferedOutputStream(
new CipherOutputStream(fileOutputStream, 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 {
dataOutputStream = new DataOutputStream(new BufferedOutputStream(fileOutputStream));
}
if (encrypted) dataOutputStream.writeLong(KEY_CHECKER);
dataOutputStream.writeLong(state.timestampMillis);
dataOutputStream.writeInt(contentsStateBytes.length);
dataOutputStream.write(contentsStateBytes);
dataOutputStream.writeInt(state.parentId);
dataOutputStream.writeUTF(state.openerAppId != null ? state.openerAppId : "");
dataOutputStream.writeInt(state.contentsState.version());
dataOutputStream.writeLong(-1); // Obsolete sync ID.
dataOutputStream.writeBoolean(false); // Obsolete attribute |SHOULD_PRESERVE|.
dataOutputStream.writeInt(state.themeColor);
dataOutputStream.writeInt(
state.tabLaunchTypeAtCreation != null ? state.tabLaunchTypeAtCreation : -1);
dataOutputStream.writeInt(state.rootId);
dataOutputStream.writeInt(state.userAgent);
dataOutputStream.writeLong(state.lastNavigationCommittedTimestampMillis);
long tokenHigh = NO_TAB_GROUP_ID;
long tokenLow = NO_TAB_GROUP_ID;
if (state.tabGroupId != null) {
tokenHigh = state.tabGroupId.getHigh();
tokenLow = state.tabGroupId.getLow();
}
dataOutputStream.writeLong(tokenHigh);
dataOutputStream.writeLong(tokenLow);
long saveTime = SystemClock.elapsedRealtime() - startTime;
RecordHistogram.recordTimesHistogram("Tabs.TabState.SaveTime", saveTime);
RecordHistogram.recordTimesHistogram("Tabs.TabState.SaveTime.Legacy", saveTime);
} catch (FileNotFoundException e) {
Log.w(TAG, "FileNotFoundException while attempting to save TabState.");
} catch (IOException e) {
Log.w(TAG, "IOException while attempting to save TabState.");
} finally {
StreamUtil.closeQuietly(dataOutputStream);
StreamUtil.closeQuietly(fileOutputStream);
}
}
private static void saveStateFlatBuffer(
File file,
TabState state,
boolean encrypted,
byte[] contentsStateBytes,
long startTime) {
FileOutputStream fileOutputStream = null;
CipherOutputStream cipherOutputStream = null;
DataOutputStream dataOutputStream = null;
boolean success = false;
AtomicFile atomicFile = new AtomicFile(file);
try {
fileOutputStream = atomicFile.startWrite();
FlatBufferTabStateSerializer serializer = new FlatBufferTabStateSerializer(encrypted);
ByteBuffer data = serializer.serialize(state, contentsStateBytes);
if (encrypted) {
Cipher cipher = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
if (cipher == null) {
Log.e(TAG, "Cannot save TabState FlatBuffer file because cipher is null");
return;
}
cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
dataOutputStream = new DataOutputStream(cipherOutputStream);
dataOutputStream.writeLong(KEY_CHECKER);
int size = data.remaining();
dataOutputStream.writeInt(size);
WritableByteChannel channel = Channels.newChannel(dataOutputStream);
channel.write(data);
} else {
FileChannel channel = fileOutputStream.getChannel();
channel.write(data);
}
success = true;
RecordHistogram.recordTimesHistogram(
"Tabs.TabState.SaveTime.FlatBuffer", SystemClock.elapsedRealtime() - startTime);
} catch (Throwable e) {
// Catch all in case of an issue saving the FlatBuffer file. Avoid crashing
// the app and simply log what went wrong.
Log.e(TAG, "Exception writing " + file.getName(), e);
} finally {
StreamUtil.closeQuietly(dataOutputStream);
StreamUtil.closeQuietly(cipherOutputStream);
StreamUtil.closeQuietly(fileOutputStream);
if (success) {
safelyFinishOrFailWrite(atomicFile, fileOutputStream);
} else {
safelyFailWrite(atomicFile, fileOutputStream);
}
}
}
private static void safelyFinishOrFailWrite(
AtomicFile atomicFile, FileOutputStream fileOutputStream) {
try {
atomicFile.finishWrite(fileOutputStream);
} catch (Throwable e) {
Log.e(TAG, "Error finishing atomic write of " + atomicFile, e);
safelyFailWrite(atomicFile, fileOutputStream);
}
}
private static void safelyFailWrite(AtomicFile atomicFile, FileOutputStream fileOutputStream) {
try {
atomicFile.failWrite(fileOutputStream);
} catch (Throwable e) {
Log.e(TAG, "Error failing atomic write of " + atomicFile, e);
}
}
/**
* Returns a File corresponding to the given TabState.
*
* @param directory Directory containing the TabState files.
* @param tabId ID of the TabState to delete.
* @param encrypted Whether the TabState is encrypted.
* @param isFlatbuffer true if the TabState file is FlatBuffer schema based.
* @return File corresponding to the given TabState.
*/
public static File getTabStateFile(
File directory, int tabId, boolean encrypted, boolean isFlatbuffer) {
return new File(directory, getTabStateFilename(tabId, encrypted, isFlatbuffer));
}
/**
* Deletes the TabState corresponding to the given Tab.
* @param directory Directory containing the TabState files.
* @param tabId ID of the TabState to delete.
* @param encrypted Whether the TabState is encrypted.
*/
public static void deleteTabState(File directory, int tabId, boolean encrypted) {
for (boolean useFlatBuffer : new boolean[] {false, true}) {
File file = getTabStateFile(directory, tabId, encrypted, useFlatBuffer);
if (file.exists() && !file.delete()) Log.e(TAG, "Failed to delete TabState: " + file);
}
}
/**
* Delete migrated TabState file for corresponding Tab
*
* @param directory directory TabState files are stored in
* @param tabId identifier for {@link Tab}
* @param encrypted isEncrypted true if the {@link Tab} is incognito. Otherwise false.
*/
public static void deleteMigratedFile(File directory, int tabId, boolean encrypted) {
File file = getTabStateFile(directory, tabId, encrypted, /* isFlatbuffer= */ true);
if (file != null && file.exists() && !file.delete()) {
Log.e(TAG, "Failed to delete TabState: " + file);
}
}
/**
* Generates the name of the state file that should represent the Tab specified by {@code id}
* and {@code encrypted}.
*
* @param id The id of the {@link Tab} to save.
* @param encrypted Whether or not the tab is incognito and should be encrypted.
* @return The name of the file the Tab state should be saved to.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static String getTabStateFilename(int id, boolean encrypted, boolean isFlatBuffer) {
if (isFlatBuffer) {
return (encrypted
? FLATBUFFER_SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO
: FLATBUFFER_SAVED_TAB_STATE_FILE_PREFIX)
+ id;
}
return (encrypted ? SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO : SAVED_TAB_STATE_FILE_PREFIX)
+ id;
}
/**
* Delete TabState file asynchronously on a background thread.
*
* @param directory directory the TabState files are stored in.
* @param tabId identifier for the Tab
* @param encrypted True if the Tab is incognito.
*/
public static void deleteAsync(File directory, int tabId, boolean encrypted) {
PostTask.runOrPostTask(
TaskTraits.BEST_EFFORT_MAY_BLOCK,
() -> {
ThreadUtils.assertOnBackgroundThread();
deleteTabState(directory, tabId, encrypted);
});
}
/**
* Cleanup FlatBuffer files while the experiment is turned off. This ensures when the user
* re-enters the FlatBuffer migration experiment we don't attempt to restore their Tabs using
* out of date FlatBuffer files.
*
* @param stateDirectory directory where TabState files are saved.
*/
public static void cleanupUnusedFiles(File stateDirectory) {
if (isFlatBufferSchemaEnabled()) {
return;
}
PostTask.postTask(
TaskTraits.BEST_EFFORT_MAY_BLOCK,
() -> {
ThreadUtils.assertOnBackgroundThread();
deleteFlatBufferFiles(stateDirectory);
});
}
@VisibleForTesting
protected static void deleteFlatBufferFiles(File stateDirectory) {
if (stateDirectory == null || stateDirectory.listFiles() == null) {
return;
}
for (String filename :
stateDirectory.list(
new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name != null && name.startsWith(FLATBUFFER_PREFIX);
}
})) {
File file = new File(stateDirectory, filename);
if (!file.delete()) {
Log.e(TAG, "Failed to delete FlatBuffer TabState: " + file);
}
}
}
/**
* Parse the tab id and whether the tab is incognito from the tab state filename.
* @param name The given filename for the tab state file.
* @return A {@link Pair} with tab id and incognito state read from the filename.
*/
public static Pair<Integer, Boolean> parseInfoFromFilename(String name) {
try {
if (name.startsWith(SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO)) {
int id =
Integer.parseInt(
name.substring(SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO.length()));
return Pair.create(id, true);
} else if (name.startsWith(SAVED_TAB_STATE_FILE_PREFIX)) {
int id = Integer.parseInt(name.substring(SAVED_TAB_STATE_FILE_PREFIX.length()));
return Pair.create(id, false);
} else if (name.startsWith(FLATBUFFER_SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO)) {
int id =
Integer.parseInt(
name.substring(
FLATBUFFER_SAVED_TAB_STATE_FILE_PREFIX_INCOGNITO.length()));
return Pair.create(id, true);
} else if (name.startsWith(FLATBUFFER_SAVED_TAB_STATE_FILE_PREFIX)) {
int id =
Integer.parseInt(
name.substring(FLATBUFFER_SAVED_TAB_STATE_FILE_PREFIX.length()));
return Pair.create(id, false);
}
} catch (NumberFormatException ex) {
// Expected for files not related to tab state.
}
return null;
}
/** @return Whether a Stable channel build of Chrome is being used. */
private static boolean isStableChannelBuild() {
if ("stable".equals(sChannelNameOverrideForTest)) return true;
return VersionInfo.isStableBuild();
}
/**
* Overrides the channel name for testing.
* @param name Channel to use.
*/
public static void setChannelNameOverrideForTest(String name) {
sChannelNameOverrideForTest = name;
ResettersForTesting.register(() -> sChannelNameOverrideForTest = null);
}
private static boolean isFlatBufferSchemaEnabled() {
return ChromeFeatureList.sTabStateFlatBuffer.isEnabled();
}
private static boolean isMigrateStaleTabsToFlatBufferEnabled() {
return MIGRATE_STALE_TABS_CACHED_PARAM.getValue();
}
}