1 /* 2 * Copyright (C) 2018 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 com.android.textclassifier.common.statsd; 18 19 import static com.google.common.base.Charsets.UTF_8; 20 import static com.google.common.base.Strings.nullToEmpty; 21 22 import android.util.StatsEvent; 23 import android.util.StatsLog; 24 import android.view.textclassifier.TextClassifier; 25 import com.android.textclassifier.common.base.TcLog; 26 import com.android.textclassifier.common.logging.ResultIdUtils; 27 import com.android.textclassifier.common.logging.TextClassificationContext; 28 import com.android.textclassifier.common.logging.TextClassificationSessionId; 29 import com.android.textclassifier.common.logging.TextClassifierEvent; 30 import com.google.common.base.Preconditions; 31 import com.google.common.collect.ImmutableList; 32 import com.google.common.hash.Hashing; 33 import java.util.List; 34 import javax.annotation.Nullable; 35 36 /** Logs {@link android.view.textclassifier.TextClassifierEvent}. */ 37 public final class TextClassifierEventLogger { 38 private static final String TAG = "TCEventLogger"; 39 // These constants are defined in atoms.proto. 40 private static final int TEXT_SELECTION_EVENT_ATOM_ID = 219; 41 static final int TEXT_LINKIFY_EVENT_ATOM_ID = 220; 42 private static final int CONVERSATION_ACTIONS_EVENT_ATOM_ID = 221; 43 private static final int LANGUAGE_DETECTION_EVENT_ATOM_ID = 222; 44 45 /** Emits a text classifier event to the logs. */ writeEvent( @ullable TextClassificationSessionId sessionId, TextClassifierEvent event)46 public void writeEvent( 47 @Nullable TextClassificationSessionId sessionId, TextClassifierEvent event) { 48 Preconditions.checkNotNull(event); 49 if (TcLog.ENABLE_FULL_LOGGING) { 50 TcLog.v( 51 TAG, 52 String.format( 53 "TextClassifierEventLogger.writeEvent: sessionId=%s,event=%s", sessionId, event)); 54 } 55 if (event instanceof TextClassifierEvent.TextSelectionEvent) { 56 logTextSelectionEvent(sessionId, (TextClassifierEvent.TextSelectionEvent) event); 57 } else if (event instanceof TextClassifierEvent.TextLinkifyEvent) { 58 logTextLinkifyEvent(sessionId, (TextClassifierEvent.TextLinkifyEvent) event); 59 } else if (event instanceof TextClassifierEvent.ConversationActionsEvent) { 60 logConversationActionsEvent(sessionId, (TextClassifierEvent.ConversationActionsEvent) event); 61 } else if (event instanceof TextClassifierEvent.LanguageDetectionEvent) { 62 logLanguageDetectionEvent(sessionId, (TextClassifierEvent.LanguageDetectionEvent) event); 63 } else { 64 TcLog.w(TAG, "Unexpected events, category=" + event.getEventCategory()); 65 } 66 } 67 logTextSelectionEvent( @ullable TextClassificationSessionId sessionId, TextClassifierEvent.TextSelectionEvent event)68 private static void logTextSelectionEvent( 69 @Nullable TextClassificationSessionId sessionId, 70 TextClassifierEvent.TextSelectionEvent event) { 71 ImmutableList<String> modelNames = getModelNames(event); 72 StatsEvent statsEvent = 73 StatsEvent.newBuilder() 74 .setAtomId(TEXT_SELECTION_EVENT_ATOM_ID) 75 .writeString(sessionId == null ? null : sessionId.getValue()) 76 .writeInt(getEventType(event)) 77 .writeString(getItemAt(modelNames, /* index= */ 0, /* defaultValue= */ null)) 78 .writeInt(getWidgetType(event)) 79 .writeInt(event.getEventIndex()) 80 .writeString(getItemAt(event.getEntityTypes(), /* index= */ 0)) 81 .writeInt(event.getRelativeWordStartIndex()) 82 .writeInt(event.getRelativeWordEndIndex()) 83 .writeInt(event.getRelativeSuggestedWordStartIndex()) 84 .writeInt(event.getRelativeSuggestedWordEndIndex()) 85 .writeString(getPackageName(event)) 86 .writeString(getItemAt(modelNames, /* index= */ 1, /* defaultValue= */ null)) 87 .usePooledBuffer() 88 .build(); 89 StatsLog.write(statsEvent); 90 } 91 getEventType(TextClassifierEvent.TextSelectionEvent event)92 private static int getEventType(TextClassifierEvent.TextSelectionEvent event) { 93 if (event.getEventType() == TextClassifierEvent.TYPE_AUTO_SELECTION) { 94 if (ResultIdUtils.isFromDefaultTextClassifier(event.getResultId())) { 95 return event.getRelativeWordEndIndex() - event.getRelativeWordStartIndex() > 1 96 ? TextClassifierEvent.TYPE_SMART_SELECTION_MULTI 97 : TextClassifierEvent.TYPE_SMART_SELECTION_SINGLE; 98 } 99 } 100 return event.getEventType(); 101 } 102 logTextLinkifyEvent( TextClassificationSessionId sessionId, TextClassifierEvent.TextLinkifyEvent event)103 private static void logTextLinkifyEvent( 104 TextClassificationSessionId sessionId, TextClassifierEvent.TextLinkifyEvent event) { 105 ImmutableList<String> modelNames = getModelNames(event); 106 StatsEvent statsEvent = 107 StatsEvent.newBuilder() 108 .setAtomId(TEXT_LINKIFY_EVENT_ATOM_ID) 109 .writeString(sessionId == null ? null : sessionId.getValue()) 110 .writeInt(event.getEventType()) 111 .writeString(getItemAt(modelNames, /* index= */ 0, /* defaultValue= */ null)) 112 .writeInt(getWidgetType(event)) 113 .writeInt(event.getEventIndex()) 114 .writeString(getItemAt(event.getEntityTypes(), /* index= */ 0)) 115 .writeInt(/* numOfLinks */ 0) 116 .writeInt(/* linkedTextLength */ 0) 117 .writeInt(/* textLength */ 0) 118 .writeLong(/* latencyInMillis */ 0L) 119 .writeString(getPackageName(event)) 120 .writeString(getItemAt(modelNames, /* index= */ 1, /* defaultValue= */ null)) 121 .usePooledBuffer() 122 .build(); 123 StatsLog.write(statsEvent); 124 } 125 logConversationActionsEvent( @ullable TextClassificationSessionId sessionId, TextClassifierEvent.ConversationActionsEvent event)126 private static void logConversationActionsEvent( 127 @Nullable TextClassificationSessionId sessionId, 128 TextClassifierEvent.ConversationActionsEvent event) { 129 String resultId = nullToEmpty(event.getResultId()); 130 ImmutableList<String> modelNames = ResultIdUtils.getModelNames(resultId); 131 StatsEvent statsEvent = 132 StatsEvent.newBuilder() 133 .setAtomId(CONVERSATION_ACTIONS_EVENT_ATOM_ID) 134 // TODO: Update ExtServices to set the session id. 135 .writeString( 136 sessionId == null 137 ? Hashing.goodFastHash(64).hashString(resultId, UTF_8).toString() 138 : sessionId.getValue()) 139 .writeInt(event.getEventType()) 140 .writeString(getItemAt(modelNames, /* index= */ 0, /* defaultValue= */ null)) 141 .writeInt(getWidgetType(event)) 142 .writeString(getItemAt(event.getEntityTypes(), /* index= */ 0)) 143 .writeString(getItemAt(event.getEntityTypes(), /* index= */ 1)) 144 .writeString(getItemAt(event.getEntityTypes(), /* index= */ 2)) 145 .writeFloat(getFloatAt(event.getScores(), /* index= */ 0)) 146 .writeString(getPackageName(event)) 147 .writeString(getItemAt(modelNames, /* index= */ 1, /* defaultValue= */ null)) 148 .writeString(getItemAt(modelNames, /* index= */ 2, /* defaultValue= */ null)) 149 .usePooledBuffer() 150 .build(); 151 StatsLog.write(statsEvent); 152 } 153 logLanguageDetectionEvent( @ullable TextClassificationSessionId sessionId, TextClassifierEvent.LanguageDetectionEvent event)154 private static void logLanguageDetectionEvent( 155 @Nullable TextClassificationSessionId sessionId, 156 TextClassifierEvent.LanguageDetectionEvent event) { 157 StatsEvent statsEvent = 158 StatsEvent.newBuilder() 159 .setAtomId(LANGUAGE_DETECTION_EVENT_ATOM_ID) 160 .writeString(sessionId == null ? null : sessionId.getValue()) 161 .writeInt(event.getEventType()) 162 .writeString(getItemAt(getModelNames(event), /* index= */ 0, /* defaultValue= */ null)) 163 .writeInt(getWidgetType(event)) 164 .writeString(getItemAt(event.getEntityTypes(), /* index= */ 0)) 165 .writeFloat(getFloatAt(event.getScores(), /* index= */ 0)) 166 .writeInt(getIntAt(event.getActionIndices(), /* index= */ 0)) 167 .writeString(getPackageName(event)) 168 .usePooledBuffer() 169 .build(); 170 StatsLog.write(statsEvent); 171 } 172 173 @Nullable getItemAt(List<T> list, int index, T defaultValue)174 private static <T> T getItemAt(List<T> list, int index, T defaultValue) { 175 if (list == null) { 176 return defaultValue; 177 } 178 if (index >= list.size()) { 179 return defaultValue; 180 } 181 return list.get(index); 182 } 183 184 @Nullable getItemAt(@ullable T[] array, int index)185 private static <T> T getItemAt(@Nullable T[] array, int index) { 186 if (array == null) { 187 return null; 188 } 189 if (index >= array.length) { 190 return null; 191 } 192 return array[index]; 193 } 194 getFloatAt(@ullable float[] array, int index)195 private static float getFloatAt(@Nullable float[] array, int index) { 196 if (array == null) { 197 return 0f; 198 } 199 if (index >= array.length) { 200 return 0f; 201 } 202 return array[index]; 203 } 204 getIntAt(@ullable int[] array, int index)205 private static int getIntAt(@Nullable int[] array, int index) { 206 if (array == null) { 207 return 0; 208 } 209 if (index >= array.length) { 210 return 0; 211 } 212 return array[index]; 213 } 214 getModelNames(TextClassifierEvent event)215 private static ImmutableList<String> getModelNames(TextClassifierEvent event) { 216 if (event.getModelName() != null) { 217 return ImmutableList.of(event.getModelName()); 218 } 219 return ResultIdUtils.getModelNames(event.getResultId()); 220 } 221 222 @Nullable getPackageName(TextClassifierEvent event)223 private static String getPackageName(TextClassifierEvent event) { 224 TextClassificationContext eventContext = event.getEventContext(); 225 if (eventContext == null) { 226 return null; 227 } 228 return eventContext.getPackageName(); 229 } 230 getWidgetType(TextClassifierEvent event)231 private static int getWidgetType(TextClassifierEvent event) { 232 TextClassificationContext eventContext = event.getEventContext(); 233 if (eventContext == null) { 234 return WidgetType.WIDGET_TYPE_UNKNOWN; 235 } 236 switch (eventContext.getWidgetType()) { 237 case TextClassifier.WIDGET_TYPE_UNKNOWN: 238 return WidgetType.WIDGET_TYPE_UNKNOWN; 239 case TextClassifier.WIDGET_TYPE_TEXTVIEW: 240 return WidgetType.WIDGET_TYPE_TEXTVIEW; 241 case TextClassifier.WIDGET_TYPE_EDITTEXT: 242 return WidgetType.WIDGET_TYPE_EDITTEXT; 243 case TextClassifier.WIDGET_TYPE_UNSELECTABLE_TEXTVIEW: 244 return WidgetType.WIDGET_TYPE_UNSELECTABLE_TEXTVIEW; 245 case TextClassifier.WIDGET_TYPE_WEBVIEW: 246 return WidgetType.WIDGET_TYPE_WEBVIEW; 247 case TextClassifier.WIDGET_TYPE_EDIT_WEBVIEW: 248 return WidgetType.WIDGET_TYPE_EDIT_WEBVIEW; 249 case TextClassifier.WIDGET_TYPE_CUSTOM_TEXTVIEW: 250 return WidgetType.WIDGET_TYPE_CUSTOM_TEXTVIEW; 251 case TextClassifier.WIDGET_TYPE_CUSTOM_EDITTEXT: 252 return WidgetType.WIDGET_TYPE_CUSTOM_EDITTEXT; 253 case TextClassifier.WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW: 254 return WidgetType.WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW; 255 case TextClassifier.WIDGET_TYPE_NOTIFICATION: 256 return WidgetType.WIDGET_TYPE_NOTIFICATION; 257 default: // fall out 258 } 259 return WidgetType.WIDGET_TYPE_UNKNOWN; 260 } 261 262 /** Widget type constants for logging. */ 263 public static final class WidgetType { 264 // Sync these constants with textclassifier_enums.proto. 265 public static final int WIDGET_TYPE_UNKNOWN = 0; 266 public static final int WIDGET_TYPE_TEXTVIEW = 1; 267 public static final int WIDGET_TYPE_EDITTEXT = 2; 268 public static final int WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = 3; 269 public static final int WIDGET_TYPE_WEBVIEW = 4; 270 public static final int WIDGET_TYPE_EDIT_WEBVIEW = 5; 271 public static final int WIDGET_TYPE_CUSTOM_TEXTVIEW = 6; 272 public static final int WIDGET_TYPE_CUSTOM_EDITTEXT = 7; 273 public static final int WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = 8; 274 public static final int WIDGET_TYPE_NOTIFICATION = 9; 275 WidgetType()276 private WidgetType() {} 277 } 278 } 279