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