diff options
15 files changed, 705 insertions, 152 deletions
diff --git a/core/java/android/service/dreams/DreamOverlayConnectionHandler.java b/core/java/android/service/dreams/DreamOverlayConnectionHandler.java new file mode 100644 index 000000000000..cafe02ad8658 --- /dev/null +++ b/core/java/android/service/dreams/DreamOverlayConnectionHandler.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.dreams; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ObservableServiceConnection; +import com.android.internal.util.PersistentServiceConnection; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * Handles the service connection to {@link IDreamOverlay} + * + * @hide + */ +@VisibleForTesting +public final class DreamOverlayConnectionHandler { + private static final String TAG = "DreamOverlayConnection"; + + private static final int MSG_ADD_CONSUMER = 1; + private static final int MSG_REMOVE_CONSUMER = 2; + private static final int MSG_OVERLAY_CLIENT_READY = 3; + + private final Handler mHandler; + private final PersistentServiceConnection<IDreamOverlay> mConnection; + // Retrieved Client + private IDreamOverlayClient mClient; + // A list of pending requests to execute on the overlay. + private final List<Consumer<IDreamOverlayClient>> mConsumers = new ArrayList<>(); + private final OverlayConnectionCallback mCallback; + + DreamOverlayConnectionHandler( + Context context, + Looper looper, + Intent serviceIntent, + int minConnectionDurationMs, + int maxReconnectAttempts, + int baseReconnectDelayMs) { + this(context, looper, serviceIntent, minConnectionDurationMs, maxReconnectAttempts, + baseReconnectDelayMs, new Injector()); + } + + @VisibleForTesting + public DreamOverlayConnectionHandler( + Context context, + Looper looper, + Intent serviceIntent, + int minConnectionDurationMs, + int maxReconnectAttempts, + int baseReconnectDelayMs, + Injector injector) { + mCallback = new OverlayConnectionCallback(); + mHandler = new Handler(looper, new OverlayHandlerCallback()); + mConnection = injector.buildConnection( + context, + mHandler, + serviceIntent, + minConnectionDurationMs, + maxReconnectAttempts, + baseReconnectDelayMs + ); + } + + /** + * Bind to the overlay service. If binding fails, we automatically call unbind to clean + * up resources. + * + * @return true if binding was successful, false otherwise. + */ + public boolean bind() { + mConnection.addCallback(mCallback); + final boolean success = mConnection.bind(); + if (!success) { + unbind(); + } + return success; + } + + /** + * Unbind from the overlay service, clearing any pending callbacks. + */ + public void unbind() { + mConnection.removeCallback(mCallback); + // Remove any pending messages. + mHandler.removeCallbacksAndMessages(null); + mClient = null; + mConsumers.clear(); + mConnection.unbind(); + } + + /** + * Adds a consumer to run once the overlay service has connected. If the overlay service + * disconnects (eg binding dies) and then reconnects, this consumer will be re-run unless + * removed. + * + * @param consumer The consumer to run. This consumer is always executed asynchronously. + */ + public void addConsumer(Consumer<IDreamOverlayClient> consumer) { + final Message msg = mHandler.obtainMessage(MSG_ADD_CONSUMER, consumer); + mHandler.sendMessage(msg); + } + + /** + * Removes the consumer, preventing this consumer from being called again. + * + * @param consumer The consumer to remove. + */ + public void removeConsumer(Consumer<IDreamOverlayClient> consumer) { + final Message msg = mHandler.obtainMessage(MSG_REMOVE_CONSUMER, consumer); + mHandler.sendMessage(msg); + // Clear any pending messages to add this consumer + mHandler.removeMessages(MSG_ADD_CONSUMER, consumer); + } + + private final class OverlayHandlerCallback implements Handler.Callback { + @Override + public boolean handleMessage(@NonNull Message msg) { + switch (msg.what) { + case MSG_OVERLAY_CLIENT_READY: + onOverlayClientReady((IDreamOverlayClient) msg.obj); + break; + case MSG_ADD_CONSUMER: + onAddConsumer((Consumer<IDreamOverlayClient>) msg.obj); + break; + case MSG_REMOVE_CONSUMER: + onRemoveConsumer((Consumer<IDreamOverlayClient>) msg.obj); + break; + } + return true; + } + } + + private void onOverlayClientReady(IDreamOverlayClient client) { + mClient = client; + for (Consumer<IDreamOverlayClient> consumer : mConsumers) { + consumer.accept(mClient); + } + } + + private void onAddConsumer(Consumer<IDreamOverlayClient> consumer) { + if (mClient != null) { + consumer.accept(mClient); + } + mConsumers.add(consumer); + } + + private void onRemoveConsumer(Consumer<IDreamOverlayClient> consumer) { + mConsumers.remove(consumer); + } + + private final class OverlayConnectionCallback implements + ObservableServiceConnection.Callback<IDreamOverlay> { + + private final IDreamOverlayClientCallback mClientCallback = + new IDreamOverlayClientCallback.Stub() { + @Override + public void onDreamOverlayClient(IDreamOverlayClient client) { + final Message msg = + mHandler.obtainMessage(MSG_OVERLAY_CLIENT_READY, client); + mHandler.sendMessage(msg); + } + }; + + @Override + public void onConnected( + ObservableServiceConnection<IDreamOverlay> connection, + IDreamOverlay service) { + try { + service.getClient(mClientCallback); + } catch (RemoteException e) { + Log.e(TAG, "could not get DreamOverlayClient", e); + } + } + + @Override + public void onDisconnected(ObservableServiceConnection<IDreamOverlay> connection, + int reason) { + mClient = null; + // Cancel any pending messages about the overlay being ready, since it is no + // longer ready. + mHandler.removeMessages(MSG_OVERLAY_CLIENT_READY); + } + } + + /** + * Injector for testing + */ + @VisibleForTesting + public static class Injector { + /** + * Returns milliseconds since boot, not counting time spent in deep sleep. Can be overridden + * in tests with a fake clock. + */ + public PersistentServiceConnection<IDreamOverlay> buildConnection( + Context context, + Handler handler, + Intent serviceIntent, + int minConnectionDurationMs, + int maxReconnectAttempts, + int baseReconnectDelayMs) { + final Executor executor = handler::post; + final int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE; + return new PersistentServiceConnection<>( + context, + executor, + handler, + IDreamOverlay.Stub::asInterface, + serviceIntent, + flags, + minConnectionDurationMs, + maxReconnectAttempts, + baseReconnectDelayMs + ); + } + } +} diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 356a566fa913..9107c5f4bbdb 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -68,8 +68,6 @@ import android.view.accessibility.AccessibilityEvent; import com.android.internal.R; import com.android.internal.util.DumpUtils; -import com.android.internal.util.ObservableServiceConnection; -import com.android.internal.util.PersistentServiceConnection; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -77,8 +75,6 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -234,7 +230,6 @@ public class DreamService extends Service implements Window.Callback { private boolean mCanDoze; private boolean mDozing; private boolean mWindowless; - private boolean mOverlayFinishing; private int mDozeScreenState = Display.STATE_UNKNOWN; private int mDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT; @@ -246,88 +241,7 @@ public class DreamService extends Service implements Window.Callback { private DreamServiceWrapper mDreamServiceWrapper; private Runnable mDispatchAfterOnAttachedToWindow; - private OverlayConnection mOverlayConnection; - - private static class OverlayConnection extends PersistentServiceConnection<IDreamOverlay> { - // Retrieved Client - private IDreamOverlayClient mClient; - - // A list of pending requests to execute on the overlay. - private final ArrayList<Consumer<IDreamOverlayClient>> mConsumers = new ArrayList<>(); - - private final IDreamOverlayClientCallback mClientCallback = - new IDreamOverlayClientCallback.Stub() { - @Override - public void onDreamOverlayClient(IDreamOverlayClient client) { - mClient = client; - - for (Consumer<IDreamOverlayClient> consumer : mConsumers) { - consumer.accept(mClient); - } - } - }; - - private final Callback<IDreamOverlay> mCallback = new Callback<IDreamOverlay>() { - @Override - public void onConnected(ObservableServiceConnection<IDreamOverlay> connection, - IDreamOverlay service) { - try { - service.getClient(mClientCallback); - } catch (RemoteException e) { - Log.e(TAG, "could not get DreamOverlayClient", e); - } - } - - @Override - public void onDisconnected(ObservableServiceConnection<IDreamOverlay> connection, - int reason) { - mClient = null; - } - }; - - OverlayConnection(Context context, - Executor executor, - Handler handler, - ServiceTransformer<IDreamOverlay> transformer, - Intent serviceIntent, - int flags, - int minConnectionDurationMs, - int maxReconnectAttempts, - int baseReconnectDelayMs) { - super(context, executor, handler, transformer, serviceIntent, flags, - minConnectionDurationMs, - maxReconnectAttempts, baseReconnectDelayMs); - } - - @Override - public boolean bind() { - addCallback(mCallback); - return super.bind(); - } - - @Override - public void unbind() { - removeCallback(mCallback); - super.unbind(); - } - - public void addConsumer(Consumer<IDreamOverlayClient> consumer) { - execute(() -> { - mConsumers.add(consumer); - if (mClient != null) { - consumer.accept(mClient); - } - }); - } - - public void removeConsumer(Consumer<IDreamOverlayClient> consumer) { - execute(() -> mConsumers.remove(consumer)); - } - - public void clearConsumers() { - execute(() -> mConsumers.clear()); - } - } + private DreamOverlayConnectionHandler mOverlayConnection; private final IDreamOverlayCallback mOverlayCallback = new IDreamOverlayCallback.Stub() { @Override @@ -1030,18 +944,18 @@ public class DreamService extends Service implements Window.Callback { final Resources resources = getResources(); final Intent overlayIntent = new Intent().setComponent(overlayComponent); - mOverlayConnection = new OverlayConnection( + mOverlayConnection = new DreamOverlayConnectionHandler( /* context= */ this, - getMainExecutor(), - mHandler, - IDreamOverlay.Stub::asInterface, + Looper.getMainLooper(), overlayIntent, - /* flags= */ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, resources.getInteger(R.integer.config_minDreamOverlayDurationMs), resources.getInteger(R.integer.config_dreamOverlayMaxReconnectAttempts), resources.getInteger(R.integer.config_dreamOverlayReconnectTimeoutMs)); - mOverlayConnection.bind(); + if (!mOverlayConnection.bind()) { + // Binding failed. + mOverlayConnection = null; + } } return mDreamServiceWrapper; @@ -1069,9 +983,7 @@ public class DreamService extends Service implements Window.Callback { // If there is an active overlay connection, signal that the dream is ending before // continuing. Note that the overlay cannot rely on the unbound state, since another dream // might have bound to it in the meantime. - if (mOverlayConnection != null && !mOverlayFinishing) { - // Set mOverlayFinish to true to only allow this consumer to be added once. - mOverlayFinishing = true; + if (mOverlayConnection != null) { mOverlayConnection.addConsumer(overlay -> { try { overlay.endDream(); @@ -1082,7 +994,6 @@ public class DreamService extends Service implements Window.Callback { Log.e(mTag, "could not inform overlay of dream end:" + e); } }); - mOverlayConnection.clearConsumers(); return; } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt index 0d880759bd09..f9e8aafcfa47 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt @@ -184,6 +184,9 @@ object CustomizationProviderContract { /** Flag denoting whether the Monochromatic Theme is enabled. */ const val FLAG_NAME_MONOCHROMATIC_THEME = "is_monochromatic_theme_enabled" + /** Flag denoting AI Wallpapers are enabled in wallpaper picker. */ + const val FLAG_NAME_WALLPAPER_PICKER_UI_FOR_AIWP = "wallpaper_picker_ui_for_aiwp" + object Columns { /** String. Unique ID for the flag. */ const val NAME = "name" diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 21adb3ff13c3..2e22bfbdf9aa 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -172,42 +172,96 @@ public class QuickStepContract { public static String getSystemUiStateString(int flags) { StringJoiner str = new StringJoiner("|"); - str.add((flags & SYSUI_STATE_SCREEN_PINNING) != 0 ? "screen_pinned" : ""); - str.add((flags & SYSUI_STATE_OVERVIEW_DISABLED) != 0 ? "overview_disabled" : ""); - str.add((flags & SYSUI_STATE_HOME_DISABLED) != 0 ? "home_disabled" : ""); - str.add((flags & SYSUI_STATE_SEARCH_DISABLED) != 0 ? "search_disabled" : ""); - str.add((flags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0 ? "navbar_hidden" : ""); - str.add((flags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) != 0 ? "notif_visible" : ""); - str.add((flags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) != 0 ? "qs_visible" : ""); - str.add((flags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING) != 0 ? "keygrd_visible" : ""); - str.add((flags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) != 0 - ? "keygrd_occluded" : ""); - str.add((flags & SYSUI_STATE_BOUNCER_SHOWING) != 0 ? "bouncer_visible" : ""); - str.add((flags & SYSUI_STATE_DIALOG_SHOWING) != 0 ? "dialog_showing" : ""); - str.add((flags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0 ? "a11y_click" : ""); - str.add((flags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0 ? "a11y_long_click" : ""); - str.add((flags & SYSUI_STATE_TRACING_ENABLED) != 0 ? "tracing" : ""); - str.add((flags & SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED) != 0 - ? "asst_gesture_constrain" : ""); - str.add((flags & SYSUI_STATE_BUBBLES_EXPANDED) != 0 ? "bubbles_expanded" : ""); - str.add((flags & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0 ? "one_handed_active" : ""); - str.add((flags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0 - ? "allow_gesture" : ""); - str.add((flags & SYSUI_STATE_IME_SHOWING) != 0 ? "ime_visible" : ""); - str.add((flags & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0 ? "magnification_overlap" : ""); - str.add((flags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0 ? "ime_switcher_showing" : ""); - str.add((flags & SYSUI_STATE_DEVICE_DOZING) != 0 ? "device_dozing" : ""); - str.add((flags & SYSUI_STATE_BACK_DISABLED) != 0 ? "back_disabled" : ""); - str.add((flags & SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0 - ? "bubbles_mange_menu_expanded" : ""); - str.add((flags & SYSUI_STATE_IMMERSIVE_MODE) != 0 ? "immersive_mode" : ""); - str.add((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 ? "vis_win_showing" : ""); - str.add((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0 - ? "freeform_active_in_desktop_mode" : ""); - str.add((flags & SYSUI_STATE_DEVICE_DREAMING) != 0 ? "device_dreaming" : ""); - str.add("screen_" - + ((flags & SYSUI_STATE_SCREEN_TRANSITION) != 0 ? "turning_" : "") - + ((flags & SYSUI_STATE_SCREEN_ON) != 0 ? "on" : "off")); + if ((flags & SYSUI_STATE_SCREEN_PINNING) != 0) { + str.add("screen_pinned"); + } + if ((flags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) { + str.add("overview_disabled"); + } + if ((flags & SYSUI_STATE_HOME_DISABLED) != 0) { + str.add("home_disabled"); + } + if ((flags & SYSUI_STATE_SEARCH_DISABLED) != 0) { + str.add("search_disabled"); + } + if ((flags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0) { + str.add("navbar_hidden"); + } + if ((flags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) != 0) { + str.add("notif_visible"); + } + if ((flags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) != 0) { + str.add("qs_visible"); + } + if ((flags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING) != 0) { + str.add("keygrd_visible"); + } + if ((flags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED) != 0) { + str.add("keygrd_occluded"); + } + if ((flags & SYSUI_STATE_BOUNCER_SHOWING) != 0) { + str.add("bouncer_visible"); + } + if ((flags & SYSUI_STATE_DIALOG_SHOWING) != 0) { + str.add("dialog_showing"); + } + if ((flags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0) { + str.add("a11y_click"); + } + if ((flags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0) { + str.add("a11y_long_click"); + } + if ((flags & SYSUI_STATE_TRACING_ENABLED) != 0) { + str.add("tracing"); + } + if ((flags & SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED) != 0) { + str.add("asst_gesture_constrain"); + } + if ((flags & SYSUI_STATE_BUBBLES_EXPANDED) != 0) { + str.add("bubbles_expanded"); + } + if ((flags & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0) { + str.add("one_handed_active"); + } + if ((flags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) { + str.add("allow_gesture"); + } + if ((flags & SYSUI_STATE_IME_SHOWING) != 0) { + str.add("ime_visible"); + } + if ((flags & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0) { + str.add("magnification_overlap"); + } + if ((flags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0) { + str.add("ime_switcher_showing"); + } + if ((flags & SYSUI_STATE_DEVICE_DOZING) != 0) { + str.add("device_dozing"); + } + if ((flags & SYSUI_STATE_BACK_DISABLED) != 0) { + str.add("back_disabled"); + } + if ((flags & SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0) { + str.add("bubbles_mange_menu_expanded"); + } + if ((flags & SYSUI_STATE_IMMERSIVE_MODE) != 0) { + str.add("immersive_mode"); + } + if ((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0) { + str.add("vis_win_showing"); + } + if ((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0) { + str.add("freeform_active_in_desktop_mode"); + } + if ((flags & SYSUI_STATE_DEVICE_DREAMING) != 0) { + str.add("device_dreaming"); + } + if ((flags & SYSUI_STATE_SCREEN_TRANSITION) != 0) { + str.add("screen_transition"); + } + if ((flags & SYSUI_STATE_SCREEN_ON) != 0) { + str.add("screen_on"); + } return str.toString(); } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 48b726e7c1eb..d055fe98bfe2 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -221,6 +221,15 @@ object Flags { teamfood = true, ) + /** Enables UI updates for AI wallpapers in the wallpaper picker. */ + // TODO(b/267722622): Tracking Bug + @JvmField + val WALLPAPER_PICKER_UI_FOR_AIWP = + releasedFlag( + 229, + "wallpaper_picker_ui_for_aiwp" + ) + /** Whether to inflate the bouncer view on a background thread. */ // TODO(b/272091103): Tracking Bug @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 66f87bade308..e31771a8cecd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -407,6 +407,10 @@ constructor( KeyguardPickerFlag( name = Contract.FlagsTable.FLAG_NAME_MONOCHROMATIC_THEME, value = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME) + ), + KeyguardPickerFlag( + name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_PICKER_UI_FOR_AIWP, + value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_UI_FOR_AIWP) ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index b4724ddebb9a..1a85a0f47749 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -89,6 +89,9 @@ constructor( R.id.turbulence_noise_view, R.id.touch_ripple_view, ) + + // Sizing view id for recommendation card view. + val recSizingViewId = R.id.sizing_view } /** A listener when the current dimensions of the player change */ @@ -176,7 +179,21 @@ constructor( // Layout dimensions are possibly changing, so we need to update them. (at // least on large screen devices) lastOrientation = newOrientation - loadLayoutForType(type) + // Update the height of media controls for the expanded layout. it is needed + // for large screen devices. + if (type == TYPE.PLAYER) { + backgroundIds.forEach { id -> + expandedLayout.getConstraint(id).layout.mHeight = + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_height_expanded + ) + } + } else { + expandedLayout.getConstraint(recSizingViewId).layout.mHeight = + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_height_expanded + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 36be44256fcc..b71c14bcc5a2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -2732,6 +2732,7 @@ public final class NotificationPanelViewController implements Dumpable { public void setIsLaunchAnimationRunning(boolean running) { boolean wasRunning = mIsLaunchAnimationRunning; mIsLaunchAnimationRunning = running; + mCentralSurfaces.updateIsKeyguard(); if (wasRunning != mIsLaunchAnimationRunning) { mShadeExpansionStateManager.notifyLaunchingActivityChanged(running); } @@ -3789,6 +3790,10 @@ public final class NotificationPanelViewController implements Dumpable { return mClosing || mIsLaunchAnimationRunning; } + public boolean isLaunchAnimationRunning() { + return mIsLaunchAnimationRunning; + } + public boolean isTracking() { return mTracking; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 156e4fd1889f..e7759df6e81b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -301,9 +301,11 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } private void applyKeyguardFlags(NotificationShadeWindowState state) { - final boolean keyguardOrAod = state.keyguardShowing + // Keyguard is visible if it's showing or if it's fading away (in which case we're animating + // it out, but the wallpaper should remain visible as a backdrop for the animation); + final boolean keyguardOrAodVisible = (state.keyguardShowing || state.keyguardFadingAway) || (state.dozing && mDozeParameters.getAlwaysOn()); - if ((keyguardOrAod && !state.mediaBackdropShowing && !state.lightRevealScrimOpaque) + if ((keyguardOrAodVisible && !state.mediaBackdropShowing && !state.lightRevealScrimOpaque) || mKeyguardViewMediator.isAnimatingBetweenKeyguardAndSurfaceBehind()) { // Show the wallpaper if we're on keyguard/AOD and the wallpaper is not occluded by a // solid backdrop. Also, show it if we are currently animating between the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 02621fe4c103..906b9592e3dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -827,18 +827,16 @@ public class NotificationStackScrollLayoutController { private boolean isInVisibleLocation(NotificationEntry entry) { ExpandableNotificationRow row = entry.getRow(); - ExpandableViewState childViewState = row.getViewState(); - - if (childViewState == null) { + if (row == null) { return false; } + + ExpandableViewState childViewState = row.getViewState(); if ((childViewState.location & ExpandableViewState.VISIBLE_LOCATIONS) == 0) { return false; } - if (row.getVisibility() != View.VISIBLE) { - return false; - } - return true; + + return row.getVisibility() == View.VISIBLE; } public boolean isViewAffectedBySwipe(ExpandableView expandableView) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 5131772e34a0..191302547458 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1007,6 +1007,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // The light reveal scrim should always be fully revealed by the time the keyguard // is done going away. Double check that this is true. if (!mKeyguardStateController.isKeyguardGoingAway()) { + updateIsKeyguard(); + if (mLightRevealScrim.getRevealAmount() != 1f) { Log.e(TAG, "Keyguard is done going away, but someone left the light reveal " + "scrim at reveal amount: " + mLightRevealScrim.getRevealAmount()); @@ -2893,6 +2895,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { showKeyguardImpl(); } } else { + final boolean isLaunchingOrGoingAway = + mNotificationPanelViewController.isLaunchAnimationRunning() + || mKeyguardStateController.isKeyguardGoingAway(); + // During folding a foldable device this might be called as a result of // 'onScreenTurnedOff' call for the inner display. // In this case: @@ -2904,7 +2910,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { if (!mScreenOffAnimationController.isKeyguardHideDelayed() // If we're animating occluded, there's an activity launching over the keyguard // UI. Wait to hide it until after the animation concludes. - && !mKeyguardViewMediator.isOccludeAnimationPlaying()) { + && !mKeyguardViewMediator.isOccludeAnimationPlaying() + // If we're occluded, but playing an animation (launch or going away animations) + // the keyguard is visible behind the animation. + && !(mKeyguardStateController.isOccluded() && isLaunchingOrGoingAway)) { + // If we're going away and occluded, it means we are launching over the + // unsecured keyguard, which will subsequently go away. Wait to hide it until + // after the animation concludes to avoid the lockscreen UI changing into the + // shade UI behind the launch animation. return hideKeyguardImpl(forceStateChange); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index 0fac3db2dc1f..4565762929d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -21,7 +21,6 @@ import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View -import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase @@ -61,8 +60,6 @@ class MediaViewControllerTest : SysuiTestCase() { @Mock private lateinit var mediaSubTitleWidgetState: WidgetState @Mock private lateinit var mediaContainerWidgetState: WidgetState @Mock private lateinit var mediaFlags: MediaFlags - @Mock private lateinit var expandedLayout: ConstraintSet - @Mock private lateinit var collapsedLayout: ConstraintSet val delta = 0.1F @@ -82,16 +79,47 @@ class MediaViewControllerTest : SysuiTestCase() { } @Test - fun testOrientationChanged_layoutsAreLoaded() { - mediaViewController.expandedLayout = expandedLayout - mediaViewController.collapsedLayout = collapsedLayout + fun testOrientationChanged_heightOfPlayerIsUpdated() { + val newConfig = Configuration() + + mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) + // Change the height to see the effect of orientation change. + MediaViewController.backgroundIds.forEach { id -> + mediaViewController.expandedLayout.getConstraint(id).layout.mHeight = 10 + } + newConfig.orientation = ORIENTATION_LANDSCAPE + configurationController.onConfigurationChanged(newConfig) + MediaViewController.backgroundIds.forEach { id -> + assertTrue( + mediaViewController.expandedLayout.getConstraint(id).layout.mHeight == + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_height_expanded + ) + ) + } + } + + @Test + fun testOrientationChanged_heightOfRecCardIsUpdated() { val newConfig = Configuration() + + mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION) + // Change the height to see the effect of orientation change. + mediaViewController.expandedLayout + .getConstraint(MediaViewController.recSizingViewId) + .layout + .mHeight = 10 newConfig.orientation = ORIENTATION_LANDSCAPE configurationController.onConfigurationChanged(newConfig) - verify(expandedLayout).load(context, R.xml.media_session_expanded) - verify(collapsedLayout).load(context, R.xml.media_session_collapsed) + assertTrue( + mediaViewController.expandedLayout + .getConstraint(MediaViewController.recSizingViewId) + .layout + .mHeight == + context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 526dc8d150fe..dd7929771bb2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -223,6 +223,16 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test + public void attach_fadingAway_wallpaperVisible() { + clearInvocations(mWindowManager); + mNotificationShadeWindowController.attach(); + mNotificationShadeWindowController.setKeyguardFadingAway(true); + + verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); + assertThat((mLayoutParameters.getValue().flags & FLAG_SHOW_WALLPAPER) != 0).isTrue(); + } + + @Test public void setBackgroundBlurRadius_expandedWithBlurs() { mNotificationShadeWindowController.setBackgroundBlurRadius(10); verify(mNotificationShadeWindowView).setVisibility(eq(View.VISIBLE)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 33a813f5b0ea..bf05d4e43224 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -1287,6 +1287,18 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test + public void keyguard_notHidden_ifGoingAwayAndOccluded() { + setKeyguardShowingAndOccluded(true /* showing */, false /* occluded */); + + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(true); + + mCentralSurfaces.updateIsKeyguard(false); + + verify(mStatusBarStateController, never()).setState(eq(SHADE), anyBoolean()); + } + + @Test public void frpLockedDevice_shadeDisabled() { when(mDeviceProvisionedController.isFrpActive()).thenReturn(true); when(mDozeServiceHost.isPulsing()).thenReturn(true); diff --git a/services/tests/mockingservicestests/src/android/service/dreams/DreamOverlayConnectionHandlerTest.java b/services/tests/mockingservicestests/src/android/service/dreams/DreamOverlayConnectionHandlerTest.java new file mode 100644 index 000000000000..22d7e7300bba --- /dev/null +++ b/services/tests/mockingservicestests/src/android/service/dreams/DreamOverlayConnectionHandlerTest.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.dreams; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.RemoteException; +import android.os.test.TestLooper; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.util.ObservableServiceConnection; +import com.android.internal.util.PersistentServiceConnection; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DreamOverlayConnectionHandlerTest { + private static final int MIN_CONNECTION_DURATION_MS = 100; + private static final int MAX_RECONNECT_ATTEMPTS = 3; + private static final int BASE_RECONNECT_DELAY_MS = 50; + + @Mock + private Context mContext; + @Mock + private PersistentServiceConnection<IDreamOverlay> mConnection; + @Mock + private Intent mServiceIntent; + @Mock + private IDreamOverlay mOverlayService; + @Mock + private IDreamOverlayClient mOverlayClient; + + private TestLooper mTestLooper; + private DreamOverlayConnectionHandler mDreamOverlayConnectionHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTestLooper = new TestLooper(); + mDreamOverlayConnectionHandler = new DreamOverlayConnectionHandler( + mContext, + mTestLooper.getLooper(), + mServiceIntent, + MIN_CONNECTION_DURATION_MS, + MAX_RECONNECT_ATTEMPTS, + BASE_RECONNECT_DELAY_MS, + new TestInjector(mConnection)); + } + + @Test + public void consumerShouldRunImmediatelyWhenClientAvailable() throws RemoteException { + mDreamOverlayConnectionHandler.bind(); + connectService(); + provideClient(); + + final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); + mDreamOverlayConnectionHandler.addConsumer(consumer); + mTestLooper.dispatchAll(); + verify(consumer).accept(mOverlayClient); + } + + @Test + public void consumerShouldRunAfterClientAvailable() throws RemoteException { + mDreamOverlayConnectionHandler.bind(); + connectService(); + + final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); + mDreamOverlayConnectionHandler.addConsumer(consumer); + mTestLooper.dispatchAll(); + // No client yet, so we shouldn't have executed + verify(consumer, never()).accept(mOverlayClient); + + provideClient(); + mTestLooper.dispatchAll(); + verify(consumer).accept(mOverlayClient); + } + + @Test + public void consumerShouldNeverRunIfClientConnectsAndDisconnects() throws RemoteException { + mDreamOverlayConnectionHandler.bind(); + connectService(); + + final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); + mDreamOverlayConnectionHandler.addConsumer(consumer); + mTestLooper.dispatchAll(); + // No client yet, so we shouldn't have executed + verify(consumer, never()).accept(mOverlayClient); + + provideClient(); + // Service disconnected before looper could handle the message. + disconnectService(); + mTestLooper.dispatchAll(); + verify(consumer, never()).accept(mOverlayClient); + } + + @Test + public void consumerShouldNeverRunIfUnbindCalled() throws RemoteException { + mDreamOverlayConnectionHandler.bind(); + connectService(); + provideClient(); + + final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); + mDreamOverlayConnectionHandler.addConsumer(consumer); + mDreamOverlayConnectionHandler.unbind(); + mTestLooper.dispatchAll(); + // We unbinded immediately after adding consumer, so should never have run. + verify(consumer, never()).accept(mOverlayClient); + } + + @Test + public void consumersOnlyRunOnceIfUnbound() throws RemoteException { + mDreamOverlayConnectionHandler.bind(); + connectService(); + provideClient(); + + AtomicInteger counter = new AtomicInteger(); + // Add 10 consumers in a row which call unbind within the consumer. + for (int i = 0; i < 10; i++) { + mDreamOverlayConnectionHandler.addConsumer(client -> { + counter.getAndIncrement(); + mDreamOverlayConnectionHandler.unbind(); + }); + } + mTestLooper.dispatchAll(); + // Only the first consumer should have run, since we unbinded. + assertThat(counter.get()).isEqualTo(1); + } + + @Test + public void consumerShouldRunAgainAfterReconnect() throws RemoteException { + mDreamOverlayConnectionHandler.bind(); + connectService(); + provideClient(); + + final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); + mDreamOverlayConnectionHandler.addConsumer(consumer); + mTestLooper.dispatchAll(); + verify(consumer, times(1)).accept(mOverlayClient); + + disconnectService(); + mTestLooper.dispatchAll(); + // No new calls should happen when service disconnected. + verify(consumer, times(1)).accept(mOverlayClient); + + connectService(); + provideClient(); + mTestLooper.dispatchAll(); + // We should trigger the consumer again once the server reconnects. + verify(consumer, times(2)).accept(mOverlayClient); + } + + @Test + public void consumerShouldNeverRunIfRemovedImmediately() throws RemoteException { + mDreamOverlayConnectionHandler.bind(); + connectService(); + provideClient(); + + final Consumer<IDreamOverlayClient> consumer = Mockito.mock(Consumer.class); + mDreamOverlayConnectionHandler.addConsumer(consumer); + mDreamOverlayConnectionHandler.removeConsumer(consumer); + mTestLooper.dispatchAll(); + verify(consumer, never()).accept(mOverlayClient); + } + + private void connectService() { + final ObservableServiceConnection.Callback<IDreamOverlay> callback = + captureConnectionCallback(); + callback.onConnected(mConnection, mOverlayService); + } + + private void disconnectService() { + final ObservableServiceConnection.Callback<IDreamOverlay> callback = + captureConnectionCallback(); + callback.onDisconnected(mConnection, /* reason= */ 0); + } + + private void provideClient() throws RemoteException { + final IDreamOverlayClientCallback callback = captureClientCallback(); + callback.onDreamOverlayClient(mOverlayClient); + } + + private ObservableServiceConnection.Callback<IDreamOverlay> captureConnectionCallback() { + ArgumentCaptor<ObservableServiceConnection.Callback<IDreamOverlay>> + callbackCaptor = + ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class); + verify(mConnection).addCallback(callbackCaptor.capture()); + return callbackCaptor.getValue(); + } + + private IDreamOverlayClientCallback captureClientCallback() throws RemoteException { + ArgumentCaptor<IDreamOverlayClientCallback> callbackCaptor = + ArgumentCaptor.forClass(IDreamOverlayClientCallback.class); + verify(mOverlayService, atLeastOnce()).getClient(callbackCaptor.capture()); + return callbackCaptor.getValue(); + } + + static class TestInjector extends DreamOverlayConnectionHandler.Injector { + private final PersistentServiceConnection<IDreamOverlay> mConnection; + + TestInjector(PersistentServiceConnection<IDreamOverlay> connection) { + mConnection = connection; + } + + @Override + public PersistentServiceConnection<IDreamOverlay> buildConnection(Context context, + Handler handler, Intent serviceIntent, int minConnectionDurationMs, + int maxReconnectAttempts, int baseReconnectDelayMs) { + return mConnection; + } + } +} |