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