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