Merge "Adding feature flag to enable new external proximity sensor API" into main
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 7b45600..7a6c292 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -130,7 +130,6 @@
import android.graphics.HardwareRenderer;
import android.graphics.HardwareRenderer.FrameDrawingCallback;
import android.graphics.HardwareRendererObserver;
-import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
@@ -206,7 +205,6 @@
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
-import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.ContentCaptureSession;
@@ -4029,56 +4027,20 @@
}
private void notifyContentCaptureEvents() {
- try {
- if (!isContentCaptureEnabled()) {
- if (DEBUG_CONTENT_CAPTURE) {
- Log.d(mTag, "notifyContentCaptureEvents while disabled");
- }
- mAttachInfo.mContentCaptureEvents = null;
- return;
- }
- if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
- Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
- }
- MainContentCaptureSession mainSession = mAttachInfo.mContentCaptureManager
- .getMainContentCaptureSession();
- for (int i = 0; i < mAttachInfo.mContentCaptureEvents.size(); i++) {
- int sessionId = mAttachInfo.mContentCaptureEvents.keyAt(i);
- mainSession.notifyViewTreeEvent(sessionId, /* started= */ true);
- ArrayList<Object> events = mAttachInfo.mContentCaptureEvents
- .valueAt(i);
- for_each_event: for (int j = 0; j < events.size(); j++) {
- Object event = events.get(j);
- if (event instanceof AutofillId) {
- mainSession.notifyViewDisappeared(sessionId, (AutofillId) event);
- } else if (event instanceof View) {
- View view = (View) event;
- ContentCaptureSession session = view.getContentCaptureSession();
- if (session == null) {
- Log.w(mTag, "no content capture session on view: " + view);
- continue for_each_event;
- }
- int actualId = session.getId();
- if (actualId != sessionId) {
- Log.w(mTag, "content capture session mismatch for view (" + view
- + "): was " + sessionId + " before, it's " + actualId + " now");
- continue for_each_event;
- }
- ViewStructure structure = session.newViewStructure(view);
- view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
- session.notifyViewAppeared(structure);
- } else if (event instanceof Insets) {
- mainSession.notifyViewInsetsChanged(sessionId, (Insets) event);
- } else {
- Log.w(mTag, "invalid content capture event: " + event);
- }
- }
- mainSession.notifyViewTreeEvent(sessionId, /* started= */ false);
+ if (!isContentCaptureEnabled()) {
+ if (DEBUG_CONTENT_CAPTURE) {
+ Log.d(mTag, "notifyContentCaptureEvents while disabled");
}
mAttachInfo.mContentCaptureEvents = null;
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ return;
}
+
+ final ContentCaptureManager manager = mAttachInfo.mContentCaptureManager;
+ if (manager != null && mAttachInfo.mContentCaptureEvents != null) {
+ final MainContentCaptureSession session = manager.getMainContentCaptureSession();
+ session.notifyContentCaptureEvents(mAttachInfo.mContentCaptureEvents);
+ }
+ mAttachInfo.mContentCaptureEvents = null;
}
private void notifyHolderSurfaceDestroyed() {
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 5a058ff..a829747 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -18,6 +18,7 @@
import static android.view.contentcapture.ContentCaptureHelper.sDebug;
import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
import static android.view.contentcapture.ContentCaptureHelper.toSet;
+import static android.view.contentcapture.flags.Flags.runOnBackgroundThreadEnabled;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
@@ -52,6 +53,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
import com.android.internal.util.RingBuffer;
import com.android.internal.util.SyncResultReceiver;
@@ -495,10 +497,9 @@
@GuardedBy("mLock")
private int mFlags;
- // TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler
- // held at the Application level
- @NonNull
- private final Handler mHandler;
+ @Nullable
+ @GuardedBy("mLock")
+ private Handler mHandler;
@GuardedBy("mLock")
private MainContentCaptureSession mMainSession;
@@ -562,11 +563,6 @@
if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName());
- // TODO(b/119220549): we might not even need a handler, as the IPCs are oneway. But if we
- // do, then we should optimize it to run the tests after the Choreographer finishes the most
- // important steps of the frame.
- mHandler = Handler.createAsync(Looper.getMainLooper());
-
mDataShareAdapterResourceManager = new LocalDataShareAdapterResourceManager();
if (mOptions.contentProtectionOptions.enableReceiver
@@ -594,13 +590,27 @@
public MainContentCaptureSession getMainContentCaptureSession() {
synchronized (mLock) {
if (mMainSession == null) {
- mMainSession = new MainContentCaptureSession(mContext, this, mHandler, mService);
+ mMainSession = new MainContentCaptureSession(
+ mContext, this, prepareContentCaptureHandler(), mService);
if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession);
}
return mMainSession;
}
}
+ @NonNull
+ @GuardedBy("mLock")
+ private Handler prepareContentCaptureHandler() {
+ if (mHandler == null) {
+ if (runOnBackgroundThreadEnabled()) {
+ mHandler = BackgroundThread.getHandler();
+ } else {
+ mHandler = Handler.createAsync(Looper.getMainLooper());
+ }
+ }
+ return mHandler;
+ }
+
/** @hide */
@UiThread
public void onActivityCreated(@NonNull IBinder applicationToken,
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index d9b0f80..542c783c 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -34,7 +34,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.UiThread;
import android.content.ComponentName;
import android.content.pm.ParceledListSlice;
import android.graphics.Insets;
@@ -50,7 +49,10 @@
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Log;
+import android.util.SparseArray;
import android.util.TimeUtils;
+import android.view.View;
+import android.view.ViewStructure;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ViewNode.ViewStructureImpl;
import android.view.contentprotection.ContentProtectionEventProcessor;
@@ -207,7 +209,8 @@
} else {
binder = null;
}
- mainSession.mHandler.post(() -> mainSession.onSessionStarted(resultCode, binder));
+ mainSession.mHandler.post(() ->
+ mainSession.onSessionStarted(resultCode, binder));
}
}
@@ -244,9 +247,14 @@
/**
* Starts this session.
*/
- @UiThread
void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
@NonNull ComponentName component, int flags) {
+ runOnContentCaptureThread(() -> startImpl(token, shareableActivityToken, component, flags));
+ }
+
+ private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
+ @NonNull ComponentName component, int flags) {
+ checkOnContentCaptureThread();
if (!isContentCaptureEnabled()) return;
if (sVerbose) {
@@ -280,17 +288,15 @@
Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
}
}
-
@Override
void onDestroy() {
- mHandler.removeMessages(MSG_FLUSH);
- mHandler.post(() -> {
+ clearAndRunOnContentCaptureThread(() -> {
try {
flush(FLUSH_REASON_SESSION_FINISHED);
} finally {
destroySession();
}
- });
+ }, MSG_FLUSH);
}
/**
@@ -302,8 +308,8 @@
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
- @UiThread
public void onSessionStarted(int resultCode, @Nullable IBinder binder) {
+ checkOnContentCaptureThread();
if (binder != null) {
mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
mDirectServiceVulture = () -> {
@@ -347,13 +353,12 @@
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
- @UiThread
public void sendEvent(@NonNull ContentCaptureEvent event) {
sendEvent(event, /* forceFlush= */ false);
}
- @UiThread
private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+ checkOnContentCaptureThread();
final int eventType = event.getType();
if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
@@ -396,15 +401,15 @@
}
}
- @UiThread
private void sendContentProtectionEvent(@NonNull ContentCaptureEvent event) {
+ checkOnContentCaptureThread();
if (mContentProtectionEventProcessor != null) {
mContentProtectionEventProcessor.processEvent(event);
}
}
- @UiThread
private void sendContentCaptureEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+ checkOnContentCaptureThread();
final int eventType = event.getType();
final int maxBufferSize = mManager.mOptions.maxBufferSize;
if (mEvents == null) {
@@ -538,13 +543,13 @@
flush(flushReason);
}
- @UiThread
private boolean hasStarted() {
+ checkOnContentCaptureThread();
return mState != UNKNOWN_STATE;
}
- @UiThread
private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
+ checkOnContentCaptureThread();
if (sVerbose) {
Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
+ ", checkExisting=" + checkExisting);
@@ -588,8 +593,8 @@
mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs);
}
- @UiThread
private void flushIfNeeded(@FlushReason int reason) {
+ checkOnContentCaptureThread();
if (mEvents == null || mEvents.isEmpty()) {
if (sVerbose) Log.v(TAG, "Nothing to flush");
return;
@@ -600,8 +605,12 @@
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@Override
- @UiThread
public void flush(@FlushReason int reason) {
+ runOnContentCaptureThread(() -> flushImpl(reason));
+ }
+
+ private void flushImpl(@FlushReason int reason) {
+ checkOnContentCaptureThread();
if (mEvents == null || mEvents.size() == 0) {
if (sVerbose) {
Log.v(TAG, "Don't flush for empty event buffer.");
@@ -669,8 +678,8 @@
* Resets the buffer and return a {@link ParceledListSlice} with the previous events.
*/
@NonNull
- @UiThread
private ParceledListSlice<ContentCaptureEvent> clearEvents() {
+ checkOnContentCaptureThread();
// NOTE: we must save a reference to the current mEvents and then set it to to null,
// otherwise clearing it would clear it in the receiving side if the service is also local.
if (mEvents == null) {
@@ -684,8 +693,8 @@
/** hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
- @UiThread
public void destroySession() {
+ checkOnContentCaptureThread();
if (sDebug) {
Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
+ (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
@@ -710,8 +719,8 @@
// clearings out.
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
- @UiThread
public void resetSession(int newState) {
+ checkOnContentCaptureThread();
if (sVerbose) {
Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
+ getStateAsString(mState) + " to " + getStateAsString(newState));
@@ -794,24 +803,26 @@
// change should also get get rid of the "internalNotifyXXXX" methods above
void notifyChildSessionStarted(int parentSessionId, int childSessionId,
@NonNull ContentCaptureContext clientContext) {
- mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
+ runOnContentCaptureThread(
+ () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
.setParentSessionId(parentSessionId).setClientContext(clientContext),
FORCE_FLUSH));
}
void notifyChildSessionFinished(int parentSessionId, int childSessionId) {
- mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
+ runOnContentCaptureThread(
+ () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
.setParentSessionId(parentSessionId), FORCE_FLUSH));
}
void notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) {
- mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
+ runOnContentCaptureThread(() ->
+ sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
.setViewNode(node.mNode)));
}
- /** Public because is also used by ViewRootImpl */
- public void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
- mHandler.post(() -> sendEvent(
+ void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id)));
}
@@ -836,52 +847,102 @@
final int startIndex = Selection.getSelectionStart(text);
final int endIndex = Selection.getSelectionEnd(text);
- mHandler.post(() -> sendEvent(
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED)
.setAutofillId(id).setText(eventText)
.setComposingIndex(composingStart, composingEnd)
.setSelectionIndex(startIndex, endIndex)));
}
- /** Public because is also used by ViewRootImpl */
- public void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) {
- mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
+ void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) {
+ runOnContentCaptureThread(() ->
+ sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
.setInsets(viewInsets)));
}
- /** Public because is also used by ViewRootImpl */
- public void notifyViewTreeEvent(int sessionId, boolean started) {
+ void notifyViewTreeEvent(int sessionId, boolean started) {
final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED;
final boolean disableFlush = mManager.getFlushViewTreeAppearingEventDisabled();
- mHandler.post(() -> sendEvent(
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, type),
disableFlush ? !started : FORCE_FLUSH));
}
void notifySessionResumed(int sessionId) {
- mHandler.post(() -> sendEvent(
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_SESSION_RESUMED), FORCE_FLUSH));
}
void notifySessionPaused(int sessionId) {
- mHandler.post(() -> sendEvent(
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_SESSION_PAUSED), FORCE_FLUSH));
}
void notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) {
- mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
+ runOnContentCaptureThread(() ->
+ sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
.setClientContext(context), FORCE_FLUSH));
}
/** public because is also used by ViewRootImpl */
public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) {
- mHandler.post(() -> sendEvent(
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED)
.setBounds(bounds)
));
}
+ /** public because is also used by ViewRootImpl */
+ public void notifyContentCaptureEvents(
+ @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
+ runOnContentCaptureThread(() -> notifyContentCaptureEventsImpl(contentCaptureEvents));
+ }
+
+ private void notifyContentCaptureEventsImpl(
+ @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
+ checkOnContentCaptureThread();
+ try {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
+ }
+ for (int i = 0; i < contentCaptureEvents.size(); i++) {
+ int sessionId = contentCaptureEvents.keyAt(i);
+ notifyViewTreeEvent(sessionId, /* started= */ true);
+ ArrayList<Object> events = contentCaptureEvents.valueAt(i);
+ for_each_event: for (int j = 0; j < events.size(); j++) {
+ Object event = events.get(j);
+ if (event instanceof AutofillId) {
+ notifyViewDisappeared(sessionId, (AutofillId) event);
+ } else if (event instanceof View) {
+ View view = (View) event;
+ ContentCaptureSession session = view.getContentCaptureSession();
+ if (session == null) {
+ Log.w(TAG, "no content capture session on view: " + view);
+ continue for_each_event;
+ }
+ int actualId = session.getId();
+ if (actualId != sessionId) {
+ Log.w(TAG, "content capture session mismatch for view (" + view
+ + "): was " + sessionId + " before, it's " + actualId + " now");
+ continue for_each_event;
+ }
+ ViewStructure structure = session.newViewStructure(view);
+ view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
+ session.notifyViewAppeared(structure);
+ } else if (event instanceof Insets) {
+ notifyViewInsetsChanged(sessionId, (Insets) event);
+ } else {
+ Log.w(TAG, "invalid content capture event: " + event);
+ }
+ }
+ notifyViewTreeEvent(sessionId, /* started= */ false);
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
@Override
void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
super.dump(prefix, pw);
@@ -960,17 +1021,14 @@
return getDebugState() + ", reason=" + getFlushReasonAsString(reason);
}
- @UiThread
private boolean isContentProtectionReceiverEnabled() {
return mManager.mOptions.contentProtectionOptions.enableReceiver;
}
- @UiThread
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
@@ -980,4 +1038,42 @@
&& (!mManager.mOptions.contentProtectionOptions.requiredGroups.isEmpty()
|| !mManager.mOptions.contentProtectionOptions.optionalGroups.isEmpty());
}
+
+ /**
+ * Checks that the current work is running on the assigned thread from {@code mHandler}.
+ *
+ * <p>It is not guaranteed that the callers always invoke function from a single thread.
+ * Therefore, accessing internal properties in {@link MainContentCaptureSession} should
+ * always delegate to the assigned thread from {@code mHandler} for synchronization.</p>
+ */
+ private void checkOnContentCaptureThread() {
+ // TODO(b/309411951): Add metrics to track the issue instead.
+ final boolean onContentCaptureThread = mHandler.getLooper().isCurrentThread();
+ if (!onContentCaptureThread) {
+ Log.e(TAG, "MainContentCaptureSession running on " + Thread.currentThread());
+ }
+ }
+
+ /**
+ * Ensures that {@code r} will be running on the assigned thread.
+ *
+ * <p>This is to prevent unnecessary delegation to Handler that results in fragmented runnable.
+ * </p>
+ */
+ private void runOnContentCaptureThread(@NonNull Runnable r) {
+ if (!mHandler.getLooper().isCurrentThread()) {
+ mHandler.post(r);
+ } else {
+ r.run();
+ }
+ }
+
+ private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) {
+ if (!mHandler.getLooper().isCurrentThread()) {
+ mHandler.removeMessages(what);
+ mHandler.post(r);
+ } else {
+ r.run();
+ }
+ }
}
diff --git a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
index aaf90bd..858401a9 100644
--- a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
+++ b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
@@ -18,7 +18,6 @@
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;
@@ -102,7 +101,6 @@
}
/** Main entry point for {@link ContentCaptureEvent} processing. */
- @UiThread
public void processEvent(@NonNull ContentCaptureEvent event) {
if (EVENT_TYPES_TO_STORE.contains(event.getType())) {
storeEvent(event);
@@ -112,7 +110,6 @@
}
}
- @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();
@@ -121,7 +118,6 @@
mEventBuffer.append(event);
}
- @UiThread
private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) {
ViewNode viewNode = event.getViewNode();
String eventText = ContentProtectionUtils.getEventTextLower(event);
@@ -154,7 +150,6 @@
}
}
- @UiThread
private void loginDetected() {
if (mLastFlushTime == null
|| Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) {
@@ -163,13 +158,11 @@
resetLoginFlags();
}
- @UiThread
private void resetLoginFlags() {
mGroupsAll.forEach(group -> group.mFound = false);
mAnyGroupFound = false;
}
- @UiThread
private void maybeResetLoginFlags() {
if (mAnyGroupFound) {
if (mResetLoginRemainingEventsToProcess <= 0) {
@@ -183,7 +176,6 @@
}
}
- @UiThread
private void flush() {
mLastFlushTime = Instant.now();
@@ -192,7 +184,6 @@
mHandler.post(() -> handlerOnLoginDetected(events));
}
- @UiThread
@NonNull
private ParceledListSlice<ContentCaptureEvent> clearEvents() {
List<ContentCaptureEvent> events = Arrays.asList(mEventBuffer.toArray());
diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
index d47d789..1cdcb37 100644
--- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
@@ -17,11 +17,15 @@
package android.view.contentcapture;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
+import static android.view.contentcapture.ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARED;
+import static android.view.contentcapture.ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARING;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -29,14 +33,20 @@
import android.content.ContentCaptureOptions;
import android.content.Context;
import android.content.pm.ParceledListSlice;
+import android.graphics.Insets;
import android.os.Handler;
-import android.os.Looper;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.autofill.AutofillId;
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.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -56,8 +66,9 @@
* <p>Run with: {@code atest
* FrameworksCoreTests:android.view.contentcapture.MainContentCaptureSessionTest}
*/
-@RunWith(AndroidJUnit4.class)
+@RunWith(AndroidTestingRunner.class)
@SmallTest
+@TestableLooper.RunWithLooper
public class MainContentCaptureSessionTest {
private static final int BUFFER_SIZE = 100;
@@ -75,6 +86,8 @@
private static final ContentCaptureManager.StrippedContext sStrippedContext =
new ContentCaptureManager.StrippedContext(sContext);
+ private TestableLooper mTestableLooper;
+
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock private IContentCaptureManager mMockSystemServerInterface;
@@ -83,12 +96,18 @@
@Mock private IContentCaptureDirectManager mMockContentCaptureDirectManager;
+ @Before
+ public void setup() {
+ mTestableLooper = TestableLooper.get(this);
+ }
+
@Test
public void onSessionStarted_contentProtectionEnabled_processorCreated() {
MainContentCaptureSession session = createSession();
assertThat(session.mContentProtectionEventProcessor).isNull();
session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+ mTestableLooper.processAllMessages();
assertThat(session.mContentProtectionEventProcessor).isNotNull();
}
@@ -102,6 +121,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+ mTestableLooper.processAllMessages();
assertThat(session.mContentProtectionEventProcessor).isNull();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -122,6 +142,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+ mTestableLooper.processAllMessages();
assertThat(session.mContentProtectionEventProcessor).isNull();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -142,6 +163,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+ mTestableLooper.processAllMessages();
assertThat(session.mContentProtectionEventProcessor).isNull();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -153,6 +175,7 @@
session.mComponentName = null;
session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+ mTestableLooper.processAllMessages();
assertThat(session.mContentProtectionEventProcessor).isNull();
}
@@ -166,6 +189,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.sendEvent(EVENT);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
assertThat(session.mEvents).isNull();
@@ -180,6 +204,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.sendEvent(EVENT);
+ mTestableLooper.processAllMessages();
verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
assertThat(session.mEvents).isNull();
@@ -194,6 +219,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.sendEvent(EVENT);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
assertThat(session.mEvents).isNotNull();
@@ -206,6 +232,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.sendEvent(EVENT);
+ mTestableLooper.processAllMessages();
verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
assertThat(session.mEvents).isNotNull();
@@ -220,6 +247,7 @@
/* enableContentProtectionReceiver= */ true);
session.sendEvent(EVENT);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
assertThat(session.mEvents).isNull();
@@ -236,6 +264,7 @@
session.mDirectServiceInterface = mMockContentCaptureDirectManager;
session.flush(REASON);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
verifyZeroInteractions(mMockContentCaptureDirectManager);
@@ -252,6 +281,7 @@
session.mDirectServiceInterface = mMockContentCaptureDirectManager;
session.flush(REASON);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
verifyZeroInteractions(mMockContentCaptureDirectManager);
@@ -269,6 +299,7 @@
session.mDirectServiceInterface = mMockContentCaptureDirectManager;
session.flush(REASON);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
assertThat(session.mEvents).isEmpty();
@@ -286,6 +317,7 @@
session.mDirectServiceInterface = mMockContentCaptureDirectManager;
session.flush(REASON);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
assertThat(session.mEvents).isEmpty();
@@ -298,6 +330,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.destroySession();
+ mTestableLooper.processAllMessages();
verify(mMockSystemServerInterface).finishSession(anyInt());
verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -311,6 +344,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.resetSession(/* newState= */ 0);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockSystemServerInterface);
verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -318,6 +352,111 @@
assertThat(session.mContentProtectionEventProcessor).isNull();
}
+ @Test
+ @SuppressWarnings("GuardedBy")
+ public void notifyContentCaptureEvents_notStarted_ContentCaptureDisabled_ProtectionDisabled() {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ false,
+ /* enableContentProtectionReceiver= */ false);
+ MainContentCaptureSession session = createSession(options);
+
+ notifyContentCaptureEvents(session);
+ mTestableLooper.processAllMessages();
+
+ verifyZeroInteractions(mMockContentCaptureDirectManager);
+ verifyZeroInteractions(mMockContentProtectionEventProcessor);
+ assertThat(session.mEvents).isNull();
+ }
+
+ @Test
+ @SuppressWarnings("GuardedBy")
+ public void notifyContentCaptureEvents_started_ContentCaptureDisabled_ProtectionDisabled() {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ false,
+ /* enableContentProtectionReceiver= */ false);
+ MainContentCaptureSession session = createSession(options);
+
+ session.onSessionStarted(0x2, null);
+ notifyContentCaptureEvents(session);
+ mTestableLooper.processAllMessages();
+
+ verifyZeroInteractions(mMockContentCaptureDirectManager);
+ verifyZeroInteractions(mMockContentProtectionEventProcessor);
+ assertThat(session.mEvents).isNull();
+ }
+
+ @Test
+ @SuppressWarnings("GuardedBy")
+ public void notifyContentCaptureEvents_notStarted_ContentCaptureEnabled_ProtectionEnabled() {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ true,
+ /* enableContentProtectionReceiver= */ true);
+ MainContentCaptureSession session = createSession(options);
+ session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+ session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+ notifyContentCaptureEvents(session);
+ mTestableLooper.processAllMessages();
+
+ verifyZeroInteractions(mMockContentCaptureDirectManager);
+ verifyZeroInteractions(mMockContentProtectionEventProcessor);
+ assertThat(session.mEvents).isNull();
+ }
+
+ @Test
+ @SuppressWarnings("GuardedBy")
+ public void notifyContentCaptureEvents_started_ContentCaptureEnabled_ProtectionEnabled()
+ throws RemoteException {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ true,
+ /* enableContentProtectionReceiver= */ true);
+ MainContentCaptureSession session = createSession(options);
+ session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+ session.onSessionStarted(0x2, null);
+ // Override the processor for interaction verification.
+ session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+ notifyContentCaptureEvents(session);
+ mTestableLooper.processAllMessages();
+
+ // Force flush will happen twice.
+ verify(mMockContentCaptureDirectManager, times(1))
+ .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARING), any());
+ verify(mMockContentCaptureDirectManager, times(1))
+ .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARED), any());
+ // Other than the five view events, there will be two additional tree appearing events.
+ verify(mMockContentProtectionEventProcessor, times(7)).processEvent(any());
+ assertThat(session.mEvents).isEmpty();
+ }
+
+ /** Simulates the regular content capture events sequence. */
+ private void notifyContentCaptureEvents(final MainContentCaptureSession session) {
+ final ArrayList<Object> events = new ArrayList<>(
+ List.of(
+ prepareView(session),
+ prepareView(session),
+ new AutofillId(0),
+ prepareView(session),
+ Insets.of(0, 0, 0, 0)
+ )
+ );
+
+ final SparseArray<ArrayList<Object>> contentCaptureEvents = new SparseArray<>();
+ contentCaptureEvents.set(session.getId(), events);
+
+ session.notifyContentCaptureEvents(contentCaptureEvents);
+ }
+
+ private View prepareView(final MainContentCaptureSession session) {
+ final View view = new View(sContext);
+ view.setContentCaptureSession(session);
+ return view;
+ }
+
private static ContentCaptureOptions createOptions(
boolean enableContentCaptureReceiver,
ContentCaptureOptions.ContentProtectionOptions contentProtectionOptions) {
@@ -354,7 +493,7 @@
new MainContentCaptureSession(
sStrippedContext,
manager,
- new Handler(Looper.getMainLooper()),
+ Handler.createAsync(mTestableLooper.getLooper()),
mMockSystemServerInterface);
session.mComponentName = COMPONENT_NAME;
return session;
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
index af33de0..50ab3f8 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
@@ -63,13 +63,27 @@
*/
int SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED = 5;
+ /**
+ * Indicating that the supported device states have changed because an external display was
+ * added.
+ */
+ int SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED = 6;
+
+ /**
+ * Indicating that the supported device states have changed because an external display was
+ * removed.
+ */
+ int SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED = 7;
+
@IntDef(prefix = { "SUPPORTED_DEVICE_STATES_CHANGED_" }, value = {
SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT,
SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED,
SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL,
SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL,
SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED,
- SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED
+ SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED,
+ SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED,
+ SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED
})
@Retention(RetentionPolicy.SOURCE)
@interface SupportedStatesUpdatedReason {}
diff --git a/services/foldables/devicestateprovider/Android.bp b/services/foldables/devicestateprovider/Android.bp
index 34737ef..56daea7 100644
--- a/services/foldables/devicestateprovider/Android.bp
+++ b/services/foldables/devicestateprovider/Android.bp
@@ -5,9 +5,12 @@
java_library {
name: "foldable-device-state-provider",
srcs: [
- "src/**/*.java"
+ "src/**/*.java",
],
libs: [
"services",
],
+ static_libs: [
+ "device_state_flags_lib",
+ ],
}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index aea46d1..4c487a7 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -21,6 +21,7 @@
import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.TYPE_EXTERNAL;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -33,11 +34,14 @@
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
-import android.os.PowerManager;
import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
import android.os.Trace;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.view.Display;
import com.android.internal.annotations.GuardedBy;
@@ -45,24 +49,26 @@
import com.android.internal.util.Preconditions;
import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider;
+import com.android.server.policy.feature.flags.FeatureFlags;
+import com.android.server.policy.feature.flags.FeatureFlagsImpl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.BooleanSupplier;
-import java.util.function.Function;
+import java.util.function.Predicate;
/**
* Device state provider for foldable devices.
- *
+ * <p>
* It is an implementation of {@link DeviceStateProvider} tailored specifically for
* foldable devices and allows simple callback-based configuration with hall sensor
* and hinge angle sensor values.
*/
public final class FoldableDeviceStateProvider implements DeviceStateProvider,
SensorEventListener, PowerManager.OnThermalStatusChangedListener,
- DisplayManager.DisplayListener {
+ DisplayManager.DisplayListener {
private static final String TAG = "FoldableDeviceStateProvider";
private static final boolean DEBUG = false;
@@ -77,9 +83,17 @@
// are met for the device to be in the state.
private final SparseArray<BooleanSupplier> mStateConditions = new SparseArray<>();
+ // Map of state identifier to a boolean supplier that returns true when the device state has all
+ // the conditions needed for availability.
+ private final SparseArray<BooleanSupplier> mStateAvailabilityConditions = new SparseArray<>();
+
+ @GuardedBy("mLock")
+ private final SparseBooleanArray mExternalDisplaysConnected = new SparseBooleanArray();
+
private final Sensor mHingeAngleSensor;
private final DisplayManager mDisplayManager;
private final Sensor mHallSensor;
+ private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true;
@Nullable
@GuardedBy("mLock")
@@ -99,7 +113,23 @@
@GuardedBy("mLock")
private boolean mPowerSaveModeEnabled;
- public FoldableDeviceStateProvider(@NonNull Context context,
+ private final boolean mIsDualDisplayBlockingEnabled;
+
+ public FoldableDeviceStateProvider(
+ @NonNull Context context,
+ @NonNull SensorManager sensorManager,
+ @NonNull Sensor hingeAngleSensor,
+ @NonNull Sensor hallSensor,
+ @NonNull DisplayManager displayManager,
+ @NonNull DeviceStateConfiguration[] deviceStateConfigurations) {
+ this(new FeatureFlagsImpl(), context, sensorManager, hingeAngleSensor, hallSensor,
+ displayManager, deviceStateConfigurations);
+ }
+
+ @VisibleForTesting
+ public FoldableDeviceStateProvider(
+ @NonNull FeatureFlags featureFlags,
+ @NonNull Context context,
@NonNull SensorManager sensorManager,
@NonNull Sensor hingeAngleSensor,
@NonNull Sensor hallSensor,
@@ -112,6 +142,7 @@
mHingeAngleSensor = hingeAngleSensor;
mHallSensor = hallSensor;
mDisplayManager = displayManager;
+ mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
sensorManager.registerListener(this, mHingeAngleSensor, SENSOR_DELAY_FASTEST);
sensorManager.registerListener(this, mHallSensor, SENSOR_DELAY_FASTEST);
@@ -121,20 +152,15 @@
final DeviceStateConfiguration configuration = deviceStateConfigurations[i];
mOrderedStates[i] = configuration.mDeviceState;
- if (mStateConditions.get(configuration.mDeviceState.getIdentifier()) != null) {
- throw new IllegalArgumentException("Device state configurations must have unique"
- + " device state identifiers, found duplicated identifier: " +
- configuration.mDeviceState.getIdentifier());
- }
-
- mStateConditions.put(configuration.mDeviceState.getIdentifier(), () ->
- configuration.mPredicate.apply(this));
+ assertUniqueDeviceStateIdentifier(configuration);
+ initialiseStateConditions(configuration);
+ initialiseStateAvailabilityConditions(configuration);
}
+ Handler handler = new Handler(Looper.getMainLooper());
mDisplayManager.registerDisplayListener(
/* listener = */ this,
- /* handler= */ null,
- /* eventsMask= */ DisplayManager.EVENT_FLAG_DISPLAY_CHANGED);
+ /* handler= */ handler);
Arrays.sort(mOrderedStates, Comparator.comparingInt(DeviceState::getIdentifier));
@@ -167,6 +193,24 @@
}
}
+ private void assertUniqueDeviceStateIdentifier(DeviceStateConfiguration configuration) {
+ if (mStateConditions.get(configuration.mDeviceState.getIdentifier()) != null) {
+ throw new IllegalArgumentException("Device state configurations must have unique"
+ + " device state identifiers, found duplicated identifier: "
+ + configuration.mDeviceState.getIdentifier());
+ }
+ }
+
+ private void initialiseStateConditions(DeviceStateConfiguration configuration) {
+ mStateConditions.put(configuration.mDeviceState.getIdentifier(), () ->
+ configuration.mActiveStatePredicate.test(this));
+ }
+
+ private void initialiseStateAvailabilityConditions(DeviceStateConfiguration configuration) {
+ mStateAvailabilityConditions.put(configuration.mDeviceState.getIdentifier(), () ->
+ configuration.mAvailabilityPredicate.test(this));
+ }
+
@Override
public void setListener(Listener listener) {
synchronized (mLock) {
@@ -189,16 +233,9 @@
}
listener = mListener;
for (DeviceState deviceState : mOrderedStates) {
- if (isThermalStatusCriticalOrAbove(mThermalStatus)
- && deviceState.hasFlag(
- DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
- continue;
+ if (isStateSupported(deviceState)) {
+ supportedStates.add(deviceState);
}
- if (mPowerSaveModeEnabled && deviceState.hasFlag(
- DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
- continue;
- }
- supportedStates.add(deviceState);
}
}
@@ -206,6 +243,26 @@
supportedStates.toArray(new DeviceState[supportedStates.size()]), reason);
}
+ @GuardedBy("mLock")
+ private boolean isStateSupported(DeviceState deviceState) {
+ if (isThermalStatusCriticalOrAbove(mThermalStatus)
+ && deviceState.hasFlag(
+ DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
+ return false;
+ }
+ if (mPowerSaveModeEnabled && deviceState.hasFlag(
+ DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
+ return false;
+ }
+ if (mIsDualDisplayBlockingEnabled
+ && mStateAvailabilityConditions.contains(deviceState.getIdentifier())) {
+ return mStateAvailabilityConditions
+ .get(deviceState.getIdentifier())
+ .getAsBoolean();
+ }
+ return true;
+ }
+
/** Computes the current device state and notifies the listener of a change, if needed. */
void notifyDeviceStateChangedIfNeeded() {
int stateToReport = INVALID_DEVICE_STATE;
@@ -294,7 +351,7 @@
private void dumpSensorValues() {
Slog.i(TAG, "Sensor values:");
dumpSensorValues("Hall Sensor", mHallSensor, mLastHallSensorEvent);
- dumpSensorValues("Hinge Angle Sensor",mHingeAngleSensor, mLastHingeAngleSensorEvent);
+ dumpSensorValues("Hinge Angle Sensor", mHingeAngleSensor, mLastHingeAngleSensorEvent);
Slog.i(TAG, "isScreenOn: " + isScreenOn());
}
@@ -307,12 +364,35 @@
@Override
public void onDisplayAdded(int displayId) {
+ // TODO(b/312397262): consider virtual displays cases
+ synchronized (mLock) {
+ if (mIsDualDisplayBlockingEnabled
+ && !mExternalDisplaysConnected.get(displayId, false)
+ && mDisplayManager.getDisplay(displayId).getType() == TYPE_EXTERNAL) {
+ mExternalDisplaysConnected.put(displayId, true);
+ // Only update the supported state when going from 0 external display to 1
+ if (mExternalDisplaysConnected.size() == 1) {
+ notifySupportedStatesChanged(
+ SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED);
+ }
+ }
+ }
}
@Override
public void onDisplayRemoved(int displayId) {
+ synchronized (mLock) {
+ if (mIsDualDisplayBlockingEnabled && mExternalDisplaysConnected.get(displayId, false)) {
+ mExternalDisplaysConnected.delete(displayId);
+ // Only update the supported states when going from 1 external display to 0
+ if (mExternalDisplaysConnected.size() == 0) {
+ notifySupportedStatesChanged(
+ SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED);
+ }
+ }
+ }
}
@Override
@@ -338,48 +418,71 @@
*/
public static class DeviceStateConfiguration {
private final DeviceState mDeviceState;
- private final Function<FoldableDeviceStateProvider, Boolean> mPredicate;
+ private final Predicate<FoldableDeviceStateProvider> mActiveStatePredicate;
+ private final Predicate<FoldableDeviceStateProvider> mAvailabilityPredicate;
- private DeviceStateConfiguration(DeviceState deviceState,
- Function<FoldableDeviceStateProvider, Boolean> predicate) {
+ private DeviceStateConfiguration(
+ @NonNull DeviceState deviceState,
+ @NonNull Predicate<FoldableDeviceStateProvider> predicate) {
+ this(deviceState, predicate, ALLOWED);
+ }
+
+ private DeviceStateConfiguration(
+ @NonNull DeviceState deviceState,
+ @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate,
+ @NonNull Predicate<FoldableDeviceStateProvider> availabilityPredicate) {
+
mDeviceState = deviceState;
- mPredicate = predicate;
+ mActiveStatePredicate = activeStatePredicate;
+ mAvailabilityPredicate = availabilityPredicate;
}
public static DeviceStateConfiguration createConfig(
@IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
@NonNull String name,
@DeviceState.DeviceStateFlags int flags,
- Function<FoldableDeviceStateProvider, Boolean> predicate
+ @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate
) {
return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
- predicate);
+ activeStatePredicate);
}
public static DeviceStateConfiguration createConfig(
@IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
@NonNull String name,
- Function<FoldableDeviceStateProvider, Boolean> predicate
+ @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate
) {
return new DeviceStateConfiguration(new DeviceState(identifier, name, /* flags= */ 0),
- predicate);
+ activeStatePredicate);
+ }
+
+ /** Create a configuration with availability predicate **/
+ public static DeviceStateConfiguration createConfig(
+ @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
+ @NonNull String name,
+ @DeviceState.DeviceStateFlags int flags,
+ @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate,
+ @NonNull Predicate<FoldableDeviceStateProvider> availabilityPredicate
+ ) {
+ return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
+ activeStatePredicate, availabilityPredicate);
}
/**
* Creates a device state configuration for a closed tent-mode aware state.
- *
+ * <p>
* During tent mode:
* - The inner display is OFF
* - The outer display is ON
* - The device is partially unfolded (left and right edges could be on the table)
* In this mode the device the device so it could be used in a posture where both left
* and right edges of the unfolded device are on the table.
- *
+ * <p>
* The predicate returns false after the hinge angle reaches
* {@code tentModeSwitchAngleDegrees}. Then it switches back only when the hinge angle
* becomes less than {@code maxClosedAngleDegrees}. Hinge angle is 0 degrees when the device
* is fully closed and 180 degrees when it is fully unfolded.
- *
+ * <p>
* For example, when tentModeSwitchAngleDegrees = 90 and maxClosedAngleDegrees = 5 degrees:
* - when unfolding the device from fully closed posture (last state == closed or it is
* undefined yet) this state will become not matching after reaching the angle
@@ -435,6 +538,15 @@
}
/**
+ * @return Whether there is an external connected display.
+ */
+ public boolean hasNoConnectedExternalDisplay() {
+ synchronized (mLock) {
+ return mExternalDisplaysConnected.size() == 0;
+ }
+ }
+
+ /**
* @return Whether the screen is on.
*/
public boolean isScreenOn() {
@@ -442,6 +554,7 @@
return mIsScreenOn;
}
}
+
/**
* @return current hinge angle value of a foldable device
*/
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
index 5f2cf3c..5968b63 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
@@ -33,6 +33,10 @@
import com.android.server.devicestate.DeviceStatePolicy;
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
+import com.android.server.policy.feature.flags.FeatureFlags;
+import com.android.server.policy.feature.flags.FeatureFlagsImpl;
+
+import java.util.function.Predicate;
/**
* Device state policy for a foldable device that supports tent mode: a mode when the device
@@ -55,6 +59,10 @@
private final DeviceStateProvider mProvider;
+ private final boolean mIsDualDisplayBlockingEnabled;
+ private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true;
+ private static final Predicate<FoldableDeviceStateProvider> NOT_ALLOWED = p -> false;
+
/**
* Creates TentModeDeviceStatePolicy
*
@@ -67,6 +75,12 @@
*/
public TentModeDeviceStatePolicy(@NonNull Context context,
@NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, int closeAngleDegrees) {
+ this(new FeatureFlagsImpl(), context, hingeAngleSensor, hallSensor, closeAngleDegrees);
+ }
+
+ public TentModeDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context,
+ @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor,
+ int closeAngleDegrees) {
super(context);
final SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
@@ -74,8 +88,10 @@
final DeviceStateConfiguration[] configuration = createConfiguration(closeAngleDegrees);
- mProvider = new FoldableDeviceStateProvider(mContext, sensorManager, hingeAngleSensor,
- hallSensor, displayManager, configuration);
+ mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
+
+ mProvider = new FoldableDeviceStateProvider(mContext, sensorManager,
+ hingeAngleSensor, hallSensor, displayManager, configuration);
}
private DeviceStateConfiguration[] createConfiguration(int closeAngleDegrees) {
@@ -83,24 +99,27 @@
createClosedConfiguration(closeAngleDegrees),
createConfig(DEVICE_STATE_HALF_OPENED,
/* name= */ "HALF_OPENED",
- (provider) -> {
+ /* activeStatePredicate= */ (provider) -> {
final float hingeAngle = provider.getHingeAngle();
return hingeAngle >= MAX_CLOSED_ANGLE_DEGREES
&& hingeAngle <= TABLE_TOP_MODE_SWITCH_ANGLE_DEGREES;
}),
createConfig(DEVICE_STATE_OPENED,
/* name= */ "OPENED",
- (provider) -> true),
+ /* activeStatePredicate= */ ALLOWED),
createConfig(DEVICE_STATE_REAR_DISPLAY_STATE,
/* name= */ "REAR_DISPLAY_STATE",
/* flags= */ FLAG_EMULATED_ONLY,
- (provider) -> false),
+ /* activeStatePredicate= */ NOT_ALLOWED),
createConfig(DEVICE_STATE_CONCURRENT_INNER_DEFAULT,
/* name= */ "CONCURRENT_INNER_DEFAULT",
/* flags= */ FLAG_EMULATED_ONLY | FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP
| FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
| FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE,
- (provider) -> false)
+ /* activeStatePredicate= */ NOT_ALLOWED,
+ /* availabilityPredicate= */
+ provider -> !mIsDualDisplayBlockingEnabled
+ || provider.hasNoConnectedExternalDisplay())
};
}
@@ -111,7 +130,7 @@
DEVICE_STATE_CLOSED,
/* name= */ "CLOSED",
/* flags= */ FLAG_CANCEL_OVERRIDE_REQUESTS,
- (provider) -> {
+ /* activeStatePredicate= */ (provider) -> {
final float hingeAngle = provider.getHingeAngle();
return hingeAngle <= closeAngleDegrees;
}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
new file mode 100644
index 0000000..6ad8d79
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
@@ -0,0 +1,12 @@
+aconfig_declarations {
+ name: "device_state_flags",
+ package: "com.android.server.policy.feature.flags",
+ srcs: [
+ "device_state_flags.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "device_state_flags_lib",
+ aconfig_declarations: "device_state_flags",
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
new file mode 100644
index 0000000..47c2a1b
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.policy.feature.flags"
+
+flag {
+ name: "enable_dual_display_blocking"
+ namespace: "display_manager"
+ description: "Feature flag for dual display blocking"
+ bug: "278667199"
+}
\ No newline at end of file
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
index 8fa4ce5..ddf4a08 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
@@ -17,18 +17,21 @@
package com.android.server.policy;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.STATE_OFF;
+import static android.view.Display.STATE_ON;
+import static android.view.Display.TYPE_EXTERNAL;
+import static android.view.Display.TYPE_INTERNAL;
+
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED;
import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED;
import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED;
import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED;
import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL;
import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL;
-import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.Display.STATE_OFF;
-import static android.view.Display.STATE_ON;
-
import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
@@ -36,12 +39,11 @@
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.nullable;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -51,20 +53,21 @@
import android.hardware.SensorManager;
import android.hardware.display.DisplayManager;
import android.hardware.input.InputSensorInfo;
-import android.os.PowerManager;
import android.os.Handler;
+import android.os.PowerManager;
import android.testing.AndroidTestingRunner;
import android.view.Display;
import com.android.server.devicestate.DeviceState;
-import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.devicestate.DeviceStateProvider.Listener;
+import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
+import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl;
+import com.android.server.policy.feature.flags.Flags;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatchers;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -95,10 +98,16 @@
@Mock
private DisplayManager mDisplayManager;
private FoldableDeviceStateProvider mProvider;
+ @Mock
+ private Display mDefaultDisplay;
+ @Mock
+ private Display mExternalDisplay;
+ private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl();
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
+ mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_DUAL_DISPLAY_BLOCKING, true);
mHallSensor = new Sensor(mInputSensorInfo);
mHingeAngleSensor = new Sensor(mInputSensorInfo);
@@ -473,6 +482,133 @@
assertThat(mProvider.isScreenOn()).isFalse();
}
+ @Test
+ public void test_dualScreenDisabledWhenExternalScreenIsConnected() throws Exception {
+ when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+ when(mDefaultDisplay.getType()).thenReturn(TYPE_INTERNAL);
+
+ createProvider(createConfig(/* identifier= */ 1, /* name= */ "CLOSED",
+ (c) -> c.getHingeAngle() < 5f),
+ createConfig(/* identifier= */ 2, /* name= */ "HALF_OPENED",
+ (c) -> c.getHingeAngle() < 90f),
+ createConfig(/* identifier= */ 3, /* name= */ "OPENED",
+ (c) -> c.getHingeAngle() < 180f),
+ createConfig(/* identifier= */ 4, /* name= */ "DUAL_DISPLAY", /* flags */ 0,
+ (c) -> false, FoldableDeviceStateProvider::hasNoConnectedExternalDisplay));
+
+ Listener listener = mock(Listener.class);
+ mProvider.setListener(listener);
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+ assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+ new DeviceState[]{
+ new DeviceState(1, "CLOSED", 0 /* flags */),
+ new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+ new DeviceState(3, "OPENED", 0 /* flags */),
+ new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+
+ clearInvocations(listener);
+
+ when(mDisplayManager.getDisplays())
+ .thenReturn(new Display[]{mDefaultDisplay, mExternalDisplay});
+ when(mDisplayManager.getDisplay(1)).thenReturn(mExternalDisplay);
+ when(mExternalDisplay.getType()).thenReturn(TYPE_EXTERNAL);
+
+ // The DUAL_DISPLAY state should be disabled.
+ mProvider.onDisplayAdded(1);
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+ assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+ new DeviceState[]{
+ new DeviceState(1, "CLOSED", 0 /* flags */),
+ new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+ new DeviceState(3, "OPENED", 0 /* flags */)}).inOrder();
+ clearInvocations(listener);
+
+ // The DUAL_DISPLAY state should be re-enabled.
+ when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+ mProvider.onDisplayRemoved(1);
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED));
+ assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+ new DeviceState[]{
+ new DeviceState(1, "CLOSED", 0 /* flags */),
+ new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+ new DeviceState(3, "OPENED", 0 /* flags */),
+ new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+ }
+
+ @Test
+ public void test_notifySupportedStatesChangedCalledOnlyOnInitialExternalScreenAddition() {
+ when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+ when(mDefaultDisplay.getType()).thenReturn(TYPE_INTERNAL);
+
+ createProvider(createConfig(/* identifier= */ 1, /* name= */ "CLOSED",
+ (c) -> c.getHingeAngle() < 5f),
+ createConfig(/* identifier= */ 2, /* name= */ "HALF_OPENED",
+ (c) -> c.getHingeAngle() < 90f),
+ createConfig(/* identifier= */ 3, /* name= */ "OPENED",
+ (c) -> c.getHingeAngle() < 180f),
+ createConfig(/* identifier= */ 4, /* name= */ "DUAL_DISPLAY", /* flags */ 0,
+ (c) -> false, FoldableDeviceStateProvider::hasNoConnectedExternalDisplay));
+
+ Listener listener = mock(Listener.class);
+ mProvider.setListener(listener);
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+ assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+ new DeviceState[]{
+ new DeviceState(1, "CLOSED", 0 /* flags */),
+ new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+ new DeviceState(3, "OPENED", 0 /* flags */),
+ new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+
+ clearInvocations(listener);
+
+ addExternalDisplay(1);
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+ addExternalDisplay(2);
+ addExternalDisplay(3);
+ addExternalDisplay(4);
+ verify(listener, times(1))
+ .onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+ }
+
+ @Test
+ public void hasNoConnectedDisplay_afterExternalDisplayAdded_returnsFalse() {
+ createProvider(
+ createConfig(
+ /* identifier= */ 1, /* name= */ "ONE",
+ /* flags= */0, (c) -> true,
+ FoldableDeviceStateProvider::hasNoConnectedExternalDisplay)
+ );
+
+ addExternalDisplay(/* displayId */ 1);
+
+ assertThat(mProvider.hasNoConnectedExternalDisplay()).isFalse();
+ }
+
+ @Test
+ public void hasNoConnectedDisplay_afterExternalDisplayAddedAndRemoved_returnsTrue() {
+ createProvider(
+ createConfig(
+ /* identifier= */ 1, /* name= */ "ONE",
+ /* flags= */0, (c) -> true,
+ FoldableDeviceStateProvider::hasNoConnectedExternalDisplay)
+ );
+
+ addExternalDisplay(/* displayId */ 1);
+ mProvider.onDisplayRemoved(1);
+
+ assertThat(mProvider.hasNoConnectedExternalDisplay()).isTrue();
+ }
+ private void addExternalDisplay(int displayId) {
+ when(mDisplayManager.getDisplay(displayId)).thenReturn(mExternalDisplay);
+ when(mExternalDisplay.getType()).thenReturn(TYPE_EXTERNAL);
+ mProvider.onDisplayAdded(displayId);
+ }
private void setScreenOn(boolean isOn) {
Display mockDisplay = mock(Display.class);
int state = isOn ? STATE_ON : STATE_OFF;
@@ -508,12 +644,11 @@
}
private void createProvider(DeviceStateConfiguration... configurations) {
- mProvider = new FoldableDeviceStateProvider(mContext, mSensorManager, mHingeAngleSensor,
- mHallSensor, mDisplayManager, configurations);
+ mProvider = new FoldableDeviceStateProvider(mFakeFeatureFlags, mContext, mSensorManager,
+ mHingeAngleSensor, mHallSensor, mDisplayManager, configurations);
verify(mDisplayManager)
.registerDisplayListener(
mDisplayListenerCaptor.capture(),
- nullable(Handler.class),
- anyLong());
+ nullable(Handler.class));
}
}