/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.settings.intelligence.search.indexing; import android.content.Context; import android.content.Intent; import android.text.TextUtils; import com.android.settings.intelligence.search.ResultPayload; import com.android.settings.intelligence.search.ResultPayloadUtils; import java.text.Normalizer; import java.util.Locale; import java.util.regex.Pattern; /** * Data class representing a single row in the Setting Search results database. */ public class IndexData { /** * This is different from intentTargetPackage. * * @see SearchIndexableData#iconResId */ public final String packageName; public final String authority; public final String locale; public final String updatedTitle; public final String normalizedTitle; public final String updatedSummaryOn; public final String normalizedSummaryOn; public final String entries; public final String className; public final String childClassName; public final String screenTitle; public final int iconResId; public final String spaceDelimitedKeywords; public final String intentAction; public final String intentTargetPackage; public final String intentTargetClass; public final boolean enabled; public final String key; public final int payloadType; public final byte[] payload; public final String highlightableMenuKey; // the key of top level settings row public final String topLevelMenuKey; // the key for highlighting the menu entry private final Builder mBuilder; private static final String NON_BREAKING_HYPHEN = "\u2011"; private static final String EMPTY = ""; private static final String HYPHEN = "-"; private static final String SPACE = " "; // Regex matching a comma, and any number of subsequent white spaces. private static final String LIST_DELIMITERS = "[,]\\s*"; private static final Pattern REMOVE_DIACRITICALS_PATTERN = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); protected IndexData(Builder builder) { locale = Locale.getDefault().toString(); updatedTitle = normalizeHyphen(builder.mTitle); updatedSummaryOn = normalizeHyphen(builder.mSummaryOn); if (Locale.JAPAN.toString().equalsIgnoreCase(locale)) { // Special case for JP. Convert charset to the same type for indexing purpose. normalizedTitle = normalizeJapaneseString(builder.mTitle); normalizedSummaryOn = normalizeJapaneseString(builder.mSummaryOn); } else { normalizedTitle = normalizeString(builder.mTitle); normalizedSummaryOn = normalizeString(builder.mSummaryOn); } entries = builder.mEntries; className = builder.mClassName; childClassName = builder.mChildClassName; screenTitle = builder.mScreenTitle; iconResId = builder.mIconResId; spaceDelimitedKeywords = normalizeKeywords(builder.mKeywords); intentAction = builder.mIntentAction; packageName = builder.mPackageName; authority = builder.mAuthority; intentTargetPackage = builder.mIntentTargetPackage; intentTargetClass = builder.mIntentTargetClass; enabled = builder.mEnabled; key = builder.mKey; payloadType = builder.mPayloadType; payload = builder.mPayload != null ? ResultPayloadUtils.marshall(builder.mPayload) : null; highlightableMenuKey = builder.mHighlightableMenuKey; topLevelMenuKey = builder.mTopLevelMenuKey; mBuilder = builder; } /** Returns the builder of the IndexData. */ public Builder mutate() { return mBuilder; } @Override public String toString() { return new StringBuilder(updatedTitle) .append(": ") .append(updatedSummaryOn) .toString(); } /** * In the list of keywords, replace the comma and all subsequent whitespace with a single space. */ public static String normalizeKeywords(String input) { return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY; } /** * @return {@param input} where all non-standard hyphens are replaced by normal hyphens. */ public static String normalizeHyphen(String input) { return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; } /** * @return {@param input} with all hyphens removed, and all letters lower case. */ public static String normalizeString(String input) { final String normalizedHypen = normalizeHyphen(input); final String nohyphen = (input != null) ? normalizedHypen.replaceAll(HYPHEN, EMPTY) : EMPTY; final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD); return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase(); } public static String normalizeJapaneseString(String input) { final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY; final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFKD); final StringBuffer sb = new StringBuffer(); final int length = normalized.length(); for (int i = 0; i < length; i++) { char c = normalized.charAt(i); // Convert Hiragana to full-width Katakana if (c >= '\u3041' && c <= '\u3096') { sb.append((char) (c - '\u3041' + '\u30A1')); } else { sb.append(c); } } return REMOVE_DIACRITICALS_PATTERN.matcher(sb.toString()).replaceAll("").toLowerCase(); } public static class Builder { private String mTitle; private String mSummaryOn; private String mEntries; private String mClassName; private String mChildClassName; private String mScreenTitle; private String mPackageName; private String mAuthority; private int mIconResId; private String mKeywords; private String mIntentAction; private String mIntentTargetPackage; private String mIntentTargetClass; private boolean mEnabled; private String mKey; @ResultPayload.PayloadType private int mPayloadType; private ResultPayload mPayload; private String mHighlightableMenuKey; private String mTopLevelMenuKey; @Override public String toString() { return "IndexData.Builder {" + "title: " + mTitle + "," + "package: " + mPackageName + "}"; } public Builder setTitle(String title) { mTitle = title; return this; } public String getKey() { return mKey; } public String getIntentAction() { return mIntentAction; } public String getIntentTargetPackage() { return mIntentTargetPackage; } public String getIntentTargetClass() { return mIntentTargetClass; } public Builder setSummaryOn(String summaryOn) { mSummaryOn = summaryOn; return this; } public Builder setEntries(String entries) { mEntries = entries; return this; } public Builder setClassName(String className) { mClassName = className; return this; } public Builder setChildClassName(String childClassName) { mChildClassName = childClassName; return this; } public Builder setScreenTitle(String screenTitle) { mScreenTitle = screenTitle; return this; } public Builder setPackageName(String packageName) { mPackageName = packageName; return this; } public Builder setAuthority(String authority) { mAuthority = authority; return this; } public Builder setIconResId(int iconResId) { mIconResId = iconResId; return this; } public Builder setKeywords(String keywords) { mKeywords = keywords; return this; } public Builder setIntentAction(String intentAction) { mIntentAction = intentAction; return this; } public Builder setIntentTargetPackage(String intentTargetPackage) { mIntentTargetPackage = intentTargetPackage; return this; } public Builder setIntentTargetClass(String intentTargetClass) { mIntentTargetClass = intentTargetClass; return this; } public Builder setEnabled(boolean enabled) { mEnabled = enabled; return this; } public Builder setKey(String key) { mKey = key; return this; } public Builder setPayload(ResultPayload payload) { mPayload = payload; if (mPayload != null) { setPayloadType(mPayload.getType()); } return this; } public Builder setHighlightableMenuKey(String highlightableMenuKey) { mHighlightableMenuKey = highlightableMenuKey; return this; } Builder setTopLevelMenuKey(String topLevelMenuKey) { mTopLevelMenuKey = topLevelMenuKey; mPayload = null; // clear the payload to rebuild intent return this; } /** * Payload type is added when a Payload is added to the Builder in {setPayload} * * @param payloadType PayloadType * @return The Builder */ private Builder setPayloadType(@ResultPayload.PayloadType int payloadType) { mPayloadType = payloadType; return this; } /** * Adds intent to inline payloads, or creates an Intent Payload as a fallback if the * payload is null. */ private void setIntent() { if (mPayload != null) { return; } final Intent intent = buildIntent(); mPayload = new ResultPayload(intent); mPayloadType = ResultPayload.PayloadType.INTENT; } /** * Builds Intent payload for the builder. * This protected method that can be overridden in a subclass for custom intents. */ protected Intent buildIntent() { final Intent intent; // TODO REFACTOR (b/62807132) With inline results re-add proper intent support boolean isEmptyIntentAction = TextUtils.isEmpty(mIntentAction); if (isEmptyIntentAction) { // No intent action is set, or the intent action is for a sub-setting. intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mClassName, mKey, mScreenTitle, mTopLevelMenuKey); } else { if (!TextUtils.isEmpty(mTopLevelMenuKey)) { intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mIntentAction, mIntentTargetPackage, mIntentTargetClass, mKey, mTopLevelMenuKey); } else { intent = DatabaseIndexingUtils.buildDirectSearchResultIntent(mIntentAction, mIntentTargetPackage, mIntentTargetClass, mKey); } } return intent; } @Deprecated protected Intent buildIntent(Context context) { return buildIntent(); } public IndexData build() { setIntent(); return new IndexData(this); } } }