1 /*
2  * Copyright (C) 2018 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 package com.android.textclassifier;
18 
19 import android.provider.DeviceConfig;
20 import android.view.textclassifier.ConversationAction;
21 import android.view.textclassifier.TextClassifier;
22 import com.android.textclassifier.utils.IndentingPrintWriter;
23 import com.google.common.annotations.VisibleForTesting;
24 import com.google.common.base.Splitter;
25 import com.google.common.collect.ImmutableList;
26 import java.util.Arrays;
27 import java.util.Collections;
28 import java.util.List;
29 import javax.annotation.Nullable;
30 
31 /**
32  * TextClassifier specific settings.
33  *
34  * <p>Currently, this class does not guarantee co-diverted flags are updated atomically.
35  *
36  * <p>Example of setting the values for testing.
37  *
38  * <pre>
39  * adb shell cmd device_config put textclassifier system_textclassifier_enabled true
40  * </pre>
41  *
42  * @see android.provider.DeviceConfig#NAMESPACE_TEXTCLASSIFIER
43  */
44 public final class TextClassifierSettings {
45   private static final String DELIMITER = ":";
46 
47   /** Whether the user language profile feature is enabled. */
48   private static final String USER_LANGUAGE_PROFILE_ENABLED = "user_language_profile_enabled";
49   /** Max length of text that suggestSelection can accept. */
50   @VisibleForTesting
51   static final String SUGGEST_SELECTION_MAX_RANGE_LENGTH = "suggest_selection_max_range_length";
52   /** Max length of text that classifyText can accept. */
53   private static final String CLASSIFY_TEXT_MAX_RANGE_LENGTH = "classify_text_max_range_length";
54   /** Max length of text that generateLinks can accept. */
55   private static final String GENERATE_LINKS_MAX_TEXT_LENGTH = "generate_links_max_text_length";
56   /** Sampling rate for generateLinks logging. */
57   private static final String GENERATE_LINKS_LOG_SAMPLE_RATE = "generate_links_log_sample_rate";
58   /**
59    * Extra count that is added to some languages, e.g. system languages, when deducing the frequent
60    * languages in {@link
61    * com.android.textclassifier.ulp.LanguageProfileAnalyzer#getFrequentLanguages(int)}.
62    */
63 
64   /**
65    * A colon(:) separated string that specifies the default entities types for generateLinks when
66    * hint is not given.
67    */
68   @VisibleForTesting static final String ENTITY_LIST_DEFAULT = "entity_list_default";
69   /**
70    * A colon(:) separated string that specifies the default entities types for generateLinks when
71    * the text is in a not editable UI widget.
72    */
73   private static final String ENTITY_LIST_NOT_EDITABLE = "entity_list_not_editable";
74   /**
75    * A colon(:) separated string that specifies the default entities types for generateLinks when
76    * the text is in an editable UI widget.
77    */
78   private static final String ENTITY_LIST_EDITABLE = "entity_list_editable";
79   /**
80    * A colon(:) separated string that specifies the default action types for
81    * suggestConversationActions when the suggestions are used in an app.
82    */
83   private static final String IN_APP_CONVERSATION_ACTION_TYPES_DEFAULT =
84       "in_app_conversation_action_types_default";
85   /**
86    * A colon(:) separated string that specifies the default action types for
87    * suggestConversationActions when the suggestions are used in a notification.
88    */
89   private static final String NOTIFICATION_CONVERSATION_ACTION_TYPES_DEFAULT =
90       "notification_conversation_action_types_default";
91   /** Threshold to accept a suggested language from LangID model. */
92   @VisibleForTesting static final String LANG_ID_THRESHOLD_OVERRIDE = "lang_id_threshold_override";
93   /** Whether to enable {@link com.android.textclassifier.intent.TemplateIntentFactory}. */
94   @VisibleForTesting
95   static final String TEMPLATE_INTENT_FACTORY_ENABLED = "template_intent_factory_enabled";
96   /** Whether to enable "translate" action in classifyText. */
97   private static final String TRANSLATE_IN_CLASSIFICATION_ENABLED =
98       "translate_in_classification_enabled";
99   /**
100    * Whether to detect the languages of the text in request by using langId for the native model.
101    */
102   private static final String DETECT_LANGUAGES_FROM_TEXT_ENABLED =
103       "detect_languages_from_text_enabled";
104   /**
105    * A colon(:) separated string that specifies the configuration to use when including surrounding
106    * context text in language detection queries.
107    *
108    * <p>Format= minimumTextSize<int>:penalizeRatio<float>:textScoreRatio<float>
109    *
110    * <p>e.g. 20:1.0:0.4
111    *
112    * <p>Accept all text lengths with minimumTextSize=0
113    *
114    * <p>Reject all text less than minimumTextSize with penalizeRatio=0
115    *
116    * @see {@code TextClassifierImpl#detectLanguages(String, int, int)} for reference.
117    */
118   @VisibleForTesting static final String LANG_ID_CONTEXT_SETTINGS = "lang_id_context_settings";
119   /** Default threshold to translate the language of the context the user selects */
120   private static final String TRANSLATE_ACTION_THRESHOLD = "translate_action_threshold";
121 
122   // Sync this with ConversationAction.TYPE_ADD_CONTACT;
123   public static final String TYPE_ADD_CONTACT = "add_contact";
124   // Sync this with ConversationAction.COPY;
125   public static final String TYPE_COPY = "copy";
126 
127   private static final int SUGGEST_SELECTION_MAX_RANGE_LENGTH_DEFAULT = 10 * 1000;
128   private static final int CLASSIFY_TEXT_MAX_RANGE_LENGTH_DEFAULT = 10 * 1000;
129   private static final int GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT = 100 * 1000;
130   private static final int GENERATE_LINKS_LOG_SAMPLE_RATE_DEFAULT = 100;
131 
132   private static final ImmutableList<String> ENTITY_LIST_DEFAULT_VALUE =
133       ImmutableList.of(
134           TextClassifier.TYPE_ADDRESS,
135           TextClassifier.TYPE_EMAIL,
136           TextClassifier.TYPE_PHONE,
137           TextClassifier.TYPE_URL,
138           TextClassifier.TYPE_DATE,
139           TextClassifier.TYPE_DATE_TIME,
140           TextClassifier.TYPE_FLIGHT_NUMBER);
141   private static final ImmutableList<String> CONVERSATION_ACTIONS_TYPES_DEFAULT_VALUES =
142       ImmutableList.of(
143           ConversationAction.TYPE_TEXT_REPLY,
144           ConversationAction.TYPE_CREATE_REMINDER,
145           ConversationAction.TYPE_CALL_PHONE,
146           ConversationAction.TYPE_OPEN_URL,
147           ConversationAction.TYPE_SEND_EMAIL,
148           ConversationAction.TYPE_SEND_SMS,
149           ConversationAction.TYPE_TRACK_FLIGHT,
150           ConversationAction.TYPE_VIEW_CALENDAR,
151           ConversationAction.TYPE_VIEW_MAP,
152           TYPE_ADD_CONTACT,
153           TYPE_COPY);
154   /**
155    * < 0 : Not set. Use value from LangId model. 0 - 1: Override value in LangId model.
156    *
157    * @see EntityConfidence
158    */
159   private static final float LANG_ID_THRESHOLD_OVERRIDE_DEFAULT = -1f;
160 
161   private static final float TRANSLATE_ACTION_THRESHOLD_DEFAULT = 0.5f;
162 
163   private static final boolean USER_LANGUAGE_PROFILE_ENABLED_DEFAULT = true;
164   private static final boolean TEMPLATE_INTENT_FACTORY_ENABLED_DEFAULT = true;
165   private static final boolean TRANSLATE_IN_CLASSIFICATION_ENABLED_DEFAULT = true;
166   private static final boolean DETECT_LANGUAGES_FROM_TEXT_ENABLED_DEFAULT = true;
167   private static final float[] LANG_ID_CONTEXT_SETTINGS_DEFAULT = new float[] {20f, 1.0f, 0.4f};
168 
169   public int getSuggestSelectionMaxRangeLength() {
170     return DeviceConfig.getInt(
171         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
172         SUGGEST_SELECTION_MAX_RANGE_LENGTH,
173         SUGGEST_SELECTION_MAX_RANGE_LENGTH_DEFAULT);
174   }
175 
176   public int getClassifyTextMaxRangeLength() {
177     return DeviceConfig.getInt(
178         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
179         CLASSIFY_TEXT_MAX_RANGE_LENGTH,
180         CLASSIFY_TEXT_MAX_RANGE_LENGTH_DEFAULT);
181   }
182 
183   public int getGenerateLinksMaxTextLength() {
184     return DeviceConfig.getInt(
185         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
186         GENERATE_LINKS_MAX_TEXT_LENGTH,
187         GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT);
188   }
189 
190   public int getGenerateLinksLogSampleRate() {
191     return DeviceConfig.getInt(
192         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
193         GENERATE_LINKS_LOG_SAMPLE_RATE,
194         GENERATE_LINKS_LOG_SAMPLE_RATE_DEFAULT);
195   }
196 
197   public List<String> getEntityListDefault() {
198     return getDeviceConfigStringList(ENTITY_LIST_DEFAULT, ENTITY_LIST_DEFAULT_VALUE);
199   }
200 
201   public List<String> getEntityListNotEditable() {
202     return getDeviceConfigStringList(ENTITY_LIST_NOT_EDITABLE, ENTITY_LIST_DEFAULT_VALUE);
203   }
204 
205   public List<String> getEntityListEditable() {
206     return getDeviceConfigStringList(ENTITY_LIST_EDITABLE, ENTITY_LIST_DEFAULT_VALUE);
207   }
208 
209   public List<String> getInAppConversationActionTypes() {
210     return getDeviceConfigStringList(
211         IN_APP_CONVERSATION_ACTION_TYPES_DEFAULT, CONVERSATION_ACTIONS_TYPES_DEFAULT_VALUES);
212   }
213 
214   public List<String> getNotificationConversationActionTypes() {
215     return getDeviceConfigStringList(
216         NOTIFICATION_CONVERSATION_ACTION_TYPES_DEFAULT, CONVERSATION_ACTIONS_TYPES_DEFAULT_VALUES);
217   }
218 
219   public float getLangIdThresholdOverride() {
220     return DeviceConfig.getFloat(
221         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
222         LANG_ID_THRESHOLD_OVERRIDE,
223         LANG_ID_THRESHOLD_OVERRIDE_DEFAULT);
224   }
225 
226   public float getTranslateActionThreshold() {
227     return DeviceConfig.getFloat(
228         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
229         TRANSLATE_ACTION_THRESHOLD,
230         TRANSLATE_ACTION_THRESHOLD_DEFAULT);
231   }
232 
233   public boolean isUserLanguageProfileEnabled() {
234     return DeviceConfig.getBoolean(
235         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
236         USER_LANGUAGE_PROFILE_ENABLED,
237         USER_LANGUAGE_PROFILE_ENABLED_DEFAULT);
238   }
239 
240   public boolean isTemplateIntentFactoryEnabled() {
241     return DeviceConfig.getBoolean(
242         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
243         TEMPLATE_INTENT_FACTORY_ENABLED,
244         TEMPLATE_INTENT_FACTORY_ENABLED_DEFAULT);
245   }
246 
247   public boolean isTranslateInClassificationEnabled() {
248     return DeviceConfig.getBoolean(
249         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
250         TRANSLATE_IN_CLASSIFICATION_ENABLED,
251         TRANSLATE_IN_CLASSIFICATION_ENABLED_DEFAULT);
252   }
253 
254   public boolean isDetectLanguagesFromTextEnabled() {
255     return DeviceConfig.getBoolean(
256         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
257         DETECT_LANGUAGES_FROM_TEXT_ENABLED,
258         DETECT_LANGUAGES_FROM_TEXT_ENABLED_DEFAULT);
259   }
260 
261   public float[] getLangIdContextSettings() {
262     return getDeviceConfigFloatArray(LANG_ID_CONTEXT_SETTINGS, LANG_ID_CONTEXT_SETTINGS_DEFAULT);
263   }
264 
265   void dump(IndentingPrintWriter pw) {
266     pw.println("TextClassifierSettings:");
267     pw.increaseIndent();
268     pw.printPair("classify_text_max_range_length", getClassifyTextMaxRangeLength());
269     pw.printPair("detect_language_from_text_enabled", isDetectLanguagesFromTextEnabled());
270     pw.printPair("entity_list_default", getEntityListDefault());
271     pw.printPair("entity_list_editable", getEntityListEditable());
272     pw.printPair("entity_list_not_editable", getEntityListNotEditable());
273     pw.printPair("generate_links_log_sample_rate", getGenerateLinksLogSampleRate());
274     pw.printPair("generate_links_max_text_length", getGenerateLinksMaxTextLength());
275     pw.printPair("in_app_conversation_action_types_default", getInAppConversationActionTypes());
276     pw.printPair("lang_id_context_settings", Arrays.toString(getLangIdContextSettings()));
277     pw.printPair("lang_id_threshold_override", getLangIdThresholdOverride());
278     pw.printPair("translate_action_threshold", getTranslateActionThreshold());
279     pw.printPair(
280         "notification_conversation_action_types_default", getNotificationConversationActionTypes());
281     pw.printPair("suggest_selection_max_range_length", getSuggestSelectionMaxRangeLength());
282     pw.printPair("user_language_profile_enabled", isUserLanguageProfileEnabled());
283     pw.printPair("template_intent_factory_enabled", isTemplateIntentFactoryEnabled());
284     pw.printPair("translate_in_classification_enabled", isTranslateInClassificationEnabled());
285     pw.decreaseIndent();
286   }
287 
288   private static List<String> getDeviceConfigStringList(String key, List<String> defaultValue) {
289     return parse(
290         DeviceConfig.getString(DeviceConfig.NAMESPACE_TEXTCLASSIFIER, key, null), defaultValue);
291   }
292 
293   private static float[] getDeviceConfigFloatArray(String key, float[] defaultValue) {
294     return parse(
295         DeviceConfig.getString(DeviceConfig.NAMESPACE_TEXTCLASSIFIER, key, null), defaultValue);
296   }
297 
298   private static List<String> parse(@Nullable String listStr, List<String> defaultValue) {
299     if (listStr != null) {
300       return Collections.unmodifiableList(Arrays.asList(listStr.split(DELIMITER)));
301     }
302     return defaultValue;
303   }
304 
305   private static float[] parse(@Nullable String arrayStr, float[] defaultValue) {
306     if (arrayStr != null) {
307       final List<String> split = Splitter.onPattern(DELIMITER).splitToList(arrayStr);
308       if (split.size() != defaultValue.length) {
309         return defaultValue;
310       }
311       final float[] result = new float[split.size()];
312       for (int i = 0; i < split.size(); i++) {
313         try {
314           result[i] = Float.parseFloat(split.get(i));
315         } catch (NumberFormatException e) {
316           return defaultValue;
317         }
318       }
319       return result;
320     } else {
321       return defaultValue;
322     }
323   }
324 }
325