1 /* 2 * Copyright 2018 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.car.settings.common; 18 19 import static androidx.lifecycle.Lifecycle.Event.ON_CREATE; 20 import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY; 21 import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE; 22 import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; 23 import static androidx.lifecycle.Lifecycle.Event.ON_START; 24 import static androidx.lifecycle.Lifecycle.Event.ON_STOP; 25 import static androidx.lifecycle.Lifecycle.State.CREATED; 26 import static androidx.lifecycle.Lifecycle.State.DESTROYED; 27 import static androidx.lifecycle.Lifecycle.State.INITIALIZED; 28 import static androidx.lifecycle.Lifecycle.State.RESUMED; 29 import static androidx.lifecycle.Lifecycle.State.STARTED; 30 31 import static org.mockito.Mockito.mock; 32 33 import android.car.drivingstate.CarUxRestrictions; 34 import android.content.Context; 35 36 import androidx.annotation.NonNull; 37 import androidx.lifecycle.Lifecycle; 38 import androidx.preference.Preference; 39 import androidx.preference.PreferenceManager; 40 import androidx.preference.PreferenceScreen; 41 42 import org.robolectric.util.ReflectionHelpers; 43 import org.robolectric.util.ReflectionHelpers.ClassParameter; 44 45 /** 46 * Helper for testing {@link PreferenceController} classes. 47 * 48 * @param <T> the type of preference controller under test. 49 */ 50 public class PreferenceControllerTestHelper<T extends PreferenceController> { 51 52 private static final String PREFERENCE_KEY = "preference_key"; 53 54 private static final CarUxRestrictions UX_RESTRICTIONS = 55 new CarUxRestrictions.Builder( 56 /* reqOpt= */ true, 57 CarUxRestrictions.UX_RESTRICTIONS_BASELINE, 58 /* timestamp= */ 0) 59 .build(); 60 61 private Lifecycle.State mState = INITIALIZED; 62 63 private final FragmentController mMockFragmentController; 64 private final T mPreferenceController; 65 private final PreferenceScreen mScreen; 66 private boolean mSetPreferenceCalled; 67 68 /** 69 * Constructs a new helper. Call {@link #setPreference(Preference)} once initialization on the 70 * controller is complete to associate the controller with a preference. 71 * 72 * @param context the {@link Context} to use to instantiate the preference controller. 73 * @param preferenceControllerType the class type under test. 74 */ PreferenceControllerTestHelper(Context context, Class<T> preferenceControllerType)75 public PreferenceControllerTestHelper(Context context, Class<T> preferenceControllerType) { 76 mMockFragmentController = mock(FragmentController.class); 77 mPreferenceController = 78 ReflectionHelpers.callConstructor( 79 preferenceControllerType, 80 ClassParameter.from(Context.class, context), 81 ClassParameter.from(String.class, PREFERENCE_KEY), 82 ClassParameter.from(FragmentController.class, mMockFragmentController), 83 ClassParameter.from(CarUxRestrictions.class, UX_RESTRICTIONS)); 84 mScreen = new PreferenceManager(context).createPreferenceScreen(context); 85 } 86 87 /** 88 * Constructs a new helper. Call {@link #setPreference(Preference)} once initialization on the 89 * controller is complete to associate the controller with a preference. 90 * 91 * @param context the {@link Context} to use to instantiate the preference controller. 92 * @param preferenceControllerType the class type under test. 93 * @param fragmentController Mock {@link FragmentController} to use for the preference 94 * controller. 95 */ PreferenceControllerTestHelper( Context context, Class<T> preferenceControllerType, FragmentController fragmentController)96 public PreferenceControllerTestHelper( 97 Context context, 98 Class<T> preferenceControllerType, 99 FragmentController fragmentController) { 100 mMockFragmentController = fragmentController; 101 mPreferenceController = 102 ReflectionHelpers.callConstructor( 103 preferenceControllerType, 104 ClassParameter.from(Context.class, context), 105 ClassParameter.from(String.class, PREFERENCE_KEY), 106 ClassParameter.from(FragmentController.class, mMockFragmentController), 107 ClassParameter.from(CarUxRestrictions.class, UX_RESTRICTIONS)); 108 mScreen = new PreferenceManager(context).createPreferenceScreen(context); 109 } 110 111 /** 112 * Convenience constructor for a new helper for controllers which do not need to do additional 113 * initialization before a preference is set. 114 * 115 * @param preference the {@link Preference} to associate with the controller. 116 */ PreferenceControllerTestHelper( Context context, Class<T> preferenceControllerType, Preference preference)117 public PreferenceControllerTestHelper( 118 Context context, Class<T> preferenceControllerType, Preference preference) { 119 this(context, preferenceControllerType); 120 setPreference(preference); 121 } 122 123 /** 124 * Convenience constructor for a new helper for controllers which do not need to do additional 125 * initialization before a preference is set. 126 * 127 * @param preference the {@link Preference} to associate with the controller. 128 * @param fragmentController Mock {@link FragmentController} to use for the preference 129 * controller. 130 */ PreferenceControllerTestHelper( Context context, Class<T> preferenceControllerType, Preference preference, FragmentController fragmentController)131 public PreferenceControllerTestHelper( 132 Context context, 133 Class<T> preferenceControllerType, 134 Preference preference, 135 FragmentController fragmentController) { 136 this(context, preferenceControllerType, fragmentController); 137 setPreference(preference); 138 } 139 140 /** Associates the controller with the given preference. This should only be called once. */ setPreference(Preference preference)141 public void setPreference(Preference preference) { 142 if (mSetPreferenceCalled) { 143 throw new IllegalStateException( 144 "setPreference should only be called once. Create a new helper if needed."); 145 } 146 preference.setKey(PREFERENCE_KEY); 147 mScreen.addPreference(preference); 148 mPreferenceController.setPreference(preference); 149 mSetPreferenceCalled = true; 150 } 151 152 /** Returns the {@link PreferenceController} of this helper. */ getController()153 public T getController() { 154 return mPreferenceController; 155 } 156 157 /** 158 * Returns a mock {@link FragmentController} that can be used to verify controller navigation 159 * and stub finding dialog fragments. 160 */ getMockFragmentController()161 public FragmentController getMockFragmentController() { 162 return mMockFragmentController; 163 } 164 165 /** 166 * Sends a {@link Lifecycle.Event} to the controller. This is preferred over calling the 167 * controller's lifecycle methods directly as it ensures intermediate events are dispatched. For 168 * example, sending {@link Lifecycle.Event#ON_START} to an {@link Lifecycle.State#INITIALIZED} 169 * controller will dispatch {@link Lifecycle.Event#ON_CREATE} and {@link 170 * Lifecycle.Event#ON_START} while moving the controller to the {@link Lifecycle.State#STARTED} 171 * state. 172 */ sendLifecycleEvent(Lifecycle.Event event)173 public void sendLifecycleEvent(Lifecycle.Event event) { 174 markState(getStateAfter(event)); 175 } 176 177 /** 178 * Move the {@link PreferenceController} to the given {@code state}. This is preferred over 179 * calling the controller's lifecycle methods directly as it ensures intermediate events are 180 * dispatched. For example, marking the {@link Lifecycle.State#STARTED} state on an {@link 181 * Lifecycle.State#INITIALIZED} controller will also send the {@link Lifecycle.Event#ON_CREATE} 182 * and {@link Lifecycle.Event#ON_START} events. 183 */ markState(Lifecycle.State state)184 public void markState(Lifecycle.State state) { 185 while (mState != state) { 186 while (mState.compareTo(state) > 0) { 187 dispatchEvent(downEvent(mState)); 188 } 189 while (mState.compareTo(state) < 0) { 190 dispatchEvent(upEvent(mState)); 191 } 192 } 193 } 194 getKey()195 public static String getKey() { 196 return PREFERENCE_KEY; 197 } 198 199 /* 200 * Ideally we would use androidx.lifecycle.LifecycleRegistry to drive the lifecycle changes. 201 * However, doing so led to test flakiness with an unknown root cause. We dispatch state 202 * changes manually for now, borrowing from LifecycleRegistry's implementation, pending 203 * further investigation. 204 */ 205 206 @NonNull getLifecycle()207 private Lifecycle getLifecycle() { 208 throw new UnsupportedOperationException(); 209 } 210 dispatchEvent(Lifecycle.Event event)211 private void dispatchEvent(Lifecycle.Event event) { 212 switch (event) { 213 case ON_CREATE: 214 mScreen.onAttached(); 215 mPreferenceController.onCreate(this::getLifecycle); 216 break; 217 case ON_START: 218 mPreferenceController.onStart(this::getLifecycle); 219 break; 220 case ON_RESUME: 221 mPreferenceController.onResume(this::getLifecycle); 222 break; 223 case ON_PAUSE: 224 mPreferenceController.onPause(this::getLifecycle); 225 break; 226 case ON_STOP: 227 mPreferenceController.onStop(this::getLifecycle); 228 break; 229 case ON_DESTROY: 230 mScreen.onDetached(); 231 mPreferenceController.onDestroy(this::getLifecycle); 232 break; 233 case ON_ANY: 234 throw new IllegalArgumentException(); 235 } 236 237 mState = getStateAfter(event); 238 } 239 getStateAfter(Lifecycle.Event event)240 private static Lifecycle.State getStateAfter(Lifecycle.Event event) { 241 switch (event) { 242 case ON_CREATE: 243 case ON_STOP: 244 return CREATED; 245 case ON_START: 246 case ON_PAUSE: 247 return STARTED; 248 case ON_RESUME: 249 return RESUMED; 250 case ON_DESTROY: 251 return DESTROYED; 252 case ON_ANY: 253 break; 254 } 255 throw new IllegalArgumentException("Unexpected event value " + event); 256 } 257 downEvent(Lifecycle.State state)258 private static Lifecycle.Event downEvent(Lifecycle.State state) { 259 switch (state) { 260 case INITIALIZED: 261 throw new IllegalArgumentException(); 262 case CREATED: 263 return ON_DESTROY; 264 case STARTED: 265 return ON_STOP; 266 case RESUMED: 267 return ON_PAUSE; 268 case DESTROYED: 269 throw new IllegalArgumentException(); 270 } 271 throw new IllegalArgumentException("Unexpected state value " + state); 272 } 273 upEvent(Lifecycle.State state)274 private static Lifecycle.Event upEvent(Lifecycle.State state) { 275 switch (state) { 276 case INITIALIZED: 277 case DESTROYED: 278 return ON_CREATE; 279 case CREATED: 280 return ON_START; 281 case STARTED: 282 return ON_RESUME; 283 case RESUMED: 284 throw new IllegalArgumentException(); 285 } 286 throw new IllegalArgumentException("Unexpected state value " + state); 287 } 288 } 289