summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/test-current.txt14
-rw-r--r--core/java/android/app/DreamManager.java25
-rw-r--r--core/java/android/service/dreams/DreamOverlayService.java77
-rw-r--r--core/java/android/service/dreams/DreamService.java60
-rw-r--r--packages/SystemUI/AndroidManifest.xml10
-rw-r--r--packages/SystemUI/res/values/config.xml18
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java128
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java195
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java150
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/OverlayHost.java48
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/OverlayHostView.java43
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/OverlayProvider.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimer.java119
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayProvider.java82
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/appwidgets/AppWidgetProvider.java101
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/dagger/AppWidgetOverlayComponent.java39
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java28
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/AppWidgetOverlayProviderTest.java135
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java153
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java86
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/appwidgets/AppWidgetOverlayPrimerTest.java188
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java14
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;