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