chromium/ui/android/java/src/org/chromium/ui/dragdrop/DragDropGlobalState.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.ui.dragdrop;

import android.os.SystemClock;
import android.view.DragEvent;
import android.view.View.DragShadowBuilder;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;

/**
 * Drag-Drop objects to be shared across instances. General usage:
 * <li>When drag starts, the client that puts the global instance will do {@link #store} and
 *     generate a tracker token for later use.
 * <li>During the drag, client that starts the drag can {@link #getState(TrackerToken)} to access /
 *     modify the global state.
 * <li>When dropped, view that receive the drop can use {@link #getState(DragEvent)} to access /
 *     modify the global state.
 * <li>When drag ends, client that starts the drag need to do {@link #clear(TrackerToken)} to reset
 *     the global state.
 */
public class DragDropGlobalState {
    private static final String TAG = "DnDGlobalState";
    private @Nullable static GlobalStateHolder sGlobalStateHolder;

    /** Token used to retrieve from {@link DragDropGlobalState#getState(TrackerToken)}. */
    public static final class TrackerToken {
        final long mKey;

        TrackerToken() {
            this.mKey = SystemClock.elapsedRealtime();
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            if (!(obj instanceof TrackerToken that)) {
                return false;
            }
            return that.mKey == this.mKey;
        }
    }

    /**
     * Build a {@link DragDropGlobalState} to be stored in the static instance, and return the
     * {@link TrackerToken} to retrieve and modify the state.
     *
     * @param dragSourceInstanceId Instance Id from the source window
     * @param dropData {@link DropDataAndroid} used to start this drag and drop.
     * @param dragShadowBuilder {@link DragShadowBuilder} used for starting the drag and drop.
     * @return Token used to retrieve and clear the global state.
     */
    public static TrackerToken store(
            int dragSourceInstanceId,
            @NonNull DropDataAndroid dropData,
            @Nullable DragShadowBuilder dragShadowBuilder) {
        if (sGlobalStateHolder != null) {
            Log.w(
                    TAG,
                    "Holder isn't cleared before store. "
                            + sGlobalStateHolder.mInstance.toString());
        }
        sGlobalStateHolder =
                new GlobalStateHolder(
                        new DragDropGlobalState(dragSourceInstanceId, dropData), dragShadowBuilder);
        return sGlobalStateHolder.mToken;
    }

    /** Returns boolean indicating if static instance was created. */
    public static boolean hasValue() {
        return sGlobalStateHolder != null;
    }

    /**
     * Get the global state using tracker token without cleaning the global state.
     *
     * @param token Token returned by {@link #store(int, ChromeDropDataAndroid, DragShadowBuilder)}
     * @return The stored global state.
     */
    public static @Nullable DragDropGlobalState getState(TrackerToken token) {
        if (sGlobalStateHolder == null || !sGlobalStateHolder.mToken.equals(token)) return null;
        return sGlobalStateHolder.mInstance;
    }

    /** Get the global state using DragEvent with {@link DragEvent#ACTION_DROP}. */
    public static @Nullable DragDropGlobalState getState(@NonNull DragEvent dropEvent) {
        if (sGlobalStateHolder == null || dropEvent.getAction() != DragEvent.ACTION_DROP) {
            return null;
        }
        return sGlobalStateHolder.mInstance;
    }

    /**
     * Return the drag shadow builder during this drag and drop process, if provided to the
     * DragDropGlobalState.
     */
    public static DragShadowBuilder getDragShadowBuilder() {
        if (sGlobalStateHolder == null) return null;
        return sGlobalStateHolder.mDragShadowBuilder;
    }

    /**
     * Tokens are released when startDragAndDrop fails or by listeners on drag end event. If a
     * caller fails to release token, sHolder is not cleared and the next build call will fail.
     */
    public static void clear(@NonNull TrackerToken token) {
        assert sGlobalStateHolder == null || sGlobalStateHolder.mToken.equals(token)
                : "Token mismatch.";
        sGlobalStateHolder = null;
    }

    private final int mDragSourceInstanceId;
    private final @NonNull DropDataAndroid mDropData;

    DragDropGlobalState(int dragSourceInstanceId, @NonNull DropDataAndroid dropData) {
        mDragSourceInstanceId = dragSourceInstanceId;
        mDropData = dropData;
    }

    /** Return whether the drag state started by |instanceId|. */
    public boolean isDragSourceInstance(int instanceId) {
        return mDragSourceInstanceId == instanceId;
    }

    /** Return the Chrome instance id of the drag source held by the global state. */
    public int getDragSourceInstance() {
        return mDragSourceInstanceId;
    }

    /** Return the {@link DropDataAndroid} held by the global state. */
    public @NonNull DropDataAndroid getData() {
        return mDropData;
    }

    @NonNull
    @Override
    public String toString() {
        return "DragDropGlobalState" + " sourceId:" + mDragSourceInstanceId;
    }

    /** Helper class to store the static instance of global state. */
    private static class GlobalStateHolder {
        final DragDropGlobalState mInstance;
        final TrackerToken mToken;
        final @Nullable DragShadowBuilder mDragShadowBuilder;

        private GlobalStateHolder(
                DragDropGlobalState instance, @Nullable DragShadowBuilder dragShadowBuilder) {
            mInstance = instance;
            mToken = new TrackerToken();
            mDragShadowBuilder = dragShadowBuilder;
        }
    }

    public static void setInstanceForTesting(DragDropGlobalState instance) {
        sGlobalStateHolder = new GlobalStateHolder(instance, null);
        ResettersForTesting.register(() -> sGlobalStateHolder = null);
    }

    public static DragDropGlobalState getForTesting() {
        assert sGlobalStateHolder != null;
        return sGlobalStateHolder.mInstance;
    }

    public static void clearForTesting() {
        sGlobalStateHolder = null;
    }
}