• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY;
20 
21 import static com.android.settings.SettingsActivity.EXTRA_IS_FROM_SLICE;
22 import static com.android.settings.core.BasePreferenceController.DISABLED_DEPENDENT_SETTING;
23 import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY;
24 
25 import android.annotation.ColorInt;
26 import android.app.PendingIntent;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.os.UserHandle;
33 import android.provider.SettingsSlicesContract;
34 import android.text.TextUtils;
35 import android.util.ArraySet;
36 import android.util.Log;
37 import android.util.Pair;
38 
39 import androidx.annotation.VisibleForTesting;
40 import androidx.core.graphics.drawable.IconCompat;
41 import androidx.slice.Slice;
42 import androidx.slice.builders.ListBuilder;
43 import androidx.slice.builders.ListBuilder.InputRangeBuilder;
44 import androidx.slice.builders.ListBuilder.RowBuilder;
45 import androidx.slice.builders.SliceAction;
46 
47 import com.android.settings.R;
48 import com.android.settings.SettingsActivity;
49 import com.android.settings.SubSettings;
50 import com.android.settings.Utils;
51 import com.android.settings.accessibility.AccessibilitySlicePreferenceController;
52 import com.android.settings.core.BasePreferenceController;
53 import com.android.settings.core.SliderPreferenceController;
54 import com.android.settings.core.SubSettingLauncher;
55 import com.android.settings.core.TogglePreferenceController;
56 import com.android.settingslib.RestrictedLockUtils;
57 import com.android.settingslib.RestrictedLockUtilsInternal;
58 import com.android.settingslib.core.AbstractPreferenceController;
59 
60 import java.util.Arrays;
61 import java.util.List;
62 import java.util.Set;
63 import java.util.stream.Collectors;
64 
65 
66 /**
67  * Utility class to build Slices objects and Preference Controllers based on the Database managed
68  * by {@link SlicesDatabaseHelper}
69  */
70 public class SliceBuilderUtils {
71 
72     private static final String TAG = "SliceBuilder";
73 
74     /**
75      * Build a Slice from {@link SliceData}.
76      *
77      * @return a {@link Slice} based on the data provided by {@param sliceData}.
78      * Will build an {@link Intent} based Slice unless the Preference Controller name in
79      * {@param sliceData} is an inline controller.
80      */
buildSlice(Context context, SliceData sliceData)81     public static Slice buildSlice(Context context, SliceData sliceData) {
82         Log.d(TAG, "Creating slice for: " + sliceData.getPreferenceController());
83         final BasePreferenceController controller = getPreferenceController(context, sliceData);
84 
85         if (!controller.isAvailable()) {
86             // Cannot guarantee setting page is accessible, let the presenter handle error case.
87             return null;
88         }
89 
90         if (controller.getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
91             return buildUnavailableSlice(context, sliceData);
92         }
93 
94         String userRestriction = sliceData.getUserRestriction();
95         if (!TextUtils.isEmpty(userRestriction)) {
96             RestrictedLockUtils.EnforcedAdmin admin =
97                     RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context,
98                             userRestriction, UserHandle.myUserId());
99             if (admin != null) {
100                 return buildIntentSlice(context, sliceData, controller);
101             }
102         }
103 
104         switch (sliceData.getSliceType()) {
105             case SliceData.SliceType.INTENT:
106                 return buildIntentSlice(context, sliceData, controller);
107             case SliceData.SliceType.SWITCH:
108                 return buildToggleSlice(context, sliceData, controller);
109             case SliceData.SliceType.SLIDER:
110                 return buildSliderSlice(context, sliceData, controller);
111             default:
112                 throw new IllegalArgumentException(
113                         "Slice type passed was invalid: " + sliceData.getSliceType());
114         }
115     }
116 
117     /**
118      * Splits the Settings Slice Uri path into its two expected components:
119      * - intent/action
120      * - key
121      * <p>
122      * Examples of valid paths are:
123      * - /intent/wifi
124      * - /intent/bluetooth
125      * - /action/wifi
126      * - /action/accessibility/servicename
127      *
128      * @param uri of the Slice. Follows pattern outlined in {@link SettingsSliceProvider}.
129      * @return Pair whose first element {@code true} if the path is prepended with "intent", and
130      * second is a key.
131      */
getPathData(Uri uri)132     public static Pair<Boolean, String> getPathData(Uri uri) {
133         final String path = uri.getPath();
134         final String[] split = path.split("/", 3);
135 
136         // Split should be: [{}, SLICE_TYPE, KEY].
137         // Example: "/action/wifi" -> [{}, "action", "wifi"]
138         //          "/action/longer/path" -> [{}, "action", "longer/path"]
139         if (split.length != 3) {
140             return null;
141         }
142 
143         final boolean isIntent = TextUtils.equals(SettingsSlicesContract.PATH_SETTING_INTENT,
144                 split[1]);
145 
146         return new Pair<>(isIntent, split[2]);
147     }
148 
149     /**
150      * Looks at the controller classname in in {@link SliceData} from {@param sliceData}
151      * and attempts to build an {@link AbstractPreferenceController}.
152      */
getPreferenceController(Context context, SliceData sliceData)153     public static BasePreferenceController getPreferenceController(Context context,
154             SliceData sliceData) {
155         return getPreferenceController(context, sliceData.getPreferenceController(),
156                 sliceData.getKey());
157     }
158 
159     /**
160      * @return {@link PendingIntent} for a non-primary {@link SliceAction}.
161      */
getActionIntent(Context context, String action, SliceData data)162     public static PendingIntent getActionIntent(Context context, String action, SliceData data) {
163         final Intent intent = new Intent(action)
164                 .setData(data.getUri())
165                 .setClass(context, SliceBroadcastReceiver.class)
166                 .putExtra(EXTRA_SLICE_KEY, data.getKey());
167         return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent,
168                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
169     }
170 
171     /**
172      * @return {@link PendingIntent} for the primary {@link SliceAction}.
173      */
getContentPendingIntent(Context context, SliceData sliceData)174     public static PendingIntent getContentPendingIntent(Context context, SliceData sliceData) {
175         final Intent intent = getContentIntent(context, sliceData);
176         return PendingIntent.getActivity(context, 0 /* requestCode */, intent,
177                 PendingIntent.FLAG_IMMUTABLE);
178     }
179 
180     /**
181      * @return the summary text for a {@link Slice} built for {@param sliceData}.
182      */
getSubtitleText(Context context, BasePreferenceController controller, SliceData sliceData)183     public static CharSequence getSubtitleText(Context context,
184             BasePreferenceController controller, SliceData sliceData) {
185 
186         // Priority 1 : User prefers showing the dynamic summary in slice view rather than static
187         // summary. Note it doesn't require a valid summary - so we can force some slices to have
188         // empty summaries (ex: volume).
189         if (controller.useDynamicSliceSummary()) {
190             return controller.getSummary();
191         }
192 
193         // Priority 2: Show summary from slice data.
194         CharSequence summaryText = sliceData.getSummary();
195         if (isValidSummary(context, summaryText)) {
196             return summaryText;
197         }
198 
199         // Priority 3: Show screen title.
200         summaryText = sliceData.getScreenTitle();
201         if (isValidSummary(context, summaryText) && !TextUtils.equals(summaryText,
202                 sliceData.getTitle())) {
203             return summaryText;
204         }
205 
206         // Priority 4: Show empty text.
207         return "";
208     }
209 
buildSearchResultPageIntent(Context context, String className, String key, String screenTitle, int sourceMetricsCategory, int highlightMenuRes)210     public static Intent buildSearchResultPageIntent(Context context, String className, String key,
211             String screenTitle, int sourceMetricsCategory, int highlightMenuRes) {
212         final Bundle args = new Bundle();
213         String highlightMenuKey = null;
214         if (highlightMenuRes != 0) {
215             highlightMenuKey = context.getString(highlightMenuRes);
216             if (TextUtils.isEmpty(highlightMenuKey)) {
217                 Log.w(TAG, "Invalid menu key res from: " + screenTitle);
218             }
219         }
220         args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
221         final Intent searchDestination = new SubSettingLauncher(context)
222                 .setDestination(className)
223                 .setArguments(args)
224                 .setTitleText(screenTitle)
225                 .setSourceMetricsCategory(sourceMetricsCategory)
226                 .toIntent();
227         searchDestination
228                 .putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key)
229                 .putExtra(EXTRA_IS_FROM_SLICE, true)
230                 .putExtra(EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY, highlightMenuKey)
231                 .setAction("com.android.settings.SEARCH_RESULT_TRAMPOLINE")
232                 .setComponent(null);
233         searchDestination.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
234 
235         return searchDestination;
236     }
237 
238     /**
239      * Build a search result page intent for {@link CustomSliceable}
240      */
buildSearchResultPageIntent(Context context, String className, String key, String screenTitle, int sourceMetricsCategory, CustomSliceable sliceable)241     public static Intent buildSearchResultPageIntent(Context context, String className, String key,
242             String screenTitle, int sourceMetricsCategory, CustomSliceable sliceable) {
243         return buildSearchResultPageIntent(context, className, key, screenTitle,
244                 sourceMetricsCategory, sliceable.getSliceHighlightMenuRes());
245     }
246 
getContentIntent(Context context, SliceData sliceData)247     public static Intent getContentIntent(Context context, SliceData sliceData) {
248         final Uri contentUri = new Uri.Builder().appendPath(sliceData.getKey()).build();
249         final String screenTitle = TextUtils.isEmpty(sliceData.getScreenTitle()) ? null
250                 : sliceData.getScreenTitle().toString();
251         final Intent intent = buildSearchResultPageIntent(context,
252                 sliceData.getFragmentClassName(), sliceData.getKey(),
253                 screenTitle, 0 /* TODO */, sliceData.getHighlightMenuRes());
254         intent.setClassName(context.getPackageName(), SubSettings.class.getName());
255         intent.setData(contentUri);
256         return intent;
257     }
258 
buildToggleSlice(Context context, SliceData sliceData, BasePreferenceController controller)259     private static Slice buildToggleSlice(Context context, SliceData sliceData,
260             BasePreferenceController controller) {
261         final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
262         final IconCompat icon = getSafeIcon(context, sliceData);
263         final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
264         @ColorInt final int color = Utils.getColorAccentDefaultColor(context);
265         final TogglePreferenceController toggleController =
266                 (TogglePreferenceController) controller;
267         final SliceAction sliceAction = getToggleAction(context, sliceData,
268                 toggleController.isChecked());
269         final Set<String> keywords = buildSliceKeywords(sliceData);
270         final RowBuilder rowBuilder = new RowBuilder()
271                 .setTitle(sliceData.getTitle())
272                 .setPrimaryAction(
273                         SliceAction.createDeeplink(contentIntent, icon,
274                                 ListBuilder.ICON_IMAGE, sliceData.getTitle()))
275                 .addEndItem(sliceAction);
276         if (!Utils.isSettingsIntelligence(context)) {
277             rowBuilder.setSubtitle(subtitleText);
278         }
279 
280         return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY)
281                 .setAccentColor(color)
282                 .addRow(rowBuilder)
283                 .setKeywords(keywords)
284                 .build();
285     }
286 
buildIntentSlice(Context context, SliceData sliceData, BasePreferenceController controller)287     private static Slice buildIntentSlice(Context context, SliceData sliceData,
288             BasePreferenceController controller) {
289         final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
290         final IconCompat icon = getSafeIcon(context, sliceData);
291         final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
292         @ColorInt final int color = Utils.getColorAccentDefaultColor(context);
293         final Set<String> keywords = buildSliceKeywords(sliceData);
294         final RowBuilder rowBuilder = new RowBuilder()
295                 .setTitle(sliceData.getTitle())
296                 .setPrimaryAction(
297                         SliceAction.createDeeplink(contentIntent, icon,
298                                 ListBuilder.ICON_IMAGE,
299                                 sliceData.getTitle()));
300         if (!Utils.isSettingsIntelligence(context)) {
301             rowBuilder.setSubtitle(subtitleText);
302         }
303 
304         return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY)
305                 .setAccentColor(color)
306                 .addRow(rowBuilder)
307                 .setKeywords(keywords)
308                 .build();
309     }
310 
buildSliderSlice(Context context, SliceData sliceData, BasePreferenceController controller)311     private static Slice buildSliderSlice(Context context, SliceData sliceData,
312             BasePreferenceController controller) {
313         final SliderPreferenceController sliderController = (SliderPreferenceController) controller;
314         if (sliderController.getMax() <= sliderController.getMin()) {
315             Log.e(TAG, "Invalid sliderController: " + sliderController.getPreferenceKey());
316             return null;
317         }
318         final PendingIntent actionIntent = getSliderAction(context, sliceData);
319         final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
320         final IconCompat icon = getSafeIcon(context, sliceData);
321         @ColorInt int color = Utils.getColorAccentDefaultColor(context);
322         final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
323         final SliceAction primaryAction = SliceAction.createDeeplink(contentIntent, icon,
324                 ListBuilder.ICON_IMAGE, sliceData.getTitle());
325         final Set<String> keywords = buildSliceKeywords(sliceData);
326 
327         int cur = sliderController.getSliderPosition();
328         if (cur < sliderController.getMin()) {
329             cur = sliderController.getMin();
330         }
331         if (cur > sliderController.getMax()) {
332             cur = sliderController.getMax();
333         }
334         final InputRangeBuilder inputRangeBuilder = new InputRangeBuilder()
335                 .setTitle(sliceData.getTitle())
336                 .setPrimaryAction(primaryAction)
337                 .setMax(sliderController.getMax())
338                 .setMin(sliderController.getMin())
339                 .setValue(cur)
340                 .setInputAction(actionIntent);
341         if (sliceData.getIconResource() != 0) {
342             inputRangeBuilder.setTitleItem(icon, ListBuilder.ICON_IMAGE);
343             color = CustomSliceable.COLOR_NOT_TINTED;
344         }
345         if (!Utils.isSettingsIntelligence(context)) {
346             inputRangeBuilder.setSubtitle(subtitleText);
347         }
348 
349         SliceAction endItemAction = sliderController.getSliceEndItem(context);
350         if (endItemAction != null) {
351             inputRangeBuilder.addEndItem(endItemAction);
352         }
353 
354         return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY)
355                 .setAccentColor(color)
356                 .addInputRange(inputRangeBuilder)
357                 .setKeywords(keywords)
358                 .build();
359     }
360 
getPreferenceController(Context context, String controllerClassName, String controllerKey)361     static BasePreferenceController getPreferenceController(Context context,
362             String controllerClassName, String controllerKey) {
363         try {
364             return BasePreferenceController.createInstance(context, controllerClassName);
365         } catch (IllegalStateException e) {
366             // Do nothing
367         }
368 
369         return BasePreferenceController.createInstance(context, controllerClassName, controllerKey);
370     }
371 
getToggleAction(Context context, SliceData sliceData, boolean isChecked)372     private static SliceAction getToggleAction(Context context, SliceData sliceData,
373             boolean isChecked) {
374         PendingIntent actionIntent = getActionIntent(context,
375                 SettingsSliceProvider.ACTION_TOGGLE_CHANGED, sliceData);
376         return SliceAction.createToggle(actionIntent, null, isChecked);
377     }
378 
getSliderAction(Context context, SliceData sliceData)379     private static PendingIntent getSliderAction(Context context, SliceData sliceData) {
380         return getActionIntent(context, SettingsSliceProvider.ACTION_SLIDER_CHANGED, sliceData);
381     }
382 
isValidSummary(Context context, CharSequence summary)383     private static boolean isValidSummary(Context context, CharSequence summary) {
384         if (summary == null || TextUtils.isEmpty(summary.toString().trim())) {
385             return false;
386         }
387 
388         final CharSequence placeHolder = context.getText(R.string.summary_placeholder);
389         final CharSequence doublePlaceHolder =
390                 context.getText(R.string.summary_two_lines_placeholder);
391 
392         return !(TextUtils.equals(summary, placeHolder)
393                 || TextUtils.equals(summary, doublePlaceHolder));
394     }
395 
buildSliceKeywords(SliceData data)396     private static Set<String> buildSliceKeywords(SliceData data) {
397         final Set<String> keywords = new ArraySet<>();
398 
399         keywords.add(data.getTitle());
400 
401         if (!TextUtils.isEmpty(data.getScreenTitle())
402                 && !TextUtils.equals(data.getTitle(), data.getScreenTitle())) {
403             keywords.add(data.getScreenTitle().toString());
404         }
405 
406         final String keywordString = data.getKeywords();
407         if (keywordString != null) {
408             final String[] keywordArray = keywordString.split(",");
409             final List<String> strippedKeywords = Arrays.stream(keywordArray)
410                     .map(s -> s = s.trim())
411                     .collect(Collectors.toList());
412             keywords.addAll(strippedKeywords);
413         }
414 
415         return keywords;
416     }
417 
buildUnavailableSlice(Context context, SliceData data)418     private static Slice buildUnavailableSlice(Context context, SliceData data) {
419         final String title = data.getTitle();
420         final Set<String> keywords = buildSliceKeywords(data);
421         @ColorInt final int color = Utils.getColorAccentDefaultColor(context);
422 
423         final String customSubtitle = data.getUnavailableSliceSubtitle();
424         final CharSequence subtitle = !TextUtils.isEmpty(customSubtitle) ? customSubtitle
425                 : context.getText(R.string.disabled_dependent_setting_summary);
426         final IconCompat icon = getSafeIcon(context, data);
427         final SliceAction primaryAction = SliceAction.createDeeplink(
428                 getContentPendingIntent(context, data),
429                 icon, ListBuilder.ICON_IMAGE, title);
430         final RowBuilder rowBuilder = new RowBuilder()
431                 .setTitle(title)
432                 .setTitleItem(icon, ListBuilder.ICON_IMAGE)
433                 .setPrimaryAction(primaryAction);
434         if (!Utils.isSettingsIntelligence(context)) {
435             rowBuilder.setSubtitle(subtitle);
436         }
437 
438         return new ListBuilder(context, data.getUri(), ListBuilder.INFINITY)
439                 .setAccentColor(color)
440                 .addRow(rowBuilder)
441                 .setKeywords(keywords)
442                 .build();
443     }
444 
445     @VisibleForTesting
getSafeIcon(Context context, SliceData data)446     static IconCompat getSafeIcon(Context context, SliceData data) {
447         int iconResource = data.getIconResource();
448 
449         if (iconResource == 0) {
450             iconResource = R.drawable.ic_settings_accent;
451         }
452         try {
453             // LINT.IfChange(createA11yIcon)
454             if (AccessibilitySlicePreferenceController.class.getName().equals(
455                     data.getPreferenceController())) {
456                 ComponentName serviceComponent = ComponentName.unflattenFromString(data.getKey());
457                 return IconCompat.createWithResource(
458                         context.createPackageContext(serviceComponent.getPackageName(), 0),
459                         iconResource);
460                 // LINT.ThenChange()
461             } else {
462                 return IconCompat.createWithResource(context, iconResource);
463             }
464         } catch (Exception e) {
465             Log.w(TAG, "Falling back to settings icon because there is an error getting slice icon "
466                     + data.getUri(), e);
467             return IconCompat.createWithResource(context, R.drawable.ic_settings_accent);
468         }
469     }
470 }
471