diff options
49 files changed, 1243 insertions, 250 deletions
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 3575545e202d..f64418587918 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -2608,7 +2608,14 @@ public final class ActivityThread extends ClientTransactionHandler 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 @@ public final class ActivityThread extends ClientTransactionHandler 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 18dc1ce18baf..62a50dbbd6f7 100644 --- a/core/java/android/app/ConfigurationController.java +++ b/core/java/android/app/ConfigurationController.java @@ -21,6 +21,7 @@ import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded; 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 @@ class ConfigurationController { */ 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 8f81ae2ae7d6..cf0641651d66 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -50,8 +50,8 @@ interface INotificationManager 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 a8d61db1ce3a..6e7e93009993 100644 --- a/core/java/android/app/servertransaction/ClientTransactionItem.java +++ b/core/java/android/app/servertransaction/ClientTransactionItem.java @@ -53,6 +53,7 @@ public abstract class ClientTransactionItem implements BaseClientRequest, Parcel 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 c55b0f110b3b..722d5f0fe462 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 static java.util.Objects.requireNonNull; 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 @@ public class ClientTransactionListenerController { 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 @@ public class ClientTransactionListenerController { } } - /** - * Called when receives a {@link ClientTransaction} that is updating display-related - * window configuration. - */ - public void onDisplayChanged(int displayId) { - if (!bundleClientTransactionFlag()) { + /** 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; } - if (ActivityThread.isSystem()) { + 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 c83719149821..480205ebc756 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.shouldExcl 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 @@ public class TransactionExecutor { 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 @@ public class TransactionExecutor { 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 @@ public class TransactionExecutor { } } - 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 253073a83827..69228cafa34b 100644 --- a/core/java/android/view/HapticFeedbackConstants.java +++ b/core/java/android/view/HapticFeedbackConstants.java @@ -83,11 +83,7 @@ public class HapticFeedbackConstants { */ 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 class HapticFeedbackConstants { 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 65984f55ded2..ead8887fda35 100644 --- a/core/java/android/widget/Toast.java +++ b/core/java/android/widget/Toast.java @@ -45,8 +45,10 @@ import android.util.Log; 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 @@ public class Toast { 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 @@ public class Toast { } } + /** + * 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 @@ public class Toast { 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 @@ public class Toast { @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 @@ public class Toast { 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 class Toast { } 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 @@ public class Toast { 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 e60fa157e8e4..515fa55b7b90 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 @@ flag { 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 4a3aba13fd54..a868d487b82f 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -25,6 +25,7 @@ import android.annotation.NonNull; 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 @@ public class WindowTokenClient extends Binder { * 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 913b63e71d68..c694426a5aa4 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 77d31a5f27e7..8506905e6ca0 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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentat 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 @@ public class ClientTransactionListenerControllerTest { 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 @@ public class ClientTransactionListenerControllerTest { 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 @@ public class ClientTransactionListenerControllerTest { } @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 2d778b1218d2..aca52a870655 100644 --- a/core/tests/mockingcoretests/Android.bp +++ b/core/tests/mockingcoretests/Android.bp @@ -40,6 +40,7 @@ android_test { "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 000000000000..c0cbea98cc57 --- /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 000000000000..79bc81d57727 --- /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 761bb7918afd..91bd7916b0ab 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.FooterPageProvider 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.PreferenceMainPageProvider 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 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { 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 1f028d5e7bc9..654719d906a9 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.FooterPageProvider 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 @@ object HomePageProvider : SettingsPageProvider { 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 index dc45e6da0bc1..029773fdf8df 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.SettingsPager 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 { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } - } + fun buildInjectEntry() = 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 @@ object SettingsPagerPageProvider : SettingsPageProvider { @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 000000000000..66cc38f74b07 --- /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/scaffold/ScrollablePagerPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/ScrollablePagerPageProvider.kt new file mode 100644 index 000000000000..689a98a16b23 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/ScrollablePagerPageProvider.kt @@ -0,0 +1,74 @@ +/* + * 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 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 +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.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.SettingsPager +import com.android.settingslib.spa.widget.scaffold.SettingsScaffold + +object ScrollablePagerPageProvider : SettingsPageProvider { + override val name = "ScrollablePager" + private const val TITLE = "Sample Scrollable SettingsPager" + + fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = createSettingsPage()) + .setUiLayoutFn { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } + + 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")) { + LazyColumn { + items(30) { + Preference(object : PreferenceModel { + override val title = it.toString() + }) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ScrollablePagerPageProviderPreview() { + SettingsTheme { + 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 6fc8de3ac49c..a0ab2ce6945d 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.foundation.layout.padding 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.preference.PreferenceModel 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 @@ private fun Page() { 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 f372a45f9e59..163766a7a9c5 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 @@ package com.android.settingslib.spa.widget.scaffold 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.Scaffold 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 @@ fun SuwScaffold( title: String, actionButton: BottomAppBarButton? = null, dismissButton: BottomAppBarButton? = null, - content: @Composable ColumnScope.() -> Unit, + content: @Composable () -> Unit, ) { ActivityTitle(title) Scaffold { innerPadding -> @@ -59,6 +60,7 @@ fun SuwScaffold( .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 @@ fun SuwScaffold( .verticalScroll(rememberScrollState()) ) { Header(imageVector, title) - content() + movableContent() } BottomBar(actionButton, dismissButton) } @@ -82,8 +84,9 @@ fun SuwScaffold( 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 2a2762383333..7f4bebcf4a62 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 @@ package com.android.settingslib.drawer; 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 @@ public abstract class Tile implements Parcelable { 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 b1cfdcf07977..dbec059715b4 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 @@ internal class SceneTransitionLayoutImpl( } // 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 4950b96b077f..85774c67bccb 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 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { 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 66f7e015a133..776f1a55fdcb 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.coroutines.collectLastValue 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.statusbar.notification.stack.ui.viewmodel.notificati 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 @@ import platform.test.runner.parameterized.Parameters 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 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { 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 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { 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 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { @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 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { } ) kosmos.setCommunalAvailable(isCommunalAvailable) + kosmos.fakePowerRepository.updateWakefulness( + rawState = + if (isShadeTouchable) { + WakefulnessState.AWAKE + } else { + WakefulnessState.ASLEEP + }, + ) val destinationScenes by collectLastValue(underTest.destinationScenes) @@ -148,14 +198,25 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { 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 9856f9050c4b..93302e32b607 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 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { 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 3fd5306abb71..75e66fb06ce8 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.domain.interactor.keyguardInteractor 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 @@ class SceneContainerStartableTest : SysuiTestCase() { occlusionInteractor = kosmos.sceneContainerOcclusionInteractor, faceUnlockInteractor = kosmos.deviceEntryFaceAuthInteractor, deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor, + shadeInteractor = kosmos.shadeInteractor, ) } @@ -1127,6 +1131,33 @@ class SceneContainerStartableTest : SysuiTestCase() { 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 @@ class SceneContainerStartableTest : SysuiTestCase() { isLockscreenEnabled: Boolean = true, startsAwake: Boolean = true, isDeviceProvisioned: Boolean = true, + isInteractive: Boolean = true, ): MutableStateFlow<ObservableTransitionState> { if (authenticationMethod?.isSecure == true) { assert(isLockscreenEnabled) { @@ -1205,6 +1237,7 @@ class SceneContainerStartableTest : SysuiTestCase() { } 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 3a45db17b64c..61d1c713fb77 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 @@ class UdfpsControllerOverlay @JvmOverloads constructor( 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 993e81bfbf69..d4c8456e0d71 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.ExperimentalCoroutinesApi 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 @@ constructor( 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 4e0b5762b6cf..ab0b0b7b7c6c 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 @@ constructor( 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 32d72e0bac22..1f935f97e771 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.domain.interactor.SceneInteractor 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 @@ constructor( private val headsUpInteractor: HeadsUpNotificationInteractor, private val occlusionInteractor: SceneContainerOcclusionInteractor, private val faceUnlockInteractor: DeviceEntryFaceAuthInteractor, + private val shadeInteractor: ShadeInteractor, ) : CoreStartable { override fun start() { @@ -185,6 +187,14 @@ constructor( /** 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 @@ constructor( } } } + } + + private fun handleSimUnlock() { applicationScope.launch { simBouncerInteractor .get() @@ -229,6 +242,9 @@ constructor( } } } + } + + private fun handleDeviceUnlockStatus() { applicationScope.launch { deviceUnlockedInteractor.deviceUnlockStatus .mapNotNull { deviceUnlockStatus -> @@ -288,7 +304,9 @@ constructor( ) } } + } + private fun handlePowerState() { applicationScope.launch { powerInteractor.isAsleep.collect { isAsleep -> if (isAsleep) { @@ -317,7 +335,7 @@ constructor( ) { 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 @@ constructor( } } + 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 f6b1bcc0ceea..f418e7e0278f 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.GestureDetector 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.communal.ui.viewmodel.CommunalViewModel 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.statusbar.phone.SystemUIDialogFactory 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 @@ constructor( ): 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 ebebbe65d54b..8c15817898a8 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 @@ constructor( // 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 9ce38db1aebe..8b673c951b94 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.os.SystemClock; 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 @@ public final class NotificationEntry extends ListEntry { 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 @@ public final class NotificationEntry extends ListEntry { 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 @@ public final class NotificationEntry extends ListEntry { 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 @@ public final class NotificationEntry extends ListEntry { 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 @@ public final class NotificationEntry extends ListEntry { 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 a100fe06c407..3d01c5f8ed48 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 @@ final class InputMethodBindingController { 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 956e10c79246..ebc1a2a45579 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 @@ public class NotificationManagerService extends SystemService { // ============================================================================ @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 @@ public class NotificationManagerService extends SystemService { || (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 @@ public class NotificationManagerService extends SystemService { boolean isAppRenderedToast = (callback != null); if (!checkCanEnqueueToast(pkg, callingUid, displayId, isAppRenderedToast, isSystemToast)) { - return; + return false; } synchronized (mToastQueue) { @@ -3477,7 +3477,7 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { 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 2a9325544833..c8bcc5128c3a 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.UserHandle; 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 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub implements Rollba // 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 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub implements Rollba } }); + mUserBroadcastReceivers = new ArrayMap<>(); + UserManager userManager = mContext.getSystemService(UserManager.class); for (UserHandle user : userManager.getUserHandles(true)) { registerUserCallbacks(user); @@ -275,7 +281,9 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub implements Rollba } }, 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 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub implements Rollba 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 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub implements Rollba 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 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub implements Rollba 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 96f045d7e258..8138168f609e 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 @@ public final class HapticFeedbackVibrationProvider { 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 @@ public final class HapticFeedbackVibrationProvider { return getKeyboardVibration(effectId); case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE: - case HapticFeedbackConstants.ENTRY_BUMP: case HapticFeedbackConstants.DRAG_CROSSING: return getVibration( effectId, @@ -131,6 +132,7 @@ public final class HapticFeedbackVibrationProvider { 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 @@ public final class HapticFeedbackVibrationProvider { 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 @@ public final class HapticFeedbackVibrationProvider { 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 @@ public final class HapticFeedbackVibrationProvider { 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 9e9025e35e60..8281ac1c9d28 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 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { 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 c6b401b3d7b8..36e52008f223 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 @@ final class WearableSensingManagerPerUserService extends 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 207d42b6b58d..ad1f6486eca2 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } // 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 3f245456d849..f220c9d06e14 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 @@ final class LetterboxUiController { || 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 55a624895ff6..b0eee0881f63 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 @@ public class DisplayManagerServiceTest { // 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 @@ public class DisplayManagerServiceTest { 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 @@ public class DisplayManagerServiceTest { logicalDisplayMapper.setEnabledLocked(display, /* isEnabled= */ true); logicalDisplayMapper.updateLogicalDisplays(); callback.waitForExpectedEvent(); - callback.clear(); assertThrows(SecurityException.class, () -> bs.disableConnectedDisplay(displayId)); } @@ -3376,7 +3384,7 @@ public class DisplayManagerServiceTest { 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 610627886c1b..f08fbde962ef 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .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 a14ea5598758..c0f514fb9673 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 e3d45967848a..633a3c985b7f 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.Composition.PRIMITIVE_TICK; 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 @@ public class HapticFeedbackVibrationProviderTest { 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 @@ public class HapticFeedbackVibrationProviderTest { } @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 @@ public class HapticFeedbackVibrationProviderTest { } } + @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 185677f966a4..d6c0fef9649a 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 @@ public class VibratorManagerServiceTest { } @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 856ad2a02444..8a6059aa3ccb 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 @@ public class SizeCompatTests extends WindowTestsBase { } @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; |