1 /* 2 * Copyright (C) 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 android.view.contentprotection; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.ContentCaptureOptions; 22 import android.content.pm.ParceledListSlice; 23 import android.os.Handler; 24 import android.util.Log; 25 import android.view.contentcapture.ContentCaptureEvent; 26 import android.view.contentcapture.IContentCaptureManager; 27 import android.view.contentcapture.ViewNode; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.internal.util.RingBuffer; 31 32 import java.time.Duration; 33 import java.time.Instant; 34 import java.util.Arrays; 35 import java.util.Collection; 36 import java.util.List; 37 import java.util.Set; 38 import java.util.stream.Stream; 39 40 /** 41 * Main entry point for processing {@link ContentCaptureEvent} for the content protection flow. 42 * 43 * @hide 44 */ 45 public class ContentProtectionEventProcessor { 46 47 private static final String TAG = "ContentProtectionEventProcessor"; 48 49 private static final Duration MIN_DURATION_BETWEEN_FLUSHING = Duration.ofSeconds(3); 50 51 private static final Set<Integer> EVENT_TYPES_TO_STORE = 52 Set.of( 53 ContentCaptureEvent.TYPE_VIEW_APPEARED, 54 ContentCaptureEvent.TYPE_VIEW_DISAPPEARED, 55 ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED); 56 57 private static final int RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS = 150; 58 59 @NonNull private final RingBuffer<ContentCaptureEvent> mEventBuffer; 60 61 @NonNull private final Handler mHandler; 62 63 @NonNull private final IContentCaptureManager mContentCaptureManager; 64 65 @NonNull private final String mPackageName; 66 67 @NonNull private final ContentCaptureOptions.ContentProtectionOptions mOptions; 68 69 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) 70 @Nullable 71 public Instant mLastFlushTime; 72 73 private int mResetLoginRemainingEventsToProcess; 74 75 private boolean mAnyGroupFound = false; 76 77 // Ordered by priority 78 private final List<SearchGroup> mGroupsRequired; 79 80 // Ordered by priority 81 private final List<SearchGroup> mGroupsOptional; 82 83 // Ordered by priority 84 private final List<SearchGroup> mGroupsAll; 85 ContentProtectionEventProcessor( @onNull RingBuffer<ContentCaptureEvent> eventBuffer, @NonNull Handler handler, @NonNull IContentCaptureManager contentCaptureManager, @NonNull String packageName, @NonNull ContentCaptureOptions.ContentProtectionOptions options)86 public ContentProtectionEventProcessor( 87 @NonNull RingBuffer<ContentCaptureEvent> eventBuffer, 88 @NonNull Handler handler, 89 @NonNull IContentCaptureManager contentCaptureManager, 90 @NonNull String packageName, 91 @NonNull ContentCaptureOptions.ContentProtectionOptions options) { 92 mEventBuffer = eventBuffer; 93 mHandler = handler; 94 mContentCaptureManager = contentCaptureManager; 95 mPackageName = packageName; 96 mOptions = options; 97 mGroupsRequired = options.requiredGroups.stream().map(SearchGroup::new).toList(); 98 mGroupsOptional = options.optionalGroups.stream().map(SearchGroup::new).toList(); 99 mGroupsAll = 100 Stream.of(mGroupsRequired, mGroupsOptional).flatMap(Collection::stream).toList(); 101 } 102 103 /** Main entry point for {@link ContentCaptureEvent} processing. */ processEvent(@onNull ContentCaptureEvent event)104 public void processEvent(@NonNull ContentCaptureEvent event) { 105 if (EVENT_TYPES_TO_STORE.contains(event.getType())) { 106 storeEvent(event); 107 } 108 if (event.getType() == ContentCaptureEvent.TYPE_VIEW_APPEARED) { 109 processViewAppearedEvent(event); 110 } 111 } 112 storeEvent(@onNull ContentCaptureEvent event)113 private void storeEvent(@NonNull ContentCaptureEvent event) { 114 // Ensure receiver gets the package name which might not be set 115 ViewNode viewNode = (event.getViewNode() != null) ? event.getViewNode() : new ViewNode(); 116 viewNode.setTextIdEntry(mPackageName); 117 event.setViewNode(viewNode); 118 mEventBuffer.append(event); 119 } 120 processViewAppearedEvent(@onNull ContentCaptureEvent event)121 private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) { 122 ViewNode viewNode = event.getViewNode(); 123 String eventText = ContentProtectionUtils.getEventTextLower(event); 124 String viewNodeText = ContentProtectionUtils.getViewNodeTextLower(viewNode); 125 String hintText = ContentProtectionUtils.getHintTextLower(viewNode); 126 127 mGroupsAll.stream() 128 .filter(group -> !group.mFound) 129 .filter( 130 group -> 131 group.matches(eventText) 132 || group.matches(viewNodeText) 133 || group.matches(hintText)) 134 .findFirst() 135 .ifPresent( 136 group -> { 137 group.mFound = true; 138 mAnyGroupFound = true; 139 }); 140 141 boolean loginDetected = 142 mGroupsRequired.stream().allMatch(group -> group.mFound) 143 && mGroupsOptional.stream().filter(group -> group.mFound).count() 144 >= mOptions.optionalGroupsThreshold; 145 146 if (loginDetected) { 147 loginDetected(); 148 } else { 149 maybeResetLoginFlags(); 150 } 151 } 152 loginDetected()153 private void loginDetected() { 154 if (mLastFlushTime == null 155 || Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) { 156 flush(); 157 } 158 resetLoginFlags(); 159 } 160 resetLoginFlags()161 private void resetLoginFlags() { 162 mGroupsAll.forEach(group -> group.mFound = false); 163 mAnyGroupFound = false; 164 } 165 maybeResetLoginFlags()166 private void maybeResetLoginFlags() { 167 if (mAnyGroupFound) { 168 if (mResetLoginRemainingEventsToProcess <= 0) { 169 mResetLoginRemainingEventsToProcess = RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS; 170 } else { 171 mResetLoginRemainingEventsToProcess--; 172 if (mResetLoginRemainingEventsToProcess <= 0) { 173 resetLoginFlags(); 174 } 175 } 176 } 177 } 178 flush()179 private void flush() { 180 mLastFlushTime = Instant.now(); 181 182 // Note the thread annotations, do not move clearEvents to mHandler 183 ParceledListSlice<ContentCaptureEvent> events = clearEvents(); 184 mHandler.post(() -> handlerOnLoginDetected(events)); 185 } 186 187 @NonNull clearEvents()188 private ParceledListSlice<ContentCaptureEvent> clearEvents() { 189 List<ContentCaptureEvent> events = Arrays.asList(mEventBuffer.toArray()); 190 mEventBuffer.clear(); 191 return new ParceledListSlice<>(events); 192 } 193 handlerOnLoginDetected(@onNull ParceledListSlice<ContentCaptureEvent> events)194 private void handlerOnLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) { 195 try { 196 mContentCaptureManager.onLoginDetected(events); 197 } catch (Exception ex) { 198 Log.e(TAG, "Failed to flush events for: " + mPackageName, ex); 199 } 200 } 201 202 private static final class SearchGroup { 203 204 @NonNull private final List<String> mSearchStrings; 205 206 public boolean mFound = false; 207 SearchGroup(@onNull List<String> searchStrings)208 SearchGroup(@NonNull List<String> searchStrings) { 209 mSearchStrings = searchStrings; 210 } 211 matches(@ullable String text)212 public boolean matches(@Nullable String text) { 213 if (text == null) { 214 return false; 215 } 216 return mSearchStrings.stream().anyMatch(text::contains); 217 } 218 } 219 } 220