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