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