Home | History | Annotate | Download | only in slices
      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.AVAILABLE;
     20 import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
     21 import static com.android.settings.core.BasePreferenceController.DISABLED_DEPENDENT_SETTING;
     22 import static com.android.settings.core.BasePreferenceController.DISABLED_FOR_USER;
     23 import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
     24 import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY;
     25 import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_PLATFORM_DEFINED;
     26 
     27 import android.annotation.ColorInt;
     28 import android.app.PendingIntent;
     29 import android.content.ContentResolver;
     30 import android.content.Context;
     31 import android.content.Intent;
     32 import android.net.Uri;
     33 import android.provider.Settings;
     34 import android.provider.SettingsSlicesContract;
     35 import android.text.TextUtils;
     36 import android.util.Log;
     37 import android.util.Pair;
     38 
     39 import com.android.internal.annotations.VisibleForTesting;
     40 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     41 import com.android.settings.R;
     42 import com.android.settings.SubSettings;
     43 import com.android.settings.Utils;
     44 import com.android.settings.core.BasePreferenceController;
     45 import com.android.settings.core.SliderPreferenceController;
     46 import com.android.settings.core.TogglePreferenceController;
     47 import com.android.settings.overlay.FeatureFactory;
     48 import com.android.settings.search.DatabaseIndexingUtils;
     49 import com.android.settingslib.SliceBroadcastRelay;
     50 import com.android.settingslib.core.AbstractPreferenceController;
     51 
     52 import android.support.v4.graphics.drawable.IconCompat;
     53 
     54 import java.util.ArrayList;
     55 import java.util.Arrays;
     56 import java.util.List;
     57 import java.util.stream.Collectors;
     58 
     59 import androidx.slice.Slice;
     60 import androidx.slice.builders.ListBuilder;
     61 import androidx.slice.builders.SliceAction;
     62 
     63 
     64 /**
     65  * Utility class to build Slices objects and Preference Controllers based on the Database managed
     66  * by {@link SlicesDatabaseHelper}
     67  */
     68 public class SliceBuilderUtils {
     69 
     70     private static final String TAG = "SliceBuilder";
     71 
     72     /**
     73      * Build a Slice from {@link SliceData}.
     74      *
     75      * @return a {@link Slice} based on the data provided by {@param sliceData}.
     76      * Will build an {@link Intent} based Slice unless the Preference Controller name in
     77      * {@param sliceData} is an inline controller.
     78      */
     79     public static Slice buildSlice(Context context, SliceData sliceData) {
     80         Log.d(TAG, "Creating slice for: " + sliceData.getPreferenceController());
     81         final BasePreferenceController controller = getPreferenceController(context, sliceData);
     82         final Pair<Integer, Object> sliceNamePair =
     83                 Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, sliceData.getKey());
     84         // Log Slice requests using the same schema as SharedPreferenceLogger (but with a different
     85         // action name).
     86         FeatureFactory.getFactory(context).getMetricsFeatureProvider()
     87                 .action(context, MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED, sliceNamePair);
     88 
     89         if (!controller.isAvailable()) {
     90             // Cannot guarantee setting page is accessible, let the presenter handle error case.
     91             return null;
     92         }
     93 
     94         if (controller.getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
     95             return buildUnavailableSlice(context, sliceData);
     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      * @return the {@link SliceData.SliceType} for the {@param controllerClassName} and key.
    113      */
    114     @SliceData.SliceType
    115     public static int getSliceType(Context context, String controllerClassName,
    116             String controllerKey) {
    117         BasePreferenceController controller = getPreferenceController(context, controllerClassName,
    118                 controllerKey);
    119         return controller.getSliceType();
    120     }
    121 
    122     /**
    123      * Splits the Settings Slice Uri path into its two expected components:
    124      * - intent/action
    125      * - key
    126      * <p>
    127      * Examples of valid paths are:
    128      * - /intent/wifi
    129      * - /intent/bluetooth
    130      * - /action/wifi
    131      * - /action/accessibility/servicename
    132      *
    133      * @param uri of the Slice. Follows pattern outlined in {@link SettingsSliceProvider}.
    134      * @return Pair whose first element {@code true} if the path is prepended with "intent", and
    135      * second is a key.
    136      */
    137     public static Pair<Boolean, String> getPathData(Uri uri) {
    138         final String path = uri.getPath();
    139         final String[] split = path.split("/", 3);
    140 
    141         // Split should be: [{}, SLICE_TYPE, KEY].
    142         // Example: "/action/wifi" -> [{}, "action", "wifi"]
    143         //          "/action/longer/path" -> [{}, "action", "longer/path"]
    144         if (split.length != 3) {
    145             return null;
    146         }
    147 
    148         final boolean isIntent = TextUtils.equals(SettingsSlicesContract.PATH_SETTING_INTENT,
    149                 split[1]);
    150 
    151         return new Pair<>(isIntent, split[2]);
    152     }
    153 
    154     /**
    155      * Looks at the controller classname in in {@link SliceData} from {@param sliceData}
    156      * and attempts to build an {@link AbstractPreferenceController}.
    157      */
    158     public static BasePreferenceController getPreferenceController(Context context,
    159             SliceData sliceData) {
    160         return getPreferenceController(context, sliceData.getPreferenceController(),
    161                 sliceData.getKey());
    162     }
    163 
    164     /**
    165      * @return {@link PendingIntent} for a non-primary {@link SliceAction}.
    166      */
    167     public static PendingIntent getActionIntent(Context context, String action, SliceData data) {
    168         final Intent intent = new Intent(action);
    169         intent.setClass(context, SliceBroadcastReceiver.class);
    170         intent.putExtra(EXTRA_SLICE_KEY, data.getKey());
    171         intent.putExtra(EXTRA_SLICE_PLATFORM_DEFINED, data.isPlatformDefined());
    172         return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent,
    173                 PendingIntent.FLAG_CANCEL_CURRENT);
    174     }
    175 
    176     /**
    177      * @return {@link PendingIntent} for the primary {@link SliceAction}.
    178      */
    179     public static PendingIntent getContentPendingIntent(Context context, SliceData sliceData) {
    180         final Intent intent = getContentIntent(context, sliceData);
    181         return PendingIntent.getActivity(context, 0 /* requestCode */, intent, 0 /* flags */);
    182     }
    183 
    184     /**
    185      * @return the summary text for a {@link Slice} built for {@param sliceData}.
    186      */
    187     public static CharSequence getSubtitleText(Context context,
    188             AbstractPreferenceController controller, SliceData sliceData) {
    189         CharSequence summaryText = sliceData.getScreenTitle();
    190         if (isValidSummary(context, summaryText) && !TextUtils.equals(summaryText,
    191                 sliceData.getTitle())) {
    192             return summaryText;
    193         }
    194 
    195         if (controller != null) {
    196             summaryText = controller.getSummary();
    197 
    198             if (isValidSummary(context, summaryText)) {
    199                 return summaryText;
    200             }
    201         }
    202 
    203         summaryText = sliceData.getSummary();
    204         if (isValidSummary(context, summaryText)) {
    205             return summaryText;
    206         }
    207 
    208         return "";
    209     }
    210 
    211     public static Uri getUri(String path, boolean isPlatformSlice) {
    212         final String authority = isPlatformSlice
    213                 ? SettingsSlicesContract.AUTHORITY
    214                 : SettingsSliceProvider.SLICE_AUTHORITY;
    215         return new Uri.Builder()
    216                 .scheme(ContentResolver.SCHEME_CONTENT)
    217                 .authority(authority)
    218                 .appendPath(path)
    219                 .build();
    220     }
    221 
    222     @VisibleForTesting
    223     static Intent getContentIntent(Context context, SliceData sliceData) {
    224         final Uri contentUri = new Uri.Builder().appendPath(sliceData.getKey()).build();
    225         final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
    226                 sliceData.getFragmentClassName(), sliceData.getKey(),
    227                 sliceData.getScreenTitle().toString(), 0 /* TODO */);
    228         intent.setClassName(context.getPackageName(), SubSettings.class.getName());
    229         intent.setData(contentUri);
    230         return intent;
    231     }
    232 
    233     private static Slice buildToggleSlice(Context context, SliceData sliceData,
    234             BasePreferenceController controller) {
    235         final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
    236         final IconCompat icon = IconCompat.createWithResource(context, sliceData.getIconResource());
    237         final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
    238         @ColorInt final int color = Utils.getColorAccent(context);
    239         final TogglePreferenceController toggleController =
    240                 (TogglePreferenceController) controller;
    241         final SliceAction sliceAction = getToggleAction(context, sliceData,
    242                 toggleController.isChecked());
    243         final List<String> keywords = buildSliceKeywords(sliceData);
    244 
    245         return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY)
    246                 .setAccentColor(color)
    247                 .addRow(rowBuilder -> rowBuilder
    248                         .setTitle(sliceData.getTitle())
    249                         .setSubtitle(subtitleText)
    250                         .setPrimaryAction(
    251                                 new SliceAction(contentIntent, icon, sliceData.getTitle()))
    252                         .addEndItem(sliceAction))
    253                 .setKeywords(keywords)
    254                 .build();
    255     }
    256 
    257     private static Slice buildIntentSlice(Context context, SliceData sliceData,
    258             BasePreferenceController controller) {
    259         final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
    260         final IconCompat icon = IconCompat.createWithResource(context, sliceData.getIconResource());
    261         final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
    262         @ColorInt final int color = Utils.getColorAccent(context);
    263         final List<String> keywords = buildSliceKeywords(sliceData);
    264 
    265         return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY)
    266                 .setAccentColor(color)
    267                 .addRow(rowBuilder -> rowBuilder
    268                         .setTitle(sliceData.getTitle())
    269                         .setSubtitle(subtitleText)
    270                         .setPrimaryAction(
    271                                 new SliceAction(contentIntent, icon, sliceData.getTitle())))
    272                 .setKeywords(keywords)
    273                 .build();
    274     }
    275 
    276     private static Slice buildSliderSlice(Context context, SliceData sliceData,
    277             BasePreferenceController controller) {
    278         final SliderPreferenceController sliderController = (SliderPreferenceController) controller;
    279         final PendingIntent actionIntent = getSliderAction(context, sliceData);
    280         final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
    281         final IconCompat icon = IconCompat.createWithResource(context, sliceData.getIconResource());
    282         final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
    283         @ColorInt final int color = Utils.getColorAccent(context);
    284         final SliceAction primaryAction = new SliceAction(contentIntent, icon,
    285                 sliceData.getTitle());
    286         final List<String> keywords = buildSliceKeywords(sliceData);
    287 
    288         return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY)
    289                 .setAccentColor(color)
    290                 .addInputRange(builder -> builder
    291                         .setTitle(sliceData.getTitle())
    292                         .setSubtitle(subtitleText)
    293                         .setPrimaryAction(primaryAction)
    294                         .setMax(sliderController.getMaxSteps())
    295                         .setValue(sliderController.getSliderPosition())
    296                         .setInputAction(actionIntent))
    297                 .setKeywords(keywords)
    298                 .build();
    299     }
    300 
    301     private static BasePreferenceController getPreferenceController(Context context,
    302             String controllerClassName, String controllerKey) {
    303         try {
    304             return BasePreferenceController.createInstance(context, controllerClassName);
    305         } catch (IllegalStateException e) {
    306             // Do nothing
    307         }
    308 
    309         return BasePreferenceController.createInstance(context, controllerClassName, controllerKey);
    310     }
    311 
    312     private static SliceAction getToggleAction(Context context, SliceData sliceData,
    313             boolean isChecked) {
    314         PendingIntent actionIntent = getActionIntent(context,
    315                 SettingsSliceProvider.ACTION_TOGGLE_CHANGED, sliceData);
    316         return new SliceAction(actionIntent, null, isChecked);
    317     }
    318 
    319     private static PendingIntent getSliderAction(Context context, SliceData sliceData) {
    320         return getActionIntent(context, SettingsSliceProvider.ACTION_SLIDER_CHANGED, sliceData);
    321     }
    322 
    323     private static boolean isValidSummary(Context context, CharSequence summary) {
    324         if (summary == null || TextUtils.isEmpty(summary.toString().trim())) {
    325             return false;
    326         }
    327 
    328         final CharSequence placeHolder = context.getText(R.string.summary_placeholder);
    329         final CharSequence doublePlaceHolder =
    330                 context.getText(R.string.summary_two_lines_placeholder);
    331 
    332         return !(TextUtils.equals(summary, placeHolder)
    333                 || TextUtils.equals(summary, doublePlaceHolder));
    334     }
    335 
    336     private static List<String> buildSliceKeywords(SliceData data) {
    337         final List<String> keywords = new ArrayList<>();
    338 
    339         keywords.add(data.getTitle());
    340 
    341         if (!TextUtils.equals(data.getTitle(), data.getScreenTitle())) {
    342             keywords.add(data.getScreenTitle().toString());
    343         }
    344 
    345         final String keywordString = data.getKeywords();
    346         if (keywordString != null) {
    347             final String[] keywordArray = keywordString.split(",");
    348             final List<String> strippedKeywords = Arrays.stream(keywordArray)
    349                     .map(s -> s = s.trim())
    350                     .collect(Collectors.toList());
    351             keywords.addAll(strippedKeywords);
    352         }
    353 
    354         return keywords;
    355     }
    356 
    357     private static Slice buildUnavailableSlice(Context context, SliceData data) {
    358         final String title = data.getTitle();
    359         final List<String> keywords = buildSliceKeywords(data);
    360         @ColorInt final int color = Utils.getColorAccent(context);
    361         final CharSequence summary = context.getText(R.string.disabled_dependent_setting_summary);
    362         final IconCompat icon = IconCompat.createWithResource(context, data.getIconResource());
    363         final SliceAction primaryAction = new SliceAction(getContentPendingIntent(context, data),
    364                 icon, title);
    365 
    366         return new ListBuilder(context, data.getUri(), ListBuilder.INFINITY)
    367                 .setAccentColor(color)
    368                 .addRow(builder -> builder
    369                         .setTitle(title)
    370                         .setTitleItem(icon)
    371                         .setSubtitle(summary)
    372                         .setPrimaryAction(primaryAction))
    373                 .setKeywords(keywords)
    374                 .build();
    375     }
    376 }
    377