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