1 /*
2  * Copyright (C) 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.settings.slices;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.Mockito.doReturn;
22 import static org.mockito.Mockito.spy;
23 import static org.mockito.Mockito.verify;
24 
25 import android.app.settings.SettingsEnums;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.net.Uri;
29 import android.provider.Settings;
30 import android.provider.SettingsSlicesContract;
31 import android.util.Pair;
32 
33 import androidx.core.graphics.drawable.IconCompat;
34 import androidx.slice.Slice;
35 import androidx.slice.SliceMetadata;
36 import androidx.slice.SliceProvider;
37 import androidx.slice.core.SliceAction;
38 import androidx.slice.widget.SliceLiveData;
39 
40 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
41 import com.android.settings.R;
42 import com.android.settings.core.BasePreferenceController;
43 import com.android.settings.testutils.FakeCopyableController;
44 import com.android.settings.testutils.FakeFeatureFactory;
45 import com.android.settings.testutils.FakeInvalidSliderController;
46 import com.android.settings.testutils.FakeSliderController;
47 import com.android.settings.testutils.FakeToggleController;
48 import com.android.settings.testutils.FakeUnavailablePreferenceController;
49 import com.android.settings.testutils.SliceTester;
50 
51 import org.junit.Before;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 import org.robolectric.RobolectricTestRunner;
55 import org.robolectric.RuntimeEnvironment;
56 
57 @RunWith(RobolectricTestRunner.class)
58 public class SliceBuilderUtilsTest {
59 
60     private final String KEY = "KEY";
61     private final String TITLE = "title";
62     private final String SUMMARY = "summary";
63     private final String SCREEN_TITLE = "screen title";
64     private final String KEYWORDS = "a, b, c";
65     private final String FRAGMENT_NAME = "fragment name";
66     private final int ICON = R.drawable.ic_settings_accent;
67     private final Uri URI = Uri.parse("content://com.android.settings.slices/test");
68     private final Class TOGGLE_CONTROLLER = FakeToggleController.class;
69     private final Class SLIDER_CONTROLLER = FakeSliderController.class;
70     private final Class INVALID_SLIDER_CONTROLLER = FakeInvalidSliderController.class;
71     private final Class COPYABLE_CONTROLLER = FakeCopyableController.class;
72     private final Class CONTEXT_CONTROLLER = FakeContextOnlyPreferenceController.class;
73 
74     private final String INTENT_PATH = SettingsSlicesContract.PATH_SETTING_INTENT + "/" + KEY;
75     private final String ACTION_PATH = SettingsSlicesContract.PATH_SETTING_ACTION + "/" + KEY;
76 
77     private Context mContext;
78     private FakeFeatureFactory mFeatureFactory;
79 
80     @Before
setUp()81     public void setUp() {
82         mContext = RuntimeEnvironment.application;
83         mFeatureFactory = FakeFeatureFactory.setupForTest();
84         // Set-up specs for SliceMetadata.
85         SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
86     }
87 
88     @Test
buildIntentSlice_returnsMatchingSlice()89     public void buildIntentSlice_returnsMatchingSlice() {
90         final SliceData sliceData = getMockData(CONTEXT_CONTROLLER, SliceData.SliceType.INTENT);
91         final Slice slice = SliceBuilderUtils.buildSlice(mContext, sliceData);
92 
93         SliceTester.testSettingsIntentSlice(mContext, slice, sliceData);
94     }
95 
96     @Test
buildToggleSlice_returnsMatchingSlice()97     public void buildToggleSlice_returnsMatchingSlice() {
98         final SliceData mockData = getMockData(TOGGLE_CONTROLLER, SliceData.SliceType.SWITCH);
99 
100         final Slice slice = SliceBuilderUtils.buildSlice(mContext, mockData);
101         verify(mFeatureFactory.metricsFeatureProvider)
102                 .action(SettingsEnums.PAGE_UNKNOWN,
103                         MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED,
104                         SettingsEnums.PAGE_UNKNOWN,
105                         mockData.getKey(),
106                         0);
107         SliceTester.testSettingsToggleSlice(mContext, slice, mockData);
108     }
109 
110     @Test
buildSliderSlice_returnsMatchingSlice()111     public void buildSliderSlice_returnsMatchingSlice() {
112         final SliceData data = getMockData(SLIDER_CONTROLLER, SliceData.SliceType.SLIDER);
113 
114         final Slice slice = SliceBuilderUtils.buildSlice(mContext, data);
115         verify(mFeatureFactory.metricsFeatureProvider)
116                 .action(SettingsEnums.PAGE_UNKNOWN,
117                         MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED,
118                         SettingsEnums.PAGE_UNKNOWN,
119                         data.getKey(),
120                         0);
121         SliceTester.testSettingsSliderSlice(mContext, slice, data);
122     }
123 
124     @Test
buildCopyableSlice_returnsMatchingSlice()125     public void buildCopyableSlice_returnsMatchingSlice() {
126         final SliceData mockData = getMockData(COPYABLE_CONTROLLER, -1);
127 
128         final Slice slice = SliceBuilderUtils.buildSlice(mContext, mockData);
129         verify(mFeatureFactory.metricsFeatureProvider)
130                 .action(SettingsEnums.PAGE_UNKNOWN,
131                         MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED,
132                         SettingsEnums.PAGE_UNKNOWN,
133                         mockData.getKey(),
134                         0);
135         SliceTester.testSettingsCopyableSlice(mContext, slice, mockData);
136     }
137 
138     @Test
testGetPreferenceController_buildsMatchingController()139     public void testGetPreferenceController_buildsMatchingController() {
140         final BasePreferenceController controller =
141                 SliceBuilderUtils.getPreferenceController(mContext, getMockData());
142 
143         assertThat(controller).isInstanceOf(FakeToggleController.class);
144     }
145 
146     @Test
testGetPreferenceController_contextOnly_buildsMatchingController()147     public void testGetPreferenceController_contextOnly_buildsMatchingController() {
148         final BasePreferenceController controller =
149                 SliceBuilderUtils.getPreferenceController(mContext,
150                         getMockData(CONTEXT_CONTROLLER, 0));
151 
152         assertThat(controller).isInstanceOf(FakeContextOnlyPreferenceController.class);
153     }
154 
155     @Test
getDynamicSummary_allowDynamicSummary_returnsControllerSummary()156     public void getDynamicSummary_allowDynamicSummary_returnsControllerSummary() {
157         final SliceData data = getMockData();
158         final FakePreferenceController controller = spy(
159                 new FakePreferenceController(mContext, KEY));
160         final String controllerSummary = "new_Summary";
161         doReturn(controllerSummary).when(controller).getSummary();
162 
163         final CharSequence summary = SliceBuilderUtils.getSubtitleText(mContext, controller, data);
164 
165         assertThat(summary).isEqualTo(controllerSummary);
166     }
167 
168     @Test
getDynamicSummary_allowDynamicSummary_nullSummary_returnsNull()169     public void getDynamicSummary_allowDynamicSummary_nullSummary_returnsNull() {
170         final SliceData data = getMockData();
171         final FakePreferenceController controller = spy(
172                 new FakePreferenceController(mContext, KEY));
173 
174         doReturn(null).when(controller).getSummary();
175 
176         final CharSequence summary = SliceBuilderUtils.getSubtitleText(mContext, controller, data);
177 
178         assertThat(summary).isNull();
179     }
180 
181     @Test
getDynamicSummary_noScreenTitle_returnsPrefControllerSummary()182     public void getDynamicSummary_noScreenTitle_returnsPrefControllerSummary() {
183         final SliceData data = getMockData("", "");
184         final FakePreferenceController controller = spy(
185                 new FakePreferenceController(mContext, KEY));
186         final String controllerSummary = "new_Summary";
187         doReturn(controllerSummary).when(controller).getSummary();
188 
189         final CharSequence summary = SliceBuilderUtils.getSubtitleText(mContext, controller, data);
190 
191         assertThat(summary).isEqualTo(controllerSummary);
192     }
193 
194     @Test
getDynamicSummary_screenTitleMatchesTitle_returnsPrefControllerSummary()195     public void getDynamicSummary_screenTitleMatchesTitle_returnsPrefControllerSummary() {
196         final SliceData data = getMockData("", TITLE);
197         final FakePreferenceController controller = spy(
198                 new FakePreferenceController(mContext, KEY));
199         final String controllerSummary = "new_Summary";
200         doReturn(controllerSummary).when(controller).getSummary();
201 
202         final CharSequence summary = SliceBuilderUtils.getSubtitleText(mContext, controller, data);
203 
204         assertThat(summary).isEqualTo(controllerSummary);
205     }
206 
207     @Test
getDynamicSummary_emptyScreenTitle_emptyControllerSummary_returnsEmptyString()208     public void getDynamicSummary_emptyScreenTitle_emptyControllerSummary_returnsEmptyString() {
209         final SliceData data = getMockData(null, null);
210         final FakePreferenceController controller = new FakePreferenceController(mContext, KEY);
211         final CharSequence summary = SliceBuilderUtils.getSubtitleText(mContext, controller, data);
212 
213         assertThat(summary).isNull();
214     }
215 
216     @Test
getDynamicSummary_screenTitleAndControllerPlaceholder_returnsSliceEmptyString()217     public void getDynamicSummary_screenTitleAndControllerPlaceholder_returnsSliceEmptyString() {
218         final String summaryPlaceholder = mContext.getString(R.string.summary_placeholder);
219         final SliceData data = getMockData(summaryPlaceholder, summaryPlaceholder);
220         final FakePreferenceController controller = spy(
221                 new FakePreferenceController(mContext, KEY));
222         doReturn(summaryPlaceholder).when(controller).getSummary();
223 
224         CharSequence summary = SliceBuilderUtils.getSubtitleText(mContext, controller, data);
225 
226         assertThat(summary).isEqualTo(summaryPlaceholder);
227     }
228 
229     @Test
getPathData_splitsIntentUri()230     public void getPathData_splitsIntentUri() {
231         final Uri uri = new Uri.Builder()
232                 .authority(SettingsSliceProvider.SLICE_AUTHORITY)
233                 .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT)
234                 .appendPath(KEY)
235                 .build();
236 
237         final Pair<Boolean, String> pathPair = SliceBuilderUtils.getPathData(uri);
238 
239         assertThat(pathPair.first).isTrue();
240         assertThat(pathPair.second).isEqualTo(KEY);
241     }
242 
243     @Test
getPathData_splitsActionUri()244     public void getPathData_splitsActionUri() {
245         final Uri uri = new Uri.Builder()
246                 .authority(SettingsSliceProvider.SLICE_AUTHORITY)
247                 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
248                 .appendPath(KEY)
249                 .build();
250 
251         final Pair<Boolean, String> pathPair = SliceBuilderUtils.getPathData(uri);
252 
253         assertThat(pathPair.first).isFalse();
254         assertThat(pathPair.second).isEqualTo(KEY);
255     }
256 
257     @Test
getPathData_noKey_returnsNull()258     public void getPathData_noKey_returnsNull() {
259         final Uri uri = new Uri.Builder()
260                 .authority(SettingsSliceProvider.SLICE_AUTHORITY)
261                 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
262                 .build();
263 
264         final Pair<Boolean, String> pathPair = SliceBuilderUtils.getPathData(uri);
265 
266         assertThat(pathPair).isNull();
267     }
268 
269     @Test
getPathData_extraArg_returnsNull()270     public void getPathData_extraArg_returnsNull() {
271         final Uri uri = new Uri.Builder()
272                 .authority(SettingsSliceProvider.SLICE_AUTHORITY)
273                 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
274                 .appendPath(KEY)
275                 .appendPath(KEY)
276                 .build();
277 
278         final Pair<Boolean, String> pathPair = SliceBuilderUtils.getPathData(uri);
279 
280         assertThat(pathPair.first).isFalse();
281         assertThat(pathPair.second).isEqualTo(KEY + "/" + KEY);
282     }
283 
284     @Test
testUnsupportedSlice_validTitleSummary()285     public void testUnsupportedSlice_validTitleSummary() {
286         final SliceData data = getMockData(FakeUnavailablePreferenceController.class,
287                 SliceData.SliceType.SWITCH);
288         Settings.Global.putInt(mContext.getContentResolver(),
289                 FakeUnavailablePreferenceController.AVAILABILITY_KEY,
290                 BasePreferenceController.UNSUPPORTED_ON_DEVICE);
291 
292         final Slice slice = SliceBuilderUtils.buildSlice(mContext, data);
293 
294         assertThat(slice).isNull();
295     }
296 
297     @Test
testDisabledForUserSlice_validTitleSummary()298     public void testDisabledForUserSlice_validTitleSummary() {
299         final SliceData data = getMockData(FakeUnavailablePreferenceController.class,
300                 SliceData.SliceType.SWITCH);
301         Settings.Global.putInt(mContext.getContentResolver(),
302                 FakeUnavailablePreferenceController.AVAILABILITY_KEY,
303                 BasePreferenceController.DISABLED_FOR_USER);
304 
305         final Slice slice = SliceBuilderUtils.buildSlice(mContext, data);
306 
307         assertThat(slice).isNull();
308     }
309 
310     @Test
testDisabledDependentSettingSlice_validTitleSummary()311     public void testDisabledDependentSettingSlice_validTitleSummary() {
312         final SliceData data = getMockData(FakeUnavailablePreferenceController.class,
313                 SliceData.SliceType.INTENT);
314         Settings.Global.putInt(mContext.getContentResolver(),
315                 FakeUnavailablePreferenceController.AVAILABILITY_KEY,
316                 BasePreferenceController.DISABLED_DEPENDENT_SETTING);
317 
318         final Slice slice = SliceBuilderUtils.buildSlice(mContext, data);
319 
320         verify(mFeatureFactory.metricsFeatureProvider)
321                 .action(SettingsEnums.PAGE_UNKNOWN,
322                         MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED,
323                         SettingsEnums.PAGE_UNKNOWN,
324                         data.getKey(),
325                         0);
326 
327         SliceTester.testSettingsUnavailableSlice(mContext, slice, data);
328     }
329 
330     @Test
testConditionallyUnavailableSlice_validTitleSummary()331     public void testConditionallyUnavailableSlice_validTitleSummary() {
332         final SliceData data = getMockData(FakeUnavailablePreferenceController.class,
333                 SliceData.SliceType.SWITCH);
334         Settings.Global.putInt(mContext.getContentResolver(),
335                 FakeUnavailablePreferenceController.AVAILABILITY_KEY,
336                 BasePreferenceController.CONDITIONALLY_UNAVAILABLE);
337 
338         final Slice slice = SliceBuilderUtils.buildSlice(mContext, data);
339 
340         verify(mFeatureFactory.metricsFeatureProvider)
341                 .action(SettingsEnums.PAGE_UNKNOWN,
342                         MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED,
343                         SettingsEnums.PAGE_UNKNOWN,
344                         data.getKey(),
345                         0);
346     }
347 
348     @Test
testContentIntent_includesUniqueData()349     public void testContentIntent_includesUniqueData() {
350         final SliceData sliceData = getMockData();
351         final Uri expectedUri = new Uri.Builder().appendPath(sliceData.getKey()).build();
352 
353         final Intent intent = SliceBuilderUtils.getContentIntent(mContext, sliceData);
354         final Uri intentData = intent.getData();
355 
356         assertThat(intentData).isEqualTo(expectedUri);
357     }
358 
359     @Test
buildIntentSlice_noIconPassed_returnsSliceWithIcon()360     public void buildIntentSlice_noIconPassed_returnsSliceWithIcon() {
361         final int expectedIconResource = IconCompat.createWithResource(mContext,
362                 R.drawable.ic_settings_accent).toIcon().getResId();
363         final SliceData sliceData = getMockData(CONTEXT_CONTROLLER, SliceData.SliceType.INTENT,
364                 0x0);
365 
366         final Slice slice = SliceBuilderUtils.buildSlice(mContext, sliceData);
367 
368         final SliceMetadata metadata = SliceMetadata.from(mContext, slice);
369         final SliceAction primaryAction = metadata.getPrimaryAction();
370         final int actualIconResource = primaryAction.getIcon().toIcon().getResId();
371         assertThat(actualIconResource).isEqualTo(expectedIconResource);
372     }
373 
374     @Test
buildDisabledDependentSlice_noIconPassed_returnsSliceWithIcon()375     public void buildDisabledDependentSlice_noIconPassed_returnsSliceWithIcon() {
376         final int expectedIconResource = IconCompat.createWithResource(mContext,
377                 R.drawable.ic_settings_accent).toIcon().getResId();
378         final SliceData data = getMockData(FakeUnavailablePreferenceController.class,
379                 SUMMARY, SliceData.SliceType.SWITCH, SCREEN_TITLE, 0 /* icon */,
380                 null /* unavailableSliceSubtitle */);
381         Settings.Global.putInt(mContext.getContentResolver(),
382                 FakeUnavailablePreferenceController.AVAILABILITY_KEY,
383                 BasePreferenceController.DISABLED_DEPENDENT_SETTING);
384 
385         final Slice slice = SliceBuilderUtils.buildSlice(mContext, data);
386 
387         final SliceMetadata metadata = SliceMetadata.from(mContext, slice);
388         final SliceAction primaryAction = metadata.getPrimaryAction();
389         final int actualIconResource = primaryAction.getIcon().toIcon().getResId();
390         assertThat(actualIconResource).isEqualTo(expectedIconResource);
391     }
392 
393     @Test
buildToggleSlice_noIconPassed_returnsSliceWithIcon()394     public void buildToggleSlice_noIconPassed_returnsSliceWithIcon() {
395         final int expectedIconResource = IconCompat.createWithResource(mContext,
396                 R.drawable.ic_settings_accent).toIcon().getResId();
397         final SliceData mockData = getMockData(TOGGLE_CONTROLLER, SliceData.SliceType.SWITCH,
398                 0x0);
399 
400         final Slice slice = SliceBuilderUtils.buildSlice(mContext, mockData);
401 
402         final SliceMetadata metadata = SliceMetadata.from(mContext, slice);
403         final SliceAction primaryAction = metadata.getPrimaryAction();
404         final int actualIconResource = primaryAction.getIcon().toIcon().getResId();
405         assertThat(actualIconResource).isEqualTo(expectedIconResource);
406     }
407 
408     @Test
buildSliderSlice_noIconPassed_returnsSliceWithIcon()409     public void buildSliderSlice_noIconPassed_returnsSliceWithIcon() {
410         final int expectedIconResource = IconCompat.createWithResource(mContext,
411                 R.drawable.ic_settings_accent).toIcon().getResId();
412         final SliceData data = getMockData(SLIDER_CONTROLLER, SliceData.SliceType.SLIDER, 0x0);
413 
414         final Slice slice = SliceBuilderUtils.buildSlice(mContext, data);
415 
416         final SliceMetadata metadata = SliceMetadata.from(mContext, slice);
417         final SliceAction primaryAction = metadata.getPrimaryAction();
418         final int actualIconResource = primaryAction.getIcon().toIcon().getResId();
419         assertThat(actualIconResource).isEqualTo(expectedIconResource);
420     }
421 
422     @Test
buildSliderSlice_invalidSlider_returnNull()423     public void buildSliderSlice_invalidSlider_returnNull() {
424         final SliceData data = getMockData(INVALID_SLIDER_CONTROLLER, SliceData.SliceType.SLIDER,
425                 0x0 /* icon */);
426 
427         assertThat(SliceBuilderUtils.buildSlice(mContext, data)).isNull();
428     }
429 
430     @Test
getSafeIcon_replacesEmptyIconWithSettingsIcon()431     public void getSafeIcon_replacesEmptyIconWithSettingsIcon() {
432         final int settingsIcon = R.drawable.ic_settings_accent;
433         final int zeroIcon = 0x0;
434         final SliceData data = getMockData(TOGGLE_CONTROLLER, SliceData.SliceType.SWITCH,
435                 zeroIcon);
436 
437         final IconCompat actualIcon = SliceBuilderUtils.getSafeIcon(mContext, data);
438 
439         final int actualIconResource = actualIcon.toIcon().getResId();
440         assertThat(actualIconResource).isNotEqualTo(zeroIcon);
441         assertThat(actualIconResource).isEqualTo(settingsIcon);
442     }
443 
444     @Test
getSafeIcon_invalidResource_shouldFallbackToSettingsIcon()445     public void getSafeIcon_invalidResource_shouldFallbackToSettingsIcon() {
446         final int settingsIcon = R.drawable.ic_settings_accent;
447         final int badIcon = 0x12345678;
448         final SliceData data = getMockData(TOGGLE_CONTROLLER, SliceData.SliceType.SWITCH,
449                 badIcon);
450 
451         final IconCompat actualIcon = SliceBuilderUtils.getSafeIcon(mContext, data);
452 
453         final int actualIconResource = actualIcon.toIcon().getResId();
454         assertThat(actualIconResource).isEqualTo(settingsIcon);
455     }
456 
getMockData()457     private SliceData getMockData() {
458         return getMockData(TOGGLE_CONTROLLER, SUMMARY, SliceData.SliceType.SWITCH, SCREEN_TITLE,
459                 ICON, null /* unavailableSliceSubtitle */);
460     }
461 
getMockData(Class prefController, int sliceType, int icon)462     private SliceData getMockData(Class prefController, int sliceType, int icon) {
463         return getMockData(prefController, SUMMARY, sliceType, SCREEN_TITLE,
464                 icon, null /* unavailableSliceSubtitle */);
465     }
466 
getMockData(String summary, String screenTitle)467     private SliceData getMockData(String summary, String screenTitle) {
468         return getMockData(TOGGLE_CONTROLLER, summary, SliceData.SliceType.SWITCH, screenTitle,
469                 ICON, null /* unavailableSliceSubtitle */);
470     }
471 
getMockData(Class prefController, int sliceType)472     private SliceData getMockData(Class prefController, int sliceType) {
473         return getMockData(prefController, SUMMARY, sliceType, SCREEN_TITLE, ICON,
474                 null /* unavailableSliceSubtitle */);
475     }
476 
getMockData(Class prefController, String summary, int sliceType, String screenTitle, int icon, String unavailableSliceSubtitle)477     private SliceData getMockData(Class prefController, String summary, int sliceType,
478             String screenTitle, int icon, String unavailableSliceSubtitle) {
479         return new SliceData.Builder()
480                 .setKey(KEY)
481                 .setTitle(TITLE)
482                 .setSummary(summary)
483                 .setScreenTitle(screenTitle)
484                 .setKeywords(KEYWORDS)
485                 .setIcon(icon)
486                 .setFragmentName(FRAGMENT_NAME)
487                 .setUri(URI)
488                 .setPreferenceControllerClassName(prefController.getName())
489                 .setSliceType(sliceType)
490                 .setUnavailableSliceSubtitle(unavailableSliceSubtitle)
491                 .build();
492     }
493 }
494