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