diff options
5 files changed, 500 insertions, 0 deletions
diff --git a/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml b/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml new file mode 100644 index 000000000000..4f0a78e9c35d --- /dev/null +++ b/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 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. +--> +<ImageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/home_controls_chip" + android:layout_height="@dimen/keyguard_affordance_fixed_height" + android:layout_width="@dimen/keyguard_affordance_fixed_width" + android:layout_gravity="bottom|start" + android:scaleType="center" + android:tint="?android:attr/textColorPrimary" + android:src="@drawable/controls_icon" + android:background="@drawable/keyguard_bottom_affordance_bg" + android:layout_marginStart="@dimen/keyguard_affordance_horizontal_offset" + android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset" + android:contentDescription="@string/quick_controls_title" /> diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java new file mode 100644 index 000000000000..1a9d9b50104b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2022 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.complication; + +import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE; +import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK; +import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.UNAVAILABLE; +import static com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent.DreamHomeControlsModule.DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS; +import static com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent.DreamHomeControlsModule.DREAM_HOME_CONTROLS_CHIP_VIEW; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; + +import com.android.systemui.CoreStartable; +import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.controls.dagger.ControlsComponent; +import com.android.systemui.controls.management.ControlsListingController; +import com.android.systemui.controls.ui.ControlsActivity; +import com.android.systemui.controls.ui.ControlsUiController; +import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.util.ViewController; + +import javax.inject.Inject; +import javax.inject.Named; + +/** + * A dream complication that shows a home controls chip to launch device controls (to control + * devices at home like lights and thermostats). + */ +public class DreamHomeControlsComplication implements Complication { + private final DreamHomeControlsComplicationComponent.Factory mComponentFactory; + + @Inject + public DreamHomeControlsComplication( + DreamHomeControlsComplicationComponent.Factory componentFactory) { + mComponentFactory = componentFactory; + } + + @Override + public ViewHolder createView(ComplicationViewModel model) { + return mComponentFactory.create().getViewHolder(); + } + + @Override + public int getRequiredTypeAvailability() { + return COMPLICATION_TYPE_NONE; + } + + /** + * {@link CoreStartable} for registering the complication with SystemUI on startup. + */ + public static class Registrant extends CoreStartable { + private final DreamHomeControlsComplication mComplication; + private final DreamOverlayStateController mDreamOverlayStateController; + private final ControlsComponent mControlsComponent; + + private boolean mControlServicesAvailable = false; + + // Callback for when the home controls service availability changes. + private final ControlsListingController.ControlsListingCallback mControlsCallback = + serviceInfos -> { + boolean available = !serviceInfos.isEmpty(); + + if (available != mControlServicesAvailable) { + mControlServicesAvailable = available; + updateComplicationAvailability(); + } + }; + + @Inject + public Registrant(Context context, DreamHomeControlsComplication complication, + DreamOverlayStateController dreamOverlayStateController, + ControlsComponent controlsComponent) { + super(context); + + mComplication = complication; + mControlsComponent = controlsComponent; + mDreamOverlayStateController = dreamOverlayStateController; + } + + @Override + public void start() { + mControlsComponent.getControlsListingController().ifPresent( + c -> c.addCallback(mControlsCallback)); + } + + private void updateComplicationAvailability() { + final boolean hasFavorites = mControlsComponent.getControlsController() + .map(c -> !c.getFavorites().isEmpty()) + .orElse(false); + if (!hasFavorites || !mControlServicesAvailable + || mControlsComponent.getVisibility() == UNAVAILABLE) { + mDreamOverlayStateController.removeComplication(mComplication); + } else { + mDreamOverlayStateController.addComplication(mComplication); + } + } + } + + /** + * Contains values/logic associated with the dream complication view. + */ + public static class DreamHomeControlsChipViewHolder implements ViewHolder { + private final View mView; + private final ComplicationLayoutParams mLayoutParams; + private final DreamHomeControlsChipViewController mViewController; + + @Inject + DreamHomeControlsChipViewHolder( + DreamHomeControlsChipViewController dreamHomeControlsChipViewController, + @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) ImageView view, + @Named(DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS) ComplicationLayoutParams layoutParams + ) { + mView = view; + mLayoutParams = layoutParams; + mViewController = dreamHomeControlsChipViewController; + mViewController.init(); + } + + @Override + public View getView() { + return mView; + } + + @Override + public ComplicationLayoutParams getLayoutParams() { + return mLayoutParams; + } + } + + /** + * Controls behavior of the dream complication. + */ + static class DreamHomeControlsChipViewController extends ViewController<ImageView> { + private static final boolean DEBUG = false; + private static final String TAG = "DreamHomeControlsCtrl"; + + private final ActivityStarter mActivityStarter; + private final Context mContext; + private final ControlsComponent mControlsComponent; + + @Inject + DreamHomeControlsChipViewController( + @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) ImageView view, + ActivityStarter activityStarter, + Context context, + ControlsComponent controlsComponent) { + super(view); + + mActivityStarter = activityStarter; + mContext = context; + mControlsComponent = controlsComponent; + } + + @Override + protected void onViewAttached() { + mView.setImageResource(mControlsComponent.getTileImageId()); + mView.setContentDescription(mContext.getString(mControlsComponent.getTileTitleId())); + mView.setOnClickListener(this::onClickHomeControls); + } + + @Override + protected void onViewDetached() {} + + private void onClickHomeControls(View v) { + if (DEBUG) Log.d(TAG, "home controls complication tapped"); + + final Intent intent = new Intent(mContext, ControlsActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(ControlsUiController.EXTRA_ANIMATE, true); + + final ActivityLaunchAnimator.Controller controller = + v != null ? ActivityLaunchAnimator.Controller.fromView(v, null /* cujType */) + : null; + if (mControlsComponent.getVisibility() == AVAILABLE) { + // Controls can be made visible. + mActivityStarter.startActivity(intent, true /* dismissShade */, controller, + true /* showOverLockscreenWhenLocked */); + } else if (mControlsComponent.getVisibility() == AVAILABLE_AFTER_UNLOCK) { + // Controls can be made visible only after device unlock. + mActivityStarter.postStartActivityDismissingKeyguard(intent, 0 /* delay */, + controller); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java new file mode 100644 index 000000000000..033ce392220a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2022 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.complication.dagger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import android.content.res.Resources; +import android.view.LayoutInflater; +import android.widget.ImageView; + +import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dreams.complication.ComplicationLayoutParams; +import com.android.systemui.dreams.complication.DreamHomeControlsComplication; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Named; +import javax.inject.Scope; + +import dagger.Module; +import dagger.Provides; +import dagger.Subcomponent; + +/** + * Responsible for generating dependencies for the {@link DreamHomeControlsComplication}. + */ +@Subcomponent(modules = DreamHomeControlsComplicationComponent.DreamHomeControlsModule.class) +@DreamHomeControlsComplicationComponent.DreamHomeControlsComplicationScope +public interface DreamHomeControlsComplicationComponent { + /** + * Creates a view holder for the home controls complication. + */ + DreamHomeControlsComplication.DreamHomeControlsChipViewHolder getViewHolder(); + + /** + * Scope of the home controls complication. + */ + @Documented + @Retention(RUNTIME) + @Scope + @interface DreamHomeControlsComplicationScope {} + + /** + * Factory that generates a {@link DreamHomeControlsComplicationComponent}. + */ + @Subcomponent.Factory + interface Factory { + DreamHomeControlsComplicationComponent create(); + } + + /** + * Scoped injected values for the {@link DreamHomeControlsComplicationComponent}. + */ + @Module + interface DreamHomeControlsModule { + String DREAM_HOME_CONTROLS_CHIP_VIEW = "dream_home_controls_chip_view"; + String DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS = "home_controls_chip_layout_params"; + + // TODO(b/217199227): move to a single location. + // Weight of order in the parent container. The home controls complication should have low + // weight and be placed at the end. + int INSERT_ORDER_WEIGHT = 0; + + /** + * Provides the dream home controls chip view. + */ + @Provides + @DreamHomeControlsComplicationScope + @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) + static ImageView provideHomeControlsChipView(LayoutInflater layoutInflater) { + return (ImageView) layoutInflater.inflate(R.layout.dream_overlay_home_controls_chip, + null, false); + } + + /** + * Provides the layout parameters for the dream home controls complication. + */ + @Provides + @DreamHomeControlsComplicationScope + @Named(DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS) + static ComplicationLayoutParams provideLayoutParams(@Main Resources res) { + return new ComplicationLayoutParams( + res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width), + res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height), + ComplicationLayoutParams.POSITION_BOTTOM + | ComplicationLayoutParams.POSITION_START, + ComplicationLayoutParams.DIRECTION_END, + INSERT_ORDER_WEIGHT); + } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java index e45437dedd6e..4a515f09b5a7 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java @@ -27,6 +27,9 @@ import dagger.Module; @Module(includes = { DreamClockDateComplicationModule.class, DreamClockTimeComplicationModule.class, + }, + subcomponents = { + DreamHomeControlsComplicationComponent.class, }) public interface RegisteredComplicationsModule { } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java new file mode 100644 index 000000000000..5191f635a2cf --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2022 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.complication; + +import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.controls.ControlsServiceInfo; +import com.android.systemui.controls.controller.ControlsController; +import com.android.systemui.controls.controller.StructureInfo; +import com.android.systemui.controls.dagger.ControlsComponent; +import com.android.systemui.controls.management.ControlsListingController; +import com.android.systemui.dreams.DreamOverlayStateController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; +import java.util.Optional; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DreamHomeControlsComplicationTest extends SysuiTestCase { + @Mock + private DreamHomeControlsComplication mComplication; + + @Mock + private DreamOverlayStateController mDreamOverlayStateController; + + @Mock + private Context mContext; + + @Mock + private ControlsComponent mControlsComponent; + + @Mock + private ControlsController mControlsController; + + @Mock + private ControlsListingController mControlsListingController; + + @Captor + private ArgumentCaptor<ControlsListingController.ControlsListingCallback> mCallbackCaptor; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(mContext.getString(anyInt())).thenReturn(""); + when(mControlsComponent.getControlsController()).thenReturn( + Optional.of(mControlsController)); + when(mControlsComponent.getControlsListingController()).thenReturn( + Optional.of(mControlsListingController)); + when(mControlsComponent.getVisibility()).thenReturn(AVAILABLE); + } + + @Test + public void complicationAvailability_serviceNotAvailable_noFavorites_doNotAddComplication() { + final DreamHomeControlsComplication.Registrant registrant = + new DreamHomeControlsComplication.Registrant(mContext, mComplication, + mDreamOverlayStateController, mControlsComponent); + registrant.start(); + + setHaveFavorites(false); + setServiceAvailable(false); + + verify(mDreamOverlayStateController, never()).addComplication(mComplication); + } + + @Test + public void complicationAvailability_serviceAvailable_noFavorites_doNotAddComplication() { + final DreamHomeControlsComplication.Registrant registrant = + new DreamHomeControlsComplication.Registrant(mContext, mComplication, + mDreamOverlayStateController, mControlsComponent); + registrant.start(); + + setHaveFavorites(false); + setServiceAvailable(true); + + verify(mDreamOverlayStateController, never()).addComplication(mComplication); + } + + @Test + public void complicationAvailability_serviceNotAvailable_haveFavorites_doNotAddComplication() { + final DreamHomeControlsComplication.Registrant registrant = + new DreamHomeControlsComplication.Registrant(mContext, mComplication, + mDreamOverlayStateController, mControlsComponent); + registrant.start(); + + setHaveFavorites(true); + setServiceAvailable(false); + + verify(mDreamOverlayStateController, never()).addComplication(mComplication); + } + + @Test + public void complicationAvailability_serviceAvailable_haveFavorites_addComplication() { + final DreamHomeControlsComplication.Registrant registrant = + new DreamHomeControlsComplication.Registrant(mContext, mComplication, + mDreamOverlayStateController, mControlsComponent); + registrant.start(); + + setHaveFavorites(true); + setServiceAvailable(true); + + verify(mDreamOverlayStateController).addComplication(mComplication); + } + + private void setHaveFavorites(boolean value) { + final List<StructureInfo> favorites = mock(List.class); + when(favorites.isEmpty()).thenReturn(!value); + when(mControlsController.getFavorites()).thenReturn(favorites); + } + + private void setServiceAvailable(boolean value) { + final List<ControlsServiceInfo> serviceInfos = mock(List.class); + when(serviceInfos.isEmpty()).thenReturn(!value); + triggerControlsListingCallback(serviceInfos); + } + + private void triggerControlsListingCallback(List<ControlsServiceInfo> serviceInfos) { + verify(mControlsListingController).addCallback(mCallbackCaptor.capture()); + mCallbackCaptor.getValue().onServicesUpdated(serviceInfos); + } +} |