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