Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f0d376d4 authored by Bryce Lee's avatar Bryce Lee
Browse files

Allow App Widget Overlays by Product Configuration.

This changelist adds support for loading a set of app
widget overlays to be shown over dreams from config.
Products can specify the components to show along with
the position within the parent by gravity.

Bug: 201676043
Test: atest AppWidgetOverlayPrimerTest
Change-Id: Ic2f706598f776e0802106e333c4003242227faea
parent 54b8f33e
Loading
Loading
Loading
Loading
+16 −1
Original line number Diff line number Diff line
@@ -715,7 +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>
+8 −0
Original line number Diff line number Diff line
@@ -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);
}
+119 −0
Original line number Diff line number Diff line
/*
 * 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());
        }
    }
}
+0 −1
Original line number Diff line number Diff line
@@ -109,7 +109,6 @@ public class AppWidgetOverlayProviderTest extends SysuiTestCase {
        mInteractionHandler = creationCallbackCapture.getValue();
    }


    @Test
    public void testWidgetBringup() {
        // Make sure widget was requested.
+188 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}