diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index efd50e7d2343f..2241fd5dc37ab 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -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 mEvents; + public ArrayList 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 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; + } } diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java new file mode 100644 index 0000000000000..3373b8b132736 --- /dev/null +++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java @@ -0,0 +1,360 @@ +/* + * 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}. + * + *

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 captor = ArgumentCaptor.forClass(ParceledListSlice.class); + verify(mMockContentCaptureDirectManager) + .sendEvents(captor.capture(), eq(REASON), eq(options)); + + assertThat(captor.getValue()).isNotNull(); + List actual = captor.getValue().getList(); + assertThat(actual).isNotNull(); + assertThat(actual).containsExactly(EVENT); + } +}