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