1 /*
2  * Copyright 2020 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 androidx.car.app.testing;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.util.Pair;
22 
23 import androidx.car.app.Screen;
24 import androidx.car.app.model.Template;
25 import androidx.lifecycle.DefaultLifecycleObserver;
26 import androidx.lifecycle.Lifecycle;
27 import androidx.lifecycle.Lifecycle.Event;
28 import androidx.lifecycle.Lifecycle.State;
29 import androidx.lifecycle.LifecycleOwner;
30 
31 import org.jspecify.annotations.NonNull;
32 import org.jspecify.annotations.Nullable;
33 
34 import java.util.ArrayList;
35 import java.util.List;
36 
37 /**
38  * {@link ScreenController} provides API that allows testing of a {@link Screen}.
39  *
40  * <p>This controller will allows:
41  *
42  * <ul>
43  *   <li>Moving a {@link Screen} through its different {@link State}s.
44  *   <li>Retrieving all {@link Template}s returned from {@link Screen#onGetTemplate}. The values can
45  *       be reset with {@link #reset}.
46  * </ul>
47  */
48 public class ScreenController {
49     private final TestCarContext mTestCarContext;
50     private final Screen mScreen;
51     private final TestLifecycleOwner mLifecycleOwner;
52 
53     /**
54      * Creates a ScreenController to control a {@link Screen} for testing.
55      *
56      * @throws NullPointerException     if {@code screen} is null
57      * @throws IllegalArgumentException if {@code screen} was not created with a
58      *                                  {@link TestCarContext}
59      */
ScreenController(@onNull Screen screen)60     public ScreenController(@NonNull Screen screen) {
61         mScreen = requireNonNull(screen);
62         TestCarContext carContext = (TestCarContext) mScreen.getCarContext();
63         if (carContext == null) {
64             throw new IllegalArgumentException(
65                     "Screen should be created with TestCarContext for testing");
66         }
67         mTestCarContext = carContext;
68 
69         mLifecycleOwner = new TestLifecycleOwner();
70         mLifecycleOwner.getRegistry().addObserver(new ScreenLifecycleObserver());
71     }
72 
73     /** Returns the {@link Screen} being controlled. */
getScreen()74     public @NonNull Screen getScreen() {
75         return mScreen;
76     }
77 
78     /** Resets values tracked by this {@link ScreenController}. */
reset()79     public void reset() {
80         mTestCarContext.getCarService(TestAppManager.class).resetTemplatesStoredForScreen(
81                 getScreen());
82     }
83 
84     /**
85      * Returns all the {@link Template}s returned from {@link Screen#onGetTemplate} for the {@link
86      * Screen} being controlled.
87      *
88      * <p>The templates are stored in the order in which they were returned from
89      * {@link Screen#onGetTemplate}, where the first template in the list, is the first template
90      * returned.
91      *
92      * <p>The templates will be stored until {@link #reset} is called.
93      */
getTemplatesReturned()94     public @NonNull List<Template> getTemplatesReturned() {
95         List<Template> templates = new ArrayList<>();
96         for (Pair<Screen, Template> pair :
97                 mTestCarContext.getCarService(TestAppManager.class).getTemplatesReturned()) {
98             if (pair.first == getScreen()) {
99                 templates.add(pair.second);
100             }
101         }
102         return templates;
103     }
104 
105     /**
106      * Moves the {@link Screen} being controlled to the input {@code state}.
107      *
108      * <p>Note that moving the {@link Screen} up a state will also push the {@link Screen} onto
109      * the {@link androidx.car.app.ScreenManager}'s screen stack if it isn't the current top.
110      *
111      * <p>{@link Lifecycle.State#DESTROYED} is a terminal state, and you cannot move to any other
112      * state after the {@link Screen} reaches that state.
113      *
114      * @see Screen#getLifecycle
115      */
moveToState(Lifecycle.@onNull State state)116     public @NonNull ScreenController moveToState(Lifecycle.@NonNull State state) {
117         mLifecycleOwner.getRegistry().setCurrentState(state);
118         return this;
119     }
120 
121     /**
122      * Returns the result that was set via {@link Screen#setResult(Object)}, or {@code null} if
123      * one was not set.
124      */
getScreenResult()125     public @Nullable Object getScreenResult() {
126         return mScreen.getResultInternal();
127     }
128 
putScreenOnStackIfNotTop()129     void putScreenOnStackIfNotTop() {
130         TestScreenManager testScreenManager = mTestCarContext.getCarService(
131                 TestScreenManager.class);
132         if (!testScreenManager.hasScreens() || !mScreen.equals(testScreenManager.getTop())) {
133             testScreenManager.push(mScreen);
134         }
135     }
136 
dispatchLifecycleEvent(Event event)137     void dispatchLifecycleEvent(Event event) {
138         mScreen.dispatchLifecycleEvent(event);
139     }
140 
141     /**
142      * A helper class to forward the lifecycle events from this controller to the screen.
143      */
144     class ScreenLifecycleObserver implements DefaultLifecycleObserver {
145         @Override
onCreate(@onNull LifecycleOwner owner)146         public void onCreate(@NonNull LifecycleOwner owner) {
147             putScreenOnStackIfNotTop();
148             dispatchLifecycleEvent(Event.ON_CREATE);
149         }
150 
151         @Override
onStart(@onNull LifecycleOwner owner)152         public void onStart(@NonNull LifecycleOwner owner) {
153             putScreenOnStackIfNotTop();
154             dispatchLifecycleEvent(Event.ON_START);
155         }
156 
157         @Override
onResume(@onNull LifecycleOwner owner)158         public void onResume(@NonNull LifecycleOwner owner) {
159             putScreenOnStackIfNotTop();
160             dispatchLifecycleEvent(Event.ON_RESUME);
161         }
162 
163         @Override
onPause(@onNull LifecycleOwner owner)164         public void onPause(@NonNull LifecycleOwner owner) {
165             dispatchLifecycleEvent(Event.ON_PAUSE);
166         }
167 
168         @Override
onStop(@onNull LifecycleOwner owner)169         public void onStop(@NonNull LifecycleOwner owner) {
170             dispatchLifecycleEvent(Event.ON_STOP);
171         }
172 
173         @Override
onDestroy(@onNull LifecycleOwner owner)174         public void onDestroy(@NonNull LifecycleOwner owner) {
175             dispatchLifecycleEvent(Event.ON_DESTROY);
176         }
177     }
178 }
179