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