summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Nino Jagar <njagar@google.com> 2023-05-29 00:12:19 +0000
committer Nino Jagar <njagar@google.com> 2023-05-31 21:55:26 +0000
commit93b34c38962b2f7cf47f571a8883d3777c996d80 (patch)
treee55d2fa8a34b5b651ddb11704d49906142c1ee15
parent90ca501cdfb2563d80d404fdf092516d7605e1bf (diff)
Implement content protection processor
Bug: 275732576 Test: Added new tests Change-Id: Ic00758e7f538de7b0ed62d96f5cb7156bff786ab
-rw-r--r--core/java/android/view/contentprotection/ContentProtectionEventProcessor.java228
-rw-r--r--core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java477
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);
+ }
+}