Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(36)

Side by Side Diff: content/public/android/java/src/org/chromium/content/browser/WebActionMode.java

Issue 2407303005: Let embedder provide select action mode (Closed)
Patch Set: more comments addressed Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 package org.chromium.content.browser; 5 package org.chromium.content.browser;
boliu 2016/11/01 00:37:36 remove all imports of this class outside of conten
Jinsuk Kim 2016/11/01 04:43:56 Done.
6 6
7 import android.annotation.TargetApi; 7 import android.annotation.TargetApi;
8 import android.app.Activity;
9 import android.app.SearchManager;
10 import android.content.ClipboardManager;
11 import android.content.ContentResolver;
12 import android.content.Context;
13 import android.content.Intent;
14 import android.content.pm.PackageManager;
15 import android.content.pm.ResolveInfo;
16 import android.content.res.Resources;
17 import android.graphics.Rect;
8 import android.os.Build; 18 import android.os.Build;
19 import android.provider.Browser;
20 import android.text.TextUtils;
21 import android.util.SparseBooleanArray;
9 import android.view.ActionMode; 22 import android.view.ActionMode;
23 import android.view.Menu;
24 import android.view.MenuInflater;
25 import android.view.MenuItem;
10 import android.view.View; 26 import android.view.View;
11 import android.view.ViewConfiguration; 27 import android.view.ViewConfiguration;
28 import android.view.WindowManager;
12 29
13 import org.chromium.base.Log; 30 import org.chromium.base.Log;
31 import org.chromium.base.VisibleForTesting;
32 import org.chromium.base.metrics.RecordUserAction;
33 import org.chromium.content.R;
34 import org.chromium.content.browser.input.FloatingPastePopupMenu;
35 import org.chromium.content.browser.input.ImeAdapter;
36 import org.chromium.content.browser.input.LGEmailActionModeWorkaround;
37 import org.chromium.content.browser.input.LegacyPastePopupMenu;
38 import org.chromium.content.browser.input.PastePopupMenu;
39 import org.chromium.content.browser.input.PastePopupMenu.PastePopupMenuDelegate;
40 import org.chromium.content_public.browser.WebContents;
41 import org.chromium.ui.base.DeviceFormFactor;
42 import org.chromium.ui.base.WindowAndroid;
43 import org.chromium.ui.touch_selection.SelectionEventType;
44
45 import java.util.List;
14 46
15 /** 47 /**
16 * An ActionMode for in-page web content selection. This class wraps an ActionMo de created 48 * An ActionMode for in-page web content selection. This class wraps an ActionMo de created
17 * by the associated View, providing modified interaction with that ActionMode. 49 * by the associated View, providing modified interaction with that ActionMode.
50 *
51 * Embedders use {@link WebActionModeDelegate}, a delegate of this class, to bui ld
52 * {@link ActionMode.Callback} instance to configure the selection action mode t asks to
53 * their requirements.
18 */ 54 */
19 @TargetApi(Build.VERSION_CODES.M) 55 @TargetApi(Build.VERSION_CODES.M)
20 public class WebActionMode { 56 public class WebActionMode implements ActionModeCallbackHelper {
21 private static final String TAG = "cr.WebActionMode"; 57 private static final String TAG = "cr.WebActionMode";
22 58
59 /** Google search doesn't support requests slightly larger than this. */
60 public static final int MAX_SEARCH_QUERY_LENGTH = 1000;
boliu 2016/11/01 00:37:36 this should be in the interface (I think it may ne
Jinsuk Kim 2016/11/01 04:43:56 Done.
61
62 /**
63 * Android Intent size limitations prevent sending over a megabyte of data. Limit
64 * query lengths to 100kB because other things may be added to the Intent.
65 */
66 public static final int MAX_SHARE_QUERY_LENGTH = 100000;
boliu 2016/11/01 00:37:36 private
Jinsuk Kim 2016/11/01 04:43:56 Done.
67
68 // TODO(hush): Use these constants from android.webkit.WebSettings, when the y are made
69 // available. crbug.com/546762.
70 public static final int MENU_ITEM_SHARE = 1 << 0;
boliu 2016/11/01 00:37:36 all private here
Jinsuk Kim 2016/11/01 04:43:56 Moved to the abstract class ActionModeCallbackHelp
71 public static final int MENU_ITEM_WEB_SEARCH = 1 << 1;
72 public static final int MENU_ITEM_PROCESS_TEXT = 1 << 2;
73
74 public static final EmptyActionCallback EMPTY_CALLBACK = new EmptyActionCall back();
75
23 // Default delay for reshowing the {@link ActionMode} after it has been 76 // Default delay for reshowing the {@link ActionMode} after it has been
24 // hidden. This avoids flickering issues if there are trailing rect 77 // hidden. This avoids flickering issues if there are trailing rect
25 // invalidations after the ActionMode is shown. For example, after the user 78 // invalidations after the ActionMode is shown. For example, after the user
26 // stops dragging a selection handle, in turn showing the ActionMode, the 79 // stops dragging a selection handle, in turn showing the ActionMode, the
27 // selection change response will be asynchronous. 300ms should accomodate 80 // selection change response will be asynchronous. 300ms should accomodate
28 // most such trailing, async delays. 81 // most such trailing, async delays.
29 private static final int SHOW_DELAY_MS = 300; 82 private static final int SHOW_DELAY_MS = 300;
30 83
31 protected final ActionMode mActionMode; 84 // Creation failure event can be shared among WebActionMode instances as it depends on
32 private final View mView; 85 // underlying Android platform version.
33 private boolean mHidden; 86 private static boolean sFloatingActionModeCreationFailed;
boliu 2016/11/01 00:37:36 this was not static before, any reason why was mad
Jinsuk Kim 2016/11/01 04:43:56 Previously this flag was being set for every CVC.
boliu 2016/11/01 22:24:32 Did you test that? What if failure is due to some
Jinsuk Kim 2016/11/02 03:48:43 Done. Please note that the flag is already shared
34 private boolean mPendingInvalidateContentRect; 87
88 private final Context mContext;
89 private final WindowAndroid mWindowAndroid;
90 private final WebContents mWebContents;
91 private final ActionMode.Callback mCallback;
92 private final RenderCoordinates mRenderCoordinates;
93
94 // Selection rectangle in DIP.
95 private final Rect mSelectionRect = new Rect();
35 96
36 // Self-repeating task that repeatedly hides the ActionMode. This is 97 // Self-repeating task that repeatedly hides the ActionMode. This is
37 // required because ActionMode only exposes a temporary hide routine. 98 // required because ActionMode only exposes a temporary hide routine.
38 private final Runnable mRepeatingHideRunnable; 99 private final Runnable mRepeatingHideRunnable;
39 100
101 private View mView;
102 private ActionMode mActionMode;
103 private boolean mDraggingSelection;
104
105 // Boolean array with mappings from menu item to a flag indicating it is all owed.
106 // The menu items are allowed by default if they not contained in the array.
107 private SparseBooleanArray mAllowedMenuItems;
108 private boolean mMenuDefaultAllowed;
109
110 private boolean mHidden;
111 private boolean mPendingInvalidateContentRect;
112
113 private boolean mEditable;
114 private boolean mIsPasswordType;
115 private boolean mIsInsertion;
116
117 // Indicates whether the action mode needs to be redrawn since last invalida tion.
118 private boolean mNeedsPrepare;
119
120 private boolean mUnselectAllOnDismiss;
121 private String mLastSelectedText;
122
123 // Tracks whether a selection is currently active. When applied to selected text, indicates
124 // whether the last selected text is still highlighted.
125 private boolean mHasSelection;
126
127 // Lazily created paste popup menu, triggered either via long press in an
128 // editable region or from tapping the insertion handle.
129 private PastePopupMenu mPastePopupMenu;
130 private boolean mWasPastePopupShowingOnInsertionDragStart;
131
132 // The client that implements Contextual Search functionality, or null if no ne exists.
133 private ContextualSearchClient mContextualSearchClient;
134
40 /** 135 /**
41 * Constructs a SelectActionMode instance wrapping a concrete ActionMode. 136 * Create {@link WebActionMode} instance.
42 * @param actionMode the wrapped ActionMode. 137 * @param context Context for action mode.
43 * @param view the associated View. 138 * @param window WindowAndroid instance.
139 * @param webContents WebContents instance.
140 * @param view Container view.
141 * @param renderCoordinates Coordinates info used to position elements.
142 * @param callback ActionMode.Callback handling the callbacks from action mo de.
44 */ 143 */
45 public WebActionMode(ActionMode actionMode, View view) { 144 public WebActionMode(Context context, WindowAndroid window, WebContents webC ontents,
46 assert actionMode != null; 145 View view, RenderCoordinates renderCoordinates, ActionMode.Callback callback) {
47 assert view != null; 146 mContext = context;
48 mActionMode = actionMode; 147 mWindowAndroid = window;
148 mWebContents = webContents;
49 mView = view; 149 mView = view;
150 mRenderCoordinates = renderCoordinates;
151 mCallback = callback;
152 mMenuDefaultAllowed = true;
50 mRepeatingHideRunnable = new Runnable() { 153 mRepeatingHideRunnable = new Runnable() {
51 @Override 154 @Override
52 public void run() { 155 public void run() {
53 assert mHidden; 156 assert mHidden;
54 final long hideDuration = getDefaultHideDuration(); 157 final long hideDuration = getDefaultHideDuration();
55 // Ensure the next hide call occurs before the ActionMode reappe ars. 158 // Ensure the next hide call occurs before the ActionMode reappe ars.
56 mView.postDelayed(mRepeatingHideRunnable, hideDuration - 1); 159 mView.postDelayed(mRepeatingHideRunnable, hideDuration - 1);
57 hideTemporarily(hideDuration); 160 hideTemporarily(hideDuration);
58 } 161 }
59 }; 162 };
60 } 163 }
61 164
62 /** 165 /**
166 * Update the container view.
167 */
168 void setContainerView(View view) {
169 assert view != null;
170
171 // Cleans up action mode before switching to a new container view.
172 if (isActionModeValid()) finishActionMode();
173 mUnselectAllOnDismiss = true;
174 destroyPastePopup();
175
176 mView = view;
177 }
178
179 @Override
180 public boolean isActionModeValid() {
181 return mActionMode != null;
182 }
183
184 // True if action mode is not yet initialized or set to no-op mode.
185 private boolean isEmpty() {
186 return mCallback == EMPTY_CALLBACK;
187 }
188
189 @Override
190 public void setAllowedMenuItems(SparseBooleanArray allowedMenuItems, boolean defaultValue) {
191 mAllowedMenuItems = allowedMenuItems;
192 mMenuDefaultAllowed = defaultValue;
193 }
194
195 /**
196 * Show (activate) android action mode by starting it.
197 *
198 * <p>Action mode in floating mode is tried first, and then falls back to
199 * a normal one if allowed.
200 * @param allowFallback A flag indicating if we allow for falling back to
201 * normal action mode in case floating action mode creation fails.
202 * @return {@code true} if the action mode was started; {@code false} otherw ise due to
203 * the condition not being met.
204 */
205 public boolean showActionMode(boolean allowFallback) {
206 if (isEmpty()) return false;
207
208 // Just refreshes the view if it is already showing.
209 if (isActionModeValid()) {
210 invalidateActionMode();
211 return false;
boliu 2016/11/01 00:37:36 this should return true?
Jinsuk Kim 2016/11/01 04:43:56 CVC.showSelectActionMode() uses this value to dete
boliu 2016/11/01 22:24:32 You sure? This is meant to return whether showActi
Jinsuk Kim 2016/11/02 03:48:43 This is what was being done before change:
212 }
213
214 // On ICS, startActionMode throws an NPE when getParent() is null.
215 ActionMode actionMode = null;
216 if (mView.getParent() != null) {
217 assert mWebContents != null;
218 if (supportsFloatingActionMode()) actionMode = startFloatingActionMo de();
219 if (actionMode == null && allowFallback) actionMode = mView.startAct ionMode(mCallback);
220 }
221 if (actionMode != null) {
222 // This is to work around an LGE email issue. See crbug.com/651706 f or more details.
223 LGEmailActionModeWorkaround.runIfNecessary(mContext, actionMode);
224 }
225 mActionMode = actionMode;
226 return true;
227 }
228
229 /**
230 * Tell if the platform supports floating type action mode. Used not to repe atedly
231 * attempt the creation if the request fails once at the beginning. Also che ck
232 * platform version since the floating type is supported only on M or later version
233 * of Android platform.
234 */
235 public static boolean supportsFloatingActionMode() {
236 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false;
237 return !sFloatingActionModeCreationFailed;
238 }
239
240 private static void setFloatingActionModeCreationFailed() {
241 sFloatingActionModeCreationFailed = true;
242 }
243
244 @TargetApi(Build.VERSION_CODES.M)
245 private ActionMode startFloatingActionMode() {
246 ActionMode actionMode = mView.startActionMode(
247 new FloatingWebActionModeCallback(this, mCallback), ActionMode.T YPE_FLOATING);
248 if (actionMode == null) setFloatingActionModeCreationFailed();
249 return actionMode;
250 }
251
252 void showPastePopup(int x, int y) {
253 if (mView.getParent() == null || mView.getVisibility() != View.VISIBLE) {
254 return;
255 }
256
257 if (!isInsertion() || (!supportsFloatingActionMode() && !canPaste())) re turn;
258
259 PastePopupMenu pastePopupMenu = getPastePopup();
260 if (pastePopupMenu == null) return;
261
262 // Coordinates are in DIP.
263 final float deviceScale = mRenderCoordinates.getDeviceScaleFactor();
264 final int xPix = (int) (x * deviceScale);
265 final int yPix = (int) (y * deviceScale);
266 final float topControlsShownPix = mRenderCoordinates.getContentOffsetYPi x();
boliu 2016/11/01 00:37:36 this variable this got renamed to browserControlsS
Jinsuk Kim 2016/11/01 04:43:56 Thanks. rebased.
267 try {
268 pastePopupMenu.show(xPix, (int) (yPix + topControlsShownPix));
269 } catch (WindowManager.BadTokenException e) {
270 }
271 }
272
273 void hidePastePopup() {
274 if (mPastePopupMenu != null) mPastePopupMenu.hide();
275 }
276
277 private PastePopupMenu getPastePopup() {
278 if (mPastePopupMenu == null) {
279 PastePopupMenuDelegate delegate = new PastePopupMenuDelegate() {
280 @Override
281 public void paste() {
282 mWebContents.paste();
283 mWebContents.dismissTextHandles();
284 }
285 };
286 Context windowContext = mWindowAndroid.getContext().get();
287 if (windowContext == null) return null;
288 if (supportsFloatingActionMode()) {
289 mPastePopupMenu = new FloatingPastePopupMenu(windowContext, mVie w, delegate);
290 } else {
291 mPastePopupMenu = new LegacyPastePopupMenu(windowContext, mView, delegate);
292 }
293 }
294 return mPastePopupMenu;
295 }
296
297 void destroyPastePopup() {
298 hidePastePopup();
299 mPastePopupMenu = null;
300 }
301
302 @VisibleForTesting
303 public boolean isPastePopupShowing() {
304 return mPastePopupMenu != null && mPastePopupMenu.isShowing();
305 }
306
307 private Context getContext() {
308 return mContext;
309 }
310
311 // Composition methods for android.view.ActionMode
312
313 /**
314 * @see ActionMode#getType()}
315 */
316 public int getType() {
boliu 2016/11/01 00:37:36 package visible
Jinsuk Kim 2016/11/01 04:43:56 In fact it's not used any more. Removed.
317 return isActionModeValid() ? mActionMode.getType() : ActionMode.TYPE_PRI MARY;
318 }
319
320 /**
63 * @see ActionMode#finish() 321 * @see ActionMode#finish()
64 */ 322 */
65 public void finish() { 323 @Override
66 mActionMode.finish(); 324 public void finishActionMode() {
325 if (isActionModeValid()) mActionMode.finish();
67 } 326 }
68 327
69 /** 328 /**
70 * @see ActionMode#invalidate() 329 * @see ActionMode#invalidate()
71 * Note that invalidation will also reset visibility state. The caller 330 * Note that invalidation will also reset visibility state. The caller
72 * should account for this when making subsequent visibility updates. 331 * should account for this when making subsequent visibility updates.
73 */ 332 */
74 public void invalidate() { 333 public void invalidateActionMode() {
boliu 2016/11/01 00:37:36 private
Jinsuk Kim 2016/11/01 04:43:56 Done.
334 if (!isActionModeValid()) return;
75 if (mHidden) { 335 if (mHidden) {
76 assert canHide(); 336 assert canHide();
77 mHidden = false; 337 mHidden = false;
78 mView.removeCallbacks(mRepeatingHideRunnable); 338 mView.removeCallbacks(mRepeatingHideRunnable);
79 mPendingInvalidateContentRect = false; 339 mPendingInvalidateContentRect = false;
80 } 340 }
81 341
82 // Try/catch necessary for framework bug, crbug.com/446717. 342 // Try/catch necessary for framework bug, crbug.com/446717.
83 try { 343 try {
84 mActionMode.invalidate(); 344 mActionMode.invalidate();
85 } catch (NullPointerException e) { 345 } catch (NullPointerException e) {
86 Log.w(TAG, "Ignoring NPE from ActionMode.invalidate() as workaround for L", e); 346 Log.w(TAG, "Ignoring NPE from ActionMode.invalidate() as workaround for L", e);
87 } 347 }
88 } 348 }
89 349
boliu 2016/11/01 00:37:36 I read up to this far..
Jinsuk Kim 2016/11/01 04:43:56 Acknowledged.
90 /** 350 /**
91 * @see ActionMode#invalidateContentRect() 351 * @see ActionMode#invalidateContentRect()
92 */ 352 */
93 public void invalidateContentRect() { 353 public void invalidateContentRect() {
94 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 354 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
95 if (mHidden) { 355 if (mHidden) {
96 mPendingInvalidateContentRect = true; 356 mPendingInvalidateContentRect = true;
97 } else { 357 } else {
98 mPendingInvalidateContentRect = false; 358 mPendingInvalidateContentRect = false;
99 mActionMode.invalidateContentRect(); 359 if (isActionModeValid()) mActionMode.invalidateContentRect();
100 } 360 }
101 } 361 }
102 } 362 }
103 363
104 /** 364 /**
105 * @see ActionMode#onWindowFocusChanged() 365 * @see ActionMode#onWindowFocusChanged()
106 */ 366 */
107 public void onWindowFocusChanged(boolean hasWindowFocus) { 367 public void onWindowFocusChanged(boolean hasWindowFocus) {
108 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 368 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
109 mActionMode.onWindowFocusChanged(hasWindowFocus); 369 if (isActionModeValid()) mActionMode.onWindowFocusChanged(hasWindowF ocus);
110 } 370 }
111 } 371 }
112 372
113 /** 373 /**
114 * Hide or reveal the ActionMode. Note that this only has visible 374 * Hide or reveal the ActionMode. Note that this only has visible
115 * side-effects if the underlying ActionMode supports hiding. 375 * side-effects if the underlying ActionMode supports hiding.
116 * @param hide whether to hide or show the ActionMode. 376 * @param hide whether to hide or show the ActionMode.
117 */ 377 */
118 public void hide(boolean hide) { 378 void hideActionMode(boolean hide) {
119 if (!canHide()) return; 379 if (!canHide()) return;
120 if (mHidden == hide) return; 380 if (mHidden == hide) return;
121 mHidden = hide; 381 mHidden = hide;
122 if (mHidden) { 382 if (mHidden) {
123 mRepeatingHideRunnable.run(); 383 mRepeatingHideRunnable.run();
124 } else { 384 } else {
125 mHidden = false; 385 mHidden = false;
126 mView.removeCallbacks(mRepeatingHideRunnable); 386 mView.removeCallbacks(mRepeatingHideRunnable);
127 hideTemporarily(SHOW_DELAY_MS); 387 hideTemporarily(SHOW_DELAY_MS);
128 if (mPendingInvalidateContentRect) { 388 if (mPendingInvalidateContentRect) {
129 mPendingInvalidateContentRect = false; 389 mPendingInvalidateContentRect = false;
130 invalidateContentRect(); 390 invalidateContentRect();
131 } 391 }
132 } 392 }
133 } 393 }
134 394
135 /** 395 /**
136 * @see ActionMode#hide(long) 396 * @see ActionMode#hide(long)
137 */ 397 */
138 private void hideTemporarily(long duration) { 398 private void hideTemporarily(long duration) {
139 assert canHide(); 399 assert canHide();
140 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 400 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
141 mActionMode.hide(duration); 401 if (isActionModeValid()) mActionMode.hide(duration);
142 } 402 }
143 } 403 }
144 404
145 private boolean canHide() { 405 private boolean canHide() {
146 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 406 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
147 return mActionMode.getType() == ActionMode.TYPE_FLOATING; 407 && isActionModeValid()
148 } 408 && mActionMode.getType() == ActionMode.TYPE_FLOATING;
149 return false;
150 } 409 }
151 410
152 private long getDefaultHideDuration() { 411 private long getDefaultHideDuration() {
153 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 412 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
154 return ViewConfiguration.getDefaultActionModeHideDuration(); 413 return ViewConfiguration.getDefaultActionModeHideDuration();
155 } 414 }
156 return 2000; 415 return 2000;
157 } 416 }
417
418 // Default handlers for action mode callbacks.
419
420 @Override
421 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
422 mode.setTitle(DeviceFormFactor.isTablet(getContext())
423 ? getContext().getString(R.string.actionbar_textselectio n_title)
424 : null);
425 mode.setSubtitle(null);
426 createActionMenu(mode, menu);
427 mNeedsPrepare = false;
428 return true;
429 }
430
431 @Override
432 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
433 if (mNeedsPrepare) {
434 menu.clear();
435 createActionMenu(mode, menu);
436 mNeedsPrepare = false;
437 return true;
438 }
439 return false;
440 }
441
442 /**
443 * Initialize the menu by populating all the available items. Embedders shou ld remove
444 * the items that are not relevant to the input text being edited.
445 */
446 public static void initializeMenu(Context context, ActionMode mode, Menu men u) {
447 try {
448 mode.getMenuInflater().inflate(R.menu.select_action_menu, menu);
449 } catch (Resources.NotFoundException e) {
450 // TODO(tobiasjs) by the time we get here we have already
451 // caused a resource loading failure to be logged. WebView
452 // resource access needs to be improved so that this
453 // logspam can be avoided.
454 new MenuInflater(context).inflate(R.menu.select_action_menu, menu);
455 }
456 }
457
458 private void createActionMenu(ActionMode mode, Menu menu) {
459 initializeMenu(mContext, mode, menu);
460
461 if (!isSelectionEditable() || !canPaste()) {
462 menu.removeItem(R.id.select_action_menu_paste);
463 }
464
465 if (isInsertion()) {
466 menu.removeItem(R.id.select_action_menu_select_all);
467 menu.removeItem(R.id.select_action_menu_cut);
468 menu.removeItem(R.id.select_action_menu_copy);
469 menu.removeItem(R.id.select_action_menu_share);
470 menu.removeItem(R.id.select_action_menu_web_search);
471 return;
472 }
473
474 if (!isSelectionEditable()) {
475 menu.removeItem(R.id.select_action_menu_cut);
476 }
477
478 if (isSelectionEditable() || !isSelectActionModeAllowed(MENU_ITEM_SHARE) ) {
479 menu.removeItem(R.id.select_action_menu_share);
480 }
481
482 if (isSelectionEditable() || isIncognito()
483 || !isSelectActionModeAllowed(MENU_ITEM_WEB_SEARCH)) {
484 menu.removeItem(R.id.select_action_menu_web_search);
485 }
486
487 if (isSelectionPassword()) {
488 menu.removeItem(R.id.select_action_menu_copy);
489 menu.removeItem(R.id.select_action_menu_cut);
490 return;
491 }
492
493 initializeTextProcessingMenu(menu);
494 }
495
496 private boolean canPaste() {
497 ClipboardManager clipMgr = (ClipboardManager)
498 getContext().getSystemService(Context.CLIPBOARD_SERVICE);
499 return clipMgr.hasPrimaryClip();
500 }
501
502 /**
503 * Intialize the menu items for processing text, if there is any.
504 */
505 private void initializeTextProcessingMenu(Menu menu) {
506 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
507 || !isSelectActionModeAllowed(MENU_ITEM_PROCESS_TEXT)) {
508 return;
509 }
510
511 PackageManager packageManager = getContext().getPackageManager();
512 List<ResolveInfo> supportedActivities =
513 packageManager.queryIntentActivities(createProcessTextIntent(), 0);
514 for (int i = 0; i < supportedActivities.size(); i++) {
515 ResolveInfo resolveInfo = supportedActivities.get(i);
516 CharSequence label = resolveInfo.loadLabel(getContext().getPackageMa nager());
517 menu.add(R.id.select_action_menu_text_processing_menus, Menu.NONE, i , label)
518 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo ))
519 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
520 }
521 }
522
523 @TargetApi(Build.VERSION_CODES.M)
524 private Intent createProcessTextIntent() {
525 return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/ plain");
526 }
527
528 @TargetApi(Build.VERSION_CODES.M)
529 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
530 boolean isReadOnly = !isSelectionEditable();
531 return createProcessTextIntent()
532 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, isReadOnly)
533 .setClassName(info.activityInfo.packageName, info.activityInfo.n ame);
534 }
535
536 @Override
537 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
538 if (!isActionModeValid()) return true;
539
540 int id = item.getItemId();
541 int groupId = item.getGroupId();
542
543 if (id == R.id.select_action_menu_select_all) {
544 selectAll();
545 } else if (id == R.id.select_action_menu_cut) {
546 cut();
547 mode.finish();
548 } else if (id == R.id.select_action_menu_copy) {
549 copy();
550 mode.finish();
551 } else if (id == R.id.select_action_menu_paste) {
552 paste();
553 mode.finish();
554 } else if (id == R.id.select_action_menu_share) {
555 share();
556 mode.finish();
557 } else if (id == R.id.select_action_menu_web_search) {
558 search();
559 mode.finish();
560 } else if (groupId == R.id.select_action_menu_text_processing_menus) {
561 processText(item.getIntent());
562 // The ActionMode is not dismissed to match the behavior with
563 // TextView in Android M.
564 } else {
565 return false;
566 }
567 return true;
568 }
569
570 @Override
571 public void onDestroyActionMode() {
572 mActionMode = null;
573 if (mUnselectAllOnDismiss) {
574 mWebContents.dismissTextHandles();
575 mWebContents.unselect();
576 }
577 }
578
579 /**
580 * Called when an ActionMode needs to be positioned on screen, potentially o ccluding view
581 * content. Note this may be called on a per-frame basis.
582 *
583 * @param mode The ActionMode that requires positioning.
584 * @param view The View that originated the ActionMode, in whose coordinates the Rect should
585 * be provided.
586 * @param outRect The Rect to be populated with the content position.
587 */
588 @Override
589 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
590 float deviceScale = mRenderCoordinates.getDeviceScaleFactor();
591 outRect.set((int) (mSelectionRect.left * deviceScale),
592 (int) (mSelectionRect.top * deviceScale),
593 (int) (mSelectionRect.right * deviceScale),
594 (int) (mSelectionRect.bottom * deviceScale));
595
596 // The selection coordinates are relative to the content viewport, but w e need
597 // coordinates relative to the containing View.
598 outRect.offset(0, (int) mRenderCoordinates.getContentOffsetYPix());
599 }
600
601 /**
602 * Perform a select all action.
603 */
604 public void selectAll() {
605 mWebContents.selectAll();
606 // Even though the above statement logged a SelectAll user action, we wa nt to
607 // track whether the focus was in an editable field, so log that too.
608 if (isSelectionEditable()) {
609 RecordUserAction.record("MobileActionMode.SelectAllWasEditable");
610 } else {
611 RecordUserAction.record("MobileActionMode.SelectAllWasNonEditable");
612 }
613 }
614
615 /**
616 * Perform a cut (to clipboard) action.
617 */
618 public void cut() {
619 mWebContents.cut();
620 }
621
622 /**
623 * Perform a copy (to clipboard) action.
624 */
625 public void copy() {
626 mWebContents.copy();
627 }
628
629 /**
630 * Perform a paste action.
631 */
632 public void paste() {
633 mWebContents.paste();
634 }
635
636 /**
637 * Perform a share action.
638 */
639 public void share() {
640 RecordUserAction.record("MobileActionMode.Share");
641 String query = sanitizeQuery(getSelectedText(), MAX_SHARE_QUERY_LENGTH);
642 if (TextUtils.isEmpty(query)) return;
643
644 Intent send = new Intent(Intent.ACTION_SEND);
645 send.setType("text/plain");
646 send.putExtra(Intent.EXTRA_TEXT, query);
647 try {
648 Intent i = Intent.createChooser(send, mContext.getString(R.string.ac tionbar_share));
649 i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
650 mContext.startActivity(i);
651 } catch (android.content.ActivityNotFoundException ex) {
652 // If no app handles it, do nothing.
653 }
654 }
655
656 /**
657 * Perform a processText action (translating the text, for example).
658 */
659 public void processText(Intent intent) {
660 RecordUserAction.record("MobileActionMode.ProcessTextIntent");
661 assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
662
663 String query = sanitizeQuery(getSelectedText(), MAX_SEARCH_QUERY_LENGTH) ;
664 if (TextUtils.isEmpty(query)) return;
665
666 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, query);
667
668 // Intent is sent by WindowAndroid by default.
669 try {
670 mWindowAndroid.showIntent(intent, new WindowAndroid.IntentCallback() {
671 @Override
672 public void onIntentCompleted(WindowAndroid window,
673 int resultCode, ContentResolver contentResolver, Int ent data) {
674 onReceivedProcessTextResult(resultCode, data);
675 }
676 }, null);
677 } catch (android.content.ActivityNotFoundException ex) {
678 // If no app handles it, do nothing.
679 }
680 }
681
682 /**
683 * Perform a search action.
684 */
685 public void search() {
686 RecordUserAction.record("MobileActionMode.WebSearch");
687 String query = sanitizeQuery(getSelectedText(), MAX_SEARCH_QUERY_LENGTH) ;
688 if (TextUtils.isEmpty(query)) return;
689
690 Intent i = new Intent(Intent.ACTION_WEB_SEARCH);
691 i.putExtra(SearchManager.EXTRA_NEW_SEARCH, true);
692 i.putExtra(SearchManager.QUERY, query);
693 i.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
694 i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
695 try {
696 mContext.startActivity(i);
697 } catch (android.content.ActivityNotFoundException ex) {
698 // If no app handles it, do nothing.
699 }
700 }
701
702 /**
703 * @return true if the current selection is of password type.
704 */
705 public boolean isSelectionPassword() {
706 return mIsPasswordType;
707 }
708
709 /**
710 * @return true iff the current selection is editable (e.g. text within an i nput field).
711 */
712 public boolean isSelectionEditable() {
713 return mEditable;
714 }
715
716 /**
717 * @return true if the current selection is an insertion point.
718 */
719 @VisibleForTesting
720 public boolean isInsertion() {
721 return mIsInsertion;
722 }
723
724 @Override
725 public boolean isIncognito() {
726 return mWebContents.isIncognito();
727 }
728
729 /**
730 * @param actionModeItem the flag for the action mode item in question. The valid flags are
731 * {@link #MENU_ITEM_SHARE}, {@link #MENU_ITEM_WEB_SEARCH}, and
732 * {@link #MENU_ITEM_PROCESS_TEXT}.
733 * @return true if the menu item action is allowed. Otherwise, the menu item
734 * should be removed from the menu.
735 */
736 private boolean isSelectActionModeAllowed(int actionModeItem) {
737 boolean isAllowedByClient = mAllowedMenuItems != null
738 ? mAllowedMenuItems.get(actionModeItem, mMenuDefaultAllowed)
739 : mMenuDefaultAllowed;
740 if (actionModeItem == MENU_ITEM_SHARE) {
741 return isAllowedByClient && isShareAvailable();
742 }
743 if (actionModeItem == MENU_ITEM_WEB_SEARCH) {
744 return isAllowedByClient && isWebSearchAvailable();
745 }
746 return isAllowedByClient;
747 }
748
749 @Override
750 public void onReceivedProcessTextResult(int resultCode, Intent data) {
751 if (mWebContents == null || resultCode != Activity.RESULT_OK || data == null) return;
752
753 // Do not handle the result if no text is selected or current selection is not editable.
754 if (!mHasSelection || !isSelectionEditable()) return;
755
756 CharSequence result = data.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEX T);
757 if (result != null) {
758 // TODO(hush): Use a variant of replace that re-selects the replaced text.
759 // crbug.com/546710
760 mWebContents.replace(result.toString());
761 }
762 }
763
764 void restoreSelectionPopupsIfNecessary() {
765 if (mHasSelection && isActionModeValid()) showActionMode(true);
766 }
767
768 // All coordinates are in DIP.
769 void onSelectionEvent(int eventType, int xAnchor, int yAnchor,
770 int left, int top, int right, int bottom, boolean isScrollInProgress ,
771 boolean touchScrollInProgress, ImeAdapter imeAdapter) {
772 // Ensure the provided selection coordinates form a non-empty rect, as r equired by
773 // the selection action mode.
774 if (left == right) ++right;
775 if (top == bottom) ++bottom;
776 switch (eventType) {
777 case SelectionEventType.SELECTION_HANDLES_SHOWN:
778 mSelectionRect.set(left, top, right, bottom);
779 mHasSelection = true;
780 mUnselectAllOnDismiss = true;
781 if (showActionMode(true) && !isActionModeValid()) {
782 if (isSelectionEditable()) {
783 imeAdapter.moveCursorToSelectionEnd();
784 } else {
785 if (mWebContents != null) mWebContents.unselect();
786 }
787 }
788 break;
789
790 case SelectionEventType.SELECTION_HANDLES_MOVED:
791 mSelectionRect.set(left, top, right, bottom);
792 invalidateContentRect();
793 break;
794
795 case SelectionEventType.SELECTION_HANDLES_CLEARED:
796 mHasSelection = false;
797 mUnselectAllOnDismiss = false;
798 mSelectionRect.setEmpty();
799 finishActionMode();
800 mDraggingSelection = false;
801 break;
802
803 case SelectionEventType.SELECTION_HANDLE_DRAG_STARTED:
804 mDraggingSelection = true;
805 updateActionModeVisibility(touchScrollInProgress);
806 break;
807
808 case SelectionEventType.SELECTION_HANDLE_DRAG_STOPPED:
809 mDraggingSelection = false;
810 updateActionModeVisibility(touchScrollInProgress);
811 break;
812
813 case SelectionEventType.INSERTION_HANDLE_SHOWN:
814 mSelectionRect.set(left, top, right, bottom);
815 setIsInsertion(true);
816 break;
817
818 case SelectionEventType.INSERTION_HANDLE_MOVED:
819 mSelectionRect.set(left, top, right, bottom);
820 if (!isScrollInProgress && isPastePopupShowing()) {
821 showPastePopup(xAnchor, yAnchor);
822 } else {
823 hidePastePopup();
824 }
825 break;
826
827 case SelectionEventType.INSERTION_HANDLE_TAPPED:
828 if (mWasPastePopupShowingOnInsertionDragStart) {
829 hidePastePopup();
830 } else {
831 showPastePopup(xAnchor, yAnchor);
832 }
833 mWasPastePopupShowingOnInsertionDragStart = false;
834 break;
835
836 case SelectionEventType.INSERTION_HANDLE_CLEARED:
837 hidePastePopup();
838 setIsInsertion(false);
839 mSelectionRect.setEmpty();
840 break;
841
842 case SelectionEventType.INSERTION_HANDLE_DRAG_STARTED:
843 mWasPastePopupShowingOnInsertionDragStart = isPastePopupShowing( );
844 hidePastePopup();
845 break;
846
847 case SelectionEventType.INSERTION_HANDLE_DRAG_STOPPED:
848 if (mWasPastePopupShowingOnInsertionDragStart) {
849 showPastePopup(xAnchor, yAnchor);
850 }
851 mWasPastePopupShowingOnInsertionDragStart = false;
852 break;
853
854 case SelectionEventType.SELECTION_ESTABLISHED:
855 case SelectionEventType.SELECTION_DISSOLVED:
856 break;
857
858 default:
859 assert false : "Invalid selection event type.";
860 }
861
862 if (mContextualSearchClient != null) {
863 final float deviceScale = mRenderCoordinates.getDeviceScaleFactor();
864 int xAnchorPix = (int) (xAnchor * deviceScale);
865 int yAnchorPix = (int) (yAnchor * deviceScale);
866 mContextualSearchClient.onSelectionEvent(eventType, xAnchorPix, yAnc horPix);
867 }
868 }
869
870 void onSelectionChanged(String text) {
871 mLastSelectedText = text;
872 if (mContextualSearchClient != null) {
873 mContextualSearchClient.onSelectionChanged(text);
874 }
875 }
876
877 // The client that implements Contextual Search functionality, or null if no ne exists.
878 void setContextualSearchClient(ContextualSearchClient contextualSearchClient ) {
879 mContextualSearchClient = contextualSearchClient;
880 }
881
882 void onShowUnhandledTapUIIfNeeded(int x, int y) {
883 if (mContextualSearchClient != null) {
884 mContextualSearchClient.showUnhandledTapUIIfNeeded(x, y);
885 }
886 }
887
888 void destroyActionModeAndUnselect() {
889 mUnselectAllOnDismiss = true;
890 }
891
892 void destroyActionModeAndKeepSelection() {
893 mUnselectAllOnDismiss = false;
894 }
895
896 void updateSelectionState(boolean editable, boolean isPassword) {
897 if (!editable) hidePastePopup();
898 if (isActionModeValid()
899 && (editable != isSelectionEditable() || isPassword != isSelecti onPassword())) {
900 mActionMode.invalidate();
901 mNeedsPrepare = true;
902 }
903 mEditable = editable;
904 mIsPasswordType = isPassword;
905 }
906
907 /**
908 * @return Whether the page has an active, touch-controlled selection region .
909 */
910 @VisibleForTesting
911 public boolean hasSelection() {
912 return mHasSelection;
913 }
914
915 void updateActionModeVisibility(boolean touchScrollInProgress) {
916 // The active fling count isn't reliable with WebView, so only use the
917 // active touch scroll signal for hiding. The fling animation movement
918 // will naturally hide the ActionMode by invalidating its content rect.
919 hideActionMode(mDraggingSelection || touchScrollInProgress);
920 }
921
922 @Override
923 public String getSelectedText() {
924 return mHasSelection ? mLastSelectedText : "";
925 }
926
927 private void setIsInsertion(boolean insertion) {
928 if (isActionModeValid() && mIsInsertion != insertion) mNeedsPrepare = tr ue;
929 mIsInsertion = insertion;
930 }
931
932 /**
933 * Trim a given string query to be processed safely.
934 *
935 * @param query a raw query to sanitize.
936 * @param maxLength maximum length to which the query will be truncated.
937 */
938 public static String sanitizeQuery(String query, int maxLength) {
939 if (TextUtils.isEmpty(query) || query.length() < maxLength) return query ;
940 Log.w(TAG, "Truncating oversized query (" + query.length() + ").");
941 return query.substring(0, maxLength) + "…";
942 }
943
944 private boolean isShareAvailable() {
945 Intent intent = new Intent(Intent.ACTION_SEND);
946 intent.setType("text/plain");
947 return mContext.getPackageManager().queryIntentActivities(intent,
948 PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
949 }
950
951 private boolean isWebSearchAvailable() {
952 Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
953 intent.putExtra(SearchManager.EXTRA_NEW_SEARCH, true);
954 return mContext.getPackageManager().queryIntentActivities(intent,
955 PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
956 }
957
958 /**
959 * Empty {@link ActionMode.Callback} that does nothing. Used for {@link #emp ty()}.
960 */
961 private static class EmptyActionCallback implements ActionMode.Callback {
962 @Override
963 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
964 return false;
965 }
966
967 @Override
968 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
969 return false;
970 }
971
972 @Override
973 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
974 return false;
975 }
976
977 @Override
978 public void onDestroyActionMode(ActionMode mode) {}
979 };
158 } 980 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698