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