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