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