Merge "Revert^3 "Remove dozeui binder call"" into main
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 3575545..f644185 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -2608,7 +2608,14 @@
break;
case EXECUTE_TRANSACTION:
final ClientTransaction transaction = (ClientTransaction) msg.obj;
- mTransactionExecutor.execute(transaction);
+ final ClientTransactionListenerController controller =
+ ClientTransactionListenerController.getInstance();
+ controller.onClientTransactionStarted();
+ try {
+ mTransactionExecutor.execute(transaction);
+ } finally {
+ controller.onClientTransactionFinished();
+ }
if (isSystem()) {
// Client transactions inside system process are recycled on the client side
// instead of ClientLifecycleManager to avoid being cleared before this
@@ -6747,6 +6754,21 @@
void handleActivityConfigurationChanged(@NonNull ActivityClientRecord r,
@NonNull Configuration overrideConfig, int displayId,
@NonNull ActivityWindowInfo activityWindowInfo, boolean alwaysReportChange) {
+ final ClientTransactionListenerController controller =
+ ClientTransactionListenerController.getInstance();
+ final Context contextToUpdate = r.activity;
+ controller.onContextConfigurationPreChanged(contextToUpdate);
+ try {
+ handleActivityConfigurationChangedInner(r, overrideConfig, displayId,
+ activityWindowInfo, alwaysReportChange);
+ } finally {
+ controller.onContextConfigurationPostChanged(contextToUpdate);
+ }
+ }
+
+ private void handleActivityConfigurationChangedInner(@NonNull ActivityClientRecord r,
+ @NonNull Configuration overrideConfig, int displayId,
+ @NonNull ActivityWindowInfo activityWindowInfo, boolean alwaysReportChange) {
synchronized (mPendingOverrideConfigs) {
final Configuration pendingOverrideConfig = mPendingOverrideConfigs.get(r.token);
if (overrideConfig.isOtherSeqNewer(pendingOverrideConfig)) {
diff --git a/core/java/android/app/ConfigurationController.java b/core/java/android/app/ConfigurationController.java
index 18dc1ce..62a50db 100644
--- a/core/java/android/app/ConfigurationController.java
+++ b/core/java/android/app/ConfigurationController.java
@@ -21,6 +21,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.servertransaction.ClientTransactionListenerController;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.res.CompatibilityInfo;
@@ -145,6 +146,24 @@
*/
void handleConfigurationChanged(@Nullable Configuration config,
@Nullable CompatibilityInfo compat) {
+ final ClientTransactionListenerController controller =
+ ClientTransactionListenerController.getInstance();
+ final Context contextToUpdate = ActivityThread.currentApplication();
+ controller.onContextConfigurationPreChanged(contextToUpdate);
+ try {
+ handleConfigurationChangedInner(config, compat);
+ } finally {
+ controller.onContextConfigurationPostChanged(contextToUpdate);
+ }
+ }
+
+ /**
+ * Update the configuration to latest.
+ * @param config The new configuration.
+ * @param compat The new compatibility information.
+ */
+ private void handleConfigurationChangedInner(@Nullable Configuration config,
+ @Nullable CompatibilityInfo compat) {
int configDiff;
boolean equivalent;
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 8f81ae2..cf06416 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -50,8 +50,8 @@
void cancelAllNotifications(String pkg, int userId);
void clearData(String pkg, int uid, boolean fromApp);
- void enqueueTextToast(String pkg, IBinder token, CharSequence text, int duration, boolean isUiContext, int displayId, @nullable ITransientNotificationCallback callback);
- void enqueueToast(String pkg, IBinder token, ITransientNotification callback, int duration, boolean isUiContext, int displayId);
+ boolean enqueueTextToast(String pkg, IBinder token, CharSequence text, int duration, boolean isUiContext, int displayId, @nullable ITransientNotificationCallback callback);
+ boolean enqueueToast(String pkg, IBinder token, ITransientNotification callback, int duration, boolean isUiContext, int displayId);
void cancelToast(String pkg, IBinder token);
void finishToken(String pkg, IBinder token);
diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java
index a8d61db..6e7e930 100644
--- a/core/java/android/app/servertransaction/ClientTransactionItem.java
+++ b/core/java/android/app/servertransaction/ClientTransactionItem.java
@@ -53,6 +53,7 @@
return true;
}
+ // TODO(b/260873529): cleanup
/**
* If this {@link ClientTransactionItem} is updating configuration, returns the {@link Context}
* it is updating; otherwise, returns {@code null}.
diff --git a/core/java/android/app/servertransaction/ClientTransactionListenerController.java b/core/java/android/app/servertransaction/ClientTransactionListenerController.java
index c55b0f1..722d5f0 100644
--- a/core/java/android/app/servertransaction/ClientTransactionListenerController.java
+++ b/core/java/android/app/servertransaction/ClientTransactionListenerController.java
@@ -16,6 +16,8 @@
package android.app.servertransaction;
+import static android.app.WindowConfiguration.areConfigurationsEqualForDisplay;
+
import static com.android.window.flags.Flags.activityWindowInfoFlag;
import static com.android.window.flags.Flags.bundleClientTransactionFlag;
@@ -24,8 +26,11 @@
import android.annotation.NonNull;
import android.app.Activity;
import android.app.ActivityThread;
+import android.content.Context;
+import android.content.res.Configuration;
import android.hardware.display.DisplayManagerGlobal;
import android.os.IBinder;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.window.ActivityWindowInfo;
@@ -51,6 +56,15 @@
private final ArraySet<BiConsumer<IBinder, ActivityWindowInfo>>
mActivityWindowInfoChangedListeners = new ArraySet<>();
+ /**
+ * Keeps track of the Context whose Configuration will get updated, mapping to the config before
+ * the change.
+ */
+ private final ArrayMap<Context, Configuration> mContextToPreChangedConfigMap = new ArrayMap<>();
+
+ /** Whether there is an {@link ClientTransaction} being executed. */
+ private boolean mIsClientTransactionExecuting;
+
/** Gets the singleton controller. */
@NonNull
public static ClientTransactionListenerController getInstance() {
@@ -126,18 +140,92 @@
}
}
- /**
- * Called when receives a {@link ClientTransaction} that is updating display-related
- * window configuration.
- */
- public void onDisplayChanged(int displayId) {
- if (!bundleClientTransactionFlag()) {
- return;
- }
- if (ActivityThread.isSystem()) {
+ /** Called when starts executing a remote {@link ClientTransaction}. */
+ public void onClientTransactionStarted() {
+ mIsClientTransactionExecuting = true;
+ }
+
+ /** Called when finishes executing a remote {@link ClientTransaction}. */
+ public void onClientTransactionFinished() {
+ notifyDisplayManagerIfNeeded();
+ mIsClientTransactionExecuting = false;
+ }
+
+ /** Called before updating the Configuration of the given {@code context}. */
+ public void onContextConfigurationPreChanged(@NonNull Context context) {
+ if (!bundleClientTransactionFlag() || ActivityThread.isSystem()) {
// Not enable for system server.
return;
}
+ if (mContextToPreChangedConfigMap.containsKey(context)) {
+ // There is an earlier change that hasn't been reported yet.
+ return;
+ }
+ mContextToPreChangedConfigMap.put(context,
+ new Configuration(context.getResources().getConfiguration()));
+ }
+
+ /** Called after updating the Configuration of the given {@code context}. */
+ public void onContextConfigurationPostChanged(@NonNull Context context) {
+ if (!bundleClientTransactionFlag() || ActivityThread.isSystem()) {
+ // Not enable for system server.
+ return;
+ }
+ if (mIsClientTransactionExecuting) {
+ // Wait until #onClientTransactionFinished to prevent it from triggering the same
+ // #onDisplayChanged multiple times within the same ClientTransaction.
+ return;
+ }
+ final Configuration preChangedConfig = mContextToPreChangedConfigMap.remove(context);
+ if (preChangedConfig != null && shouldReportDisplayChange(context, preChangedConfig)) {
+ onDisplayChanged(context.getDisplayId());
+ }
+ }
+
+ /**
+ * When {@link Configuration} is changed, we want to trigger display change callback as well,
+ * because Display reads some fields from {@link Configuration}.
+ */
+ private void notifyDisplayManagerIfNeeded() {
+ if (mContextToPreChangedConfigMap.isEmpty()) {
+ return;
+ }
+ // Whether the configuration change should trigger DisplayListener#onDisplayChanged.
+ try {
+ // Calculate display ids that have config changed.
+ final ArraySet<Integer> configUpdatedDisplayIds = new ArraySet<>();
+ final int contextCount = mContextToPreChangedConfigMap.size();
+ for (int i = 0; i < contextCount; i++) {
+ final Context context = mContextToPreChangedConfigMap.keyAt(i);
+ final Configuration preChangedConfig = mContextToPreChangedConfigMap.valueAt(i);
+ if (shouldReportDisplayChange(context, preChangedConfig)) {
+ configUpdatedDisplayIds.add(context.getDisplayId());
+ }
+ }
+
+ // Dispatch the display changed callbacks.
+ final int displayCount = configUpdatedDisplayIds.size();
+ for (int i = 0; i < displayCount; i++) {
+ final int displayId = configUpdatedDisplayIds.valueAt(i);
+ onDisplayChanged(displayId);
+ }
+ } finally {
+ mContextToPreChangedConfigMap.clear();
+ }
+ }
+
+ private boolean shouldReportDisplayChange(@NonNull Context context,
+ @NonNull Configuration preChangedConfig) {
+ final Configuration postChangedConfig = context.getResources().getConfiguration();
+ return !areConfigurationsEqualForDisplay(postChangedConfig, preChangedConfig);
+ }
+
+ /**
+ * Called when receives a {@link Configuration} changed event that is updating display-related
+ * window configuration.
+ */
+ @VisibleForTesting
+ public void onDisplayChanged(int displayId) {
mDisplayManager.handleDisplayChangeFromWindowManager(displayId);
}
}
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index c837191..480205e 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -16,7 +16,6 @@
package android.app.servertransaction;
-import static android.app.WindowConfiguration.areConfigurationsEqualForDisplay;
import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE;
import static android.app.servertransaction.ActivityLifecycleItem.ON_DESTROY;
import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
@@ -32,17 +31,12 @@
import static android.app.servertransaction.TransactionExecutorHelper.tId;
import static android.app.servertransaction.TransactionExecutorHelper.transactionToString;
-import static com.android.window.flags.Flags.bundleClientTransactionFlag;
-
import android.annotation.NonNull;
import android.app.ActivityThread.ActivityClientRecord;
import android.app.ClientTransactionHandler;
import android.content.Context;
-import android.content.res.Configuration;
import android.os.IBinder;
import android.os.Trace;
-import android.util.ArrayMap;
-import android.util.ArraySet;
import android.util.IntArray;
import android.util.Slog;
@@ -63,12 +57,6 @@
private final PendingTransactionActions mPendingActions = new PendingTransactionActions();
private final TransactionExecutorHelper mHelper = new TransactionExecutorHelper();
- /**
- * Keeps track of the Context whose Configuration got updated within a transaction, mapping to
- * the config before the transaction.
- */
- private final ArrayMap<Context, Configuration> mContextToPreChangedConfigMap = new ArrayMap<>();
-
/** Initialize an instance with transaction handler, that will execute all requested actions. */
public TransactionExecutor(@NonNull ClientTransactionHandler clientTransactionHandler) {
mTransactionHandler = clientTransactionHandler;
@@ -104,37 +92,6 @@
Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
}
- if (!mContextToPreChangedConfigMap.isEmpty()) {
- // Whether this transaction should trigger DisplayListener#onDisplayChanged.
- try {
- // Calculate display ids that have config changed.
- final ArraySet<Integer> configUpdatedDisplayIds = new ArraySet<>();
- final int contextCount = mContextToPreChangedConfigMap.size();
- for (int i = 0; i < contextCount; i++) {
- final Context context = mContextToPreChangedConfigMap.keyAt(i);
- final Configuration preTransactionConfig =
- mContextToPreChangedConfigMap.valueAt(i);
- final Configuration postTransactionConfig = context.getResources()
- .getConfiguration();
- if (!areConfigurationsEqualForDisplay(
- postTransactionConfig, preTransactionConfig)) {
- configUpdatedDisplayIds.add(context.getDisplayId());
- }
- }
-
- // Dispatch the display changed callbacks.
- final ClientTransactionListenerController controller =
- ClientTransactionListenerController.getInstance();
- final int displayCount = configUpdatedDisplayIds.size();
- for (int i = 0; i < displayCount; i++) {
- final int displayId = configUpdatedDisplayIds.valueAt(i);
- controller.onDisplayChanged(displayId);
- }
- } finally {
- mContextToPreChangedConfigMap.clear();
- }
- }
-
mPendingActions.clear();
if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "End resolving transaction");
}
@@ -214,20 +171,6 @@
}
}
- final boolean shouldTrackConfigUpdatedContext =
- // No configuration change for local transaction.
- !mTransactionHandler.isExecutingLocalTransaction()
- && bundleClientTransactionFlag();
- final Context configUpdatedContext = shouldTrackConfigUpdatedContext
- ? item.getContextToUpdate(mTransactionHandler)
- : null;
- if (configUpdatedContext != null
- && !mContextToPreChangedConfigMap.containsKey(configUpdatedContext)) {
- // Keep track of the first pre-executed config of each changed Context.
- mContextToPreChangedConfigMap.put(configUpdatedContext,
- new Configuration(configUpdatedContext.getResources().getConfiguration()));
- }
-
item.execute(mTransactionHandler, mPendingActions);
item.postExecute(mTransactionHandler, mPendingActions);
diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java
index 253073a..69228ca 100644
--- a/core/java/android/view/HapticFeedbackConstants.java
+++ b/core/java/android/view/HapticFeedbackConstants.java
@@ -83,11 +83,7 @@
*/
public static final int TEXT_HANDLE_MOVE = 9;
- /**
- * The user unlocked the device
- * @hide
- */
- public static final int ENTRY_BUMP = 10;
+ // REMOVED: ENTRY_BUMP = 10
/**
* The user has moved the dragged object within a droppable area.
@@ -230,6 +226,22 @@
public static final int LONG_PRESS_POWER_BUTTON = 10003;
/**
+ * A haptic effect to signal the confirmation of a user biometric authentication
+ * (e.g. fingerprint reading).
+ * This is a private constant to be used only by system apps.
+ * @hide
+ */
+ public static final int BIOMETRIC_CONFIRM = 10004;
+
+ /**
+ * A haptic effect to signal the rejection of a user biometric authentication attempt
+ * (e.g. fingerprint reading).
+ * This is a private constant to be used only by system apps.
+ * @hide
+ */
+ public static final int BIOMETRIC_REJECT = 10005;
+
+ /**
* Flag for {@link View#performHapticFeedback(int, int)
* View.performHapticFeedback(int, int)}: Ignore the setting in the
* view for whether to perform haptic feedback, do it always.
diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java
index 65984f5..ead8887 100644
--- a/core/java/android/widget/Toast.java
+++ b/core/java/android/widget/Toast.java
@@ -45,8 +45,10 @@
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.IAccessibilityManager;
+import android.widget.flags.Flags;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -205,27 +207,41 @@
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
- tn.mNextView = new WeakReference<>(mNextView);
+ if (Flags.toastNoWeakref()) {
+ tn.mNextView = mNextView;
+ } else {
+ tn.mNextViewWeakRef = new WeakReference<>(mNextView);
+ }
final boolean isUiContext = mContext.isUiContext();
final int displayId = mContext.getDisplayId();
+ boolean wasEnqueued = false;
try {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
if (mNextView != null) {
// It's a custom toast
- service.enqueueToast(pkg, mToken, tn, mDuration, isUiContext, displayId);
+ wasEnqueued = service.enqueueToast(pkg, mToken, tn, mDuration, isUiContext,
+ displayId);
} else {
// It's a text toast
ITransientNotificationCallback callback =
new CallbackBinder(mCallbacks, mHandler);
- service.enqueueTextToast(pkg, mToken, mText, mDuration, isUiContext, displayId,
- callback);
+ wasEnqueued = service.enqueueTextToast(pkg, mToken, mText, mDuration,
+ isUiContext, displayId, callback);
}
} else {
- service.enqueueToast(pkg, mToken, tn, mDuration, isUiContext, displayId);
+ wasEnqueued = service.enqueueToast(pkg, mToken, tn, mDuration, isUiContext,
+ displayId);
}
} catch (RemoteException e) {
// Empty
+ } finally {
+ if (Flags.toastNoWeakref()) {
+ if (!wasEnqueued) {
+ tn.mNextViewWeakRef = null;
+ tn.mNextView = null;
+ }
+ }
}
}
@@ -581,6 +597,16 @@
}
}
+ /**
+ * Get the Toast.TN ITransientNotification object
+ * @return TN
+ * @hide
+ */
+ @VisibleForTesting
+ public TN getTn() {
+ return mTN;
+ }
+
// =======================================================================================
// All the gunk below is the interaction with the Notification Service, which handles
// the proper ordering of these system-wide.
@@ -599,7 +625,11 @@
return sService;
}
- private static class TN extends ITransientNotification.Stub {
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public static class TN extends ITransientNotification.Stub {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
private final WindowManager.LayoutParams mParams;
@@ -620,7 +650,9 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
View mView;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
- WeakReference<View> mNextView;
+ WeakReference<View> mNextViewWeakRef;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ View mNextView;
int mDuration;
WindowManager mWM;
@@ -662,14 +694,22 @@
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
- mNextView = null;
+ if (Flags.toastNoWeakref()) {
+ mNextView = null;
+ } else {
+ mNextViewWeakRef = null;
+ }
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
- mNextView = null;
+ if (Flags.toastNoWeakref()) {
+ mNextView = null;
+ } else {
+ mNextViewWeakRef = null;
+ }
try {
getService().cancelToast(mPackageName, mToken);
} catch (RemoteException e) {
@@ -716,21 +756,43 @@
}
public void handleShow(IBinder windowToken) {
- if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
- + " mNextView=" + mNextView);
+ if (Flags.toastNoWeakref()) {
+ if (localLOGV) {
+ Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ + " mNextView=" + mNextView);
+ }
+ } else {
+ if (localLOGV) {
+ Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ + " mNextView=" + mNextViewWeakRef);
+ }
+ }
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
- if (mNextView != null && mView != mNextView.get()) {
- // remove the old view if necessary
- handleHide();
- mView = mNextView.get();
- if (mView != null) {
- mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
- mHorizontalMargin, mVerticalMargin,
- new CallbackBinder(getCallbacks(), mHandler));
+ if (Flags.toastNoWeakref()) {
+ if (mNextView != null && mView != mNextView) {
+ // remove the old view if necessary
+ handleHide();
+ mView = mNextView;
+ if (mView != null) {
+ mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
+ mHorizontalMargin, mVerticalMargin,
+ new CallbackBinder(getCallbacks(), mHandler));
+ }
+ }
+ } else {
+ if (mNextViewWeakRef != null && mView != mNextViewWeakRef.get()) {
+ // remove the old view if necessary
+ handleHide();
+ mView = mNextViewWeakRef.get();
+ if (mView != null) {
+ mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
+ mHorizontalMargin, mVerticalMargin,
+ new CallbackBinder(getCallbacks(), mHandler));
+ }
}
}
}
@@ -745,6 +807,23 @@
mView = null;
}
}
+
+ /**
+ * Get the next view to show for enqueued toasts
+ * Custom toast views are deprecated.
+ * @see #setView(View)
+ *
+ * @return next view
+ * @hide
+ */
+ @VisibleForTesting
+ public View getNextView() {
+ if (Flags.toastNoWeakref()) {
+ return mNextView;
+ } else {
+ return (mNextViewWeakRef != null) ? mNextViewWeakRef.get() : null;
+ }
+ }
}
/**
diff --git a/core/java/android/widget/flags/notification_widget_flags.aconfig b/core/java/android/widget/flags/notification_widget_flags.aconfig
index e60fa15..515fa55 100644
--- a/core/java/android/widget/flags/notification_widget_flags.aconfig
+++ b/core/java/android/widget/flags/notification_widget_flags.aconfig
@@ -25,4 +25,14 @@
metadata {
purpose: PURPOSE_BUGFIX
}
-}
\ No newline at end of file
+}
+
+flag {
+ name: "toast_no_weakref"
+ namespace: "systemui"
+ description: "Do not use WeakReference for custom view Toast"
+ bug: "321732224"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index 4a3aba1..a868d48 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -25,6 +25,7 @@
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.ResourcesManager;
+import android.app.servertransaction.ClientTransactionListenerController;
import android.content.Context;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
@@ -137,12 +138,24 @@
* should be dispatched to listeners.
*/
@AnyThread
- public void onConfigurationChanged(Configuration newConfig, int newDisplayId,
+ public void onConfigurationChanged(@NonNull Configuration newConfig, int newDisplayId,
boolean shouldReportConfigChange) {
final Context context = mContextRef.get();
if (context == null) {
return;
}
+ final ClientTransactionListenerController controller =
+ ClientTransactionListenerController.getInstance();
+ controller.onContextConfigurationPreChanged(context);
+ try {
+ onConfigurationChangedInner(context, newConfig, newDisplayId, shouldReportConfigChange);
+ } finally {
+ controller.onContextConfigurationPostChanged(context);
+ }
+ }
+
+ private void onConfigurationChangedInner(@NonNull Context context,
+ @NonNull Configuration newConfig, int newDisplayId, boolean shouldReportConfigChange) {
CompatibilityInfo.applyOverrideScaleIfNeeded(newConfig);
final boolean displayChanged;
final boolean shouldUpdateResources;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 913b63e..c694426 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2593,6 +2593,13 @@
<permission android:name="android.permission.VIBRATE_ALWAYS_ON"
android:protectionLevel="signature" />
+ <!-- Allows access to system-only haptic feedback constants.
+ <p>Protection level: signature
+ @hide
+ -->
+ <permission android:name="android.permission.VIBRATE_SYSTEM_CONSTANTS"
+ android:protectionLevel="signature" />
+
<!-- @SystemApi Allows access to the vibrator state.
<p>Protection level: signature
@hide
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
index 77d31a5..8506905 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
@@ -23,11 +23,17 @@
import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import android.app.Activity;
+import android.content.res.Configuration;
+import android.content.res.Resources;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerGlobal;
@@ -76,6 +82,13 @@
private BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener;
@Mock
private IBinder mActivityToken;
+ @Mock
+ private Activity mActivity;
+ @Mock
+ private Resources mResources;
+
+ private Configuration mConfiguration;
+
private DisplayManagerGlobal mDisplayManager;
private Handler mHandler;
@@ -88,7 +101,12 @@
MockitoAnnotations.initMocks(this);
mDisplayManager = new DisplayManagerGlobal(mIDisplayManager);
mHandler = getInstrumentation().getContext().getMainThreadHandler();
- mController = ClientTransactionListenerController.createInstanceForTesting(mDisplayManager);
+ mController = spy(ClientTransactionListenerController
+ .createInstanceForTesting(mDisplayManager));
+
+ mConfiguration = new Configuration();
+ doReturn(mConfiguration).when(mResources).getConfiguration();
+ doReturn(mResources).when(mActivity).getResources();
}
@Test
@@ -107,6 +125,43 @@
}
@Test
+ public void testOnContextConfigurationChanged() {
+ doNothing().when(mController).onDisplayChanged(anyInt());
+ doReturn(123).when(mActivity).getDisplayId();
+
+ // Not trigger onDisplayChanged when there is no change.
+ mController.onContextConfigurationPreChanged(mActivity);
+ mController.onContextConfigurationPostChanged(mActivity);
+
+ verify(mController, never()).onDisplayChanged(anyInt());
+
+ mController.onContextConfigurationPreChanged(mActivity);
+ mConfiguration.windowConfiguration.setMaxBounds(new Rect(0, 0, 100, 200));
+ mController.onContextConfigurationPostChanged(mActivity);
+
+ verify(mController).onDisplayChanged(123);
+ }
+
+ @Test
+ public void testOnContextConfigurationChanged_duringClientTransaction() {
+ doNothing().when(mController).onDisplayChanged(anyInt());
+ doReturn(123).when(mActivity).getDisplayId();
+
+ // Not trigger onDisplayChanged until ClientTransaction finished execution.
+ mController.onClientTransactionStarted();
+
+ mController.onContextConfigurationPreChanged(mActivity);
+ mConfiguration.windowConfiguration.setMaxBounds(new Rect(0, 0, 100, 200));
+ mController.onContextConfigurationPostChanged(mActivity);
+
+ verify(mController, never()).onDisplayChanged(anyInt());
+
+ mController.onClientTransactionFinished();
+
+ verify(mController).onDisplayChanged(123);
+ }
+
+ @Test
public void testActivityWindowInfoChangedListener() {
mSetFlagsRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
diff --git a/core/tests/mockingcoretests/Android.bp b/core/tests/mockingcoretests/Android.bp
index 2d778b1..aca52a8 100644
--- a/core/tests/mockingcoretests/Android.bp
+++ b/core/tests/mockingcoretests/Android.bp
@@ -40,6 +40,7 @@
"platform-test-annotations",
"truth",
"testables",
+ "flag-junit",
],
libs: [
diff --git a/core/tests/mockingcoretests/src/android/widget/OWNERS b/core/tests/mockingcoretests/src/android/widget/OWNERS
new file mode 100644
index 0000000..c0cbea9
--- /dev/null
+++ b/core/tests/mockingcoretests/src/android/widget/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/notification/OWNERS
\ No newline at end of file
diff --git a/core/tests/mockingcoretests/src/android/widget/ToastTest.java b/core/tests/mockingcoretests/src/android/widget/ToastTest.java
new file mode 100644
index 0000000..79bc81d
--- /dev/null
+++ b/core/tests/mockingcoretests/src/android/widget/ToastTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 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.widget;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyString;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.when;
+
+import android.app.INotificationManager;
+import android.content.Context;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.View;
+import android.widget.flags.Flags;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+
+/**
+ * ToastTest tests {@link Toast}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ToastTest {
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private Context mContext;
+ private MockitoSession mMockingSession;
+ private static INotificationManager.Stub sMockNMS;
+
+ @Before
+ public void setup() {
+ mContext = InstrumentationRegistry.getContext();
+ mMockingSession =
+ ExtendedMockito.mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(ServiceManager.class)
+ .startMocking();
+
+ //Toast caches the NotificationManager service as static class member
+ if (sMockNMS == null) {
+ sMockNMS = mock(INotificationManager.Stub.class);
+ }
+ doReturn(sMockNMS).when(sMockNMS).queryLocalInterface("android.app.INotificationManager");
+ doReturn(sMockNMS).when(() -> ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+ }
+
+ @After
+ public void tearDown() {
+ if (mMockingSession != null) {
+ mMockingSession.finishMocking();
+ }
+ reset(sMockNMS);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_TOAST_NO_WEAKREF)
+ public void enqueueFail_nullifiesNextView() throws RemoteException {
+ Looper.prepare();
+
+ // allow 1st toast and fail on the 2nd
+ when(sMockNMS.enqueueToast(anyString(), any(), any(), anyInt(), anyBoolean(),
+ anyInt())).thenReturn(true, false);
+
+ // first toast is enqueued
+ Toast t = Toast.makeText(mContext, "Toast1", Toast.LENGTH_SHORT);
+ t.setView(mock(View.class));
+ t.show();
+ Toast.TN tn = t.getTn();
+ assertThat(tn.getNextView()).isNotNull();
+
+ // second toast is not enqueued
+ t = Toast.makeText(mContext, "Toast2", Toast.LENGTH_SHORT);
+ t.setView(mock(View.class));
+ t.show();
+ tn = t.getTn();
+ assertThat(tn.getNextView()).isNull();
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_TOAST_NO_WEAKREF)
+ public void enqueueFail_doesNotNullifyNextView() throws RemoteException {
+ Looper.prepare();
+
+ // allow 1st toast and fail on the 2nd
+ when(sMockNMS.enqueueToast(anyString(), any(), any(), anyInt(), anyBoolean(),
+ anyInt())).thenReturn(true, false);
+
+ // first toast is enqueued
+ Toast t = Toast.makeText(mContext, "Toast1", Toast.LENGTH_SHORT);
+ t.setView(mock(View.class));
+ t.show();
+ Toast.TN tn = t.getTn();
+ assertThat(tn.getNextView()).isNotNull();
+
+ // second toast is not enqueued
+ t = Toast.makeText(mContext, "Toast2", Toast.LENGTH_SHORT);
+ t.setView(mock(View.class));
+ t.show();
+ tn = t.getTn();
+ assertThat(tn.getNextView()).isNotNull();
+ }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index 761bb79..91bd791 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -40,7 +40,7 @@
import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
import com.android.settingslib.spa.gallery.page.LoadingBarPageProvider
import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
-import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider
+import com.android.settingslib.spa.gallery.scaffold.NonScrollablePagerPageProvider
import com.android.settingslib.spa.gallery.page.SliderPageProvider
import com.android.settingslib.spa.gallery.preference.ListPreferencePageProvider
import com.android.settingslib.spa.gallery.preference.MainSwitchPreferencePageProvider
@@ -48,10 +48,12 @@
import com.android.settingslib.spa.gallery.preference.PreferencePageProvider
import com.android.settingslib.spa.gallery.preference.SwitchPreferencePageProvider
import com.android.settingslib.spa.gallery.preference.TwoTargetSwitchPreferencePageProvider
+import com.android.settingslib.spa.gallery.scaffold.PagerMainPageProvider
import com.android.settingslib.spa.gallery.scaffold.SearchScaffoldPageProvider
import com.android.settingslib.spa.gallery.scaffold.SuwScaffoldPageProvider
import com.android.settingslib.spa.gallery.ui.CategoryPageProvider
import com.android.settingslib.spa.gallery.ui.CopyablePageProvider
+import com.android.settingslib.spa.gallery.scaffold.ScrollablePagerPageProvider
import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
import com.android.settingslib.spa.slice.SpaSliceBroadcastReceiver
@@ -84,7 +86,9 @@
ArgumentPageProvider,
SliderPageProvider,
SpinnerPageProvider,
- SettingsPagerPageProvider,
+ PagerMainPageProvider,
+ NonScrollablePagerPageProvider,
+ ScrollablePagerPageProvider,
FooterPageProvider,
IllustrationPageProvider,
CategoryPageProvider,
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
index 1f028d5..654719d 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
@@ -39,9 +39,9 @@
import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
import com.android.settingslib.spa.gallery.page.LoadingBarPageProvider
import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
-import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider
import com.android.settingslib.spa.gallery.page.SliderPageProvider
import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider
+import com.android.settingslib.spa.gallery.scaffold.PagerMainPageProvider
import com.android.settingslib.spa.gallery.scaffold.SearchScaffoldPageProvider
import com.android.settingslib.spa.gallery.scaffold.SuwScaffoldPageProvider
import com.android.settingslib.spa.gallery.ui.CategoryPageProvider
@@ -63,7 +63,7 @@
SuwScaffoldPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
SliderPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
SpinnerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
- SettingsPagerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+ PagerMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
FooterPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/NonScrollablePagerPageProvider.kt
similarity index 70%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/NonScrollablePagerPageProvider.kt
index dc45e6d..029773f 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/NonScrollablePagerPageProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.settingslib.spa.gallery.page
+package com.android.settingslib.spa.gallery.scaffold
import android.os.Bundle
import androidx.compose.foundation.layout.Box
@@ -33,24 +33,19 @@
import com.android.settingslib.spa.widget.scaffold.SettingsScaffold
import com.android.settingslib.spa.widget.ui.PlaceholderTitle
-private const val TITLE = "Sample SettingsPager"
+object NonScrollablePagerPageProvider : SettingsPageProvider {
+ override val name = "NonScrollablePager"
+ private const val TITLE = "Sample Non Scrollable SettingsPager"
-object SettingsPagerPageProvider : SettingsPageProvider {
- override val name = "SettingsPager"
+ fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = createSettingsPage())
+ .setUiLayoutFn {
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ })
+ }
- fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = createSettingsPage())
- .setUiLayoutFn {
- Preference(object : PreferenceModel {
- override val title = TITLE
- override val onClick = navigator(name)
- })
- }
- }
-
- override fun getTitle(arguments: Bundle?): String {
- return TITLE
- }
+ override fun getTitle(arguments: Bundle?) = TITLE
@Composable
override fun Page(arguments: Bundle?) {
@@ -66,8 +61,8 @@
@Preview(showBackground = true)
@Composable
-private fun SettingsPagerPagePreview() {
+private fun NonScrollablePagerPageProviderPreview() {
SettingsTheme {
- SettingsPagerPageProvider.Page(null)
+ NonScrollablePagerPageProvider.Page(null)
}
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt
new file mode 100644
index 0000000..66cc38f
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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 com.android.settingslib.spa.gallery.scaffold
+
+import android.os.Bundle
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+
+object PagerMainPageProvider : SettingsPageProvider {
+ override val name = "PagerMain"
+ private val owner = createSettingsPage()
+ private const val TITLE = "Category: Pager"
+
+ override fun buildEntry(arguments: Bundle?) = listOf(
+ NonScrollablePagerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+ ScrollablePagerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+ )
+
+ fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner)
+ .setUiLayoutFn {
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ })
+ }
+
+ override fun getTitle(arguments: Bundle?) = TITLE
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/ScrollablePagerPageProvider.kt
similarity index 62%
copy from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
copy to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/ScrollablePagerPageProvider.kt
index dc45e6d..689a98a 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/ScrollablePagerPageProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-package com.android.settingslib.spa.gallery.page
+package com.android.settingslib.spa.gallery.scaffold
import android.os.Bundle
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@@ -31,33 +32,33 @@
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.SettingsPager
import com.android.settingslib.spa.widget.scaffold.SettingsScaffold
-import com.android.settingslib.spa.widget.ui.PlaceholderTitle
-private const val TITLE = "Sample SettingsPager"
+object ScrollablePagerPageProvider : SettingsPageProvider {
+ override val name = "ScrollablePager"
+ private const val TITLE = "Sample Scrollable SettingsPager"
-object SettingsPagerPageProvider : SettingsPageProvider {
- override val name = "SettingsPager"
+ fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = createSettingsPage())
+ .setUiLayoutFn {
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ })
+ }
- fun buildInjectEntry(): SettingsEntryBuilder {
- return SettingsEntryBuilder.createInject(owner = createSettingsPage())
- .setUiLayoutFn {
- Preference(object : PreferenceModel {
- override val title = TITLE
- override val onClick = navigator(name)
- })
- }
- }
-
- override fun getTitle(arguments: Bundle?): String {
- return TITLE
- }
+ override fun getTitle(arguments: Bundle?) = TITLE
@Composable
override fun Page(arguments: Bundle?) {
SettingsScaffold(title = getTitle(arguments)) { paddingValues ->
Box(Modifier.padding(paddingValues)) {
SettingsPager(listOf("Personal", "Work")) {
- PlaceholderTitle("Page $it")
+ LazyColumn {
+ items(30) {
+ Preference(object : PreferenceModel {
+ override val title = it.toString()
+ })
+ }
+ }
}
}
}
@@ -66,8 +67,8 @@
@Preview(showBackground = true)
@Composable
-private fun SettingsPagerPagePreview() {
+private fun ScrollablePagerPageProviderPreview() {
SettingsTheme {
- SettingsPagerPageProvider.Page(null)
+ ScrollablePagerPageProvider.Page(null)
}
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt
index 6fc8de3..a0ab2ce 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt
@@ -22,6 +22,10 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SignalCellularAlt
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPageProvider
@@ -37,6 +41,8 @@
import com.android.settingslib.spa.widget.scaffold.BottomAppBarButton
import com.android.settingslib.spa.widget.scaffold.SuwScaffold
import com.android.settingslib.spa.widget.ui.SettingsBody
+import com.android.settingslib.spa.widget.ui.Spinner
+import com.android.settingslib.spa.widget.ui.SpinnerOption
private const val TITLE = "Sample SuwScaffold"
@@ -67,13 +73,12 @@
actionButton = BottomAppBarButton("Next") {},
dismissButton = BottomAppBarButton("Cancel") {},
) {
- Column(Modifier.padding(SettingsDimension.itemPadding)) {
- SettingsBody("To add another SIM, download a new eSIM.")
- }
- Illustration(object : IllustrationModel {
- override val resId = R.drawable.accessibility_captioning_banner
- override val resourceType = ResourceType.IMAGE
- })
+ var selectedId by rememberSaveable { mutableIntStateOf(1) }
+ Spinner(
+ options = (1..3).map { SpinnerOption(id = it, text = "Option $it") },
+ selectedId = selectedId,
+ setId = { selectedId = it },
+ )
Column(Modifier.padding(SettingsDimension.itemPadding)) {
SettingsBody("To add another SIM, download a new eSIM.")
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt
index f372a45..163766a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt
@@ -19,7 +19,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
@@ -33,6 +32,8 @@
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.movableContentOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import com.android.settingslib.spa.framework.theme.SettingsDimension
@@ -50,7 +51,7 @@
title: String,
actionButton: BottomAppBarButton? = null,
dismissButton: BottomAppBarButton? = null,
- content: @Composable ColumnScope.() -> Unit,
+ content: @Composable () -> Unit,
) {
ActivityTitle(title)
Scaffold { innerPadding ->
@@ -59,6 +60,7 @@
.padding(innerPadding)
.padding(top = SettingsDimension.itemPaddingAround)
) {
+ val movableContent = remember(content) { movableContentOf { content() } }
// Use single column layout in portrait, two columns in landscape.
val useSingleColumn = maxWidth < maxHeight
if (useSingleColumn) {
@@ -69,7 +71,7 @@
.verticalScroll(rememberScrollState())
) {
Header(imageVector, title)
- content()
+ movableContent()
}
BottomBar(actionButton, dismissButton)
}
@@ -82,8 +84,9 @@
Column(
Modifier
.weight(1f)
- .verticalScroll(rememberScrollState())) {
- content()
+ .verticalScroll(rememberScrollState())
+ ) {
+ movableContent()
}
}
BottomBar(actionButton, dismissButton)
diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
index 2a27623..7f4bebc 100644
--- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
+++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
@@ -19,6 +19,7 @@
import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER;
import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE;
import static com.android.settingslib.drawer.TileUtils.META_DATA_NEW_TASK;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SEARCHABLE;
@@ -79,6 +80,9 @@
mComponentName = mComponentInfo.name;
mCategory = category;
mMetaData = metaData;
+ if (mMetaData != null) {
+ mGroupKey = metaData.getString(META_DATA_PREFERENCE_GROUP_KEY);
+ }
mIntent = new Intent().setClassName(mComponentPackage, mComponentName);
if (isNewTask()) {
mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index b1cfdcf..dbec059 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -204,15 +204,16 @@
}
// Handle back events.
- // TODO(b/290184746): Make sure that this works with SystemUI once we use
- // SceneTransitionLayout in Flexiglass.
- scene(state.transitionState.currentScene).userActions[Back]?.let { result ->
- // TODO(b/290184746): Handle predictive back and use result.distance if
- // specified.
- BackHandler {
- val targetScene = result.toScene
- if (state.canChangeScene(targetScene)) {
- with(state) { coroutineScope.onChangeScene(targetScene) }
+ val targetSceneForBackOrNull =
+ scene(state.transitionState.currentScene).userActions[Back]?.toScene
+ BackHandler(
+ enabled = targetSceneForBackOrNull != null,
+ ) {
+ targetSceneForBackOrNull?.let { targetSceneForBack ->
+ // TODO(b/290184746): Handle predictive back and use result.distance if
+ // specified.
+ if (state.canChangeScene(targetSceneForBack)) {
+ with(state) { coroutineScope.onChangeScene(targetSceneForBack) }
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 4950b96..85774c6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -537,4 +537,65 @@
assertThat(lp.height).isEqualTo(overlayParams.sensorBounds.height())
}
}
+
+ @Test
+ fun addViewPending_layoutIsNotUpdated() =
+ testScope.runTest {
+ withReasonSuspend(REASON_AUTH_KEYGUARD) {
+ mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
+ mSetFlagsRule.enableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
+
+ // GIVEN going to sleep
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OFF,
+ to = KeyguardState.GONE,
+ testScope = this,
+ )
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.STARTING_TO_SLEEP,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
+ runCurrent()
+
+ // WHEN a request comes to show the view
+ controllerOverlay.show(udfpsController, overlayParams)
+ runCurrent()
+
+ // THEN the view does not get added immediately
+ verify(windowManager, never()).addView(any(), any())
+
+ // WHEN updateOverlayParams gets called when the view is pending to be added
+ controllerOverlay.updateOverlayParams(overlayParams)
+
+ // THEN the view layout is never updated
+ verify(windowManager, never()).updateViewLayout(any(), any())
+
+ // CLEANUPL we hide to end the job that listens for the finishedGoingToSleep signal
+ controllerOverlay.hide()
+ }
+ }
+
+ @Test
+ fun updateOverlayParams_viewLayoutUpdated() =
+ testScope.runTest {
+ withReasonSuspend(REASON_AUTH_KEYGUARD) {
+ mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.AWAKE,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
+ runCurrent()
+ controllerOverlay.show(udfpsController, overlayParams)
+ runCurrent()
+ verify(windowManager).addView(any(), any())
+
+ // WHEN updateOverlayParams gets called
+ controllerOverlay.updateOverlayParams(overlayParams)
+
+ // THEN the view layout is updated
+ verify(windowManager, never()).updateViewLayout(any(), any())
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index 66f7e01..776f1a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -34,6 +34,8 @@
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.shadeRepository
@@ -43,6 +45,7 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import kotlin.math.pow
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.BeforeClass
@@ -57,22 +60,26 @@
class LockscreenSceneViewModelTest : SysuiTestCase() {
companion object {
+ private const val parameterCount = 6
+
@Parameters(
name =
"canSwipeToEnter={0}, downWithTwoPointers={1}, downFromEdge={2}," +
- " isSingleShade={3}, isCommunalAvailable={4}"
+ " isSingleShade={3}, isCommunalAvailable={4}, isShadeTouchable={5}"
)
@JvmStatic
fun combinations() = buildList {
- repeat(32) { combination ->
+ repeat(2f.pow(parameterCount).toInt()) { combination ->
add(
arrayOf(
- /* canSwipeToEnter= */ combination and 1 != 0,
- /* downWithTwoPointers= */ combination and 2 != 0,
- /* downFromEdge= */ combination and 4 != 0,
- /* isSingleShade= */ combination and 8 != 0,
- /* isCommunalAvailable= */ combination and 16 != 0,
- )
+ /* canSwipeToEnter= */ combination and 1 != 0,
+ /* downWithTwoPointers= */ combination and 2 != 0,
+ /* downFromEdge= */ combination and 4 != 0,
+ /* isSingleShade= */ combination and 8 != 0,
+ /* isCommunalAvailable= */ combination and 16 != 0,
+ /* isShadeTouchable= */ combination and 32 != 0,
+ )
+ .also { check(it.size == parameterCount) }
)
}
}
@@ -82,8 +89,15 @@
fun setUp() {
val combinationStrings =
combinations().map { array ->
- check(array.size == 5)
- "${array[4]},${array[3]},${array[2]},${array[1]},${array[0]}"
+ check(array.size == parameterCount)
+ buildString {
+ ((parameterCount - 1) downTo 0).forEach { index ->
+ append("${array[index]}")
+ if (index > 0) {
+ append(",")
+ }
+ }
+ }
}
val uniqueCombinations = combinationStrings.toSet()
assertThat(combinationStrings).hasSize(uniqueCombinations.size)
@@ -92,8 +106,35 @@
private fun expectedDownDestination(
downFromEdge: Boolean,
isSingleShade: Boolean,
- ): SceneKey {
- return if (downFromEdge && isSingleShade) Scenes.QuickSettings else Scenes.Shade
+ isShadeTouchable: Boolean,
+ ): SceneKey? {
+ return when {
+ !isShadeTouchable -> null
+ downFromEdge && isSingleShade -> Scenes.QuickSettings
+ else -> Scenes.Shade
+ }
+ }
+
+ private fun expectedUpDestination(
+ canSwipeToEnter: Boolean,
+ isShadeTouchable: Boolean,
+ ): SceneKey? {
+ return when {
+ !isShadeTouchable -> null
+ canSwipeToEnter -> Scenes.Gone
+ else -> Scenes.Bouncer
+ }
+ }
+
+ private fun expectedLeftDestination(
+ isCommunalAvailable: Boolean,
+ isShadeTouchable: Boolean,
+ ): SceneKey? {
+ return when {
+ !isShadeTouchable -> null
+ isCommunalAvailable -> Scenes.Communal
+ else -> null
+ }
}
}
@@ -106,6 +147,7 @@
@JvmField @Parameter(2) var downFromEdge: Boolean = false
@JvmField @Parameter(3) var isSingleShade: Boolean = true
@JvmField @Parameter(4) var isCommunalAvailable: Boolean = false
+ @JvmField @Parameter(5) var isShadeTouchable: Boolean = false
private val underTest by lazy { createLockscreenSceneViewModel() }
@@ -130,6 +172,14 @@
}
)
kosmos.setCommunalAvailable(isCommunalAvailable)
+ kosmos.fakePowerRepository.updateWakefulness(
+ rawState =
+ if (isShadeTouchable) {
+ WakefulnessState.AWAKE
+ } else {
+ WakefulnessState.ASLEEP
+ },
+ )
val destinationScenes by collectLastValue(underTest.destinationScenes)
@@ -148,14 +198,25 @@
expectedDownDestination(
downFromEdge = downFromEdge,
isSingleShade = isSingleShade,
+ isShadeTouchable = isShadeTouchable,
)
)
assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
- .isEqualTo(if (canSwipeToEnter) Scenes.Gone else Scenes.Bouncer)
+ .isEqualTo(
+ expectedUpDestination(
+ canSwipeToEnter = canSwipeToEnter,
+ isShadeTouchable = isShadeTouchable,
+ )
+ )
assertThat(destinationScenes?.get(Swipe(SwipeDirection.Left))?.toScene)
- .isEqualTo(Scenes.Communal.takeIf { isCommunalAvailable })
+ .isEqualTo(
+ expectedLeftDestination(
+ isCommunalAvailable = isCommunalAvailable,
+ isShadeTouchable = isShadeTouchable,
+ )
+ )
}
private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 9856f90..93302e3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -293,6 +293,7 @@
occlusionInteractor = kosmos.sceneContainerOcclusionInteractor,
faceUnlockInteractor = kosmos.deviceEntryFaceAuthInteractor,
deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor,
+ shadeInteractor = kosmos.shadeInteractor,
)
startable.start()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 3fd5306..75e66fb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -48,14 +48,17 @@
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.model.sysUiState
+import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shared.system.QuickStepContract
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
@@ -140,6 +143,7 @@
occlusionInteractor = kosmos.sceneContainerOcclusionInteractor,
faceUnlockInteractor = kosmos.deviceEntryFaceAuthInteractor,
deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor,
+ shadeInteractor = kosmos.shadeInteractor,
)
}
@@ -1127,6 +1131,33 @@
assertThat(kosmos.fakeDeviceEntryFaceAuthRepository.isAuthRunning.value).isTrue()
}
+ @Test
+ fun switchToLockscreen_whenShadeBecomesNotTouchable() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val isShadeTouchable by collectLastValue(kosmos.shadeInteractor.isShadeTouchable)
+ val transitionStateFlow = prepareState()
+ underTest.start()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ // Flung to bouncer, 90% of the way there:
+ transitionStateFlow.value =
+ ObservableTransitionState.Transition(
+ fromScene = Scenes.Lockscreen,
+ toScene = Scenes.Bouncer,
+ progress = flowOf(0.9f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(false),
+ )
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+ kosmos.fakePowerRepository.updateWakefulness(WakefulnessState.ASLEEP)
+ runCurrent()
+ assertThat(isShadeTouchable).isFalse()
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ }
+
private fun TestScope.emulateSceneTransition(
transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
toScene: SceneKey,
@@ -1166,6 +1197,7 @@
isLockscreenEnabled: Boolean = true,
startsAwake: Boolean = true,
isDeviceProvisioned: Boolean = true,
+ isInteractive: Boolean = true,
): MutableStateFlow<ObservableTransitionState> {
if (authenticationMethod?.isSecure == true) {
assert(isLockscreenEnabled) {
@@ -1205,6 +1237,7 @@
} else {
powerInteractor.setAsleepForTest()
}
+ kosmos.fakePowerRepository.setInteractive(isInteractive)
kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(isDeviceProvisioned)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 3a45db1..61d1c71 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -323,7 +323,13 @@
overlayParams = updatedOverlayParams
sensorBounds = updatedOverlayParams.sensorBounds
getTouchOverlay()?.let {
- windowManager.updateViewLayout(it, coreLayoutParams.updateDimensions(null))
+ if (addViewRunnable != null) {
+ // Only updateViewLayout if there's no pending view to add to WM.
+ // If there is a pending view, that means the view hasn't been added yet so there's
+ // no need to update any layouts. Instead the correct params will be used when the
+ // view is eventually added.
+ windowManager.updateViewLayout(it, coreLayoutParams.updateDimensions(null))
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
index 993e81b..d4c8456e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
@@ -37,6 +37,8 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
/** Models UI state and handles user input for the lockscreen scene. */
@@ -52,16 +54,23 @@
val notifications: NotificationsPlaceholderViewModel,
) {
val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
- combine(
- deviceEntryInteractor.isUnlocked,
- communalInteractor.isCommunalAvailable,
- shadeInteractor.shadeMode,
- ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
- destinationScenes(
- isDeviceUnlocked = isDeviceUnlocked,
- isCommunalAvailable = isCommunalAvailable,
- shadeMode = shadeMode,
- )
+ shadeInteractor.isShadeTouchable
+ .flatMapLatest { isShadeTouchable ->
+ if (!isShadeTouchable) {
+ flowOf(emptyMap())
+ } else {
+ combine(
+ deviceEntryInteractor.isUnlocked,
+ communalInteractor.isCommunalAvailable,
+ shadeInteractor.shadeMode,
+ ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
+ destinationScenes(
+ isDeviceUnlocked = isDeviceUnlocked,
+ isCommunalAvailable = isCommunalAvailable,
+ shadeMode = shadeMode,
+ )
+ }
+ }
}
.stateIn(
scope = applicationScope,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index 4e0b576..ab0b0b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -52,8 +52,6 @@
val destinationScenes =
qsSceneAdapter.isCustomizing.flatMapLatest { customizing ->
if (customizing) {
- // TODO(b/332749288) Empty map so there are no back handlers and back can close
- // customizer
flowOf(emptyMap())
// TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade
// while customizing
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 32d72e0..1f935f97 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -47,6 +47,7 @@
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.logger.SceneLogger
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -103,6 +104,7 @@
private val headsUpInteractor: HeadsUpNotificationInteractor,
private val occlusionInteractor: SceneContainerOcclusionInteractor,
private val faceUnlockInteractor: DeviceEntryFaceAuthInteractor,
+ private val shadeInteractor: ShadeInteractor,
) : CoreStartable {
override fun start() {
@@ -185,6 +187,14 @@
/** Switches between scenes based on ever-changing application state. */
private fun automaticallySwitchScenes() {
+ handleBouncerImeVisibility()
+ handleSimUnlock()
+ handleDeviceUnlockStatus()
+ handlePowerState()
+ handleShadeTouchability()
+ }
+
+ private fun handleBouncerImeVisibility() {
applicationScope.launch {
// TODO (b/308001302): Move this to a bouncer specific interactor.
bouncerInteractor.onImeHiddenByUser.collectLatest {
@@ -196,6 +206,9 @@
}
}
}
+ }
+
+ private fun handleSimUnlock() {
applicationScope.launch {
simBouncerInteractor
.get()
@@ -229,6 +242,9 @@
}
}
}
+ }
+
+ private fun handleDeviceUnlockStatus() {
applicationScope.launch {
deviceUnlockedInteractor.deviceUnlockStatus
.mapNotNull { deviceUnlockStatus ->
@@ -288,7 +304,9 @@
)
}
}
+ }
+ private fun handlePowerState() {
applicationScope.launch {
powerInteractor.isAsleep.collect { isAsleep ->
if (isAsleep) {
@@ -317,7 +335,7 @@
) {
switchToScene(
targetSceneKey = Scenes.Bouncer,
- loggingReason = "device is starting to wake up with a locked sim"
+ loggingReason = "device is starting to wake up with a locked sim",
)
}
}
@@ -325,6 +343,20 @@
}
}
+ private fun handleShadeTouchability() {
+ applicationScope.launch {
+ shadeInteractor.isShadeTouchable
+ .distinctUntilChanged()
+ .filter { !it }
+ .collect {
+ switchToScene(
+ targetSceneKey = Scenes.Lockscreen,
+ loggingReason = "device became non-interactive",
+ )
+ }
+ }
+ }
+
/** Keeps [SysUiState] up-to-date */
private fun hydrateSystemUiState() {
applicationScope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index f6b1bcc..f418e7e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -23,7 +23,13 @@
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedDispatcherOwner
+import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.compose.ui.platform.ComposeView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import com.android.compose.theme.PlatformTheme
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.communal.dagger.Communal
@@ -33,6 +39,7 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -40,6 +47,7 @@
import com.android.systemui.util.kotlin.collectFlow
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
/**
* Controller that's responsible for the glanceable hub container view and its touch handling.
@@ -139,13 +147,33 @@
): View {
return initView(
ComposeView(context).apply {
- setContent {
- PlatformTheme {
- CommunalContainer(
- viewModel = communalViewModel,
- dataSourceDelegator = dataSourceDelegator,
- dialogFactory = dialogFactory,
- )
+ repeatWhenAttached {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ setViewTreeOnBackPressedDispatcherOwner(
+ object : OnBackPressedDispatcherOwner {
+ override val onBackPressedDispatcher =
+ OnBackPressedDispatcher().apply {
+ setOnBackInvokedDispatcher(
+ viewRootImpl.onBackInvokedDispatcher
+ )
+ }
+
+ override val lifecycle: Lifecycle =
+ this@repeatWhenAttached.lifecycle
+ }
+ )
+
+ setContent {
+ PlatformTheme {
+ CommunalContainer(
+ viewModel = communalViewModel,
+ dataSourceDelegator = dataSourceDelegator,
+ dialogFactory = dialogFactory,
+ )
+ }
+ }
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index ebebbe6..8c15817 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -124,7 +124,6 @@
// release focus immediately to kick off focus change transition
notificationShadeWindowController.setNotificationShadeFocusable(false)
notificationStackScrollLayout.cancelExpandHelper()
- sceneInteractor.changeScene(Scenes.Shade, "ShadeController.animateExpandShade")
if (delayed) {
scope.launch {
delay(125)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 9ce38db..8b673c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -49,7 +49,6 @@
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
-import android.util.ArraySet;
import android.view.ContentInfo;
import androidx.annotation.NonNull;
@@ -150,10 +149,8 @@
private int mCachedContrastColor = COLOR_INVALID;
private int mCachedContrastColorIsFor = COLOR_INVALID;
private InflationTask mRunningTask = null;
- private Throwable mDebugThrowable;
public CharSequence remoteInputTextWhenReset;
public long lastRemoteInputSent = NOT_LAUNCHED_YET;
- public final ArraySet<Integer> mActiveAppOps = new ArraySet<>(3);
private final MutableStateFlow<CharSequence> mHeadsUpStatusBarText =
StateFlowKt.MutableStateFlow(null);
@@ -190,11 +187,6 @@
private boolean mBlockable;
/**
- * The {@link SystemClock#elapsedRealtime()} when this notification entry was created.
- */
- public long mCreationElapsedRealTime;
-
- /**
* Whether this notification has ever been a non-sticky HUN.
*/
private boolean mIsDemoted = false;
@@ -264,13 +256,8 @@
mKey = sbn.getKey();
setSbn(sbn);
setRanking(ranking);
- mCreationElapsedRealTime = SystemClock.elapsedRealtime();
}
- @VisibleForTesting
- public void setCreationElapsedRealTime(long time) {
- mCreationElapsedRealTime = time;
- }
@Override
public NotificationEntry getRepresentativeEntry() {
return this;
@@ -581,19 +568,6 @@
return mRunningTask;
}
- /**
- * Set a throwable that is used for debugging
- *
- * @param debugThrowable the throwable to save
- */
- public void setDebugThrowable(Throwable debugThrowable) {
- mDebugThrowable = debugThrowable;
- }
-
- public Throwable getDebugThrowable() {
- return mDebugThrowable;
- }
-
public void onRemoteInputInserted() {
lastRemoteInputSent = NOT_LAUNCHED_YET;
remoteInputTextWhenReset = null;
@@ -749,12 +723,6 @@
return row != null && row.areChildrenExpanded();
}
-
- //TODO: probably less confusing to say "is group fully visible"
- public boolean isGroupNotFullyVisible() {
- return row == null || row.isGroupNotFullyVisible();
- }
-
public NotificationGuts getGuts() {
if (row != null) return row.getGuts();
return null;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index a100fe0..3d01c5f 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -410,7 +410,7 @@
Slog.v(TAG,
"Removing window token: " + mCurToken + " for display: " + curTokenDisplayId);
}
- mWindowManagerInternal.removeWindowToken(mCurToken, false /* removeWindows */,
+ mWindowManagerInternal.removeWindowToken(mCurToken, true /* removeWindows */,
false /* animateExit */, curTokenDisplayId);
mCurToken = null;
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 956e10c..ebc1a2a 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -3399,21 +3399,21 @@
// ============================================================================
@Override
- public void enqueueTextToast(String pkg, IBinder token, CharSequence text, int duration,
+ public boolean enqueueTextToast(String pkg, IBinder token, CharSequence text, int duration,
boolean isUiContext, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
- enqueueToast(pkg, token, text, /* callback= */ null, duration, isUiContext, displayId,
- textCallback);
+ return enqueueToast(pkg, token, text, /* callback= */ null, duration, isUiContext,
+ displayId, textCallback);
}
@Override
- public void enqueueToast(String pkg, IBinder token, ITransientNotification callback,
+ public boolean enqueueToast(String pkg, IBinder token, ITransientNotification callback,
int duration, boolean isUiContext, int displayId) {
- enqueueToast(pkg, token, /* text= */ null, callback, duration, isUiContext, displayId,
- /* textCallback= */ null);
+ return enqueueToast(pkg, token, /* text= */ null, callback, duration, isUiContext,
+ displayId, /* textCallback= */ null);
}
- private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
+ private boolean enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
@Nullable ITransientNotification callback, int duration, boolean isUiContext,
int displayId, @Nullable ITransientNotificationCallback textCallback) {
if (DBG) {
@@ -3425,7 +3425,7 @@
|| (text != null && callback != null) || token == null) {
Slog.e(TAG, "Not enqueuing toast. pkg=" + pkg + " text=" + text + " callback="
+ " token=" + token);
- return;
+ return false;
}
final int callingUid = Binder.getCallingUid();
@@ -3451,7 +3451,7 @@
boolean isAppRenderedToast = (callback != null);
if (!checkCanEnqueueToast(pkg, callingUid, displayId, isAppRenderedToast,
isSystemToast)) {
- return;
+ return false;
}
synchronized (mToastQueue) {
@@ -3477,7 +3477,7 @@
if (count >= MAX_PACKAGE_TOASTS) {
Slog.e(TAG, "Package has already queued " + count
+ " toasts. Not showing more. Package=" + pkg);
- return;
+ return false;
}
}
}
@@ -3513,6 +3513,7 @@
Binder.restoreCallingIdentity(callingId);
}
}
+ return true;
}
@GuardedBy("mToastQueue")
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 2a93255..c8bcc51 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -54,8 +54,10 @@
import android.os.UserManager;
import android.os.ext.SdkExtensions;
import android.provider.DeviceConfig;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.LongArrayQueue;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
@@ -173,6 +175,8 @@
// Accessed on the handler thread only.
private long mRelativeBootTime = calculateRelativeBootTime();
+ private final ArrayMap<Integer, Pair<Context, BroadcastReceiver>> mUserBroadcastReceivers;
+
RollbackManagerServiceImpl(Context context) {
mContext = context;
// Note that we're calling onStart here because this object is only constructed on
@@ -210,6 +214,8 @@
}
});
+ mUserBroadcastReceivers = new ArrayMap<>();
+
UserManager userManager = mContext.getSystemService(UserManager.class);
for (UserHandle user : userManager.getUserHandles(true)) {
registerUserCallbacks(user);
@@ -275,7 +281,9 @@
}
}, enableRollbackTimedOutFilter, null, getHandler());
- IntentFilter userAddedIntentFilter = new IntentFilter(Intent.ACTION_USER_ADDED);
+ IntentFilter userIntentFilter = new IntentFilter();
+ userIntentFilter.addAction(Intent.ACTION_USER_ADDED);
+ userIntentFilter.addAction(Intent.ACTION_USER_REMOVED);
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -287,9 +295,15 @@
return;
}
registerUserCallbacks(UserHandle.of(newUserId));
+ } else if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
+ final int newUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (newUserId == -1) {
+ return;
+ }
+ unregisterUserCallbacks(UserHandle.of(newUserId));
}
}
- }, userAddedIntentFilter, null, getHandler());
+ }, userIntentFilter, null, getHandler());
registerTimeChangeReceiver();
}
@@ -335,7 +349,7 @@
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
filter.addDataScheme("package");
- context.registerReceiver(new BroadcastReceiver() {
+ BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
assertInWorkerThread();
@@ -354,7 +368,21 @@
onPackageFullyRemoved(packageName);
}
}
- }, filter, null, getHandler());
+ };
+ context.registerReceiver(receiver, filter, null, getHandler());
+ mUserBroadcastReceivers.put(user.getIdentifier(), new Pair(context, receiver));
+ }
+
+ @AnyThread
+ private void unregisterUserCallbacks(UserHandle user) {
+ Pair<Context, BroadcastReceiver> pair = mUserBroadcastReceivers.get(user.getIdentifier());
+ if (pair == null || pair.first == null || pair.second == null) {
+ Slog.e(TAG, "No receiver found for the user" + user);
+ return;
+ }
+
+ pair.first.unregisterReceiver(pair.second);
+ mUserBroadcastReceivers.remove(user.getIdentifier());
}
@ExtThread
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
index 96f045d..8138168 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
@@ -44,6 +44,8 @@
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_PHYSICAL_EMULATION);
private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
+ private static final VibrationAttributes COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES =
+ VibrationAttributes.createForUsage(VibrationAttributes.USAGE_COMMUNICATION_REQUEST);
private final VibratorInfo mVibratorInfo;
private final boolean mHapticTextHandleEnabled;
@@ -120,7 +122,6 @@
return getKeyboardVibration(effectId);
case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE:
- case HapticFeedbackConstants.ENTRY_BUMP:
case HapticFeedbackConstants.DRAG_CROSSING:
return getVibration(
effectId,
@@ -131,6 +132,7 @@
case HapticFeedbackConstants.EDGE_RELEASE:
case HapticFeedbackConstants.CALENDAR_DATE:
case HapticFeedbackConstants.CONFIRM:
+ case HapticFeedbackConstants.BIOMETRIC_CONFIRM:
case HapticFeedbackConstants.GESTURE_START:
case HapticFeedbackConstants.SCROLL_ITEM_FOCUS:
case HapticFeedbackConstants.SCROLL_LIMIT:
@@ -143,6 +145,7 @@
return getVibration(effectId, VibrationEffect.EFFECT_HEAVY_CLICK);
case HapticFeedbackConstants.REJECT:
+ case HapticFeedbackConstants.BIOMETRIC_REJECT:
return getVibration(effectId, VibrationEffect.EFFECT_DOUBLE_CLICK);
case HapticFeedbackConstants.SAFE_MODE_ENABLED:
@@ -207,6 +210,10 @@
case HapticFeedbackConstants.KEYBOARD_RELEASE:
attrs = createKeyboardVibrationAttributes(fromIme);
break;
+ case HapticFeedbackConstants.BIOMETRIC_CONFIRM:
+ case HapticFeedbackConstants.BIOMETRIC_REJECT:
+ attrs = COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES;
+ break;
default:
attrs = TOUCH_VIBRATION_ATTRIBUTES;
}
@@ -225,6 +232,23 @@
return flags == 0 ? attrs : new VibrationAttributes.Builder(attrs).setFlags(flags).build();
}
+ /**
+ * Returns true if given haptic feedback is restricted to system apps with permission
+ * {@code android.permission.VIBRATE_SYSTEM_CONSTANTS}.
+ *
+ * @param effectId the haptic feedback effect ID to check.
+ * @return true if the haptic feedback is restricted, false otherwise.
+ */
+ public boolean isRestrictedHapticFeedback(int effectId) {
+ switch (effectId) {
+ case HapticFeedbackConstants.BIOMETRIC_CONFIRM:
+ case HapticFeedbackConstants.BIOMETRIC_REJECT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
/** Dumps relevant state. */
public void dump(String prefix, PrintWriter pw) {
pw.print("mHapticTextHandleEnabled="); pw.println(mHapticTextHandleEnabled);
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 9e9025e..8281ac1 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -439,6 +439,11 @@
Slog.w(TAG, "performHapticFeedback; haptic vibration provider not ready.");
return null;
}
+ if (hapticVibrationProvider.isRestrictedHapticFeedback(constant)
+ && !hasPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS)) {
+ Slog.w(TAG, "performHapticFeedback; no permission for effect " + constant);
+ return null;
+ }
VibrationEffect effect = hapticVibrationProvider.getVibrationForHapticFeedback(constant);
if (effect == null) {
Slog.w(TAG, "performHapticFeedback; vibration absent for effect " + constant);
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
index c6b401b..36e5200 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
@@ -525,6 +525,10 @@
futureFromWearableSensingService.complete(null);
return;
}
+ if (pfdFromApp == null) {
+ futureFromWearableSensingService.complete(null);
+ return;
+ }
if (isReadOnly(pfdFromApp)) {
futureFromWearableSensingService.complete(
pfdFromApp);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 207d42b6..ad1f648 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8544,7 +8544,9 @@
}
// If activity in fullscreen mode is letterboxed because of fixed orientation then bounds
// are already calculated in resolveFixedOrientationConfiguration.
- } else if (!isLetterboxedForFixedOrientationAndAspectRatio()) {
+ // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer.
+ } else if (!isLetterboxedForFixedOrientationAndAspectRatio()
+ && !mLetterboxUiController.hasFullscreenOverride()) {
resolveAspectRatioRestriction(newParentConfiguration);
}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 3f24545..f220c9d 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -1084,6 +1084,10 @@
|| mUserAspectRatio == USER_MIN_ASPECT_RATIO_FULLSCREEN);
}
+ boolean hasFullscreenOverride() {
+ return isSystemOverrideToFullscreenEnabled() || shouldApplyUserFullscreenOverride();
+ }
+
float getUserMinAspectRatio() {
switch (mUserAspectRatio) {
case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 55a6248..b0eee08 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -2633,11 +2633,19 @@
// Create default display device
createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
callback.waitForExpectedEvent();
+
+ callback.expectsEvent(EVENT_DISPLAY_ADDED);
FakeDisplayDevice displayDevice =
createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+ callback.waitForExpectedEvent();
+
+ callback.expectsEvent(EVENT_DISPLAY_REMOVED);
+ displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+ callback.waitForExpectedEvent();
+
+ callback.expectsEvent(EVENT_DISPLAY_ADDED);
LogicalDisplay display =
logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
- callback.expectsEvent(EVENT_DISPLAY_ADDED);
logicalDisplayMapper.setEnabledLocked(display, /* isEnabled= */ true);
logicalDisplayMapper.updateLogicalDisplays();
callback.waitForExpectedEvent();
@@ -2660,6 +2668,7 @@
LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS);
+ displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
callback.expectsEvent(EVENT_DISPLAY_ADDED);
// Create default display device
createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
@@ -2673,7 +2682,6 @@
logicalDisplayMapper.setEnabledLocked(display, /* isEnabled= */ true);
logicalDisplayMapper.updateLogicalDisplays();
callback.waitForExpectedEvent();
- callback.clear();
assertThrows(SecurityException.class, () -> bs.disableConnectedDisplay(displayId));
}
@@ -3376,7 +3384,7 @@
void waitForExpectedEvent(Duration timeout) {
try {
- assertWithMessage("Event '" + mExpectedEvent + "' is received.")
+ assertWithMessage("Expected '" + mExpectedEvent + "'")
.that(mLatch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)).isTrue();
} catch (InterruptedException ex) {
throw new AssertionError("Waiting for expected event interrupted", ex);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 6106278..f08fbde 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -7916,8 +7916,9 @@
setAppInForegroundForToasts(mUid, true);
// enqueue toast -> toast should still enqueue
- enqueueToast(testPackage, new TestableToastCallback());
+ boolean wasEnqueued = enqueueToast(testPackage, new TestableToastCallback());
assertEquals(1, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isTrue();
}
@Test
@@ -7936,8 +7937,9 @@
setAppInForegroundForToasts(mUid, false);
// enqueue toast -> no toasts enqueued
- enqueueToast(testPackage, new TestableToastCallback());
+ boolean wasEnqueued = enqueueToast(testPackage, new TestableToastCallback());
assertEquals(0, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isFalse();
}
@Test
@@ -8045,8 +8047,9 @@
setAppInForegroundForToasts(mUid, true);
// enqueue toast -> toast should still enqueue
- enqueueTextToast(testPackage, "Text");
+ boolean wasEnqueued = enqueueTextToast(testPackage, "Text");
assertEquals(1, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isTrue();
}
@Test
@@ -8065,8 +8068,9 @@
setAppInForegroundForToasts(mUid, false);
// enqueue toast -> toast should still enqueue
- enqueueTextToast(testPackage, "Text");
+ boolean wasEnqueued = enqueueTextToast(testPackage, "Text");
assertEquals(1, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isTrue();
}
@Test
@@ -8220,8 +8224,9 @@
setAppInForegroundForToasts(mUid, false);
// enqueue toast -> toast should still enqueue
- enqueueToast(testPackage, new TestableToastCallback());
+ boolean wasEnqueued = enqueueToast(testPackage, new TestableToastCallback());
assertEquals(1, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isTrue();
verify(mAm).setProcessImportant(any(), anyInt(), eq(true), any());
}
@@ -8242,8 +8247,9 @@
setAppInForegroundForToasts(mUid, true);
// enqueue toast -> toast should still enqueue
- enqueueTextToast(testPackage, "Text");
+ boolean wasEnqueued = enqueueTextToast(testPackage, "Text");
assertEquals(1, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isTrue();
verify(mAm).setProcessImportant(any(), anyInt(), eq(false), any());
}
@@ -8264,8 +8270,9 @@
setAppInForegroundForToasts(mUid, false);
// enqueue toast -> toast should still enqueue
- enqueueTextToast(testPackage, "Text");
+ boolean wasEnqueued = enqueueTextToast(testPackage, "Text");
assertEquals(1, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isTrue();
verify(mAm).setProcessImportant(any(), anyInt(), eq(false), any());
}
@@ -8274,7 +8281,8 @@
allowTestPackageToToast();
// enqueue toast -> no toasts enqueued
- enqueueTextToast(TEST_PACKAGE, "Text");
+ boolean wasEnqueued = enqueueTextToast(TEST_PACKAGE, "Text");
+ assertThat(wasEnqueued).isTrue();
verifyToastShownForTestPackage("Text", DEFAULT_DISPLAY);
}
@@ -8367,10 +8375,11 @@
.thenReturn(false);
// enqueue toast -> no toasts enqueued
- enqueueTextToast(testPackage, "Text");
+ boolean wasEnqueued = enqueueTextToast(testPackage, "Text");
verify(mStatusBar, never()).showToast(anyInt(), any(), any(), any(), any(), anyInt(), any(),
anyInt());
assertEquals(0, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isFalse();
}
@Test
@@ -8390,10 +8399,11 @@
when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
// enqueue toast -> no toasts enqueued
- enqueueToast(testPackage, new TestableToastCallback());
+ boolean wasEnqueued = enqueueToast(testPackage, new TestableToastCallback());
verify(mStatusBar, never()).showToast(anyInt(), any(), any(), any(), any(), anyInt(), any(),
anyInt());
assertEquals(0, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isFalse();
}
@Test
@@ -8415,8 +8425,9 @@
setAppInForegroundForToasts(mUid, false);
// enqueue toast -> no toasts enqueued
- enqueueToast(testPackage, new TestableToastCallback());
+ boolean wasEnqueued = enqueueToast(testPackage, new TestableToastCallback());
assertEquals(0, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isFalse();
}
@Test
@@ -8437,8 +8448,9 @@
setAppInForegroundForToasts(mUid, false);
// enqueue toast -> system toast can still be enqueued
- enqueueToast(testPackage, new TestableToastCallback());
+ boolean wasEnqueued = enqueueToast(testPackage, new TestableToastCallback());
assertEquals(1, mService.mToastQueue.size());
+ assertThat(wasEnqueued).isTrue();
}
@Test
@@ -8458,7 +8470,12 @@
// Trying to quickly enqueue more toast than allowed.
for (int i = 0; i < NotificationManagerService.MAX_PACKAGE_TOASTS + 1; i++) {
- enqueueTextToast(testPackage, "Text");
+ boolean wasEnqueued = enqueueTextToast(testPackage, "Text");
+ if (i < NotificationManagerService.MAX_PACKAGE_TOASTS) {
+ assertThat(wasEnqueued).isTrue();
+ } else {
+ assertThat(wasEnqueued).isFalse();
+ }
}
// Only allowed number enqueued, rest ignored.
assertEquals(NotificationManagerService.MAX_PACKAGE_TOASTS, mService.mToastQueue.size());
@@ -15089,25 +15106,27 @@
.thenReturn(false);
}
- private void enqueueToast(String testPackage, ITransientNotification callback)
+ private boolean enqueueToast(String testPackage, ITransientNotification callback)
throws RemoteException {
- enqueueToast((INotificationManager) mService.mService, testPackage, new Binder(), callback);
+ return enqueueToast((INotificationManager) mService.mService, testPackage, new Binder(),
+ callback);
}
- private void enqueueToast(INotificationManager service, String testPackage,
+ private boolean enqueueToast(INotificationManager service, String testPackage,
IBinder token, ITransientNotification callback) throws RemoteException {
- service.enqueueToast(testPackage, token, callback, TOAST_DURATION, /* isUiContext= */ true,
- DEFAULT_DISPLAY);
+ return service.enqueueToast(testPackage, token, callback, TOAST_DURATION, /* isUiContext= */
+ true, DEFAULT_DISPLAY);
}
- private void enqueueTextToast(String testPackage, CharSequence text) throws RemoteException {
- enqueueTextToast(testPackage, text, /* isUiContext= */ true, DEFAULT_DISPLAY);
+ private boolean enqueueTextToast(String testPackage, CharSequence text) throws RemoteException {
+ return enqueueTextToast(testPackage, text, /* isUiContext= */ true, DEFAULT_DISPLAY);
}
- private void enqueueTextToast(String testPackage, CharSequence text, boolean isUiContext,
+ private boolean enqueueTextToast(String testPackage, CharSequence text, boolean isUiContext,
int displayId) throws RemoteException {
- ((INotificationManager) mService.mService).enqueueTextToast(testPackage, new Binder(), text,
- TOAST_DURATION, isUiContext, displayId, /* textCallback= */ null);
+ return ((INotificationManager) mService.mService).enqueueTextToast(testPackage,
+ new Binder(), text, TOAST_DURATION, isUiContext, displayId,
+ /* textCallback= */ null);
}
private void mockIsVisibleBackgroundUsersSupported(boolean supported) {
diff --git a/services/tests/vibrator/AndroidManifest.xml b/services/tests/vibrator/AndroidManifest.xml
index a14ea55..c0f514f 100644
--- a/services/tests/vibrator/AndroidManifest.xml
+++ b/services/tests/vibrator/AndroidManifest.xml
@@ -30,6 +30,8 @@
<uses-permission android:name="android.permission.ACCESS_VIBRATOR_STATE" />
<!-- Required to set always-on vibrations -->
<uses-permission android:name="android.permission.VIBRATE_ALWAYS_ON" />
+ <!-- Required to play system-only haptic feedback constants -->
+ <uses-permission android:name="android.permission.VIBRATE_SYSTEM_CONSTANTS" />
<application android:debuggable="true">
<uses-library android:name="android.test.mock" android:required="true" />
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
index e3d4596..633a3c9 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
@@ -27,6 +27,8 @@
import static android.os.VibrationEffect.EFFECT_CLICK;
import static android.os.VibrationEffect.EFFECT_TEXTURE_TICK;
import static android.os.VibrationEffect.EFFECT_TICK;
+import static android.view.HapticFeedbackConstants.BIOMETRIC_CONFIRM;
+import static android.view.HapticFeedbackConstants.BIOMETRIC_REJECT;
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
import static android.view.HapticFeedbackConstants.CONTEXT_CLICK;
import static android.view.HapticFeedbackConstants.KEYBOARD_RELEASE;
@@ -80,6 +82,8 @@
new int[] {SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK};
private static final int[] KEYBOARD_FEEDBACK_CONSTANTS =
new int[] {KEYBOARD_TAP, KEYBOARD_RELEASE};
+ private static final int[] BIOMETRIC_FEEDBACK_CONSTANTS =
+ new int[] {BIOMETRIC_CONFIRM, BIOMETRIC_REJECT};
private static final float KEYBOARD_VIBRATION_FIXED_AMPLITUDE = 0.62f;
@@ -283,6 +287,17 @@
}
@Test
+ public void testVibrationAttribute_biometricConstants_returnsCommunicationRequestUsage() {
+ HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
+
+ for (int effectId : BIOMETRIC_FEEDBACK_CONSTANTS) {
+ VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
+ effectId, /* bypassVibrationIntensitySetting= */ false, /* fromIme= */ false);
+ assertThat(attrs.getUsage()).isEqualTo(VibrationAttributes.USAGE_COMMUNICATION_REQUEST);
+ }
+ }
+
+ @Test
public void testVibrationAttribute_forNotBypassingIntensitySettings() {
HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
@@ -422,6 +437,15 @@
}
}
+ @Test
+ public void testIsRestricted_biometricConstants_returnsTrue() {
+ HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
+
+ for (int effectId : BIOMETRIC_FEEDBACK_CONSTANTS) {
+ assertThat(hapticProvider.isRestrictedHapticFeedback(effectId)).isTrue();
+ }
+ }
+
private HapticFeedbackVibrationProvider createProviderWithDefaultCustomizations() {
return createProvider(/* customizations= */ null);
}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 185677f..d6c0fef 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -1410,6 +1410,70 @@
}
@Test
+ public void performHapticFeedback_restrictedConstantsWithoutPermission_doesNotVibrate()
+ throws Exception {
+ // Deny permission to vibrate with restricted constants
+ denyPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS);
+ // Public constant, no permission required
+ mHapticFeedbackVibrationMap.put(
+ HapticFeedbackConstants.CONFIRM,
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
+ // Hidden system-only constant, permission required
+ mHapticFeedbackVibrationMap.put(
+ HapticFeedbackConstants.BIOMETRIC_CONFIRM,
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK));
+ mockVibrators(1);
+ FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+ fakeVibrator.setSupportedEffects(
+ VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_HEAVY_CLICK);
+ VibratorManagerService service = createSystemReadyService();
+
+ performHapticFeedbackAndWaitUntilFinished(
+ service, HapticFeedbackConstants.CONFIRM, /* always= */ false);
+
+ performHapticFeedbackAndWaitUntilFinished(
+ service, HapticFeedbackConstants.BIOMETRIC_CONFIRM, /* always= */ false);
+
+ List<VibrationEffectSegment> playedSegments = fakeVibrator.getAllEffectSegments();
+ assertEquals(1, playedSegments.size());
+ PrebakedSegment segment = (PrebakedSegment) playedSegments.get(0);
+ assertEquals(VibrationEffect.EFFECT_CLICK, segment.getEffectId());
+ }
+
+ @Test
+ public void performHapticFeedback_restrictedConstantsWithPermission_playsVibration()
+ throws Exception {
+ // Grant permission to vibrate with restricted constants
+ grantPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS);
+ // Public constant, no permission required
+ mHapticFeedbackVibrationMap.put(
+ HapticFeedbackConstants.CONFIRM,
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
+ // Hidden system-only constant, permission required
+ mHapticFeedbackVibrationMap.put(
+ HapticFeedbackConstants.BIOMETRIC_CONFIRM,
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK));
+ mockVibrators(1);
+ FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+ fakeVibrator.setSupportedEffects(
+ VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_HEAVY_CLICK);
+ VibratorManagerService service = createSystemReadyService();
+
+ performHapticFeedbackAndWaitUntilFinished(
+ service, HapticFeedbackConstants.CONFIRM, /* always= */ false);
+
+ performHapticFeedbackAndWaitUntilFinished(
+ service, HapticFeedbackConstants.BIOMETRIC_CONFIRM, /* always= */ false);
+
+ List<VibrationEffectSegment> playedSegments = fakeVibrator.getAllEffectSegments();
+ assertEquals(2, playedSegments.size());
+ assertEquals(VibrationEffect.EFFECT_CLICK,
+ ((PrebakedSegment) playedSegments.get(0)).getEffectId());
+ assertEquals(VibrationEffect.EFFECT_HEAVY_CLICK,
+ ((PrebakedSegment) playedSegments.get(1)).getEffectId());
+ }
+
+ @Test
public void performHapticFeedback_doesNotVibrateWhenVibratorInfoNotReady() throws Exception {
denyPermission(android.Manifest.permission.VIBRATE);
mHapticFeedbackVibrationMap.put(
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 856ad2a..8a6059a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -2363,6 +2363,92 @@
}
@Test
+ public void testUserOverrideFullscreenForLandscapeDisplay() {
+ final int displayWidth = 1600;
+ final int displayHeight = 1400;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ spyOn(mActivity.mWmService.mLetterboxConfiguration);
+ doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+ .isUserAppAspectRatioFullscreenEnabled();
+
+ // Set user aspect ratio override
+ spyOn(mActivity.mLetterboxUiController);
+ doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN).when(mActivity.mLetterboxUiController)
+ .getUserMinAspectRatioOverrideCode();
+
+ prepareMinAspectRatio(mActivity, 16 / 9f, SCREEN_ORIENTATION_PORTRAIT);
+
+ final Rect bounds = mActivity.getBounds();
+
+ // bounds should be fullscreen
+ assertEquals(displayHeight, bounds.height());
+ assertEquals(displayWidth, bounds.width());
+ }
+
+ @Test
+ public void testUserOverrideFullscreenForPortraitDisplay() {
+ final int displayWidth = 1400;
+ final int displayHeight = 1600;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ spyOn(mActivity.mWmService.mLetterboxConfiguration);
+ doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+ .isUserAppAspectRatioFullscreenEnabled();
+
+ // Set user aspect ratio override
+ spyOn(mActivity.mLetterboxUiController);
+ doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN).when(mActivity.mLetterboxUiController)
+ .getUserMinAspectRatioOverrideCode();
+
+ prepareMinAspectRatio(mActivity, 16 / 9f, SCREEN_ORIENTATION_LANDSCAPE);
+
+ final Rect bounds = mActivity.getBounds();
+
+ // bounds should be fullscreen
+ assertEquals(displayHeight, bounds.height());
+ assertEquals(displayWidth, bounds.width());
+ }
+
+ @Test
+ public void testSystemFullscreenOverrideForLandscapeDisplay() {
+ final int displayWidth = 1600;
+ final int displayHeight = 1400;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ spyOn(mActivity.mLetterboxUiController);
+ doReturn(true).when(mActivity.mLetterboxUiController)
+ .isSystemOverrideToFullscreenEnabled();
+
+ prepareMinAspectRatio(mActivity, 16 / 9f, SCREEN_ORIENTATION_PORTRAIT);
+
+ final Rect bounds = mActivity.getBounds();
+
+ // bounds should be fullscreen
+ assertEquals(displayHeight, bounds.height());
+ assertEquals(displayWidth, bounds.width());
+ }
+
+ @Test
+ public void testSystemFullscreenOverrideForPortraitDisplay() {
+ final int displayWidth = 1400;
+ final int displayHeight = 1600;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ spyOn(mActivity.mLetterboxUiController);
+ doReturn(true).when(mActivity.mLetterboxUiController)
+ .isSystemOverrideToFullscreenEnabled();
+
+ prepareMinAspectRatio(mActivity, 16 / 9f, SCREEN_ORIENTATION_LANDSCAPE);
+
+ final Rect bounds = mActivity.getBounds();
+
+ // bounds should be fullscreen
+ assertEquals(displayHeight, bounds.height());
+ assertEquals(displayWidth, bounds.width());
+ }
+
+ @Test
public void testUserOverrideSplitScreenAspectRatioForLandscapeDisplay() {
final int displayWidth = 1600;
final int displayHeight = 1400;