diff options
25 files changed, 1747 insertions, 27 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index d5ceafb6ca3d..226deba694a5 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -257,7 +257,8 @@ package android.app { public class DreamManager { method @RequiresPermission(android.Manifest.permission.READ_DREAM_STATE) public boolean isDreaming(); - method @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) public void setActiveDream(@NonNull android.content.ComponentName); + method @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) public void setActiveDream(@Nullable android.content.ComponentName); + method @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) public void setDreamOverlay(@Nullable android.content.ComponentName); method @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) public void startDream(@NonNull android.content.ComponentName); method @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) public void stopDream(); } @@ -2364,6 +2365,17 @@ package android.service.autofill.augmented { } +package android.service.dreams { + + public abstract class DreamOverlayService extends android.app.Service { + ctor public DreamOverlayService(); + method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent); + method public abstract void onStartDream(@NonNull android.view.WindowManager.LayoutParams); + method public final void requestExit(); + } + +} + package android.service.notification { @Deprecated public abstract class ConditionProviderService extends android.app.Service { diff --git a/core/java/android/app/DreamManager.java b/core/java/android/app/DreamManager.java index f23681373f53..34ae08fd9b9a 100644 --- a/core/java/android/app/DreamManager.java +++ b/core/java/android/app/DreamManager.java @@ -17,6 +17,7 @@ package android.app; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.TestApi; @@ -91,10 +92,30 @@ public class DreamManager { @TestApi @UserHandleAware @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) - public void setActiveDream(@NonNull ComponentName dreamComponent) { + public void setActiveDream(@Nullable ComponentName dreamComponent) { ComponentName[] dreams = {dreamComponent}; + + try { + mService.setDreamComponentsForUser(mContext.getUserId(), + dreamComponent != null ? dreams : null); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Sets the active dream on the device to be "dreamComponent". + * + * <p>This is only used for testing the dream service APIs. + * + * @hide + */ + @TestApi + @UserHandleAware + @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) + public void setDreamOverlay(@Nullable ComponentName dreamOverlayComponent) { try { - mService.setDreamComponentsForUser(mContext.getUserId(), dreams); + mService.registerDreamOverlayService(dreamOverlayComponent); } catch (RemoteException e) { e.rethrowFromSystemServer(); } diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java new file mode 100644 index 000000000000..50f9d8ac2958 --- /dev/null +++ b/core/java/android/service/dreams/DreamOverlayService.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 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.annotation.Nullable; +import android.annotation.TestApi; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.view.WindowManager; + + +/** + * Basic implementation of for {@link IDreamOverlay} for testing. + * @hide + */ +@TestApi +public abstract class DreamOverlayService extends Service { + private static final String TAG = "DreamOverlayService"; + private static final boolean DEBUG = false; + + private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() { + @Override + public void startDream(WindowManager.LayoutParams layoutParams, + IDreamOverlayCallback callback) { + mDreamOverlayCallback = callback; + onStartDream(layoutParams); + } + }; + + IDreamOverlayCallback mDreamOverlayCallback; + + public DreamOverlayService() { + } + + @Nullable + @Override + public final IBinder onBind(@NonNull Intent intent) { + return mDreamOverlay.asBinder(); + } + + /** + * This method is overridden by implementations to handle when the dream has started and the + * window is ready to be interacted with. + * @param layoutParams The {@link android.view.WindowManager.LayoutParams} associated with the + * dream window. + */ + public abstract void onStartDream(@NonNull WindowManager.LayoutParams layoutParams); + + /** + * This method is invoked to request the dream exit. + */ + public final void requestExit() { + try { + mDreamOverlayCallback.onExitRequested(); + } catch (RemoteException e) { + Log.e(TAG, "Could not request exit:" + e); + } + } +} diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 096595f30b05..3ab6907557da 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -203,7 +203,6 @@ public class DreamService extends Service implements Window.Callback { private boolean mCanDoze; private boolean mDozing; private boolean mWindowless; - private boolean mOverlayServiceBound; private int mDozeScreenState = Display.STATE_UNKNOWN; private int mDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT; @@ -220,10 +219,34 @@ public class DreamService extends Service implements Window.Callback { // A Queue of pending requests to execute on the overlay. private ArrayDeque<Consumer<IDreamOverlay>> mRequests; + private boolean mBound; + OverlayConnection() { mRequests = new ArrayDeque<>(); } + public void bind(Context context, @Nullable ComponentName overlayService) { + if (overlayService == null) { + return; + } + + final Intent overlayIntent = new Intent(); + overlayIntent.setComponent(overlayService); + + context.bindService(overlayIntent, + this, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE); + mBound = true; + } + + public void unbind(Context context) { + if (!mBound) { + return; + } + + context.unbindService(this); + mBound = false; + } + public void request(Consumer<IDreamOverlay> request) { mRequests.push(request); evaluate(); @@ -930,14 +953,8 @@ public class DreamService extends Service implements Window.Callback { mDreamServiceWrapper = new DreamServiceWrapper(); // Connect to the overlay service if present. - final ComponentName overlayComponent = - intent.getParcelableExtra(EXTRA_DREAM_OVERLAY_COMPONENT); - if (overlayComponent != null && !mWindowless) { - final Intent overlayIntent = new Intent(); - overlayIntent.setComponent(overlayComponent); - - mOverlayServiceBound = getApplicationContext().bindService(overlayIntent, - mOverlayConnection, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE); + if (!mWindowless) { + mOverlayConnection.bind(this, intent.getParcelableExtra(EXTRA_DREAM_OVERLAY_COMPONENT)); } return mDreamServiceWrapper; @@ -973,10 +990,7 @@ public class DreamService extends Service implements Window.Callback { return; } - if (!mWindowless && mOverlayServiceBound) { - unbindService(mOverlayConnection); - mOverlayServiceBound = false; - } + mOverlayConnection.unbind(this); try { // finishSelf will unbind the dream controller from the dream service. This will @@ -1173,6 +1187,16 @@ public class DreamService extends Service implements Window.Callback { @Override public void onViewAttachedToWindow(View v) { mDispatchAfterOnAttachedToWindow.run(); + + // Request the DreamOverlay be told to dream with dream's window parameters + // once the window has been attached. + mOverlayConnection.request(overlay -> { + try { + overlay.startDream(mWindow.getAttributes(), mOverlayCallback); + } catch (RemoteException e) { + Log.e(TAG, "could not send window attributes:" + e); + } + }); } @Override @@ -1185,16 +1209,6 @@ public class DreamService extends Service implements Window.Callback { } } }); - - // Request the DreamOverlay be told to dream with dream's window parameters once the service - // has connected. - mOverlayConnection.request(overlay -> { - try { - overlay.startDream(mWindow.getAttributes(), mOverlayCallback); - } catch (RemoteException e) { - Log.e(TAG, "could not send window attributes:" + e); - } - }); } private boolean getWindowFlagValue(int flag, boolean defaultValue) { diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index c87ba657bf46..26a49623fabf 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -294,6 +294,11 @@ <uses-permission android:name="android.permission.READ_PEOPLE_DATA" /> + <!-- Permission for dream overlay. --> + <uses-permission android:name="android.permission.BIND_DREAM_SERVICE" /> + + <uses-permission android:name="android.permission.BIND_APPWIDGET" /> + <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" /> @@ -673,6 +678,11 @@ android:name=".keyguard.KeyguardService" android:exported="true" /> + <service + android:name=".dreams.DreamOverlayService" + android:enabled="@bool/config_dreamOverlayServiceEnabled" + android:exported="true" /> + <activity android:name=".keyguard.WorkLockActivity" android:label="@string/accessibility_desc_work_lock" android:permission="android.permission.MANAGE_USERS" diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 9878e0dcb899..96433e554919 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -715,4 +715,22 @@ <!-- Flag to enable privacy dot views, it shall be true for normal case --> <bool name="config_enablePrivacyDot">true</bool> + <!-- The positions widgets can be in defined as View.Gravity constants --> + <integer-array name="config_dreamOverlayPositions"> + </integer-array> + + <!-- Widget components to show as dream overlays --> + <string-array name="config_dreamOverlayComponents" translatable="false"> + </string-array> + + <!-- Width percentage of dream overlay components --> + <item name="config_dreamOverlayComponentWidthPercent" translatable="false" format="float" + type="dimen">0.33</item> + + <!-- Height percentage of dream overlay components --> + <item name="config_dreamOverlayComponentHeightPercent" translatable="false" format="float" + type="dimen">0.25</item> + + <!-- Flag to enable dream overlay service and its registration --> + <bool name="config_dreamOverlayServiceEnabled">false</bool> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java index fe7911045dfc..33f07c716f95 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java @@ -21,6 +21,7 @@ import android.app.Service; import com.android.systemui.ImageWallpaper; import com.android.systemui.SystemUIService; import com.android.systemui.doze.DozeService; +import com.android.systemui.dreams.DreamOverlayService; import com.android.systemui.dump.SystemUIAuxiliaryDumpService; import com.android.systemui.keyguard.KeyguardService; import com.android.systemui.screenrecord.RecordingService; @@ -56,6 +57,12 @@ public abstract class DefaultServiceBinder { /** */ @Binds @IntoMap + @ClassKey(DreamOverlayService.class) + public abstract Service bindDreamOverlayService(DreamOverlayService service); + + /** */ + @Binds + @IntoMap @ClassKey(SystemUIService.class) public abstract Service bindSystemUIService(SystemUIService service); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java index 30844ccc877b..a5d4d80598c4 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java @@ -23,6 +23,8 @@ import com.android.systemui.SystemUI; import com.android.systemui.accessibility.SystemActions; import com.android.systemui.accessibility.WindowMagnification; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.dreams.DreamOverlayRegistrant; +import com.android.systemui.dreams.appwidgets.AppWidgetOverlayPrimer; import com.android.systemui.globalactions.GlobalActionsComponent; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.dagger.KeyguardModule; @@ -188,4 +190,18 @@ public abstract class SystemUIBinder { @IntoMap @ClassKey(HomeSoundEffectController.class) public abstract SystemUI bindHomeSoundEffectController(HomeSoundEffectController sysui); + + /** Inject into DreamOverlay. */ + @Binds + @IntoMap + @ClassKey(DreamOverlayRegistrant.class) + public abstract SystemUI bindDreamOverlayRegistrant( + DreamOverlayRegistrant dreamOverlayRegistrant); + + /** Inject into AppWidgetOverlayPrimer. */ + @Binds + @IntoMap + @ClassKey(AppWidgetOverlayPrimer.class) + public abstract SystemUI bindAppWidgetOverlayPrimer( + AppWidgetOverlayPrimer appWidgetOverlayPrimer); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 799c92c6ce18..4cecb3916f03 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -36,6 +36,7 @@ import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.dagger.DemoModeModule; import com.android.systemui.doze.dagger.DozeComponent; +import com.android.systemui.dreams.dagger.DreamModule; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlagManager; import com.android.systemui.flags.FeatureFlags; @@ -101,6 +102,7 @@ import dagger.Provides; AssistModule.class, ClockModule.class, CommunalModule.class, + DreamModule.class, ControlsModule.class, DemoModeModule.class, FalsingModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java new file mode 100644 index 000000000000..20c46da14e63 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2021 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.systemui.dreams; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.PatternMatcher; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.service.dreams.DreamService; +import android.service.dreams.IDreamManager; +import android.util.Log; + +import com.android.systemui.R; +import com.android.systemui.SystemUI; +import com.android.systemui.dagger.qualifiers.Main; + +import javax.inject.Inject; + +/** + * {@link DreamOverlayRegistrant} is responsible for telling system server that SystemUI should be + * the designated dream overlay component. + */ +public class DreamOverlayRegistrant extends SystemUI { + private static final String TAG = "DreamOverlayRegistrant"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final IDreamManager mDreamManager; + private final ComponentName mOverlayServiceComponent; + private final Resources mResources; + private boolean mCurrentRegisteredState = false; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) { + Log.d(TAG, "package changed receiver - onReceive"); + } + + registerOverlayService(); + } + }; + + private void registerOverlayService() { + // Check to see if the service has been disabled by the user. In this case, we should not + // proceed modifying the enabled setting. + final PackageManager packageManager = mContext.getPackageManager(); + final int enabledState = + packageManager.getComponentEnabledSetting(mOverlayServiceComponent); + + + // TODO(b/204626521): We should not have to set the component enabled setting if the + // enabled config flag is properly applied based on the RRO. + if (enabledState != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) { + final int overlayState = mResources.getBoolean(R.bool.config_dreamOverlayServiceEnabled) + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + + if (overlayState != enabledState) { + packageManager + .setComponentEnabledSetting(mOverlayServiceComponent, overlayState, 0); + } + } + + // The overlay service is only registered when its component setting is enabled. + boolean register = packageManager.getComponentEnabledSetting(mOverlayServiceComponent) + == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + + if (mCurrentRegisteredState == register) { + return; + } + + mCurrentRegisteredState = register; + + try { + if (DEBUG) { + Log.d(TAG, mCurrentRegisteredState + ? "registering dream overlay service:" + mOverlayServiceComponent + : "clearing dream overlay service"); + } + + mDreamManager.registerDreamOverlayService( + mCurrentRegisteredState ? mOverlayServiceComponent : null); + } catch (RemoteException e) { + Log.e(TAG, "could not register dream overlay service:" + e); + } + } + + @Inject + public DreamOverlayRegistrant(Context context, @Main Resources resources) { + super(context); + mResources = resources; + mDreamManager = IDreamManager.Stub.asInterface( + ServiceManager.getService(DreamService.DREAM_SERVICE)); + mOverlayServiceComponent = new ComponentName(mContext, DreamOverlayService.class); + } + + @Override + public void start() { + final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + filter.addDataSchemeSpecificPart(mOverlayServiceComponent.getPackageName(), + PatternMatcher.PATTERN_LITERAL); + // Note that we directly register the receiver here as data schemes are not supported by + // BroadcastDispatcher. + mContext.registerReceiver(mReceiver, filter); + + registerOverlayService(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java new file mode 100644 index 000000000000..8f0ea2fb2f87 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2021 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.systemui.dreams; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.ColorDrawable; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.policy.PhoneWindow; +import com.android.systemui.dagger.qualifiers.Main; + +import java.util.concurrent.Executor; + +import javax.inject.Inject; + +/** + * The {@link DreamOverlayService} is responsible for placing overlays on top of a dream. The + * dream reaches directly out to the service with a Window reference (via LayoutParams), which the + * service uses to insert its own child Window into the dream's parent Window. + */ +public class DreamOverlayService extends android.service.dreams.DreamOverlayService { + private static final String TAG = "DreamOverlayService"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // The Context is used to construct the hosting constraint layout and child overlay views. + private final Context mContext; + // The Executor ensures actions and ui updates happen on the same thread. + private final Executor mExecutor; + // The state controller informs the service of updates to the overlays present. + private final DreamOverlayStateController mStateController; + + // The window is populated once the dream informs the service it has begun dreaming. + private Window mWindow; + private ConstraintLayout mLayout; + + private final DreamOverlayStateController.Callback mOverlayStateCallback = + new DreamOverlayStateController.Callback() { + @Override + public void onOverlayChanged() { + mExecutor.execute(() -> reloadOverlaysLocked()); + } + }; + + // The service listens to view changes in order to declare that input occurring in areas outside + // the overlay should be passed through to the dream underneath. + private View.OnAttachStateChangeListener mRootViewAttachListener = + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + v.getViewTreeObserver() + .addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener); + } + + @Override + public void onViewDetachedFromWindow(View v) { + v.getViewTreeObserver() + .removeOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener); + } + }; + + // A hook into the internal inset calculation where we declare the overlays as the only + // touchable regions. + private ViewTreeObserver.OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = + new ViewTreeObserver.OnComputeInternalInsetsListener() { + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + if (mLayout != null) { + inoutInfo.setTouchableInsets( + ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + final Region region = new Region(); + for (int i = 0; i < mLayout.getChildCount(); i++) { + View child = mLayout.getChildAt(i); + final Rect rect = new Rect(); + child.getGlobalVisibleRect(rect); + region.op(rect, Region.Op.UNION); + } + + inoutInfo.touchableRegion.set(region); + } + } + }; + + @Override + public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) { + mExecutor.execute(() -> addOverlayWindowLocked(layoutParams)); + } + + private void reloadOverlaysLocked() { + if (mLayout == null) { + return; + } + mLayout.removeAllViews(); + for (OverlayProvider overlayProvider : mStateController.getOverlays()) { + addOverlay(overlayProvider); + } + } + + /** + * Inserts {@link Window} to host dream overlays into the dream's parent window. Must be called + * from the main executing thread. The window attributes closely mirror those that are set by + * the {@link android.service.dreams.DreamService} on the dream Window. + * @param layoutParams The {@link android.view.WindowManager.LayoutParams} which allow inserting + * into the dream window. + */ + private void addOverlayWindowLocked(WindowManager.LayoutParams layoutParams) { + mWindow = new PhoneWindow(mContext); + mWindow.setAttributes(layoutParams); + mWindow.setWindowManager(null, layoutParams.token, "DreamOverlay", true); + + mWindow.setBackgroundDrawable(new ColorDrawable(0)); + + mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + mWindow.requestFeature(Window.FEATURE_NO_TITLE); + // Hide all insets when the dream is showing + mWindow.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars()); + mWindow.setDecorFitsSystemWindows(false); + + if (DEBUG) { + Log.d(TAG, "adding overlay window to dream"); + } + + mLayout = new ConstraintLayout(mContext); + mLayout.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + mLayout.addOnAttachStateChangeListener(mRootViewAttachListener); + mWindow.setContentView(mLayout); + + final WindowManager windowManager = mContext.getSystemService(WindowManager.class); + windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes()); + mExecutor.execute(this::reloadOverlaysLocked); + } + + @VisibleForTesting + protected void addOverlay(OverlayProvider provider) { + provider.onCreateOverlay(mContext, + (view, layoutParams) -> { + // Always move UI related work to the main thread. + mExecutor.execute(() -> { + if (mLayout == null) { + return; + } + + mLayout.addView(view, layoutParams); + }); + }, + () -> { + // The Callback is set on the main thread. + mExecutor.execute(() -> { + requestExit(); + }); + }); + } + + @Inject + public DreamOverlayService(Context context, @Main Executor executor, + DreamOverlayStateController overlayStateController) { + mContext = context; + mExecutor = executor; + mStateController = overlayStateController; + mStateController.addCallback(mOverlayStateCallback); + } + + @Override + public void onDestroy() { + mStateController.removeCallback(mOverlayStateCallback); + super.onDestroy(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java new file mode 100644 index 000000000000..d248a9e174f5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2021 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.systemui.dreams; + +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.policy.CallbackController; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Objects; + +import javax.inject.Inject; + +/** + * {@link DreamOverlayStateController} is the source of truth for Dream overlay configurations. + * Clients can register as listeners for changes to the overlay composition and can query for the + * overlays on-demand. + */ +@SysUISingleton +public class DreamOverlayStateController implements + CallbackController<DreamOverlayStateController.Callback> { + // A counter for guaranteeing unique overlay tokens within the scope of this state controller. + private int mNextOverlayTokenId = 0; + + /** + * {@link OverlayToken} provides a unique key for identifying {@link OverlayProvider} + * instances registered with {@link DreamOverlayStateController}. + */ + public static class OverlayToken { + private final int mId; + + private OverlayToken(int id) { + mId = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof OverlayToken)) return false; + OverlayToken that = (OverlayToken) o; + return mId == that.mId; + } + + @Override + public int hashCode() { + return Objects.hash(mId); + } + } + + /** + * Callback for dream overlay events. + */ + public interface Callback { + /** + * Called when the visibility of the communal view changes. + */ + default void onOverlayChanged() { + } + } + + private final ArrayList<Callback> mCallbacks = new ArrayList<>(); + private final HashMap<OverlayToken, OverlayProvider> mOverlays = new HashMap<>(); + + @VisibleForTesting + @Inject + public DreamOverlayStateController() { + } + + /** + * Adds an overlay to be presented on top of dreams. + * @param provider The {@link OverlayProvider} providing the dream. + * @return The {@link OverlayToken} tied to the supplied {@link OverlayProvider}. + */ + public OverlayToken addOverlay(OverlayProvider provider) { + final OverlayToken token = new OverlayToken(mNextOverlayTokenId++); + mOverlays.put(token, provider); + notifyCallbacks(); + return token; + } + + /** + * Removes an overlay from being shown on dreams. + * @param token The {@link OverlayToken} associated with the {@link OverlayProvider} to be + * removed. + * @return The removed {@link OverlayProvider}, {@code null} if not found. + */ + public OverlayProvider removeOverlay(OverlayToken token) { + final OverlayProvider removedOverlay = mOverlays.remove(token); + + if (removedOverlay != null) { + notifyCallbacks(); + } + + return removedOverlay; + } + + private void notifyCallbacks() { + for (Callback callback : mCallbacks) { + callback.onOverlayChanged(); + } + } + + @Override + public void addCallback(@NonNull Callback callback) { + Objects.requireNonNull(callback, "Callback must not be null. b/128895449"); + if (mCallbacks.contains(callback)) { + return; + } + + mCallbacks.add(callback); + + if (mOverlays.isEmpty()) { + return; + } + + callback.onOverlayChanged(); + } + + @Override + public void removeCallback(@NonNull Callback callback) { + Objects.requireNonNull(callback, "Callback must not be null. b/128895449"); + mCallbacks.remove(callback); + } + + /** + * Returns all registered {@link OverlayProvider} instances. + * @return A collection of {@link OverlayProvider}. + */ + public Collection<OverlayProvider> getOverlays() { + return mOverlays.values(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/OverlayHost.java b/packages/SystemUI/src/com/android/systemui/dreams/OverlayHost.java new file mode 100644 index 000000000000..08f0f3507e3e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/OverlayHost.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 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.systemui.dreams; + +import android.view.View; + +/** + * A collection of interfaces related to hosting an overlay. + */ +public abstract class OverlayHost { + /** + * An interface for the callback from the overlay provider to indicate when the overlay is + * ready. + */ + public interface CreationCallback { + /** + * Called to inform the overlay view is ready to be placed within the visual space. + * @param view The view representing the overlay. + * @param layoutParams The parameters to create the view with. + */ + void onCreated(View view, OverlayHostView.LayoutParams layoutParams); + } + + /** + * An interface for the callback from the overlay provider to signal interactions in the + * overlay. + */ + public interface InteractionCallback { + /** + * Called to signal the calling overlay would like to exit the dream. + */ + void onExit(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/OverlayHostView.java b/packages/SystemUI/src/com/android/systemui/dreams/OverlayHostView.java new file mode 100644 index 000000000000..7870426c78f1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/OverlayHostView.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 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.systemui.dreams; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.constraintlayout.widget.ConstraintLayout; + +/** + * {@link OverlayHostView} is the container view for housing overlays ontop of a dream. + */ +public class OverlayHostView extends ConstraintLayout { + public OverlayHostView(Context context) { + super(context, null); + } + + public OverlayHostView(Context context, AttributeSet attrs) { + super(context, attrs, 0); + } + + public OverlayHostView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr, 0); + } + + public OverlayHostView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/OverlayProvider.java b/packages/SystemUI/src/com/android/systemui/dreams/OverlayProvider.java new file mode 100644 index 000000000000..f20802527d73 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/OverlayProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 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.systemui.dreams; + +import android.content.Context; + +/** + * {@link OverlayProvider} is an interface for defining entities that can supply overlays to show + * over a dream. Presentation components such as the {@link DreamOverlayService} supply + * implementations with the necessary context for constructing such overlays. + */ +public interface OverlayProvider { + /** + * Called when the {@link OverlayHost} requests the associated overlay be produced. + * + * @param context The {@link Context} used to construct the view. + * @param creationCallback The callback to inform when the overlay has been created. + * @param interactionCallback The callback to inform when the overlay has been interacted with. + */ + void onCreateOverlay(Context context, OverlayHost.CreationCallback creationCallback, + OverlayHost.InteractionCallback interactionCallback); +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimer.java b/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimer.java new file mode 100644 index 000000000000..a0c7c29e0191 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimer.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2021 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.systemui.dreams.appwidgets; + +import android.content.ComponentName; +import android.content.Context; +import android.content.res.Resources; +import android.view.Gravity; + +import androidx.constraintlayout.widget.ConstraintSet; + +import com.android.systemui.R; +import com.android.systemui.SystemUI; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.dreams.OverlayHostView; +import com.android.systemui.dreams.dagger.AppWidgetOverlayComponent; + +import javax.inject.Inject; + +/** + * {@link AppWidgetOverlayPrimer} reads the configured App Widget Overlay from resources on start + * and populates them into the {@link DreamOverlayStateController}. + */ +public class AppWidgetOverlayPrimer extends SystemUI { + private final Resources mResources; + private final DreamOverlayStateController mDreamOverlayStateController; + private final AppWidgetOverlayComponent.Factory mComponentFactory; + + @Inject + public AppWidgetOverlayPrimer(Context context, @Main Resources resources, + DreamOverlayStateController overlayStateController, + AppWidgetOverlayComponent.Factory appWidgetOverlayFactory) { + super(context); + mResources = resources; + mDreamOverlayStateController = overlayStateController; + mComponentFactory = appWidgetOverlayFactory; + } + + @Override + public void start() { + } + + @Override + protected void onBootCompleted() { + super.onBootCompleted(); + loadDefaultWidgets(); + } + + /** + * Generates the {@link OverlayHostView.LayoutParams} for a given gravity. Default dimension + * constraints are also included in the params. + * @param gravity The gravity for the layout as defined by {@link Gravity}. + * @param resources The resourcs from which default dimensions will be extracted from. + * @return {@link OverlayHostView.LayoutParams} representing the provided gravity and default + * parameters. + */ + private static OverlayHostView.LayoutParams getLayoutParams(int gravity, Resources resources) { + final OverlayHostView.LayoutParams params = new OverlayHostView.LayoutParams( + OverlayHostView.LayoutParams.MATCH_CONSTRAINT, + OverlayHostView.LayoutParams.MATCH_CONSTRAINT); + + if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { + params.bottomToBottom = ConstraintSet.PARENT_ID; + } + + if ((gravity & Gravity.TOP) == Gravity.TOP) { + params.topToTop = ConstraintSet.PARENT_ID; + } + + if ((gravity & Gravity.END) == Gravity.END) { + params.endToEnd = ConstraintSet.PARENT_ID; + } + + if ((gravity & Gravity.START) == Gravity.START) { + params.startToStart = ConstraintSet.PARENT_ID; + } + + // For now, apply the same sizing constraints on every widget. + params.matchConstraintPercentHeight = + resources.getFloat(R.dimen.config_dreamOverlayComponentHeightPercent); + params.matchConstraintPercentWidth = + resources.getFloat(R.dimen.config_dreamOverlayComponentWidthPercent); + + return params; + } + + + /** + * Helper method for loading widgets based on configuration. + */ + private void loadDefaultWidgets() { + final int[] positions = mResources.getIntArray(R.array.config_dreamOverlayPositions); + final String[] components = + mResources.getStringArray(R.array.config_dreamOverlayComponents); + + for (int i = 0; i < Math.min(positions.length, components.length); i++) { + final AppWidgetOverlayComponent component = mComponentFactory.build( + ComponentName.unflattenFromString(components[i]), + getLayoutParams(positions[i], mResources)); + + mDreamOverlayStateController.addOverlay(component.getAppWidgetOverlayProvider()); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayProvider.java b/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayProvider.java new file mode 100644 index 000000000000..a635d3f740cf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayProvider.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 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.systemui.dreams.appwidgets; + +import android.appwidget.AppWidgetHostView; +import android.content.ComponentName; +import android.content.Context; +import android.util.Log; +import android.widget.RemoteViews; + +import com.android.systemui.dreams.OverlayHost; +import com.android.systemui.dreams.OverlayHostView; +import com.android.systemui.dreams.OverlayProvider; +import com.android.systemui.plugins.ActivityStarter; + +import javax.inject.Inject; + +/** + * {@link AppWidgetOverlayProvider} is an implementation of {@link OverlayProvider} for providing + * app widget-based overlays. + */ +public class AppWidgetOverlayProvider implements OverlayProvider { + private static final String TAG = "AppWdgtOverlayProvider"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final ActivityStarter mActivityStarter; + private final AppWidgetProvider mAppWidgetProvider; + private final ComponentName mComponentName; + private final OverlayHostView.LayoutParams mLayoutParams; + + @Inject + public AppWidgetOverlayProvider(ActivityStarter activityStarter, + ComponentName componentName, AppWidgetProvider widgetProvider, + OverlayHostView.LayoutParams layoutParams) { + mActivityStarter = activityStarter; + mComponentName = componentName; + mAppWidgetProvider = widgetProvider; + mLayoutParams = layoutParams; + } + + @Override + public void onCreateOverlay(Context context, OverlayHost.CreationCallback creationCallback, + OverlayHost.InteractionCallback interactionCallback) { + final AppWidgetHostView widget = mAppWidgetProvider.getWidget(mComponentName); + + if (widget == null) { + Log.e(TAG, "could not create widget"); + return; + } + + widget.setInteractionHandler((view, pendingIntent, response) -> { + if (pendingIntent.isActivity()) { + if (DEBUG) { + Log.d(TAG, "launching pending intent from app widget:" + mComponentName); + } + interactionCallback.onExit(); + mActivityStarter.startPendingIntentDismissingKeyguard(pendingIntent, + null /*intentSentUiThreadCallback*/, view); + return true; + } else { + return RemoteViews.startPendingIntent(view, pendingIntent, + response.getLaunchOptions(view)); + } + }); + + creationCallback.onCreated(widget, mLayoutParams); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetProvider.java b/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetProvider.java new file mode 100644 index 000000000000..d1da1e691ed6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetProvider.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021 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.systemui.dreams.appwidgets; + +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.res.Resources; +import android.util.Log; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; + +import java.util.List; + +import javax.inject.Inject; + +/** + * {@link AppWidgetProvider} is a singleton for accessing app widgets within SystemUI. This + * consolidates resources such as the App Widget Host across potentially multiple + * {@link AppWidgetOverlayProvider} instances and other usages. + */ +@SysUISingleton +public class AppWidgetProvider { + private static final String TAG = "AppWidgetProvider"; + public static final int APP_WIDGET_HOST_ID = 1025; + + private final Context mContext; + private final AppWidgetManager mAppWidgetManager; + private final AppWidgetHost mAppWidgetHost; + private final Resources mResources; + + @Inject + public AppWidgetProvider(Context context, @Main Resources resources) { + mContext = context; + mResources = resources; + mAppWidgetManager = android.appwidget.AppWidgetManager.getInstance(context); + mAppWidgetHost = new AppWidgetHost(context, APP_WIDGET_HOST_ID); + mAppWidgetHost.startListening(); + } + + /** + * Returns an {@link AppWidgetHostView} associated with a given {@link ComponentName}. + * @param component The {@link ComponentName} of the target {@link AppWidgetHostView}. + * @return The {@link AppWidgetHostView} or {@code null} on error. + */ + public AppWidgetHostView getWidget(ComponentName component) { + final List<AppWidgetProviderInfo> appWidgetInfos = + mAppWidgetManager.getInstalledProviders(); + + for (AppWidgetProviderInfo widgetInfo : appWidgetInfos) { + if (widgetInfo.provider.equals(component)) { + final int widgetId = mAppWidgetHost.allocateAppWidgetId(); + + boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(widgetId, + widgetInfo.provider); + + if (!success) { + Log.e(TAG, "could not bind to app widget:" + component); + break; + } + + final AppWidgetHostView appWidgetView = + mAppWidgetHost.createView(mContext, widgetId, widgetInfo); + + if (appWidgetView != null) { + // Register a layout change listener to update the widget on any sizing changes. + appWidgetView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + final float density = mResources.getDisplayMetrics().density; + final int height = Math.round((bottom - top) / density); + final int width = Math.round((right - left) / density); + appWidgetView.updateAppWidgetSize(null, width, height, width, + height); + }); + } + + return appWidgetView; + } + } + + return null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/AppWidgetOverlayComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/AppWidgetOverlayComponent.java new file mode 100644 index 000000000000..3103057be209 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/AppWidgetOverlayComponent.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 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.systemui.dreams.dagger; + +import android.content.ComponentName; + +import com.android.systemui.dreams.OverlayHostView; +import com.android.systemui.dreams.appwidgets.AppWidgetOverlayProvider; + +import dagger.BindsInstance; +import dagger.Subcomponent; + +/** */ +@Subcomponent +public interface AppWidgetOverlayComponent { + /** */ + @Subcomponent.Factory + interface Factory { + AppWidgetOverlayComponent build(@BindsInstance ComponentName component, + @BindsInstance OverlayHostView.LayoutParams layoutParams); + } + + /** Builds a {@link AppWidgetOverlayProvider}. */ + AppWidgetOverlayProvider getAppWidgetOverlayProvider(); +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java new file mode 100644 index 000000000000..7bf2361e471c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 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.systemui.dreams.dagger; + +import dagger.Module; + +/** + * Dagger Module providing Communal-related functionality. + */ +@Module(subcomponents = { + AppWidgetOverlayComponent.class, +}) +public interface DreamModule { +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/AppWidgetOverlayProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/AppWidgetOverlayProviderTest.java new file mode 100644 index 000000000000..0fc306b99b65 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/AppWidgetOverlayProviderTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2021 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.systemui.dreams; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.isNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetHostView; +import android.content.ComponentName; +import android.testing.AndroidTestingRunner; +import android.widget.RemoteViews; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.SysuiTestableContext; +import com.android.systemui.dreams.appwidgets.AppWidgetOverlayProvider; +import com.android.systemui.dreams.appwidgets.AppWidgetProvider; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.utils.leaks.LeakCheckedTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class AppWidgetOverlayProviderTest extends SysuiTestCase { + @Mock + ActivityStarter mActivityStarter; + + @Mock + ComponentName mComponentName; + + @Mock + AppWidgetProvider mAppWidgetProvider; + + @Mock + AppWidgetHostView mAppWidgetHostView; + + @Mock + OverlayHost.CreationCallback mCreationCallback; + + @Mock + OverlayHost.InteractionCallback mInteractionCallback; + + @Mock + PendingIntent mPendingIntent; + + @Mock + RemoteViews.RemoteResponse mRemoteResponse; + + AppWidgetOverlayProvider mOverlayProvider; + + RemoteViews.InteractionHandler mInteractionHandler; + + @Rule + public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck(); + + @Rule + public SysuiTestableContext mContext = new SysuiTestableContext( + InstrumentationRegistry.getContext(), mLeakCheck); + + OverlayHostView.LayoutParams mLayoutParams = new OverlayHostView.LayoutParams( + OverlayHostView.LayoutParams.MATCH_PARENT, OverlayHostView.LayoutParams.MATCH_PARENT); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mPendingIntent.isActivity()).thenReturn(true); + when(mAppWidgetProvider.getWidget(mComponentName)).thenReturn(mAppWidgetHostView); + + mOverlayProvider = new AppWidgetOverlayProvider( + mActivityStarter, + mComponentName, + mAppWidgetProvider, + mLayoutParams + ); + + final ArgumentCaptor<RemoteViews.InteractionHandler> creationCallbackCapture = + ArgumentCaptor.forClass(RemoteViews.InteractionHandler.class); + + mOverlayProvider.onCreateOverlay(mContext, mCreationCallback, mInteractionCallback); + verify(mAppWidgetHostView, times(1)) + .setInteractionHandler(creationCallbackCapture.capture()); + mInteractionHandler = creationCallbackCapture.getValue(); + } + + @Test + public void testWidgetBringup() { + // Make sure widget was requested. + verify(mAppWidgetProvider, times(1)).getWidget(eq(mComponentName)); + + // Make sure widget was returned to callback. + verify(mCreationCallback, times(1)).onCreated(eq(mAppWidgetHostView), + eq(mLayoutParams)); + } + + @Test + public void testWidgetInteraction() { + // Trigger interaction. + mInteractionHandler.onInteraction(mAppWidgetHostView, mPendingIntent, + mRemoteResponse); + + // Ensure activity is started. + verify(mActivityStarter, times(1)) + .startPendingIntentDismissingKeyguard(eq(mPendingIntent), isNull(), + eq(mAppWidgetHostView)); + // Verify exit is requested. + verify(mInteractionCallback, times(1)).onExit(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java new file mode 100644 index 000000000000..53bfeee9135a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2021 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.systemui.dreams; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Intent; +import android.os.IBinder; +import android.service.dreams.IDreamOverlay; +import android.service.dreams.IDreamOverlayCallback; +import android.testing.AndroidTestingRunner; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.WindowManagerImpl; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.SysuiTestableContext; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.utils.leaks.LeakCheckedTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DreamOverlayServiceTest extends SysuiTestCase { + private FakeSystemClock mFakeSystemClock = new FakeSystemClock(); + private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); + + @Rule + public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck(); + + @Rule + public SysuiTestableContext mContext = new SysuiTestableContext( + InstrumentationRegistry.getContext(), mLeakCheck); + + WindowManager.LayoutParams mWindowParams = new WindowManager.LayoutParams(); + + @Mock + IDreamOverlayCallback mDreamOverlayCallback; + + @Mock + WindowManagerImpl mWindowManager; + + @Mock + OverlayProvider mProvider; + + @Mock + DreamOverlayStateController mDreamOverlayStateController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext.addMockSystemService(WindowManager.class, mWindowManager); + } + + @Test + public void testInteraction() throws Exception { + final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor, + mDreamOverlayStateController); + final IBinder proxy = service.onBind(new Intent()); + final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); + clearInvocations(mWindowManager); + + // Inform the overlay service of dream starting. + overlay.startDream(mWindowParams, mDreamOverlayCallback); + mMainExecutor.runAllReady(); + verify(mWindowManager).addView(any(), any()); + + // Add overlay. + service.addOverlay(mProvider); + mMainExecutor.runAllReady(); + + final ArgumentCaptor<OverlayHost.CreationCallback> creationCallbackCapture = + ArgumentCaptor.forClass(OverlayHost.CreationCallback.class); + final ArgumentCaptor<OverlayHost.InteractionCallback> interactionCallbackCapture = + ArgumentCaptor.forClass(OverlayHost.InteractionCallback.class); + + // Ensure overlay provider is asked to create view. + verify(mProvider).onCreateOverlay(any(), creationCallbackCapture.capture(), + interactionCallbackCapture.capture()); + mMainExecutor.runAllReady(); + + // Inform service of overlay view creation. + final View view = new View(mContext); + creationCallbackCapture.getValue().onCreated(view, new ConstraintLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + )); + + // Ask service to exit. + interactionCallbackCapture.getValue().onExit(); + mMainExecutor.runAllReady(); + + // Ensure service informs dream host of exit. + verify(mDreamOverlayCallback).onExitRequested(); + } + + @Test + public void testListening() throws Exception { + final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor, + mDreamOverlayStateController); + + final IBinder proxy = service.onBind(new Intent()); + final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); + + // Inform the overlay service of dream starting. + overlay.startDream(mWindowParams, mDreamOverlayCallback); + mMainExecutor.runAllReady(); + + // Verify overlay service registered as listener with DreamOverlayStateController + // and inform callback of addition. + final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCapture = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + + verify(mDreamOverlayStateController).addCallback(callbackCapture.capture()); + when(mDreamOverlayStateController.getOverlays()).thenReturn(Arrays.asList(mProvider)); + callbackCapture.getValue().onOverlayChanged(); + mMainExecutor.runAllReady(); + + // Verify provider is asked to create overlay. + verify(mProvider).onCreateOverlay(any(), any(), any()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java new file mode 100644 index 000000000000..4e97be37603e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2021 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.systemui.dreams; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Collection; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DreamOverlayStateControllerTest extends SysuiTestCase { + @Mock + DreamOverlayStateController.Callback mCallback; + + @Mock + OverlayProvider mProvider; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testCallback() { + final DreamOverlayStateController stateController = new DreamOverlayStateController(); + stateController.addCallback(mCallback); + + // Add overlay and verify callback is notified. + final DreamOverlayStateController.OverlayToken token = + stateController.addOverlay(mProvider); + + verify(mCallback, times(1)).onOverlayChanged(); + + final Collection<OverlayProvider> providers = stateController.getOverlays(); + assertEquals(providers.size(), 1); + assertTrue(providers.contains(mProvider)); + + clearInvocations(mCallback); + + // Remove overlay and verify callback is notified. + stateController.removeOverlay(token); + verify(mCallback, times(1)).onOverlayChanged(); + assertTrue(providers.isEmpty()); + } + + @Test + public void testNotifyOnCallbackAdd() { + final DreamOverlayStateController stateController = new DreamOverlayStateController(); + final DreamOverlayStateController.OverlayToken token = + stateController.addOverlay(mProvider); + + // Verify callback occurs on add when an overlay is already present. + stateController.addCallback(mCallback); + verify(mCallback, times(1)).onOverlayChanged(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimerTest.java new file mode 100644 index 000000000000..2e5b1653584b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimerTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2021 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.systemui.dreams.appwidgets; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.res.Resources; +import android.testing.AndroidTestingRunner; +import android.view.Gravity; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.SysuiTestableContext; +import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.dreams.dagger.AppWidgetOverlayComponent; +import com.android.systemui.utils.leaks.LeakCheckedTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class AppWidgetOverlayPrimerTest extends SysuiTestCase { + @Rule + public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck(); + + @Rule + public SysuiTestableContext mContext = new SysuiTestableContext( + InstrumentationRegistry.getContext(), mLeakCheck); + + @Mock + Resources mResources; + + @Mock + AppWidgetOverlayComponent mAppWidgetOverlayComponent1; + @Mock + AppWidgetOverlayComponent mAppWidgetOverlayComponent2; + + @Mock + AppWidgetOverlayProvider mAppWidgetOverlayProvider1; + + @Mock + AppWidgetOverlayProvider mAppWidgetOverlayProvider2; + + final ComponentName mAppOverlayComponent1 = + ComponentName.unflattenFromString("com.foo.bar/.Baz"); + final ComponentName mAppOverlayComponent2 = + ComponentName.unflattenFromString("com.foo.bar/.Baz2"); + + final int mAppOverlayGravity1 = Gravity.BOTTOM | Gravity.START; + final int mAppOverlayGravity2 = Gravity.BOTTOM | Gravity.END; + + final String[] mComponents = new String[]{mAppOverlayComponent1.flattenToString(), + mAppOverlayComponent2.flattenToString() }; + final int[] mPositions = new int[]{ mAppOverlayGravity1, mAppOverlayGravity2 }; + + @Mock + DreamOverlayStateController mDreamOverlayStateController; + + @Mock + AppWidgetOverlayComponent.Factory mAppWidgetOverlayProviderFactory; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mAppWidgetOverlayProviderFactory.build(eq(mAppOverlayComponent1), any())) + .thenReturn(mAppWidgetOverlayComponent1); + when(mAppWidgetOverlayComponent1.getAppWidgetOverlayProvider()) + .thenReturn(mAppWidgetOverlayProvider1); + when(mAppWidgetOverlayProviderFactory.build(eq(mAppOverlayComponent2), any())) + .thenReturn(mAppWidgetOverlayComponent2); + when(mAppWidgetOverlayComponent2.getAppWidgetOverlayProvider()) + .thenReturn(mAppWidgetOverlayProvider2); + when(mResources.getIntArray(R.array.config_dreamOverlayPositions)).thenReturn(mPositions); + when(mResources.getStringArray(R.array.config_dreamOverlayComponents)) + .thenReturn(mComponents); + } + + @Test + public void testLoading() { + final AppWidgetOverlayPrimer primer = new AppWidgetOverlayPrimer(mContext, + mResources, + mDreamOverlayStateController, + mAppWidgetOverlayProviderFactory); + + // Inform primer to begin. + primer.onBootCompleted(); + + // Verify the first component is added to the state controller with the proper position. + { + final ArgumentCaptor<ConstraintLayout.LayoutParams> layoutParamsArgumentCaptor = + ArgumentCaptor.forClass(ConstraintLayout.LayoutParams.class); + verify(mAppWidgetOverlayProviderFactory, times(1)).build(eq(mAppOverlayComponent1), + layoutParamsArgumentCaptor.capture()); + + assertEquals(layoutParamsArgumentCaptor.getValue().startToStart, + ConstraintLayout.LayoutParams.PARENT_ID); + assertEquals(layoutParamsArgumentCaptor.getValue().bottomToBottom, + ConstraintLayout.LayoutParams.PARENT_ID); + + verify(mDreamOverlayStateController, times(1)) + .addOverlay(eq(mAppWidgetOverlayProvider1)); + } + + // Verify the second component is added to the state controller with the proper position. + { + final ArgumentCaptor<ConstraintLayout.LayoutParams> layoutParamsArgumentCaptor = + ArgumentCaptor.forClass(ConstraintLayout.LayoutParams.class); + verify(mAppWidgetOverlayProviderFactory, times(1)).build(eq(mAppOverlayComponent2), + layoutParamsArgumentCaptor.capture()); + + assertEquals(layoutParamsArgumentCaptor.getValue().endToEnd, + ConstraintLayout.LayoutParams.PARENT_ID); + assertEquals(layoutParamsArgumentCaptor.getValue().bottomToBottom, + ConstraintLayout.LayoutParams.PARENT_ID); + verify(mDreamOverlayStateController, times(1)) + .addOverlay(eq(mAppWidgetOverlayProvider1)); + } + } + + @Test + public void testNoComponents() { + when(mResources.getStringArray(R.array.config_dreamOverlayComponents)) + .thenReturn(new String[]{}); + + final AppWidgetOverlayPrimer primer = new AppWidgetOverlayPrimer(mContext, + mResources, + mDreamOverlayStateController, + mAppWidgetOverlayProviderFactory); + + // Inform primer to begin. + primer.onBootCompleted(); + + + // Make sure there is no request to add a widget if no components are specified by the + // product. + verify(mAppWidgetOverlayProviderFactory, never()).build(any(), any()); + verify(mDreamOverlayStateController, never()).addOverlay(any()); + } + + @Test + public void testNoPositions() { + when(mResources.getIntArray(R.array.config_dreamOverlayPositions)) + .thenReturn(new int[]{}); + + final AppWidgetOverlayPrimer primer = new AppWidgetOverlayPrimer(mContext, + mResources, + mDreamOverlayStateController, + mAppWidgetOverlayProviderFactory); + + primer.onBootCompleted(); + + // Make sure there is no request to add a widget if no positions are specified by the + // product. + verify(mAppWidgetOverlayProviderFactory, never()).build(any(), any()); + verify(mDreamOverlayStateController, never()).addOverlay(any()); + } +} diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index b966ed1af8b6..00eb0d3256df 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -88,6 +88,7 @@ import android.app.IApplicationThread; import android.app.PendingIntent; import android.app.ProfilerInfo; import android.app.WaitResult; +import android.app.WindowConfiguration; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; @@ -1745,6 +1746,16 @@ class ActivityStarter { mIntent.setFlags(mLaunchFlags); + boolean dreamStopping = false; + + for (ActivityRecord stoppingActivity : mSupervisor.mStoppingActivities) { + if (stoppingActivity.getActivityType() + == WindowConfiguration.ACTIVITY_TYPE_DREAM) { + dreamStopping = true; + break; + } + } + // Get top task at beginning because the order may be changed when reusing existing task. final Task prevTopTask = mPreferredTaskDisplayArea.getFocusedRootTask(); final Task reusedTask = getReusableTask(); @@ -1805,7 +1816,8 @@ class ActivityStarter { if (!mAvoidMoveToFront && mDoResume) { mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask); - if (!mTargetRootTask.isTopRootTaskInDisplayArea() && mService.mInternal.isDreaming()) { + if (!mTargetRootTask.isTopRootTaskInDisplayArea() && mService.mInternal.isDreaming() + && !dreamStopping) { // Launching underneath dream activity (fullscreen, always-on-top). Run the launch- // -behind transition so the Activity gets created and starts in visible state. mLaunchTaskBehind = true; |