getComplications() {
- return mComplications.values();
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
new file mode 100644
index 0000000000000..96cf50d58d10e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
@@ -0,0 +1,210 @@
+/*
+ * 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 android.annotation.IntDef;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * {@link Complication} is an interface for defining a complication, a visual component rendered
+ * above a dream. {@link Complication} instances encapsulate the logic for generating the view to be
+ * shown, along with the supporting control/logic. The decision for including the
+ * {@link Complication} is not the responsibility of the {@link Complication}. This is instead
+ * handled by domain logic and invocations to add and remove the {@link Complication} through
+ * {@link com.android.systemui.dreams.DreamOverlayStateController#addComplication(Complication)} and
+ * {@link com.android.systemui.dreams.DreamOverlayStateController#removeComplication(Complication)}.
+ * A {@link Complication} also does not represent a specific instance of the view. Instead, it
+ * should be viewed as a provider, where view instances are requested from it. The associated
+ * {@link ViewHolder} interface is requested for each view request. This object is retained for the
+ * view's lifetime, providing a container for any associated logic. The complication rendering
+ * system will consult this {@link ViewHolder} for {@link View} to show and
+ * {@link ComplicationLayoutParams} to position the view. {@link ComplicationLayoutParams} allow for
+ * specifying the sizing and position of the {@link Complication}.
+ *
+ * The following code sample exhibits the entities and lifecycle involved with a
+ * {@link Complication}.
+ *
+ * {@code
+ * // This component allows for the complication to generate a new ViewHolder for every request.
+ * @Subcomponent
+ * interface ExampleViewHolderComponent {
+ * @Subcomponent.Factory
+ * interface Factory {
+ * ExampleViewHolderComponent create();
+ * }
+ *
+ * ExampleViewHolder getViewHolder();
+ * }
+ *
+ * // An example entity that controls whether or not a complication should be included on dreams.
+ * // Note how the complication is tracked by reference for removal.
+ * public class ExampleComplicationProvider {
+ * private final DreamOverlayStateController mDreamOverlayStateController;
+ * private final ExampleComplication mComplication;
+ * @Inject
+ * public ExampleComplicationProvider(
+ * ExampleComplication complication,
+ * DreamOverlayStateController stateController) {
+ * mDreamOverlayStateController = stateController;
+ * mComplication = complication;
+ * }
+ *
+ * public void onShowConditionsMet(boolean met) {
+ * if (met) {
+ * mDreamOverlayStateController.addComplication(mComplication);
+ * } else {
+ * mDreamOverlayStateController.removeComplication(mComplication);
+ * }
+ * }
+ * }
+ *
+ * // An example complication. Note how a factory is created to supply a unique ViewHolder for each
+ * // request. Also, there is no particular view instance members defined in the complication.
+ * class ExampleComplication implements Complication {
+ * private final ExampleViewHolderComponent.Factory mFactory;
+ * @Inject
+ * public ExampleComplication(ExampleViewHolderComponent.Factory viewHolderComponentFactory) {
+ * mFactory = viewHolderComponentFactory;
+ * }
+ *
+ * @Override
+ * public ViewHolder createView(ComplicationViewModel model) {
+ * return mFactory.create().getViewHolder();
+ * }
+ * }
+ *
+ * // Not every ViewHolder needs to include a view controller. It is included here as an example of
+ * // how such logic can be contained and associated with the ViewHolder lifecycle.
+ * class ExampleViewController extends ViewController {
+ * protected ExampleViewController(FrameLayout view) {
+ * super(view);
+ * }
+ *
+ * @Override
+ * protected void onViewAttached() { }
+ *
+ * @Override
+ * protected void onViewDetached() { }
+ * }
+ *
+ * // An example ViewHolder. This is the correct place to contain any value/logic associated with a
+ * // particular instance of the ComplicationView.
+ * class ExampleViewHolder implements Complication.ViewHolder {
+ * final FrameLayout mView;
+ * final ExampleViewController mController;
+ *
+ * @Inject
+ * public ExampleViewHolder(Context context) {
+ * mView = new FrameLayout(context);
+ * mController = new ExampleViewController(mView);
+ * }
+ * @Override
+ * public View getView() {
+ * return mView;
+ * }
+ *
+ * @Override
+ * public ComplicationLayoutParams getLayoutParams() {
+ * return new ComplicationLayoutParams(
+ * 200,
+ * 100,
+ * ComplicationLayoutParams.POSITION_TOP | ComplicationLayoutParams.DIRECTION_END,
+ * ComplicationLayoutParams.DIRECTION_DOWN,
+ * 4);
+ * }
+ * }
+ * }
+ *
+ */
+public interface Complication {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "CATEGORY_" }, value = {
+ CATEGORY_STANDARD,
+ CATEGORY_SYSTEM,
+ })
+
+ @interface Category {}
+ /**
+ * {@code CATEGORY_STANDARD} indicates the complication is a normal component. Rules and
+ * settings, such as hiding all complications, will apply to this complication.
+ */
+ int CATEGORY_STANDARD = 1 << 0;
+ /**
+ * {@code CATEGORY_SYSTEM} indicates complications driven by SystemUI. Usually, these are
+ * core components that are not user controlled. These can potentially deviate from given
+ * rule sets that would normally apply to {@code CATEGORY_STANDARD}.
+ */
+ int CATEGORY_SYSTEM = 1 << 1;
+
+ /**
+ * The {@link Host} interface specifies a way a {@link Complication} to communicate with its
+ * parent entity for information and actions.
+ */
+ interface Host {
+ /**
+ * Called to signal a {@link Complication} has requested to exit the dream.
+ */
+ void requestExitDream();
+ }
+
+ /**
+ * Returned through {@link Complication#createView(ComplicationViewModel)}, {@link ViewHolder}
+ * is a container for a single {@link Complication} instance. The {@link Host} guarantees that
+ * the {@link ViewHolder} will be retained for the lifetime of the {@link Complication}
+ * instance's user. The view is responsible for providing the view that represents the
+ * {@link Complication}. This object is the proper place to store any related entities, such as
+ * a {@link com.android.systemui.util.ViewController} for the view.
+ */
+ interface ViewHolder {
+ /**
+ * Returns the {@link View} associated with the {@link ViewHolder}. This {@link View} should
+ * be stable and generated once.
+ * @return
+ */
+ View getView();
+
+ /**
+ * Returns the {@link Category} associated with the {@link Complication}. {@link Category}
+ * is a grouping which helps define the relationship of the {@link Complication} to
+ * System UI and the rest of the system. It is used for presentation and other decisions.
+ */
+ @Complication.Category
+ default int getCategory() {
+ return Complication.CATEGORY_STANDARD;
+ }
+
+ /**
+ * Returns the {@link ComplicationLayoutParams} associated with this complication. The
+ * values expressed here are treated as preference rather than requirement. The hosting
+ * entity is free to modify/interpret the parameters as deemed fit.
+ */
+ ComplicationLayoutParams getLayoutParams();
+ }
+
+ /**
+ * Generates a {@link ViewHolder} for the {@link Complication}. This captures both the view and
+ * control logic for a single instance of the complication. The {@link Complication} may be
+ * asked at any time to generate another view.
+ * @param model The {@link ComplicationViewModel} associated with this particular
+ * {@link Complication} instance.
+ * @return a {@link ViewHolder} for this {@link Complication} instance.
+ */
+ ViewHolder createView(ComplicationViewModel model);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveData.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveData.java
new file mode 100644
index 0000000000000..76818fa0c42e1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveData.java
@@ -0,0 +1,66 @@
+/*
+ * 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 androidx.lifecycle.LiveData;
+
+import com.android.systemui.dreams.DreamOverlayStateController;
+
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+/**
+ * {@link ComplicationCollectionLiveData} wraps
+ * {@link DreamOverlayStateController#getComplications()} to provide an observable
+ * {@link Complication} data set tied to a lifecycle. This should not be directly accessed. Instead,
+ * clients should access the data from {@link ComplicationCollectionViewModel}.
+ */
+public class ComplicationCollectionLiveData extends LiveData> {
+ final DreamOverlayStateController mDreamOverlayStateController;
+
+ final DreamOverlayStateController.Callback mStateControllerCallback;
+
+ {
+ mStateControllerCallback = new DreamOverlayStateController.Callback() {
+ @Override
+ public void onComplicationsChanged() {
+ setValue(mDreamOverlayStateController.getComplications());
+ }
+
+ };
+ }
+
+ @Inject
+ public ComplicationCollectionLiveData(DreamOverlayStateController stateController) {
+ super();
+ mDreamOverlayStateController = stateController;
+ }
+
+ @Override
+ protected void onActive() {
+ super.onActive();
+ mDreamOverlayStateController.addCallback(mStateControllerCallback);
+ setValue(mDreamOverlayStateController.getComplications());
+ }
+
+ @Override
+ protected void onInactive() {
+ mDreamOverlayStateController.removeCallback(mStateControllerCallback);
+ super.onInactive();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationCollectionViewModel.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationCollectionViewModel.java
new file mode 100644
index 0000000000000..7190d7a0190ab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationCollectionViewModel.java
@@ -0,0 +1,63 @@
+/*
+ * 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 androidx.lifecycle.LiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+
+/**
+ * {@link ComplicationCollectionViewModel} is an abstraction for observing and accessing
+ * {@link ComplicationViewModel} for registered {@link Complication}.
+ */
+public class ComplicationCollectionViewModel extends ViewModel {
+ private final LiveData> mComplications;
+ private final ComplicationViewModelTransformer mTransformer;
+
+ /**
+ * Injectable constructor for {@link ComplicationCollectionViewModel}. Note that this cannot
+ * be implicitly injected. Clients must bind scoped instance values through the corresponding
+ * dagger subcomponent.
+ */
+ @Inject
+ public ComplicationCollectionViewModel(
+ ComplicationCollectionLiveData complications,
+ ComplicationViewModelTransformer transformer) {
+ mComplications = Transformations.map(complications, collection -> convert(collection));
+ mTransformer = transformer;
+ }
+
+ private Collection convert(Collection complications) {
+ return complications
+ .stream()
+ .map(complication -> mTransformer.getViewModel(complication))
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns {@link LiveData} for the collection of {@link Complication} represented as
+ * {@link ComplicationViewModel}.
+ */
+ public LiveData> getComplications() {
+ return mComplications;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
new file mode 100644
index 0000000000000..f627f150384bb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
@@ -0,0 +1,138 @@
+/*
+ * 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.dreams.complication.dagger.ComplicationHostViewComponent.SCOPED_COMPLICATIONS_LAYOUT;
+import static com.android.systemui.dreams.complication.dagger.ComplicationModule.SCOPED_COMPLICATIONS_MODEL;
+
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.View;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.lifecycle.LifecycleOwner;
+
+import com.android.systemui.util.ViewController;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * The {@link ComplicationHostViewController} is responsible for displaying complications within
+ * a given container. It monitors the available {@link Complication} instances from
+ * {@link com.android.systemui.dreams.DreamOverlayStateController} and inserts/removes them through
+ * a {@link ComplicationLayoutEngine}.
+ */
+public class ComplicationHostViewController extends ViewController {
+ private final ComplicationLayoutEngine mLayoutEngine;
+ private final LifecycleOwner mLifecycleOwner;
+ private final ComplicationCollectionViewModel mComplicationCollectionViewModel;
+ private final HashMap mComplications = new HashMap<>();
+
+ @Inject
+ protected ComplicationHostViewController(
+ @Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout view,
+ ComplicationLayoutEngine layoutEngine,
+ LifecycleOwner lifecycleOwner,
+ @Named(SCOPED_COMPLICATIONS_MODEL) ComplicationCollectionViewModel viewModel) {
+ super(view);
+ mLayoutEngine = layoutEngine;
+ mLifecycleOwner = lifecycleOwner;
+ mComplicationCollectionViewModel = viewModel;
+ }
+
+ @Override
+ protected void onInit() {
+ super.onInit();
+ mComplicationCollectionViewModel.getComplications().observe(mLifecycleOwner,
+ complicationViewModels -> updateComplications(complicationViewModels));
+ }
+
+ /**
+ * Returns the region in display space occupied by complications. Touches in this region
+ * (composed of a collection of individual rectangular regions) should be directed to the
+ * complications rather than the region underneath.
+ */
+ public Region getTouchRegions() {
+ final Region region = new Region();
+ final Rect rect = new Rect();
+ final int childCount = mView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = mView.getChildAt(i);
+ if (child.getGlobalVisibleRect(rect)) {
+ region.op(rect, Region.Op.UNION);
+ }
+ }
+
+ return region;
+ }
+
+ private void updateComplications(Collection complications) {
+ final Collection ids = complications.stream()
+ .map(complicationViewModel -> complicationViewModel.getId())
+ .collect(Collectors.toSet());
+
+ final Collection removedComplicationIds =
+ mComplications.keySet().stream()
+ .filter(complicationId -> !ids.contains(complicationId))
+ .collect(Collectors.toSet());
+
+ // Trim removed complications
+ removedComplicationIds.forEach(complicationId -> {
+ mLayoutEngine.removeComplication(complicationId);
+ mComplications.remove(complicationId);
+ });
+
+ // Add new complications
+ final Collection newComplications = complications
+ .stream()
+ .filter(complication -> !mComplications.containsKey(complication.getId()))
+ .collect(Collectors.toSet());
+
+ newComplications
+ .forEach(complication -> {
+ final ComplicationId id = complication.getId();
+ final Complication.ViewHolder viewHolder = complication.getComplication()
+ .createView(complication);
+ mComplications.put(id, viewHolder);
+ mLayoutEngine.addComplication(id, viewHolder.getView(),
+ viewHolder.getLayoutParams(), viewHolder.getCategory());
+ });
+ }
+
+ @Override
+ protected void onViewAttached() {
+ }
+
+ @Override
+ protected void onViewDetached() {
+ }
+
+ /**
+ * Exposes the associated {@link View}. Since this {@link View} is instantiated through dagger
+ * in the {@link ComplicationHostViewController} constructor, the
+ * {@link ComplicationHostViewController} is responsible for surfacing it so that it can be
+ * included in the parent view hierarchy.
+ */
+ public View getView() {
+ return mView;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationId.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationId.java
new file mode 100644
index 0000000000000..420359c46b546
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationId.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+/**
+ * A {@link ComplicationId} is a value to uniquely identify a complication during the current
+ * runtime and within a particular scope. Any guarantees beyond this will need to be enforced
+ * externally.
+ */
+public class ComplicationId {
+ /**
+ * An associated factory for minting ids that are unique in for the factory's scope.
+ */
+ public static class Factory {
+ private int mNextId;
+
+ ComplicationId getNextId() {
+ return new ComplicationId(mNextId++);
+ }
+ }
+
+ private int mId;
+
+ private ComplicationId(int id) {
+ mId = id;
+ }
+
+ @Override
+ public String toString() {
+ return "ComplicationId{" + "mId=" + mId + "}";
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
new file mode 100644
index 0000000000000..cb24ae609eebd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
@@ -0,0 +1,429 @@
+/*
+ * 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.dreams.complication.dagger.ComplicationHostViewComponent.SCOPED_COMPLICATIONS_LAYOUT;
+
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.Constraints;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * {@link ComplicationLayoutEngine} arranges a collection of {@link ComplicationViewModel} based on
+ * their layout parameters and attributes. The management of this set is done by
+ * {@link ComplicationHostViewController}.
+ */
+public class ComplicationLayoutEngine {
+ public static final String TAG = "ComplicationLayoutEngine";
+
+ /**
+ * {@link ViewEntry} is an internal container, capturing information necessary for working with
+ * a particular {@link Complication} view.
+ */
+ private static class ViewEntry implements Comparable {
+ private final View mView;
+ private final ComplicationLayoutParams mLayoutParams;
+ private final Parent mParent;
+ @Complication.Category
+ private final int mCategory;
+
+ /**
+ * Default constructor. {@link Parent} allows for the {@link ViewEntry}'s surrounding
+ * view hierarchy to be accessed without traversing the entire view tree.
+ */
+ ViewEntry(View view, ComplicationLayoutParams layoutParams, int category, Parent parent) {
+ mView = view;
+ // Views that are generated programmatically do not have a unique id assigned to them
+ // at construction. A new id is assigned here to enable ConstraintLayout relative
+ // specifications. Existing ids for inflated views are not preserved.
+ // {@link Complication.ViewHolder} should not reference the root container by id.
+ mView.setId(View.generateViewId());
+ mLayoutParams = layoutParams;
+ mCategory = category;
+ mParent = parent;
+ }
+
+ /**
+ * Returns the {@link View} associated with the {@link Complication}. This is the instance
+ * passed in at construction. The reference to this {@link View} is captured when the
+ * {@link Complication} is added to the {@link ComplicationLayoutEngine}. The
+ * {@link Complication} cannot modify the {@link View} reference beyond this point.
+ */
+ private View getView() {
+ return mView;
+ }
+
+ /**
+ * Returns The {@link ComplicationLayoutParams} associated with the view.
+ */
+ public ComplicationLayoutParams getLayoutParams() {
+ return mLayoutParams;
+ }
+
+ /**
+ * Interprets the {@link #getLayoutParams()} into {@link ConstraintLayout.LayoutParams} and
+ * applies them to the view. The method accounts for the relationship of the {@link View} to
+ * the other {@link Complication} views around it. The organization of the {@link View}
+ * instances in {@link ComplicationLayoutEngine} can be seen as lists. A {@link View} is
+ * either the head of its list or a following node. This head is passed into this method,
+ * which can be a reference to the {@link View} to indicate it is the head.
+ */
+ public void applyLayoutParams(View head) {
+ // Only the basic dimension parameters from the base ViewGroup.LayoutParams are carried
+ // over verbatim from the complication specified LayoutParam. Other fields are
+ // interpreted.
+ final ConstraintLayout.LayoutParams params =
+ new Constraints.LayoutParams(mLayoutParams.width, mLayoutParams.height);
+
+ final int direction = getLayoutParams().getDirection();
+
+ // If no parent, view is the anchor. In this case, it is given the highest priority for
+ // alignment. All alignment preferences are done in relation to the parent container.
+ final boolean isRoot = head == mView;
+
+ // Each view can be seen as a vector, having a point (described here as position) and
+ // direction. When a view is the head of a position, then it is the first in a sequence
+ // of complications to appear from that position. For example, being the head for
+ // position POSITION_TOP | POSITION_END will cause the view to be shown as the first
+ // view in that corner. In this case, the positions specify which sides to align with
+ // the parent. If the view is not the head, the positions perpendicular to the direction
+ // of the view specify which side to align with the opposing side of the head view.
+ // Otherwise, the position aligns with the containing view. This means a
+ // POSITION_BOTTOM | POSITION_START with DIRECTION_UP non-head view's bottom to be
+ // aligned with the preceding view node's top and start to be aligned with the
+ // parent's start.
+ mLayoutParams.iteratePositions(position -> {
+ switch(position) {
+ case ComplicationLayoutParams.POSITION_START:
+ if (isRoot || direction != ComplicationLayoutParams.DIRECTION_END) {
+ params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
+ } else {
+ params.startToEnd = head.getId();
+ }
+ break;
+ case ComplicationLayoutParams.POSITION_TOP:
+ if (isRoot || direction != ComplicationLayoutParams.DIRECTION_DOWN) {
+ params.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
+ } else {
+ params.topToBottom = head.getId();
+ }
+ break;
+ case ComplicationLayoutParams.POSITION_BOTTOM:
+ if (isRoot || direction != ComplicationLayoutParams.DIRECTION_UP) {
+ params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
+ } else {
+ params.bottomToTop = head.getId();
+ }
+ break;
+ case ComplicationLayoutParams.POSITION_END:
+ if (isRoot || direction != ComplicationLayoutParams.DIRECTION_START) {
+ params.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
+ } else {
+ params.endToStart = head.getId();
+ }
+ break;
+ }
+ });
+
+ mView.setLayoutParams(params);
+ }
+
+ /**
+ * Informs the {@link ViewEntry}'s parent entity to remove the {@link ViewEntry} from
+ * being shown further.
+ */
+ public void remove() {
+ mParent.removeEntry(this);
+
+ ((ViewGroup) mView.getParent()).removeView(mView);
+ }
+
+ @Override
+ public int compareTo(ViewEntry viewEntry) {
+ // If the two entries have different categories, system complications take precedence.
+ if (viewEntry.mCategory != mCategory) {
+ // Note that this logic will need to be adjusted if more categories are introduced.
+ return mCategory == Complication.CATEGORY_SYSTEM ? 1 : -1;
+ }
+
+ // A higher weight indicates greater precedence if all else being equal.
+ if (viewEntry.mLayoutParams.getWeight() != mLayoutParams.getWeight()) {
+ return mLayoutParams.getWeight() > viewEntry.mLayoutParams.getWeight() ? 1 : -1;
+ }
+
+ return 0;
+ }
+
+ /**
+ * {@link Builder} allows for a multiple entities to contribute to the {@link ViewEntry}
+ * construction. This is necessary for setting an immutable parent, which might not be
+ * known until the view hierarchy is traversed.
+ */
+ private static class Builder {
+ private final View mView;
+ private final ComplicationLayoutParams mLayoutParams;
+ private final int mCategory;
+ private Parent mParent;
+
+ Builder(View view, ComplicationLayoutParams lp, @Complication.Category int category) {
+ mView = view;
+ mLayoutParams = lp;
+ mCategory = category;
+ }
+
+ /**
+ * Returns the set {@link ComplicationLayoutParams}
+ */
+ public ComplicationLayoutParams getLayoutParams() {
+ return mLayoutParams;
+ }
+
+ /**
+ * Returns the set {@link Complication.Category}.
+ */
+ @Complication.Category
+ public int getCategory() {
+ return mCategory;
+ }
+
+ /**
+ * Sets the parent. Note that this references to the entity for handling events, such as
+ * requesting the removal of the {@link View}. It is not the
+ * {@link android.view.ViewGroup} which contains the {@link View}.
+ */
+ Builder setParent(Parent parent) {
+ mParent = parent;
+ return this;
+ }
+
+ /**
+ * Builds and returns the resulting {@link ViewEntry}.
+ */
+ ViewEntry build() {
+ return new ViewEntry(mView, mLayoutParams, mCategory, mParent);
+ }
+ }
+
+ /**
+ * An interface allowing an {@link ViewEntry} to signal events.
+ */
+ interface Parent {
+ /**
+ * Indicates the {@link ViewEntry} requests removal.
+ */
+ void removeEntry(ViewEntry entry);
+ }
+ }
+
+ /**
+ * {@link PositionGroup} represents a collection of {@link Complication} at a given location.
+ * It further organizes the {@link Complication} by the direction in which they emanate from
+ * this position.
+ */
+ private static class PositionGroup implements DirectionGroup.Parent {
+ private final HashMap mDirectionGroups = new HashMap<>();
+
+ /**
+ * Invoked by the {@link PositionGroup} holder to introduce a {@link Complication} view to
+ * this group. It is assumed that the caller has correctly identified this
+ * {@link PositionGroup} as the proper home for the {@link Complication} based on its
+ * declared position.
+ */
+ public ViewEntry add(ViewEntry.Builder entryBuilder) {
+ final int direction = entryBuilder.getLayoutParams().getDirection();
+ if (!mDirectionGroups.containsKey(direction)) {
+ mDirectionGroups.put(direction, new DirectionGroup(this));
+ }
+
+ return mDirectionGroups.get(direction).add(entryBuilder);
+ }
+
+ @Override
+ public void onEntriesChanged() {
+ // Whenever an entry is added/removed from a child {@link DirectionGroup}, it is vital
+ // that all {@link DirectionGroup} children are visited. It is possible the overall
+ // head has changed, requiring constraints to be adjusted.
+ updateViews();
+ }
+
+ private void updateViews() {
+ ViewEntry head = null;
+
+ // Identify which {@link Complication} head from the set of {@link DirectionGroup}
+ // should be treated as the {@link PositionGroup} head.
+ for (DirectionGroup directionGroup : mDirectionGroups.values()) {
+ final ViewEntry groupHead = directionGroup.getHead();
+ if (head == null || (groupHead != null && groupHead.compareTo(head) > 0)) {
+ head = groupHead;
+ }
+ }
+
+ // A headless position group indicates no complications.
+ if (head == null) {
+ return;
+ }
+
+ for (DirectionGroup directionGroup : mDirectionGroups.values()) {
+ // Tell each {@link DirectionGroup} to update its containing {@link ViewEntry} based
+ // on the identified head. This iteration will also capture any newly added views.
+ directionGroup.updateViews(head.getView());
+ }
+ }
+ }
+
+ /**
+ * A {@link DirectionGroup} organizes the {@link ViewEntry} of a parent group that point are
+ * laid out in the same direction.
+ */
+ private static class DirectionGroup implements ViewEntry.Parent {
+ /**
+ * An interface implemented by the {@link DirectionGroup} parent to receive updates.
+ */
+ interface Parent {
+ /**
+ * Invoked to indicate a change to the {@link ViewEntry} composition for this
+ * {@link DirectionGroup}.
+ */
+ void onEntriesChanged();
+ }
+ private final ArrayList mViews = new ArrayList<>();
+ private final Parent mParent;
+
+ /**
+ * Creates a new {@link DirectionGroup} with the specified parent. Note that the
+ * {@link DirectionGroup} does not store its own direction. It is the responsibility of the
+ * {@link DirectionGroup.Parent} to maintain this association.
+ */
+ DirectionGroup(Parent parent) {
+ mParent = parent;
+ }
+
+ /**
+ * Returns the head of the group. It is assumed that the order of the {@link ViewEntry} is
+ * proactively maintained.
+ */
+ public ViewEntry getHead() {
+ return mViews.isEmpty() ? null : mViews.get(0);
+ }
+
+ /**
+ * Adds a {@link ViewEntry} via {@link ViewEntry.Builder} to this group.
+ */
+ public ViewEntry add(ViewEntry.Builder entryBuilder) {
+ final ViewEntry entry = entryBuilder.setParent(this).build();
+ mViews.add(entry);
+
+ // After adding view, reverse sort collection.
+ Collections.sort(mViews);
+ Collections.reverse(mViews);
+
+ mParent.onEntriesChanged();
+
+ return entry;
+ }
+
+ @Override
+ public void removeEntry(ViewEntry entry) {
+ // Sort is handled when the view is added, so should still be correct after removal.
+ // However, the head may have been removed, which may affect the layout of views in
+ // other DirectionGroups of the same PositionGroup.
+ mViews.remove(entry);
+ mParent.onEntriesChanged();
+ }
+
+ /**
+ * Invoked by {@link Parent} to update the layout of all children {@link ViewEntry} with
+ * the specified head. Note that the head might not be in this group and instead part of a
+ * neighboring group.
+ */
+ public void updateViews(View groupHead) {
+ Iterator it = mViews.iterator();
+
+ while (it.hasNext()) {
+ final ViewEntry viewEntry = it.next();
+ viewEntry.applyLayoutParams(groupHead);
+ groupHead = viewEntry.getView();
+ }
+ }
+ }
+
+ private final ConstraintLayout mLayout;
+ private final HashMap mEntries = new HashMap<>();
+ private final HashMap mPositions = new HashMap<>();
+
+ /** */
+ @Inject
+ public ComplicationLayoutEngine(@Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout) {
+ mLayout = layout;
+ }
+
+ /**
+ * Adds a complication to this {@link ComplicationLayoutEngine}.
+ * @param id A {@link ComplicationId} unique to this complication. If this matches a
+ * complication within this {@link ComplicationViewModel}, the existing complication
+ * will be removed.
+ * @param view The {@link View} to be shown.
+ * @param lp The {@link ComplicationLayoutParams} as expressed by the {@link Complication}.
+ * These will be interpreted into the final applied parameters.
+ * @param category The {@link Complication.Category} for the {@link Complication}.
+ */
+ public void addComplication(ComplicationId id, View view,
+ ComplicationLayoutParams lp, @Complication.Category int category) {
+ // If the complication is present, remove.
+ if (mEntries.containsKey(id)) {
+ removeComplication(id);
+ }
+
+ final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, lp, category);
+
+ // Add position group if doesn't already exist
+ final int position = lp.getPosition();
+ if (!mPositions.containsKey(position)) {
+ mPositions.put(position, new PositionGroup());
+ }
+
+ // Insert entry into group
+ final ViewEntry entry = mPositions.get(position).add(entryBuilder);
+ mEntries.put(id, entry);
+
+ mLayout.addView(entry.getView());
+ }
+
+ /**
+ * Removes a complication by {@link ComplicationId}.
+ */
+ public void removeComplication(ComplicationId id) {
+ if (!mEntries.containsKey(id)) {
+ Log.e(TAG, "could not find id:" + id);
+ return;
+ }
+
+ final ViewEntry entry = mEntries.get(id);
+ entry.remove();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
new file mode 100644
index 0000000000000..f9a69fadfedc9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
@@ -0,0 +1,186 @@
+/*
+ * 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 android.annotation.IntDef;
+import android.view.ViewGroup;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * {@link ComplicationLayoutParams} allows a {@link Complication} to express its preferred location
+ * and dimensions. Note that these parameters are not directly applied by any {@link ViewGroup}.
+ * They are instead consulted for the final parameters which best seem fit for usage.
+ */
+public class ComplicationLayoutParams extends ViewGroup.LayoutParams {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "POSITION_" }, value = {
+ POSITION_TOP,
+ POSITION_END,
+ POSITION_BOTTOM,
+ POSITION_START,
+ })
+
+ @interface Position {}
+ /** Align view with the top of parent or bottom of preceding {@link Complication}. */
+ static final int POSITION_TOP = 1 << 0;
+ /** Align view with the bottom of parent or top of preceding {@link Complication}. */
+ static final int POSITION_BOTTOM = 1 << 1;
+ /** Align view with the start of parent or end of preceding {@link Complication}. */
+ static final int POSITION_START = 1 << 2;
+ /** Align view with the end of parent or start of preceding {@link Complication}. */
+ static final int POSITION_END = 1 << 3;
+
+ private static final int FIRST_POSITION = POSITION_TOP;
+ private static final int LAST_POSITION = POSITION_END;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "DIRECTION_" }, value = {
+ DIRECTION_UP,
+ DIRECTION_DOWN,
+ DIRECTION_START,
+ DIRECTION_END,
+ })
+
+ @interface Direction {}
+ /** Position view upward from position. */
+ static final int DIRECTION_UP = 1 << 0;
+ /** Position view downward from position. */
+ static final int DIRECTION_DOWN = 1 << 1;
+ /** Position view towards the start of the parent. */
+ static final int DIRECTION_START = 1 << 2;
+ /** Position view towards the end of parent. */
+ static final int DIRECTION_END = 1 << 3;
+
+ @Position
+ private final int mPosition;
+
+ @Direction
+ private final int mDirection;
+
+ private final int mWeight;
+
+ // Do not allow specifying opposite positions
+ private static final int[] INVALID_POSITIONS =
+ { POSITION_BOTTOM | POSITION_TOP, POSITION_END | POSITION_START };
+
+ // Do not allow for specifying a direction towards the outside of the container.
+ private static final Map INVALID_DIRECTIONS;
+ static {
+ INVALID_DIRECTIONS = new HashMap<>();
+ INVALID_DIRECTIONS.put(POSITION_BOTTOM, DIRECTION_DOWN);
+ INVALID_DIRECTIONS.put(POSITION_TOP, DIRECTION_UP);
+ INVALID_DIRECTIONS.put(POSITION_START, DIRECTION_START);
+ INVALID_DIRECTIONS.put(POSITION_END, DIRECTION_END);
+ }
+
+ /**
+ * Constructs a {@link ComplicationLayoutParams}.
+ * @param width The width {@link android.view.View.MeasureSpec} for the view.
+ * @param height The height {@link android.view.View.MeasureSpec} for the view.
+ * @param position The place within the parent container where the view should be positioned.
+ * @param direction The direction the view should be laid out from either the parent container
+ * or preceding view.
+ * @param weight The weight that should be considered for this view when compared to other
+ * views. This has an impact on the placement of the view but not the rendering of
+ * the view.
+ */
+ public ComplicationLayoutParams(int width, int height, @Position int position,
+ @Direction int direction, int weight) {
+ super(width, height);
+
+ if (!validatePosition(position)) {
+ throw new IllegalArgumentException("invalid position:" + position);
+ }
+ mPosition = position;
+
+ if (!validateDirection(position, direction)) {
+ throw new IllegalArgumentException("invalid direction:" + direction);
+ }
+
+ mDirection = direction;
+
+ mWeight = weight;
+ }
+
+ /**
+ * Constructs {@link ComplicationLayoutParams} from an existing instance.
+ */
+ public ComplicationLayoutParams(ComplicationLayoutParams source) {
+ super(source);
+ mPosition = source.mPosition;
+ mDirection = source.mDirection;
+ mWeight = source.mWeight;
+ }
+
+ private static boolean validateDirection(@Position int position, @Direction int direction) {
+ for (int currentPosition = FIRST_POSITION; currentPosition <= LAST_POSITION;
+ currentPosition <<= 1) {
+ if ((position & currentPosition) == currentPosition
+ && INVALID_DIRECTIONS.containsKey(currentPosition)
+ && (direction & INVALID_DIRECTIONS.get(currentPosition)) != 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Iterates over the defined positions and invokes the specified {@link Consumer} for each
+ * position specified for this {@link ComplicationLayoutParams}.
+ */
+ public void iteratePositions(Consumer consumer) {
+ for (int currentPosition = FIRST_POSITION; currentPosition <= LAST_POSITION;
+ currentPosition <<= 1) {
+ if ((mPosition & currentPosition) == currentPosition) {
+ consumer.accept(currentPosition);
+ }
+ }
+ }
+
+ private static boolean validatePosition(@Position int position) {
+ if (position == 0) {
+ return false;
+ }
+
+ for (int combination : INVALID_POSITIONS) {
+ if ((position & combination) == combination) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Direction
+ public int getDirection() {
+ return mDirection;
+ }
+
+ @Position
+ public int getPosition() {
+ return mPosition;
+ }
+
+ public int getWeight() {
+ return mWeight;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java
new file mode 100644
index 0000000000000..f0239371ee636
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java
@@ -0,0 +1,67 @@
+/*
+ * 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 androidx.lifecycle.ViewModel;
+
+import javax.inject.Inject;
+
+/**
+ * {@link ComplicationViewModel} is an abstraction over {@link Complication}, providing the model
+ * from which any view-related interpretation of the {@link Complication} should be derived from.
+ */
+public class ComplicationViewModel extends ViewModel {
+ private final Complication mComplication;
+ private final ComplicationId mId;
+ private final Complication.Host mHost;
+
+ /**
+ * Default constructor for generating a {@link ComplicationViewModel}.
+ * @param complication The {@link Complication} represented by the view model.
+ * @param id The {@link ComplicationId} tied to this {@link Complication}.
+ * @param host The environment {@link Complication.Host}.
+ */
+ @Inject
+ public ComplicationViewModel(Complication complication, ComplicationId id,
+ Complication.Host host) {
+ mComplication = complication;
+ mId = id;
+ mHost = host;
+ }
+
+ /**
+ * Returns the {@link ComplicationId} for this {@link Complication} for stable id association.
+ */
+ public ComplicationId getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the underlying {@link Complication}. Should only as a redirection - for example,
+ * using the {@link Complication} to generate view. Any property should be surfaced through
+ * this ViewModel.
+ */
+ public Complication getComplication() {
+ return mComplication;
+ }
+
+ /**
+ * Requests the dream exit on behalf of the {@link Complication}.
+ */
+ public void exitDream() {
+ mHost.requestExitDream();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModelProvider.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModelProvider.java
new file mode 100644
index 0000000000000..cc17ea1ee7b90
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModelProvider.java
@@ -0,0 +1,33 @@
+/*
+ * 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 androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelStore;
+
+import com.android.systemui.dreams.complication.dagger.DaggerViewModelProviderFactory;
+
+import javax.inject.Inject;
+
+/**
+ * An intermediary to generate {@link ComplicationViewModel} tracked with a {@link ViewModelStore}.
+ */
+public class ComplicationViewModelProvider extends ViewModelProvider {
+ @Inject
+ public ComplicationViewModelProvider(ViewModelStore store, ComplicationViewModel viewModel) {
+ super(store, new DaggerViewModelProviderFactory(() -> viewModel));
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModelTransformer.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModelTransformer.java
new file mode 100644
index 0000000000000..5d113dde624e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModelTransformer.java
@@ -0,0 +1,56 @@
+/*
+ * 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 com.android.systemui.dreams.complication.dagger.ComplicationViewModelComponent;
+
+import java.util.HashMap;
+
+import javax.inject.Inject;
+
+/**
+ * The {@link ComplicationViewModelTransformer} responsibility is provide a mapping from
+ * {@link Complication} to {@link ComplicationViewModel}.
+ */
+public class ComplicationViewModelTransformer {
+ private final ComplicationId.Factory mComplicationIdFactory = new ComplicationId.Factory();
+ private final HashMap mComplicationIdMapping = new HashMap<>();
+ private final ComplicationViewModelComponent.Factory mViewModelComponentFactory;
+
+ @Inject
+ public ComplicationViewModelTransformer(
+ ComplicationViewModelComponent.Factory viewModelComponentFactory) {
+ mViewModelComponentFactory = viewModelComponentFactory;
+ }
+
+ /**
+ * Generates {@link ComplicationViewModel} from a {@link Complication}.
+ */
+ public ComplicationViewModel getViewModel(Complication complication) {
+ final ComplicationId id = getComplicationId(complication);
+ return mViewModelComponentFactory.create(complication, id)
+ .getViewModelProvider().get(id.toString(), ComplicationViewModel.class);
+ }
+
+ private ComplicationId getComplicationId(Complication complication) {
+ if (!mComplicationIdMapping.containsKey(complication)) {
+ mComplicationIdMapping.put(complication, mComplicationIdFactory.getNextId());
+ }
+
+ return mComplicationIdMapping.get(complication);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewComponent.java
new file mode 100644
index 0000000000000..4cc905e9a1907
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewComponent.java
@@ -0,0 +1,89 @@
+/*
+ * 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.view.LayoutInflater;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.android.internal.util.Preconditions;
+import com.android.systemui.R;
+import com.android.systemui.dreams.complication.ComplicationHostViewController;
+
+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;
+
+/**
+ * {@link ComplicationHostViewComponent} encapsulates the shared logic around the host view layer
+ * for complications. Anything that references the layout should be provided through this component
+ * and its child module. The factory should be used in order to best tie the lifetime of the view
+ * to components.
+ */
+@Subcomponent(modules = {
+ ComplicationHostViewComponent.ComplicationHostViewModule.class,
+})
+@ComplicationHostViewComponent.ComplicationHostViewScope
+public interface ComplicationHostViewComponent {
+ String SCOPED_COMPLICATIONS_LAYOUT = "scoped_complications_layout";
+
+ /** Scope annotation for singleton items within {@link ComplicationHostViewComponent}. */
+ @Documented
+ @Retention(RUNTIME)
+ @Scope
+ @interface ComplicationHostViewScope {}
+
+ /**
+ * Factory for generating a new scoped component.
+ */
+ @Subcomponent.Factory
+ interface Factory {
+ ComplicationHostViewComponent create();
+ }
+
+ /** */
+ ComplicationHostViewController getController();
+
+ /**
+ * Module for providing a scoped host view.
+ */
+ @Module
+ abstract class ComplicationHostViewModule {
+ /**
+ * Generates a {@link ConstraintLayout}, which can host
+ * {@link com.android.systemui.dreams.complication.Complication} instances.
+ */
+ @Provides
+ @Named(SCOPED_COMPLICATIONS_LAYOUT)
+ @ComplicationHostViewScope
+ static ConstraintLayout providesComplicationHostView(
+ LayoutInflater layoutInflater) {
+ return Preconditions.checkNotNull((ConstraintLayout)
+ layoutInflater.inflate(R.layout.dream_overlay_complications_layer,
+ null),
+ "R.layout.dream_overlay_complications_layer did not properly inflated");
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java
new file mode 100644
index 0000000000000..b29e8c9ec5a75
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java
@@ -0,0 +1,64 @@
+/*
+ * 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 androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelStore;
+
+import com.android.systemui.dreams.complication.ComplicationCollectionViewModel;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Named;
+import javax.inject.Scope;
+
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Module for housing components related to rendering complications.
+ */
+@Module(subcomponents = {
+ ComplicationViewModelComponent.class,
+ ComplicationHostViewComponent.class,
+})
+public interface ComplicationModule {
+ String SCOPED_COMPLICATIONS_MODEL = "scoped_complications_model";
+
+ /** Scope annotation for singleton items within the {@link ComplicationModule}. */
+ @Documented
+ @Retention(RUNTIME)
+ @Scope
+ @interface ComplicationScope {}
+
+ /**
+ * The complication collection is provided through this way to ensure that the instances are
+ * tied to the {@link ViewModelStore}.
+ */
+ @Provides
+ @Named(SCOPED_COMPLICATIONS_MODEL)
+ static ComplicationCollectionViewModel providesComplicationCollectionViewModel(
+ ViewModelStore store, ComplicationCollectionViewModel viewModel) {
+ final ViewModelProvider provider = new ViewModelProvider(store,
+ new DaggerViewModelProviderFactory(() -> viewModel));
+
+ return provider.get(ComplicationCollectionViewModel.class);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationViewModelComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationViewModelComponent.java
new file mode 100644
index 0000000000000..703cd28a3c254
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationViewModelComponent.java
@@ -0,0 +1,44 @@
+/*
+ * 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 com.android.systemui.dreams.complication.Complication;
+import com.android.systemui.dreams.complication.ComplicationId;
+import com.android.systemui.dreams.complication.ComplicationViewModelProvider;
+
+import dagger.BindsInstance;
+import dagger.Subcomponent;
+
+/**
+ * The {@link ComplicationViewModelComponent} allows for a
+ * {@link com.android.systemui.dreams.complication.ComplicationViewModel} for a particular
+ * {@link Complication}. This component binds these instance specific values to allow injection with
+ * values provided at the wider scope.
+ */
+@Subcomponent
+public interface ComplicationViewModelComponent {
+ /**
+ * Factory for generating {@link ComplicationViewModelComponent}.
+ */
+ @Subcomponent.Factory
+ interface Factory {
+ ComplicationViewModelComponent create(@BindsInstance Complication complication,
+ @BindsInstance ComplicationId id);
+ }
+
+ /** */
+ ComplicationViewModelProvider getViewModelProvider();
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DaggerViewModelProviderFactory.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DaggerViewModelProviderFactory.java
new file mode 100644
index 0000000000000..8ffedec9b4921
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DaggerViewModelProviderFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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 androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+/**
+ * {@link DaggerViewModelProviderFactory} is a wrapper around a lambda allowing for uses of Dagger
+ * component.
+ */
+public class DaggerViewModelProviderFactory implements ViewModelProvider.Factory {
+ /**
+ * An interface for providing a {@link ViewModel} through
+ * {@link DaggerViewModelProviderFactory}.
+ */
+ public interface ViewModelCreator {
+ /**
+ * Creates a {@link ViewModel} to be returned.
+ */
+ ViewModel create();
+ }
+
+ private final ViewModelCreator mCreator;
+
+ public DaggerViewModelProviderFactory(ViewModelCreator creator) {
+ mCreator = creator;
+ }
+
+ @NonNull
+ @Override
+ public T create(@NonNull Class aClass) {
+ return (T) mCreator.create();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 072f50db64f60..d5053a03fec64 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -22,6 +22,7 @@ import dagger.Module;
* Dagger Module providing Communal-related functionality.
*/
@Module(subcomponents = {
- DreamOverlayComponent.class})
+ DreamOverlayComponent.class,
+})
public interface DreamModule {
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
index c90332bb9f31a..f0ab6964faa1b 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
@@ -18,25 +18,36 @@ package com.android.systemui.dreams.dagger;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.ViewModelStore;
+
import com.android.systemui.dreams.DreamOverlayContainerViewController;
+import com.android.systemui.dreams.complication.Complication;
+import com.android.systemui.dreams.complication.dagger.ComplicationModule;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import javax.inject.Scope;
+import dagger.BindsInstance;
import dagger.Subcomponent;
/**
* Dagger subcomponent for {@link DreamOverlayModule}.
*/
-@Subcomponent(modules = {DreamOverlayModule.class})
+@Subcomponent(modules = {
+ DreamOverlayModule.class,
+ ComplicationModule.class,
+})
@DreamOverlayComponent.DreamOverlayScope
public interface DreamOverlayComponent {
/** Simple factory for {@link DreamOverlayComponent}. */
@Subcomponent.Factory
interface Factory {
- DreamOverlayComponent create();
+ DreamOverlayComponent create(@BindsInstance ViewModelStore store,
+ @BindsInstance Complication.Host host);
}
/** Scope annotation for singleton items within the {@link DreamOverlayComponent}. */
@@ -46,6 +57,11 @@ public interface DreamOverlayComponent {
@interface DreamOverlayScope {}
/** Builds a {@link DreamOverlayContainerViewController}. */
- @DreamOverlayScope
DreamOverlayContainerViewController getDreamOverlayContainerViewController();
+
+ /** Builds a {@link androidx.lifecycle.LifecycleRegistry} */
+ LifecycleRegistry getLifecycleRegistry();
+
+ /** Builds a {@link androidx.lifecycle.LifecycleOwner} */
+ LifecycleOwner getLifecycleOwner();
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index d2912032ed395..b56aa2c92a274 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -22,6 +22,9 @@ import android.os.Handler;
import android.view.LayoutInflater;
import android.view.ViewGroup;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
import com.android.internal.util.Preconditions;
import com.android.systemui.R;
import com.android.systemui.battery.BatteryMeterView;
@@ -36,6 +39,7 @@ import com.android.systemui.tuner.TunerService;
import javax.inject.Named;
+import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
@@ -124,4 +128,16 @@ public abstract class DreamOverlayModule {
return resources.getInteger(
R.integer.config_dreamOverlayBurnInProtectionUpdateIntervalMillis);
}
+
+ @Provides
+ @DreamOverlayComponent.DreamOverlayScope
+ static LifecycleOwner providesLifecycleOwner(Lazy lifecycleRegistryLazy) {
+ return () -> lifecycleRegistryLazy.get();
+ }
+
+ @Provides
+ @DreamOverlayComponent.DreamOverlayScope
+ static LifecycleRegistry providesLifecycleRegistry(LifecycleOwner lifecycleOwner) {
+ return new LifecycleRegistry(lifecycleOwner);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/ComplicationProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/ComplicationProviderTest.java
deleted file mode 100644
index ada7ddbdb2879..0000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/ComplicationProviderTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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;
-
-import static org.junit.Assert.assertEquals;
-
-import android.content.Context;
-import android.testing.AndroidTestingRunner;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.settingslib.dream.DreamBackend;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class ComplicationProviderTest {
- private TestComplicationProvider mComplicationProvider;
-
- @Before
- public void setup() {
- mComplicationProvider = new TestComplicationProvider();
- }
-
- @Test
- public void testConvertComplicationType() {
- assertEquals(ComplicationProvider.COMPLICATION_TYPE_TIME,
- mComplicationProvider.convertComplicationType(DreamBackend.COMPLICATION_TYPE_TIME));
- assertEquals(ComplicationProvider.COMPLICATION_TYPE_DATE,
- mComplicationProvider.convertComplicationType(DreamBackend.COMPLICATION_TYPE_DATE));
- assertEquals(ComplicationProvider.COMPLICATION_TYPE_WEATHER,
- mComplicationProvider.convertComplicationType(
- DreamBackend.COMPLICATION_TYPE_WEATHER));
- assertEquals(ComplicationProvider.COMPLICATION_TYPE_AIR_QUALITY,
- mComplicationProvider.convertComplicationType(
- DreamBackend.COMPLICATION_TYPE_AIR_QUALITY));
- assertEquals(ComplicationProvider.COMPLICATION_TYPE_CAST_INFO,
- mComplicationProvider.convertComplicationType(
- DreamBackend.COMPLICATION_TYPE_CAST_INFO));
- }
-
- private static class TestComplicationProvider implements ComplicationProvider {
- @Override
- public void onCreateComplication(Context context,
- ComplicationHost.CreationCallback creationCallback,
- ComplicationHost.InteractionCallback interactionCallback) {
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 7c5f57fe0b390..7af039b7fa06c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -27,15 +27,15 @@ import static org.mockito.Mockito.when;
import android.content.res.Resources;
import android.os.Handler;
import android.testing.AndroidTestingRunner;
-import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
-import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.test.filters.SmallTest;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.complication.ComplicationHostViewController;
+import com.android.systemui.dreams.complication.dagger.ComplicationHostViewComponent;
import org.junit.Before;
import org.junit.Test;
@@ -63,6 +63,15 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
@Mock
DreamOverlayContainerView mDreamOverlayContainerView;
+ @Mock
+ ComplicationHostViewController mComplicationHostViewController;
+
+ @Mock
+ ComplicationHostViewComponent.Factory mComplicationHostViewComponentFactory;
+
+ @Mock
+ ComplicationHostViewComponent mComplicationHostViewComponent;
+
@Mock
ViewGroup mDreamOverlayContentView;
@@ -80,9 +89,14 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
DREAM_OVERLAY_NOTIFICATIONS_DRAG_AREA_HEIGHT);
when(mDreamOverlayContainerView.getResources()).thenReturn(mResources);
when(mDreamOverlayContainerView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
+ when(mComplicationHostViewComponentFactory.create())
+ .thenReturn(mComplicationHostViewComponent);
+ when(mComplicationHostViewComponent.getController())
+ .thenReturn(mComplicationHostViewController);
mController = new DreamOverlayContainerViewController(
mDreamOverlayContainerView,
+ mComplicationHostViewComponentFactory,
mDreamOverlayContentView,
mDreamOverlayStatusBarViewController,
mHandler,
@@ -103,20 +117,6 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
DREAM_OVERLAY_NOTIFICATIONS_DRAG_AREA_HEIGHT);
}
- @Test
- public void testAddOverlayAddsOverlayToContentView() {
- View overlay = new View(getContext());
- ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(100, 100);
- mController.addOverlay(overlay, layoutParams);
- verify(mDreamOverlayContentView).addView(overlay, layoutParams);
- }
-
- @Test
- public void testRemoveAllOverlaysRemovesOverlaysFromContentView() {
- mController.removeAllOverlays();
- verify(mDreamOverlayContentView).removeAllViews();
- }
-
@Test
public void testOnViewAttachedRegistersComputeInsetsListener() {
mController.onViewAttached();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index c0b7271c6de76..6b156a42dcc81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -28,12 +28,11 @@ import android.service.dreams.DreamService;
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.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -48,18 +47,21 @@ 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 final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
+ @Mock
+ LifecycleOwner mLifecycleOwner;
+
+ @Mock
+ LifecycleRegistry mLifecycleRegistry;
+
@Rule
public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck();
@@ -75,12 +77,6 @@ public class DreamOverlayServiceTest extends SysuiTestCase {
@Mock
WindowManagerImpl mWindowManager;
- @Mock
- ComplicationProvider mProvider;
-
- @Mock
- DreamOverlayStateController mDreamOverlayStateController;
-
@Mock
DreamOverlayComponent.Factory mDreamOverlayComponentFactory;
@@ -102,13 +98,18 @@ public class DreamOverlayServiceTest extends SysuiTestCase {
when(mDreamOverlayComponent.getDreamOverlayContainerViewController())
.thenReturn(mDreamOverlayContainerViewController);
- when(mDreamOverlayComponentFactory.create())
+ when(mDreamOverlayComponent.getLifecycleOwner())
+ .thenReturn(mLifecycleOwner);
+ when(mDreamOverlayComponent.getLifecycleRegistry())
+ .thenReturn(mLifecycleRegistry);
+ when(mDreamOverlayComponentFactory
+ .create(any(), any()))
.thenReturn(mDreamOverlayComponent);
when(mDreamOverlayContainerViewController.getContainerView())
.thenReturn(mDreamOverlayContainerView);
mService = new DreamOverlayService(mContext, mMainExecutor,
- mDreamOverlayStateController, mDreamOverlayComponentFactory);
+ mDreamOverlayComponentFactory);
final IBinder proxy = mService.onBind(new Intent());
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
@@ -127,78 +128,6 @@ public class DreamOverlayServiceTest extends SysuiTestCase {
verify(mDreamOverlayContainerViewController).init();
}
- @Test
- public void testAddingOverlayToDream() throws Exception {
- // Add overlay.
- mService.addComplication(mProvider);
- mMainExecutor.runAllReady();
-
- final ArgumentCaptor creationCallbackCapture =
- ArgumentCaptor.forClass(ComplicationHost.CreationCallback.class);
- final ArgumentCaptor interactionCallbackCapture =
- ArgumentCaptor.forClass(ComplicationHost.InteractionCallback.class);
-
- // Ensure overlay provider is asked to create view.
- verify(mProvider).onCreateComplication(any(), creationCallbackCapture.capture(),
- interactionCallbackCapture.capture());
- mMainExecutor.runAllReady();
-
- // Inform service of overlay view creation.
- final View view = new View(mContext);
- final ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
- );
- creationCallbackCapture.getValue().onCreated(view, lp);
- mMainExecutor.runAllReady();
-
- // Verify that DreamOverlayContainerViewController is asked to add an overlay for the view.
- verify(mDreamOverlayContainerViewController).addOverlay(view, lp);
- }
-
- @Test
- public void testDreamOverlayExit() throws Exception {
- // Add overlay.
- mService.addComplication(mProvider);
- mMainExecutor.runAllReady();
-
- // Capture interaction callback from overlay creation.
- final ArgumentCaptor interactionCallbackCapture =
- ArgumentCaptor.forClass(ComplicationHost.InteractionCallback.class);
- verify(mProvider).onCreateComplication(any(), any(), interactionCallbackCapture.capture());
-
- // Ask service to exit.
- interactionCallbackCapture.getValue().onExit();
- mMainExecutor.runAllReady();
-
- // Ensure service informs dream host of exit.
- verify(mDreamOverlayCallback).onExitRequested();
- }
-
- @Test
- public void testListenerRegisteredWithDreamOverlayStateController() {
- // Verify overlay service registered as listener with DreamOverlayStateController
- // and inform callback of addition.
- final ArgumentCaptor callbackCapture =
- ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
-
- verify(mDreamOverlayStateController).addCallback(callbackCapture.capture());
- when(mDreamOverlayStateController.getComplications()).thenReturn(Arrays.asList(mProvider));
- callbackCapture.getValue().onComplicationsChanged();
- mMainExecutor.runAllReady();
-
- // Verify provider is asked to create overlay.
- verify(mProvider).onCreateComplication(any(), any(), any());
- }
-
- @Test
- public void testOnDestroyRemovesOverlayStateCallback() {
- final ArgumentCaptor callbackCapture =
- ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
- verify(mDreamOverlayStateController).addCallback(callbackCapture.capture());
- mService.onDestroy();
- verify(mDreamOverlayStateController).removeCallback(callbackCapture.getValue());
- }
-
@Test
public void testShouldShowComplicationsTrueByDefault() {
assertThat(mService.shouldShowComplications()).isTrue();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index efc3c7c7611ad..7d0833db7ae41 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -27,11 +27,10 @@ import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
-import com.google.common.util.concurrent.ListenableFuture;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -47,7 +46,7 @@ public class DreamOverlayStateControllerTest extends SysuiTestCase {
DreamOverlayStateController.Callback mCallback;
@Mock
- ComplicationProvider mProvider;
+ Complication mComplication;
final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
@@ -63,24 +62,23 @@ public class DreamOverlayStateControllerTest extends SysuiTestCase {
stateController.addCallback(mCallback);
// Add complication and verify callback is notified.
- final ListenableFuture tokenFuture =
- stateController.addComplication(mProvider);
+ stateController.addComplication(mComplication);
mExecutor.runAllReady();
verify(mCallback, times(1)).onComplicationsChanged();
- final Collection providers = stateController.getComplications();
- assertEquals(providers.size(), 1);
- assertTrue(providers.contains(mProvider));
+ final Collection complications = stateController.getComplications();
+ assertEquals(complications.size(), 1);
+ assertTrue(complications.contains(mComplication));
clearInvocations(mCallback);
// Remove complication and verify callback is notified.
- stateController.removeComplication(tokenFuture.get());
+ stateController.removeComplication(mComplication);
mExecutor.runAllReady();
verify(mCallback, times(1)).onComplicationsChanged();
- assertTrue(providers.isEmpty());
+ assertTrue(stateController.getComplications().isEmpty());
}
@Test
@@ -88,7 +86,7 @@ public class DreamOverlayStateControllerTest extends SysuiTestCase {
final DreamOverlayStateController stateController =
new DreamOverlayStateController(mExecutor);
- stateController.addComplication(mProvider);
+ stateController.addComplication(mComplication);
mExecutor.runAllReady();
// Verify callback occurs on add when an overlay is already present.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java
new file mode 100644
index 0000000000000..afc0309dd1950
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.lifecycle.Observer;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+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.Mockito;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class ComplicationCollectionLiveDataTest extends SysuiTestCase {
+ @Before
+ public void setUp() throws Exception {
+ allowTestableLooperAsMainThread();
+ }
+
+ @Test
+ /**
+ * Ensures registration and callback lifecycles are respected.
+ */
+ public void testLifecycle() {
+ getContext().getMainExecutor().execute(() -> {
+ final DreamOverlayStateController stateController =
+ Mockito.mock(DreamOverlayStateController.class);
+ final ComplicationCollectionLiveData liveData =
+ new ComplicationCollectionLiveData(stateController);
+ final HashSet complications = new HashSet<>();
+ final Observer> observer = Mockito.mock(Observer.class);
+ complications.add(Mockito.mock(Complication.class));
+
+ when(stateController.getComplications()).thenReturn(complications);
+
+ liveData.observeForever(observer);
+ ArgumentCaptor callbackCaptor =
+ ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+
+ verify(stateController).addCallback(callbackCaptor.capture());
+ verifyUpdate(observer, complications);
+
+ complications.add(Mockito.mock(Complication.class));
+ callbackCaptor.getValue().onComplicationsChanged();
+
+ verifyUpdate(observer, complications);
+ });
+ }
+
+ void verifyUpdate(Observer> observer,
+ Collection targetCollection) {
+ ArgumentCaptor> collectionCaptor =
+ ArgumentCaptor.forClass(Collection.class);
+
+ verify(observer).onChanged(collectionCaptor.capture());
+
+ assertThat(collectionCaptor.getValue().equals(targetCollection)).isTrue();
+ Mockito.clearInvocations(observer);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
new file mode 100644
index 0000000000000..3b9e398141ee5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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 org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+import android.view.View;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+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.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ComplicationHostViewControllerTest extends SysuiTestCase {
+ @Mock
+ ConstraintLayout mComplicationHostView;
+
+ @Mock
+ LifecycleOwner mLifecycleOwner;
+
+ @Mock
+ LiveData> mComplicationViewModelLiveData;
+
+ @Mock
+ ComplicationCollectionViewModel mViewModel;
+
+ @Mock
+ ComplicationViewModel mComplicationViewModel;
+
+ @Mock
+ ComplicationLayoutEngine mLayoutEngine;
+
+ @Mock
+ ComplicationId mComplicationId;
+
+ @Mock
+ Complication mComplication;
+
+ @Mock
+ Complication.ViewHolder mViewHolder;
+
+ @Mock
+ View mComplicationView;
+
+ @Mock
+ ComplicationLayoutParams mComplicationLayoutParams;
+
+ @Complication.Category
+ static final int COMPLICATION_CATEGORY = Complication.CATEGORY_SYSTEM;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mViewModel.getComplications()).thenReturn(mComplicationViewModelLiveData);
+ when(mComplicationViewModel.getId()).thenReturn(mComplicationId);
+ when(mComplicationViewModel.getComplication()).thenReturn(mComplication);
+ when(mComplication.createView(eq(mComplicationViewModel))).thenReturn(mViewHolder);
+ when(mViewHolder.getView()).thenReturn(mComplicationView);
+ when(mViewHolder.getCategory()).thenReturn(COMPLICATION_CATEGORY);
+ when(mViewHolder.getLayoutParams()).thenReturn(mComplicationLayoutParams);
+ when(mComplicationView.getParent()).thenReturn(mComplicationHostView);
+ }
+
+ /**
+ * Ensures the lifecycle of complications is properly handled.
+ */
+ @Test
+ public void testViewModelObservation() {
+ final ArgumentCaptor>> observerArgumentCaptor =
+ ArgumentCaptor.forClass(Observer.class);
+ final ComplicationHostViewController controller = new ComplicationHostViewController(
+ mComplicationHostView,
+ mLayoutEngine,
+ mLifecycleOwner,
+ mViewModel);
+
+ controller.init();
+
+ verify(mComplicationViewModelLiveData).observe(eq(mLifecycleOwner),
+ observerArgumentCaptor.capture());
+
+ final Observer> observer =
+ observerArgumentCaptor.getValue();
+
+ // Add complication and ensure it is added to the view.
+ final HashSet complications = new HashSet<>(
+ Arrays.asList(mComplicationViewModel));
+ observer.onChanged(complications);
+
+ verify(mLayoutEngine).addComplication(eq(mComplicationId), eq(mComplicationView),
+ eq(mComplicationLayoutParams), eq(COMPLICATION_CATEGORY));
+
+ // Remove complication and ensure it is removed from the view by id.
+ observer.onChanged(new HashSet<>());
+
+ verify(mLayoutEngine).removeComplication(eq(mComplicationId));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java
new file mode 100644
index 0000000000000..f227a9b78c39f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java
@@ -0,0 +1,303 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+import android.view.View;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+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.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Consumer;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ComplicationLayoutEngineTest extends SysuiTestCase {
+ @Mock
+ ConstraintLayout mLayout;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private static class ViewInfo {
+ private static int sNextId = 1;
+ public final ComplicationId id;
+ public final View view;
+ public final ComplicationLayoutParams lp;
+
+ @Complication.Category
+ public final int category;
+
+ private static ComplicationId.Factory sFactory = new ComplicationId.Factory();
+
+ ViewInfo(ComplicationLayoutParams params, @Complication.Category int category,
+ ConstraintLayout layout) {
+ this.lp = params;
+ this.category = category;
+ this.view = Mockito.mock(View.class);
+ this.id = sFactory.getNextId();
+ when(view.getId()).thenReturn(sNextId++);
+ when(view.getParent()).thenReturn(layout);
+ }
+
+ void clearInvocations() {
+ Mockito.clearInvocations(view);
+ }
+ }
+
+ private void verifyChange(ViewInfo viewInfo,
+ boolean verifyAdd,
+ Consumer paramConsumer) {
+ ArgumentCaptor lpCaptor =
+ ArgumentCaptor.forClass(ConstraintLayout.LayoutParams.class);
+ verify(viewInfo.view).setLayoutParams(lpCaptor.capture());
+
+ if (verifyAdd) {
+ verify(mLayout).addView(eq(viewInfo.view));
+ }
+
+ ConstraintLayout.LayoutParams capturedParams = lpCaptor.getValue();
+ paramConsumer.accept(capturedParams);
+ }
+
+ private void addComplication(ComplicationLayoutEngine engine, ViewInfo info) {
+ engine.addComplication(info.id, info.view, info.lp, info.category);
+ }
+
+ /**
+ * Makes sure the engine properly places a view within the {@link ConstraintLayout}.
+ */
+ @Test
+ public void testSingleLayout() {
+ final ViewInfo firstViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 0),
+ Complication.CATEGORY_STANDARD,
+ mLayout);
+
+ final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout);
+ addComplication(engine, firstViewInfo);
+
+ // Ensure the view is added to the top end corner
+ verifyChange(firstViewInfo, true, lp -> {
+ assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ });
+ }
+
+ /**
+ * Ensures layout in a particular direction updates.
+ */
+ @Test
+ public void testDirectionLayout() {
+ final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout);
+
+ final ViewInfo firstViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 0),
+ Complication.CATEGORY_STANDARD,
+ mLayout);
+
+ addComplication(engine, firstViewInfo);
+
+ firstViewInfo.clearInvocations();
+
+ final ViewInfo secondViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 0),
+ Complication.CATEGORY_SYSTEM,
+ mLayout);
+
+ addComplication(engine, secondViewInfo);
+
+ // The first added view should now be underneath the second view.
+ verifyChange(firstViewInfo, false, lp -> {
+ assertThat(lp.topToBottom == secondViewInfo.view.getId()).isTrue();
+ assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ });
+
+ // The second view should be in the top position.
+ verifyChange(secondViewInfo, true, lp -> {
+ assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ });
+ }
+
+ /**
+ * Ensures layout in a particular position updates.
+ */
+ @Test
+ public void testPositionLayout() {
+ final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout);
+
+ final ViewInfo firstViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 0),
+ Complication.CATEGORY_STANDARD,
+ mLayout);
+
+ addComplication(engine, firstViewInfo);
+
+ final ViewInfo secondViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 0),
+ Complication.CATEGORY_SYSTEM,
+ mLayout);
+
+ addComplication(engine, secondViewInfo);
+
+ firstViewInfo.clearInvocations();
+ secondViewInfo.clearInvocations();
+
+ final ViewInfo thirdViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_START,
+ 1),
+ Complication.CATEGORY_SYSTEM,
+ mLayout);
+
+ addComplication(engine, thirdViewInfo);
+
+ // The first added view should now be underneath the second view.
+ verifyChange(firstViewInfo, false, lp -> {
+ assertThat(lp.topToBottom == secondViewInfo.view.getId()).isTrue();
+ assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ });
+
+ // The second view should be in underneath the third view.
+ verifyChange(secondViewInfo, false, lp -> {
+ assertThat(lp.topToBottom == thirdViewInfo.view.getId()).isTrue();
+ assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ });
+
+ // The third view should be in at the top.
+ verifyChange(thirdViewInfo, true, lp -> {
+ assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ });
+
+ final ViewInfo fourthViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_START,
+ 1),
+ Complication.CATEGORY_STANDARD,
+ mLayout);
+
+ addComplication(engine, fourthViewInfo);
+
+ verifyChange(fourthViewInfo, true, lp -> {
+ assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ assertThat(lp.endToStart == thirdViewInfo.view.getId()).isTrue();
+ });
+ }
+
+ /**
+ * Ensures layout in a particular position updates.
+ */
+ @Test
+ public void testRemoval() {
+ final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout);
+
+ final ViewInfo firstViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 0),
+ Complication.CATEGORY_STANDARD,
+ mLayout);
+
+ engine.addComplication(firstViewInfo.id, firstViewInfo.view, firstViewInfo.lp,
+ firstViewInfo.category);
+
+ final ViewInfo secondViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 0),
+ Complication.CATEGORY_SYSTEM,
+ mLayout);
+
+ engine.addComplication(secondViewInfo.id, secondViewInfo.view, secondViewInfo.lp,
+ secondViewInfo.category);
+
+ firstViewInfo.clearInvocations();
+
+ engine.removeComplication(secondViewInfo.id);
+ verify(mLayout).removeView(eq(secondViewInfo.view));
+
+ verifyChange(firstViewInfo, true, lp -> {
+ assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
+ });
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
new file mode 100644
index 0000000000000..d080bbca06168
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ComplicationLayoutParamsTest extends SysuiTestCase {
+ /**
+ * Ensures ComplicationLayoutParams cannot be constructed with improper position or direction.
+ */
+ @Test
+ public void testPositionValidation() {
+ final HashSet invalidCombinations = new HashSet(Arrays.asList(
+ ComplicationLayoutParams.POSITION_BOTTOM | ComplicationLayoutParams.POSITION_TOP,
+ ComplicationLayoutParams.POSITION_END | ComplicationLayoutParams.POSITION_START
+ ));
+
+ final int allPositions = ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_START
+ | ComplicationLayoutParams.POSITION_END
+ | ComplicationLayoutParams.POSITION_BOTTOM;
+
+ final HashSet allDirections = new HashSet(Arrays.asList(
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ ComplicationLayoutParams.DIRECTION_UP,
+ ComplicationLayoutParams.DIRECTION_START,
+ ComplicationLayoutParams.DIRECTION_END
+ ));
+
+ final HashMap invalidDirections = new HashMap<>();
+ invalidDirections.put(ComplicationLayoutParams.DIRECTION_DOWN,
+ ComplicationLayoutParams.POSITION_BOTTOM);
+ invalidDirections.put(ComplicationLayoutParams.DIRECTION_UP,
+ ComplicationLayoutParams.POSITION_TOP);
+ invalidDirections.put(ComplicationLayoutParams.DIRECTION_START,
+ ComplicationLayoutParams.POSITION_START);
+ invalidDirections.put(ComplicationLayoutParams.DIRECTION_END,
+ ComplicationLayoutParams.POSITION_END);
+
+
+ for (int position = 0; position <= allPositions; ++position) {
+ boolean properPosition = position != 0;
+ if (properPosition) {
+ for (Integer combination : invalidCombinations) {
+ if ((combination & position) == combination) {
+ properPosition = false;
+ }
+ }
+ }
+ boolean exceptionEncountered = false;
+ for (Integer direction : allDirections) {
+ final int invalidPosition = invalidDirections.get(direction);
+ final boolean properDirection = (invalidPosition & position) != invalidPosition;
+
+ try {
+ final ComplicationLayoutParams params = new ComplicationLayoutParams(
+ 100,
+ 100,
+ position,
+ direction,
+ 0);
+ } catch (Exception e) {
+ exceptionEncountered = true;
+ }
+
+ assertThat((properPosition && properDirection) || exceptionEncountered).isTrue();
+ }
+ }
+ }
+
+ /**
+ * Ensures ComplicationLayoutParams is properly duplicated on copy construction.
+ */
+ @Test
+ public void testCopyConstruction() {
+ final ComplicationLayoutParams params = new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 3);
+ final ComplicationLayoutParams copy = new ComplicationLayoutParams(params);
+
+ assertThat(copy.getDirection() == params.getDirection()).isTrue();
+ assertThat(copy.getPosition() == params.getPosition()).isTrue();
+ assertThat(copy.getWeight() == params.getWeight()).isTrue();
+ assertThat(copy.height == params.height).isTrue();
+ assertThat(copy.width == params.width).isTrue();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationViewModelTransformerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationViewModelTransformerTest.java
new file mode 100644
index 0000000000000..2bc427d1cd971
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationViewModelTransformerTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.complication.dagger.ComplicationViewModelComponent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ComplicationViewModelTransformerTest extends SysuiTestCase {
+ @Mock
+ ComplicationViewModelComponent.Factory mFactory;
+
+ @Mock
+ ComplicationViewModelComponent mComponent;
+
+ @Mock
+ ComplicationViewModelProvider mViewModelProvider;
+
+ @Mock
+ ComplicationViewModel mViewModel;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mFactory.create(Mockito.any(), Mockito.any())).thenReturn(mComponent);
+ when(mComponent.getViewModelProvider()).thenReturn(mViewModelProvider);
+ when(mViewModelProvider.get(Mockito.any(), Mockito.any())).thenReturn(mViewModel);
+ }
+
+ /**
+ * Ensure the same id is returned for the same complication across invocations.
+ */
+ @Test
+ public void testStableIds() {
+ final ComplicationViewModelTransformer transformer =
+ new ComplicationViewModelTransformer(mFactory);
+
+ final Complication complication = Mockito.mock(Complication.class);
+
+ ArgumentCaptor idCaptor = ArgumentCaptor.forClass(ComplicationId.class);
+
+ transformer.getViewModel(complication);
+ verify(mFactory).create(Mockito.any(), idCaptor.capture());
+ final ComplicationId firstId = idCaptor.getValue();
+
+ Mockito.clearInvocations(mFactory);
+
+ transformer.getViewModel(complication);
+ verify(mFactory).create(Mockito.any(), idCaptor.capture());
+ final ComplicationId secondId = idCaptor.getValue();
+
+ assertEquals(secondId, firstId);
+ }
+
+ /**
+ * Ensure unique ids are assigned to different complications.
+ */
+ @Test
+ public void testUniqueIds() {
+ final ComplicationViewModelTransformer transformer =
+ new ComplicationViewModelTransformer(mFactory);
+
+ final Complication firstComplication = Mockito.mock(Complication.class);
+ final Complication secondComplication = Mockito.mock(Complication.class);
+
+ ArgumentCaptor idCaptor = ArgumentCaptor.forClass(ComplicationId.class);
+
+ transformer.getViewModel(firstComplication);
+ verify(mFactory).create(Mockito.any(), idCaptor.capture());
+ final ComplicationId firstId = idCaptor.getValue();
+
+ Mockito.clearInvocations(mFactory);
+
+ transformer.getViewModel(secondComplication);
+ verify(mFactory).create(Mockito.any(), idCaptor.capture());
+ final ComplicationId secondId = idCaptor.getValue();
+
+ assertNotEquals(secondId, firstId);
+ }
+}