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