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             Arrays.asList(
124                     CustomSliceRegistry.BLUETOOTH_URI,
125                     CustomSliceRegistry.FLASHLIGHT_SLICE_URI,
126                     CustomSliceRegistry.LOCATION_SLICE_URI,
127                     CustomSliceRegistry.MOBILE_DATA_SLICE_URI,
128                     CustomSliceRegistry.WIFI_CALLING_URI,
129                     CustomSliceRegistry.WIFI_SLICE_URI,
130                     CustomSliceRegistry.ZEN_MODE_SLICE_URI
131             );
132 
133     private static final KeyValueListParser KEY_VALUE_LIST_PARSER = new KeyValueListParser(',');
134 
135     @VisibleForTesting
136     SlicesDatabaseAccessor mSlicesDatabaseAccessor;
137 
138     @VisibleForTesting
139     Map<Uri, SliceData> mSliceWeakDataCache;
140 
141     @VisibleForTesting
142     final Map<Uri, SliceBackgroundWorker> mPinnedWorkers = new ArrayMap<>();
143 
144     private Boolean mNightMode;
145     private boolean mFirstSlicePinned;
146     private boolean mFirstSliceBound;
147 
SettingsSliceProvider()148     public SettingsSliceProvider() {
149         super(READ_SEARCH_INDEXABLES);
150         Log.d(TAG, "init");
151     }
152 
153     @Override
onCreateSliceProvider()154     public boolean onCreateSliceProvider() {
155         Log.d(TAG, "onCreateSliceProvider");
156         mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(getContext());
157         mSliceWeakDataCache = new WeakHashMap<>();
158         return true;
159     }
160 
161     @Override
onSlicePinned(Uri sliceUri)162     public void onSlicePinned(Uri sliceUri) {
163         if (!mFirstSlicePinned) {
164             Log.d(TAG, "onSlicePinned: " + sliceUri);
165             mFirstSlicePinned = true;
166         }
167         FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
168                 .action(SettingsEnums.PAGE_UNKNOWN,
169                         SettingsEnums.ACTION_SETTINGS_SLICE_REQUESTED,
170                         SettingsEnums.PAGE_UNKNOWN,
171                         sliceUri.getLastPathSegment(),
172                         0);
173 
174         if (CustomSliceRegistry.isValidUri(sliceUri)) {
175             final Context context = getContext();
176             final CustomSliceable sliceable = FeatureFactory.getFeatureFactory()
177                     .getSlicesFeatureProvider().getSliceableFromUri(context, sliceUri);
178             final IntentFilter filter = sliceable.getIntentFilter();
179             if (filter != null) {
180                 registerIntentToUri(filter, sliceUri);
181             }
182             ThreadUtils.postOnMainThread(() -> startBackgroundWorker(sliceable, sliceUri));
183             return;
184         }
185 
186         if (CustomSliceRegistry.ZEN_MODE_SLICE_URI.equals(sliceUri)) {
187             registerIntentToUri(ZenModeSliceBuilder.INTENT_FILTER, sliceUri);
188             return;
189         } else if (CustomSliceRegistry.BLUETOOTH_URI.equals(sliceUri)) {
190             registerIntentToUri(BluetoothSliceBuilder.INTENT_FILTER, sliceUri);
191             return;
192         }
193 
194         // Start warming the slice, we expect someone will want it soon.
195         loadSliceInBackground(sliceUri);
196     }
197 
198     @Override
onSliceUnpinned(Uri sliceUri)199     public void onSliceUnpinned(Uri sliceUri) {
200         mSliceWeakDataCache.remove(sliceUri);
201         final Context context = getContext();
202         if (!VolumeSliceHelper.unregisterUri(context, sliceUri)) {
203             SliceBroadcastRelay.unregisterReceivers(context, sliceUri);
204         }
205         ThreadUtils.postOnMainThread(() -> stopBackgroundWorker(sliceUri));
206     }
207 
208     @Override
onBindSlice(Uri sliceUri)209     public Slice onBindSlice(Uri sliceUri) {
210         if (!mFirstSliceBound) {
211             Log.d(TAG, "onBindSlice start: " + sliceUri);
212         }
213         final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
214         try {
215             if (!ThreadUtils.isMainThread()) {
216                 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
217                         .permitAll()
218                         .build());
219             }
220             final Set<String> blockedKeys = getBlockedKeys();
221             final String key = sliceUri.getLastPathSegment();
222             if (blockedKeys.contains(key)) {
223                 Log.e(TAG, "Requested blocked slice with Uri: " + sliceUri);
224                 return null;
225             }
226 
227             final boolean nightMode = Utils.isNightMode(getContext());
228             if (mNightMode == null) {
229                 mNightMode = nightMode;
230                 getContext().setTheme(com.android.settingslib.widget.theme.R.style.Theme_SettingsBase);
231             } else if (mNightMode != nightMode) {
232                 Log.d(TAG, "Night mode changed, reload theme");
233                 mNightMode = nightMode;
234                 getContext().getTheme().rebase();
235             }
236 
237             // Checking if some semi-sensitive slices are requested by a guest user. If so, will
238             // return an empty slice.
239             final UserManager userManager = getContext().getSystemService(UserManager.class);
240             if (userManager.isGuestUser() && RestrictedSliceUtils.isGuestRestricted(sliceUri)) {
241                 Log.i(TAG, "Guest user access denied.");
242                 return null;
243             }
244 
245             // Before adding a slice to {@link CustomSliceManager}, please get approval
246             // from the Settings team.
247             if (CustomSliceRegistry.isValidUri(sliceUri)) {
248                 final Context context = getContext();
249                 return FeatureFactory.getFeatureFactory()
250                         .getSlicesFeatureProvider().getSliceableFromUri(context, sliceUri)
251                         .getSlice();
252             }
253 
254             if (CustomSliceRegistry.WIFI_CALLING_URI.equals(sliceUri)) {
255                 return FeatureFactory.getFeatureFactory()
256                         .getSlicesFeatureProvider()
257                         .getNewWifiCallingSliceHelper(getContext())
258                         .createWifiCallingSlice(sliceUri);
259             } else if (CustomSliceRegistry.ZEN_MODE_SLICE_URI.equals(sliceUri)) {
260                 return ZenModeSliceBuilder.getSlice(getContext());
261             } else if (CustomSliceRegistry.BLUETOOTH_URI.equals(sliceUri)) {
262                 return BluetoothSliceBuilder.getSlice(getContext());
263             } else if (CustomSliceRegistry.ENHANCED_4G_SLICE_URI.equals(sliceUri)) {
264                 return FeatureFactory.getFeatureFactory()
265                         .getSlicesFeatureProvider()
266                         .getNewEnhanced4gLteSliceHelper(getContext())
267                         .createEnhanced4gLteSlice(sliceUri);
268             } else if (CustomSliceRegistry.WIFI_CALLING_PREFERENCE_URI.equals(sliceUri)) {
269                 return FeatureFactory.getFeatureFactory()
270                         .getSlicesFeatureProvider()
271                         .getNewWifiCallingSliceHelper(getContext())
272                         .createWifiCallingPreferenceSlice(sliceUri);
273             }
274 
275             final SliceData cachedSliceData = mSliceWeakDataCache.get(sliceUri);
276             if (cachedSliceData == null) {
277                 loadSliceInBackground(sliceUri);
278                 return getSliceStub(sliceUri);
279             }
280             return SliceBuilderUtils.buildSlice(getContext(), cachedSliceData);
281         } finally {
282             StrictMode.setThreadPolicy(oldPolicy);
283             if (!mFirstSliceBound) {
284                 Log.v(TAG, "onBindSlice end");
285                 mFirstSliceBound = true;
286             }
287         }
288     }
289 
290     /**
291      * Get a list of all valid Uris based on the keys indexed in the Slices database.
292      * <p>
293      * This will return a list of {@link Uri uris} depending on {@param uri}, following:
294      * 1. Authority & Full Path -> Only {@param uri}. It is only a prefix for itself.
295      * 2. Authority & No path -> A list of authority/action/$KEY$, where
296      * {@code $KEY$} is a list of all Slice-enabled keys for the authority.
297      * 3. Authority & action path -> A list of authority/action/$KEY$, where
298      * {@code $KEY$} is a list of all Slice-enabled keys for the authority.
299      * 4. Empty authority & path -> A list of Uris with all keys for both supported authorities.
300      * 5. Else -> Empty list.
301      * <p>
302      * Note that the authority will stay consistent with {@param uri}, and the list of valid Slice
303      * keys depends on if the authority is {@link SettingsSlicesContract#AUTHORITY} or
304      * {@link #SLICE_AUTHORITY}.
305      *
306      * @param uri The uri to look for descendants under.
307      * @returns all valid Settings uris for which {@param uri} is a prefix.
308      */
309     @Override
onGetSliceDescendants(Uri uri)310     public Collection<Uri> onGetSliceDescendants(Uri uri) {
311         final List<Uri> descendants = new ArrayList<>();
312         Uri finalUri = uri;
313 
314         if (isPrivateSlicesNeeded(finalUri)) {
315             descendants.addAll(
316                     mSlicesDatabaseAccessor.getSliceUris(finalUri.getAuthority(),
317                             false /* isPublicSlice */));
318             Log.d(TAG, "provide " + descendants.size() + " non-public slices");
319             finalUri = new Uri.Builder()
320                     .scheme(ContentResolver.SCHEME_CONTENT)
321                     .authority(finalUri.getAuthority())
322                     .build();
323         }
324 
325         final Pair<Boolean, String> pathData = SliceBuilderUtils.getPathData(finalUri);
326 
327         if (pathData != null) {
328             // Uri has a full path and will not have any descendants.
329             descendants.add(finalUri);
330             return descendants;
331         }
332 
333         final String authority = finalUri.getAuthority();
334         final String path = finalUri.getPath();
335         final boolean isPathEmpty = path.isEmpty();
336 
337         // Path is anything but empty, "action", or "intent". Return empty list.
338         if (!isPathEmpty
339                 && !TextUtils.equals(path, "/" + SettingsSlicesContract.PATH_SETTING_ACTION)
340                 && !TextUtils.equals(path, "/" + SettingsSlicesContract.PATH_SETTING_INTENT)) {
341             // Invalid path prefix, there are no valid Uri descendants.
342             return descendants;
343         }
344 
345         // Add all descendants from db with matching authority.
346         descendants.addAll(mSlicesDatabaseAccessor.getSliceUris(authority, true /*isPublicSlice*/));
347 
348         if (isPathEmpty && TextUtils.isEmpty(authority)) {
349             // No path nor authority. Return all possible Uris by adding all special slice uri
350             descendants.addAll(PUBLICLY_SUPPORTED_CUSTOM_SLICE_URIS);
351         } else {
352             // Can assume authority belongs to the provider. Return all Uris for the authority.
353             final List<Uri> customSlices = PUBLICLY_SUPPORTED_CUSTOM_SLICE_URIS.stream()
354                     .filter(sliceUri -> TextUtils.equals(authority, sliceUri.getAuthority()))
355                     .collect(Collectors.toList());
356             descendants.addAll(customSlices);
357         }
358         grantAllowlistedPackagePermissions(getContext(), descendants);
359         return descendants;
360     }
361 
362     @Nullable
363     @Override
onCreatePermissionRequest(@onNull Uri sliceUri, @NonNull String callingPackage)364     public PendingIntent onCreatePermissionRequest(@NonNull Uri sliceUri,
365             @NonNull String callingPackage) {
366         final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS)
367                 .setPackage(Utils.SETTINGS_PACKAGE_NAME);
368         final PendingIntent noOpIntent = PendingIntent.getActivity(getContext(),
369                 0 /* requestCode */, settingsIntent, PendingIntent.FLAG_IMMUTABLE);
370         return noOpIntent;
371     }
372 
373     @VisibleForTesting
grantAllowlistedPackagePermissions(Context context, List<Uri> descendants)374     static void grantAllowlistedPackagePermissions(Context context, List<Uri> descendants) {
375         if (descendants == null) {
376             Log.d(TAG, "No descendants to grant permission with, skipping.");
377         }
378         final String[] allowlistPackages =
379                 context.getResources().getStringArray(R.array.slice_allowlist_package_names);
380         if (allowlistPackages == null || allowlistPackages.length == 0) {
381             Log.d(TAG, "No packages to allowlist, skipping.");
382             return;
383         } else {
384             Log.d(TAG, String.format(
385                     "Allowlisting %d uris to %d pkgs.",
386                     descendants.size(), allowlistPackages.length));
387         }
388         final SliceManager sliceManager = context.getSystemService(SliceManager.class);
389         for (Uri descendant : descendants) {
390             for (String toPackage : allowlistPackages) {
391                 sliceManager.grantSlicePermission(toPackage, descendant);
392             }
393         }
394     }
395 
396     @Override
shutdown()397     public void shutdown() {
398         ThreadUtils.postOnMainThread(() -> {
399             SliceBackgroundWorker.shutdown();
400         });
401     }
402 
403     @VisibleForTesting
loadSlice(Uri uri)404     void loadSlice(Uri uri) {
405         if (mSliceWeakDataCache.containsKey(uri)) {
406             Log.d(TAG, uri + " loaded from cache");
407             return;
408         }
409         long startBuildTime = System.currentTimeMillis();
410 
411         final SliceData sliceData;
412         try {
413             sliceData = mSlicesDatabaseAccessor.getSliceDataFromUri(uri);
414         } catch (IllegalStateException e) {
415             Log.d(TAG, "Could not create slicedata for uri: " + uri, e);
416             return;
417         }
418 
419         final BasePreferenceController controller = SliceBuilderUtils.getPreferenceController(
420                 getContext(), sliceData);
421 
422         final IntentFilter filter = controller.getIntentFilter();
423         if (filter != null) {
424             if (controller instanceof VolumeSeekBarPreferenceController) {
425                 // Register volume slices to a broadcast relay to reduce unnecessary UI updates
426                 VolumeSliceHelper.registerIntentToUri(getContext(), filter, uri,
427                         ((VolumeSeekBarPreferenceController) controller).getAudioStream());
428             } else {
429                 registerIntentToUri(filter, uri);
430             }
431         }
432 
433         ThreadUtils.postOnMainThread(() -> startBackgroundWorker(controller, uri));
434 
435         mSliceWeakDataCache.put(uri, sliceData);
436         getContext().getContentResolver().notifyChange(uri, null /* content observer */);
437 
438         Log.d(TAG, "Built slice (" + uri + ") in: " +
439                 (System.currentTimeMillis() - startBuildTime));
440     }
441 
442     @VisibleForTesting
loadSliceInBackground(Uri uri)443     void loadSliceInBackground(Uri uri) {
444         ThreadUtils.postOnBackgroundThread(() -> loadSlice(uri));
445     }
446 
447     @VisibleForTesting
448     /**
449      * Registers an IntentFilter in SysUI to notify changes to {@param sliceUri} when broadcasts to
450      * {@param intentFilter} happen.
451      */
registerIntentToUri(IntentFilter intentFilter, Uri sliceUri)452     void registerIntentToUri(IntentFilter intentFilter, Uri sliceUri) {
453         SliceBroadcastRelay.registerReceiver(getContext(), sliceUri, SliceRelayReceiver.class,
454                 intentFilter);
455     }
456 
457     @VisibleForTesting
getBlockedKeys()458     Set<String> getBlockedKeys() {
459         final String value = Settings.Global.getString(getContext().getContentResolver(),
460                 Settings.Global.BLOCKED_SLICES);
461         final Set<String> set = new ArraySet<>();
462 
463         try {
464             KEY_VALUE_LIST_PARSER.setString(value);
465         } catch (IllegalArgumentException e) {
466             Log.e(TAG, "Bad Settings Slices Allowlist flags", e);
467             return set;
468         }
469 
470         final String[] parsedValues = parseStringArray(value);
471         Collections.addAll(set, parsedValues);
472         return set;
473     }
474 
475     @VisibleForTesting
isPrivateSlicesNeeded(Uri uri)476     boolean isPrivateSlicesNeeded(Uri uri) {
477         final Context context = getContext();
478         final String queryUri = context.getString(R.string.config_non_public_slice_query_uri);
479 
480         if (!TextUtils.isEmpty(queryUri) && TextUtils.equals(uri.toString(), queryUri)) {
481             // check if the calling package is eligible for private slices
482             final int callingUid = Binder.getCallingUid();
483             final boolean hasPermission =
484                     context.checkPermission(
485                                     android.Manifest.permission.READ_SEARCH_INDEXABLES,
486                                     Binder.getCallingPid(),
487                                     callingUid)
488                             == PackageManager.PERMISSION_GRANTED;
489             final String[] packages = context.getPackageManager().getPackagesForUid(callingUid);
490             final String callingPackage =
491                     packages != null && packages.length > 0 ? packages[0] : null;
492             return hasPermission
493                     && TextUtils.equals(
494                             callingPackage,
495                             context.getString(R.string.config_settingsintelligence_package_name));
496         }
497         return false;
498     }
499 
startBackgroundWorker(Sliceable sliceable, Uri uri)500     private void startBackgroundWorker(Sliceable sliceable, Uri uri) {
501         final Class workerClass = sliceable.getBackgroundWorkerClass();
502         if (workerClass == null) {
503             return;
504         }
505 
506         if (mPinnedWorkers.containsKey(uri)) {
507             return;
508         }
509 
510         Log.d(TAG, "Starting background worker for: " + uri);
511         final SliceBackgroundWorker worker = SliceBackgroundWorker.getInstance(
512                 getContext(), sliceable, uri);
513         mPinnedWorkers.put(uri, worker);
514         worker.pin();
515     }
516 
stopBackgroundWorker(Uri uri)517     private void stopBackgroundWorker(Uri uri) {
518         final SliceBackgroundWorker worker = mPinnedWorkers.get(uri);
519         if (worker != null) {
520             Log.d(TAG, "Stopping background worker for: " + uri);
521             worker.unpin();
522             mPinnedWorkers.remove(uri);
523         }
524     }
525 
526     /**
527      * @return an empty {@link Slice} with {@param uri} to be used as a stub while the real
528      * {@link SliceData} is loaded from {@link SlicesDatabaseHelper.Tables#TABLE_SLICES_INDEX}.
529      */
getSliceStub(Uri uri)530     private static Slice getSliceStub(Uri uri) {
531         // TODO: Switch back to ListBuilder when slice loading states are fixed.
532         return new Slice.Builder(uri).addHints(HINT_PARTIAL).build();
533     }
534 
parseStringArray(String value)535     private static String[] parseStringArray(String value) {
536         if (value != null) {
537             String[] parts = value.split(":");
538             if (parts.length > 0) {
539                 return parts;
540             }
541         }
542         return new String[0];
543     }
544 }
545