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(/* reqOpt= */ true, 56 CarUxRestrictions.UX_RESTRICTIONS_BASELINE, /* timestamp= */ 0).build(); 57 58 private Lifecycle.State mState = INITIALIZED; 59 60 private final FragmentController mMockFragmentController; 61 private final T mPreferenceController; 62 private final PreferenceScreen mScreen; 63 private boolean mSetPreferenceCalled; 64 65 /** 66 * Constructs a new helper. Call {@link #setPreference(Preference)} once initialization on the 67 * controller is complete to associate the controller with a preference. 68 * 69 * @param context the {@link Context} to use to instantiate the preference 70 * controller. 71 * @param preferenceControllerType the class type under test. 72 */ PreferenceControllerTestHelper(Context context, Class<T> preferenceControllerType)73 public PreferenceControllerTestHelper(Context context, Class<T> preferenceControllerType) { 74 mMockFragmentController = mock(FragmentController.class); 75 mPreferenceController = ReflectionHelpers.callConstructor(preferenceControllerType, 76 ClassParameter.from(Context.class, context), 77 ClassParameter.from(String.class, PREFERENCE_KEY), 78 ClassParameter.from(FragmentController.class, mMockFragmentController), 79 ClassParameter.from(CarUxRestrictions.class, UX_RESTRICTIONS)); 80 mScreen = new PreferenceManager(context).createPreferenceScreen(context); 81 } 82 83 /** 84 * Convenience constructor for a new helper for controllers which do not need to do additional 85 * initialization before a preference is set. 86 * 87 * @param preference the {@link Preference} to associate with the controller. 88 */ PreferenceControllerTestHelper(Context context, Class<T> preferenceControllerType, Preference preference)89 public PreferenceControllerTestHelper(Context context, Class<T> preferenceControllerType, 90 Preference preference) { 91 this(context, preferenceControllerType); 92 setPreference(preference); 93 } 94 95 /** 96 * Associates the controller with the given preference. This should only be called once. 97 */ setPreference(Preference preference)98 public void setPreference(Preference preference) { 99 if (mSetPreferenceCalled) { 100 throw new IllegalStateException( 101 "setPreference should only be called once. Create a new helper if needed."); 102 } 103 preference.setKey(PREFERENCE_KEY); 104 mScreen.addPreference(preference); 105 mPreferenceController.setPreference(preference); 106 mSetPreferenceCalled = true; 107 } 108 109 /** 110 * Returns the {@link PreferenceController} of this helper. 111 */ getController()112 public T getController() { 113 return mPreferenceController; 114 } 115 116 /** 117 * Returns a mock {@link FragmentController} that can be used to verify controller navigation 118 * and stub finding dialog fragments. 119 */ getMockFragmentController()120 public FragmentController getMockFragmentController() { 121 return mMockFragmentController; 122 } 123 124 /** 125 * Sends a {@link Lifecycle.Event} to the controller. This is preferred over calling the 126 * controller's lifecycle methods directly as it ensures intermediate events are dispatched. 127 * For example, sending {@link Lifecycle.Event#ON_START} to an 128 * {@link Lifecycle.State#INITIALIZED} controller will dispatch 129 * {@link Lifecycle.Event#ON_CREATE} and {@link Lifecycle.Event#ON_START} while moving the 130 * controller to the {@link Lifecycle.State#STARTED} state. 131 */ sendLifecycleEvent(Lifecycle.Event event)132 public void sendLifecycleEvent(Lifecycle.Event event) { 133 markState(getStateAfter(event)); 134 } 135 136 /** 137 * Move the {@link PreferenceController} to the given {@code state}. This is preferred over 138 * calling the controller's lifecycle methods directly as it ensures intermediate events are 139 * dispatched. For example, marking the {@link Lifecycle.State#STARTED} state on an 140 * {@link Lifecycle.State#INITIALIZED} controller will also send the 141 * {@link Lifecycle.Event#ON_CREATE} and {@link Lifecycle.Event#ON_START} events. 142 */ markState(Lifecycle.State state)143 public void markState(Lifecycle.State state) { 144 while (mState != state) { 145 while (mState.compareTo(state) > 0) { 146 dispatchEvent(downEvent(mState)); 147 } 148 while (mState.compareTo(state) < 0) { 149 dispatchEvent(upEvent(mState)); 150 } 151 } 152 } 153 getKey()154 public static String getKey() { 155 return PREFERENCE_KEY; 156 } 157 158 /* 159 * Ideally we would use androidx.lifecycle.LifecycleRegistry to drive the lifecycle changes. 160 * However, doing so led to test flakiness with an unknown root cause. We dispatch state 161 * changes manually for now, borrowing from LifecycleRegistry's implementation, pending 162 * further investigation. 163 */ 164 165 @NonNull getLifecycle()166 private Lifecycle getLifecycle() { 167 throw new UnsupportedOperationException(); 168 } 169 dispatchEvent(Lifecycle.Event event)170 private void dispatchEvent(Lifecycle.Event event) { 171 switch (event) { 172 case ON_CREATE: 173 mScreen.onAttached(); 174 mPreferenceController.onCreate(this::getLifecycle); 175 break; 176 case ON_START: 177 mPreferenceController.onStart(this::getLifecycle); 178 break; 179 case ON_RESUME: 180 mPreferenceController.onResume(this::getLifecycle); 181 break; 182 case ON_PAUSE: 183 mPreferenceController.onPause(this::getLifecycle); 184 break; 185 case ON_STOP: 186 mPreferenceController.onStop(this::getLifecycle); 187 break; 188 case ON_DESTROY: 189 mScreen.onDetached(); 190 mPreferenceController.onDestroy(this::getLifecycle); 191 break; 192 case ON_ANY: 193 throw new IllegalArgumentException(); 194 } 195 196 mState = getStateAfter(event); 197 } 198 getStateAfter(Lifecycle.Event event)199 private static Lifecycle.State getStateAfter(Lifecycle.Event event) { 200 switch (event) { 201 case ON_CREATE: 202 case ON_STOP: 203 return CREATED; 204 case ON_START: 205 case ON_PAUSE: 206 return STARTED; 207 case ON_RESUME: 208 return RESUMED; 209 case ON_DESTROY: 210 return DESTROYED; 211 case ON_ANY: 212 break; 213 } 214 throw new IllegalArgumentException("Unexpected event value " + event); 215 } 216 downEvent(Lifecycle.State state)217 private static Lifecycle.Event downEvent(Lifecycle.State state) { 218 switch (state) { 219 case INITIALIZED: 220 throw new IllegalArgumentException(); 221 case CREATED: 222 return ON_DESTROY; 223 case STARTED: 224 return ON_STOP; 225 case RESUMED: 226 return ON_PAUSE; 227 case DESTROYED: 228 throw new IllegalArgumentException(); 229 } 230 throw new IllegalArgumentException("Unexpected state value " + state); 231 } 232 upEvent(Lifecycle.State state)233 private static Lifecycle.Event upEvent(Lifecycle.State state) { 234 switch (state) { 235 case INITIALIZED: 236 case DESTROYED: 237 return ON_CREATE; 238 case CREATED: 239 return ON_START; 240 case STARTED: 241 return ON_RESUME; 242 case RESUMED: 243 throw new IllegalArgumentException(); 244 } 245 throw new IllegalArgumentException("Unexpected state value " + state); 246 } 247 } 248