1 // Copyright 2017 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.content.browser.selection; 6 7 import android.annotation.TargetApi; 8 import android.content.Context; 9 import android.os.Build; 10 import android.view.textclassifier.SelectionEvent; 11 import android.view.textclassifier.TextClassificationContext; 12 import android.view.textclassifier.TextClassificationManager; 13 import android.view.textclassifier.TextClassifier; 14 15 import org.chromium.base.Log; 16 import org.chromium.base.annotations.VerifiesOnP; 17 import org.chromium.content.browser.WindowEventObserver; 18 import org.chromium.content.browser.WindowEventObserverManager; 19 import org.chromium.content_public.browser.SelectionClient; 20 import org.chromium.content_public.browser.SelectionMetricsLogger; 21 import org.chromium.content_public.browser.WebContents; 22 import org.chromium.ui.base.WindowAndroid; 23 24 /** 25 * Smart Selection logger, wrapper of Android logger methods. 26 * We are logging word indices here. For one example: 27 * New York City , NY 28 * -1 0 1 2 3 4 29 * We selected "York" at the first place, so York is [0, 1). Then we assume that Smart Selection 30 * expanded the selection range to the whole "New York City , NY", we need to log [-1, 4). After 31 * that, we single tap on "City", Smart Selection reset get triggered, we need to log [1, 2). Spaces 32 * are ignored but we count each punctuation mark as a word. 33 */ 34 @VerifiesOnP 35 @TargetApi(Build.VERSION_CODES.P) 36 public class SmartSelectionMetricsLogger implements SelectionMetricsLogger { 37 private static final String TAG = "SmartSelectionLogger"; 38 private static final boolean DEBUG = false; 39 40 private WindowAndroid mWindowAndroid; 41 42 private TextClassifier mSession; 43 44 private SelectionIndicesConverter mConverter; 45 create(WebContents webContents)46 public static SmartSelectionMetricsLogger create(WebContents webContents) { 47 if (webContents.getTopLevelNativeWindow().getContext().get() == null) { 48 return null; 49 } 50 return new SmartSelectionMetricsLogger(webContents); 51 } 52 SmartSelectionMetricsLogger(WebContents webContents)53 private SmartSelectionMetricsLogger(WebContents webContents) { 54 mWindowAndroid = webContents.getTopLevelNativeWindow(); 55 WindowEventObserverManager manager = WindowEventObserverManager.from(webContents); 56 if (manager != null) { 57 manager.addObserver(new WindowEventObserver() { 58 @Override 59 public void onWindowAndroidChanged(WindowAndroid newWindowAndroid) { 60 mWindowAndroid = newWindowAndroid; 61 } 62 }); 63 } 64 } 65 logSelectionStarted(String selectionText, int startOffset, boolean editable)66 public void logSelectionStarted(String selectionText, int startOffset, boolean editable) { 67 Context context = mWindowAndroid.getContext().get(); 68 if (context == null) return; 69 70 mSession = createSession(context, editable); 71 mConverter = new SelectionIndicesConverter(); 72 mConverter.updateSelectionState(selectionText, startOffset); 73 mConverter.setInitialStartOffset(startOffset); 74 75 if (DEBUG) Log.d(TAG, "logSelectionStarted"); 76 logEvent(SelectionEvent.createSelectionStartedEvent(SelectionEvent.INVOCATION_MANUAL, 0)); 77 } 78 logSelectionModified( String selectionText, int startOffset, SelectionClient.Result result)79 public void logSelectionModified( 80 String selectionText, int startOffset, SelectionClient.Result result) { 81 if (mSession == null) return; 82 if (!mConverter.updateSelectionState(selectionText, startOffset)) { 83 // DOM change detected, end logging session. 84 endTextClassificationSession(); 85 return; 86 } 87 88 int endOffset = startOffset + selectionText.length(); 89 int[] indices = new int[2]; 90 if (!mConverter.getWordDelta(startOffset, endOffset, indices)) { 91 // Invalid indices, end logging session. 92 endTextClassificationSession(); 93 return; 94 } 95 96 if (DEBUG) Log.d(TAG, "logSelectionModified [%d, %d)", indices[0], indices[1]); 97 if (result != null && result.textSelection != null) { 98 logEvent(SelectionEvent.createSelectionModifiedEvent( 99 indices[0], indices[1], result.textSelection)); 100 } else if (result != null && result.textClassification != null) { 101 logEvent(SelectionEvent.createSelectionModifiedEvent( 102 indices[0], indices[1], result.textClassification)); 103 } else { 104 logEvent(SelectionEvent.createSelectionModifiedEvent(indices[0], indices[1])); 105 } 106 } 107 logSelectionAction( String selectionText, int startOffset, int action, SelectionClient.Result result)108 public void logSelectionAction( 109 String selectionText, int startOffset, int action, SelectionClient.Result result) { 110 if (mSession == null) { 111 return; 112 } 113 if (!mConverter.updateSelectionState(selectionText, startOffset)) { 114 // DOM change detected, end logging session. 115 endTextClassificationSession(); 116 return; 117 } 118 119 int endOffset = startOffset + selectionText.length(); 120 int[] indices = new int[2]; 121 if (!mConverter.getWordDelta(startOffset, endOffset, indices)) { 122 // Invalid indices, end logging session. 123 endTextClassificationSession(); 124 return; 125 } 126 127 if (DEBUG) { 128 Log.d(TAG, "logSelectionAction [%d, %d)", indices[0], indices[1]); 129 Log.d(TAG, "logSelectionAction ActionType = %d", action); 130 } 131 132 if (result != null && result.textClassification != null) { 133 logEvent(SelectionEvent.createSelectionActionEvent( 134 indices[0], indices[1], action, result.textClassification)); 135 } else { 136 logEvent(SelectionEvent.createSelectionActionEvent(indices[0], indices[1], action)); 137 } 138 139 if (SelectionEvent.isTerminal(action)) { 140 endTextClassificationSession(); 141 } 142 } 143 createSession(Context context, boolean editable)144 private TextClassifier createSession(Context context, boolean editable) { 145 TextClassificationContext textClassificationContext = 146 new TextClassificationContext 147 .Builder(context.getPackageName(), 148 editable ? TextClassifier.WIDGET_TYPE_EDIT_WEBVIEW 149 : TextClassifier.WIDGET_TYPE_WEBVIEW) 150 .build(); 151 TextClassificationManager tcm = (TextClassificationManager) context.getSystemService( 152 Context.TEXT_CLASSIFICATION_SERVICE); 153 return tcm.createTextClassificationSession(textClassificationContext); 154 } 155 endTextClassificationSession()156 private void endTextClassificationSession() { 157 if (mSession == null || mSession.isDestroyed()) { 158 return; 159 } 160 mSession.destroy(); 161 mSession = null; 162 } 163 logEvent(SelectionEvent selectionEvent)164 public void logEvent(SelectionEvent selectionEvent) { 165 mSession.onSelectionEvent(selectionEvent); 166 } 167 } 168