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 18 package com.android.settings.search; 19 20 import android.Manifest; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.PackageInfo; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.text.TextUtils; 30 import android.util.ArrayMap; 31 import android.util.Log; 32 33 import com.android.internal.logging.nano.MetricsProto; 34 import com.android.settings.SettingsActivity; 35 import com.android.settings.Utils; 36 import com.android.settings.core.PreferenceControllerMixin; 37 import com.android.settingslib.core.AbstractPreferenceController; 38 39 import java.lang.reflect.Field; 40 import java.text.Normalizer; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.regex.Pattern; 44 45 /** 46 * Utility class for {@like DatabaseIndexingManager} to handle the mapping between Payloads 47 * and Preference controllers, and managing indexable classes. 48 */ 49 public class DatabaseIndexingUtils { 50 51 private static final String TAG = "IndexingUtil"; 52 53 private static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER = 54 "SEARCH_INDEX_DATA_PROVIDER"; 55 56 private static final String NON_BREAKING_HYPHEN = "\u2011"; 57 private static final String EMPTY = ""; 58 private static final String LIST_DELIMITERS = "[,]\\s*"; 59 private static final String HYPHEN = "-"; 60 private static final String SPACE = " "; 61 62 private static final Pattern REMOVE_DIACRITICALS_PATTERN 63 = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); 64 65 /** 66 * Builds intent into a subsetting. 67 */ buildSubsettingIntent(Context context, String className, String key, String screenTitle)68 public static Intent buildSubsettingIntent(Context context, String className, String key, 69 String screenTitle) { 70 final Bundle args = new Bundle(); 71 args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); 72 return Utils.onBuildStartFragmentIntent(context, 73 className, args, null, 0, screenTitle, false, 74 MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS); 75 } 76 77 /** 78 * @param className which wil provide the map between from {@link Uri}s to 79 * {@link PreferenceControllerMixin} 80 * @param context 81 * @return A map between {@link Uri}s and {@link PreferenceControllerMixin}s to get the payload 82 * types for Settings. 83 */ getPreferenceControllerUriMap( String className, Context context)84 public static Map<String, PreferenceControllerMixin> getPreferenceControllerUriMap( 85 String className, Context context) { 86 if (context == null) { 87 return null; 88 } 89 90 final Class<?> clazz = getIndexableClass(className); 91 92 if (clazz == null) { 93 Log.d(TAG, "SearchIndexableResource '" + className + 94 "' should implement the " + Indexable.class.getName() + " interface!"); 95 return null; 96 } 97 98 // Will be non null only for a Local provider implementing a 99 // SEARCH_INDEX_DATA_PROVIDER field 100 final Indexable.SearchIndexProvider provider = getSearchIndexProvider(clazz); 101 102 List<AbstractPreferenceController> controllers = 103 provider.getPreferenceControllers(context); 104 105 if (controllers == null ) { 106 return null; 107 } 108 109 ArrayMap<String, PreferenceControllerMixin> map = new ArrayMap<>(); 110 111 for (AbstractPreferenceController controller : controllers) { 112 if (controller instanceof PreferenceControllerMixin) { 113 map.put(controller.getPreferenceKey(), (PreferenceControllerMixin) controller); 114 } else { 115 throw new IllegalStateException(controller.getClass().getName() 116 + " must implement " + PreferenceControllerMixin.class.getName()); 117 } 118 } 119 120 return map; 121 } 122 123 /** 124 * @param uriMap Map between the {@link PreferenceControllerMixin} keys 125 * and the controllers themselves. 126 * @param key The look-up key 127 * @return The Payload from the {@link PreferenceControllerMixin} specified by the key, 128 * if it exists. Otherwise null. 129 */ getPayloadFromUriMap(Map<String, PreferenceControllerMixin> uriMap, String key)130 public static ResultPayload getPayloadFromUriMap(Map<String, PreferenceControllerMixin> uriMap, 131 String key) { 132 if (uriMap == null) { 133 return null; 134 } 135 136 PreferenceControllerMixin controller = uriMap.get(key); 137 if (controller == null) { 138 return null; 139 } 140 141 return controller.getResultPayload(); 142 } 143 getIndexableClass(String className)144 public static Class<?> getIndexableClass(String className) { 145 final Class<?> clazz; 146 try { 147 clazz = Class.forName(className); 148 } catch (ClassNotFoundException e) { 149 Log.d(TAG, "Cannot find class: " + className); 150 return null; 151 } 152 return isIndexableClass(clazz) ? clazz : null; 153 } 154 isIndexableClass(final Class<?> clazz)155 public static boolean isIndexableClass(final Class<?> clazz) { 156 return (clazz != null) && Indexable.class.isAssignableFrom(clazz); 157 } 158 getSearchIndexProvider(final Class<?> clazz)159 public static Indexable.SearchIndexProvider getSearchIndexProvider(final Class<?> clazz) { 160 try { 161 final Field f = clazz.getField(FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER); 162 return (Indexable.SearchIndexProvider) f.get(null); 163 } catch (NoSuchFieldException e) { 164 Log.d(TAG, "Cannot find field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 165 } catch (SecurityException se) { 166 Log.d(TAG, "Security exception for field '" + 167 FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 168 } catch (IllegalAccessException e) { 169 Log.d(TAG, "Illegal access to field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 170 } catch (IllegalArgumentException e) { 171 Log.d(TAG, "Illegal argument when accessing field '" + 172 FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 173 } 174 return null; 175 } 176 177 /** 178 * Only allow a "well known" SearchIndexablesProvider. The provider should: 179 * 180 * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES} 181 * - be from a privileged package 182 */ isWellKnownProvider(ResolveInfo info, Context context)183 static boolean isWellKnownProvider(ResolveInfo info, Context context) { 184 final String authority = info.providerInfo.authority; 185 final String packageName = info.providerInfo.applicationInfo.packageName; 186 187 if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) { 188 return false; 189 } 190 191 final String readPermission = info.providerInfo.readPermission; 192 final String writePermission = info.providerInfo.writePermission; 193 194 if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) { 195 return false; 196 } 197 198 if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) || 199 !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) { 200 return false; 201 } 202 203 return isPrivilegedPackage(packageName, context); 204 } 205 normalizeHyphen(String input)206 static String normalizeHyphen(String input) { 207 return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; 208 } 209 normalizeString(String input)210 static String normalizeString(String input) { 211 final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY; 212 final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD); 213 214 return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase(); 215 } 216 normalizeKeywords(String input)217 static String normalizeKeywords(String input) { 218 return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY; 219 } 220 isPrivilegedPackage(String packageName, Context context)221 private static boolean isPrivilegedPackage(String packageName, Context context) { 222 final PackageManager pm = context.getPackageManager(); 223 try { 224 PackageInfo packInfo = pm.getPackageInfo(packageName, 0); 225 return ((packInfo.applicationInfo.privateFlags 226 & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0); 227 } catch (PackageManager.NameNotFoundException e) { 228 return false; 229 } 230 } 231 } 232