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