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