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 com.android.settings.core.PreferenceXmlParserUtils.METADATA_CONTROLLER;
     20 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_ICON;
     21 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_KEY;
     22 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_PLATFORM_SLICE_FLAG;
     23 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_SUMMARY;
     24 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_TITLE;
     25 
     26 import android.accessibilityservice.AccessibilityServiceInfo;
     27 import android.content.ComponentName;
     28 import android.content.Context;
     29 import android.content.pm.PackageManager;
     30 import android.content.pm.ResolveInfo;
     31 import android.content.pm.ServiceInfo;
     32 import android.content.res.Resources;
     33 import android.content.res.XmlResourceParser;
     34 import android.os.Bundle;
     35 import android.provider.SearchIndexableResource;
     36 import android.text.TextUtils;
     37 import android.util.AttributeSet;
     38 import android.util.Log;
     39 import android.util.Xml;
     40 import android.view.accessibility.AccessibilityManager;
     41 
     42 import com.android.internal.annotations.VisibleForTesting;
     43 import com.android.settings.R;
     44 import com.android.settings.accessibility.AccessibilitySettings;
     45 import com.android.settings.accessibility.AccessibilitySlicePreferenceController;
     46 import com.android.settings.core.BasePreferenceController;
     47 import com.android.settings.core.PreferenceXmlParserUtils;
     48 import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag;
     49 import com.android.settings.dashboard.DashboardFragment;
     50 import com.android.settings.overlay.FeatureFactory;
     51 import com.android.settings.search.DatabaseIndexingUtils;
     52 import com.android.settings.search.Indexable.SearchIndexProvider;
     53 
     54 import org.xmlpull.v1.XmlPullParser;
     55 import org.xmlpull.v1.XmlPullParserException;
     56 
     57 import java.io.IOException;
     58 import java.util.ArrayList;
     59 import java.util.Collection;
     60 import java.util.Collections;
     61 import java.util.HashSet;
     62 import java.util.List;
     63 import java.util.Set;
     64 
     65 /**
     66  * Converts all Slice sources into {@link SliceData}.
     67  * This includes:
     68  * - All {@link DashboardFragment DashboardFragments} indexed by settings search
     69  * - Accessibility services
     70  */
     71 class SliceDataConverter {
     72 
     73     private static final String TAG = "SliceDataConverter";
     74 
     75     private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen";
     76 
     77     private Context mContext;
     78 
     79     private List<SliceData> mSliceData;
     80 
     81     public SliceDataConverter(Context context) {
     82         mContext = context;
     83         mSliceData = new ArrayList<>();
     84     }
     85 
     86     /**
     87      * @return a list of {@link SliceData} to be indexed and later referenced as a Slice.
     88      *
     89      * The collection works as follows:
     90      * - Collects a list of Fragments from
     91      * {@link FeatureFactory#getSearchFeatureProvider()}.
     92      * - From each fragment, grab a {@link SearchIndexProvider}.
     93      * - For each provider, collect XML resource layout and a list of
     94      * {@link com.android.settings.core.BasePreferenceController}.
     95      */
     96     public List<SliceData> getSliceData() {
     97         if (!mSliceData.isEmpty()) {
     98             return mSliceData;
     99         }
    100 
    101         final Collection<Class> indexableClasses = FeatureFactory.getFactory(mContext)
    102                 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
    103 
    104         for (Class clazz : indexableClasses) {
    105             final String fragmentName = clazz.getName();
    106 
    107             final SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider(
    108                     clazz);
    109 
    110             // CodeInspection test guards against the null check. Keep check in case of bad actors.
    111             if (provider == null) {
    112                 Log.e(TAG, fragmentName + " dose not implement Search Index Provider");
    113                 continue;
    114             }
    115 
    116             final List<SliceData> providerSliceData = getSliceDataFromProvider(provider,
    117                     fragmentName);
    118             mSliceData.addAll(providerSliceData);
    119         }
    120 
    121         final List<SliceData> a11ySliceData = getAccessibilitySliceData();
    122         mSliceData.addAll(a11ySliceData);
    123         return mSliceData;
    124     }
    125 
    126     private List<SliceData> getSliceDataFromProvider(SearchIndexProvider provider,
    127             String fragmentName) {
    128         final List<SliceData> sliceData = new ArrayList<>();
    129 
    130         final List<SearchIndexableResource> resList =
    131                 provider.getXmlResourcesToIndex(mContext, true /* enabled */);
    132 
    133         if (resList == null) {
    134             return sliceData;
    135         }
    136 
    137         // TODO (b/67996923) get a list of permanent NIKs and skip the invalid keys.
    138 
    139         for (SearchIndexableResource resource : resList) {
    140             int xmlResId = resource.xmlResId;
    141             if (xmlResId == 0) {
    142                 Log.e(TAG, fragmentName + " provides invalid XML (0) in search provider.");
    143                 continue;
    144             }
    145 
    146             List<SliceData> xmlSliceData = getSliceDataFromXML(xmlResId, fragmentName);
    147             sliceData.addAll(xmlSliceData);
    148         }
    149 
    150         return sliceData;
    151     }
    152 
    153     private List<SliceData> getSliceDataFromXML(int xmlResId, String fragmentName) {
    154         XmlResourceParser parser = null;
    155 
    156         final List<SliceData> xmlSliceData = new ArrayList<>();
    157 
    158         try {
    159             parser = mContext.getResources().getXml(xmlResId);
    160 
    161             int type;
    162             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
    163                     && type != XmlPullParser.START_TAG) {
    164                 // Parse next until start tag is found
    165             }
    166 
    167             String nodeName = parser.getName();
    168             if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) {
    169                 throw new RuntimeException(
    170                         "XML document must start with <PreferenceScreen> tag; found"
    171                                 + nodeName + " at " + parser.getPositionDescription());
    172             }
    173 
    174             final AttributeSet attrs = Xml.asAttributeSet(parser);
    175             final String screenTitle = PreferenceXmlParserUtils.getDataTitle(mContext, attrs);
    176 
    177             // TODO (b/67996923) Investigate if we need headers for Slices, since they never
    178             // correspond to an actual setting.
    179 
    180             final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(mContext,
    181                     xmlResId,
    182                     MetadataFlag.FLAG_NEED_KEY
    183                             | MetadataFlag.FLAG_NEED_PREF_CONTROLLER
    184                             | MetadataFlag.FLAG_NEED_PREF_TYPE
    185                             | MetadataFlag.FLAG_NEED_PREF_TITLE
    186                             | MetadataFlag.FLAG_NEED_PREF_ICON
    187                             | MetadataFlag.FLAG_NEED_PREF_SUMMARY
    188                             | MetadataFlag.FLAG_NEED_PLATFORM_SLICE_FLAG);
    189 
    190             for (Bundle bundle : metadata) {
    191                 // TODO (b/67996923) Non-controller Slices should become intent-only slices.
    192                 // Note that without a controller, dynamic summaries are impossible.
    193                 final String controllerClassName = bundle.getString(METADATA_CONTROLLER);
    194                 if (TextUtils.isEmpty(controllerClassName)) {
    195                     continue;
    196                 }
    197 
    198                 final String key = bundle.getString(METADATA_KEY);
    199                 final String title = bundle.getString(METADATA_TITLE);
    200                 final String summary = bundle.getString(METADATA_SUMMARY);
    201                 final int iconResId = bundle.getInt(METADATA_ICON);
    202                 final int sliceType = SliceBuilderUtils.getSliceType(mContext, controllerClassName,
    203                         key);
    204                 final boolean isPlatformSlice = bundle.getBoolean(METADATA_PLATFORM_SLICE_FLAG);
    205 
    206                 final SliceData xmlSlice = new SliceData.Builder()
    207                         .setKey(key)
    208                         .setTitle(title)
    209                         .setSummary(summary)
    210                         .setIcon(iconResId)
    211                         .setScreenTitle(screenTitle)
    212                         .setPreferenceControllerClassName(controllerClassName)
    213                         .setFragmentName(fragmentName)
    214                         .setSliceType(sliceType)
    215                         .setPlatformDefined(isPlatformSlice)
    216                         .build();
    217 
    218                 final BasePreferenceController controller =
    219                         SliceBuilderUtils.getPreferenceController(mContext, xmlSlice);
    220 
    221                 // Only add pre-approved Slices available on the device.
    222                 if (controller.isAvailable() && controller.isSliceable()) {
    223                     xmlSliceData.add(xmlSlice);
    224                 }
    225             }
    226         } catch (SliceData.InvalidSliceDataException e) {
    227             Log.w(TAG, "Invalid data when building SliceData for " + fragmentName, e);
    228         } catch (XmlPullParserException e) {
    229             Log.w(TAG, "XML Error parsing PreferenceScreen: ", e);
    230         } catch (IOException e) {
    231             Log.w(TAG, "IO Error parsing PreferenceScreen: ", e);
    232         } catch (Resources.NotFoundException e) {
    233             Log.w(TAG, "Resource not found error parsing PreferenceScreen: ", e);
    234         } finally {
    235             if (parser != null) parser.close();
    236         }
    237         return xmlSliceData;
    238     }
    239 
    240     private List<SliceData> getAccessibilitySliceData() {
    241         final List<SliceData> sliceData = new ArrayList<>();
    242 
    243         final String accessibilityControllerClassName =
    244                 AccessibilitySlicePreferenceController.class.getName();
    245         final String fragmentClassName = AccessibilitySettings.class.getName();
    246         final CharSequence screenTitle = mContext.getText(R.string.accessibility_settings);
    247 
    248         final SliceData.Builder sliceDataBuilder = new SliceData.Builder()
    249                 .setFragmentName(fragmentClassName)
    250                 .setScreenTitle(screenTitle)
    251                 .setPreferenceControllerClassName(accessibilityControllerClassName);
    252 
    253         final Set<String> a11yServiceNames = new HashSet<>();
    254         Collections.addAll(a11yServiceNames, mContext.getResources()
    255                 .getStringArray(R.array.config_settings_slices_accessibility_components));
    256         final List<AccessibilityServiceInfo> installedServices = getAccessibilityServiceInfoList();
    257         final PackageManager packageManager = mContext.getPackageManager();
    258 
    259         for (AccessibilityServiceInfo a11yServiceInfo : installedServices) {
    260             final ResolveInfo resolveInfo = a11yServiceInfo.getResolveInfo();
    261             final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
    262             final String packageName = serviceInfo.packageName;
    263             final ComponentName componentName = new ComponentName(packageName, serviceInfo.name);
    264             final String flattenedName = componentName.flattenToString();
    265 
    266             if (!a11yServiceNames.contains(flattenedName)) {
    267                 continue;
    268             }
    269 
    270             final String title = resolveInfo.loadLabel(packageManager).toString();
    271             int iconResource = resolveInfo.getIconResource();
    272             if (iconResource == 0) {
    273                 iconResource = R.mipmap.ic_accessibility_generic;
    274             }
    275 
    276             sliceDataBuilder.setKey(flattenedName)
    277                     .setTitle(title)
    278                     .setIcon(iconResource)
    279                     .setSliceType(SliceData.SliceType.SWITCH);
    280             try {
    281                 sliceData.add(sliceDataBuilder.build());
    282             } catch (SliceData.InvalidSliceDataException e) {
    283                 Log.w(TAG, "Invalid data when building a11y SliceData for " + flattenedName, e);
    284             }
    285         }
    286 
    287         return sliceData;
    288     }
    289 
    290     @VisibleForTesting
    291     List<AccessibilityServiceInfo> getAccessibilityServiceInfoList() {
    292         final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(
    293                 mContext);
    294         return accessibilityManager.getInstalledAccessibilityServiceList();
    295     }
    296 }