/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.view.textclassifier;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.metrics.LogMaker;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.Preconditions;

import java.text.BreakIterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.StringJoiner;

A helper for logging selection session events.
@hide
/** * A helper for logging selection session events. * @hide */
public final class SelectionSessionLogger { private static final String LOG_TAG = "SelectionSessionLogger"; private static final boolean DEBUG_LOG_ENABLED = false; static final String CLASSIFIER_ID = "androidtc"; private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START; private static final int PREV_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_PREVIOUS; private static final int INDEX = MetricsEvent.FIELD_SELECTION_SESSION_INDEX; private static final int WIDGET_TYPE = MetricsEvent.FIELD_SELECTION_WIDGET_TYPE; private static final int WIDGET_VERSION = MetricsEvent.FIELD_SELECTION_WIDGET_VERSION; private static final int MODEL_NAME = MetricsEvent.FIELD_TEXTCLASSIFIER_MODEL; private static final int ENTITY_TYPE = MetricsEvent.FIELD_SELECTION_ENTITY_TYPE; private static final int SMART_START = MetricsEvent.FIELD_SELECTION_SMART_RANGE_START; private static final int SMART_END = MetricsEvent.FIELD_SELECTION_SMART_RANGE_END; private static final int EVENT_START = MetricsEvent.FIELD_SELECTION_RANGE_START; private static final int EVENT_END = MetricsEvent.FIELD_SELECTION_RANGE_END; private static final int SESSION_ID = MetricsEvent.FIELD_SELECTION_SESSION_ID; private static final String ZERO = "0"; private static final String UNKNOWN = "unknown"; private final MetricsLogger mMetricsLogger; public SelectionSessionLogger() { mMetricsLogger = new MetricsLogger(); } @VisibleForTesting public SelectionSessionLogger(@NonNull MetricsLogger metricsLogger) { mMetricsLogger = Preconditions.checkNotNull(metricsLogger); }
Emits a selection event to the logs.
/** Emits a selection event to the logs. */
public void writeEvent(@NonNull SelectionEvent event) { Preconditions.checkNotNull(event); final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION) .setType(getLogType(event)) .setSubtype(getLogSubType(event)) .setPackageName(event.getPackageName()) .addTaggedData(START_EVENT_DELTA, event.getDurationSinceSessionStart()) .addTaggedData(PREV_EVENT_DELTA, event.getDurationSincePreviousEvent()) .addTaggedData(INDEX, event.getEventIndex()) .addTaggedData(WIDGET_TYPE, event.getWidgetType()) .addTaggedData(WIDGET_VERSION, event.getWidgetVersion()) .addTaggedData(MODEL_NAME, SignatureParser.getModelName(event.getResultId())) .addTaggedData(ENTITY_TYPE, event.getEntityType()) .addTaggedData(SMART_START, event.getSmartStart()) .addTaggedData(SMART_END, event.getSmartEnd()) .addTaggedData(EVENT_START, event.getStart()) .addTaggedData(EVENT_END, event.getEnd()); if (event.getSessionId() != null) { log.addTaggedData(SESSION_ID, event.getSessionId().flattenToString()); } mMetricsLogger.write(log); debugLog(log); } private static int getLogType(SelectionEvent event) { switch (event.getEventType()) { case SelectionEvent.ACTION_OVERTYPE: return MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE; case SelectionEvent.ACTION_COPY: return MetricsEvent.ACTION_TEXT_SELECTION_COPY; case SelectionEvent.ACTION_PASTE: return MetricsEvent.ACTION_TEXT_SELECTION_PASTE; case SelectionEvent.ACTION_CUT: return MetricsEvent.ACTION_TEXT_SELECTION_CUT; case SelectionEvent.ACTION_SHARE: return MetricsEvent.ACTION_TEXT_SELECTION_SHARE; case SelectionEvent.ACTION_SMART_SHARE: return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE; case SelectionEvent.ACTION_DRAG: return MetricsEvent.ACTION_TEXT_SELECTION_DRAG; case SelectionEvent.ACTION_ABANDON: return MetricsEvent.ACTION_TEXT_SELECTION_ABANDON; case SelectionEvent.ACTION_OTHER: return MetricsEvent.ACTION_TEXT_SELECTION_OTHER; case SelectionEvent.ACTION_SELECT_ALL: return MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL; case SelectionEvent.ACTION_RESET: return MetricsEvent.ACTION_TEXT_SELECTION_RESET; case SelectionEvent.EVENT_SELECTION_STARTED: return MetricsEvent.ACTION_TEXT_SELECTION_START; case SelectionEvent.EVENT_SELECTION_MODIFIED: return MetricsEvent.ACTION_TEXT_SELECTION_MODIFY; case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE; case SelectionEvent.EVENT_SMART_SELECTION_MULTI: return MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI; case SelectionEvent.EVENT_AUTO_SELECTION: return MetricsEvent.ACTION_TEXT_SELECTION_AUTO; default: return MetricsEvent.VIEW_UNKNOWN; } } private static int getLogSubType(SelectionEvent event) { switch (event.getInvocationMethod()) { case SelectionEvent.INVOCATION_MANUAL: return MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL; case SelectionEvent.INVOCATION_LINK: return MetricsEvent.TEXT_SELECTION_INVOCATION_LINK; default: return MetricsEvent.TEXT_SELECTION_INVOCATION_UNKNOWN; } } private static String getLogTypeString(int logType) { switch (logType) { case MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE: return "OVERTYPE"; case MetricsEvent.ACTION_TEXT_SELECTION_COPY: return "COPY"; case MetricsEvent.ACTION_TEXT_SELECTION_PASTE: return "PASTE"; case MetricsEvent.ACTION_TEXT_SELECTION_CUT: return "CUT"; case MetricsEvent.ACTION_TEXT_SELECTION_SHARE: return "SHARE"; case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE: return "SMART_SHARE"; case MetricsEvent.ACTION_TEXT_SELECTION_DRAG: return "DRAG"; case MetricsEvent.ACTION_TEXT_SELECTION_ABANDON: return "ABANDON"; case MetricsEvent.ACTION_TEXT_SELECTION_OTHER: return "OTHER"; case MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL: return "SELECT_ALL"; case MetricsEvent.ACTION_TEXT_SELECTION_RESET: return "RESET"; case MetricsEvent.ACTION_TEXT_SELECTION_START: return "SELECTION_STARTED"; case MetricsEvent.ACTION_TEXT_SELECTION_MODIFY: return "SELECTION_MODIFIED"; case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE: return "SMART_SELECTION_SINGLE"; case MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI: return "SMART_SELECTION_MULTI"; case MetricsEvent.ACTION_TEXT_SELECTION_AUTO: return "AUTO_SELECTION"; default: return UNKNOWN; } } private static String getLogSubTypeString(int logSubType) { switch (logSubType) { case MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL: return "MANUAL"; case MetricsEvent.TEXT_SELECTION_INVOCATION_LINK: return "LINK"; default: return UNKNOWN; } } private static void debugLog(LogMaker log) { if (!DEBUG_LOG_ENABLED) return; final String widgetType = Objects.toString(log.getTaggedData(WIDGET_TYPE), UNKNOWN); final String widgetVersion = Objects.toString(log.getTaggedData(WIDGET_VERSION), ""); final String widget = widgetVersion.isEmpty() ? widgetType : widgetType + "-" + widgetVersion; final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO)); if (log.getType() == MetricsEvent.ACTION_TEXT_SELECTION_START) { String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), ""); sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1); Log.d(LOG_TAG, String.format("New selection session: %s (%s)", widget, sessionId)); } final String model = Objects.toString(log.getTaggedData(MODEL_NAME), UNKNOWN); final String entity = Objects.toString(log.getTaggedData(ENTITY_TYPE), UNKNOWN); final String type = getLogTypeString(log.getType()); final String subType = getLogSubTypeString(log.getSubtype()); final int smartStart = Integer.parseInt( Objects.toString(log.getTaggedData(SMART_START), ZERO)); final int smartEnd = Integer.parseInt( Objects.toString(log.getTaggedData(SMART_END), ZERO)); final int eventStart = Integer.parseInt( Objects.toString(log.getTaggedData(EVENT_START), ZERO)); final int eventEnd = Integer.parseInt( Objects.toString(log.getTaggedData(EVENT_END), ZERO)); Log.d(LOG_TAG, String.format(Locale.US, "%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)", index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd, widget, model)); }
Returns a token iterator for tokenizing text for logging purposes.
/** * Returns a token iterator for tokenizing text for logging purposes. */
public static BreakIterator getTokenIterator(@NonNull Locale locale) { return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale)); }
Creates a string id that may be used to identify a TextClassifier result.
/** * Creates a string id that may be used to identify a TextClassifier result. */
public static String createId( String text, int start, int end, Context context, int modelVersion, List<Locale> locales) { Preconditions.checkNotNull(text); Preconditions.checkNotNull(context); Preconditions.checkNotNull(locales); final StringJoiner localesJoiner = new StringJoiner(","); for (Locale locale : locales) { localesJoiner.add(locale.toLanguageTag()); } final String modelName = String.format(Locale.US, "%s_v%d", localesJoiner.toString(), modelVersion); final int hash = Objects.hash(text, start, end, context.getPackageName()); return SignatureParser.createSignature(CLASSIFIER_ID, modelName, hash); }
Helper for creating and parsing string ids for TextClassifierImpl.
/** * Helper for creating and parsing string ids for * {@link android.view.textclassifier.TextClassifierImpl}. */
@VisibleForTesting public static final class SignatureParser { static String createSignature(String classifierId, String modelName, int hash) { return String.format(Locale.US, "%s|%s|%d", classifierId, modelName, hash); } static String getClassifierId(@Nullable String signature) { if (signature == null) { return ""; } final int end = signature.indexOf("|"); if (end >= 0) { return signature.substring(0, end); } return ""; } static String getModelName(@Nullable String signature) { if (signature == null) { return ""; } final int start = signature.indexOf("|") + 1; final int end = signature.indexOf("|", start); if (start >= 1 && end >= start) { return signature.substring(start, end); } return ""; } static int getHash(@Nullable String signature) { if (signature == null) { return 0; } final int index1 = signature.indexOf("|"); final int index2 = signature.indexOf("|", index1); if (index2 > 0) { return Integer.parseInt(signature.substring(index2)); } return 0; } } }