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