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