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