1  /*
2   * Copyright (C) 2022 The Android Open Source Project
3   *
4   * Licensed under the Apache License, Version 2.0 (the "License");
5   * you may not use this file except in compliance with the License.
6   * You may obtain a copy of the License at
7   *
8   *      http://www.apache.org/licenses/LICENSE-2.0
9   *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.android.systemui.complication;
18  
19  import static com.android.systemui.complication.ComplicationLayoutParams.DIRECTION_DOWN;
20  import static com.android.systemui.complication.ComplicationLayoutParams.DIRECTION_END;
21  import static com.android.systemui.complication.ComplicationLayoutParams.DIRECTION_START;
22  import static com.android.systemui.complication.ComplicationLayoutParams.DIRECTION_UP;
23  import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTOM;
24  import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_END;
25  import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_START;
26  import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP;
27  import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_IN_DURATION;
28  import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_OUT_DURATION;
29  import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATION_DIRECTIONAL_SPACING_DEFAULT;
30  import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN_POSITION_BOTTOM;
31  import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN_POSITION_END;
32  import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN_POSITION_START;
33  import static com.android.systemui.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN_POSITION_TOP;
34  import static com.android.systemui.complication.dagger.ComplicationHostViewModule.SCOPED_COMPLICATIONS_LAYOUT;
35  
36  import android.util.Log;
37  import android.view.View;
38  import android.view.ViewGroup;
39  
40  import androidx.constraintlayout.widget.ConstraintLayout;
41  import androidx.constraintlayout.widget.Constraints;
42  
43  import com.android.systemui.R;
44  import com.android.systemui.complication.ComplicationLayoutParams.Direction;
45  import com.android.systemui.complication.ComplicationLayoutParams.Position;
46  import com.android.systemui.complication.dagger.ComplicationModule;
47  import com.android.systemui.statusbar.CrossFadeHelper;
48  import com.android.systemui.touch.TouchInsetManager;
49  
50  import java.util.ArrayList;
51  import java.util.Collections;
52  import java.util.HashMap;
53  import java.util.Iterator;
54  import java.util.List;
55  import java.util.function.Consumer;
56  import java.util.stream.Collectors;
57  
58  import javax.inject.Inject;
59  import javax.inject.Named;
60  
61  /**
62   * {@link ComplicationLayoutEngine} arranges a collection of {@link ComplicationViewModel} based on
63   * their layout parameters and attributes. The management of this set is done by
64   * {@link ComplicationHostViewController}.
65   */
66  @ComplicationModule.ComplicationScope
67  public class ComplicationLayoutEngine implements Complication.VisibilityController {
68      public static final String TAG = "ComplicationLayoutEng";
69  
70      /**
71       * Container for storing and operating on a tuple of margin values.
72       */
73      public static class Margins {
74          public final int start;
75          public final int top;
76          public final int end;
77          public final int bottom;
78  
79          /**
80           * Default constructor with all margins set to 0.
81           */
Margins()82          public Margins() {
83              this(0, 0, 0, 0);
84          }
85  
86          /**
87           * Cosntructor to specify margin in each direction.
88           * @param start start margin
89           * @param top top margin
90           * @param end end margin
91           * @param bottom bottom margin
92           */
Margins(int start, int top, int end, int bottom)93          public Margins(int start, int top, int end, int bottom) {
94              this.start = start;
95              this.top = top;
96              this.end = end;
97              this.bottom = bottom;
98          }
99  
100          /**
101           * Creates a new {@link Margins} by adding the corresponding dimensions together.
102           */
combine(Margins margins1, Margins margins2)103          public static Margins combine(Margins margins1, Margins margins2) {
104              return new Margins(margins1.start + margins2.start,
105                      margins1.top + margins2.top,
106                      margins1.end + margins2.end,
107                      margins1.bottom + margins2.bottom);
108          }
109      }
110  
111      /**
112       * {@link ViewEntry} is an internal container, capturing information necessary for working with
113       * a particular {@link Complication} view.
114       */
115      private static class ViewEntry implements Comparable<ViewEntry> {
116          private final View mView;
117          private final ComplicationLayoutParams mLayoutParams;
118          private final TouchInsetManager.TouchInsetSession mTouchInsetSession;
119          private final Parent mParent;
120          @Complication.Category
121          private final int mCategory;
122  
123          /**
124           * Default constructor. {@link Parent} allows for the {@link ViewEntry}'s surrounding
125           * view hierarchy to be accessed without traversing the entire view tree.
126           */
ViewEntry(View view, ComplicationLayoutParams layoutParams, TouchInsetManager.TouchInsetSession touchSession, int category, Parent parent)127          ViewEntry(View view, ComplicationLayoutParams layoutParams,
128                  TouchInsetManager.TouchInsetSession touchSession, int category, Parent parent) {
129              mView = view;
130              // Views that are generated programmatically do not have a unique id assigned to them
131              // at construction. A new id is assigned here to enable ConstraintLayout relative
132              // specifications. Existing ids for inflated views are not preserved.
133              // {@link Complication.ViewHolder} should not reference the root container by id.
134              mView.setId(View.generateViewId());
135              mLayoutParams = layoutParams;
136              mTouchInsetSession = touchSession;
137              mCategory = category;
138              mParent = parent;
139  
140              touchSession.addViewToTracking(mView);
141          }
142  
143          /**
144           * Returns the {@link View} associated with the {@link Complication}. This is the instance
145           * passed in at construction. The reference to this {@link View} is captured when the
146           * {@link Complication} is added to the {@link ComplicationLayoutEngine}. The
147           * {@link Complication} cannot modify the {@link View} reference beyond this point.
148           */
getView()149          private View getView() {
150              return mView;
151          }
152  
153          /**
154           * Returns The {@link ComplicationLayoutParams} associated with the view.
155           */
getLayoutParams()156          public ComplicationLayoutParams getLayoutParams() {
157              return mLayoutParams;
158          }
159  
160          /**
161           * Interprets the {@link #getLayoutParams()} into {@link ConstraintLayout.LayoutParams} and
162           * applies them to the view. The method accounts for the relationship of the {@link View} to
163           * the other {@link Complication} views around it. The organization of the {@link View}
164           * instances in {@link ComplicationLayoutEngine} can be seen as lists. A {@link View} is
165           * either the head of its list or a following node. This head is passed into this method,
166           * which can be a reference to the {@link View} to indicate it is the head.
167           */
applyLayoutParams(View head)168          public void applyLayoutParams(View head) {
169              // Only the basic dimension parameters from the base ViewGroup.LayoutParams are carried
170              // over verbatim from the complication specified LayoutParam. Other fields are
171              // interpreted.
172              final ConstraintLayout.LayoutParams params =
173                      new Constraints.LayoutParams(mLayoutParams.width, mLayoutParams.height);
174  
175              final int direction = getLayoutParams().getDirection();
176  
177              final boolean snapsToGuide = getLayoutParams().snapsToGuide();
178  
179              // If no parent, view is the anchor. In this case, it is given the highest priority for
180              // alignment. All alignment preferences are done in relation to the parent container.
181              final boolean isRoot = head == mView;
182  
183              // Each view can be seen as a vector, having a point (described here as position) and
184              // direction. When a view is the head of a position, then it is the first in a sequence
185              // of complications to appear from that position. For example, being the head for
186              // position POSITION_TOP | POSITION_END will cause the view to be shown as the first
187              // view in that corner. In this case, the positions specify which sides to align with
188              // the parent. If the view is not the head, the positions perpendicular to the direction
189              // of the view specify which side to align with the opposing side of the head view.
190              // Otherwise, the position aligns with the containing view. This means a
191              // POSITION_BOTTOM | POSITION_START with DIRECTION_UP non-head view's bottom to be
192              // aligned with the preceding view node's top and start to be aligned with the
193              // parent's start.
194              mLayoutParams.iteratePositions(position -> {
195                  switch(position) {
196                      case ComplicationLayoutParams.POSITION_START:
197                          if (isRoot || direction != ComplicationLayoutParams.DIRECTION_END) {
198                              params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
199                          } else {
200                              params.startToEnd = head.getId();
201                          }
202                          if (snapsToGuide
203                                  && (direction == ComplicationLayoutParams.DIRECTION_DOWN
204                                  || direction == ComplicationLayoutParams.DIRECTION_UP)) {
205                              params.endToStart = R.id.complication_start_guide;
206                          }
207                          break;
208                      case ComplicationLayoutParams.POSITION_TOP:
209                          if (isRoot || direction != ComplicationLayoutParams.DIRECTION_DOWN) {
210                              params.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
211                          } else {
212                              params.topToBottom = head.getId();
213                          }
214                          if (snapsToGuide
215                                  && (direction == ComplicationLayoutParams.DIRECTION_END
216                                  || direction == ComplicationLayoutParams.DIRECTION_START)) {
217                              params.endToStart = R.id.complication_top_guide;
218                          }
219                          break;
220                      case ComplicationLayoutParams.POSITION_BOTTOM:
221                          if (isRoot || direction != ComplicationLayoutParams.DIRECTION_UP) {
222                              params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
223                          } else {
224                              params.bottomToTop = head.getId();
225                          }
226                          if (snapsToGuide
227                                  && (direction == ComplicationLayoutParams.DIRECTION_END
228                                  || direction == ComplicationLayoutParams.DIRECTION_START)) {
229                              params.topToBottom = R.id.complication_bottom_guide;
230                          }
231                          break;
232                      case ComplicationLayoutParams.POSITION_END:
233                          if (isRoot || direction != ComplicationLayoutParams.DIRECTION_START) {
234                              params.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
235                          } else {
236                              params.endToStart = head.getId();
237                          }
238                          if (snapsToGuide
239                                  && (direction == ComplicationLayoutParams.DIRECTION_UP
240                                  || direction == ComplicationLayoutParams.DIRECTION_DOWN)) {
241                              params.startToEnd = R.id.complication_end_guide;
242                          }
243                          break;
244                  }
245  
246                  final Margins margins = mParent.getMargins(this, isRoot);
247                  params.setMarginsRelative(margins.start, margins.top, margins.end, margins.bottom);
248              });
249  
250              if (mLayoutParams.constraintSpecified()) {
251                  switch (direction) {
252                      case ComplicationLayoutParams.DIRECTION_START:
253                      case ComplicationLayoutParams.DIRECTION_END:
254                          params.matchConstraintMaxWidth = mLayoutParams.getConstraint();
255                          break;
256                      case ComplicationLayoutParams.DIRECTION_UP:
257                      case ComplicationLayoutParams.DIRECTION_DOWN:
258                          params.matchConstraintMaxHeight = mLayoutParams.getConstraint();
259                          break;
260                  }
261              }
262  
263              mView.setLayoutParams(params);
264          }
265  
setGuide(ConstraintLayout.LayoutParams lp, int validDirections, Consumer<ConstraintLayout.LayoutParams> consumer)266          private void setGuide(ConstraintLayout.LayoutParams lp, int validDirections,
267                  Consumer<ConstraintLayout.LayoutParams> consumer) {
268              final ComplicationLayoutParams layoutParams = getLayoutParams();
269              if (!layoutParams.snapsToGuide()) {
270                  return;
271              }
272  
273              consumer.accept(lp);
274          }
275  
276          /**
277           * Informs the {@link ViewEntry}'s parent entity to remove the {@link ViewEntry} from
278           * being shown further.
279           */
remove()280          public void remove() {
281              mParent.removeEntry(this);
282  
283              ((ViewGroup) mView.getParent()).removeView(mView);
284              mTouchInsetSession.removeViewFromTracking(mView);
285          }
286  
287          @Override
compareTo(ViewEntry viewEntry)288          public int compareTo(ViewEntry viewEntry) {
289              // If the two entries have different categories, system complications take precedence.
290              if (viewEntry.mCategory != mCategory) {
291                  // Note that this logic will need to be adjusted if more categories are introduced.
292                  return mCategory == Complication.CATEGORY_SYSTEM ? 1 : -1;
293              }
294  
295              // A higher weight indicates greater precedence if all else being equal.
296              if (viewEntry.mLayoutParams.getWeight() != mLayoutParams.getWeight()) {
297                  return mLayoutParams.getWeight() > viewEntry.mLayoutParams.getWeight() ? 1 : -1;
298              }
299  
300              return 0;
301          }
302  
303          /**
304           * {@link Builder} allows for a multiple entities to contribute to the {@link ViewEntry}
305           * construction. This is necessary for setting an immutable parent, which might not be
306           * known until the view hierarchy is traversed.
307           */
308          private static class Builder {
309              private final View mView;
310              private final TouchInsetManager.TouchInsetSession mTouchSession;
311              private final ComplicationLayoutParams mLayoutParams;
312              private final int mCategory;
313              private Parent mParent;
314  
Builder(View view, TouchInsetManager.TouchInsetSession touchSession, ComplicationLayoutParams lp, @Complication.Category int category)315              Builder(View view, TouchInsetManager.TouchInsetSession touchSession,
316                      ComplicationLayoutParams lp, @Complication.Category int category) {
317                  mView = view;
318                  mLayoutParams = lp;
319                  mCategory = category;
320                  mTouchSession = touchSession;
321              }
322  
323              /**
324               * Returns the set {@link ComplicationLayoutParams}
325               */
getLayoutParams()326              public ComplicationLayoutParams getLayoutParams() {
327                  return mLayoutParams;
328              }
329  
330              /**
331               * Returns the set {@link Complication.Category}.
332               */
333              @Complication.Category
getCategory()334              public int getCategory() {
335                  return mCategory;
336              }
337  
338              /**
339               * Sets the parent. Note that this references to the entity for handling events, such as
340               * requesting the removal of the {@link View}. It is not the
341               * {@link android.view.ViewGroup} which contains the {@link View}.
342               */
setParent(Parent parent)343              Builder setParent(Parent parent) {
344                  mParent = parent;
345                  return this;
346              }
347  
348              /**
349               * Builds and returns the resulting {@link ViewEntry}.
350               */
build()351              ViewEntry build() {
352                  return new ViewEntry(mView, mLayoutParams, mTouchSession, mCategory, mParent);
353              }
354          }
355  
356          /**
357           * An interface allowing an {@link ViewEntry} to signal events.
358           */
359          interface Parent {
360              /**
361               * Indicates the {@link ViewEntry} requests removal.
362               */
removeEntry(ViewEntry entry)363              void removeEntry(ViewEntry entry);
364  
365              /**
366               * Returns the margins to be applied to the entry
367               */
getMargins(ViewEntry entry, boolean isRoot)368              Margins getMargins(ViewEntry entry, boolean isRoot);
369          }
370      }
371  
372      /**
373       * {@link PositionGroup} represents a collection of {@link Complication} at a given location.
374       * It further organizes the {@link Complication} by the direction in which they emanate from
375       * this position.
376       */
377      private static class PositionGroup implements DirectionGroup.Parent {
378          private final HashMap<Integer, DirectionGroup> mDirectionGroups = new HashMap<>();
379  
380          private final HashMap<Integer, Margins> mDirectionalMargins;
381  
382          private final int mDefaultDirectionalSpacing;
383  
PositionGroup(int defaultDirectionalSpacing, HashMap<Integer, Margins> directionalMargins)384          PositionGroup(int defaultDirectionalSpacing, HashMap<Integer, Margins> directionalMargins) {
385              mDefaultDirectionalSpacing = defaultDirectionalSpacing;
386              mDirectionalMargins = directionalMargins;
387          }
388  
389          /**
390           * Invoked by the {@link PositionGroup} holder to introduce a {@link Complication} view to
391           * this group. It is assumed that the caller has correctly identified this
392           * {@link PositionGroup} as the proper home for the {@link Complication} based on its
393           * declared position.
394           */
add(ViewEntry.Builder entryBuilder)395          public ViewEntry add(ViewEntry.Builder entryBuilder) {
396              final int direction = entryBuilder.getLayoutParams().getDirection();
397              if (!mDirectionGroups.containsKey(direction)) {
398                  mDirectionGroups.put(direction, new DirectionGroup(this));
399              }
400  
401              return mDirectionGroups.get(direction).add(entryBuilder);
402          }
403  
404          @Override
onEntriesChanged()405          public void onEntriesChanged() {
406              // Whenever an entry is added/removed from a child {@link DirectionGroup}, it is vital
407              // that all {@link DirectionGroup} children are visited. It is possible the overall
408              // head has changed, requiring constraints to be adjusted.
409              updateViews();
410          }
411  
412          @Override
getDefaultDirectionalSpacing()413          public int getDefaultDirectionalSpacing() {
414              return mDefaultDirectionalSpacing;
415          }
416  
417          @Override
getMargins(ViewEntry entry, boolean isRoot)418          public Margins getMargins(ViewEntry entry, boolean isRoot) {
419              if (isRoot) {
420                  Margins cumulativeMargins = new Margins();
421  
422                  for (Margins margins : mDirectionalMargins.values()) {
423                      cumulativeMargins = Margins.combine(margins, cumulativeMargins);
424                  }
425  
426                  return cumulativeMargins;
427              }
428  
429              return mDirectionalMargins.get(entry.getLayoutParams().getDirection());
430          }
431  
updateViews()432          private void updateViews() {
433              ViewEntry head = null;
434  
435              // Identify which {@link Complication} head from the set of {@link DirectionGroup}
436              // should be treated as the {@link PositionGroup} head.
437              for (DirectionGroup directionGroup : mDirectionGroups.values()) {
438                  final ViewEntry groupHead = directionGroup.getHead();
439                  if (head == null || (groupHead != null && groupHead.compareTo(head) > 0)) {
440                      head = groupHead;
441                  }
442              }
443  
444              // A headless position group indicates no complications.
445              if (head == null) {
446                  return;
447              }
448  
449              for (DirectionGroup directionGroup : mDirectionGroups.values()) {
450                  // Tell each {@link DirectionGroup} to update its containing {@link ViewEntry} based
451                  // on the identified head. This iteration will also capture any newly added views.
452                  directionGroup.updateViews(head.getView());
453              }
454          }
455  
getViews()456          private ArrayList<ViewEntry> getViews() {
457              final ArrayList<ViewEntry> views = new ArrayList<>();
458              for (DirectionGroup directionGroup : mDirectionGroups.values()) {
459                  views.addAll(directionGroup.getViews());
460              }
461              return views;
462          }
463      }
464  
465      /**
466       * A {@link DirectionGroup} organizes the {@link ViewEntry} of a parent group that point are
467       * laid out in the same direction.
468       */
469      private static class DirectionGroup implements ViewEntry.Parent {
470          /**
471           * An interface implemented by the {@link DirectionGroup} parent to receive updates.
472           */
473          interface Parent {
474              /**
475               * Invoked to indicate a change to the {@link ViewEntry} composition for this
476               * {@link DirectionGroup}.
477               */
onEntriesChanged()478              void onEntriesChanged();
479  
480              /**
481               * Returns the default spacing between elements.
482               */
getDefaultDirectionalSpacing()483              int getDefaultDirectionalSpacing();
484  
485              /**
486               * Returns the margins for the view entry.
487               */
getMargins(ViewEntry entry, boolean isRoot)488              Margins getMargins(ViewEntry entry, boolean isRoot);
489          }
490          private final ArrayList<ViewEntry> mViews = new ArrayList<>();
491          private final Parent mParent;
492  
493          /**
494           * Creates a new {@link DirectionGroup} with the specified parent.
495           */
DirectionGroup(Parent parent)496          DirectionGroup(Parent parent) {
497              mParent = parent;
498          }
499  
500          /**
501           * Returns the head of the group. It is assumed that the order of the {@link ViewEntry} is
502           * proactively maintained.
503           */
getHead()504          public ViewEntry getHead() {
505              return mViews.isEmpty() ? null : mViews.get(0);
506          }
507  
508          /**
509           * Adds a {@link ViewEntry} via {@link ViewEntry.Builder} to this group.
510           */
add(ViewEntry.Builder entryBuilder)511          public ViewEntry add(ViewEntry.Builder entryBuilder) {
512              final ViewEntry entry = entryBuilder.setParent(this).build();
513              mViews.add(entry);
514  
515              // After adding view, reverse sort collection.
516              Collections.sort(mViews);
517              Collections.reverse(mViews);
518  
519              mParent.onEntriesChanged();
520  
521              return entry;
522          }
523  
524          @Override
removeEntry(ViewEntry entry)525          public void removeEntry(ViewEntry entry) {
526              // Sort is handled when the view is added, so should still be correct after removal.
527              // However, the head may have been removed, which may affect the layout of views in
528              // other DirectionGroups of the same PositionGroup.
529              mViews.remove(entry);
530              mParent.onEntriesChanged();
531          }
532  
533          @Override
getMargins(ViewEntry entry, boolean isRoot)534          public Margins getMargins(ViewEntry entry, boolean isRoot) {
535              int directionalSpacing = entry.getLayoutParams().getDirectionalSpacing(
536                      mParent.getDefaultDirectionalSpacing());
537  
538              Margins margins = new Margins();
539  
540              if (!isRoot) {
541                  switch (entry.getLayoutParams().getDirection()) {
542                      case ComplicationLayoutParams.DIRECTION_START:
543                          margins = new Margins(0, 0, directionalSpacing, 0);
544                          break;
545                      case ComplicationLayoutParams.DIRECTION_UP:
546                          margins = new Margins(0, 0, 0, directionalSpacing);
547                          break;
548                      case ComplicationLayoutParams.DIRECTION_END:
549                          margins = new Margins(directionalSpacing, 0, 0, 0);
550                          break;
551                      case ComplicationLayoutParams.DIRECTION_DOWN:
552                          margins = new Margins(0, directionalSpacing, 0, 0);
553                          break;
554                  }
555              }
556  
557              return Margins.combine(mParent.getMargins(entry, isRoot), margins);
558          }
559  
560          /**
561           * Invoked by {@link Parent} to update the layout of all children {@link ViewEntry} with
562           * the specified head. Note that the head might not be in this group and instead part of a
563           * neighboring group.
564           */
updateViews(View groupHead)565          public void updateViews(View groupHead) {
566              Iterator<ViewEntry> it = mViews.iterator();
567  
568              while (it.hasNext()) {
569                  final ViewEntry viewEntry = it.next();
570                  viewEntry.applyLayoutParams(groupHead);
571                  groupHead = viewEntry.getView();
572              }
573          }
574  
getViews()575          private List<ViewEntry> getViews() {
576              return mViews;
577          }
578      }
579  
580      private final ConstraintLayout mLayout;
581      private final int mDefaultDirectionalSpacing;
582      private final HashMap<ComplicationId, ViewEntry> mEntries = new HashMap<>();
583      private final HashMap<Integer, PositionGroup> mPositions = new HashMap<>();
584      private final TouchInsetManager.TouchInsetSession mSession;
585      private final int mFadeInDuration;
586      private final int mFadeOutDuration;
587      private final HashMap<Integer, HashMap<Integer, Margins>> mPositionDirectionMarginMapping;
588  
589      /** */
590      @Inject
ComplicationLayoutEngine(@amedSCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout, @Named(COMPLICATION_DIRECTIONAL_SPACING_DEFAULT) int defaultDirectionalSpacing, @Named(COMPLICATION_MARGIN_POSITION_START) int complicationMarginPositionStart, @Named(COMPLICATION_MARGIN_POSITION_TOP) int complicationMarginPositionTop, @Named(COMPLICATION_MARGIN_POSITION_END) int complicationMarginPositionEnd, @Named(COMPLICATION_MARGIN_POSITION_BOTTOM) int complicationMarginPositionBottom, TouchInsetManager.TouchInsetSession session, @Named(COMPLICATIONS_FADE_IN_DURATION) int fadeInDuration, @Named(COMPLICATIONS_FADE_OUT_DURATION) int fadeOutDuration)591      public ComplicationLayoutEngine(@Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout,
592              @Named(COMPLICATION_DIRECTIONAL_SPACING_DEFAULT) int defaultDirectionalSpacing,
593              @Named(COMPLICATION_MARGIN_POSITION_START) int complicationMarginPositionStart,
594              @Named(COMPLICATION_MARGIN_POSITION_TOP) int complicationMarginPositionTop,
595              @Named(COMPLICATION_MARGIN_POSITION_END) int complicationMarginPositionEnd,
596              @Named(COMPLICATION_MARGIN_POSITION_BOTTOM) int complicationMarginPositionBottom,
597              TouchInsetManager.TouchInsetSession session,
598              @Named(COMPLICATIONS_FADE_IN_DURATION) int fadeInDuration,
599              @Named(COMPLICATIONS_FADE_OUT_DURATION) int fadeOutDuration) {
600          mLayout = layout;
601          mDefaultDirectionalSpacing = defaultDirectionalSpacing;
602          mSession = session;
603          mFadeInDuration = fadeInDuration;
604          mFadeOutDuration = fadeOutDuration;
605          mPositionDirectionMarginMapping = generatePositionDirectionalMarginsMapping(
606                  complicationMarginPositionStart,
607                  complicationMarginPositionTop,
608                  complicationMarginPositionEnd,
609                  complicationMarginPositionBottom);
610      }
611  
612      private static HashMap<Integer, HashMap<Integer, Margins>>
generatePositionDirectionalMarginsMapping(int complicationMarginPositionStart, int complicationMarginPositionTop, int complicationMarginPositionEnd, int complicationMarginPositionBottom)613              generatePositionDirectionalMarginsMapping(int complicationMarginPositionStart,
614              int complicationMarginPositionTop,
615              int complicationMarginPositionEnd,
616              int complicationMarginPositionBottom) {
617          HashMap<Integer, HashMap<Integer, Margins>> mapping = new HashMap<>();
618  
619          final Margins startMargins = new Margins(complicationMarginPositionStart, 0, 0, 0);
620          final Margins topMargins = new Margins(0, complicationMarginPositionTop, 0, 0);
621          final Margins endMargins = new Margins(0, 0, complicationMarginPositionEnd, 0);
622          final Margins bottomMargins = new Margins(0, 0, 0, complicationMarginPositionBottom);
623  
624          addToMapping(mapping, POSITION_START | POSITION_TOP, DIRECTION_END, topMargins);
625          addToMapping(mapping, POSITION_START | POSITION_TOP, DIRECTION_DOWN, startMargins);
626  
627          addToMapping(mapping, POSITION_START | POSITION_BOTTOM, DIRECTION_END, bottomMargins);
628          addToMapping(mapping, POSITION_START | POSITION_BOTTOM, DIRECTION_UP, startMargins);
629  
630          addToMapping(mapping, POSITION_END | POSITION_TOP, DIRECTION_START, topMargins);
631          addToMapping(mapping, POSITION_END | POSITION_TOP, DIRECTION_DOWN, endMargins);
632  
633          addToMapping(mapping, POSITION_END | POSITION_BOTTOM, DIRECTION_START, bottomMargins);
634          addToMapping(mapping, POSITION_END | POSITION_BOTTOM, DIRECTION_UP, endMargins);
635  
636          return mapping;
637      }
638  
addToMapping(HashMap<Integer, HashMap<Integer, Margins>> mapping, @Position int position, @Direction int direction, Margins margins)639      private static void addToMapping(HashMap<Integer, HashMap<Integer, Margins>> mapping,
640              @Position int position, @Direction int direction, Margins margins) {
641          if (!mapping.containsKey(position)) {
642              mapping.put(position, new HashMap<>());
643          }
644          mapping.get(position).put(direction, margins);
645      }
646  
647      @Override
setVisibility(int visibility)648      public void setVisibility(int visibility) {
649          if (visibility == View.VISIBLE) {
650              CrossFadeHelper.fadeIn(mLayout, mFadeInDuration, /* delay= */ 0);
651          } else {
652              CrossFadeHelper.fadeOut(
653                      mLayout,
654                      mFadeOutDuration,
655                      /* delay= */ 0,
656                      /* endRunnable= */ null);
657          }
658      }
659  
660      /**
661       * Adds a complication to this {@link ComplicationLayoutEngine}.
662       * @param id A {@link ComplicationId} unique to this complication. If this matches a
663       *           complication within this {@link ComplicationViewModel}, the existing complication
664       *           will be removed.
665       * @param view The {@link View} to be shown.
666       * @param lp The {@link ComplicationLayoutParams} as expressed by the {@link Complication}.
667       *           These will be interpreted into the final applied parameters.
668       * @param category The {@link Complication.Category} for the {@link Complication}.
669       */
addComplication(ComplicationId id, View view, ComplicationLayoutParams lp, @Complication.Category int category)670      public void addComplication(ComplicationId id, View view,
671              ComplicationLayoutParams lp, @Complication.Category int category) {
672          Log.d(TAG, "@" + Integer.toHexString(this.hashCode()) + " addComplication: " + id);
673  
674          // If the complication is present, remove.
675          if (mEntries.containsKey(id)) {
676              removeComplication(id);
677          }
678  
679          final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, mSession, lp, category);
680  
681          // Add position group if doesn't already exist
682          final int position = lp.getPosition();
683          if (!mPositions.containsKey(position)) {
684              mPositions.put(position, new PositionGroup(mDefaultDirectionalSpacing,
685                      mPositionDirectionMarginMapping.get(lp.getPosition())));
686          }
687  
688          // Insert entry into group
689          final ViewEntry entry = mPositions.get(position).add(entryBuilder);
690          mEntries.put(id, entry);
691  
692          mLayout.addView(entry.getView());
693      }
694  
695      /**
696       * Removes a complication by {@link ComplicationId}.
697       */
removeComplication(ComplicationId id)698      public boolean removeComplication(ComplicationId id) {
699          final ViewEntry entry = mEntries.remove(id);
700  
701          if (entry == null) {
702              Log.e(TAG, "could not find id:" + id);
703              return false;
704          }
705  
706          entry.remove();
707          return true;
708      }
709  
710      /**
711       * Gets an unordered list of all the views at a particular position.
712       */
getViewsAtPosition(@osition int position)713      public List<View> getViewsAtPosition(@Position int position) {
714          return mPositions.entrySet().stream()
715                  .filter(entry -> (entry.getKey() & position) == position)
716                  .flatMap(entry -> entry.getValue().getViews().stream())
717                  .map(ViewEntry::getView)
718                  .collect(Collectors.toList());
719      }
720  }
721