SelectionSessionLogger.java revision dfd3c653cef679b05a5e4f79b2e894b5b0e55c37
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
17package android.view.textclassifier;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.metrics.LogMaker;
23
24import com.android.internal.annotations.VisibleForTesting;
25import com.android.internal.logging.MetricsLogger;
26import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
27import com.android.internal.util.Preconditions;
28
29import java.text.BreakIterator;
30import java.util.List;
31import java.util.Locale;
32import java.util.Objects;
33import java.util.StringJoiner;
34
35/**
36 * A helper for logging selection session events.
37 * @hide
38 */
39public 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
63    public SelectionSessionLogger() {
64        mMetricsLogger = new MetricsLogger();
65    }
66
67    @VisibleForTesting
68    public SelectionSessionLogger(@NonNull MetricsLogger metricsLogger) {
69        mMetricsLogger = Preconditions.checkNotNull(metricsLogger);
70    }
71
72    /** Emits a selection event to the logs. */
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
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
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
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
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
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     */
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     */
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
263        static String createSignature(String classifierId, String modelName, int hash) {
264            return String.format(Locale.US, "%s|%s|%d", classifierId, modelName, hash);
265        }
266
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
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
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