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.Manifest.permission.READ_SEARCH_INDEXABLES; 20 import static android.app.slice.Slice.HINT_PARTIAL; 21 22 import android.app.PendingIntent; 23 import android.app.settings.SettingsEnums; 24 import android.app.slice.SliceManager; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.content.pm.PackageManager; 30 import android.net.Uri; 31 import android.os.Binder; 32 import android.os.StrictMode; 33 import android.os.UserManager; 34 import android.provider.Settings; 35 import android.provider.SettingsSlicesContract; 36 import android.text.TextUtils; 37 import android.util.ArrayMap; 38 import android.util.KeyValueListParser; 39 import android.util.Log; 40 import android.util.Pair; 41 42 import androidx.annotation.NonNull; 43 import androidx.annotation.Nullable; 44 import androidx.annotation.VisibleForTesting; 45 import androidx.collection.ArraySet; 46 import androidx.slice.Slice; 47 import androidx.slice.SliceProvider; 48 49 import com.android.settings.R; 50 import com.android.settings.Utils; 51 import com.android.settings.bluetooth.BluetoothSliceBuilder; 52 import com.android.settings.core.BasePreferenceController; 53 import com.android.settings.notification.VolumeSeekBarPreferenceController; 54 import com.android.settings.notification.zen.ZenModeSliceBuilder; 55 import com.android.settings.overlay.FeatureFactory; 56 import com.android.settingslib.SliceBroadcastRelay; 57 import com.android.settingslib.utils.ThreadUtils; 58 59 import java.util.ArrayList; 60 import java.util.Arrays; 61 import java.util.Collection; 62 import java.util.Collections; 63 import java.util.List; 64 import java.util.Map; 65 import java.util.Set; 66 import java.util.WeakHashMap; 67 import java.util.stream.Collectors; 68 69 /** 70 * A {@link SliceProvider} for Settings to enabled inline results in system apps. 71 * 72 * <p>{@link SettingsSliceProvider} accepts a {@link Uri} with {@link #SLICE_AUTHORITY} and a 73 * {@code String} key based on the setting intended to be changed. This provider builds a 74 * {@link Slice} and responds to Slice actions through the database defined by 75 * {@link SlicesDatabaseHelper}, whose data is written by {@link SlicesIndexer}. 76 * 77 * <p>When a {@link Slice} is requested, we start loading {@link SliceData} in the background and 78 * return an stub {@link Slice} with the correct {@link Uri} immediately. In the background, the 79 * data corresponding to the key in the {@link Uri} is read by {@link SlicesDatabaseAccessor}, and 80 * the entire row is converted into a {@link SliceData}. Once complete, it is stored in 81 * {@link #mSliceWeakDataCache}, and then an update sent via the Slice framework to the Slice. 82 * The {@link Slice} displayed by the Slice-presenter will re-query this Slice-provider and find 83 * the {@link SliceData} cached to build the full {@link Slice}. 84 * 85 * <p>When an action is taken on that {@link Slice}, we receive the action in 86 * {@link SliceBroadcastReceiver}, and use the 87 * {@link com.android.settings.core.BasePreferenceController} indexed as 88 * {@link SlicesDatabaseHelper.IndexColumns#CONTROLLER} to manipulate the setting. 89 */ 90 public class SettingsSliceProvider extends SliceProvider { 91 92 private static final String TAG = "SettingsSliceProvider"; 93 94 /** 95 * Authority for Settings slices not officially supported by the platform, but extensible for 96 * OEMs. 97 */ 98 public static final String SLICE_AUTHORITY = "com.android.settings.slices"; 99 100 /** 101 * Action passed for changes to Toggle Slices. 102 */ 103 public static final String ACTION_TOGGLE_CHANGED = 104 "com.android.settings.slice.action.TOGGLE_CHANGED"; 105 106 /** 107 * Action passed for changes to Slider Slices. 108 */ 109 public static final String ACTION_SLIDER_CHANGED = 110 "com.android.settings.slice.action.SLIDER_CHANGED"; 111 112 /** 113 * Intent Extra passed for the key identifying the Setting Slice. 114 */ 115 public static final String EXTRA_SLICE_KEY = "com.android.settings.slice.extra.key"; 116 117 /** 118 * A list of custom slice uris that are supported publicly. This is a subset of slices defined 119 * in {@link CustomSliceRegistry}. Things here are exposed publicly so all clients with proper 120 * permission can use them. 121 */ 122 private static final List<Uri> PUBLICLY_SUPPORTED_CUSTOM_SLICE_URIS = 123 android.app.Flags.modesUi() 124 ? 125 Arrays.asList( 126 CustomSliceRegistry.BLUETOOTH_URI, 127 CustomSliceRegistry.FLASHLIGHT_SLICE_URI, 128 CustomSliceRegistry.LOCATION_SLICE_URI, 129 CustomSliceRegistry.MOBILE_DATA_SLICE_URI, 130 CustomSliceRegistry.WIFI_CALLING_URI, 131 CustomSliceRegistry.WIFI_SLICE_URI 132 ) : 133 Arrays.asList( 134 CustomSliceRegistry.BLUETOOTH_URI, 135 CustomSliceRegistry.FLASHLIGHT_SLICE_URI, 136 CustomSliceRegistry.LOCATION_SLICE_URI, 137 CustomSliceRegistry.MOBILE_DATA_SLICE_URI, 138 CustomSliceRegistry.WIFI_CALLING_URI, 139 CustomSliceRegistry.WIFI_SLICE_URI, 140 CustomSliceRegistry.ZEN_MODE_SLICE_URI 141 ); 142 143 private static final KeyValueListParser KEY_VALUE_LIST_PARSER = new KeyValueListParser(','); 144 145 @VisibleForTesting 146 SlicesDatabaseAccessor mSlicesDatabaseAccessor; 147 148 @VisibleForTesting 149 Map<Uri, SliceData> mSliceWeakDataCache; 150 151 @VisibleForTesting 152 final Map<Uri, SliceBackgroundWorker> mPinnedWorkers = new ArrayMap<>(); 153 154 private Boolean mNightMode; 155 private boolean mFirstSlicePinned; 156 private boolean mFirstSliceBound; 157 SettingsSliceProvider()158 public SettingsSliceProvider() { 159 super(READ_SEARCH_INDEXABLES); 160 Log.d(TAG, "init"); 161 } 162 163 @Override onCreateSliceProvider()164 public boolean onCreateSliceProvider() { 165 Log.d(TAG, "onCreateSliceProvider"); 166 mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(getContext()); 167 mSliceWeakDataCache = new WeakHashMap<>(); 168 return true; 169 } 170 171 @Override onSlicePinned(Uri sliceUri)172 public void onSlicePinned(Uri sliceUri) { 173 if (!mFirstSlicePinned) { 174 Log.d(TAG, "onSlicePinned: " + sliceUri); 175 mFirstSlicePinned = true; 176 } 177 FeatureFactory.getFeatureFactory().getMetricsFeatureProvider() 178 .action(SettingsEnums.PAGE_UNKNOWN, 179 SettingsEnums.ACTION_SETTINGS_SLICE_REQUESTED, 180 SettingsEnums.PAGE_UNKNOWN, 181 sliceUri.getLastPathSegment(), 182 0); 183 184 if (CustomSliceRegistry.isValidUri(sliceUri)) { 185 final Context context = getContext(); 186 final CustomSliceable sliceable = FeatureFactory.getFeatureFactory() 187 .getSlicesFeatureProvider().getSliceableFromUri(context, sliceUri); 188 final IntentFilter filter = sliceable.getIntentFilter(); 189 if (filter != null) { 190 registerIntentToUri(filter, sliceUri); 191 } 192 ThreadUtils.postOnMainThread(() -> startBackgroundWorker(sliceable, sliceUri)); 193 return; 194 } 195 196 if (CustomSliceRegistry.ZEN_MODE_SLICE_URI.equals(sliceUri)) { 197 if (!android.app.Flags.modesUi()) { 198 registerIntentToUri(ZenModeSliceBuilder.INTENT_FILTER, sliceUri); 199 } 200 return; 201 } else if (CustomSliceRegistry.BLUETOOTH_URI.equals(sliceUri)) { 202 registerIntentToUri(BluetoothSliceBuilder.INTENT_FILTER, sliceUri); 203 return; 204 } 205 206 // Start warming the slice, we expect someone will want it soon. 207 loadSliceInBackground(sliceUri); 208 } 209 210 @Override onSliceUnpinned(Uri sliceUri)211 public void onSliceUnpinned(Uri sliceUri) { 212 mSliceWeakDataCache.remove(sliceUri); 213 final Context context = getContext(); 214 if (!VolumeSliceHelper.unregisterUri(context, sliceUri)) { 215 SliceBroadcastRelay.unregisterReceivers(context, sliceUri); 216 } 217 ThreadUtils.postOnMainThread(() -> stopBackgroundWorker(sliceUri)); 218 } 219 220 @Override onBindSlice(Uri sliceUri)221 public Slice onBindSlice(Uri sliceUri) { 222 if (!mFirstSliceBound) { 223 Log.d(TAG, "onBindSlice start: " + sliceUri); 224 } 225 final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); 226 try { 227 if (!ThreadUtils.isMainThread()) { 228 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() 229 .permitAll() 230 .build()); 231 } 232 final Set<String> blockedKeys = getBlockedKeys(); 233 final String key = sliceUri.getLastPathSegment(); 234 if (blockedKeys.contains(key)) { 235 Log.e(TAG, "Requested blocked slice with Uri: " + sliceUri); 236 return null; 237 } 238 239 final boolean nightMode = Utils.isNightMode(getContext()); 240 if (mNightMode == null) { 241 mNightMode = nightMode; 242 getContext().setTheme(com.android.settingslib.widget.theme.R.style.Theme_SettingsBase); 243 } else if (mNightMode != nightMode) { 244 Log.d(TAG, "Night mode changed, reload theme"); 245 mNightMode = nightMode; 246 getContext().getTheme().rebase(); 247 } 248 249 // Checking if some semi-sensitive slices are requested by a guest user. If so, will 250 // return an empty slice. 251 final UserManager userManager = getContext().getSystemService(UserManager.class); 252 if (userManager.isGuestUser() && RestrictedSliceUtils.isGuestRestricted(sliceUri)) { 253 Log.i(TAG, "Guest user access denied."); 254 return null; 255 } 256 257 // Before adding a slice to {@link CustomSliceManager}, please get approval 258 // from the Settings team. 259 if (CustomSliceRegistry.isValidUri(sliceUri)) { 260 final Context context = getContext(); 261 return FeatureFactory.getFeatureFactory() 262 .getSlicesFeatureProvider().getSliceableFromUri(context, sliceUri) 263 .getSlice(); 264 } 265 266 if (CustomSliceRegistry.WIFI_CALLING_URI.equals(sliceUri)) { 267 return FeatureFactory.getFeatureFactory() 268 .getSlicesFeatureProvider() 269 .getNewWifiCallingSliceHelper(getContext()) 270 .createWifiCallingSlice(sliceUri); 271 } else if (!android.app.Flags.modesUi() 272 && CustomSliceRegistry.ZEN_MODE_SLICE_URI.equals(sliceUri)) { 273 return ZenModeSliceBuilder.getSlice(getContext()); 274 } else if (CustomSliceRegistry.BLUETOOTH_URI.equals(sliceUri)) { 275 return BluetoothSliceBuilder.getSlice(getContext()); 276 } else if (CustomSliceRegistry.ENHANCED_4G_SLICE_URI.equals(sliceUri)) { 277 return FeatureFactory.getFeatureFactory() 278 .getSlicesFeatureProvider() 279 .getNewEnhanced4gLteSliceHelper(getContext()) 280 .createEnhanced4gLteSlice(sliceUri); 281 } else if (CustomSliceRegistry.WIFI_CALLING_PREFERENCE_URI.equals(sliceUri)) { 282 return FeatureFactory.getFeatureFactory() 283 .getSlicesFeatureProvider() 284 .getNewWifiCallingSliceHelper(getContext()) 285 .createWifiCallingPreferenceSlice(sliceUri); 286 } 287 288 final SliceData cachedSliceData = mSliceWeakDataCache.get(sliceUri); 289 if (cachedSliceData == null) { 290 loadSliceInBackground(sliceUri); 291 return getSliceStub(sliceUri); 292 } 293 return SliceBuilderUtils.buildSlice(getContext(), cachedSliceData); 294 } finally { 295 StrictMode.setThreadPolicy(oldPolicy); 296 if (!mFirstSliceBound) { 297 Log.v(TAG, "onBindSlice end"); 298 mFirstSliceBound = true; 299 } 300 } 301 } 302 303 /** 304 * Get a list of all valid Uris based on the keys indexed in the Slices database. 305 * <p> 306 * This will return a list of {@link Uri uris} depending on {@param uri}, following: 307 * 1. Authority & Full Path -> Only {@param uri}. It is only a prefix for itself. 308 * 2. Authority & No path -> A list of authority/action/$KEY$, where 309 * {@code $KEY$} is a list of all Slice-enabled keys for the authority. 310 * 3. Authority & action path -> A list of authority/action/$KEY$, where 311 * {@code $KEY$} is a list of all Slice-enabled keys for the authority. 312 * 4. Empty authority & path -> A list of Uris with all keys for both supported authorities. 313 * 5. Else -> Empty list. 314 * <p> 315 * Note that the authority will stay consistent with {@param uri}, and the list of valid Slice 316 * keys depends on if the authority is {@link SettingsSlicesContract#AUTHORITY} or 317 * {@link #SLICE_AUTHORITY}. 318 * 319 * @param uri The uri to look for descendants under. 320 * @returns all valid Settings uris for which {@param uri} is a prefix. 321 */ 322 @Override onGetSliceDescendants(Uri uri)323 public Collection<Uri> onGetSliceDescendants(Uri uri) { 324 final List<Uri> descendants = new ArrayList<>(); 325 Uri finalUri = uri; 326 327 if (isPrivateSlicesNeeded(finalUri)) { 328 descendants.addAll( 329 mSlicesDatabaseAccessor.getSliceUris(finalUri.getAuthority(), 330 false /* isPublicSlice */)); 331 Log.d(TAG, "provide " + descendants.size() + " non-public slices"); 332 finalUri = new Uri.Builder() 333 .scheme(ContentResolver.SCHEME_CONTENT) 334 .authority(finalUri.getAuthority()) 335 .build(); 336 } 337 338 final Pair<Boolean, String> pathData = SliceBuilderUtils.getPathData(finalUri); 339 340 if (pathData != null) { 341 // Uri has a full path and will not have any descendants. 342 descendants.add(finalUri); 343 return descendants; 344 } 345 346 final String authority = finalUri.getAuthority(); 347 final String path = finalUri.getPath(); 348 final boolean isPathEmpty = path.isEmpty(); 349 350 // Path is anything but empty, "action", or "intent". Return empty list. 351 if (!isPathEmpty 352 && !TextUtils.equals(path, "/" + SettingsSlicesContract.PATH_SETTING_ACTION) 353 && !TextUtils.equals(path, "/" + SettingsSlicesContract.PATH_SETTING_INTENT)) { 354 // Invalid path prefix, there are no valid Uri descendants. 355 return descendants; 356 } 357 358 // Add all descendants from db with matching authority. 359 descendants.addAll(mSlicesDatabaseAccessor.getSliceUris(authority, true /*isPublicSlice*/)); 360 361 if (isPathEmpty && TextUtils.isEmpty(authority)) { 362 // No path nor authority. Return all possible Uris by adding all special slice uri 363 descendants.addAll(PUBLICLY_SUPPORTED_CUSTOM_SLICE_URIS); 364 } else { 365 // Can assume authority belongs to the provider. Return all Uris for the authority. 366 final List<Uri> customSlices = PUBLICLY_SUPPORTED_CUSTOM_SLICE_URIS.stream() 367 .filter(sliceUri -> TextUtils.equals(authority, sliceUri.getAuthority())) 368 .collect(Collectors.toList()); 369 descendants.addAll(customSlices); 370 } 371 grantAllowlistedPackagePermissions(getContext(), descendants); 372 return descendants; 373 } 374 375 @Nullable 376 @Override onCreatePermissionRequest(@onNull Uri sliceUri, @NonNull String callingPackage)377 public PendingIntent onCreatePermissionRequest(@NonNull Uri sliceUri, 378 @NonNull String callingPackage) { 379 final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS) 380 .setPackage(Utils.SETTINGS_PACKAGE_NAME); 381 final PendingIntent noOpIntent = PendingIntent.getActivity(getContext(), 382 0 /* requestCode */, settingsIntent, PendingIntent.FLAG_IMMUTABLE); 383 return noOpIntent; 384 } 385 386 @VisibleForTesting grantAllowlistedPackagePermissions(Context context, List<Uri> descendants)387 static void grantAllowlistedPackagePermissions(Context context, List<Uri> descendants) { 388 if (descendants == null) { 389 Log.d(TAG, "No descendants to grant permission with, skipping."); 390 } 391 final String[] allowlistPackages = 392 context.getResources().getStringArray(R.array.slice_allowlist_package_names); 393 if (allowlistPackages == null || allowlistPackages.length == 0) { 394 Log.d(TAG, "No packages to allowlist, skipping."); 395 return; 396 } else { 397 Log.d(TAG, String.format( 398 "Allowlisting %d uris to %d pkgs.", 399 descendants.size(), allowlistPackages.length)); 400 } 401 final SliceManager sliceManager = context.getSystemService(SliceManager.class); 402 for (Uri descendant : descendants) { 403 for (String toPackage : allowlistPackages) { 404 sliceManager.grantSlicePermission(toPackage, descendant); 405 } 406 } 407 } 408 409 @Override shutdown()410 public void shutdown() { 411 ThreadUtils.postOnMainThread(() -> { 412 SliceBackgroundWorker.shutdown(); 413 }); 414 } 415 416 @VisibleForTesting loadSlice(Uri uri)417 void loadSlice(Uri uri) { 418 if (mSliceWeakDataCache.containsKey(uri)) { 419 Log.d(TAG, uri + " loaded from cache"); 420 return; 421 } 422 long startBuildTime = System.currentTimeMillis(); 423 424 final SliceData sliceData; 425 try { 426 sliceData = mSlicesDatabaseAccessor.getSliceDataFromUri(uri); 427 } catch (IllegalStateException e) { 428 Log.d(TAG, "Could not create slicedata for uri: " + uri, e); 429 return; 430 } 431 432 final BasePreferenceController controller = SliceBuilderUtils.getPreferenceController( 433 getContext(), sliceData); 434 435 final IntentFilter filter = controller.getIntentFilter(); 436 if (filter != null) { 437 if (controller instanceof VolumeSeekBarPreferenceController) { 438 // Register volume slices to a broadcast relay to reduce unnecessary UI updates 439 VolumeSliceHelper.registerIntentToUri(getContext(), filter, uri, 440 ((VolumeSeekBarPreferenceController) controller).getAudioStream()); 441 } else { 442 registerIntentToUri(filter, uri); 443 } 444 } 445 446 ThreadUtils.postOnMainThread(() -> startBackgroundWorker(controller, uri)); 447 448 mSliceWeakDataCache.put(uri, sliceData); 449 getContext().getContentResolver().notifyChange(uri, null /* content observer */); 450 451 Log.d(TAG, "Built slice (" + uri + ") in: " + 452 (System.currentTimeMillis() - startBuildTime)); 453 } 454 455 @VisibleForTesting loadSliceInBackground(Uri uri)456 void loadSliceInBackground(Uri uri) { 457 ThreadUtils.postOnBackgroundThread(() -> loadSlice(uri)); 458 } 459 460 @VisibleForTesting 461 /** 462 * Registers an IntentFilter in SysUI to notify changes to {@param sliceUri} when broadcasts to 463 * {@param intentFilter} happen. 464 */ registerIntentToUri(IntentFilter intentFilter, Uri sliceUri)465 void registerIntentToUri(IntentFilter intentFilter, Uri sliceUri) { 466 SliceBroadcastRelay.registerReceiver(getContext(), sliceUri, SliceRelayReceiver.class, 467 intentFilter); 468 } 469 470 @VisibleForTesting getBlockedKeys()471 Set<String> getBlockedKeys() { 472 final String value = Settings.Global.getString(getContext().getContentResolver(), 473 Settings.Global.BLOCKED_SLICES); 474 final Set<String> set = new ArraySet<>(); 475 476 try { 477 KEY_VALUE_LIST_PARSER.setString(value); 478 } catch (IllegalArgumentException e) { 479 Log.e(TAG, "Bad Settings Slices Allowlist flags", e); 480 return set; 481 } 482 483 final String[] parsedValues = parseStringArray(value); 484 Collections.addAll(set, parsedValues); 485 return set; 486 } 487 488 @VisibleForTesting isPrivateSlicesNeeded(Uri uri)489 boolean isPrivateSlicesNeeded(Uri uri) { 490 final Context context = getContext(); 491 final String queryUri = context.getString(R.string.config_non_public_slice_query_uri); 492 493 if (!TextUtils.isEmpty(queryUri) && TextUtils.equals(uri.toString(), queryUri)) { 494 // check if the calling package is eligible for private slices 495 final int callingUid = Binder.getCallingUid(); 496 final boolean hasPermission = 497 context.checkPermission( 498 android.Manifest.permission.READ_SEARCH_INDEXABLES, 499 Binder.getCallingPid(), 500 callingUid) 501 == PackageManager.PERMISSION_GRANTED; 502 final String[] packages = context.getPackageManager().getPackagesForUid(callingUid); 503 final String callingPackage = 504 packages != null && packages.length > 0 ? packages[0] : null; 505 return hasPermission 506 && TextUtils.equals( 507 callingPackage, 508 context.getString(R.string.config_settingsintelligence_package_name)); 509 } 510 return false; 511 } 512 startBackgroundWorker(Sliceable sliceable, Uri uri)513 private void startBackgroundWorker(Sliceable sliceable, Uri uri) { 514 final Class workerClass = sliceable.getBackgroundWorkerClass(); 515 if (workerClass == null) { 516 return; 517 } 518 519 if (mPinnedWorkers.containsKey(uri)) { 520 return; 521 } 522 523 Log.d(TAG, "Starting background worker for: " + uri); 524 final SliceBackgroundWorker worker = SliceBackgroundWorker.getInstance( 525 getContext(), sliceable, uri); 526 mPinnedWorkers.put(uri, worker); 527 worker.pin(); 528 } 529 stopBackgroundWorker(Uri uri)530 private void stopBackgroundWorker(Uri uri) { 531 final SliceBackgroundWorker worker = mPinnedWorkers.get(uri); 532 if (worker != null) { 533 Log.d(TAG, "Stopping background worker for: " + uri); 534 worker.unpin(); 535 mPinnedWorkers.remove(uri); 536 } 537 } 538 539 /** 540 * @return an empty {@link Slice} with {@param uri} to be used as a stub while the real 541 * {@link SliceData} is loaded from {@link SlicesDatabaseHelper.Tables#TABLE_SLICES_INDEX}. 542 */ getSliceStub(Uri uri)543 private static Slice getSliceStub(Uri uri) { 544 // TODO: Switch back to ListBuilder when slice loading states are fixed. 545 return new Slice.Builder(uri).addHints(HINT_PARTIAL).build(); 546 } 547 parseStringArray(String value)548 private static String[] parseStringArray(String value) { 549 if (value != null) { 550 String[] parts = value.split(":"); 551 if (parts.length > 0) { 552 return parts; 553 } 554 } 555 return new String[0]; 556 } 557 } 558