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

Commit 699deb95 authored by Nino Jagar's avatar Nino Jagar
Browse files

Connect main CC session with the protection flow

BYPASS_INCLUSIVE_LANGUAGE_REASON=Existing code in production

Bug: 275732576
Test: Added tests

Change-Id: I5972b876c0c4451ac0ae05efae4fa8ea740038f7
parent a86313f3
Loading
Loading
Loading
Loading
+92 −14
Original line number Diff line number Diff line
@@ -52,8 +52,10 @@ import android.util.Log;
import android.util.TimeUtils;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ViewNode.ViewStructureImpl;
import android.view.contentprotection.ContentProtectionEventProcessor;
import android.view.inputmethod.BaseInputConnection;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;

import java.io.PrintWriter;
@@ -118,9 +120,13 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
    /**
     * Direct interface to the service binder object - it's used to send the events, including the
     * last ones (when the session is finished)
     *
     * @hide
     */
    @NonNull
    private IContentCaptureDirectManager mDirectServiceInterface;
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    @Nullable
    public IContentCaptureDirectManager mDirectServiceInterface;

    @Nullable
    private DeathRecipient mDirectServiceVulture;

@@ -131,14 +137,19 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
    @Nullable
    private IBinder mShareableActivityToken;

    /** @hide */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    @Nullable
    private ComponentName mComponentName;
    public ComponentName mComponentName;

    /**
     * List of events held to be sent as a batch.
     *
     * @hide
     */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    @Nullable
    private ArrayList<ContentCaptureEvent> mEvents;
    public ArrayList<ContentCaptureEvent> mEvents;

    // Used just for debugging purposes (on dump)
    private long mNextFlush;
@@ -157,6 +168,11 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
    @NonNull
    private final SessionStateReceiver mSessionStateReceiver;

    /** @hide */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    @Nullable
    public ContentProtectionEventProcessor mContentProtectionEventProcessor;

    private static class SessionStateReceiver extends IResultReceiver.Stub {
        private final WeakReference<MainContentCaptureSession> mMainSession;

@@ -194,8 +210,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
        }
    }

    protected MainContentCaptureSession(@NonNull ContentCaptureManager.StrippedContext context,
            @NonNull ContentCaptureManager manager, @NonNull Handler handler,
    /** @hide */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
    public MainContentCaptureSession(
            @NonNull ContentCaptureManager.StrippedContext context,
            @NonNull ContentCaptureManager manager,
            @NonNull Handler handler,
            @NonNull IContentCaptureManager systemServerInterface) {
        mContext = context;
        mManager = manager;
@@ -273,15 +293,16 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
    }

    /**
     * Callback from {@code system_server} after call to
     * {@link IContentCaptureManager#startSession(IBinder, ComponentName, String, int,
     * IResultReceiver)}.
     * Callback from {@code system_server} after call to {@link
     * IContentCaptureManager#startSession(IBinder, ComponentName, String, int, IResultReceiver)}.
     *
     * @param resultCode session state
     * @param binder handle to {@code IContentCaptureDirectManager}
     * @hide
     */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    @UiThread
    private void onSessionStarted(int resultCode, @Nullable IBinder binder) {
    public void onSessionStarted(int resultCode, @Nullable IBinder binder) {
        if (binder != null) {
            mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
            mDirectServiceVulture = () -> {
@@ -296,6 +317,20 @@ 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) {
            mContentProtectionEventProcessor =
                    new ContentProtectionEventProcessor(
                            mManager.getContentProtectionEventBuffer(),
                            mHandler,
                            mSystemServerInterface,
                            mComponentName.getPackageName());
        } else {
            mContentProtectionEventProcessor = null;
        }

        if ((resultCode & STATE_DISABLED) != 0) {
            resetSession(resultCode);
        } else {
@@ -311,8 +346,10 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
        }
    }

    /** @hide */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    @UiThread
    private void sendEvent(@NonNull ContentCaptureEvent event) {
    public void sendEvent(@NonNull ContentCaptureEvent event) {
        sendEvent(event, /* forceFlush= */ false);
    }

@@ -337,6 +374,25 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
            if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
            return;
        }

        if (isContentProtectionReceiverEnabled()) {
            sendContentProtectionEvent(event);
        }
        if (isContentCaptureReceiverEnabled()) {
            sendContentCaptureEvent(event, forceFlush);
        }
    }

    @UiThread
    private void sendContentProtectionEvent(@NonNull ContentCaptureEvent event) {
        if (mContentProtectionEventProcessor != null) {
            mContentProtectionEventProcessor.processEvent(event);
        }
    }

    @UiThread
    private void sendContentCaptureEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
        final int eventType = event.getType();
        final int maxBufferSize = mManager.mOptions.maxBufferSize;
        if (mEvents == null) {
            if (sVerbose) {
@@ -528,9 +584,11 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
        flush(reason);
    }

    /** @hide */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    @Override
    @UiThread
    void flush(@FlushReason int reason) {
    public void flush(@FlushReason int reason) {
        if (mEvents == null || mEvents.size() == 0) {
            if (sVerbose) {
                Log.v(TAG, "Don't flush for empty event buffer.");
@@ -544,6 +602,10 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
            return;
        }

        if (!isContentCaptureReceiverEnabled()) {
            return;
        }

        if (mDirectServiceInterface == null) {
            if (sVerbose) {
                Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
@@ -607,8 +669,10 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
        return new ParceledListSlice<>(events);
    }

    /** hide */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    @UiThread
    private void destroySession() {
    public void destroySession() {
        if (sDebug) {
            Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
                    + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
@@ -626,12 +690,15 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
            mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
        }
        mDirectServiceInterface = null;
        mContentProtectionEventProcessor = null;
    }

    // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
    // clearings out.
    /** @hide */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    @UiThread
    private void resetSession(int newState) {
    public void resetSession(int newState) {
        if (sVerbose) {
            Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
                    + getStateAsString(mState) + " to " + getStateAsString(newState));
@@ -651,6 +718,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
            }
        }
        mDirectServiceInterface = null;
        mContentProtectionEventProcessor = null;
        mHandler.removeMessages(MSG_FLUSH);
    }

@@ -878,4 +946,14 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
    private String getDebugState(@FlushReason int reason) {
        return getDebugState() + ", reason=" + getFlushReasonAsString(reason);
    }

    @UiThread
    private boolean isContentProtectionReceiverEnabled() {
        return mManager.mOptions.contentProtectionOptions.enableReceiver;
    }

    @UiThread
    private boolean isContentCaptureReceiverEnabled() {
        return mManager.mOptions.enableReceiver;
    }
}
+360 −0
Original line number Diff line number Diff line
/*
 * 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.contentcapture;

import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;

import android.content.ComponentName;
import android.content.ContentCaptureOptions;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.os.Handler;
import android.os.Looper;
import android.view.contentprotection.ContentProtectionEventProcessor;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Test for {@link MainContentCaptureSession}.
 *
 * <p>Run with: {@code atest
 * FrameworksCoreTests:android.view.contentcapture.MainContentCaptureSessionTest}
 */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MainContentCaptureSessionTest {

    private static final int BUFFER_SIZE = 100;

    private static final int REASON = 123;

    private static final ContentCaptureEvent EVENT =
            new ContentCaptureEvent(/* sessionId= */ 0, TYPE_SESSION_STARTED);

    private static final ComponentName COMPONENT_NAME =
            new ComponentName("com.test.package", "TestClass");

    private static final Context sContext = ApplicationProvider.getApplicationContext();

    private static final ContentCaptureManager.StrippedContext sStrippedContext =
            new ContentCaptureManager.StrippedContext(sContext);

    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();

    @Mock private IContentCaptureManager mMockSystemServerInterface;

    @Mock private ContentProtectionEventProcessor mMockContentProtectionEventProcessor;

    @Mock private IContentCaptureDirectManager mMockContentCaptureDirectManager;

    @Test
    public void onSessionStarted_contentProtectionEnabled_processorCreated() {
        MainContentCaptureSession session = createSession();
        assertThat(session.mContentProtectionEventProcessor).isNull();

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

        assertThat(session.mContentProtectionEventProcessor).isNotNull();
    }

    @Test
    public void onSessionStarted_contentProtectionDisabled_processorNotCreated() {
        MainContentCaptureSession session =
                createSession(
                        /* enableContentCaptureReceiver= */ true,
                        /* enableContentProtectionReceiver= */ false);
        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;

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

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

    @Test
    public void onSessionStarted_contentProtectionNoBuffer_processorNotCreated() {
        ContentCaptureOptions options =
                createOptions(
                        /* enableContentCaptureReceiver= */ true,
                        new ContentCaptureOptions.ContentProtectionOptions(
                                /* enableReceiver= */ true, -BUFFER_SIZE));
        MainContentCaptureSession session = createSession(options);
        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;

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

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

    @Test
    public void onSessionStarted_noComponentName_processorNotCreated() {
        MainContentCaptureSession session = createSession();
        session.mComponentName = null;

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

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

    @Test
    public void sendEvent_contentCaptureDisabled_contentProtectionDisabled() {
        MainContentCaptureSession session =
                createSession(
                        /* enableContentCaptureReceiver= */ false,
                        /* enableContentProtectionReceiver= */ false);
        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;

        session.sendEvent(EVENT);

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

    @Test
    public void sendEvent_contentCaptureDisabled_contentProtectionEnabled() {
        MainContentCaptureSession session =
                createSession(
                        /* enableContentCaptureReceiver= */ false,
                        /* enableContentProtectionReceiver= */ true);
        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;

        session.sendEvent(EVENT);

        verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
        assertThat(session.mEvents).isNull();
    }

    @Test
    public void sendEvent_contentCaptureEnabled_contentProtectionDisabled() {
        MainContentCaptureSession session =
                createSession(
                        /* enableContentCaptureReceiver= */ true,
                        /* enableContentProtectionReceiver= */ false);
        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;

        session.sendEvent(EVENT);

        verifyZeroInteractions(mMockContentProtectionEventProcessor);
        assertThat(session.mEvents).isNotNull();
        assertThat(session.mEvents).containsExactly(EVENT);
    }

    @Test
    public void sendEvent_contentCaptureEnabled_contentProtectionEnabled() {
        MainContentCaptureSession session = createSession();
        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;

        session.sendEvent(EVENT);

        verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
        assertThat(session.mEvents).isNotNull();
        assertThat(session.mEvents).containsExactly(EVENT);
    }

    @Test
    public void sendEvent_contentProtectionEnabled_processorNotCreated() {
        MainContentCaptureSession session =
                createSession(
                        /* enableContentCaptureReceiver= */ false,
                        /* enableContentProtectionReceiver= */ true);

        session.sendEvent(EVENT);

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

    @Test
    public void flush_contentCaptureDisabled_contentProtectionDisabled() throws Exception {
        ContentCaptureOptions options =
                createOptions(
                        /* enableContentCaptureReceiver= */ false,
                        /* enableContentProtectionReceiver= */ false);
        MainContentCaptureSession session = createSession(options);
        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
        session.mDirectServiceInterface = mMockContentCaptureDirectManager;

        session.flush(REASON);

        verifyZeroInteractions(mMockContentProtectionEventProcessor);
        verifyZeroInteractions(mMockContentCaptureDirectManager);
        assertThat(session.mEvents).containsExactly(EVENT);
    }

    @Test
    public void flush_contentCaptureDisabled_contentProtectionEnabled() {
        MainContentCaptureSession session =
                createSession(
                        /* enableContentCaptureReceiver= */ false,
                        /* enableContentProtectionReceiver= */ true);
        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
        session.mDirectServiceInterface = mMockContentCaptureDirectManager;

        session.flush(REASON);

        verifyZeroInteractions(mMockContentProtectionEventProcessor);
        verifyZeroInteractions(mMockContentCaptureDirectManager);
        assertThat(session.mEvents).containsExactly(EVENT);
    }

    @Test
    public void flush_contentCaptureEnabled_contentProtectionDisabled() throws Exception {
        ContentCaptureOptions options =
                createOptions(
                        /* enableContentCaptureReceiver= */ true,
                        /* enableContentProtectionReceiver= */ false);
        MainContentCaptureSession session = createSession(options);
        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
        session.mDirectServiceInterface = mMockContentCaptureDirectManager;

        session.flush(REASON);

        verifyZeroInteractions(mMockContentProtectionEventProcessor);
        assertThat(session.mEvents).isEmpty();
        assertEventFlushedContentCapture(options);
    }

    @Test
    public void flush_contentCaptureEnabled_contentProtectionEnabled() throws Exception {
        ContentCaptureOptions options =
                createOptions(
                        /* enableContentCaptureReceiver= */ true,
                        /* enableContentProtectionReceiver= */ true);
        MainContentCaptureSession session = createSession(options);
        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
        session.mDirectServiceInterface = mMockContentCaptureDirectManager;

        session.flush(REASON);

        verifyZeroInteractions(mMockContentProtectionEventProcessor);
        assertThat(session.mEvents).isEmpty();
        assertEventFlushedContentCapture(options);
    }

    @Test
    public void destroySession() throws Exception {
        MainContentCaptureSession session = createSession();
        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;

        session.destroySession();

        verify(mMockSystemServerInterface).finishSession(anyInt());
        verifyZeroInteractions(mMockContentProtectionEventProcessor);
        assertThat(session.mDirectServiceInterface).isNull();
        assertThat(session.mContentProtectionEventProcessor).isNull();
    }

    @Test
    public void resetSession() {
        MainContentCaptureSession session = createSession();
        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;

        session.resetSession(/* newState= */ 0);

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

    private static ContentCaptureOptions createOptions(
            boolean enableContentCaptureReceiver,
            ContentCaptureOptions.ContentProtectionOptions contentProtectionOptions) {
        return new ContentCaptureOptions(
                /* loggingLevel= */ 0,
                BUFFER_SIZE,
                /* idleFlushingFrequencyMs= */ 0,
                /* textChangeFlushingFrequencyMs= */ 0,
                /* logHistorySize= */ 0,
                /* disableFlushForViewTreeAppearing= */ false,
                enableContentCaptureReceiver,
                contentProtectionOptions,
                /* whitelistedComponents= */ null);
    }

    private static ContentCaptureOptions createOptions(
            boolean enableContentCaptureReceiver, boolean enableContentProtectionReceiver) {
        return createOptions(
                enableContentCaptureReceiver,
                new ContentCaptureOptions.ContentProtectionOptions(
                        enableContentProtectionReceiver, BUFFER_SIZE));
    }

    private ContentCaptureManager createManager(ContentCaptureOptions options) {
        return new ContentCaptureManager(sContext, mMockSystemServerInterface, options);
    }

    private MainContentCaptureSession createSession(ContentCaptureManager manager) {
        MainContentCaptureSession session =
                new MainContentCaptureSession(
                        sStrippedContext,
                        manager,
                        new Handler(Looper.getMainLooper()),
                        mMockSystemServerInterface);
        session.mComponentName = COMPONENT_NAME;
        return session;
    }

    private MainContentCaptureSession createSession(ContentCaptureOptions options) {
        return createSession(createManager(options));
    }

    private MainContentCaptureSession createSession(
            boolean enableContentCaptureReceiver, boolean enableContentProtectionReceiver) {
        return createSession(
                createOptions(enableContentCaptureReceiver, enableContentProtectionReceiver));
    }

    private MainContentCaptureSession createSession() {
        return createSession(
                /* enableContentCaptureReceiver= */ true,
                /* enableContentProtectionReceiver= */ true);
    }

    private void assertEventFlushedContentCapture(ContentCaptureOptions options) throws Exception {
        ArgumentCaptor<ParceledListSlice> captor = ArgumentCaptor.forClass(ParceledListSlice.class);
        verify(mMockContentCaptureDirectManager)
                .sendEvents(captor.capture(), eq(REASON), eq(options));

        assertThat(captor.getValue()).isNotNull();
        List<ContentCaptureEvent> actual = captor.getValue().getList();
        assertThat(actual).isNotNull();
        assertThat(actual).containsExactly(EVENT);
    }
}