chromium/chrome/browser/tab/java/src/org/chromium/chrome/browser/tab/state/LevelDBPersistedTabDataStorage.java

// 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.tab.state;

import androidx.annotation.MainThread;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.base.lifetime.Destroyable;
import org.chromium.chrome.browser.profiles.Profile;

import java.nio.ByteBuffer;
import java.util.List;
import java.util.Locale;

/**
 * {@link LevelDBPersistedTabDataStorage} provides a level db backed implementation
 * of {@link PersistedTabDataStorage}.
 */
public class LevelDBPersistedTabDataStorage implements PersistedTabDataStorage, Destroyable {
    // In a mock environment, the native code will not be running so we should not
    // make assertions about mNativePersistedStateDB
    // LevelDBPersistedTabDataStorage needs to have an empty namespace for backwards compatibility.
    // LevelDBPersitsedDataStorage is a generalization of the original
    // LevelDBPersistedTabDataStorage which introduced namespaces to avoid collisions between
    // clients.
    private static String sNamespace = "";
    private LevelDBPersistedDataStorage mPersistedDataStorage;
    // Callback is only used for synchronization of save and delete in testing.
    // Otherwise it is a no-op.
    // TODO(crbug.com/40156389) Apply tricks like @CheckDiscard or proguard rules to improve
    // performance
    private boolean mIsDestroyed;

    LevelDBPersistedTabDataStorage(Profile profile) {
        assert !profile.isOffTheRecord()
                : "LevelDBPersistedTabDataStorage is not supported for incognito profiles";
        mPersistedDataStorage = new LevelDBPersistedDataStorage(profile, sNamespace);
    }

    @MainThread
    @Override
    public void save(int tabId, String dataId, Serializer<ByteBuffer> serializer) {
        // TODO(crbug.com/40186903) update LevelDB storage in native to use ByteBuffer instead
        // of byte[] to avoid conversion
        serializer.preSerialize();
        mPersistedDataStorage.save(getKey(tabId, dataId), toByteArray(serializer.get()));
    }

    @Override
    public void save(
            int tabId,
            String dataId,
            Serializer<ByteBuffer> serializer,
            Callback<Integer> callback) {
        assert false : "save with callback unused in LevelDBPersistedTabDataStorage";
    }

    private static byte[] toByteArray(ByteBuffer buffer) {
        if (buffer == null) {
            return null;
        }
        if (buffer.hasArray() && buffer.arrayOffset() == 0) {
            return buffer.array();
        }
        byte[] bytes = new byte[buffer.limit()];
        buffer.rewind();
        buffer.get(bytes);
        return bytes;
    }

    @MainThread
    public void saveForTesting(int tabId, String dataId, byte[] data, Runnable onComplete) {
        mPersistedDataStorage.saveForTesting(getKey(tabId, dataId), data, onComplete); // IN-TEST
    }

    @MainThread
    @Override
    public void restore(int tabId, String dataId, Callback<ByteBuffer> callback) {
        mPersistedDataStorage.load(
                getKey(tabId, dataId),
                (res) -> {
                    callback.onResult(res == null ? null : ByteBuffer.wrap(res));
                });
    }

    /**
     * Synchronous restore was an exception provided for an edge case in
     * {@link CriticalPersistedTabData} and is not typically part of the public API.
     */
    @Deprecated
    @MainThread
    @Override
    public ByteBuffer restore(int tabId, String dataId) {
        assert false : "Synchronous restore is not supported for LevelDBPersistedTabDataStorage";
        return null;
    }

    @MainThread
    @Override
    public <U extends PersistedTabDataResult> U restore(
            int tabId, String dataId, PersistedTabDataMapper<U> mapper) {
        assert false : "Restore with mapper currently unused in LevelDBPersistedTabDataStorage";
        return null;
    }

    @MainThread
    @Override
    public <U extends PersistedTabDataResult> void restore(
            int tabId, String dataId, Callback<U> callback, PersistedTabDataMapper<U> mapper) {
        assert false : "Restore with mapper currently unused in LevelDBPersistedTabDataStorage";
    }

    @MainThread
    @Override
    public void delete(int tabId, String dataId) {
        mPersistedDataStorage.delete(getKey(tabId, dataId));
    }

    @MainThread
    public void deleteForTesting(int tabId, String dataId, Runnable onComplete) {
        mPersistedDataStorage.deleteForTesting(getKey(tabId, dataId), onComplete); // IN-TEST
    }

    @Override
    public String getUmaTag() {
        return "LevelDB";
    }

    @Override
    public void performMaintenance(List<Integer> tabIds, String dataId) {
        mPersistedDataStorage.performMaintenance(getKeysToKeep(tabIds, dataId), dataId);
    }

    public void performMaintenanceForTesting(
            List<Integer> tabIds, String dataId, Runnable onComplete) {
        mPersistedDataStorage.performMaintenanceForTesting(
                getKeysToKeep(tabIds, dataId), dataId, onComplete); // IN-TEST
    }

    private static String[] getKeysToKeep(List<Integer> tabIds, String dataId) {
        String[] keysToKeep = new String[tabIds.size()];
        for (int i = 0; i < tabIds.size(); i++) {
            keysToKeep[i] = getKey(tabIds.get(i), dataId);
        }
        return keysToKeep;
    }

    // TODO(crbug.com/40156023) Implement URL -> byte[] mapping rather
    // than tab id -> byte[] mapping so we don't store the same data
    // multiple times when the user has multiple tabs at the same URL.
    private static final String getKey(int tabId, String dataId) {
        return String.format(Locale.US, "%d-%s", tabId, dataId);
    }

    /** Destroy native instance of persisted_tab_state */
    @Override
    public void destroy() {
        mPersistedDataStorage.destroy();
        mIsDestroyed = true;
    }

    @VisibleForTesting
    protected boolean isDestroyed() {
        return mIsDestroyed;
    }
}