• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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