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