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