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