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