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