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 private 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 setSummaryOn(String summaryOn)189 public Builder setSummaryOn(String summaryOn) { 190 mSummaryOn = summaryOn; 191 return this; 192 } 193 setEntries(String entries)194 public Builder setEntries(String entries) { 195 mEntries = entries; 196 return this; 197 } 198 setClassName(String className)199 public Builder setClassName(String className) { 200 mClassName = className; 201 return this; 202 } 203 setChildClassName(String childClassName)204 public Builder setChildClassName(String childClassName) { 205 mChildClassName = childClassName; 206 return this; 207 } 208 setScreenTitle(String screenTitle)209 public Builder setScreenTitle(String screenTitle) { 210 mScreenTitle = screenTitle; 211 return this; 212 } 213 setPackageName(String packageName)214 public Builder setPackageName(String packageName) { 215 mPackageName = packageName; 216 return this; 217 } 218 setAuthority(String authority)219 public Builder setAuthority(String authority) { 220 mAuthority = authority; 221 return this; 222 } 223 setIconResId(int iconResId)224 public Builder setIconResId(int iconResId) { 225 mIconResId = iconResId; 226 return this; 227 } 228 setKeywords(String keywords)229 public Builder setKeywords(String keywords) { 230 mKeywords = keywords; 231 return this; 232 } 233 setIntentAction(String intentAction)234 public Builder setIntentAction(String intentAction) { 235 mIntentAction = intentAction; 236 return this; 237 } 238 setIntentTargetPackage(String intentTargetPackage)239 public Builder setIntentTargetPackage(String intentTargetPackage) { 240 mIntentTargetPackage = intentTargetPackage; 241 return this; 242 } 243 setIntentTargetClass(String intentTargetClass)244 public Builder setIntentTargetClass(String intentTargetClass) { 245 mIntentTargetClass = intentTargetClass; 246 return this; 247 } 248 setEnabled(boolean enabled)249 public Builder setEnabled(boolean enabled) { 250 mEnabled = enabled; 251 return this; 252 } 253 setKey(String key)254 public Builder setKey(String key) { 255 mKey = key; 256 return this; 257 } 258 setPayload(ResultPayload payload)259 public Builder setPayload(ResultPayload payload) { 260 mPayload = payload; 261 262 if (mPayload != null) { 263 setPayloadType(mPayload.getType()); 264 } 265 return this; 266 } 267 268 /** 269 * Payload type is added when a Payload is added to the Builder in {setPayload} 270 * 271 * @param payloadType PayloadType 272 * @return The Builder 273 */ setPayloadType(@esultPayload.PayloadType int payloadType)274 private Builder setPayloadType(@ResultPayload.PayloadType int payloadType) { 275 mPayloadType = payloadType; 276 return this; 277 } 278 279 /** 280 * Adds intent to inline payloads, or creates an Intent Payload as a fallback if the 281 * payload is null. 282 */ setIntent(Context context)283 private void setIntent(Context context) { 284 if (mPayload != null) { 285 return; 286 } 287 final Intent intent = buildIntent(context); 288 mPayload = new ResultPayload(intent); 289 mPayloadType = ResultPayload.PayloadType.INTENT; 290 } 291 292 /** 293 * Adds Intent payload to builder. 294 */ buildIntent(Context context)295 private Intent buildIntent(Context context) { 296 final Intent intent; 297 298 // TODO REFACTOR (b/62807132) With inline results re-add proper intent support 299 boolean isEmptyIntentAction = TextUtils.isEmpty(mIntentAction); 300 if (isEmptyIntentAction) { 301 // No intent action is set, or the intent action is for a sub-setting. 302 intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(context, mClassName, 303 mKey, mScreenTitle); 304 } else { 305 intent = DatabaseIndexingUtils.buildDirectSearchResultIntent(mIntentAction, 306 mIntentTargetPackage, mIntentTargetClass, mKey); 307 } 308 return intent; 309 } 310 build(Context context)311 public IndexData build(Context context) { 312 setIntent(context); 313 return new IndexData(this); 314 } 315 } 316 }