diff options
| author | 2023-05-29 00:12:19 +0000 | |
|---|---|---|
| committer | 2023-05-31 21:55:26 +0000 | |
| commit | 93b34c38962b2f7cf47f571a8883d3777c996d80 (patch) | |
| tree | e55d2fa8a34b5b651ddb11704d49906142c1ee15 | |
| parent | 90ca501cdfb2563d80d404fdf092516d7605e1bf (diff) | |
Implement content protection processor
Bug: 275732576
Test: Added new tests
Change-Id: Ic00758e7f538de7b0ed62d96f5cb7156bff786ab
2 files changed, 705 insertions, 0 deletions
diff --git a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java new file mode 100644 index 000000000000..0840b66e5d1e --- /dev/null +++ b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.contentprotection; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UiThread; +import android.content.pm.ParceledListSlice; +import android.os.Handler; +import android.text.InputType; +import android.util.Log; +import android.view.contentcapture.ContentCaptureEvent; +import android.view.contentcapture.IContentCaptureManager; +import android.view.contentcapture.ViewNode; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.RingBuffer; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Main entry point for processing {@link ContentCaptureEvent} for the content protection flow. + * + * @hide + */ +public class ContentProtectionEventProcessor { + + private static final String TAG = "ContentProtectionEventProcessor"; + + private static final List<Integer> PASSWORD_FIELD_INPUT_TYPES = + Collections.unmodifiableList( + Arrays.asList( + InputType.TYPE_NUMBER_VARIATION_PASSWORD, + InputType.TYPE_TEXT_VARIATION_PASSWORD, + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD, + InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD)); + + private static final List<String> PASSWORD_TEXTS = + Collections.unmodifiableList( + Arrays.asList("password", "pass word", "code", "pin", "credential")); + + private static final List<String> ADDITIONAL_SUSPICIOUS_TEXTS = + Collections.unmodifiableList( + Arrays.asList("user", "mail", "phone", "number", "login", "log in", "sign in")); + + private static final Duration MIN_DURATION_BETWEEN_FLUSHING = Duration.ofSeconds(3); + + private static final String ANDROID_CLASS_NAME_PREFIX = "android."; + + private static final Set<Integer> EVENT_TYPES_TO_STORE = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList( + ContentCaptureEvent.TYPE_VIEW_APPEARED, + ContentCaptureEvent.TYPE_VIEW_DISAPPEARED, + ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED))); + + @NonNull private final RingBuffer<ContentCaptureEvent> mEventBuffer; + + @NonNull private final Handler mHandler; + + @NonNull private final IContentCaptureManager mContentCaptureManager; + + @NonNull private final String mPackageName; + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public boolean mPasswordFieldDetected = false; + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public boolean mSuspiciousTextDetected = false; + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + @Nullable + public Instant mLastFlushTime; + + public ContentProtectionEventProcessor( + @NonNull RingBuffer<ContentCaptureEvent> eventBuffer, + @NonNull Handler handler, + @NonNull IContentCaptureManager contentCaptureManager, + @NonNull String packageName) { + mEventBuffer = eventBuffer; + mHandler = handler; + mContentCaptureManager = contentCaptureManager; + mPackageName = packageName; + } + + /** Main entry point for {@link ContentCaptureEvent} processing. */ + @UiThread + public void processEvent(@NonNull ContentCaptureEvent event) { + if (EVENT_TYPES_TO_STORE.contains(event.getType())) { + storeEvent(event); + } + if (event.getType() == ContentCaptureEvent.TYPE_VIEW_APPEARED) { + processViewAppearedEvent(event); + } + } + + @UiThread + private void storeEvent(@NonNull ContentCaptureEvent event) { + // Ensure receiver gets the package name which might not be set + ViewNode viewNode = (event.getViewNode() != null) ? event.getViewNode() : new ViewNode(); + viewNode.setTextIdEntry(mPackageName); + event.setViewNode(viewNode); + mEventBuffer.append(event); + } + + @UiThread + private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) { + mPasswordFieldDetected |= isPasswordField(event); + mSuspiciousTextDetected |= isSuspiciousText(event); + if (mPasswordFieldDetected && mSuspiciousTextDetected) { + loginDetected(); + } + } + + @UiThread + private void loginDetected() { + if (mLastFlushTime == null + || Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) { + flush(); + } + mPasswordFieldDetected = false; + mSuspiciousTextDetected = false; + } + + @UiThread + private void flush() { + mLastFlushTime = Instant.now(); + + // Note the thread annotations, do not move clearEvents to mHandler + ParceledListSlice<ContentCaptureEvent> events = clearEvents(); + mHandler.post(() -> handlerOnLoginDetected(events)); + } + + @UiThread + @NonNull + private ParceledListSlice<ContentCaptureEvent> clearEvents() { + List<ContentCaptureEvent> events = Arrays.asList(mEventBuffer.toArray()); + mEventBuffer.clear(); + return new ParceledListSlice<>(events); + } + + private void handlerOnLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) { + try { + // TODO(b/275732576): Call mContentCaptureManager + } catch (Exception ex) { + Log.e(TAG, "Failed to flush events for: " + mPackageName, ex); + } + } + + private boolean isPasswordField(@NonNull ContentCaptureEvent event) { + return isPasswordField(event.getViewNode()); + } + + private boolean isPasswordField(@Nullable ViewNode viewNode) { + if (viewNode == null) { + return false; + } + return isAndroidPasswordField(viewNode) || isWebViewPasswordField(viewNode); + } + + private boolean isAndroidPasswordField(@NonNull ViewNode viewNode) { + if (!isAndroidViewNode(viewNode)) { + return false; + } + int inputType = viewNode.getInputType(); + return PASSWORD_FIELD_INPUT_TYPES.stream() + .anyMatch(passwordInputType -> (inputType & passwordInputType) != 0); + } + + private boolean isWebViewPasswordField(@NonNull ViewNode viewNode) { + if (viewNode.getClassName() != null) { + return false; + } + return isPasswordText(ContentProtectionUtils.getViewNodeText(viewNode)); + } + + private boolean isAndroidViewNode(@NonNull ViewNode viewNode) { + String className = viewNode.getClassName(); + return className != null && className.startsWith(ANDROID_CLASS_NAME_PREFIX); + } + + private boolean isSuspiciousText(@NonNull ContentCaptureEvent event) { + return isSuspiciousText(ContentProtectionUtils.getEventText(event)) + || isSuspiciousText(ContentProtectionUtils.getViewNodeText(event)); + } + + private boolean isSuspiciousText(@Nullable String text) { + if (text == null) { + return false; + } + if (isPasswordText(text)) { + return true; + } + String lowerCaseText = text.toLowerCase(); + return ADDITIONAL_SUSPICIOUS_TEXTS.stream() + .anyMatch(suspiciousText -> lowerCaseText.contains(suspiciousText)); + } + + private boolean isPasswordText(@Nullable String text) { + if (text == null) { + return false; + } + String lowerCaseText = text.toLowerCase(); + return PASSWORD_TEXTS.stream() + .anyMatch(passwordText -> lowerCaseText.contains(passwordText)); + } +} diff --git a/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java new file mode 100644 index 000000000000..fd5627da6d39 --- /dev/null +++ b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java @@ -0,0 +1,477 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.contentprotection; + +import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED; +import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED; +import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.annotation.Nullable; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.InputType; +import android.view.View; +import android.view.contentcapture.ContentCaptureEvent; +import android.view.contentcapture.IContentCaptureManager; +import android.view.contentcapture.ViewNode; +import android.view.contentcapture.ViewNode.ViewStructureImpl; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.util.RingBuffer; + +import com.google.common.collect.ImmutableSet; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Test for {@link ContentProtectionEventProcessor}. + * + * <p>Run with: {@code atest + * FrameworksCoreTests:android.view.contentprotection.ContentProtectionEventProcessorTest} + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ContentProtectionEventProcessorTest { + + private static final String PACKAGE_NAME = "com.test.package.name"; + + private static final String ANDROID_CLASS_NAME = "android.test.some.class.name"; + + private static final String PASSWORD_TEXT = "ENTER PASSWORD HERE"; + + private static final String SUSPICIOUS_TEXT = "PLEASE SIGN IN"; + + private static final String SAFE_TEXT = "SAFE TEXT"; + + private static final ContentCaptureEvent PROCESS_EVENT = createProcessEvent(); + + private static final Set<Integer> EVENT_TYPES_TO_STORE = + ImmutableSet.of(TYPE_VIEW_APPEARED, TYPE_VIEW_DISAPPEARED, TYPE_VIEW_TEXT_CHANGED); + + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock private RingBuffer<ContentCaptureEvent> mMockEventBuffer; + + @Mock private IContentCaptureManager mMockContentCaptureManager; + + private final Context mContext = ApplicationProvider.getApplicationContext(); + + private ContentProtectionEventProcessor mContentProtectionEventProcessor; + + @Before + public void setup() { + mContentProtectionEventProcessor = + new ContentProtectionEventProcessor( + mMockEventBuffer, + new Handler(Looper.getMainLooper()), + mMockContentCaptureManager, + PACKAGE_NAME); + } + + @Test + public void processEvent_buffer_storesOnlySubsetOfEventTypes() { + List<ContentCaptureEvent> expectedEvents = new ArrayList<>(); + for (int type = -100; type <= 100; type++) { + ContentCaptureEvent event = createEvent(type); + if (EVENT_TYPES_TO_STORE.contains(type)) { + expectedEvents.add(event); + } + + mContentProtectionEventProcessor.processEvent(event); + } + + assertThat(expectedEvents).hasSize(EVENT_TYPES_TO_STORE.size()); + expectedEvents.forEach((expectedEvent) -> verify(mMockEventBuffer).append(expectedEvent)); + verifyNoMoreInteractions(mMockEventBuffer); + } + + @Test + public void processEvent_buffer_setsTextIdEntry_withoutExistingViewNode() { + ContentCaptureEvent event = createStoreEvent(); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(event.getViewNode()).isNotNull(); + assertThat(event.getViewNode().getTextIdEntry()).isEqualTo(PACKAGE_NAME); + verify(mMockEventBuffer).append(event); + } + + @Test + public void processEvent_buffer_setsTextIdEntry_withExistingViewNode() { + ViewNode viewNode = new ViewNode(); + viewNode.setTextIdEntry(PACKAGE_NAME + "TO BE OVERWRITTEN"); + ContentCaptureEvent event = createStoreEvent(); + event.setViewNode(viewNode); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(event.getViewNode()).isSameInstanceAs(viewNode); + assertThat(viewNode.getTextIdEntry()).isEqualTo(PACKAGE_NAME); + verify(mMockEventBuffer).append(event); + } + + @Test + public void processEvent_loginDetected_inspectsOnlyTypeViewAppeared() { + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + + for (int type = -100; type <= 100; type++) { + if (type == TYPE_VIEW_APPEARED) { + continue; + } + + mContentProtectionEventProcessor.processEvent(createEvent(type)); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + } + + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void processEvent_loginDetected() { + when(mMockEventBuffer.toArray()).thenReturn(new ContentCaptureEvent[0]); + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer).clear(); + verify(mMockEventBuffer).toArray(); + } + + @Test + public void processEvent_loginDetected_passwordFieldNotDetected() { + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void processEvent_loginDetected_suspiciousTextNotDetected() { + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void processEvent_loginDetected_withoutViewNode() { + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void processEvent_multipleLoginsDetected_belowFlushThreshold() { + when(mMockEventBuffer.toArray()).thenReturn(new ContentCaptureEvent[0]); + + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer).clear(); + verify(mMockEventBuffer).toArray(); + } + + @Test + public void processEvent_multipleLoginsDetected_aboveFlushThreshold() { + when(mMockEventBuffer.toArray()).thenReturn(new ContentCaptureEvent[0]); + + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + mContentProtectionEventProcessor.mLastFlushTime = Instant.now().minusSeconds(5); + + mContentProtectionEventProcessor.mPasswordFieldDetected = true; + mContentProtectionEventProcessor.mSuspiciousTextDetected = true; + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer, times(2)).clear(); + verify(mMockEventBuffer, times(2)).toArray(); + } + + @Test + public void isPasswordField_android() { + ContentCaptureEvent event = + createAndroidPasswordFieldEvent( + ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_PASSWORD); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isPasswordField_android_withoutClassName() { + ContentCaptureEvent event = + createAndroidPasswordFieldEvent( + /* className= */ null, InputType.TYPE_TEXT_VARIATION_PASSWORD); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isPasswordField_android_wrongClassName() { + ContentCaptureEvent event = + createAndroidPasswordFieldEvent( + "wrong.prefix" + ANDROID_CLASS_NAME, + InputType.TYPE_TEXT_VARIATION_PASSWORD); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isPasswordField_android_wrongInputType() { + ContentCaptureEvent event = + createAndroidPasswordFieldEvent( + ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_NORMAL); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isPasswordField_webView() { + when(mMockEventBuffer.toArray()).thenReturn(new ContentCaptureEvent[0]); + + ContentCaptureEvent event = + createWebViewPasswordFieldEvent( + /* className= */ null, /* eventText= */ null, PASSWORD_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer).clear(); + verify(mMockEventBuffer).toArray(); + } + + @Test + public void isPasswordField_webView_withClassName() { + ContentCaptureEvent event = + createWebViewPasswordFieldEvent( + /* className= */ "any.class.name", /* eventText= */ null, PASSWORD_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isPasswordField_webView_withSafeViewNodeText() { + ContentCaptureEvent event = + createWebViewPasswordFieldEvent( + /* className= */ null, /* eventText= */ null, SAFE_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isPasswordField_webView_withEventText() { + ContentCaptureEvent event = + createWebViewPasswordFieldEvent(/* className= */ null, PASSWORD_TEXT, SAFE_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isSuspiciousText_withSafeText() { + ContentCaptureEvent event = createSuspiciousTextEvent(SAFE_TEXT, SAFE_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isSuspiciousText_eventText_suspiciousText() { + ContentCaptureEvent event = createSuspiciousTextEvent(SUSPICIOUS_TEXT, SAFE_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isSuspiciousText_viewNodeText_suspiciousText() { + ContentCaptureEvent event = createSuspiciousTextEvent(SAFE_TEXT, SUSPICIOUS_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isSuspiciousText_eventText_passwordText() { + ContentCaptureEvent event = createSuspiciousTextEvent(PASSWORD_TEXT, SAFE_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + @Test + public void isSuspiciousText_viewNodeText_passwordText() { + // Specify the class to differ from {@link isPasswordField_webView} test in this version + ContentCaptureEvent event = + createProcessEvent( + "test.class.not.a.web.view", /* inputType= */ 0, SAFE_TEXT, PASSWORD_TEXT); + + mContentProtectionEventProcessor.processEvent(event); + + assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + } + + private static ContentCaptureEvent createEvent(int type) { + return new ContentCaptureEvent(/* sessionId= */ 123, type); + } + + private static ContentCaptureEvent createStoreEvent() { + return createEvent(TYPE_VIEW_TEXT_CHANGED); + } + + private static ContentCaptureEvent createProcessEvent() { + return createEvent(TYPE_VIEW_APPEARED); + } + + private ContentCaptureEvent createProcessEvent( + @Nullable String className, + int inputType, + @Nullable String eventText, + @Nullable String viewNodeText) { + View view = new View(mContext); + ViewStructureImpl viewStructure = new ViewStructureImpl(view); + if (className != null) { + viewStructure.setClassName(className); + } + if (viewNodeText != null) { + viewStructure.setText(viewNodeText); + } + viewStructure.setInputType(inputType); + + ContentCaptureEvent event = createProcessEvent(); + event.setViewNode(viewStructure.getNode()); + if (eventText != null) { + event.setText(eventText); + } + + return event; + } + + private ContentCaptureEvent createAndroidPasswordFieldEvent( + @Nullable String className, int inputType) { + return createProcessEvent( + className, inputType, /* eventText= */ null, /* viewNodeText= */ null); + } + + private ContentCaptureEvent createWebViewPasswordFieldEvent( + @Nullable String className, @Nullable String eventText, @Nullable String viewNodeText) { + return createProcessEvent(className, /* inputType= */ 0, eventText, viewNodeText); + } + + private ContentCaptureEvent createSuspiciousTextEvent( + @Nullable String eventText, @Nullable String viewNodeText) { + return createProcessEvent( + /* className= */ null, /* inputType= */ 0, eventText, viewNodeText); + } +} |