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 package com.android.systemui.complication;
17 
18 import static com.google.common.truth.Truth.assertThat;
19 
20 import static org.mockito.ArgumentMatchers.eq;
21 import static org.mockito.Mockito.never;
22 import static org.mockito.Mockito.verify;
23 import static org.mockito.Mockito.when;
24 
25 import android.testing.AndroidTestingRunner;
26 import android.view.View;
27 
28 import androidx.constraintlayout.widget.ConstraintLayout;
29 import androidx.test.filters.SmallTest;
30 
31 import com.android.systemui.R;
32 import com.android.systemui.SysuiTestCase;
33 import com.android.systemui.complication.ComplicationLayoutEngine.Margins;
34 import com.android.systemui.touch.TouchInsetManager;
35 
36 import org.junit.Before;
37 import org.junit.Test;
38 import org.junit.runner.RunWith;
39 import org.mockito.ArgumentCaptor;
40 import org.mockito.Mock;
41 import org.mockito.Mockito;
42 import org.mockito.MockitoAnnotations;
43 
44 import java.util.Arrays;
45 import java.util.List;
46 import java.util.Random;
47 import java.util.function.Consumer;
48 import java.util.stream.Collectors;
49 
50 @SmallTest
51 @RunWith(AndroidTestingRunner.class)
52 public class ComplicationLayoutEngineTest extends SysuiTestCase {
53     @Mock
54     ConstraintLayout mLayout;
55 
56     @Mock
57     TouchInsetManager.TouchInsetSession mTouchSession;
58 
createComplicationLayoutEngine()59     ComplicationLayoutEngine createComplicationLayoutEngine() {
60         return createComplicationLayoutEngine(0);
61     }
62 
createComplicationLayoutEngine(int spacing)63     ComplicationLayoutEngine createComplicationLayoutEngine(int spacing) {
64         return new ComplicationLayoutEngine(mLayout, spacing, 0, 0, 0, 0, mTouchSession, 0, 0);
65     }
66 
67     @Before
setup()68     public void setup() {
69         MockitoAnnotations.initMocks(this);
70     }
71 
72     private static class ViewInfo {
73         private static int sNextId = 1;
74         public final ComplicationId id;
75         public final View view;
76         public final ComplicationLayoutParams lp;
77 
78         @Complication.Category
79         public final int category;
80 
81         private static ComplicationId.Factory sFactory = new ComplicationId.Factory();
82 
ViewInfo(ComplicationLayoutParams params, @Complication.Category int category, ConstraintLayout layout)83         ViewInfo(ComplicationLayoutParams params, @Complication.Category int category,
84                 ConstraintLayout layout) {
85             this.lp = params;
86             this.category = category;
87             this.view = Mockito.mock(View.class);
88             this.id = sFactory.getNextId();
89             when(view.getId()).thenReturn(sNextId++);
90             when(view.getParent()).thenReturn(layout);
91         }
92 
clearInvocations()93         void clearInvocations() {
94             Mockito.clearInvocations(view);
95         }
96     }
97 
verifyChange(ViewInfo viewInfo, boolean verifyAdd, Consumer<ConstraintLayout.LayoutParams> paramConsumer)98     private void verifyChange(ViewInfo viewInfo,
99             boolean verifyAdd,
100             Consumer<ConstraintLayout.LayoutParams> paramConsumer) {
101         ArgumentCaptor<ConstraintLayout.LayoutParams> lpCaptor =
102                 ArgumentCaptor.forClass(ConstraintLayout.LayoutParams.class);
103         verify(viewInfo.view).setLayoutParams(lpCaptor.capture());
104 
105         if (verifyAdd) {
106             verify(mLayout).addView(eq(viewInfo.view));
107         }
108 
109         ConstraintLayout.LayoutParams capturedParams = lpCaptor.getValue();
110         paramConsumer.accept(capturedParams);
111     }
112 
addComplication(ComplicationLayoutEngine engine, ViewInfo info)113     private void addComplication(ComplicationLayoutEngine engine, ViewInfo info) {
114         engine.addComplication(info.id, info.view, info.lp, info.category);
115     }
116 
117     @Test
testCombineMargins()118     public void testCombineMargins() {
119         final Random rand = new Random();
120         final Margins margins1 = new Margins(rand.nextInt(), rand.nextInt(), rand.nextInt(),
121                 rand.nextInt());
122         final Margins margins2 = new Margins(rand.nextInt(), rand.nextInt(), rand.nextInt(),
123                 rand.nextInt());
124         final Margins combined = Margins.combine(margins1, margins2);
125         assertThat(margins1.start + margins2.start).isEqualTo(combined.start);
126         assertThat(margins1.top + margins2.top).isEqualTo(combined.top);
127         assertThat(margins1.end + margins2.end).isEqualTo(combined.end);
128         assertThat(margins1.bottom + margins2.bottom).isEqualTo(combined.bottom);
129     }
130 
131     @Test
testComplicationMarginPosition()132     public void testComplicationMarginPosition() {
133         final Random rand = new Random();
134         final int startMargin = rand.nextInt();
135         final int topMargin = rand.nextInt();
136         final int endMargin = rand.nextInt();
137         final int bottomMargin = rand.nextInt();
138         final int spacing = rand.nextInt();
139 
140         final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout, spacing,
141                 startMargin, topMargin, endMargin, bottomMargin, mTouchSession, 0, 0);
142 
143         final ViewInfo firstViewInfo = new ViewInfo(
144                 new ComplicationLayoutParams(
145                         100,
146                         100,
147                         ComplicationLayoutParams.POSITION_TOP
148                                 | ComplicationLayoutParams.POSITION_END,
149                         ComplicationLayoutParams.DIRECTION_DOWN,
150                         0),
151                 Complication.CATEGORY_SYSTEM,
152                 mLayout);
153 
154         addComplication(engine, firstViewInfo);
155         firstViewInfo.clearInvocations();
156 
157         final ViewInfo secondViewInfo = new ViewInfo(
158                 new ComplicationLayoutParams(
159                         100,
160                         100,
161                         ComplicationLayoutParams.POSITION_TOP
162                                 | ComplicationLayoutParams.POSITION_END,
163                         ComplicationLayoutParams.DIRECTION_DOWN,
164                         0),
165                 Complication.CATEGORY_STANDARD,
166                 mLayout);
167 
168         addComplication(engine, secondViewInfo);
169 
170 
171         // The first added view should have margins from both directions from the corner position.
172         verifyChange(firstViewInfo, false, lp -> {
173             assertThat(lp.topMargin).isEqualTo(topMargin);
174             assertThat(lp.getMarginEnd()).isEqualTo(endMargin);
175         });
176 
177         // The second view should be spaced below the first view and have the side end margin.
178         verifyChange(secondViewInfo, false, lp -> {
179             assertThat(lp.topMargin).isEqualTo(spacing);
180             assertThat(lp.getMarginEnd()).isEqualTo(endMargin);
181         });
182     }
183 
184     /**
185      * Makes sure the engine properly places a view within the {@link ConstraintLayout}.
186      */
187     @Test
testSingleLayout()188     public void testSingleLayout() {
189         final ViewInfo firstViewInfo = new ViewInfo(
190                 new ComplicationLayoutParams(
191                         100,
192                         100,
193                         ComplicationLayoutParams.POSITION_TOP
194                         | ComplicationLayoutParams.POSITION_END,
195                         ComplicationLayoutParams.DIRECTION_DOWN,
196                         0),
197                 Complication.CATEGORY_STANDARD,
198                 mLayout);
199 
200         final ComplicationLayoutEngine engine = createComplicationLayoutEngine();
201         addComplication(engine, firstViewInfo);
202 
203         // Ensure the view is added to the top end corner
204         verifyChange(firstViewInfo, true, lp -> {
205             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
206             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
207         });
208     }
209 
210     /**
211      * Makes sure the engine properly places a view within the {@link ConstraintLayout}.
212      */
213     @Test
testSnapToGuide()214     public void testSnapToGuide() {
215         final ViewInfo firstViewInfo = new ViewInfo(
216                 new ComplicationLayoutParams(
217                         100,
218                         100,
219                         ComplicationLayoutParams.POSITION_TOP
220                                 | ComplicationLayoutParams.POSITION_END,
221                         ComplicationLayoutParams.DIRECTION_DOWN,
222                         0,
223                         true),
224                 Complication.CATEGORY_STANDARD,
225                 mLayout);
226 
227         final ComplicationLayoutEngine engine = createComplicationLayoutEngine();
228         addComplication(engine, firstViewInfo);
229 
230         // Ensure the view is added to the top end corner
231         verifyChange(firstViewInfo, true, lp -> {
232             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
233             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
234             assertThat(lp.startToEnd == R.id.complication_end_guide).isTrue();
235         });
236     }
237 
238     /**
239      * Ensures layout in a particular direction updates.
240      */
241     @Test
testDirectionLayout()242     public void testDirectionLayout() {
243         final ComplicationLayoutEngine engine = createComplicationLayoutEngine();
244 
245         final ViewInfo firstViewInfo = new ViewInfo(
246                 new ComplicationLayoutParams(
247                         100,
248                         100,
249                         ComplicationLayoutParams.POSITION_TOP
250                                 | ComplicationLayoutParams.POSITION_END,
251                         ComplicationLayoutParams.DIRECTION_DOWN,
252                         0),
253                 Complication.CATEGORY_STANDARD,
254                 mLayout);
255 
256         addComplication(engine, firstViewInfo);
257 
258         firstViewInfo.clearInvocations();
259 
260         final ViewInfo secondViewInfo = new ViewInfo(
261                 new ComplicationLayoutParams(
262                         100,
263                         100,
264                         ComplicationLayoutParams.POSITION_TOP
265                                 | ComplicationLayoutParams.POSITION_END,
266                         ComplicationLayoutParams.DIRECTION_DOWN,
267                         0),
268                 Complication.CATEGORY_SYSTEM,
269                 mLayout);
270 
271         addComplication(engine, secondViewInfo);
272 
273         // The first added view should now be underneath the second view.
274         verifyChange(firstViewInfo, false, lp -> {
275             assertThat(lp.topToBottom == secondViewInfo.view.getId()).isTrue();
276             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
277         });
278 
279         // The second view should be in the top position.
280         verifyChange(secondViewInfo, true, lp -> {
281             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
282             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
283         });
284     }
285 
286     /**
287      * Ensures layout in a particular position updates.
288      */
289     @Test
testPositionLayout()290     public void testPositionLayout() {
291         final ComplicationLayoutEngine engine = createComplicationLayoutEngine();
292 
293         final ViewInfo firstViewInfo = new ViewInfo(
294                 new ComplicationLayoutParams(
295                         100,
296                         100,
297                         ComplicationLayoutParams.POSITION_TOP
298                                 | ComplicationLayoutParams.POSITION_END,
299                         ComplicationLayoutParams.DIRECTION_DOWN,
300                         0),
301                 Complication.CATEGORY_STANDARD,
302                 mLayout);
303 
304         addComplication(engine, firstViewInfo);
305 
306         final ViewInfo secondViewInfo = new ViewInfo(
307                 new ComplicationLayoutParams(
308                         100,
309                         100,
310                         ComplicationLayoutParams.POSITION_TOP
311                                 | ComplicationLayoutParams.POSITION_END,
312                         ComplicationLayoutParams.DIRECTION_DOWN,
313                         0),
314                 Complication.CATEGORY_SYSTEM,
315                 mLayout);
316 
317         addComplication(engine, secondViewInfo);
318 
319         firstViewInfo.clearInvocations();
320         secondViewInfo.clearInvocations();
321 
322         final ViewInfo thirdViewInfo = new ViewInfo(
323                 new ComplicationLayoutParams(
324                         100,
325                         100,
326                         ComplicationLayoutParams.POSITION_TOP
327                                 | ComplicationLayoutParams.POSITION_END,
328                         ComplicationLayoutParams.DIRECTION_START,
329                         1),
330                 Complication.CATEGORY_SYSTEM,
331                 mLayout);
332 
333         addComplication(engine, thirdViewInfo);
334 
335         // The first added view should now be underneath the second view.
336         verifyChange(firstViewInfo, false, lp -> {
337             assertThat(lp.topToBottom == secondViewInfo.view.getId()).isTrue();
338             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
339         });
340 
341         // The second view should be in underneath the third view.
342         verifyChange(secondViewInfo, false, lp -> {
343             assertThat(lp.topToBottom == thirdViewInfo.view.getId()).isTrue();
344             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
345         });
346 
347         // The third view should be in at the top.
348         verifyChange(thirdViewInfo, true, lp -> {
349             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
350             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
351         });
352 
353         final ViewInfo fourthViewInfo = new ViewInfo(
354                 new ComplicationLayoutParams(
355                         100,
356                         100,
357                         ComplicationLayoutParams.POSITION_TOP
358                                 | ComplicationLayoutParams.POSITION_END,
359                         ComplicationLayoutParams.DIRECTION_START,
360                         1),
361                 Complication.CATEGORY_STANDARD,
362                 mLayout);
363 
364         addComplication(engine, fourthViewInfo);
365 
366         verifyChange(fourthViewInfo, true, lp -> {
367             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
368             assertThat(lp.endToStart == thirdViewInfo.view.getId()).isTrue();
369         });
370     }
371 
372     /**
373      * Ensures default margin is applied
374      */
375     @Test
testDefaultMargin()376     public void testDefaultMargin() {
377         final int margin = 5;
378         final ComplicationLayoutEngine engine = createComplicationLayoutEngine(margin);
379 
380         final ViewInfo firstViewInfo = new ViewInfo(
381                 new ComplicationLayoutParams(
382                         100,
383                         100,
384                         ComplicationLayoutParams.POSITION_TOP
385                                 | ComplicationLayoutParams.POSITION_END,
386                         ComplicationLayoutParams.DIRECTION_DOWN,
387                         0),
388                 Complication.CATEGORY_STANDARD,
389                 mLayout);
390 
391         addComplication(engine, firstViewInfo);
392 
393         final ViewInfo secondViewInfo = new ViewInfo(
394                 new ComplicationLayoutParams(
395                         100,
396                         100,
397                         ComplicationLayoutParams.POSITION_TOP
398                                 | ComplicationLayoutParams.POSITION_END,
399                         ComplicationLayoutParams.DIRECTION_START,
400                         0),
401                 Complication.CATEGORY_SYSTEM,
402                 mLayout);
403 
404         addComplication(engine, secondViewInfo);
405 
406         firstViewInfo.clearInvocations();
407         secondViewInfo.clearInvocations();
408 
409         final ViewInfo thirdViewInfo = new ViewInfo(
410                 new ComplicationLayoutParams(
411                         100,
412                         100,
413                         ComplicationLayoutParams.POSITION_TOP
414                                 | ComplicationLayoutParams.POSITION_END,
415                         ComplicationLayoutParams.DIRECTION_START,
416                         1),
417                 Complication.CATEGORY_SYSTEM,
418                 mLayout);
419 
420         addComplication(engine, thirdViewInfo);
421 
422         // The first added view should now be underneath the third view.
423         verifyChange(firstViewInfo, false, lp -> {
424             assertThat(lp.topToBottom == thirdViewInfo.view.getId()).isTrue();
425             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
426             assertThat(lp.topMargin).isEqualTo(margin);
427         });
428 
429         // The second view should be to the start of the third view.
430         verifyChange(secondViewInfo, false, lp -> {
431             assertThat(lp.endToStart == thirdViewInfo.view.getId()).isTrue();
432             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
433             assertThat(lp.getMarginEnd()).isEqualTo(margin);
434         });
435 
436         // The third view should be at the top end corner. No margin should be applied.
437         verifyChange(thirdViewInfo, true, lp -> {
438             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
439             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
440             assertThat(lp.getMarginStart()).isEqualTo(0);
441             assertThat(lp.getMarginEnd()).isEqualTo(0);
442             assertThat(lp.topMargin).isEqualTo(0);
443             assertThat(lp.bottomMargin).isEqualTo(0);
444         });
445     }
446 
447     /**
448      * Ensures complication margin is applied
449      */
450     @Test
testComplicationMargin()451     public void testComplicationMargin() {
452         final int defaultMargin = 5;
453         final int complicationMargin = 10;
454         final ComplicationLayoutEngine engine = createComplicationLayoutEngine(defaultMargin);
455 
456         final ViewInfo firstViewInfo = new ViewInfo(
457                 new ComplicationLayoutParams(
458                         100,
459                         100,
460                         ComplicationLayoutParams.POSITION_TOP
461                                 | ComplicationLayoutParams.POSITION_END,
462                         ComplicationLayoutParams.DIRECTION_DOWN,
463                         0,
464                         complicationMargin),
465                 Complication.CATEGORY_STANDARD,
466                 mLayout);
467 
468         addComplication(engine, firstViewInfo);
469 
470         final ViewInfo secondViewInfo = new ViewInfo(
471                 new ComplicationLayoutParams(
472                         100,
473                         100,
474                         ComplicationLayoutParams.POSITION_TOP
475                                 | ComplicationLayoutParams.POSITION_END,
476                         ComplicationLayoutParams.DIRECTION_START,
477                         0),
478                 Complication.CATEGORY_SYSTEM,
479                 mLayout);
480 
481         addComplication(engine, secondViewInfo);
482 
483         firstViewInfo.clearInvocations();
484         secondViewInfo.clearInvocations();
485 
486         final ViewInfo thirdViewInfo = new ViewInfo(
487                 new ComplicationLayoutParams(
488                         100,
489                         100,
490                         ComplicationLayoutParams.POSITION_TOP
491                                 | ComplicationLayoutParams.POSITION_END,
492                         ComplicationLayoutParams.DIRECTION_START,
493                         1),
494                 Complication.CATEGORY_SYSTEM,
495                 mLayout);
496 
497         addComplication(engine, thirdViewInfo);
498 
499         // The first added view should now be underneath the third view.
500         verifyChange(firstViewInfo, false, lp -> {
501             assertThat(lp.topToBottom == thirdViewInfo.view.getId()).isTrue();
502             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
503             assertThat(lp.topMargin).isEqualTo(complicationMargin);
504         });
505 
506         // The second view should be to the start of the third view.
507         verifyChange(secondViewInfo, false, lp -> {
508             assertThat(lp.endToStart == thirdViewInfo.view.getId()).isTrue();
509             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
510             assertThat(lp.getMarginEnd()).isEqualTo(defaultMargin);
511         });
512     }
513 
514     /**
515      * Ensures layout sets correct max width constraint.
516      */
517     @Test
testWidthConstraint()518     public void testWidthConstraint() {
519         final int maxWidth = 20;
520         final ComplicationLayoutEngine engine = createComplicationLayoutEngine();
521 
522         final ViewInfo viewStartDirection = new ViewInfo(
523                 new ComplicationLayoutParams(
524                         100,
525                         100,
526                         ComplicationLayoutParams.POSITION_TOP
527                                 | ComplicationLayoutParams.POSITION_END,
528                         ComplicationLayoutParams.DIRECTION_START,
529                         0,
530                         5,
531                         maxWidth),
532                 Complication.CATEGORY_STANDARD,
533                 mLayout);
534         final ViewInfo viewEndDirection = new ViewInfo(
535                 new ComplicationLayoutParams(
536                         100,
537                         100,
538                         ComplicationLayoutParams.POSITION_TOP
539                                 | ComplicationLayoutParams.POSITION_START,
540                         ComplicationLayoutParams.DIRECTION_END,
541                         0,
542                         5,
543                         maxWidth),
544                 Complication.CATEGORY_STANDARD,
545                 mLayout);
546 
547         addComplication(engine, viewStartDirection);
548         addComplication(engine, viewEndDirection);
549 
550         // Verify both horizontal direction views have max width set correctly, and max height is
551         // not set.
552         verifyChange(viewStartDirection, false, lp -> {
553             assertThat(lp.matchConstraintMaxWidth).isEqualTo(maxWidth);
554             assertThat(lp.matchConstraintMaxHeight).isEqualTo(0);
555         });
556         verifyChange(viewEndDirection, false, lp -> {
557             assertThat(lp.matchConstraintMaxWidth).isEqualTo(maxWidth);
558             assertThat(lp.matchConstraintMaxHeight).isEqualTo(0);
559         });
560     }
561 
562     /**
563      * Ensures layout sets correct max height constraint.
564      */
565     @Test
testHeightConstraint()566     public void testHeightConstraint() {
567         final int maxHeight = 20;
568         final ComplicationLayoutEngine engine = createComplicationLayoutEngine();
569 
570         final ViewInfo viewUpDirection = new ViewInfo(
571                 new ComplicationLayoutParams(
572                         100,
573                         100,
574                         ComplicationLayoutParams.POSITION_BOTTOM
575                                 | ComplicationLayoutParams.POSITION_END,
576                         ComplicationLayoutParams.DIRECTION_UP,
577                         0,
578                         5,
579                         maxHeight),
580                 Complication.CATEGORY_STANDARD,
581                 mLayout);
582         final ViewInfo viewDownDirection = new ViewInfo(
583                 new ComplicationLayoutParams(
584                         100,
585                         100,
586                         ComplicationLayoutParams.POSITION_TOP
587                                 | ComplicationLayoutParams.POSITION_END,
588                         ComplicationLayoutParams.DIRECTION_DOWN,
589                         0,
590                         5,
591                         maxHeight),
592                 Complication.CATEGORY_STANDARD,
593                 mLayout);
594 
595         addComplication(engine, viewUpDirection);
596         addComplication(engine, viewDownDirection);
597 
598         // Verify both vertical direction views have max height set correctly, and max width is
599         // not set.
600         verifyChange(viewUpDirection, false, lp -> {
601             assertThat(lp.matchConstraintMaxHeight).isEqualTo(maxHeight);
602             assertThat(lp.matchConstraintMaxWidth).isEqualTo(0);
603         });
604         verifyChange(viewDownDirection, false, lp -> {
605             assertThat(lp.matchConstraintMaxHeight).isEqualTo(maxHeight);
606             assertThat(lp.matchConstraintMaxWidth).isEqualTo(0);
607         });
608     }
609 
610     /**
611      * Ensures layout does not set any constraint if not specified.
612      */
613     @Test
testConstraintNotSetWhenNotSpecified()614     public void testConstraintNotSetWhenNotSpecified() {
615         final ComplicationLayoutEngine engine = createComplicationLayoutEngine();
616 
617         final ViewInfo view = new ViewInfo(
618                 new ComplicationLayoutParams(
619                         100,
620                         100,
621                         ComplicationLayoutParams.POSITION_TOP
622                                 | ComplicationLayoutParams.POSITION_END,
623                         ComplicationLayoutParams.DIRECTION_DOWN,
624                         0,
625                         5),
626                 Complication.CATEGORY_STANDARD,
627                 mLayout);
628 
629         addComplication(engine, view);
630 
631         // Verify neither max height nor max width set.
632         verifyChange(view, false, lp -> {
633             assertThat(lp.matchConstraintMaxHeight).isEqualTo(0);
634             assertThat(lp.matchConstraintMaxWidth).isEqualTo(0);
635         });
636     }
637 
638     /**
639      * Ensures layout in a particular position updates.
640      */
641     @Test
testRemoval()642     public void testRemoval() {
643         final ComplicationLayoutEngine engine = createComplicationLayoutEngine();
644 
645         final ViewInfo firstViewInfo = new ViewInfo(
646                 new ComplicationLayoutParams(
647                         100,
648                         100,
649                         ComplicationLayoutParams.POSITION_TOP
650                                 | ComplicationLayoutParams.POSITION_END,
651                         ComplicationLayoutParams.DIRECTION_DOWN,
652                         0),
653                 Complication.CATEGORY_STANDARD,
654                 mLayout);
655 
656         engine.addComplication(firstViewInfo.id, firstViewInfo.view, firstViewInfo.lp,
657                 firstViewInfo.category);
658 
659         final ViewInfo secondViewInfo = new ViewInfo(
660                 new ComplicationLayoutParams(
661                         100,
662                         100,
663                         ComplicationLayoutParams.POSITION_TOP
664                                 | ComplicationLayoutParams.POSITION_END,
665                         ComplicationLayoutParams.DIRECTION_DOWN,
666                         0),
667                 Complication.CATEGORY_SYSTEM,
668                 mLayout);
669 
670         engine.addComplication(secondViewInfo.id, secondViewInfo.view, secondViewInfo.lp,
671                 secondViewInfo.category);
672 
673         firstViewInfo.clearInvocations();
674 
675         engine.removeComplication(secondViewInfo.id);
676         verify(mLayout).removeView(eq(secondViewInfo.view));
677 
678         verifyChange(firstViewInfo, true, lp -> {
679             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
680             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
681         });
682     }
683 
684     /**
685      * Ensures a second removal of a complication is a no-op.
686      */
687     @Test
testDoubleRemoval()688     public void testDoubleRemoval() {
689         final ComplicationLayoutEngine engine = createComplicationLayoutEngine();
690 
691         final ViewInfo firstViewInfo = new ViewInfo(
692                 new ComplicationLayoutParams(
693                         100,
694                         100,
695                         ComplicationLayoutParams.POSITION_TOP
696                                 | ComplicationLayoutParams.POSITION_END,
697                         ComplicationLayoutParams.DIRECTION_DOWN,
698                         0),
699                 Complication.CATEGORY_STANDARD,
700                 mLayout);
701 
702         engine.addComplication(firstViewInfo.id, firstViewInfo.view, firstViewInfo.lp,
703                 firstViewInfo.category);
704         verify(mLayout).addView(firstViewInfo.view);
705 
706         assertThat(engine.removeComplication(firstViewInfo.id)).isTrue();
707         verify(firstViewInfo.view).getParent();
708         verify(mLayout).removeView(firstViewInfo.view);
709 
710         Mockito.clearInvocations(mLayout, firstViewInfo.view);
711         assertThat(engine.removeComplication(firstViewInfo.id)).isFalse();
712         verify(firstViewInfo.view, never()).getParent();
713         verify(mLayout, never()).removeView(firstViewInfo.view);
714     }
715 
716     @Test
testGetViews()717     public void testGetViews() {
718         final ComplicationLayoutEngine engine = createComplicationLayoutEngine();
719 
720         final ViewInfo topEndView = new ViewInfo(
721                 new ComplicationLayoutParams(
722                         100,
723                         100,
724                         ComplicationLayoutParams.POSITION_TOP
725                                 | ComplicationLayoutParams.POSITION_END,
726                         ComplicationLayoutParams.DIRECTION_DOWN,
727                         0),
728                 Complication.CATEGORY_STANDARD,
729                 mLayout);
730 
731         addComplication(engine, topEndView);
732 
733         final ViewInfo topStartView = new ViewInfo(
734                 new ComplicationLayoutParams(
735                         100,
736                         100,
737                         ComplicationLayoutParams.POSITION_TOP
738                                 | ComplicationLayoutParams.POSITION_START,
739                         ComplicationLayoutParams.DIRECTION_DOWN,
740                         0),
741                 Complication.CATEGORY_SYSTEM,
742                 mLayout);
743 
744         addComplication(engine, topStartView);
745 
746         final ViewInfo bottomEndView = new ViewInfo(
747                 new ComplicationLayoutParams(
748                         100,
749                         100,
750                         ComplicationLayoutParams.POSITION_BOTTOM
751                                 | ComplicationLayoutParams.POSITION_END,
752                         ComplicationLayoutParams.DIRECTION_START,
753                         1),
754                 Complication.CATEGORY_SYSTEM,
755                 mLayout);
756 
757         addComplication(engine, bottomEndView);
758 
759         verifyViewsAtPosition(engine, ComplicationLayoutParams.POSITION_TOP, topStartView,
760                 topEndView);
761         verifyViewsAtPosition(engine,
762                 ComplicationLayoutParams.POSITION_TOP | ComplicationLayoutParams.POSITION_START,
763                 topStartView);
764         verifyViewsAtPosition(engine,
765                 ComplicationLayoutParams.POSITION_BOTTOM,
766                 bottomEndView);
767     }
768 
verifyViewsAtPosition(ComplicationLayoutEngine engine, int position, ViewInfo... views)769     private void verifyViewsAtPosition(ComplicationLayoutEngine engine, int position,
770             ViewInfo... views) {
771         final List<Integer> idList = engine.getViewsAtPosition(position).stream()
772                 .map(View::getId)
773                 .collect(Collectors.toList());
774 
775         assertThat(idList).containsExactlyElementsIn(
776                 Arrays.stream(views)
777                         .map(viewInfo -> viewInfo.view.getId())
778                         .collect(Collectors.toList()));
779     }
780 }
781