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 }