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