diff --git a/packages/SystemUI/res/layout/dream_overlay_complications_layer.xml b/packages/SystemUI/res/layout/dream_overlay_complications_layer.xml index f898ef65213ad..51359471ff987 100644 --- a/packages/SystemUI/res/layout/dream_overlay_complications_layer.xml +++ b/packages/SystemUI/res/layout/dream_overlay_complications_layer.xml @@ -18,7 +18,6 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/dream_overlay_complications_layer" - android:padding="20dp" android:layout_width="match_parent" android:layout_height="match_parent"> - - reloadComplicationsLocked()); - } - }; + private final Complication.Host mHost = new Complication.Host() { + @Override + public void requestExitDream() { + mExecutor.execute(DreamOverlayService.this::requestExit); + } + }; + + private final LifecycleRegistry mLifecycleRegistry; + + private ViewModelStore mViewModelStore = new ViewModelStore(); @Inject public DreamOverlayService( Context context, @Main Executor executor, - DreamOverlayStateController overlayStateController, DreamOverlayComponent.Factory dreamOverlayComponentFactory) { mContext = context; mExecutor = executor; - mStateController = overlayStateController; - mDreamOverlayContainerViewController = - dreamOverlayComponentFactory.create().getDreamOverlayContainerViewController(); - mStateController.addCallback(mOverlayStateCallback); + final DreamOverlayComponent component = + dreamOverlayComponentFactory.create(mViewModelStore, mHost); + mDreamOverlayContainerViewController = component.getDreamOverlayContainerViewController(); + setCurrentState(Lifecycle.State.CREATED); + mLifecycleRegistry = component.getLifecycleRegistry(); + } + + private void setCurrentState(Lifecycle.State state) { + mExecutor.execute(() -> mLifecycleRegistry.setCurrentState(state)); } @Override public void onDestroy() { + setCurrentState(Lifecycle.State.DESTROYED); final WindowManager windowManager = mContext.getSystemService(WindowManager.class); windowManager.removeView(mWindow.getDecorView()); - mStateController.removeCallback(mOverlayStateCallback); super.onDestroy(); } @Override public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) { - mExecutor.execute(() -> addOverlayWindowLocked(layoutParams)); - } - - private void reloadComplicationsLocked() { - mDreamOverlayContainerViewController.removeAllOverlays(); - for (ComplicationProvider overlayProvider : mStateController.getComplications()) { - addComplication(overlayProvider); - } + setCurrentState(Lifecycle.State.STARTED); + mExecutor.execute(() -> { + addOverlayWindowLocked(layoutParams); + setCurrentState(Lifecycle.State.RESUMED); + }); } /** @@ -129,20 +134,5 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ final WindowManager windowManager = mContext.getSystemService(WindowManager.class); windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes()); - mExecutor.execute(this::reloadComplicationsLocked); - } - - @VisibleForTesting - protected void addComplication(ComplicationProvider provider) { - provider.onCreateComplication(mContext, - (view, layoutParams) -> { - // Always move UI related work to the main thread. - mExecutor.execute(() -> mDreamOverlayContainerViewController - .addOverlay(view, layoutParams)); - }, - () -> { - // The Callback is set on the main thread. - mExecutor.execute(this::requestExit); - }); } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java index 66679bb4ee449..e83884819f706 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java @@ -17,18 +17,17 @@ package com.android.systemui.dreams; import androidx.annotation.NonNull; -import androidx.concurrent.futures.CallbackToFutureAdapter; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dreams.complication.Complication; import com.android.systemui.statusbar.policy.CallbackController; -import com.google.common.util.concurrent.ListenableFuture; - import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.Collections; +import java.util.HashSet; import java.util.Objects; import java.util.concurrent.Executor; @@ -42,35 +41,6 @@ import javax.inject.Inject; @SysUISingleton public class DreamOverlayStateController implements CallbackController { - // A counter for guaranteeing unique complications tokens within the scope of this state - // controller. - private int mNextComplicationTokenId = 0; - - /** - * {@link ComplicationToken} provides a unique key for identifying {@link ComplicationProvider} - * instances registered with {@link DreamOverlayStateController}. - */ - public static class ComplicationToken { - private final int mId; - - private ComplicationToken(int id) { - mId = id; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof ComplicationToken)) return false; - ComplicationToken that = (ComplicationToken) o; - return mId == that.mId; - } - - @Override - public int hashCode() { - return Objects.hash(mId); - } - } - /** * Callback for dream overlay events. */ @@ -84,7 +54,8 @@ public class DreamOverlayStateController implements private final Executor mExecutor; private final ArrayList mCallbacks = new ArrayList<>(); - private final HashMap mComplications = new HashMap<>(); + + private final Collection mComplications = new HashSet(); @VisibleForTesting @Inject @@ -93,47 +64,32 @@ public class DreamOverlayStateController implements } /** - * Adds a complication to be presented on top of dreams. - * @param provider The {@link ComplicationProvider} providing the dream. - * @return The {@link ComplicationToken} tied to the supplied {@link ComplicationProvider}. + * Adds a complication to be included on the dream overlay. */ - public ListenableFuture addComplication(ComplicationProvider provider) { - return CallbackToFutureAdapter.getFuture(completer -> { - mExecutor.execute(() -> { - final ComplicationToken token = new ComplicationToken(mNextComplicationTokenId++); - mComplications.put(token, provider); - notifyCallbacks(); - completer.set(token); - }); - return "DreamOverlayStateController::addComplication"; + public void addComplication(Complication complication) { + mExecutor.execute(() -> { + if (mComplications.add(complication)) { + mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged()); + } }); } /** - * Removes a complication from being shown on dreams. - * @param token The {@link ComplicationToken} associated with the {@link ComplicationProvider} - * to be removed. - * @return The removed {@link ComplicationProvider}, {@code null} if not found. + * Removes a complication from inclusion on the dream overlay. */ - public ListenableFuture removeComplication(ComplicationToken token) { - return CallbackToFutureAdapter.getFuture(completer -> { - mExecutor.execute(() -> { - final ComplicationProvider removedComplication = mComplications.remove(token); - - if (removedComplication != null) { - notifyCallbacks(); - } - completer.set(removedComplication); - }); - - return "DreamOverlayStateController::removeComplication"; + public void removeComplication(Complication complication) { + mExecutor.execute(() -> { + if (mComplications.remove(complication)) { + mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged()); + } }); } - private void notifyCallbacks() { - for (Callback callback : mCallbacks) { - callback.onComplicationsChanged(); - } + /** + * Returns collection of present {@link Complication}. + */ + public Collection getComplications() { + return Collections.unmodifiableCollection(mComplications); } @Override @@ -161,12 +117,4 @@ public class DreamOverlayStateController implements mCallbacks.remove(callback); }); } - - /** - * Returns all registered {@link ComplicationProvider} instances. - * @return A collection of {@link ComplicationProvider}. - */ - public Collection 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); + } +}