1 /* 2 * Copyright (C) 2021 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 android.view; 18 19 import static java.util.Objects.requireNonNull; 20 21 import android.annotation.NonNull; 22 import android.annotation.UiThread; 23 import android.graphics.Rect; 24 import android.os.CancellationSignal; 25 import android.util.IndentingPrintWriter; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 29 import java.util.ArrayList; 30 import java.util.Comparator; 31 import java.util.List; 32 import java.util.concurrent.Executor; 33 import java.util.function.Consumer; 34 35 /** 36 * Collects nodes in the view hierarchy which have been identified as scrollable content. 37 * 38 * @hide 39 */ 40 @UiThread 41 public final class ScrollCaptureSearchResults { 42 private final Executor mExecutor; 43 private final List<ScrollCaptureTarget> mTargets; 44 private final CancellationSignal mCancel; 45 46 private Runnable mOnCompleteListener; 47 private int mCompleted; 48 private boolean mComplete = true; 49 ScrollCaptureSearchResults(Executor executor)50 public ScrollCaptureSearchResults(Executor executor) { 51 mExecutor = executor; 52 mTargets = new ArrayList<>(); 53 mCancel = new CancellationSignal(); 54 } 55 56 // Public 57 58 /** 59 * Add the given target to the results. 60 * 61 * @param target the target to consider 62 */ addTarget(@onNull ScrollCaptureTarget target)63 public void addTarget(@NonNull ScrollCaptureTarget target) { 64 requireNonNull(target); 65 66 mTargets.add(target); 67 mComplete = false; 68 final ScrollCaptureCallback callback = target.getCallback(); 69 final Consumer<Rect> consumer = new SearchRequest(target); 70 71 // Defer so the view hierarchy scan completes first 72 mExecutor.execute( 73 () -> callback.onScrollCaptureSearch(mCancel, consumer)); 74 } 75 isComplete()76 public boolean isComplete() { 77 return mComplete; 78 } 79 80 /** 81 * Provides a callback to be invoked as soon as all responses have been received from all 82 * targets to this point. 83 * 84 * @param onComplete listener to add 85 */ setOnCompleteListener(Runnable onComplete)86 public void setOnCompleteListener(Runnable onComplete) { 87 if (mComplete) { 88 onComplete.run(); 89 } else { 90 mOnCompleteListener = onComplete; 91 } 92 } 93 94 /** 95 * Indicates whether the search results are empty. 96 * 97 * @return true if no targets have been added 98 */ isEmpty()99 public boolean isEmpty() { 100 return mTargets.isEmpty(); 101 } 102 103 /** 104 * Force the results to complete now, cancelling any pending requests and calling a complete 105 * listener if provided. 106 */ finish()107 public void finish() { 108 if (!mComplete) { 109 mCancel.cancel(); 110 signalComplete(); 111 } 112 } 113 signalComplete()114 private void signalComplete() { 115 mComplete = true; 116 mTargets.sort(PRIORITY_ORDER); 117 if (mOnCompleteListener != null) { 118 mOnCompleteListener.run(); 119 mOnCompleteListener = null; 120 } 121 } 122 123 @VisibleForTesting getTargets()124 public List<ScrollCaptureTarget> getTargets() { 125 return new ArrayList<>(mTargets); 126 } 127 128 /** 129 * Get the top ranked result out of all completed requests. 130 * 131 * @return the top ranked result 132 */ getTopResult()133 public ScrollCaptureTarget getTopResult() { 134 ScrollCaptureTarget target = mTargets.isEmpty() ? null : mTargets.get(0); 135 return target != null && target.getScrollBounds() != null ? target : null; 136 } 137 138 private class SearchRequest implements Consumer<Rect> { 139 private ScrollCaptureTarget mTarget; 140 SearchRequest(ScrollCaptureTarget target)141 SearchRequest(ScrollCaptureTarget target) { 142 mTarget = target; 143 } 144 145 @Override accept(Rect scrollBounds)146 public void accept(Rect scrollBounds) { 147 if (mTarget == null || mCancel.isCanceled()) { 148 return; 149 } 150 mExecutor.execute(() -> consume(scrollBounds)); 151 } 152 consume(Rect scrollBounds)153 private void consume(Rect scrollBounds) { 154 if (mTarget == null || mCancel.isCanceled()) { 155 return; 156 } 157 if (!nullOrEmpty(scrollBounds)) { 158 mTarget.setScrollBounds(scrollBounds); 159 mTarget.updatePositionInWindow(); 160 } 161 mCompleted++; 162 mTarget = null; 163 164 // All done? 165 if (mCompleted == mTargets.size()) { 166 signalComplete(); 167 } 168 } 169 } 170 171 private static final int AFTER = 1; 172 private static final int BEFORE = -1; 173 private static final int EQUAL = 0; 174 175 static final Comparator<ScrollCaptureTarget> PRIORITY_ORDER = (a, b) -> { 176 if (a == null && b == null) { 177 return 0; 178 } else if (a == null || b == null) { 179 return (a == null) ? 1 : -1; 180 } 181 182 boolean emptyScrollBoundsA = nullOrEmpty(a.getScrollBounds()); 183 boolean emptyScrollBoundsB = nullOrEmpty(b.getScrollBounds()); 184 if (emptyScrollBoundsA || emptyScrollBoundsB) { 185 if (emptyScrollBoundsA && emptyScrollBoundsB) { 186 return EQUAL; 187 } 188 // Prefer the one with a non-empty scroll bounds 189 if (emptyScrollBoundsA) { 190 return AFTER; 191 } 192 return BEFORE; 193 } 194 195 final View viewA = a.getContainingView(); 196 final View viewB = b.getContainingView(); 197 198 // Prefer any view with scrollCaptureHint="INCLUDE", over one without 199 // This is an escape hatch for the next rule (descendants first) 200 boolean hintIncludeA = hasIncludeHint(viewA); 201 boolean hintIncludeB = hasIncludeHint(viewB); 202 if (hintIncludeA != hintIncludeB) { 203 return (hintIncludeA) ? BEFORE : AFTER; 204 } 205 // If the views are relatives, prefer the descendant. This allows implementations to 206 // leverage nested scrolling APIs by interacting with the innermost scrollable view (as 207 // would happen with touch input). 208 if (isDescendant(viewA, viewB)) { 209 return BEFORE; 210 } 211 if (isDescendant(viewB, viewA)) { 212 return AFTER; 213 } 214 215 // finally, prefer one with larger scroll bounds 216 int scrollAreaA = area(a.getScrollBounds()); 217 int scrollAreaB = area(b.getScrollBounds()); 218 return (scrollAreaA >= scrollAreaB) ? BEFORE : AFTER; 219 }; 220 area(Rect r)221 private static int area(Rect r) { 222 return r.width() * r.height(); 223 } 224 nullOrEmpty(Rect r)225 private static boolean nullOrEmpty(Rect r) { 226 return r == null || r.isEmpty(); 227 } 228 hasIncludeHint(View view)229 private static boolean hasIncludeHint(View view) { 230 return (view.getScrollCaptureHint() & View.SCROLL_CAPTURE_HINT_INCLUDE) != 0; 231 } 232 233 /** 234 * Determines if {@code otherView} is a descendant of {@code view}. 235 * 236 * @param view a view 237 * @param otherView another view 238 * @return true if {@code view} is an ancestor of {@code otherView} 239 */ isDescendant(@onNull View view, @NonNull View otherView)240 private static boolean isDescendant(@NonNull View view, @NonNull View otherView) { 241 if (view == otherView) { 242 return false; 243 } 244 ViewParent otherParent = otherView.getParent(); 245 while (otherParent != view && otherParent != null) { 246 otherParent = otherParent.getParent(); 247 } 248 return otherParent == view; 249 } 250 dump(IndentingPrintWriter writer)251 void dump(IndentingPrintWriter writer) { 252 writer.println("results:"); 253 writer.increaseIndent(); 254 writer.println("complete: " + isComplete()); 255 writer.println("cancelled: " + mCancel.isCanceled()); 256 writer.println("targets:"); 257 writer.increaseIndent(); 258 if (isEmpty()) { 259 writer.println("None"); 260 } else { 261 for (int i = 0; i < mTargets.size(); i++) { 262 writer.println("[" + i + "]"); 263 writer.increaseIndent(); 264 mTargets.get(i).dump(writer); 265 writer.decreaseIndent(); 266 } 267 writer.decreaseIndent(); 268 } 269 writer.decreaseIndent(); 270 } 271 } 272