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());
+ }
+}