Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b7cfa386 authored by Nino Jagar's avatar Nino Jagar Committed by Android (Google) Code Review
Browse files

Merge "Use groups in content protection processor" into main

parents 4d31912a 3772ff11
Loading
Loading
Loading
Loading
+14 −5
Original line number Diff line number Diff line
@@ -317,16 +317,14 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
            }
        }

        // Should not be possible for mComponentName to be null here but check anyway
        if (mManager.mOptions.contentProtectionOptions.enableReceiver
                && mManager.getContentProtectionEventBuffer() != null
                && mComponentName != null) {
        if (isContentProtectionEnabled()) {
            mContentProtectionEventProcessor =
                    new ContentProtectionEventProcessor(
                            mManager.getContentProtectionEventBuffer(),
                            mHandler,
                            mSystemServerInterface,
                            mComponentName.getPackageName());
                            mComponentName.getPackageName(),
                            mManager.mOptions.contentProtectionOptions);
        } else {
            mContentProtectionEventProcessor = null;
        }
@@ -956,4 +954,15 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
    private boolean isContentCaptureReceiverEnabled() {
        return mManager.mOptions.enableReceiver;
    }

    @UiThread
    private boolean isContentProtectionEnabled() {
        // Should not be possible for mComponentName to be null here but check anyway
        // Should not be possible for groups to be empty if receiver is enabled but check anyway
        return mManager.mOptions.contentProtectionOptions.enableReceiver
                && mManager.getContentProtectionEventBuffer() != null
                && mComponentName != null
                && (!mManager.mOptions.contentProtectionOptions.requiredGroups.isEmpty()
                        || !mManager.mOptions.contentProtectionOptions.optionalGroups.isEmpty());
    }
}
+64 −90
Original line number Diff line number Diff line
@@ -19,9 +19,9 @@ package android.view.contentprotection;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.content.ContentCaptureOptions;
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;
@@ -33,10 +33,10 @@ 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.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

/**
 * Main entry point for processing {@link ContentCaptureEvent} for the content protection flow.
@@ -47,33 +47,13 @@ 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(
            Set.of(
                    ContentCaptureEvent.TYPE_VIEW_APPEARED,
                    ContentCaptureEvent.TYPE_VIEW_DISAPPEARED,
                                    ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED)));
                    ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED);

    private static final int RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS = 150;

@@ -85,11 +65,7 @@ public class ContentProtectionEventProcessor {

    @NonNull private final String mPackageName;

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public boolean mPasswordFieldDetected = false;

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public boolean mSuspiciousTextDetected = false;
    @NonNull private final ContentCaptureOptions.ContentProtectionOptions mOptions;

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    @Nullable
@@ -97,15 +73,32 @@ public class ContentProtectionEventProcessor {

    private int mResetLoginRemainingEventsToProcess;

    private boolean mAnyGroupFound = false;

    // Ordered by priority
    private final List<SearchGroup> mGroupsRequired;

    // Ordered by priority
    private final List<SearchGroup> mGroupsOptional;

    // Ordered by priority
    private final List<SearchGroup> mGroupsAll;

    public ContentProtectionEventProcessor(
            @NonNull RingBuffer<ContentCaptureEvent> eventBuffer,
            @NonNull Handler handler,
            @NonNull IContentCaptureManager contentCaptureManager,
            @NonNull String packageName) {
            @NonNull String packageName,
            @NonNull ContentCaptureOptions.ContentProtectionOptions options) {
        mEventBuffer = eventBuffer;
        mHandler = handler;
        mContentCaptureManager = contentCaptureManager;
        mPackageName = packageName;
        mOptions = options;
        mGroupsRequired = options.requiredGroups.stream().map(SearchGroup::new).toList();
        mGroupsOptional = options.optionalGroups.stream().map(SearchGroup::new).toList();
        mGroupsAll =
                Stream.of(mGroupsRequired, mGroupsOptional).flatMap(Collection::stream).toList();
    }

    /** Main entry point for {@link ContentCaptureEvent} processing. */
@@ -130,9 +123,31 @@ public class ContentProtectionEventProcessor {

    @UiThread
    private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) {
        mPasswordFieldDetected |= isPasswordField(event);
        mSuspiciousTextDetected |= isSuspiciousText(event);
        if (mPasswordFieldDetected && mSuspiciousTextDetected) {
        ViewNode viewNode = event.getViewNode();
        String eventText = ContentProtectionUtils.getEventTextLower(event);
        String viewNodeText = ContentProtectionUtils.getViewNodeTextLower(viewNode);
        String hintText = ContentProtectionUtils.getHintTextLower(viewNode);

        mGroupsAll.stream()
                .filter(group -> !group.mFound)
                .filter(
                        group ->
                                group.matches(eventText)
                                        || group.matches(viewNodeText)
                                        || group.matches(hintText))
                .findFirst()
                .ifPresent(
                        group -> {
                            group.mFound = true;
                            mAnyGroupFound = true;
                        });

        boolean loginDetected =
                mGroupsRequired.stream().allMatch(group -> group.mFound)
                        && mGroupsOptional.stream().filter(group -> group.mFound).count()
                                >= mOptions.optionalGroupsThreshold;

        if (loginDetected) {
            loginDetected();
        } else {
            maybeResetLoginFlags();
@@ -150,14 +165,13 @@ public class ContentProtectionEventProcessor {

    @UiThread
    private void resetLoginFlags() {
        mPasswordFieldDetected = false;
        mSuspiciousTextDetected = false;
        mResetLoginRemainingEventsToProcess = 0;
        mGroupsAll.forEach(group -> group.mFound = false);
        mAnyGroupFound = false;
    }

    @UiThread
    private void maybeResetLoginFlags() {
        if (mPasswordFieldDetected || mSuspiciousTextDetected) {
        if (mAnyGroupFound) {
            if (mResetLoginRemainingEventsToProcess <= 0) {
                mResetLoginRemainingEventsToProcess = RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS;
            } else {
@@ -194,61 +208,21 @@ public class ContentProtectionEventProcessor {
        }
    }

    private boolean isPasswordField(@NonNull ContentCaptureEvent event) {
        return isPasswordField(event.getViewNode());
    }
    private static final class SearchGroup {

    private boolean isPasswordField(@Nullable ViewNode viewNode) {
        if (viewNode == null) {
            return false;
        }
        return isAndroidPasswordField(viewNode) || isWebViewPasswordField(viewNode);
    }
        @NonNull private final List<String> mSearchStrings;

    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));
    }
        public boolean mFound = false;

    private boolean isAndroidViewNode(@NonNull ViewNode viewNode) {
        String className = viewNode.getClassName();
        return className != null && className.startsWith(ANDROID_CLASS_NAME_PREFIX);
        SearchGroup(@NonNull List<String> searchStrings) {
            mSearchStrings = searchStrings;
        }

    private boolean isSuspiciousText(@NonNull ContentCaptureEvent event) {
        return isSuspiciousText(ContentProtectionUtils.getEventText(event))
                || isSuspiciousText(ContentProtectionUtils.getViewNodeText(event));
    }

    private boolean isSuspiciousText(@Nullable String text) {
        public boolean matches(@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;
            return mSearchStrings.stream().anyMatch(text::contains);
        }
        String lowerCaseText = text.toLowerCase();
        return PASSWORD_TEXTS.stream()
                .anyMatch(passwordText -> lowerCaseText.contains(passwordText));
    }
}
+17 −11
Original line number Diff line number Diff line
@@ -28,33 +28,39 @@ import android.view.contentcapture.ViewNode;
 */
public final class ContentProtectionUtils {

    /** Returns the text extracted directly from the {@link ContentCaptureEvent}, if set. */
    /** Returns the lowercase text extracted from the {@link ContentCaptureEvent}, if set. */
    @Nullable
    public static String getEventText(@NonNull ContentCaptureEvent event) {
    public static String getEventTextLower(@NonNull ContentCaptureEvent event) {
        CharSequence text = event.getText();
        if (text == null) {
            return null;
        }
        return text.toString();
        return text.toString().toLowerCase();
    }

    /** Returns the text extracted from the event's {@link ViewNode}, if set. */
    /** Returns the lowercase text extracted from the {@link ViewNode}, if set. */
    @Nullable
    public static String getViewNodeText(@NonNull ContentCaptureEvent event) {
        ViewNode viewNode = event.getViewNode();
    public static String getViewNodeTextLower(@Nullable ViewNode viewNode) {
        if (viewNode == null) {
            return null;
        }
        return getViewNodeText(viewNode);
        CharSequence text = viewNode.getText();
        if (text == null) {
            return null;
        }
        return text.toString().toLowerCase();
    }

    /** Returns the text extracted directly from the {@link ViewNode}, if set. */
    /** Returns the lowercase hint text extracted from the {@link ViewNode}, if set. */
    @Nullable
    public static String getViewNodeText(@NonNull ViewNode viewNode) {
        CharSequence text = viewNode.getText();
    public static String getHintTextLower(@Nullable ViewNode viewNode) {
        if (viewNode == null) {
            return null;
        }
        String text = viewNode.getHint();
        if (text == null) {
            return null;
        }
        return text.toString();
        return text.toLowerCase();
    }
}
+21 −1
Original line number Diff line number Diff line
@@ -115,6 +115,26 @@ public class MainContentCaptureSessionTest {
                        new ContentCaptureOptions.ContentProtectionOptions(
                                /* enableReceiver= */ true,
                                -BUFFER_SIZE,
                                /* requiredGroups= */ List.of(List.of("a")),
                                /* optionalGroups= */ Collections.emptyList(),
                                /* optionalGroupsThreshold= */ 0));
        MainContentCaptureSession session = createSession(options);
        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;

        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);

        assertThat(session.mContentProtectionEventProcessor).isNull();
        verifyZeroInteractions(mMockContentProtectionEventProcessor);
    }

    @Test
    public void onSessionStarted_contentProtectionNoGroups_processorNotCreated() {
        ContentCaptureOptions options =
                createOptions(
                        /* enableContentCaptureReceiver= */ true,
                        new ContentCaptureOptions.ContentProtectionOptions(
                                /* enableReceiver= */ true,
                                BUFFER_SIZE,
                                /* requiredGroups= */ Collections.emptyList(),
                                /* optionalGroups= */ Collections.emptyList(),
                                /* optionalGroupsThreshold= */ 0));
@@ -320,7 +340,7 @@ public class MainContentCaptureSessionTest {
                new ContentCaptureOptions.ContentProtectionOptions(
                        enableContentProtectionReceiver,
                        BUFFER_SIZE,
                        /* requiredGroups= */ Collections.emptyList(),
                        /* requiredGroups= */ List.of(List.of("a")),
                        /* optionalGroups= */ Collections.emptyList(),
                        /* optionalGroupsThreshold= */ 0));
    }
+197 −299

File changed.

Preview size limit exceeded, changes collapsed.

Loading