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