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 package android.view.textclassifier;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.metrics.LogMaker;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 import com.android.internal.logging.MetricsLogger;
26 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
27 import com.android.internal.util.Preconditions;
28 
29 import java.text.BreakIterator;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Objects;
33 import java.util.StringJoiner;
34 
35 /**
36  * A helper for logging selection session events.
37  * @hide
38  */
39 public final class SelectionSessionLogger {
40 
41     private static final String LOG_TAG = "SelectionSessionLogger";
42     static final String CLASSIFIER_ID = "androidtc";
43 
44     private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START;
45     private static final int PREV_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_PREVIOUS;
46     private static final int INDEX = MetricsEvent.FIELD_SELECTION_SESSION_INDEX;
47     private static final int WIDGET_TYPE = MetricsEvent.FIELD_SELECTION_WIDGET_TYPE;
48     private static final int WIDGET_VERSION = MetricsEvent.FIELD_SELECTION_WIDGET_VERSION;
49     private static final int MODEL_NAME = MetricsEvent.FIELD_TEXTCLASSIFIER_MODEL;
50     private static final int ENTITY_TYPE = MetricsEvent.FIELD_SELECTION_ENTITY_TYPE;
51     private static final int SMART_START = MetricsEvent.FIELD_SELECTION_SMART_RANGE_START;
52     private static final int SMART_END = MetricsEvent.FIELD_SELECTION_SMART_RANGE_END;
53     private static final int EVENT_START = MetricsEvent.FIELD_SELECTION_RANGE_START;
54     private static final int EVENT_END = MetricsEvent.FIELD_SELECTION_RANGE_END;
55     private static final int SESSION_ID = MetricsEvent.FIELD_SELECTION_SESSION_ID;
56 
57     private static final String ZERO = "0";
58     private static final String UNKNOWN = "unknown";
59 
60     private final MetricsLogger mMetricsLogger;
61 
SelectionSessionLogger()62     public SelectionSessionLogger() {
63         mMetricsLogger = new MetricsLogger();
64     }
65 
66     @VisibleForTesting
SelectionSessionLogger(@onNull MetricsLogger metricsLogger)67     public SelectionSessionLogger(@NonNull MetricsLogger metricsLogger) {
68         mMetricsLogger = Preconditions.checkNotNull(metricsLogger);
69     }
70 
71     /** Emits a selection event to the logs. */
writeEvent(@onNull SelectionEvent event)72     public void writeEvent(@NonNull SelectionEvent event) {
73         Preconditions.checkNotNull(event);
74         final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION)
75                 .setType(getLogType(event))
76                 .setSubtype(getLogSubType(event))
77                 .setPackageName(event.getPackageName())
78                 .addTaggedData(START_EVENT_DELTA, event.getDurationSinceSessionStart())
79                 .addTaggedData(PREV_EVENT_DELTA, event.getDurationSincePreviousEvent())
80                 .addTaggedData(INDEX, event.getEventIndex())
81                 .addTaggedData(WIDGET_TYPE, event.getWidgetType())
82                 .addTaggedData(WIDGET_VERSION, event.getWidgetVersion())
83                 .addTaggedData(ENTITY_TYPE, event.getEntityType())
84                 .addTaggedData(EVENT_START, event.getStart())
85                 .addTaggedData(EVENT_END, event.getEnd());
86         if (isPlatformLocalTextClassifierSmartSelection(event.getResultId())) {
87             // Ensure result id and smart indices are only set for events with smart selection from
88             // the platform's textclassifier.
89             log.addTaggedData(MODEL_NAME, SignatureParser.getModelName(event.getResultId()))
90                     .addTaggedData(SMART_START, event.getSmartStart())
91                     .addTaggedData(SMART_END, event.getSmartEnd());
92         }
93         if (event.getSessionId() != null) {
94             log.addTaggedData(SESSION_ID, event.getSessionId().flattenToString());
95         }
96         mMetricsLogger.write(log);
97         debugLog(log);
98     }
99 
getLogType(SelectionEvent event)100     private static int getLogType(SelectionEvent event) {
101         switch (event.getEventType()) {
102             case SelectionEvent.ACTION_OVERTYPE:
103                 return MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE;
104             case SelectionEvent.ACTION_COPY:
105                 return MetricsEvent.ACTION_TEXT_SELECTION_COPY;
106             case SelectionEvent.ACTION_PASTE:
107                 return MetricsEvent.ACTION_TEXT_SELECTION_PASTE;
108             case SelectionEvent.ACTION_CUT:
109                 return MetricsEvent.ACTION_TEXT_SELECTION_CUT;
110             case SelectionEvent.ACTION_SHARE:
111                 return MetricsEvent.ACTION_TEXT_SELECTION_SHARE;
112             case SelectionEvent.ACTION_SMART_SHARE:
113                 return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE;
114             case SelectionEvent.ACTION_DRAG:
115                 return MetricsEvent.ACTION_TEXT_SELECTION_DRAG;
116             case SelectionEvent.ACTION_ABANDON:
117                 return MetricsEvent.ACTION_TEXT_SELECTION_ABANDON;
118             case SelectionEvent.ACTION_OTHER:
119                 return MetricsEvent.ACTION_TEXT_SELECTION_OTHER;
120             case SelectionEvent.ACTION_SELECT_ALL:
121                 return MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL;
122             case SelectionEvent.ACTION_RESET:
123                 return MetricsEvent.ACTION_TEXT_SELECTION_RESET;
124             case SelectionEvent.EVENT_SELECTION_STARTED:
125                 return MetricsEvent.ACTION_TEXT_SELECTION_START;
126             case SelectionEvent.EVENT_SELECTION_MODIFIED:
127                 return MetricsEvent.ACTION_TEXT_SELECTION_MODIFY;
128             case SelectionEvent.EVENT_SMART_SELECTION_SINGLE:
129                 return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE;
130             case SelectionEvent.EVENT_SMART_SELECTION_MULTI:
131                 return MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI;
132             case SelectionEvent.EVENT_AUTO_SELECTION:
133                 return MetricsEvent.ACTION_TEXT_SELECTION_AUTO;
134             default:
135                 return MetricsEvent.VIEW_UNKNOWN;
136         }
137     }
138 
getLogSubType(SelectionEvent event)139     private static int getLogSubType(SelectionEvent event) {
140         switch (event.getInvocationMethod()) {
141             case SelectionEvent.INVOCATION_MANUAL:
142                 return MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL;
143             case SelectionEvent.INVOCATION_LINK:
144                 return MetricsEvent.TEXT_SELECTION_INVOCATION_LINK;
145             default:
146                 return MetricsEvent.TEXT_SELECTION_INVOCATION_UNKNOWN;
147         }
148     }
149 
getLogTypeString(int logType)150     private static String getLogTypeString(int logType) {
151         switch (logType) {
152             case MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE:
153                 return "OVERTYPE";
154             case MetricsEvent.ACTION_TEXT_SELECTION_COPY:
155                 return "COPY";
156             case MetricsEvent.ACTION_TEXT_SELECTION_PASTE:
157                 return "PASTE";
158             case MetricsEvent.ACTION_TEXT_SELECTION_CUT:
159                 return "CUT";
160             case MetricsEvent.ACTION_TEXT_SELECTION_SHARE:
161                 return "SHARE";
162             case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE:
163                 return "SMART_SHARE";
164             case MetricsEvent.ACTION_TEXT_SELECTION_DRAG:
165                 return "DRAG";
166             case MetricsEvent.ACTION_TEXT_SELECTION_ABANDON:
167                 return "ABANDON";
168             case MetricsEvent.ACTION_TEXT_SELECTION_OTHER:
169                 return "OTHER";
170             case MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL:
171                 return "SELECT_ALL";
172             case MetricsEvent.ACTION_TEXT_SELECTION_RESET:
173                 return "RESET";
174             case MetricsEvent.ACTION_TEXT_SELECTION_START:
175                 return "SELECTION_STARTED";
176             case MetricsEvent.ACTION_TEXT_SELECTION_MODIFY:
177                 return "SELECTION_MODIFIED";
178             case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE:
179                 return "SMART_SELECTION_SINGLE";
180             case MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI:
181                 return "SMART_SELECTION_MULTI";
182             case MetricsEvent.ACTION_TEXT_SELECTION_AUTO:
183                 return "AUTO_SELECTION";
184             default:
185                 return UNKNOWN;
186         }
187     }
188 
getLogSubTypeString(int logSubType)189     private static String getLogSubTypeString(int logSubType) {
190         switch (logSubType) {
191             case MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL:
192                 return "MANUAL";
193             case MetricsEvent.TEXT_SELECTION_INVOCATION_LINK:
194                 return "LINK";
195             default:
196                 return UNKNOWN;
197         }
198     }
199 
isPlatformLocalTextClassifierSmartSelection(String signature)200     static boolean isPlatformLocalTextClassifierSmartSelection(String signature) {
201         return SelectionSessionLogger.CLASSIFIER_ID.equals(
202                 SelectionSessionLogger.SignatureParser.getClassifierId(signature));
203     }
204 
debugLog(LogMaker log)205     private static void debugLog(LogMaker log) {
206         if (!Log.ENABLE_FULL_LOGGING) {
207             return;
208         }
209         final String widgetType = Objects.toString(log.getTaggedData(WIDGET_TYPE), UNKNOWN);
210         final String widgetVersion = Objects.toString(log.getTaggedData(WIDGET_VERSION), "");
211         final String widget = widgetVersion.isEmpty()
212                 ? widgetType : widgetType + "-" + widgetVersion;
213         final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO));
214         if (log.getType() == MetricsEvent.ACTION_TEXT_SELECTION_START) {
215             String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), "");
216             sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1);
217             Log.d(LOG_TAG, String.format("New selection session: %s (%s)", widget, sessionId));
218         }
219 
220         final String model = Objects.toString(log.getTaggedData(MODEL_NAME), UNKNOWN);
221         final String entity = Objects.toString(log.getTaggedData(ENTITY_TYPE), UNKNOWN);
222         final String type = getLogTypeString(log.getType());
223         final String subType = getLogSubTypeString(log.getSubtype());
224         final int smartStart = Integer.parseInt(
225                 Objects.toString(log.getTaggedData(SMART_START), ZERO));
226         final int smartEnd = Integer.parseInt(
227                 Objects.toString(log.getTaggedData(SMART_END), ZERO));
228         final int eventStart = Integer.parseInt(
229                 Objects.toString(log.getTaggedData(EVENT_START), ZERO));
230         final int eventEnd = Integer.parseInt(
231                 Objects.toString(log.getTaggedData(EVENT_END), ZERO));
232 
233         Log.v(LOG_TAG,
234                 String.format(Locale.US, "%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)",
235                         index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd,
236                         widget, model));
237     }
238 
239     /**
240      * Returns a token iterator for tokenizing text for logging purposes.
241      */
getTokenIterator(@onNull Locale locale)242     public static BreakIterator getTokenIterator(@NonNull Locale locale) {
243         return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale));
244     }
245 
246     /**
247      * Creates a string id that may be used to identify a TextClassifier result.
248      */
createId( String text, int start, int end, Context context, int modelVersion, List<Locale> locales)249     public static String createId(
250             String text, int start, int end, Context context, int modelVersion,
251             List<Locale> locales) {
252         Preconditions.checkNotNull(text);
253         Preconditions.checkNotNull(context);
254         Preconditions.checkNotNull(locales);
255         final StringJoiner localesJoiner = new StringJoiner(",");
256         for (Locale locale : locales) {
257             localesJoiner.add(locale.toLanguageTag());
258         }
259         final String modelName = String.format(Locale.US, "%s_v%d", localesJoiner.toString(),
260                 modelVersion);
261         final int hash = Objects.hash(text, start, end, context.getPackageName());
262         return SignatureParser.createSignature(CLASSIFIER_ID, modelName, hash);
263     }
264 
265     /**
266      * Helper for creating and parsing string ids for
267      * {@link android.view.textclassifier.TextClassifierImpl}.
268      */
269     @VisibleForTesting
270     public static final class SignatureParser {
271 
createSignature(String classifierId, String modelName, int hash)272         static String createSignature(String classifierId, String modelName, int hash) {
273             return String.format(Locale.US, "%s|%s|%d", classifierId, modelName, hash);
274         }
275 
getClassifierId(@ullable String signature)276         static String getClassifierId(@Nullable String signature) {
277             if (signature == null) {
278                 return "";
279             }
280             final int end = signature.indexOf("|");
281             if (end >= 0) {
282                 return signature.substring(0, end);
283             }
284             return "";
285         }
286 
getModelName(@ullable String signature)287         static String getModelName(@Nullable String signature) {
288             if (signature == null) {
289                 return "";
290             }
291             final int start = signature.indexOf("|") + 1;
292             final int end = signature.indexOf("|", start);
293             if (start >= 1 && end >= start) {
294                 return signature.substring(start, end);
295             }
296             return "";
297         }
298 
getHash(@ullable String signature)299         static int getHash(@Nullable String signature) {
300             if (signature == null) {
301                 return 0;
302             }
303             final int index1 = signature.indexOf("|");
304             final int index2 = signature.indexOf("|", index1);
305             if (index2 > 0) {
306                 return Integer.parseInt(signature.substring(index2));
307             }
308             return 0;
309         }
310     }
311 }
312