Merge "Complication Entity & Presentation Refactor."

This commit is contained in:
Bryce Lee
2022-01-25 15:02:58 +00:00
committed by Android (Google) Code Review
34 changed files with 2442 additions and 486 deletions

View File

@@ -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">
<TextClock

View File

@@ -28,8 +28,6 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<include layout="@layout/dream_overlay_complications_layer" />
<com.android.systemui.dreams.DreamOverlayStatusBarView
android:id="@+id/dream_overlay_status_bar"
android:layout_width="match_parent"

View File

@@ -1,48 +0,0 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.dreams;
import android.view.View;
/**
* A collection of interfaces related to hosting a complication.
*/
public abstract class ComplicationHost {
/**
* An interface for the callback from the complication provider to indicate when the
* complication is ready.
*/
public interface CreationCallback {
/**
* Called to inform the complication view is ready to be placed within the visual space.
* @param view The view representing the complication.
* @param layoutParams The parameters to create the view with.
*/
void onCreated(View view, ComplicationHostView.LayoutParams layoutParams);
}
/**
* An interface for the callback from the complication provider to signal interactions in the
* complication.
*/
public interface InteractionCallback {
/**
* Called to signal the calling complication would like to exit the dream.
*/
void onExit();
}
}

View File

@@ -1,44 +0,0 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.dreams;
import android.content.Context;
import android.util.AttributeSet;
import androidx.constraintlayout.widget.ConstraintLayout;
/**
* {@link ComplicationHostView} is the container view for housing complications above of a dream.
*/
public class ComplicationHostView extends ConstraintLayout {
public ComplicationHostView(Context context) {
super(context, null);
}
public ComplicationHostView(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public ComplicationHostView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr, 0);
}
public ComplicationHostView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.dreams;
import android.annotation.IntDef;
import android.content.Context;
import com.android.settingslib.dream.DreamBackend;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* {@link ComplicationProvider} is an interface for defining entities that can supply complications
* to show over a dream. Presentation components such as the {@link DreamOverlayService} supply
* implementations with the necessary context for constructing such overlays.
*/
public interface ComplicationProvider {
/**
* The type of dream complications which can be provided by a {@link ComplicationProvider}.
*/
@IntDef(prefix = {"COMPLICATION_TYPE_"}, flag = true, value = {
COMPLICATION_TYPE_NONE,
COMPLICATION_TYPE_TIME,
COMPLICATION_TYPE_DATE,
COMPLICATION_TYPE_WEATHER,
COMPLICATION_TYPE_AIR_QUALITY,
COMPLICATION_TYPE_CAST_INFO
})
@Retention(RetentionPolicy.SOURCE)
@interface ComplicationType {}
int COMPLICATION_TYPE_NONE = 0;
int COMPLICATION_TYPE_TIME = 1;
int COMPLICATION_TYPE_DATE = 1 << 1;
int COMPLICATION_TYPE_WEATHER = 1 << 2;
int COMPLICATION_TYPE_AIR_QUALITY = 1 << 3;
int COMPLICATION_TYPE_CAST_INFO = 1 << 4;
/**
* Called when the {@link ComplicationHost} requests the associated complication be produced.
*
* @param context The {@link Context} used to construct the view.
* @param creationCallback The callback to inform the complication has been created.
* @param interactionCallback The callback to inform the complication has been interacted with.
*/
void onCreateComplication(Context context, ComplicationHost.CreationCallback creationCallback,
ComplicationHost.InteractionCallback interactionCallback);
/**
* Converts a {@link com.android.settingslib.dream.DreamBackend.ComplicationType} to
* {@link ComplicationType}.
*/
@ComplicationType
default int convertComplicationType(@DreamBackend.ComplicationType int type) {
switch (type) {
case DreamBackend.COMPLICATION_TYPE_TIME:
return COMPLICATION_TYPE_TIME;
case DreamBackend.COMPLICATION_TYPE_DATE:
return COMPLICATION_TYPE_DATE;
case DreamBackend.COMPLICATION_TYPE_WEATHER:
return COMPLICATION_TYPE_WEATHER;
case DreamBackend.COMPLICATION_TYPE_AIR_QUALITY:
return COMPLICATION_TYPE_AIR_QUALITY;
case DreamBackend.COMPLICATION_TYPE_CAST_INFO:
return COMPLICATION_TYPE_CAST_INFO;
default:
return COMPLICATION_TYPE_NONE;
}
}
}

View File

@@ -25,11 +25,11 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.complication.ComplicationHostViewController;
import com.android.systemui.dreams.complication.dagger.ComplicationHostViewComponent;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.dreams.dagger.DreamOverlayModule;
import com.android.systemui.util.ViewController;
@@ -47,6 +47,8 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
private final int mDreamOverlayNotificationsDragAreaHeight;
private final DreamOverlayStatusBarViewController mStatusBarViewController;
private final ComplicationHostViewController mComplicationHostViewController;
// The dream overlay's content view, which is located below the status bar (in z-order) and is
// the space into which widgets are placed.
private final ViewGroup mDreamOverlayContentView;
@@ -74,6 +76,13 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
final int childCount = mDreamOverlayContentView.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = mDreamOverlayContentView.getChildAt(i);
if (mComplicationHostViewController.getView() == child) {
region.op(mComplicationHostViewController.getTouchRegions(),
Region.Op.UNION);
continue;
}
if (child.getGlobalVisibleRect(rect)) {
region.op(rect, Region.Op.UNION);
}
@@ -93,6 +102,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
@Inject
public DreamOverlayContainerViewController(
DreamOverlayContainerView containerView,
ComplicationHostViewComponent.Factory complicationHostViewFactory,
@Named(DreamOverlayModule.DREAM_OVERLAY_CONTENT_VIEW) ViewGroup contentView,
DreamOverlayStatusBarViewController statusBarViewController,
@Main Handler handler,
@@ -106,6 +116,13 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
mView.getResources().getDimensionPixelSize(
R.dimen.dream_overlay_notifications_drag_area_height);
mComplicationHostViewController = complicationHostViewFactory.create().getController();
final View view = mComplicationHostViewController.getView();
mDreamOverlayContentView.addView(view,
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
mHandler = handler;
mMaxBurnInOffset = maxBurnInOffset;
mBurnInProtectionUpdateInterval = burnInProtectionUpdateInterval;
@@ -114,6 +131,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
@Override
protected void onInit() {
mStatusBarViewController.init();
mComplicationHostViewController.init();
}
@Override
@@ -130,18 +148,10 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
.removeOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
}
void addOverlay(View overlayView, ConstraintLayout.LayoutParams layoutParams) {
mDreamOverlayContentView.addView(overlayView, layoutParams);
}
View getContainerView() {
return mView;
}
void removeAllOverlays() {
mDreamOverlayContentView.removeAllViews();
}
@VisibleForTesting
int getDreamOverlayNotificationsDragAreaHeight() {
return mDreamOverlayNotificationsDragAreaHeight;

View File

@@ -24,10 +24,13 @@ import android.view.WindowInsets;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleRegistry;
import androidx.lifecycle.ViewModelStore;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.PhoneWindow;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import java.util.concurrent.Executor;
@@ -47,8 +50,6 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
private final Context mContext;
// The Executor ensures actions and ui updates happen on the same thread.
private final Executor mExecutor;
// The state controller informs the service of updates to the complications present.
private final DreamOverlayStateController mStateController;
// A controller for the dream overlay container view (which contains both the status bar and the
// content area).
private final DreamOverlayContainerViewController mDreamOverlayContainerViewController;
@@ -56,47 +57,51 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
// A reference to the {@link Window} used to hold the dream overlay.
private Window mWindow;
private final DreamOverlayStateController.Callback mOverlayStateCallback =
new DreamOverlayStateController.Callback() {
@Override
public void onComplicationsChanged() {
mExecutor.execute(() -> 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);
});
}
}

View File

@@ -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<DreamOverlayStateController.Callback> {
// 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<Callback> mCallbacks = new ArrayList<>();
private final HashMap<ComplicationToken, ComplicationProvider> mComplications = new HashMap<>();
private final Collection<Complication> 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<ComplicationToken> 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<ComplicationProvider> 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<Complication> 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<ComplicationProvider> getComplications() {
return mComplications.values();
}
}

View File

@@ -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}.
*
* <pre>{@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<FrameLayout> {
* 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);
* }
* }
* }
* </pre>
*/
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);
}

View File

@@ -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<Collection<Complication>> {
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();
}
}

View File

@@ -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<Collection<ComplicationViewModel>> 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<ComplicationViewModel> convert(Collection<Complication> 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<Collection<ComplicationViewModel>> getComplications() {
return mComplications;
}
}

View File

@@ -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<ConstraintLayout> {
private final ComplicationLayoutEngine mLayoutEngine;
private final LifecycleOwner mLifecycleOwner;
private final ComplicationCollectionViewModel mComplicationCollectionViewModel;
private final HashMap<ComplicationId, Complication.ViewHolder> 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<ComplicationViewModel> complications) {
final Collection<ComplicationId> ids = complications.stream()
.map(complicationViewModel -> complicationViewModel.getId())
.collect(Collectors.toSet());
final Collection<ComplicationId> 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<ComplicationViewModel> 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;
}
}

View File

@@ -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 + "}";
}
}

View File

@@ -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<ViewEntry> {
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<Integer, DirectionGroup> 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<ViewEntry> 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<ViewEntry> it = mViews.iterator();
while (it.hasNext()) {
final ViewEntry viewEntry = it.next();
viewEntry.applyLayoutParams(groupHead);
groupHead = viewEntry.getView();
}
}
}
private final ConstraintLayout mLayout;
private final HashMap<ComplicationId, ViewEntry> mEntries = new HashMap<>();
private final HashMap<Integer, PositionGroup> 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();
}
}

View File

@@ -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<Integer, Integer> 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<Integer> 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;
}
}

View File

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

View File

@@ -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));
}
}

View File

@@ -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<Complication, ComplicationId> 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);
}
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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 extends ViewModel> T create(@NonNull Class<T> aClass) {
return (T) mCreator.create();
}
}

View File

@@ -22,6 +22,7 @@ import dagger.Module;
* Dagger Module providing Communal-related functionality.
*/
@Module(subcomponents = {
DreamOverlayComponent.class})
DreamOverlayComponent.class,
})
public interface DreamModule {
}

View File

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

View File

@@ -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<LifecycleRegistry> lifecycleRegistryLazy) {
return () -> lifecycleRegistryLazy.get();
}
@Provides
@DreamOverlayComponent.DreamOverlayScope
static LifecycleRegistry providesLifecycleRegistry(LifecycleOwner lifecycleOwner) {
return new LifecycleRegistry(lifecycleOwner);
}
}

View File

@@ -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) {
}
}
}

View File

@@ -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();

View File

@@ -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<ComplicationHost.CreationCallback> creationCallbackCapture =
ArgumentCaptor.forClass(ComplicationHost.CreationCallback.class);
final ArgumentCaptor<ComplicationHost.InteractionCallback> 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<ComplicationHost.InteractionCallback> 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<DreamOverlayStateController.Callback> 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<DreamOverlayStateController.Callback> 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();

View File

@@ -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<DreamOverlayStateController.ComplicationToken> tokenFuture =
stateController.addComplication(mProvider);
stateController.addComplication(mComplication);
mExecutor.runAllReady();
verify(mCallback, times(1)).onComplicationsChanged();
final Collection<ComplicationProvider> providers = stateController.getComplications();
assertEquals(providers.size(), 1);
assertTrue(providers.contains(mProvider));
final Collection<Complication> 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.

View File

@@ -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<Complication> complications = new HashSet<>();
final Observer<Collection<Complication>> observer = Mockito.mock(Observer.class);
complications.add(Mockito.mock(Complication.class));
when(stateController.getComplications()).thenReturn(complications);
liveData.observeForever(observer);
ArgumentCaptor<DreamOverlayStateController.Callback> 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<Collection<Complication>> observer,
Collection<Complication> targetCollection) {
ArgumentCaptor<Collection<Complication>> collectionCaptor =
ArgumentCaptor.forClass(Collection.class);
verify(observer).onChanged(collectionCaptor.capture());
assertThat(collectionCaptor.getValue().equals(targetCollection)).isTrue();
Mockito.clearInvocations(observer);
}
}

View File

@@ -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<Collection<ComplicationViewModel>> 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<Observer<Collection<ComplicationViewModel>>> 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<Collection<ComplicationViewModel>> observer =
observerArgumentCaptor.getValue();
// Add complication and ensure it is added to the view.
final HashSet<ComplicationViewModel> 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));
}
}

View File

@@ -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<ConstraintLayout.LayoutParams> paramConsumer) {
ArgumentCaptor<ConstraintLayout.LayoutParams> 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();
});
}
}

View File

@@ -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<Integer> 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<Integer> allDirections = new HashSet(Arrays.asList(
ComplicationLayoutParams.DIRECTION_DOWN,
ComplicationLayoutParams.DIRECTION_UP,
ComplicationLayoutParams.DIRECTION_START,
ComplicationLayoutParams.DIRECTION_END
));
final HashMap<Integer, Integer> 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();
}
}

View File

@@ -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<ComplicationId> 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<ComplicationId> 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);
}
}