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 }