2 * Copyright (C) 2017 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.settings.slices;
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;
27 import android.app.PendingIntent;
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.pm.PackageManager;
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;
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.SettingsActivity;
43 import com.android.settings.SubSettings;
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.core.AbstractPreferenceController;
51 import android.support.v4.graphics.drawable.IconCompat;
53 import java.util.ArrayList;
54 import java.util.Arrays;
55 import java.util.List;
57 import androidx.slice.Slice;
58 import androidx.slice.builders.ListBuilder;
59 import androidx.slice.builders.SliceAction;
63 * Utility class to build Slices objects and Preference Controllers based on the Database managed
64 * by {@link SlicesDatabaseHelper}
66 public class SliceBuilderUtils {
68 private static final String TAG = "SliceBuilder";
70 // A Slice should not be store for longer than 60,000 milliseconds / 1 minute.
71 public static final long SLICE_TTL_MILLIS = 60000;
74 * Build a Slice from {@link SliceData}.
76 * @return a {@link Slice} based on the data provided by {@param sliceData}.
77 * Will build an {@link Intent} based Slice unless the Preference Controller name in
78 * {@param sliceData} is an inline controller.
80 public static Slice buildSlice(Context context, SliceData sliceData) {
81 Log.d(TAG, "Creating slice for: " + sliceData.getPreferenceController());
82 final BasePreferenceController controller = getPreferenceController(context, sliceData);
83 final Pair<Integer, Object> sliceNamePair =
84 Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, sliceData.getKey());
85 // Log Slice requests using the same schema as SharedPreferenceLogger (but with a different
87 FeatureFactory.getFactory(context).getMetricsFeatureProvider()
88 .action(context, MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED, sliceNamePair);
90 if (controller.getAvailabilityStatus() != AVAILABLE) {
91 return buildUnavailableSlice(context, sliceData, controller);
94 switch (sliceData.getSliceType()) {
95 case SliceData.SliceType.INTENT:
96 return buildIntentSlice(context, sliceData, controller);
97 case SliceData.SliceType.SWITCH:
98 return buildToggleSlice(context, sliceData, controller);
99 case SliceData.SliceType.SLIDER:
100 return buildSliderSlice(context, sliceData, controller);
102 throw new IllegalArgumentException(
103 "Slice type passed was invalid: " + sliceData.getSliceType());
108 * @return the {@link SliceData.SliceType} for the {@param controllerClassName} and key.
111 public static int getSliceType(Context context, String controllerClassName,
112 String controllerKey) {
113 BasePreferenceController controller = getPreferenceController(context, controllerClassName,
115 return controller.getSliceType();
119 * Splits the Settings Slice Uri path into its two expected components:
123 * Examples of valid paths are:
125 * - /intent/bluetooth
127 * - /action/accessibility/servicename
129 * @param uri of the Slice. Follows pattern outlined in {@link SettingsSliceProvider}.
130 * @return Pair whose first element {@code true} if the path is prepended with "intent", and
133 public static Pair<Boolean, String> getPathData(Uri uri) {
134 final String path = uri.getPath();
135 final String[] split = path.split("/", 3);
137 // Split should be: [{}, SLICE_TYPE, KEY].
138 // Example: "/action/wifi" -> [{}, "action", "wifi"]
139 // "/action/longer/path" -> [{}, "action", "longer/path"]
140 if (split.length != 3) {
144 final boolean isIntent = TextUtils.equals(SettingsSlicesContract.PATH_SETTING_INTENT,
147 return new Pair<>(isIntent, split[2]);
151 * Looks at the controller classname in in {@link SliceData} from {@param sliceData}
152 * and attempts to build an {@link AbstractPreferenceController}.
154 public static BasePreferenceController getPreferenceController(Context context,
155 SliceData sliceData) {
156 return getPreferenceController(context, sliceData.getPreferenceController(),
161 * @return {@link PendingIntent} for a non-primary {@link SliceAction}.
163 public static PendingIntent getActionIntent(Context context, String action, SliceData data) {
164 final Intent intent = new Intent(action);
165 intent.setClass(context, SliceBroadcastReceiver.class);
166 intent.putExtra(EXTRA_SLICE_KEY, data.getKey());
167 intent.putExtra(EXTRA_SLICE_PLATFORM_DEFINED, data.isPlatformDefined());
168 return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent,
169 PendingIntent.FLAG_CANCEL_CURRENT);
173 * @return {@link PendingIntent} for the primary {@link SliceAction}.
175 public static PendingIntent getContentPendingIntent(Context context, SliceData sliceData) {
176 final Intent intent = getContentIntent(context, sliceData);
177 return PendingIntent.getActivity(context, 0 /* requestCode */, intent, 0 /* flags */);
181 * @return {@link PendingIntent} to the Settings home page.
183 public static PendingIntent getSettingsIntent(Context context) {
184 final Intent intent = new Intent(Settings.ACTION_SETTINGS);
185 return PendingIntent.getActivity(context, 0 /* requestCode */, intent, 0 /* flags */);
189 * @return the summary text for a {@link Slice} built for {@param sliceData}.
191 public static CharSequence getSubtitleText(Context context,
192 AbstractPreferenceController controller, SliceData sliceData) {
193 CharSequence summaryText;
194 if (controller != null) {
195 summaryText = controller.getSummary();
197 if (isValidSummary(context, summaryText)) {
202 summaryText = sliceData.getSummary();
203 if (isValidSummary(context, summaryText)) {
210 public static Uri getUri(String path, boolean isPlatformSlice) {
211 final String authority = isPlatformSlice
212 ? SettingsSlicesContract.AUTHORITY
213 : SettingsSliceProvider.SLICE_AUTHORITY;
214 return new Uri.Builder()
215 .scheme(ContentResolver.SCHEME_CONTENT)
216 .authority(authority)
222 static Intent getContentIntent(Context context, SliceData sliceData) {
223 final Uri contentUri = new Uri.Builder().appendPath(sliceData.getKey()).build();
224 final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
225 sliceData.getFragmentClassName(), sliceData.getKey(),
226 sliceData.getScreenTitle().toString(), 0 /* TODO */);
227 intent.setClassName(context.getPackageName(), SubSettings.class.getName());
228 intent.setData(contentUri);
232 private static Slice buildToggleSlice(Context context, SliceData sliceData,
233 BasePreferenceController controller) {
234 final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
235 final IconCompat icon = IconCompat.createWithResource(context, sliceData.getIconResource());
236 final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
237 final TogglePreferenceController toggleController =
238 (TogglePreferenceController) controller;
239 final SliceAction sliceAction = getToggleAction(context, sliceData,
240 toggleController.isChecked());
241 final List<String> keywords = buildSliceKeywords(sliceData);
243 return new ListBuilder(context, sliceData.getUri(), SLICE_TTL_MILLIS)
244 .addRow(rowBuilder -> rowBuilder
245 .setTitle(sliceData.getTitle())
246 .setSubtitle(subtitleText)
248 new SliceAction(contentIntent, icon, sliceData.getTitle()))
249 .addEndItem(sliceAction))
250 .setKeywords(keywords)
254 private static Slice buildIntentSlice(Context context, SliceData sliceData,
255 BasePreferenceController controller) {
256 final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
257 final IconCompat icon = IconCompat.createWithResource(context, sliceData.getIconResource());
258 final CharSequence subtitleText = getSubtitleText(context, controller, sliceData);
259 final List<String> keywords = buildSliceKeywords(sliceData);
261 return new ListBuilder(context, sliceData.getUri(), SLICE_TTL_MILLIS)
262 .addRow(rowBuilder -> rowBuilder
263 .setTitle(sliceData.getTitle())
264 .setSubtitle(subtitleText)
266 new SliceAction(contentIntent, icon, sliceData.getTitle())))
267 .setKeywords(keywords)
271 private static Slice buildSliderSlice(Context context, SliceData sliceData,
272 BasePreferenceController controller) {
273 final SliderPreferenceController sliderController = (SliderPreferenceController) controller;
274 final PendingIntent actionIntent = getSliderAction(context, sliceData);
275 final PendingIntent contentIntent = getContentPendingIntent(context, sliceData);
276 final IconCompat icon = IconCompat.createWithResource(context, sliceData.getIconResource());
277 final SliceAction primaryAction = new SliceAction(contentIntent, icon,
278 sliceData.getTitle());
279 final List<String> keywords = buildSliceKeywords(sliceData);
281 return new ListBuilder(context, sliceData.getUri(), SLICE_TTL_MILLIS)
282 .addInputRange(builder -> builder
283 .setTitle(sliceData.getTitle())
284 .setMax(sliderController.getMaxSteps())
285 .setValue(sliderController.getSliderPosition())
286 .setInputAction(actionIntent)
287 .setPrimaryAction(primaryAction))
288 .setKeywords(keywords)
292 private static BasePreferenceController getPreferenceController(Context context,
293 String controllerClassName, String controllerKey) {
295 return BasePreferenceController.createInstance(context, controllerClassName);
296 } catch (IllegalStateException e) {
300 return BasePreferenceController.createInstance(context, controllerClassName, controllerKey);
303 private static SliceAction getToggleAction(Context context, SliceData sliceData,
305 PendingIntent actionIntent = getActionIntent(context,
306 SettingsSliceProvider.ACTION_TOGGLE_CHANGED, sliceData);
307 return new SliceAction(actionIntent, null, isChecked);
310 private static PendingIntent getSliderAction(Context context, SliceData sliceData) {
311 return getActionIntent(context, SettingsSliceProvider.ACTION_SLIDER_CHANGED, sliceData);
314 private static boolean isValidSummary(Context context, CharSequence summary) {
315 if (summary == null || TextUtils.isEmpty(summary.toString().trim())) {
319 final CharSequence placeHolder = context.getText(R.string.summary_placeholder);
320 final CharSequence doublePlaceHolder =
321 context.getText(R.string.summary_two_lines_placeholder);
323 return !(TextUtils.equals(summary, placeHolder)
324 || TextUtils.equals(summary, doublePlaceHolder));
327 private static List<String> buildSliceKeywords(SliceData data) {
328 final List<String> keywords = new ArrayList<>();
330 keywords.add(data.getTitle());
332 if (!TextUtils.equals(data.getTitle(), data.getScreenTitle())) {
333 keywords.add(data.getScreenTitle().toString());
336 final String keywordString = data.getKeywords();
337 if (keywordString != null) {
338 final String[] keywordArray = keywordString.split(",");
339 keywords.addAll(Arrays.asList(keywordArray));
345 private static Slice buildUnavailableSlice(Context context, SliceData data,
346 BasePreferenceController controller) {
347 final String title = data.getTitle();
348 final List<String> keywords = buildSliceKeywords(data);
349 final String summary;
350 final SliceAction primaryAction;
351 final IconCompat icon = IconCompat.createWithResource(context, data.getIconResource());
353 switch (controller.getAvailabilityStatus()) {
354 case UNSUPPORTED_ON_DEVICE:
355 summary = context.getString(R.string.unsupported_setting_summary);
356 primaryAction = new SliceAction(getSettingsIntent(context), icon, title);
358 case DISABLED_FOR_USER:
359 summary = context.getString(R.string.disabled_for_user_setting_summary);
360 primaryAction = new SliceAction(getContentPendingIntent(context, data), icon,
363 case DISABLED_DEPENDENT_SETTING:
364 summary = context.getString(R.string.disabled_dependent_setting_summary);
365 primaryAction = new SliceAction(getContentPendingIntent(context, data), icon,
368 case CONDITIONALLY_UNAVAILABLE:
370 summary = context.getString(R.string.unknown_unavailability_setting_summary);
371 primaryAction = new SliceAction(getSettingsIntent(context), icon, title);
374 return new ListBuilder(context, data.getUri(), SLICE_TTL_MILLIS)
375 .addRow(builder -> builder
377 .setSubtitle(summary)
378 .setPrimaryAction(primaryAction))
379 .setKeywords(keywords)