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