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