From 74e50739dd595c9c3d676322d1a356a7b65e965f Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Wed, 29 Sep 2021 12:08:22 -0700 Subject: [PATCH 1/8] Correct launch task behind when dream is present. Logic was introduced to launch any activity behind the current task when a dream was present. This is problematic when a dream is stopping as the top task will not be the dream and the new activity will be launched behind the task previously behind the dream. This changelist addresses this corner case by checking the stopped state of the DreamActivity before deciding to place the new activity behind. Test: CtsWindowManagerDeviceTestCases Bug: 200752015 Change-Id: I050eac31e1a40c885b24565f5a4c3bb1a053c2a5 --- .../com/android/server/wm/ActivityStarter.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index b966ed1af8b65..00eb0d3256df6 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; From bfc555ea4a7c94d922cd605c85eecb90a7c6aa2d Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Fri, 29 Oct 2021 17:04:15 -0700 Subject: [PATCH 2/8] Pass Window to DreamOverlayService when attached. Previously, the DreamService would send the window attributes to the DreamOverlayService. However, it is possible that the Window isn't fully associated with a token at this stage. This change ensures the Window is only passed when the token is ready, tracked by when the DecorView is attached. Bug: 204605351 Test: atest DreamOverlayTest Change-Id: Ib1d25ba55edb648739aca22d861e9522102d0668 --- .../android/service/dreams/DreamService.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 096595f30b05b..9e351691310d1 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1173,6 +1173,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 +1195,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) { From 16b5913cb425455cba2c079b8442f06bbc777c72 Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Thu, 4 Nov 2021 00:28:10 -0700 Subject: [PATCH 3/8] Consolidate dream overlay binding logic. This changelist moves the binding logic to the dream overlay service into the OverlayConnection class within DreamService. This helps better track the binding lifecycle. Test: manual Test: atest DreamOverlayTest Bug: 205077932 Change-Id: I92959a9ecb53533d998abd711f960ef4fe0628d0 --- .../android/service/dreams/DreamService.java | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 9e351691310d1..3ab6907557da4 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> 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 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 From 4219dcc2c5bbe070d1cc605c67a4ace658f49e0d Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Tue, 2 Nov 2021 12:14:45 -0700 Subject: [PATCH 4/8] Add Stub implementation for DreamOverlayService. This changelist introduces a stub implementation for DreamOverlayService for test purposes. It also allows DreamManager to set the DreamOverlay. Bug: 204890435 Test: atest DreamOverlayTest Change-Id: Ia59e70d08f76d5928e72e815c990195934292ad5 --- core/api/test-current.txt | 14 +++- core/java/android/app/DreamManager.java | 25 +++++- .../service/dreams/DreamOverlayService.java | 77 +++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 core/java/android/service/dreams/DreamOverlayService.java diff --git a/core/api/test-current.txt b/core/api/test-current.txt index d5ceafb6ca3d3..226deba694a5b 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 f23681373f532..34ae08fd9b9a7 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(), dreams); + mService.setDreamComponentsForUser(mContext.getUserId(), + dreamComponent != null ? dreams : null); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Sets the active dream on the device to be "dreamComponent". + * + *

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.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 0000000000000..50f9d8ac29581 --- /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); + } + } +} From 92fdf1d25bad9833995f119f2d0bf4e65c4c995e Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Thu, 30 Sep 2021 16:24:27 -0700 Subject: [PATCH 5/8] SystemUI IDreamOverlay Implementation. This changelist implements IDreamOverlay in the form of the DreamOverlayService. DreamOverlayRegistrant is responsible for registering this service with the system as the dream overlay component. This changelist also introduces a series of interfaces, such as OverlayProvider, which define the entities DreamOverlayService interacts with the generate overlay content. Bug: 201676597 Test: atest DreamOverlayServiceTest Change-Id: Idb14172672642580fdfa042449b6cad4bf68969b --- packages/SystemUI/AndroidManifest.xml | 5 + packages/SystemUI/res/values/config.xml | 3 + .../systemui/dagger/DefaultServiceBinder.java | 7 + .../systemui/dagger/SystemUIBinder.java | 8 + .../dreams/DreamOverlayRegistrant.java | 128 ++++++++++++++ .../systemui/dreams/DreamOverlayService.java | 165 ++++++++++++++++++ .../android/systemui/dreams/OverlayHost.java | 48 +++++ .../systemui/dreams/OverlayHostView.java | 43 +++++ .../systemui/dreams/OverlayProvider.java | 36 ++++ .../dreams/DreamOverlayServiceTest.java | 120 +++++++++++++ 10 files changed, 563 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/OverlayHost.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/OverlayHostView.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/OverlayProvider.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index c87ba657bf466..25b73ab401a8a 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -673,6 +673,11 @@ android:name=".keyguard.KeyguardService" android:exported="true" /> + + true + + false + diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java index fe7911045dfcb..33f07c716f958 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; @@ -53,6 +54,12 @@ public abstract class DefaultServiceBinder { @ClassKey(KeyguardService.class) public abstract Service bindKeyguardService(KeyguardService service); + /** */ + @Binds + @IntoMap + @ClassKey(DreamOverlayService.class) + public abstract Service bindDreamOverlayService(DreamOverlayService service); + /** */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java index 30844ccc877b5..11bee7ed0669c 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java @@ -23,6 +23,7 @@ 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.globalactions.GlobalActionsComponent; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.dagger.KeyguardModule; @@ -188,4 +189,11 @@ 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); } 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 0000000000000..20c46da14e637 --- /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 0000000000000..d37914a63a390 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -0,0 +1,165 @@ +/* + * 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 window is populated once the dream informs the service it has begun dreaming. + private Window mWindow; + private ConstraintLayout mLayout; + + // 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)); + } + + /** + * 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()); + } + + @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) { + mContext = context; + mExecutor = executor; + } +} 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 0000000000000..08f0f3507e3e0 --- /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 0000000000000..7870426c78f1c --- /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 0000000000000..f20802527d739 --- /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/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java new file mode 100644 index 0000000000000..6223048f0ce5e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -0,0 +1,120 @@ +/* + * 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 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; + +@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; + + @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); + 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 creationCallbackCapture = + ArgumentCaptor.forClass(OverlayHost.CreationCallback.class); + final ArgumentCaptor 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(); + } +} From 814a2d60ef71f5da499ae2b33b5d85403a91aa1e Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Thu, 30 Sep 2021 16:49:50 -0700 Subject: [PATCH 6/8] Centralized Dream Overlay State. This changelist introduces DreamOverlayStateController, a singleton for managing dream overlay state. The controller currently maintains the collection of overlays (by way of DreamOverlayProviders) to show over a dream. Entities, such as the DreamOverlayService, can register for updates to the collection. Bug: 201676403 Test: atest DreamOverlayServiceTest Test: atest DreamOverlayStateControllerTest Change-Id: I096dbea4b99fbbe7f5ec74d954db5b8af80ef68b --- .../systemui/dreams/DreamOverlayService.java | 32 +++- .../dreams/DreamOverlayStateController.java | 150 ++++++++++++++++++ .../dreams/DreamOverlayServiceTest.java | 35 +++- .../DreamOverlayStateControllerTest.java | 86 ++++++++++ 4 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index d37914a63a390..8f0ea2fb2f878 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -52,11 +52,21 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ 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 = @@ -101,6 +111,16 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ 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 @@ -134,6 +154,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ final WindowManager windowManager = mContext.getSystemService(WindowManager.class); windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes()); + mExecutor.execute(this::reloadOverlaysLocked); } @VisibleForTesting @@ -158,8 +179,17 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ } @Inject - public DreamOverlayService(Context context, @Main Executor executor) { + 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 0000000000000..d248a9e174f51 --- /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 { + // 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 mCallbacks = new ArrayList<>(); + private final HashMap 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 getOverlays() { + return mOverlays.values(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index 6223048f0ce5e..53bfeee9135ab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -19,6 +19,7 @@ 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; @@ -48,6 +49,8 @@ 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 { @@ -72,6 +75,9 @@ public class DreamOverlayServiceTest extends SysuiTestCase { @Mock OverlayProvider mProvider; + @Mock + DreamOverlayStateController mDreamOverlayStateController; + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -80,7 +86,8 @@ public class DreamOverlayServiceTest extends SysuiTestCase { @Test public void testInteraction() throws Exception { - final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor); + final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor, + mDreamOverlayStateController); final IBinder proxy = service.onBind(new Intent()); final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); clearInvocations(mWindowManager); @@ -117,4 +124,30 @@ public class DreamOverlayServiceTest extends SysuiTestCase { // 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 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 0000000000000..4e97be37603e0 --- /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 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(); + } +} From 54b8f33ec726f23a18dfe71ee24c74c3788ac427 Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Thu, 30 Sep 2021 17:07:30 -0700 Subject: [PATCH 7/8] Add Support for App Widgets as Overlays. This changelist adds support for loading App Widgets from a ComponentName. This functionality is wrapped in an OverlayProvider for integration with DreamOverlayService. Bug: 201675932 Test: atest AppWidgetOverlayProviderTest Change-Id: Iddc6297ee0a9eb6c945a70368ba1f1b7ef0b1a0b --- packages/SystemUI/AndroidManifest.xml | 5 + .../systemui/dagger/SystemUIModule.java | 2 + .../appwidgets/AppWidgetOverlayProvider.java | 82 +++++++++++ .../dreams/appwidgets/AppWidgetProvider.java | 101 +++++++++++++ .../dagger/AppWidgetOverlayComponent.java | 39 +++++ .../systemui/dreams/dagger/DreamModule.java | 28 ++++ .../dreams/AppWidgetOverlayProviderTest.java | 136 ++++++++++++++++++ 7 files changed, 393 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayProvider.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetProvider.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/dagger/AppWidgetOverlayComponent.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/AppWidgetOverlayProviderTest.java diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 25b73ab401a8a..26a49623fabf3 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -294,6 +294,11 @@ + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 799c92c6ce185..4cecb3916f038 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/appwidgets/AppWidgetOverlayProvider.java b/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayProvider.java new file mode 100644 index 0000000000000..a635d3f740cf5 --- /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 0000000000000..d1da1e691ed6f --- /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 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 0000000000000..3103057be2093 --- /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 0000000000000..7bf2361e471c4 --- /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 0000000000000..504ba3dde3b60 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/AppWidgetOverlayProviderTest.java @@ -0,0 +1,136 @@ +/* + * 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 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(); + } +} From f0d376d45d0f67e9e1e77c67316ac25a5ba3744e Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Mon, 25 Oct 2021 09:45:59 -0700 Subject: [PATCH 8/8] Allow App Widget Overlays by Product Configuration. This changelist adds support for loading a set of app widget overlays to be shown over dreams from config. Products can specify the components to show along with the position within the parent by gravity. Bug: 201676043 Test: atest AppWidgetOverlayPrimerTest Change-Id: Ic2f706598f776e0802106e333c4003242227faea --- packages/SystemUI/res/values/config.xml | 17 +- .../systemui/dagger/SystemUIBinder.java | 8 + .../appwidgets/AppWidgetOverlayPrimer.java | 119 +++++++++++ .../dreams/AppWidgetOverlayProviderTest.java | 1 - .../AppWidgetOverlayPrimerTest.java | 188 ++++++++++++++++++ 5 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimer.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimerTest.java diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 4b798ef4c3b4c..96433e554919a 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -715,7 +715,22 @@ true + + + + + + + + + + 0.33 + + + 0.25 + false - diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java index 11bee7ed0669c..a5d4d80598c43 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java @@ -24,6 +24,7 @@ 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; @@ -196,4 +197,11 @@ public abstract class SystemUIBinder { @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/dreams/appwidgets/AppWidgetOverlayPrimer.java b/packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimer.java new file mode 100644 index 0000000000000..a0c7c29e01916 --- /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/tests/src/com/android/systemui/dreams/AppWidgetOverlayProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/AppWidgetOverlayProviderTest.java index 504ba3dde3b60..0fc306b99b651 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/AppWidgetOverlayProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/AppWidgetOverlayProviderTest.java @@ -109,7 +109,6 @@ public class AppWidgetOverlayProviderTest extends SysuiTestCase { mInteractionHandler = creationCallbackCapture.getValue(); } - @Test public void testWidgetBringup() { // Make sure widget was requested. 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 0000000000000..2e5b1653584b4 --- /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 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 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()); + } +}