Home | History | Annotate | Download | only in indexing
      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 }