Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 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.logging; |
| 18 | |
| 19 | import android.annotation.IntDef; |
| 20 | import android.annotation.NonNull; |
| 21 | import android.annotation.Nullable; |
Mathew Inwood | a570dee | 2018-08-17 14:56:00 +0100 | [diff] [blame] | 22 | import android.annotation.UnsupportedAppUsage; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 23 | import android.content.Context; |
| 24 | import android.metrics.LogMaker; |
| 25 | import android.util.Log; |
| 26 | import android.view.textclassifier.TextClassification; |
| 27 | import android.view.textclassifier.TextClassifier; |
| 28 | import android.view.textclassifier.TextSelection; |
| 29 | |
Tony Mak | b393eea | 2019-07-05 15:23:39 +0100 | [diff] [blame] | 30 | import com.android.internal.annotations.VisibleForTesting; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 31 | import com.android.internal.logging.MetricsLogger; |
| 32 | import com.android.internal.logging.nano.MetricsProto.MetricsEvent; |
| 33 | import com.android.internal.util.Preconditions; |
| 34 | |
| 35 | import java.lang.annotation.Retention; |
| 36 | import java.lang.annotation.RetentionPolicy; |
| 37 | import java.util.Objects; |
| 38 | import java.util.UUID; |
| 39 | |
| 40 | /** |
| 41 | * A selection event tracker. |
| 42 | * @hide |
| 43 | */ |
| 44 | //TODO: Do not allow any crashes from this class. |
| 45 | public final class SmartSelectionEventTracker { |
| 46 | |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 47 | private static final String LOG_TAG = "SmartSelectEventTracker"; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 48 | private static final boolean DEBUG_LOG_ENABLED = true; |
| 49 | |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 50 | private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START; |
| 51 | private static final int PREV_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_PREVIOUS; |
| 52 | private static final int INDEX = MetricsEvent.FIELD_SELECTION_SESSION_INDEX; |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 53 | private static final int WIDGET_TYPE = MetricsEvent.FIELD_SELECTION_WIDGET_TYPE; |
Jan Althaus | 5d0a14b | 2017-11-15 11:20:58 +0100 | [diff] [blame] | 54 | private static final int WIDGET_VERSION = MetricsEvent.FIELD_SELECTION_WIDGET_VERSION; |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 55 | private static final int MODEL_NAME = MetricsEvent.FIELD_TEXTCLASSIFIER_MODEL; |
| 56 | private static final int ENTITY_TYPE = MetricsEvent.FIELD_SELECTION_ENTITY_TYPE; |
| 57 | private static final int SMART_START = MetricsEvent.FIELD_SELECTION_SMART_RANGE_START; |
| 58 | private static final int SMART_END = MetricsEvent.FIELD_SELECTION_SMART_RANGE_END; |
| 59 | private static final int EVENT_START = MetricsEvent.FIELD_SELECTION_RANGE_START; |
| 60 | private static final int EVENT_END = MetricsEvent.FIELD_SELECTION_RANGE_END; |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 61 | private static final int SESSION_ID = MetricsEvent.FIELD_SELECTION_SESSION_ID; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 62 | |
| 63 | private static final String ZERO = "0"; |
| 64 | private static final String TEXTVIEW = "textview"; |
| 65 | private static final String EDITTEXT = "edittext"; |
Jan Althaus | b3c6ece | 2017-11-14 15:40:16 +0100 | [diff] [blame] | 66 | private static final String UNSELECTABLE_TEXTVIEW = "nosel-textview"; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 67 | private static final String WEBVIEW = "webview"; |
| 68 | private static final String EDIT_WEBVIEW = "edit-webview"; |
Jan Althaus | b3c6ece | 2017-11-14 15:40:16 +0100 | [diff] [blame] | 69 | private static final String CUSTOM_TEXTVIEW = "customview"; |
| 70 | private static final String CUSTOM_EDITTEXT = "customedit"; |
| 71 | private static final String CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview"; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 72 | private static final String UNKNOWN = "unknown"; |
| 73 | |
| 74 | @Retention(RetentionPolicy.SOURCE) |
| 75 | @IntDef({WidgetType.UNSPECIFIED, WidgetType.TEXTVIEW, WidgetType.WEBVIEW, |
| 76 | WidgetType.EDITTEXT, WidgetType.EDIT_WEBVIEW}) |
| 77 | public @interface WidgetType { |
Jan Althaus | b3c6ece | 2017-11-14 15:40:16 +0100 | [diff] [blame] | 78 | int UNSPECIFIED = 0; |
| 79 | int TEXTVIEW = 1; |
| 80 | int WEBVIEW = 2; |
| 81 | int EDITTEXT = 3; |
| 82 | int EDIT_WEBVIEW = 4; |
| 83 | int UNSELECTABLE_TEXTVIEW = 5; |
| 84 | int CUSTOM_TEXTVIEW = 6; |
| 85 | int CUSTOM_EDITTEXT = 7; |
| 86 | int CUSTOM_UNSELECTABLE_TEXTVIEW = 8; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 87 | } |
| 88 | |
| 89 | private final MetricsLogger mMetricsLogger = new MetricsLogger(); |
| 90 | private final int mWidgetType; |
Jan Althaus | 5d0a14b | 2017-11-15 11:20:58 +0100 | [diff] [blame] | 91 | @Nullable private final String mWidgetVersion; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 92 | private final Context mContext; |
| 93 | |
| 94 | @Nullable private String mSessionId; |
| 95 | private final int[] mSmartIndices = new int[2]; |
| 96 | private final int[] mPrevIndices = new int[2]; |
| 97 | private int mOrigStart; |
| 98 | private int mIndex; |
| 99 | private long mSessionStartTime; |
| 100 | private long mLastEventTime; |
| 101 | private boolean mSmartSelectionTriggered; |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 102 | private String mModelName; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 103 | |
Mathew Inwood | 5ca23f6 | 2019-07-03 12:20:45 +0100 | [diff] [blame] | 104 | @UnsupportedAppUsage(trackingBug = 136637107) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 105 | public SmartSelectionEventTracker(@NonNull Context context, @WidgetType int widgetType) { |
| 106 | mWidgetType = widgetType; |
Jan Althaus | 5d0a14b | 2017-11-15 11:20:58 +0100 | [diff] [blame] | 107 | mWidgetVersion = null; |
| 108 | mContext = Preconditions.checkNotNull(context); |
| 109 | } |
| 110 | |
| 111 | public SmartSelectionEventTracker( |
| 112 | @NonNull Context context, @WidgetType int widgetType, @Nullable String widgetVersion) { |
| 113 | mWidgetType = widgetType; |
| 114 | mWidgetVersion = widgetVersion; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 115 | mContext = Preconditions.checkNotNull(context); |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Logs a selection event. |
| 120 | * |
| 121 | * @param event the selection event |
| 122 | */ |
Mathew Inwood | 5ca23f6 | 2019-07-03 12:20:45 +0100 | [diff] [blame] | 123 | @UnsupportedAppUsage(trackingBug = 136637107) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 124 | public void logEvent(@NonNull SelectionEvent event) { |
| 125 | Preconditions.checkNotNull(event); |
| 126 | |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 127 | if (event.mEventType != SelectionEvent.EventType.SELECTION_STARTED && mSessionId == null |
| 128 | && DEBUG_LOG_ENABLED) { |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 129 | Log.d(LOG_TAG, "Selection session not yet started. Ignoring event"); |
| 130 | return; |
| 131 | } |
| 132 | |
| 133 | final long now = System.currentTimeMillis(); |
| 134 | switch (event.mEventType) { |
| 135 | case SelectionEvent.EventType.SELECTION_STARTED: |
| 136 | mSessionId = startNewSession(); |
| 137 | Preconditions.checkArgument(event.mEnd == event.mStart + 1); |
| 138 | mOrigStart = event.mStart; |
| 139 | mSessionStartTime = now; |
| 140 | break; |
| 141 | case SelectionEvent.EventType.SMART_SELECTION_SINGLE: // fall through |
| 142 | case SelectionEvent.EventType.SMART_SELECTION_MULTI: |
| 143 | mSmartSelectionTriggered = true; |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 144 | mModelName = getModelName(event); |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 145 | mSmartIndices[0] = event.mStart; |
| 146 | mSmartIndices[1] = event.mEnd; |
| 147 | break; |
| 148 | case SelectionEvent.EventType.SELECTION_MODIFIED: // fall through |
| 149 | case SelectionEvent.EventType.AUTO_SELECTION: |
| 150 | if (mPrevIndices[0] == event.mStart && mPrevIndices[1] == event.mEnd) { |
| 151 | // Selection did not change. Ignore event. |
| 152 | return; |
| 153 | } |
| 154 | } |
| 155 | writeEvent(event, now); |
| 156 | |
| 157 | if (event.isTerminal()) { |
| 158 | endSession(); |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | private void writeEvent(SelectionEvent event, long now) { |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 163 | final long prevEventDelta = mLastEventTime == 0 ? 0 : now - mLastEventTime; |
| 164 | final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 165 | .setType(getLogType(event)) |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 166 | .setSubtype(MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 167 | .setPackageName(mContext.getPackageName()) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 168 | .addTaggedData(START_EVENT_DELTA, now - mSessionStartTime) |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 169 | .addTaggedData(PREV_EVENT_DELTA, prevEventDelta) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 170 | .addTaggedData(INDEX, mIndex) |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 171 | .addTaggedData(WIDGET_TYPE, getWidgetTypeName()) |
Jan Althaus | 5d0a14b | 2017-11-15 11:20:58 +0100 | [diff] [blame] | 172 | .addTaggedData(WIDGET_VERSION, mWidgetVersion) |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 173 | .addTaggedData(MODEL_NAME, mModelName) |
| 174 | .addTaggedData(ENTITY_TYPE, event.mEntityType) |
| 175 | .addTaggedData(SMART_START, getSmartRangeDelta(mSmartIndices[0])) |
| 176 | .addTaggedData(SMART_END, getSmartRangeDelta(mSmartIndices[1])) |
| 177 | .addTaggedData(EVENT_START, getRangeDelta(event.mStart)) |
| 178 | .addTaggedData(EVENT_END, getRangeDelta(event.mEnd)) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 179 | .addTaggedData(SESSION_ID, mSessionId); |
| 180 | mMetricsLogger.write(log); |
| 181 | debugLog(log); |
| 182 | mLastEventTime = now; |
| 183 | mPrevIndices[0] = event.mStart; |
| 184 | mPrevIndices[1] = event.mEnd; |
| 185 | mIndex++; |
| 186 | } |
| 187 | |
| 188 | private String startNewSession() { |
| 189 | endSession(); |
| 190 | mSessionId = createSessionId(); |
| 191 | return mSessionId; |
| 192 | } |
| 193 | |
| 194 | private void endSession() { |
| 195 | // Reset fields. |
| 196 | mOrigStart = 0; |
| 197 | mSmartIndices[0] = mSmartIndices[1] = 0; |
| 198 | mPrevIndices[0] = mPrevIndices[1] = 0; |
| 199 | mIndex = 0; |
| 200 | mSessionStartTime = 0; |
| 201 | mLastEventTime = 0; |
| 202 | mSmartSelectionTriggered = false; |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 203 | mModelName = getModelName(null); |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 204 | mSessionId = null; |
| 205 | } |
| 206 | |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 207 | private static int getLogType(SelectionEvent event) { |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 208 | switch (event.mEventType) { |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 209 | case SelectionEvent.ActionType.OVERTYPE: |
| 210 | return MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE; |
| 211 | case SelectionEvent.ActionType.COPY: |
| 212 | return MetricsEvent.ACTION_TEXT_SELECTION_COPY; |
| 213 | case SelectionEvent.ActionType.PASTE: |
| 214 | return MetricsEvent.ACTION_TEXT_SELECTION_PASTE; |
| 215 | case SelectionEvent.ActionType.CUT: |
| 216 | return MetricsEvent.ACTION_TEXT_SELECTION_CUT; |
| 217 | case SelectionEvent.ActionType.SHARE: |
| 218 | return MetricsEvent.ACTION_TEXT_SELECTION_SHARE; |
| 219 | case SelectionEvent.ActionType.SMART_SHARE: |
| 220 | return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE; |
| 221 | case SelectionEvent.ActionType.DRAG: |
| 222 | return MetricsEvent.ACTION_TEXT_SELECTION_DRAG; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 223 | case SelectionEvent.ActionType.ABANDON: |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 224 | return MetricsEvent.ACTION_TEXT_SELECTION_ABANDON; |
| 225 | case SelectionEvent.ActionType.OTHER: |
| 226 | return MetricsEvent.ACTION_TEXT_SELECTION_OTHER; |
| 227 | case SelectionEvent.ActionType.SELECT_ALL: |
| 228 | return MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL; |
| 229 | case SelectionEvent.ActionType.RESET: |
| 230 | return MetricsEvent.ACTION_TEXT_SELECTION_RESET; |
| 231 | case SelectionEvent.EventType.SELECTION_STARTED: |
| 232 | return MetricsEvent.ACTION_TEXT_SELECTION_START; |
| 233 | case SelectionEvent.EventType.SELECTION_MODIFIED: |
| 234 | return MetricsEvent.ACTION_TEXT_SELECTION_MODIFY; |
| 235 | case SelectionEvent.EventType.SMART_SELECTION_SINGLE: |
| 236 | return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE; |
| 237 | case SelectionEvent.EventType.SMART_SELECTION_MULTI: |
| 238 | return MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI; |
| 239 | case SelectionEvent.EventType.AUTO_SELECTION: |
| 240 | return MetricsEvent.ACTION_TEXT_SELECTION_AUTO; |
| 241 | default: |
| 242 | return MetricsEvent.VIEW_UNKNOWN; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 243 | } |
| 244 | } |
| 245 | |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 246 | private static String getLogTypeString(int logType) { |
| 247 | switch (logType) { |
| 248 | case MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE: |
| 249 | return "OVERTYPE"; |
| 250 | case MetricsEvent.ACTION_TEXT_SELECTION_COPY: |
| 251 | return "COPY"; |
| 252 | case MetricsEvent.ACTION_TEXT_SELECTION_PASTE: |
| 253 | return "PASTE"; |
| 254 | case MetricsEvent.ACTION_TEXT_SELECTION_CUT: |
| 255 | return "CUT"; |
| 256 | case MetricsEvent.ACTION_TEXT_SELECTION_SHARE: |
| 257 | return "SHARE"; |
| 258 | case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE: |
| 259 | return "SMART_SHARE"; |
| 260 | case MetricsEvent.ACTION_TEXT_SELECTION_DRAG: |
| 261 | return "DRAG"; |
| 262 | case MetricsEvent.ACTION_TEXT_SELECTION_ABANDON: |
| 263 | return "ABANDON"; |
| 264 | case MetricsEvent.ACTION_TEXT_SELECTION_OTHER: |
| 265 | return "OTHER"; |
| 266 | case MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL: |
| 267 | return "SELECT_ALL"; |
| 268 | case MetricsEvent.ACTION_TEXT_SELECTION_RESET: |
| 269 | return "RESET"; |
| 270 | case MetricsEvent.ACTION_TEXT_SELECTION_START: |
| 271 | return "SELECTION_STARTED"; |
| 272 | case MetricsEvent.ACTION_TEXT_SELECTION_MODIFY: |
| 273 | return "SELECTION_MODIFIED"; |
| 274 | case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE: |
| 275 | return "SMART_SELECTION_SINGLE"; |
| 276 | case MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI: |
| 277 | return "SMART_SELECTION_MULTI"; |
| 278 | case MetricsEvent.ACTION_TEXT_SELECTION_AUTO: |
| 279 | return "AUTO_SELECTION"; |
| 280 | default: |
| 281 | return UNKNOWN; |
| 282 | } |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 283 | } |
| 284 | |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 285 | private int getRangeDelta(int offset) { |
| 286 | return offset - mOrigStart; |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 287 | } |
| 288 | |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 289 | private int getSmartRangeDelta(int offset) { |
| 290 | return mSmartSelectionTriggered ? getRangeDelta(offset) : 0; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 291 | } |
| 292 | |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 293 | private String getWidgetTypeName() { |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 294 | switch (mWidgetType) { |
| 295 | case WidgetType.TEXTVIEW: |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 296 | return TEXTVIEW; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 297 | case WidgetType.WEBVIEW: |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 298 | return WEBVIEW; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 299 | case WidgetType.EDITTEXT: |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 300 | return EDITTEXT; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 301 | case WidgetType.EDIT_WEBVIEW: |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 302 | return EDIT_WEBVIEW; |
Jan Althaus | b3c6ece | 2017-11-14 15:40:16 +0100 | [diff] [blame] | 303 | case WidgetType.UNSELECTABLE_TEXTVIEW: |
| 304 | return UNSELECTABLE_TEXTVIEW; |
| 305 | case WidgetType.CUSTOM_TEXTVIEW: |
| 306 | return CUSTOM_TEXTVIEW; |
| 307 | case WidgetType.CUSTOM_EDITTEXT: |
| 308 | return CUSTOM_EDITTEXT; |
| 309 | case WidgetType.CUSTOM_UNSELECTABLE_TEXTVIEW: |
| 310 | return CUSTOM_UNSELECTABLE_TEXTVIEW; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 311 | default: |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 312 | return UNKNOWN; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 313 | } |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 314 | } |
| 315 | |
| 316 | private String getModelName(@Nullable SelectionEvent event) { |
| 317 | return event == null |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 318 | ? SelectionEvent.NO_VERSION_TAG |
| 319 | : Objects.toString(event.mVersionTag, SelectionEvent.NO_VERSION_TAG); |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 320 | } |
| 321 | |
| 322 | private static String createSessionId() { |
| 323 | return UUID.randomUUID().toString(); |
| 324 | } |
| 325 | |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 326 | private static void debugLog(LogMaker log) { |
| 327 | if (!DEBUG_LOG_ENABLED) return; |
| 328 | |
Jan Althaus | 5d0a14b | 2017-11-15 11:20:58 +0100 | [diff] [blame] | 329 | final String widgetType = Objects.toString(log.getTaggedData(WIDGET_TYPE), UNKNOWN); |
| 330 | final String widgetVersion = Objects.toString(log.getTaggedData(WIDGET_VERSION), ""); |
| 331 | final String widget = widgetVersion.isEmpty() |
| 332 | ? widgetType : widgetType + "-" + widgetVersion; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 333 | final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO)); |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 334 | if (log.getType() == MetricsEvent.ACTION_TEXT_SELECTION_START) { |
| 335 | String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), ""); |
| 336 | sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1); |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 337 | Log.d(LOG_TAG, String.format("New selection session: %s (%s)", widget, sessionId)); |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 338 | } |
| 339 | |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 340 | final String model = Objects.toString(log.getTaggedData(MODEL_NAME), UNKNOWN); |
| 341 | final String entity = Objects.toString(log.getTaggedData(ENTITY_TYPE), UNKNOWN); |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 342 | final String type = getLogTypeString(log.getType()); |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 343 | final int smartStart = Integer.parseInt( |
| 344 | Objects.toString(log.getTaggedData(SMART_START), ZERO)); |
| 345 | final int smartEnd = Integer.parseInt( |
| 346 | Objects.toString(log.getTaggedData(SMART_END), ZERO)); |
| 347 | final int eventStart = Integer.parseInt( |
| 348 | Objects.toString(log.getTaggedData(EVENT_START), ZERO)); |
| 349 | final int eventEnd = Integer.parseInt( |
| 350 | Objects.toString(log.getTaggedData(EVENT_END), ZERO)); |
Jan Althaus | 786a39d | 2017-09-15 10:41:16 +0200 | [diff] [blame] | 351 | |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 352 | Log.d(LOG_TAG, String.format("%2d: %s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)", |
| 353 | index, type, entity, eventStart, eventEnd, smartStart, smartEnd, widget, model)); |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 354 | } |
| 355 | |
| 356 | /** |
| 357 | * A selection event. |
| 358 | * Specify index parameters as word token indices. |
| 359 | */ |
| 360 | public static final class SelectionEvent { |
| 361 | |
| 362 | /** |
| 363 | * Use this to specify an indeterminate positive index. |
| 364 | */ |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 365 | public static final int OUT_OF_BOUNDS = Integer.MAX_VALUE; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 366 | |
| 367 | /** |
| 368 | * Use this to specify an indeterminate negative index. |
| 369 | */ |
Jan Althaus | ae5eb83 | 2017-11-06 12:31:59 +0100 | [diff] [blame] | 370 | public static final int OUT_OF_BOUNDS_NEGATIVE = Integer.MIN_VALUE; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 371 | |
| 372 | private static final String NO_VERSION_TAG = ""; |
| 373 | |
| 374 | @Retention(RetentionPolicy.SOURCE) |
| 375 | @IntDef({ActionType.OVERTYPE, ActionType.COPY, ActionType.PASTE, ActionType.CUT, |
| 376 | ActionType.SHARE, ActionType.SMART_SHARE, ActionType.DRAG, ActionType.ABANDON, |
| 377 | ActionType.OTHER, ActionType.SELECT_ALL, ActionType.RESET}) |
| 378 | public @interface ActionType { |
| 379 | /** User typed over the selection. */ |
| 380 | int OVERTYPE = 100; |
| 381 | /** User copied the selection. */ |
| 382 | int COPY = 101; |
| 383 | /** User pasted over the selection. */ |
| 384 | int PASTE = 102; |
| 385 | /** User cut the selection. */ |
| 386 | int CUT = 103; |
| 387 | /** User shared the selection. */ |
| 388 | int SHARE = 104; |
| 389 | /** User clicked the textAssist menu item. */ |
| 390 | int SMART_SHARE = 105; |
| 391 | /** User dragged+dropped the selection. */ |
| 392 | int DRAG = 106; |
| 393 | /** User abandoned the selection. */ |
| 394 | int ABANDON = 107; |
| 395 | /** User performed an action on the selection. */ |
| 396 | int OTHER = 108; |
| 397 | |
| 398 | /* Non-terminal actions. */ |
| 399 | /** User activated Select All */ |
| 400 | int SELECT_ALL = 200; |
| 401 | /** User reset the smart selection. */ |
| 402 | int RESET = 201; |
| 403 | } |
| 404 | |
| 405 | @Retention(RetentionPolicy.SOURCE) |
| 406 | @IntDef({ActionType.OVERTYPE, ActionType.COPY, ActionType.PASTE, ActionType.CUT, |
| 407 | ActionType.SHARE, ActionType.SMART_SHARE, ActionType.DRAG, ActionType.ABANDON, |
| 408 | ActionType.OTHER, ActionType.SELECT_ALL, ActionType.RESET, |
| 409 | EventType.SELECTION_STARTED, EventType.SELECTION_MODIFIED, |
| 410 | EventType.SMART_SELECTION_SINGLE, EventType.SMART_SELECTION_MULTI, |
| 411 | EventType.AUTO_SELECTION}) |
| 412 | private @interface EventType { |
| 413 | /** User started a new selection. */ |
| 414 | int SELECTION_STARTED = 1; |
| 415 | /** User modified an existing selection. */ |
| 416 | int SELECTION_MODIFIED = 2; |
| 417 | /** Smart selection triggered for a single token (word). */ |
| 418 | int SMART_SELECTION_SINGLE = 3; |
| 419 | /** Smart selection triggered spanning multiple tokens (words). */ |
| 420 | int SMART_SELECTION_MULTI = 4; |
| 421 | /** Something else other than User or the default TextClassifier triggered a selection. */ |
| 422 | int AUTO_SELECTION = 5; |
| 423 | } |
| 424 | |
| 425 | private final int mStart; |
| 426 | private final int mEnd; |
| 427 | private @EventType int mEventType; |
| 428 | private final @TextClassifier.EntityType String mEntityType; |
| 429 | private final String mVersionTag; |
| 430 | |
| 431 | private SelectionEvent( |
| 432 | int start, int end, int eventType, |
| 433 | @TextClassifier.EntityType String entityType, String versionTag) { |
| 434 | Preconditions.checkArgument(end >= start, "end cannot be less than start"); |
| 435 | mStart = start; |
| 436 | mEnd = end; |
| 437 | mEventType = eventType; |
| 438 | mEntityType = Preconditions.checkNotNull(entityType); |
| 439 | mVersionTag = Preconditions.checkNotNull(versionTag); |
| 440 | } |
| 441 | |
| 442 | /** |
| 443 | * Creates a "selection started" event. |
| 444 | * |
| 445 | * @param start the word index of the selected word |
| 446 | */ |
Mathew Inwood | 5ca23f6 | 2019-07-03 12:20:45 +0100 | [diff] [blame] | 447 | @UnsupportedAppUsage(trackingBug = 136637107) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 448 | public static SelectionEvent selectionStarted(int start) { |
| 449 | return new SelectionEvent( |
| 450 | start, start + 1, EventType.SELECTION_STARTED, |
| 451 | TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG); |
| 452 | } |
| 453 | |
| 454 | /** |
| 455 | * Creates a "selection modified" event. |
| 456 | * Use when the user modifies the selection. |
| 457 | * |
| 458 | * @param start the start word (inclusive) index of the selection |
| 459 | * @param end the end word (exclusive) index of the selection |
| 460 | */ |
Mathew Inwood | 5ca23f6 | 2019-07-03 12:20:45 +0100 | [diff] [blame] | 461 | @UnsupportedAppUsage(trackingBug = 136637107) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 462 | public static SelectionEvent selectionModified(int start, int end) { |
| 463 | return new SelectionEvent( |
| 464 | start, end, EventType.SELECTION_MODIFIED, |
| 465 | TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG); |
| 466 | } |
| 467 | |
| 468 | /** |
| 469 | * Creates a "selection modified" event. |
| 470 | * Use when the user modifies the selection and the selection's entity type is known. |
| 471 | * |
| 472 | * @param start the start word (inclusive) index of the selection |
| 473 | * @param end the end word (exclusive) index of the selection |
| 474 | * @param classification the TextClassification object returned by the TextClassifier that |
| 475 | * classified the selected text |
| 476 | */ |
Mathew Inwood | 5ca23f6 | 2019-07-03 12:20:45 +0100 | [diff] [blame] | 477 | @UnsupportedAppUsage(trackingBug = 136637107) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 478 | public static SelectionEvent selectionModified( |
| 479 | int start, int end, @NonNull TextClassification classification) { |
| 480 | final String entityType = classification.getEntityCount() > 0 |
| 481 | ? classification.getEntity(0) |
| 482 | : TextClassifier.TYPE_UNKNOWN; |
Abodunrinwa Toki | 080c854 | 2018-03-27 00:04:06 +0100 | [diff] [blame] | 483 | final String versionTag = getVersionInfo(classification.getId()); |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 484 | return new SelectionEvent( |
| 485 | start, end, EventType.SELECTION_MODIFIED, entityType, versionTag); |
| 486 | } |
| 487 | |
| 488 | /** |
| 489 | * Creates a "selection modified" event. |
| 490 | * Use when a TextClassifier modifies the selection. |
| 491 | * |
| 492 | * @param start the start word (inclusive) index of the selection |
| 493 | * @param end the end word (exclusive) index of the selection |
| 494 | * @param selection the TextSelection object returned by the TextClassifier for the |
| 495 | * specified selection |
| 496 | */ |
Mathew Inwood | 5ca23f6 | 2019-07-03 12:20:45 +0100 | [diff] [blame] | 497 | @UnsupportedAppUsage(trackingBug = 136637107) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 498 | public static SelectionEvent selectionModified( |
| 499 | int start, int end, @NonNull TextSelection selection) { |
Abodunrinwa Toki | 080c854 | 2018-03-27 00:04:06 +0100 | [diff] [blame] | 500 | final boolean smartSelection = getSourceClassifier(selection.getId()) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 501 | .equals(TextClassifier.DEFAULT_LOG_TAG); |
| 502 | final int eventType; |
| 503 | if (smartSelection) { |
| 504 | eventType = end - start > 1 |
| 505 | ? EventType.SMART_SELECTION_MULTI |
| 506 | : EventType.SMART_SELECTION_SINGLE; |
| 507 | |
| 508 | } else { |
| 509 | eventType = EventType.AUTO_SELECTION; |
| 510 | } |
| 511 | final String entityType = selection.getEntityCount() > 0 |
| 512 | ? selection.getEntity(0) |
| 513 | : TextClassifier.TYPE_UNKNOWN; |
Abodunrinwa Toki | 080c854 | 2018-03-27 00:04:06 +0100 | [diff] [blame] | 514 | final String versionTag = getVersionInfo(selection.getId()); |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 515 | return new SelectionEvent(start, end, eventType, entityType, versionTag); |
| 516 | } |
| 517 | |
| 518 | /** |
| 519 | * Creates an event specifying an action taken on a selection. |
| 520 | * Use when the user clicks on an action to act on the selected text. |
| 521 | * |
| 522 | * @param start the start word (inclusive) index of the selection |
| 523 | * @param end the end word (exclusive) index of the selection |
| 524 | * @param actionType the action that was performed on the selection |
| 525 | */ |
Mathew Inwood | 5ca23f6 | 2019-07-03 12:20:45 +0100 | [diff] [blame] | 526 | @UnsupportedAppUsage(trackingBug = 136637107) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 527 | public static SelectionEvent selectionAction( |
| 528 | int start, int end, @ActionType int actionType) { |
| 529 | return new SelectionEvent( |
| 530 | start, end, actionType, TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG); |
| 531 | } |
| 532 | |
| 533 | /** |
| 534 | * Creates an event specifying an action taken on a selection. |
| 535 | * Use when the user clicks on an action to act on the selected text and the selection's |
| 536 | * entity type is known. |
| 537 | * |
| 538 | * @param start the start word (inclusive) index of the selection |
| 539 | * @param end the end word (exclusive) index of the selection |
| 540 | * @param actionType the action that was performed on the selection |
| 541 | * @param classification the TextClassification object returned by the TextClassifier that |
| 542 | * classified the selected text |
| 543 | */ |
Mathew Inwood | 5ca23f6 | 2019-07-03 12:20:45 +0100 | [diff] [blame] | 544 | @UnsupportedAppUsage(trackingBug = 136637107) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 545 | public static SelectionEvent selectionAction( |
| 546 | int start, int end, @ActionType int actionType, |
| 547 | @NonNull TextClassification classification) { |
| 548 | final String entityType = classification.getEntityCount() > 0 |
| 549 | ? classification.getEntity(0) |
| 550 | : TextClassifier.TYPE_UNKNOWN; |
Abodunrinwa Toki | 080c854 | 2018-03-27 00:04:06 +0100 | [diff] [blame] | 551 | final String versionTag = getVersionInfo(classification.getId()); |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 552 | return new SelectionEvent(start, end, actionType, entityType, versionTag); |
| 553 | } |
| 554 | |
Tony Mak | b393eea | 2019-07-05 15:23:39 +0100 | [diff] [blame] | 555 | @VisibleForTesting |
| 556 | public static String getVersionInfo(String signature) { |
| 557 | final int start = signature.indexOf("|") + 1; |
Abodunrinwa Toki | 008f387 | 2017-11-27 19:32:35 +0000 | [diff] [blame] | 558 | final int end = signature.indexOf("|", start); |
Tony Mak | b393eea | 2019-07-05 15:23:39 +0100 | [diff] [blame] | 559 | if (start >= 1 && end >= start) { |
Abodunrinwa Toki | 008f387 | 2017-11-27 19:32:35 +0000 | [diff] [blame] | 560 | return signature.substring(start, end); |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 561 | } |
Abodunrinwa Toki | 008f387 | 2017-11-27 19:32:35 +0000 | [diff] [blame] | 562 | return ""; |
| 563 | } |
| 564 | |
| 565 | private static String getSourceClassifier(String signature) { |
| 566 | final int end = signature.indexOf("|"); |
| 567 | if (end >= 0) { |
| 568 | return signature.substring(0, end); |
| 569 | } |
| 570 | return ""; |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 571 | } |
| 572 | |
| 573 | private boolean isTerminal() { |
| 574 | switch (mEventType) { |
| 575 | case ActionType.OVERTYPE: // fall through |
| 576 | case ActionType.COPY: // fall through |
| 577 | case ActionType.PASTE: // fall through |
| 578 | case ActionType.CUT: // fall through |
| 579 | case ActionType.SHARE: // fall through |
| 580 | case ActionType.SMART_SHARE: // fall through |
| 581 | case ActionType.DRAG: // fall through |
| 582 | case ActionType.ABANDON: // fall through |
Jan Althaus | 34aa294 | 2017-09-22 19:55:17 +0200 | [diff] [blame] | 583 | case ActionType.OTHER: // fall through |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 584 | return true; |
| 585 | default: |
| 586 | return false; |
| 587 | } |
| 588 | } |
| 589 | } |
| 590 | } |