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.intelligence.search.indexing; 19 20 import android.content.Context; 21 import android.content.Intent; 22 import android.text.TextUtils; 23 24 import com.android.settings.intelligence.search.ResultPayload; 25 import com.android.settings.intelligence.search.ResultPayloadUtils; 26 27 import java.text.Normalizer; 28 import java.util.Locale; 29 import java.util.regex.Pattern; 30 31 /** 32 * Data class representing a single row in the Setting Search results database. 33 */ 34 public class IndexData { 35 /** 36 * This is different from intentTargetPackage. 37 * 38 * @see SearchIndexableData#iconResId 39 */ 40 public final String packageName; 41 public final String authority; 42 public final String locale; 43 public final String updatedTitle; 44 public final String normalizedTitle; 45 public final String updatedSummaryOn; 46 public final String normalizedSummaryOn; 47 public final String entries; 48 public final String className; 49 public final String childClassName; 50 public final String screenTitle; 51 public final int iconResId; 52 public final String spaceDelimitedKeywords; 53 public final String intentAction; 54 public final String intentTargetPackage; 55 public final String intentTargetClass; 56 public final boolean enabled; 57 public final String key; 58 public final int payloadType; 59 public final byte[] payload; 60 public final String highlightableMenuKey; // the key of top level settings row 61 public final String topLevelMenuKey; // the key for highlighting the menu entry 62 63 private final Builder mBuilder; 64 private static final String NON_BREAKING_HYPHEN = "\u2011"; 65 private static final String EMPTY = ""; 66 private static final String HYPHEN = "-"; 67 private static final String SPACE = " "; 68 // Regex matching a comma, and any number of subsequent white spaces. 69 private static final String LIST_DELIMITERS = "[,]\\s*"; 70 71 private static final Pattern REMOVE_DIACRITICALS_PATTERN 72 = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); 73 IndexData(Builder builder)74 protected IndexData(Builder builder) { 75 locale = Locale.getDefault().toString(); 76 updatedTitle = normalizeHyphen(builder.mTitle); 77 updatedSummaryOn = normalizeHyphen(builder.mSummaryOn); 78 if (Locale.JAPAN.toString().equalsIgnoreCase(locale)) { 79 // Special case for JP. Convert charset to the same type for indexing purpose. 80 normalizedTitle = normalizeJapaneseString(builder.mTitle); 81 normalizedSummaryOn = normalizeJapaneseString(builder.mSummaryOn); 82 } else { 83 normalizedTitle = normalizeString(builder.mTitle); 84 normalizedSummaryOn = normalizeString(builder.mSummaryOn); 85 } 86 entries = builder.mEntries; 87 className = builder.mClassName; 88 childClassName = builder.mChildClassName; 89 screenTitle = builder.mScreenTitle; 90 iconResId = builder.mIconResId; 91 spaceDelimitedKeywords = normalizeKeywords(builder.mKeywords); 92 intentAction = builder.mIntentAction; 93 packageName = builder.mPackageName; 94 authority = builder.mAuthority; 95 intentTargetPackage = builder.mIntentTargetPackage; 96 intentTargetClass = builder.mIntentTargetClass; 97 enabled = builder.mEnabled; 98 key = builder.mKey; 99 payloadType = builder.mPayloadType; 100 payload = builder.mPayload != null ? ResultPayloadUtils.marshall(builder.mPayload) 101 : null; 102 highlightableMenuKey = builder.mHighlightableMenuKey; 103 topLevelMenuKey = builder.mTopLevelMenuKey; 104 mBuilder = builder; 105 } 106 107 /** Returns the builder of the IndexData. */ mutate()108 public Builder mutate() { 109 return mBuilder; 110 } 111 112 @Override toString()113 public String toString() { 114 return new StringBuilder(updatedTitle) 115 .append(": ") 116 .append(updatedSummaryOn) 117 .toString(); 118 } 119 120 /** 121 * In the list of keywords, replace the comma and all subsequent whitespace with a single space. 122 */ normalizeKeywords(String input)123 public static String normalizeKeywords(String input) { 124 return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY; 125 } 126 127 /** 128 * @return {@param input} where all non-standard hyphens are replaced by normal hyphens. 129 */ normalizeHyphen(String input)130 public static String normalizeHyphen(String input) { 131 return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; 132 } 133 134 /** 135 * @return {@param input} with all hyphens removed, and all letters lower case. 136 */ normalizeString(String input)137 public static String normalizeString(String input) { 138 final String normalizedHypen = normalizeHyphen(input); 139 final String nohyphen = (input != null) ? normalizedHypen.replaceAll(HYPHEN, EMPTY) : EMPTY; 140 final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD); 141 142 return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase(); 143 } 144 normalizeJapaneseString(String input)145 public static String normalizeJapaneseString(String input) { 146 final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY; 147 final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFKD); 148 final StringBuffer sb = new StringBuffer(); 149 final int length = normalized.length(); 150 for (int i = 0; i < length; i++) { 151 char c = normalized.charAt(i); 152 // Convert Hiragana to full-width Katakana 153 if (c >= '\u3041' && c <= '\u3096') { 154 sb.append((char) (c - '\u3041' + '\u30A1')); 155 } else { 156 sb.append(c); 157 } 158 } 159 160 return REMOVE_DIACRITICALS_PATTERN.matcher(sb.toString()).replaceAll("").toLowerCase(); 161 } 162 163 public static class Builder { 164 private String mTitle; 165 private String mSummaryOn; 166 private String mEntries; 167 private String mClassName; 168 private String mChildClassName; 169 private String mScreenTitle; 170 private String mPackageName; 171 private String mAuthority; 172 private int mIconResId; 173 private String mKeywords; 174 private String mIntentAction; 175 private String mIntentTargetPackage; 176 private String mIntentTargetClass; 177 private boolean mEnabled; 178 private String mKey; 179 @ResultPayload.PayloadType 180 private int mPayloadType; 181 private ResultPayload mPayload; 182 private String mHighlightableMenuKey; 183 private String mTopLevelMenuKey; 184 185 @Override toString()186 public String toString() { 187 return "IndexData.Builder {" 188 + "title: " + mTitle + "," 189 + "package: " + mPackageName 190 + "}"; 191 } 192 setTitle(String title)193 public Builder setTitle(String title) { 194 mTitle = title; 195 return this; 196 } 197 getKey()198 public String getKey() { 199 return mKey; 200 } 201 getIntentAction()202 public String getIntentAction() { 203 return mIntentAction; 204 } 205 getIntentTargetPackage()206 public String getIntentTargetPackage() { 207 return mIntentTargetPackage; 208 } 209 getIntentTargetClass()210 public String getIntentTargetClass() { 211 return mIntentTargetClass; 212 } 213 setSummaryOn(String summaryOn)214 public Builder setSummaryOn(String summaryOn) { 215 mSummaryOn = summaryOn; 216 return this; 217 } 218 setEntries(String entries)219 public Builder setEntries(String entries) { 220 mEntries = entries; 221 return this; 222 } 223 setClassName(String className)224 public Builder setClassName(String className) { 225 mClassName = className; 226 return this; 227 } 228 setChildClassName(String childClassName)229 public Builder setChildClassName(String childClassName) { 230 mChildClassName = childClassName; 231 return this; 232 } 233 setScreenTitle(String screenTitle)234 public Builder setScreenTitle(String screenTitle) { 235 mScreenTitle = screenTitle; 236 return this; 237 } 238 setPackageName(String packageName)239 public Builder setPackageName(String packageName) { 240 mPackageName = packageName; 241 return this; 242 } 243 setAuthority(String authority)244 public Builder setAuthority(String authority) { 245 mAuthority = authority; 246 return this; 247 } 248 setIconResId(int iconResId)249 public Builder setIconResId(int iconResId) { 250 mIconResId = iconResId; 251 return this; 252 } 253 setKeywords(String keywords)254 public Builder setKeywords(String keywords) { 255 mKeywords = keywords; 256 return this; 257 } 258 setIntentAction(String intentAction)259 public Builder setIntentAction(String intentAction) { 260 mIntentAction = intentAction; 261 return this; 262 } 263 setIntentTargetPackage(String intentTargetPackage)264 public Builder setIntentTargetPackage(String intentTargetPackage) { 265 mIntentTargetPackage = intentTargetPackage; 266 return this; 267 } 268 setIntentTargetClass(String intentTargetClass)269 public Builder setIntentTargetClass(String intentTargetClass) { 270 mIntentTargetClass = intentTargetClass; 271 return this; 272 } 273 setEnabled(boolean enabled)274 public Builder setEnabled(boolean enabled) { 275 mEnabled = enabled; 276 return this; 277 } 278 setKey(String key)279 public Builder setKey(String key) { 280 mKey = key; 281 return this; 282 } 283 setPayload(ResultPayload payload)284 public Builder setPayload(ResultPayload payload) { 285 mPayload = payload; 286 287 if (mPayload != null) { 288 setPayloadType(mPayload.getType()); 289 } 290 return this; 291 } 292 setHighlightableMenuKey(String highlightableMenuKey)293 public Builder setHighlightableMenuKey(String highlightableMenuKey) { 294 mHighlightableMenuKey = highlightableMenuKey; 295 return this; 296 } 297 setTopLevelMenuKey(String topLevelMenuKey)298 Builder setTopLevelMenuKey(String topLevelMenuKey) { 299 mTopLevelMenuKey = topLevelMenuKey; 300 mPayload = null; // clear the payload to rebuild intent 301 return this; 302 } 303 304 /** 305 * Payload type is added when a Payload is added to the Builder in {setPayload} 306 * 307 * @param payloadType PayloadType 308 * @return The Builder 309 */ setPayloadType(@esultPayload.PayloadType int payloadType)310 private Builder setPayloadType(@ResultPayload.PayloadType int payloadType) { 311 mPayloadType = payloadType; 312 return this; 313 } 314 315 /** 316 * Adds intent to inline payloads, or creates an Intent Payload as a fallback if the 317 * payload is null. 318 */ setIntent()319 private void setIntent() { 320 if (mPayload != null) { 321 return; 322 } 323 final Intent intent = buildIntent(); 324 mPayload = new ResultPayload(intent); 325 mPayloadType = ResultPayload.PayloadType.INTENT; 326 } 327 328 /** 329 * Builds Intent payload for the builder. 330 * This protected method that can be overridden in a subclass for custom intents. 331 */ buildIntent()332 protected Intent buildIntent() { 333 final Intent intent; 334 335 // TODO REFACTOR (b/62807132) With inline results re-add proper intent support 336 boolean isEmptyIntentAction = TextUtils.isEmpty(mIntentAction); 337 if (isEmptyIntentAction) { 338 // No intent action is set, or the intent action is for a sub-setting. 339 intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mClassName, mKey, 340 mScreenTitle, mTopLevelMenuKey); 341 } else { 342 if (!TextUtils.isEmpty(mTopLevelMenuKey)) { 343 intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mIntentAction, 344 mIntentTargetPackage, mIntentTargetClass, mKey, mTopLevelMenuKey); 345 } else { 346 intent = DatabaseIndexingUtils.buildDirectSearchResultIntent(mIntentAction, 347 mIntentTargetPackage, mIntentTargetClass, mKey); 348 } 349 } 350 return intent; 351 } 352 353 @Deprecated buildIntent(Context context)354 protected Intent buildIntent(Context context) { 355 return buildIntent(); 356 } 357 build()358 public IndexData build() { 359 setIntent(); 360 return new IndexData(this); 361 } 362 } 363 } 364