ContentCaptureSessionCompat.java

/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.view.contentcapture;

import static android.os.Build.VERSION.SDK_INT;

import android.os.Bundle;
import android.view.View;
import android.view.ViewStructure;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ContentCaptureSession;

import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.view.ViewCompat;
import androidx.core.view.ViewStructureCompat;

import java.util.List;
import java.util.Objects;

/**
 * Helper for accessing features in {@link ContentCaptureSession}.
 */
public class ContentCaptureSessionCompat {

    private static final String KEY_VIEW_TREE_APPEARING = "TREAT_AS_VIEW_TREE_APPEARING";
    private static final String KEY_VIEW_TREE_APPEARED = "TREAT_AS_VIEW_TREE_APPEARED";
    // Only guaranteed to be non-null on SDK_INT >= 29.
    private final Object mWrappedObj;
    private final View mView;

    /**
     * Provides a backward-compatible wrapper for {@link ContentCaptureSession}.
     * <p>
     * This method is not supported on devices running SDK < 29 since the platform
     * class will not be available.
     *
     * @param contentCaptureSession platform class to wrap
     * @param host view hosting the session.
     * @return wrapped class
     */
    @RequiresApi(29)
    @NonNull
    public static ContentCaptureSessionCompat toContentCaptureSessionCompat(
            @NonNull ContentCaptureSession contentCaptureSession, @NonNull View host) {
        return new ContentCaptureSessionCompat(contentCaptureSession, host);
    }

    /**
     * Provides the {@link ContentCaptureSession} represented by this object.
     * <p>
     * This method is not supported on devices running SDK < 29 since the platform
     * class will not be available.
     *
     * @return platform class object
     * @see ContentCaptureSessionCompat#toContentCaptureSessionCompat(ContentCaptureSession, View)
     */
    @RequiresApi(29)
    @NonNull
    public ContentCaptureSession toContentCaptureSession() {
        return (ContentCaptureSession) mWrappedObj;
    }

    /**
     * Creates a {@link ContentCaptureSessionCompat} instance.
     *
     * @param contentCaptureSession {@link ContentCaptureSession} for this host View.
     * @param host view hosting the session.
     */
    @RequiresApi(29)
    private ContentCaptureSessionCompat(@NonNull ContentCaptureSession contentCaptureSession,
            @NonNull View host) {
        this.mWrappedObj = contentCaptureSession;
        this.mView = host;
    }

    /**
     * Creates a new {@link AutofillId} for a virtual child, so it can be used to uniquely identify
     * the children in the session.
     *
     * Compatibility behavior:
     * <ul>
     * <li>SDK 29 and above, this method matches platform behavior.
     * <li>SDK 28 and below, this method returns null.
     * </ul>
     *
     * @param virtualChildId id of the virtual child, relative to the parent.
     *
     * @return {@link AutofillId} for the virtual child
     */
    @Nullable
    public AutofillId newAutofillId(long virtualChildId) {
        if (SDK_INT >= 29) {
            return Api29Impl.newAutofillId(
                    (ContentCaptureSession) mWrappedObj,
                    Objects.requireNonNull(ViewCompat.getAutofillId(mView)).toAutofillId(),
                    virtualChildId);
        }
        return null;
    }

    /**
     * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to
     * {@link #notifyViewsAppeared} by the view managing the virtual view hierarchy.
     *
     * Compatibility behavior:
     * <ul>
     * <li>SDK 29 and above, this method matches platform behavior.
     * <li>SDK 28 and below, this method returns null.
     * </ul>
     *
     * @param parentId id of the virtual view parent (it can be obtained by calling
     * {@link ViewStructure#getAutofillId()} on the parent).
     * @param virtualId id of the virtual child, relative to the parent.
     *
     * @return a new {@link ViewStructure} that can be used for Content Capture purposes.
     */
    @Nullable
    public ViewStructureCompat newVirtualViewStructure(
            @NonNull AutofillId parentId, long virtualId) {
        if (SDK_INT >= 29) {
            return ViewStructureCompat.toViewStructureCompat(
                    Api29Impl.newVirtualViewStructure(
                            (ContentCaptureSession) mWrappedObj, parentId, virtualId));
        }
        return null;
    }

    /**
     * Notifies the Content Capture Service that a list of nodes has appeared in the view structure.
     *
     * <p>Typically called manually by views that handle their own virtual view hierarchy.
     *
     * Compatibility behavior:
     * <ul>
     * <li>SDK 34 and above, this method matches platform behavior.
     * <li>SDK 29 through 33, this method is a best-effort to match platform behavior, by
     * wrapping the virtual children with a pair of special view appeared events.
     * <li>SDK 28 and below, this method does nothing.
     *
     * @param appearedNodes nodes that have appeared. Each element represents a view node that has
     * been added to the view structure. The order of the elements is important, which should be
     * preserved as the attached order of when the node is attached to the virtual view hierarchy.
     */
    public void notifyViewsAppeared(@NonNull List<ViewStructure> appearedNodes) {
        if (SDK_INT >= 34) {
            Api34Impl.notifyViewsAppeared((ContentCaptureSession) mWrappedObj, appearedNodes);
        } else if (SDK_INT >= 29) {
            ViewStructure treeAppearing = Api29Impl.newViewStructure(
                    (ContentCaptureSession) mWrappedObj, mView);
            Api23Impl.getExtras(treeAppearing).putBoolean(KEY_VIEW_TREE_APPEARING, true);
            Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppearing);

            for (int i = 0; i < appearedNodes.size(); i++) {
                Api29Impl.notifyViewAppeared(
                        (ContentCaptureSession) mWrappedObj, appearedNodes.get(i));
            }

            ViewStructure treeAppeared = Api29Impl.newViewStructure(
                    (ContentCaptureSession) mWrappedObj, mView);
            Api23Impl.getExtras(treeAppeared).putBoolean(KEY_VIEW_TREE_APPEARED, true);
            Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppeared);
        }
    }

    /**
     * Notifies the Content Capture Service that many nodes has been removed from a virtual view
     * structure.
     *
     * <p>Should only be called by views that handle their own virtual view hierarchy.
     *
     * Compatibility behavior:
     * <ul>
     * <li>SDK 34 and above, this method matches platform behavior.
     * <li>SDK 29 through 33, this method is a best-effort to match platform behavior, by
     * wrapping the virtual children with a pair of special view appeared events.
     * <li>SDK 28 and below, this method does nothing.
     * </ul>
     *
     * @param virtualIds ids of the virtual children.
     */
    public void notifyViewsDisappeared(@NonNull long[] virtualIds) {
        if (SDK_INT >= 34) {
            Api29Impl.notifyViewsDisappeared(
                    (ContentCaptureSession) mWrappedObj,
                    Objects.requireNonNull(ViewCompat.getAutofillId(mView)).toAutofillId(),
                    virtualIds);
        } else if (SDK_INT >= 29) {
            ViewStructure treeAppearing = Api29Impl.newViewStructure(
                    (ContentCaptureSession) mWrappedObj, mView);
            Api23Impl.getExtras(treeAppearing).putBoolean(KEY_VIEW_TREE_APPEARING, true);
            Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppearing);

            Api29Impl.notifyViewsDisappeared(
                    (ContentCaptureSession) mWrappedObj,
                    Objects.requireNonNull(ViewCompat.getAutofillId(mView)).toAutofillId(),
                    virtualIds);

            ViewStructure treeAppeared = Api29Impl.newViewStructure(
                    (ContentCaptureSession) mWrappedObj, mView);
            Api23Impl.getExtras(treeAppeared).putBoolean(KEY_VIEW_TREE_APPEARED, true);
            Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppeared);
        }
    }

    /**
     * Notifies the Intelligence Service that the value of a text node has been changed.
     *
     * Compatibility behavior:
     * <ul>
     * <li>SDK 29 and above, this method matches platform behavior.
     * <li>SDK 28 and below, this method does nothing.
     * </ul>
     *
     * @param id of the node.
     * @param text new text.
     */
    public void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
        if (SDK_INT >= 29) {
            Api29Impl.notifyViewTextChanged((ContentCaptureSession) mWrappedObj, id, text);
        }
    }

    @RequiresApi(34)
    private static class Api34Impl {
        private Api34Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static void notifyViewsAppeared(
                ContentCaptureSession contentCaptureSession, List<ViewStructure> appearedNodes) {
            // new API in U
            // contentCaptureSession.notifyViewsAppeared(appearedNodes);
        }
    }
    @RequiresApi(29)
    private static class Api29Impl {
        private Api29Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static void notifyViewsDisappeared(
                ContentCaptureSession contentCaptureSession, AutofillId hostId, long[] virtualIds) {
            contentCaptureSession.notifyViewsDisappeared(hostId, virtualIds);
        }

        @DoNotInline
        static void notifyViewAppeared(
                ContentCaptureSession contentCaptureSession, ViewStructure node) {
            contentCaptureSession.notifyViewAppeared(node);
        }
        @DoNotInline
        static ViewStructure newViewStructure(
                ContentCaptureSession contentCaptureSession, View view) {
            return contentCaptureSession.newViewStructure(view);
        }

        @DoNotInline
        static ViewStructure newVirtualViewStructure(ContentCaptureSession contentCaptureSession,
                AutofillId parentId, long virtualId) {
            return contentCaptureSession.newVirtualViewStructure(parentId, virtualId);
        }


        @DoNotInline
        static AutofillId newAutofillId(ContentCaptureSession contentCaptureSession,
                AutofillId hostId, long virtualChildId) {
            return contentCaptureSession.newAutofillId(hostId, virtualChildId);
        }

        @DoNotInline
        public static void notifyViewTextChanged(ContentCaptureSession contentCaptureSession,
                AutofillId id, CharSequence charSequence) {
            contentCaptureSession.notifyViewTextChanged(id, charSequence);

        }
    }
    @RequiresApi(23)
    private static class Api23Impl {
        private Api23Impl() {
            // This class is not instantiable.
        }

        @DoNotInline
        static Bundle getExtras(ViewStructure viewStructure) {
            return viewStructure.getExtras();
        }

    }
}