1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.core.view.contentcapture;
18 
19 import static android.os.Build.VERSION.SDK_INT;
20 
21 import android.os.Bundle;
22 import android.view.View;
23 import android.view.ViewStructure;
24 import android.view.autofill.AutofillId;
25 import android.view.contentcapture.ContentCaptureSession;
26 
27 import androidx.annotation.RequiresApi;
28 import androidx.core.view.ViewCompat;
29 import androidx.core.view.ViewStructureCompat;
30 
31 import org.jspecify.annotations.NonNull;
32 import org.jspecify.annotations.Nullable;
33 
34 import java.util.List;
35 import java.util.Objects;
36 
37 /**
38  * Helper for accessing features in {@link ContentCaptureSession}.
39  */
40 public class ContentCaptureSessionCompat {
41 
42     private static final String KEY_VIEW_TREE_APPEARING = "TREAT_AS_VIEW_TREE_APPEARING";
43     private static final String KEY_VIEW_TREE_APPEARED = "TREAT_AS_VIEW_TREE_APPEARED";
44     // Only guaranteed to be non-null on SDK_INT >= 29.
45     private final Object mWrappedObj;
46     private final View mView;
47 
48     /**
49      * Provides a backward-compatible wrapper for {@link ContentCaptureSession}.
50      * <p>
51      * This method is not supported on devices running SDK < 29 since the platform
52      * class will not be available.
53      *
54      * @param contentCaptureSession platform class to wrap
55      * @param host view hosting the session.
56      * @return wrapped class
57      */
58     @RequiresApi(29)
toContentCaptureSessionCompat( @onNull ContentCaptureSession contentCaptureSession, @NonNull View host)59     public static @NonNull ContentCaptureSessionCompat toContentCaptureSessionCompat(
60             @NonNull ContentCaptureSession contentCaptureSession, @NonNull View host) {
61         return new ContentCaptureSessionCompat(contentCaptureSession, host);
62     }
63 
64     /**
65      * Provides the {@link ContentCaptureSession} represented by this object.
66      * <p>
67      * This method is not supported on devices running SDK < 29 since the platform
68      * class will not be available.
69      *
70      * @return platform class object
71      * @see ContentCaptureSessionCompat#toContentCaptureSessionCompat(ContentCaptureSession, View)
72      */
73     @RequiresApi(29)
toContentCaptureSession()74     public @NonNull ContentCaptureSession toContentCaptureSession() {
75         return (ContentCaptureSession) mWrappedObj;
76     }
77 
78     /**
79      * Creates a {@link ContentCaptureSessionCompat} instance.
80      *
81      * @param contentCaptureSession {@link ContentCaptureSession} for this host View.
82      * @param host view hosting the session.
83      */
84     @RequiresApi(29)
ContentCaptureSessionCompat(@onNull ContentCaptureSession contentCaptureSession, @NonNull View host)85     private ContentCaptureSessionCompat(@NonNull ContentCaptureSession contentCaptureSession,
86             @NonNull View host) {
87         this.mWrappedObj = contentCaptureSession;
88         this.mView = host;
89     }
90 
91     /**
92      * Creates a new {@link AutofillId} for a virtual child, so it can be used to uniquely identify
93      * the children in the session.
94      *
95      * Compatibility behavior:
96      * <ul>
97      * <li>SDK 29 and above, this method matches platform behavior.
98      * <li>SDK 28 and below, this method returns null.
99      * </ul>
100      *
101      * @param virtualChildId id of the virtual child, relative to the parent.
102      *
103      * @return {@link AutofillId} for the virtual child
104      */
newAutofillId(long virtualChildId)105     public @Nullable AutofillId newAutofillId(long virtualChildId) {
106         if (SDK_INT >= 29) {
107             return Api29Impl.newAutofillId(
108                     (ContentCaptureSession) mWrappedObj,
109                     Objects.requireNonNull(ViewCompat.getAutofillId(mView)).toAutofillId(),
110                     virtualChildId);
111         }
112         return null;
113     }
114 
115     /**
116      * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to
117      * {@link #notifyViewsAppeared} by the view managing the virtual view hierarchy.
118      *
119      * Compatibility behavior:
120      * <ul>
121      * <li>SDK 29 and above, this method matches platform behavior.
122      * <li>SDK 28 and below, this method returns null.
123      * </ul>
124      *
125      * @param parentId id of the virtual view parent (it can be obtained by calling
126      * {@link ViewStructure#getAutofillId()} on the parent).
127      * @param virtualId id of the virtual child, relative to the parent.
128      *
129      * @return a new {@link ViewStructure} that can be used for Content Capture purposes.
130      */
newVirtualViewStructure( @onNull AutofillId parentId, long virtualId)131     public @Nullable ViewStructureCompat newVirtualViewStructure(
132             @NonNull AutofillId parentId, long virtualId) {
133         if (SDK_INT >= 29) {
134             return ViewStructureCompat.toViewStructureCompat(
135                     Api29Impl.newVirtualViewStructure(
136                             (ContentCaptureSession) mWrappedObj, parentId, virtualId));
137         }
138         return null;
139     }
140 
141     /**
142      * Notifies the Content Capture Service that a list of nodes has appeared in the view structure.
143      *
144      * <p>Typically called manually by views that handle their own virtual view hierarchy.
145      *
146      * Compatibility behavior:
147      * <ul>
148      * <li>SDK 34 and above, this method matches platform behavior.
149      * <li>SDK 29 through 33, this method is a best-effort to match platform behavior, by
150      * wrapping the virtual children with a pair of special view appeared events.
151      * <li>SDK 28 and below, this method does nothing.
152      *
153      * @param appearedNodes nodes that have appeared. Each element represents a view node that has
154      * been added to the view structure. The order of the elements is important, which should be
155      * preserved as the attached order of when the node is attached to the virtual view hierarchy.
156      */
notifyViewsAppeared(@onNull List<ViewStructure> appearedNodes)157     public void notifyViewsAppeared(@NonNull List<ViewStructure> appearedNodes) {
158         if (SDK_INT >= 34) {
159             Api34Impl.notifyViewsAppeared((ContentCaptureSession) mWrappedObj, appearedNodes);
160         } else if (SDK_INT >= 29) {
161             ViewStructure treeAppearing = Api29Impl.newViewStructure(
162                     (ContentCaptureSession) mWrappedObj, mView);
163             Api23Impl.getExtras(treeAppearing).putBoolean(KEY_VIEW_TREE_APPEARING, true);
164             Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppearing);
165 
166             for (int i = 0; i < appearedNodes.size(); i++) {
167                 Api29Impl.notifyViewAppeared(
168                         (ContentCaptureSession) mWrappedObj, appearedNodes.get(i));
169             }
170 
171             ViewStructure treeAppeared = Api29Impl.newViewStructure(
172                     (ContentCaptureSession) mWrappedObj, mView);
173             Api23Impl.getExtras(treeAppeared).putBoolean(KEY_VIEW_TREE_APPEARED, true);
174             Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppeared);
175         }
176     }
177 
178     /**
179      * Notifies the Content Capture Service that many nodes has been removed from a virtual view
180      * structure.
181      *
182      * <p>Should only be called by views that handle their own virtual view hierarchy.
183      *
184      * Compatibility behavior:
185      * <ul>
186      * <li>SDK 34 and above, this method matches platform behavior.
187      * <li>SDK 29 through 33, this method is a best-effort to match platform behavior, by
188      * wrapping the virtual children with a pair of special view appeared events.
189      * <li>SDK 28 and below, this method does nothing.
190      * </ul>
191      *
192      * @param virtualIds ids of the virtual children.
193      */
notifyViewsDisappeared(long @NonNull [] virtualIds)194     public void notifyViewsDisappeared(long @NonNull [] virtualIds) {
195         if (SDK_INT >= 34) {
196             Api29Impl.notifyViewsDisappeared(
197                     (ContentCaptureSession) mWrappedObj,
198                     Objects.requireNonNull(ViewCompat.getAutofillId(mView)).toAutofillId(),
199                     virtualIds);
200         } else if (SDK_INT >= 29) {
201             ViewStructure treeAppearing = Api29Impl.newViewStructure(
202                     (ContentCaptureSession) mWrappedObj, mView);
203             Api23Impl.getExtras(treeAppearing).putBoolean(KEY_VIEW_TREE_APPEARING, true);
204             Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppearing);
205 
206             Api29Impl.notifyViewsDisappeared(
207                     (ContentCaptureSession) mWrappedObj,
208                     Objects.requireNonNull(ViewCompat.getAutofillId(mView)).toAutofillId(),
209                     virtualIds);
210 
211             ViewStructure treeAppeared = Api29Impl.newViewStructure(
212                     (ContentCaptureSession) mWrappedObj, mView);
213             Api23Impl.getExtras(treeAppeared).putBoolean(KEY_VIEW_TREE_APPEARED, true);
214             Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppeared);
215         }
216     }
217 
218     /**
219      * Notifies the Intelligence Service that the value of a text node has been changed.
220      *
221      * Compatibility behavior:
222      * <ul>
223      * <li>SDK 29 and above, this method matches platform behavior.
224      * <li>SDK 28 and below, this method does nothing.
225      * </ul>
226      *
227      * @param id of the node.
228      * @param text new text.
229      */
notifyViewTextChanged(@onNull AutofillId id, @Nullable CharSequence text)230     public void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
231         if (SDK_INT >= 29) {
232             Api29Impl.notifyViewTextChanged((ContentCaptureSession) mWrappedObj, id, text);
233         }
234     }
235 
236     @RequiresApi(34)
237     private static class Api34Impl {
Api34Impl()238         private Api34Impl() {
239             // This class is not instantiable.
240         }
241 
notifyViewsAppeared( ContentCaptureSession contentCaptureSession, List<ViewStructure> appearedNodes)242         static void notifyViewsAppeared(
243                 ContentCaptureSession contentCaptureSession, List<ViewStructure> appearedNodes) {
244             contentCaptureSession.notifyViewsAppeared(appearedNodes);
245         }
246     }
247     @RequiresApi(29)
248     private static class Api29Impl {
Api29Impl()249         private Api29Impl() {
250             // This class is not instantiable.
251         }
252 
notifyViewsDisappeared( ContentCaptureSession contentCaptureSession, AutofillId hostId, long[] virtualIds)253         static void notifyViewsDisappeared(
254                 ContentCaptureSession contentCaptureSession, AutofillId hostId, long[] virtualIds) {
255             contentCaptureSession.notifyViewsDisappeared(hostId, virtualIds);
256         }
257 
notifyViewAppeared( ContentCaptureSession contentCaptureSession, ViewStructure node)258         static void notifyViewAppeared(
259                 ContentCaptureSession contentCaptureSession, ViewStructure node) {
260             contentCaptureSession.notifyViewAppeared(node);
261         }
newViewStructure( ContentCaptureSession contentCaptureSession, View view)262         static ViewStructure newViewStructure(
263                 ContentCaptureSession contentCaptureSession, View view) {
264             return contentCaptureSession.newViewStructure(view);
265         }
266 
newVirtualViewStructure(ContentCaptureSession contentCaptureSession, AutofillId parentId, long virtualId)267         static ViewStructure newVirtualViewStructure(ContentCaptureSession contentCaptureSession,
268                 AutofillId parentId, long virtualId) {
269             return contentCaptureSession.newVirtualViewStructure(parentId, virtualId);
270         }
271 
272 
newAutofillId(ContentCaptureSession contentCaptureSession, AutofillId hostId, long virtualChildId)273         static AutofillId newAutofillId(ContentCaptureSession contentCaptureSession,
274                 AutofillId hostId, long virtualChildId) {
275             return contentCaptureSession.newAutofillId(hostId, virtualChildId);
276         }
277 
notifyViewTextChanged(ContentCaptureSession contentCaptureSession, AutofillId id, CharSequence charSequence)278         public static void notifyViewTextChanged(ContentCaptureSession contentCaptureSession,
279                 AutofillId id, CharSequence charSequence) {
280             contentCaptureSession.notifyViewTextChanged(id, charSequence);
281 
282         }
283     }
284     @RequiresApi(23)
285     private static class Api23Impl {
Api23Impl()286         private Api23Impl() {
287             // This class is not instantiable.
288         }
289 
getExtras(ViewStructure viewStructure)290         static Bundle getExtras(ViewStructure viewStructure) {
291             return viewStructure.getExtras();
292         }
293 
294     }
295 }
296