chromium/chrome/browser/tabpersistence/android/java/src/org/chromium/chrome/browser/tabpersistence/FlatBufferTabStateSerializer.java

// Copyright 2023 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 androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.google.flatbuffers.FlatBufferBuilder;

import org.chromium.base.Log;
import org.chromium.base.Token;
import org.chromium.base.metrics.RecordHistogram;
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 org.chromium.chrome.browser.tab.flatbuffer.TabGroupIdToken;
import org.chromium.chrome.browser.tab.flatbuffer.TabLaunchTypeAtCreation;
import org.chromium.chrome.browser.tab.flatbuffer.TabStateFlatBufferV1;
import org.chromium.chrome.browser.tab.flatbuffer.UserAgentType;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;

/** {@link TabStateSerializer} backed by a FlatBuffer */
public class FlatBufferTabStateSerializer implements TabStateSerializer {
    private static final String TAG = "FBTSS";
    private static final String NULL_OPENER_APP_ID = " ";
    private static final long NO_TAB_GROUP_ID = 0L;

    private final boolean mIsEncrypted;

    public FlatBufferTabStateSerializer(boolean isEncrypted) {
        mIsEncrypted = isEncrypted;
    }

    @IntDef({
        TabStateFlatBufferDeserializeResult.SUCCESS,
        TabStateFlatBufferDeserializeResult.FAILURE_UNKNOWN_REASON,
        TabStateFlatBufferDeserializeResult.FAILURE_INDEX_OUT_OF_BOUNDS_EXCEPTION,
        TabStateFlatBufferDeserializeResult.NUM_ENTRIES,
    })
    @Retention(RetentionPolicy.SOURCE)
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public @interface TabStateFlatBufferDeserializeResult {
        /** FlatBuffer was successfully deserialized to TabState. */
        int SUCCESS = 0;

        /** FlatBuffer deserialization failed because of an unknown reason. */
        int FAILURE_UNKNOWN_REASON = 1;

        /** FlatBuffer deserialization failed because of an index out of bounds exception. */
        int FAILURE_INDEX_OUT_OF_BOUNDS_EXCEPTION = 2;

        /** FlatBuffer deserialization failed because of an illegal argument exception. */
        int FAILURE_ILLEGAL_ARGUMENT_EXCEPTION = 3;

        int NUM_ENTRIES = 4;
    }

    @Override
    public ByteBuffer serialize(TabState state, byte[] contentsStateBytes) {
        FlatBufferBuilder fbb = new FlatBufferBuilder();
        int webContentsState =
                TabStateFlatBufferV1.createWebContentsStateBytesVector(
                        fbb, ByteBuffer.wrap(contentsStateBytes));
        int openerAppId =
                fbb.createString(
                        state.openerAppId == null ? NULL_OPENER_APP_ID : state.openerAppId);
        TabStateFlatBufferV1.startTabStateFlatBufferV1(fbb);
        TabStateFlatBufferV1.addParentId(fbb, state.parentId);
        TabStateFlatBufferV1.addRootId(fbb, state.rootId);
        TabStateFlatBufferV1.addTimestampMillis(fbb, state.timestampMillis);
        TabStateFlatBufferV1.addWebContentsStateBytes(fbb, webContentsState);
        TabStateFlatBufferV1.addOpenerAppId(fbb, openerAppId);
        TabStateFlatBufferV1.addThemeColor(fbb, state.themeColor);
        TabStateFlatBufferV1.addLaunchTypeAtCreation(
                fbb, getLaunchTypeToFlatBuffer(state.tabLaunchTypeAtCreation));
        TabStateFlatBufferV1.addUserAgent(fbb, getUserAgentTypeToFlatBuffer(state.userAgent));
        TabStateFlatBufferV1.addLastNavigationCommittedTimestampMillis(
                fbb, 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();
        }
        TabStateFlatBufferV1.addTabGroupId(
                fbb, TabGroupIdToken.createTabGroupIdToken(fbb, tokenHigh, tokenLow));
        int r = TabStateFlatBufferV1.endTabStateFlatBufferV1(fbb);
        fbb.finish(r);
        return fbb.dataBuffer();
    }

    @Override
    public TabState deserialize(ByteBuffer bytes) {
        try {
            TabStateFlatBufferV1 tabStateFlatBuffer =
                    TabStateFlatBufferV1.getRootAsTabStateFlatBufferV1(bytes);

            TabState state = new TabState();
            state.isIncognito = mIsEncrypted;
            state.parentId = tabStateFlatBuffer.parentId();
            state.rootId = tabStateFlatBuffer.rootId();
            state.openerAppId =
                    NULL_OPENER_APP_ID.equals(tabStateFlatBuffer.openerAppId())
                            ? null
                            : tabStateFlatBuffer.openerAppId();
            state.timestampMillis = tabStateFlatBuffer.timestampMillis();
            state.lastNavigationCommittedTimestampMillis =
                    tabStateFlatBuffer.lastNavigationCommittedTimestampMillis();

            Token tabGroupId = null;
            var flatBufferTabGroupId = tabStateFlatBuffer.tabGroupId();
            if (flatBufferTabGroupId != null) {
                tabGroupId = new Token(flatBufferTabGroupId.high(), flatBufferTabGroupId.low());
            }
            state.tabGroupId = (tabGroupId == null || tabGroupId.isZero()) ? null : tabGroupId;
            state.userAgent = getTabUserAgentTypeFromFlatBuffer(tabStateFlatBuffer.userAgent());
            state.tabLaunchTypeAtCreation =
                    getLaunchTypeFromFlatBuffer(tabStateFlatBuffer.launchTypeAtCreation());
            state.themeColor = tabStateFlatBuffer.themeColor();
            ByteBuffer webContentsStateBuffer =
                    tabStateFlatBuffer.webContentsStateBytesAsByteBuffer() == null
                            ? ByteBuffer.allocateDirect(0)
                            : tabStateFlatBuffer.webContentsStateBytesAsByteBuffer().slice();
            if (mIsEncrypted) {
                state.contentsState =
                        new WebContentsState(
                                ByteBuffer.allocateDirect(webContentsStateBuffer.remaining()));
                state.contentsState.buffer().put(webContentsStateBuffer);
            } else {
                state.contentsState = new WebContentsState(webContentsStateBuffer);
            }
            state.contentsState.setVersion(WebContentsState.CONTENTS_STATE_CURRENT_VERSION);
            return state;
        } catch (IndexOutOfBoundsException e) {
            RecordHistogram.recordEnumeratedHistogram(
                    "Tabs.TabState.FlatBufferDeserializeResult",
                    TabStateFlatBufferDeserializeResult.FAILURE_INDEX_OUT_OF_BOUNDS_EXCEPTION,
                    TabStateFlatBufferDeserializeResult.NUM_ENTRIES);
        } catch (IllegalArgumentException e) {
            RecordHistogram.recordEnumeratedHistogram(
                    "Tabs.TabState.FlatBufferDeserializeResult",
                    TabStateFlatBufferDeserializeResult.FAILURE_ILLEGAL_ARGUMENT_EXCEPTION,
                    TabStateFlatBufferDeserializeResult.NUM_ENTRIES);
        } catch (Exception e) {
            RecordHistogram.recordEnumeratedHistogram(
                    "Tabs.TabState.FlatBufferDeserializeResult",
                    TabStateFlatBufferDeserializeResult.FAILURE_UNKNOWN_REASON,
                    TabStateFlatBufferDeserializeResult.NUM_ENTRIES);
            Log.e(TAG, "Error deserializing tabState FlatBuffer", e);
            assert false : e.getMessage();
        }
        return null;
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public static @Nullable @TabLaunchType Integer getLaunchTypeFromFlatBuffer(
            int flatBufferLaunchType) {
        switch (flatBufferLaunchType) {
            case TabLaunchTypeAtCreation.FROM_LINK:
                return TabLaunchType.FROM_LINK;
            case TabLaunchTypeAtCreation.FROM_EXTERNAL_APP:
                return TabLaunchType.FROM_EXTERNAL_APP;
            case TabLaunchTypeAtCreation.FROM_CHROME_UI:
                return TabLaunchType.FROM_CHROME_UI;
            case TabLaunchTypeAtCreation.FROM_RESTORE:
                return TabLaunchType.FROM_RESTORE;
            case TabLaunchTypeAtCreation.FROM_LONGPRESS_FOREGROUND:
                return TabLaunchType.FROM_LONGPRESS_FOREGROUND;
            case TabLaunchTypeAtCreation.FROM_LONGPRESS_INCOGNITO:
                return TabLaunchType.FROM_LONGPRESS_INCOGNITO;
            case TabLaunchTypeAtCreation.FROM_LONGPRESS_BACKGROUND:
                return TabLaunchType.FROM_LONGPRESS_BACKGROUND;
            case TabLaunchTypeAtCreation.FROM_REPARENTING:
                return TabLaunchType.FROM_REPARENTING;
            case TabLaunchTypeAtCreation.FROM_LAUNCHER_SHORTCUT:
                return TabLaunchType.FROM_LAUNCHER_SHORTCUT;
            case TabLaunchTypeAtCreation.FROM_SPECULATIVE_BACKGROUND_CREATION:
                return TabLaunchType.FROM_SPECULATIVE_BACKGROUND_CREATION;
            case TabLaunchTypeAtCreation.FROM_BROWSER_ACTIONS:
                return TabLaunchType.FROM_BROWSER_ACTIONS;
            case TabLaunchTypeAtCreation.FROM_LAUNCH_NEW_INCOGNITO_TAB:
                return TabLaunchType.FROM_LAUNCH_NEW_INCOGNITO_TAB;
            case TabLaunchTypeAtCreation.FROM_STARTUP:
                return TabLaunchType.FROM_STARTUP;
            case TabLaunchTypeAtCreation.FROM_START_SURFACE:
                return TabLaunchType.FROM_START_SURFACE;
            case TabLaunchTypeAtCreation.FROM_TAB_GROUP_UI:
                return TabLaunchType.FROM_TAB_GROUP_UI;
            case TabLaunchTypeAtCreation.FROM_TAB_SWITCHER_UI:
                return TabLaunchType.FROM_TAB_SWITCHER_UI;
            case TabLaunchTypeAtCreation.FROM_RESTORE_TABS_UI:
                return TabLaunchType.FROM_RESTORE_TABS_UI;
            case TabLaunchTypeAtCreation.FROM_LONGPRESS_BACKGROUND_IN_GROUP:
                return TabLaunchType.FROM_LONGPRESS_BACKGROUND_IN_GROUP;
            case TabLaunchTypeAtCreation.FROM_APP_WIDGET:
                return TabLaunchType.FROM_APP_WIDGET;
            case TabLaunchTypeAtCreation.FROM_RECENT_TABS:
                return TabLaunchType.FROM_RECENT_TABS;
            case TabLaunchTypeAtCreation.FROM_READING_LIST:
                return TabLaunchType.FROM_READING_LIST;
            case TabLaunchTypeAtCreation.FROM_OMNIBOX:
                return TabLaunchType.FROM_OMNIBOX;
            case TabLaunchTypeAtCreation.UNSET:
                return TabLaunchType.UNSET;
            case TabLaunchTypeAtCreation.FROM_SYNC_BACKGROUND:
                return TabLaunchType.FROM_SYNC_BACKGROUND;
            case TabLaunchTypeAtCreation.FROM_RECENT_TABS_FOREGROUND:
                return TabLaunchType.FROM_RECENT_TABS_FOREGROUND;
            case TabLaunchTypeAtCreation.SIZE:
                return TabLaunchType.SIZE;
            case TabLaunchTypeAtCreation.UNKNOWN:
                return null;
            default:
                assert false
                        : "Unexpected deserialization of LaunchAtCreationType: "
                                + flatBufferLaunchType;
                // shouldn't happen
                return null;
        }
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public static int getLaunchTypeToFlatBuffer(@Nullable @TabLaunchType Integer tabLaunchType) {
        if (tabLaunchType == null) {
            return TabLaunchTypeAtCreation.UNKNOWN;
        }
        switch (tabLaunchType) {
            case TabLaunchType.FROM_LINK:
                return TabLaunchTypeAtCreation.FROM_LINK;
            case TabLaunchType.FROM_EXTERNAL_APP:
                return TabLaunchTypeAtCreation.FROM_EXTERNAL_APP;
            case TabLaunchType.FROM_CHROME_UI:
                return TabLaunchTypeAtCreation.FROM_CHROME_UI;
            case TabLaunchType.FROM_RESTORE:
                return TabLaunchTypeAtCreation.FROM_RESTORE;
            case TabLaunchType.FROM_LONGPRESS_FOREGROUND:
                return TabLaunchTypeAtCreation.FROM_LONGPRESS_FOREGROUND;
            case TabLaunchType.FROM_LONGPRESS_INCOGNITO:
                return TabLaunchTypeAtCreation.FROM_LONGPRESS_INCOGNITO;
            case TabLaunchType.FROM_LONGPRESS_BACKGROUND:
                return TabLaunchTypeAtCreation.FROM_LONGPRESS_BACKGROUND;
            case TabLaunchType.FROM_REPARENTING:
                return TabLaunchTypeAtCreation.FROM_REPARENTING;
            case TabLaunchType.FROM_LAUNCHER_SHORTCUT:
                return TabLaunchTypeAtCreation.FROM_LAUNCHER_SHORTCUT;
            case TabLaunchType.FROM_SPECULATIVE_BACKGROUND_CREATION:
                return TabLaunchTypeAtCreation.FROM_SPECULATIVE_BACKGROUND_CREATION;
            case TabLaunchType.FROM_BROWSER_ACTIONS:
                return TabLaunchTypeAtCreation.FROM_BROWSER_ACTIONS;
            case TabLaunchType.FROM_LAUNCH_NEW_INCOGNITO_TAB:
                return TabLaunchTypeAtCreation.FROM_LAUNCH_NEW_INCOGNITO_TAB;
            case TabLaunchType.FROM_STARTUP:
                return TabLaunchTypeAtCreation.FROM_STARTUP;
            case TabLaunchType.FROM_START_SURFACE:
                return TabLaunchTypeAtCreation.FROM_START_SURFACE;
            case TabLaunchType.FROM_TAB_GROUP_UI:
                return TabLaunchTypeAtCreation.FROM_TAB_GROUP_UI;
            case TabLaunchType.FROM_TAB_SWITCHER_UI:
                return TabLaunchTypeAtCreation.FROM_TAB_SWITCHER_UI;
            case TabLaunchType.FROM_RESTORE_TABS_UI:
                return TabLaunchTypeAtCreation.FROM_RESTORE_TABS_UI;
            case TabLaunchType.FROM_LONGPRESS_BACKGROUND_IN_GROUP:
                return TabLaunchTypeAtCreation.FROM_LONGPRESS_BACKGROUND_IN_GROUP;
            case TabLaunchType.FROM_APP_WIDGET:
                return TabLaunchTypeAtCreation.FROM_APP_WIDGET;
            case TabLaunchType.FROM_RECENT_TABS:
                return TabLaunchTypeAtCreation.FROM_RECENT_TABS;
            case TabLaunchType.FROM_READING_LIST:
                return TabLaunchTypeAtCreation.FROM_READING_LIST;
            case TabLaunchType.FROM_OMNIBOX:
                return TabLaunchTypeAtCreation.FROM_OMNIBOX;
            case TabLaunchType.UNSET:
                return TabLaunchTypeAtCreation.UNSET;
            case TabLaunchType.FROM_SYNC_BACKGROUND:
                return TabLaunchTypeAtCreation.FROM_SYNC_BACKGROUND;
            case TabLaunchType.FROM_RECENT_TABS_FOREGROUND:
                return TabLaunchTypeAtCreation.FROM_RECENT_TABS_FOREGROUND;
            case TabLaunchType.SIZE:
                return TabLaunchTypeAtCreation.SIZE;
            default:
                assert false : "Unexpected serialization of LaunchAtCreationType: " + tabLaunchType;
                // shouldn't happen
                return TabLaunchTypeAtCreation.UNKNOWN;
        }
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public static @TabUserAgent int getTabUserAgentTypeFromFlatBuffer(int flatbufferUserAgentType) {
        switch (flatbufferUserAgentType) {
            case UserAgentType.DEFAULT:
                return TabUserAgent.DEFAULT;
            case UserAgentType.MOBILE:
                return TabUserAgent.MOBILE;
            case UserAgentType.DESKTOP:
                return TabUserAgent.DESKTOP;
            case UserAgentType.UNSET:
                return TabUserAgent.UNSET;
            case UserAgentType.USER_AGENT_SIZE:
                return TabUserAgent.SIZE;
            default:
                assert false
                        : "Unexpected deserialization of UserAgentType: " + flatbufferUserAgentType;
                // shouldn't happen
                return TabUserAgent.DEFAULT;
        }
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    public static int getUserAgentTypeToFlatBuffer(@TabUserAgent int userAgent) {
        switch (userAgent) {
            case TabUserAgent.DEFAULT:
                return UserAgentType.DEFAULT;
            case TabUserAgent.MOBILE:
                return UserAgentType.MOBILE;
            case TabUserAgent.DESKTOP:
                return UserAgentType.DESKTOP;
            case TabUserAgent.UNSET:
                return UserAgentType.UNSET;
            case TabUserAgent.SIZE:
                return UserAgentType.USER_AGENT_SIZE;
            default:
                assert false : "Unexpected serialization of UserAgentType: " + userAgent;
                // shouldn't happen
                return UserAgentType.USER_AGENT_UNKNOWN;
        }
    }
}