diff options
10 files changed, 3164 insertions, 21 deletions
diff --git a/content/browser/android/content_view_core_impl.cc b/content/browser/android/content_view_core_impl.cc index d1cf353..a3b39a2 100644 --- a/content/browser/android/content_view_core_impl.cc +++ b/content/browser/android/content_view_core_impl.cc @@ -72,7 +72,8 @@ jfieldID g_native_content_view; namespace content { struct ContentViewCoreImpl::JavaObject { - + ScopedJavaGlobalRef<jclass> rect_clazz; + jmethodID rect_constructor; }; ContentViewCore* ContentViewCore::GetNativeContentViewCore(JNIEnv* env, @@ -173,6 +174,9 @@ void ContentViewCoreImpl::Observe(int type, void ContentViewCoreImpl::InitJNI(JNIEnv* env, jobject obj) { java_object_ = new JavaObject; + java_object_->rect_clazz.Reset(GetClass(env, "android/graphics/Rect")); + java_object_->rect_constructor = + GetMethodID(env, java_object_->rect_clazz, "<init>", "(IIII)V"); } RenderWidgetHostViewAndroid* @@ -339,7 +343,40 @@ bool ContentViewCoreImpl::HasFocus() { } void ContentViewCoreImpl::OnSelectionChanged(const std::string& text) { - NOTIMPLEMENTED() << "not upstreamed yet"; + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + ScopedJavaLocalRef<jstring> jtext = ConvertUTF8ToJavaString(env, text); + Java_ContentViewCore_onSelectionChanged(env, obj.obj(), jtext.obj()); +} + +void ContentViewCoreImpl::OnSelectionBoundsChanged( + const gfx::Rect& start_rect, base::i18n::TextDirection start_dir, + const gfx::Rect& end_rect, base::i18n::TextDirection end_dir) { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + ScopedJavaLocalRef<jobject> start_rect_object(env, + env->NewObject(java_object_->rect_clazz.obj(), + java_object_->rect_constructor, + start_rect.x(), + start_rect.y(), + start_rect.right(), + start_rect.bottom())); + ScopedJavaLocalRef<jobject> end_rect_object(env, + env->NewObject(java_object_->rect_clazz.obj(), + java_object_->rect_constructor, + end_rect.x(), + end_rect.y(), + end_rect.right(), + end_rect.bottom())); + Java_ContentViewCore_onSelectionBoundsChanged(env, obj.obj(), + start_rect_object.obj(), + start_dir, + end_rect_object.obj(), + end_dir); } void ContentViewCoreImpl::StartContentIntent(const GURL& content_url) { diff --git a/content/browser/android/content_view_core_impl.h b/content/browser/android/content_view_core_impl.h index 4a60213..99b0625 100644 --- a/content/browser/android/content_view_core_impl.h +++ b/content/browser/android/content_view_core_impl.h @@ -196,12 +196,9 @@ class ContentViewCoreImpl : public ContentViewCore, void ConfirmTouchEvent(bool handled); void DidSetNeedTouchEvents(bool need_touch_events); void OnSelectionChanged(const std::string& text); - void OnSelectionBoundsChanged(int startx, - int starty, - base::i18n::TextDirection start_dir, - int endx, - int endy, - base::i18n::TextDirection end_dir); + void OnSelectionBoundsChanged( + const gfx::Rect& start_rect, base::i18n::TextDirection start_dir, + const gfx::Rect& end_rect, base::i18n::TextDirection end_dir); void StartContentIntent(const GURL& content_url); diff --git a/content/browser/renderer_host/render_widget_host_view_android.cc b/content/browser/renderer_host/render_widget_host_view_android.cc index 333b34e..1248e91 100644 --- a/content/browser/renderer_host/render_widget_host_view_android.cc +++ b/content/browser/renderer_host/render_widget_host_view_android.cc @@ -21,6 +21,22 @@ namespace content { +namespace { + +// TODO(pliard): http://crbug.com/142585. Remove this helper function and update +// the clients to deal directly with WebKit::WebTextDirection. +base::i18n::TextDirection ConvertTextDirection(WebKit::WebTextDirection dir) { + switch (dir) { + case WebKit::WebTextDirectionDefault: return base::i18n::UNKNOWN_DIRECTION; + case WebKit::WebTextDirectionLeftToRight: return base::i18n::LEFT_TO_RIGHT; + case WebKit::WebTextDirectionRightToLeft: return base::i18n::RIGHT_TO_LEFT; + } + NOTREACHED() << "Unsupported text direction " << dir; + return base::i18n::UNKNOWN_DIRECTION; +} + +} // namespace + RenderWidgetHostViewAndroid::RenderWidgetHostViewAndroid( RenderWidgetHostImpl* widget_host, ContentViewCoreImpl* content_view_core) @@ -251,6 +267,20 @@ void RenderWidgetHostViewAndroid::SelectionChanged(const string16& text, content_view_core_->OnSelectionChanged(utf8_selection); } +void RenderWidgetHostViewAndroid::SelectionBoundsChanged( + const gfx::Rect& start_rect, + WebKit::WebTextDirection start_direction, + const gfx::Rect& end_rect, + WebKit::WebTextDirection end_direction) { + if (content_view_core_) { + content_view_core_->OnSelectionBoundsChanged( + start_rect, + ConvertTextDirection(start_direction), + end_rect, + ConvertTextDirection(end_direction)); + } +} + BackingStore* RenderWidgetHostViewAndroid::AllocBackingStore( const gfx::Size& size) { NOTIMPLEMENTED(); diff --git a/content/browser/renderer_host/render_widget_host_view_android.h b/content/browser/renderer_host/render_widget_host_view_android.h index 31ae5b2..6f36e24 100644 --- a/content/browser/renderer_host/render_widget_host_view_android.h +++ b/content/browser/renderer_host/render_widget_host_view_android.h @@ -80,6 +80,11 @@ class RenderWidgetHostViewAndroid : public RenderWidgetHostViewBase { virtual void SelectionChanged(const string16& text, size_t offset, const ui::Range& range) OVERRIDE; + virtual void SelectionBoundsChanged( + const gfx::Rect& start_rect, + WebKit::WebTextDirection start_direction, + const gfx::Rect& end_rect, + WebKit::WebTextDirection end_direction) OVERRIDE; virtual void OnAcceleratedCompositingStateChange() OVERRIDE; virtual void AcceleratedSurfaceBuffersSwapped( const GpuHostMsg_AcceleratedSurfaceBuffersSwapped_Params& params, diff --git a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java index f6b9d8d..0ad997d 100644 --- a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java +++ b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java @@ -197,13 +197,21 @@ public class ContentViewCore implements MotionEventDelegate { private SelectionHandleController mSelectionHandleController; private InsertionHandleController mInsertionHandleController; + // These offsets in document space with page scale normalized to 1.0. + private final PointF mStartHandleNormalizedPoint = new PointF(); + private final PointF mEndHandleNormalizedPoint = new PointF(); + private final PointF mInsertionHandleNormalizedPoint = new PointF(); // Tracks whether a selection is currently active. When applied to selected text, indicates // whether the last selected text is still highlighted. private boolean mHasSelection; private String mLastSelectedText; private boolean mSelectionEditable; - private ActionMode mActionMode; + // TODO(http://code.google.com/p/chromium/issues/detail?id=136704): Register + // the necessary resources in ContentViewActivity so we can create an action + // bar for text editing. + // private ActionMode mActionMode; + private boolean mActionBarVisible; // Remove this when mActionMode is upstreamed. // The legacy webview DownloadListener. private DownloadListener mDownloadListener; @@ -982,8 +990,10 @@ public class ContentViewCore implements MotionEventDelegate { } void hideSelectActionBar() { - if (mActionMode != null) { - mActionMode.finish(); + if (mActionBarVisible) { + mActionBarVisible = false; + mImeAdapter.unselect(); + getContentViewClient().onContextualActionBarHidden(); } } @@ -1335,6 +1345,8 @@ public class ContentViewCore implements MotionEventDelegate { if (!mPopupZoomer.isShowing()) mPopupZoomer.setLastTouch(x, y); if (isLongPress) { + getInsertionHandleController().allowAutomaticShowing(); + getSelectionHandleController().allowAutomaticShowing(); if (mNativeContentViewCore != 0) { nativeLongPress(mNativeContentViewCore, timeMs, x, y, false); } @@ -1425,8 +1437,28 @@ public class ContentViewCore implements MotionEventDelegate { private SelectionHandleController getSelectionHandleController() { if (mSelectionHandleController == null) { - mSelectionHandleController = new SelectionHandleController(getContainerView()); - // TODO(olilan): add specific method implementations. + mSelectionHandleController = new SelectionHandleController(getContainerView()) { + @Override + public void selectBetweenCoordinates(int x1, int y1, int x2, int y2) { + if (mNativeContentViewCore != 0 && !(x1 == x2 && y1 == y2)) { + nativeSelectBetweenCoordinates(mNativeContentViewCore, x1, y1, x2, y2); + } + } + + @Override + public void showHandlesAt(int x1, int y1, int dir1, int x2, int y2, int dir2) { + super.showHandlesAt(x1, y1, dir1, x2, y2, dir2); + mStartHandleNormalizedPoint.set( + (x1 + mNativeScrollX) / mNativePageScaleFactor, + (y1 + mNativeScrollY) / mNativePageScaleFactor); + mEndHandleNormalizedPoint.set( + (x2 + mNativeScrollX) / mNativePageScaleFactor, + (y2 + mNativeScrollY) / mNativePageScaleFactor); + + showSelectActionBar(); + } + + }; mSelectionHandleController.hideAndDisallowAutomaticShowing(); } @@ -1436,8 +1468,35 @@ public class ContentViewCore implements MotionEventDelegate { private InsertionHandleController getInsertionHandleController() { if (mInsertionHandleController == null) { - mInsertionHandleController = new InsertionHandleController(getContainerView()); - // TODO(olilan): add specific method implementations. + mInsertionHandleController = new InsertionHandleController(getContainerView()) { + private static final int AVERAGE_LINE_HEIGHT = 14; + + @Override + public void setCursorPosition(int x, int y) { + if (mNativeContentViewCore != 0) { + nativeSelectBetweenCoordinates(mNativeContentViewCore, x, y, x, y); + } + } + + @Override + public void paste() { + mImeAdapter.paste(); + hideHandles(); + } + + @Override + public int getLineHeight() { + return (int) (mNativePageScaleFactor * AVERAGE_LINE_HEIGHT); + } + + @Override + public void showHandleAt(int x, int y) { + super.showHandleAt(x, y); + mInsertionHandleNormalizedPoint.set( + (x + mNativeScrollX) / mNativePageScaleFactor, + (y + mNativeScrollY) / mNativePageScaleFactor); + } + }; mInsertionHandleController.hideAndDisallowAutomaticShowing(); } @@ -1445,7 +1504,42 @@ public class ContentViewCore implements MotionEventDelegate { return mInsertionHandleController; } + private void updateHandleScreenPositions() { + if (mSelectionHandleController != null && mSelectionHandleController.isShowing()) { + float startX = mStartHandleNormalizedPoint.x * mNativePageScaleFactor - mNativeScrollX; + float startY = mStartHandleNormalizedPoint.y * mNativePageScaleFactor - mNativeScrollY; + mSelectionHandleController.setStartHandlePosition((int) startX, (int) startY); + + float endX = mEndHandleNormalizedPoint.x * mNativePageScaleFactor - mNativeScrollX; + float endY = mEndHandleNormalizedPoint.y * mNativePageScaleFactor - mNativeScrollY; + mSelectionHandleController.setEndHandlePosition((int) endX, (int) endY); + } + + if (mInsertionHandleController != null && mInsertionHandleController.isShowing()) { + float x = mInsertionHandleNormalizedPoint.x * mNativePageScaleFactor - mNativeScrollX; + float y = mInsertionHandleNormalizedPoint.y * mNativePageScaleFactor - mNativeScrollY; + mInsertionHandleController.setHandlePosition((int) x, (int) y); + } + } + + private void hideHandles() { + if (mSelectionHandleController != null) { + mSelectionHandleController.hideAndDisallowAutomaticShowing(); + } + if (mInsertionHandleController != null) { + mInsertionHandleController.hideAndDisallowAutomaticShowing(); + } + } + private void showSelectActionBar() { + if (!mActionBarVisible) { + mActionBarVisible = true; + getContentViewClient().onContextualActionBarShown(); + } + +// TODO(http://code.google.com/p/chromium/issues/detail?id=136704): Uncomment +// this code when we have the resources needed to create the action bar. +/* if (mActionMode != null) { mActionMode.invalidate(); return; @@ -1500,6 +1594,7 @@ public class ContentViewCore implements MotionEventDelegate { } else { getContentViewClient().onContextualActionBarShown(); } +*/ } public boolean getUseDesktopUserAgent() { @@ -1616,6 +1711,56 @@ public class ContentViewCore implements MotionEventDelegate { @SuppressWarnings("unused") @CalledByNative + private void onSelectionChanged(String text) { + mLastSelectedText = text; + } + + @SuppressWarnings("unused") + @CalledByNative + private void onSelectionBoundsChanged(Rect startRect, int dir1, Rect endRect, int dir2) { + int x1 = startRect.left; + int y1 = startRect.bottom; + int x2 = endRect.left; + int y2 = endRect.bottom; + if (x1 != x2 || y1 != y2) { + if (mInsertionHandleController != null) { + mInsertionHandleController.hide(); + } + getSelectionHandleController().onSelectionChanged(x1, y1, dir1, x2, y2, dir2); + mHasSelection = true; + } else { + hideSelectActionBar(); + if (x1 != 0 && y1 != 0 + && (mSelectionHandleController == null + || !mSelectionHandleController.isDragging()) + && mSelectionEditable) { + // Selection is a caret, and a text field is focused. + if (mSelectionHandleController != null) { + mSelectionHandleController.hide(); + } + getInsertionHandleController().onCursorPositionChanged(x1, y1); + InputMethodManager manager = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (manager.isWatchingCursor(mContainerView)) { + manager.updateCursor(mContainerView, startRect.left, startRect.top, + startRect.right, startRect.bottom); + } + } else { + // Deselection + if (mSelectionHandleController != null + && !mSelectionHandleController.isDragging()) { + mSelectionHandleController.hideAndDisallowAutomaticShowing(); + } + if (mInsertionHandleController != null) { + mInsertionHandleController.hideAndDisallowAutomaticShowing(); + } + } + mHasSelection = false; + } + } + + @SuppressWarnings("unused") + @CalledByNative private void onEvaluateJavaScriptResult(int id, String jsonResult) { getContentViewClient().onEvaluateJavaScriptResult(id, jsonResult); } diff --git a/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java.orig b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java.orig new file mode 100644 index 0000000..f6b9d8d --- /dev/null +++ b/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java.orig @@ -0,0 +1,2078 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.os.SystemClock; +import android.util.Log; +import android.view.ActionMode; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.webkit.DownloadListener; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.WeakContext; +import org.chromium.content.app.AppResource; +import org.chromium.content.browser.ContentViewGestureHandler.MotionEventDelegate; +import org.chromium.content.browser.accessibility.AccessibilityInjector; +import org.chromium.content.common.CleanupReference; +import org.chromium.content.common.TraceEvent; +import org.chromium.ui.gfx.NativeWindow; + +/** + * Provides a Java-side 'wrapper' around a WebContent (native) instance. + * Contains all the major functionality necessary to manage the lifecycle of a ContentView without + * being tied to the view system. + */ +@JNINamespace("content") +public class ContentViewCore implements MotionEventDelegate { + private static final String TAG = ContentViewCore.class.getName(); + + // The following constants match the ones in chrome/common/page_transition_types.h. + // Add more if you need them. + public static final int PAGE_TRANSITION_LINK = 0; + public static final int PAGE_TRANSITION_TYPED = 1; + public static final int PAGE_TRANSITION_AUTO_BOOKMARK = 2; + public static final int PAGE_TRANSITION_START_PAGE = 6; + + // Used when ContentView implements a standalone View. + public static final int PERSONALITY_VIEW = 0; + // Used for Chrome. + public static final int PERSONALITY_CHROME = 1; + + // Used to avoid enabling zooming in / out if resulting zooming will + // produce little visible difference. + private static final float ZOOM_CONTROLS_EPSILON = 0.007f; + + // To avoid checkerboard, we clamp the fling velocity based on the maximum number of tiles + // should be allowed to upload per 100ms. + private final int mMaxNumUploadTiles = 12; + + // Personality of the ContentView. + private final int mPersonality; + + /** + * Interface that consumers of {@link ContentViewCore} must implement to allow the proper + * dispatching of view methods through the containing view. + * + * <p> + * All methods with the "super_" prefix should be routed to the parent of the + * implementing container view. + */ + @SuppressWarnings("javadoc") + public static interface InternalAccessDelegate { + /** + * @see View#drawChild(Canvas, View, long) + */ + boolean drawChild(Canvas canvas, View child, long drawingTime); + + /** + * @see View#onKeyUp(keyCode, KeyEvent) + */ + boolean super_onKeyUp(int keyCode, KeyEvent event); + + /** + * @see View#dispatchKeyEventPreIme(KeyEvent) + */ + boolean super_dispatchKeyEventPreIme(KeyEvent event); + + /** + * @see View#dispatchKeyEvent(KeyEvent) + */ + boolean super_dispatchKeyEvent(KeyEvent event); + + /** + * @see View#onGenericMotionEvent(MotionEvent) + */ + boolean super_onGenericMotionEvent(MotionEvent event); + + /** + * @see View#onConfigurationChanged(Configuration) + */ + void super_onConfigurationChanged(Configuration newConfig); + + /** + * @see View#onScrollChanged(int, int, int, int) + */ + void onScrollChanged(int l, int t, int oldl, int oldt); + + /** + * @see View#awakenScrollBars() + */ + boolean awakenScrollBars(); + + /** + * @see View#awakenScrollBars(int, boolean) + */ + boolean super_awakenScrollBars(int startDelay, boolean invalidate); + } + + private static final class DestroyRunnable implements Runnable { + private final int mNativeContentViewCore; + private DestroyRunnable(int nativeContentViewCore) { + mNativeContentViewCore = nativeContentViewCore; + } + @Override + public void run() { + nativeDestroy(mNativeContentViewCore); + } + } + + private CleanupReference mCleanupReference; + + private final Context mContext; + private ViewGroup mContainerView; + private InternalAccessDelegate mContainerViewInternals; + private WebContentsObserverAndroid mWebContentsObserver; + + private ContentViewClient mContentViewClient; + + private ContentSettings mContentSettings; + + // Native pointer to C++ ContentViewCoreImpl object which will be set by nativeInit(). + private int mNativeContentViewCore = 0; + + // The vsync monitor is used to lock the compositor to the display refresh rate. + private VSyncMonitor mVSyncMonitor; + + // The VSyncMonitor gives the timebase for the actual vsync, but we don't want render until + // we have had a chance for input events to propagate to the compositor thread. This takes + // 3 ms typically, so we adjust the vsync timestamps forward by a bit to give input events a + // chance to arrive. + private static final long INPUT_EVENT_LAG_FROM_VSYNC_MICROSECONDS = 3200; + + private ContentViewGestureHandler mContentViewGestureHandler; + private ZoomManager mZoomManager; + + // Currently ContentView's scrolling is handled by the native side. We keep a cached copy of the + // scroll offset and content size so that we can display the scrollbar correctly. In the future, + // we may switch to tile rendering and handle the scrolling in the Java level. + + // Cached scroll offset from the native + private int mNativeScrollX; + private int mNativeScrollY; + + // Cached content size from the native + private int mContentWidth; + private int mContentHeight; + + // Cached page scale factor from native + private float mNativePageScaleFactor = 1.0f; + private float mNativeMinimumScale = 1.0f; + private float mNativeMaximumScale = 1.0f; + + private PopupZoomer mPopupZoomer; + + private Runnable mFakeMouseMoveRunnable = null; + + // Only valid when focused on a text / password field. + private ImeAdapter mImeAdapter; + private ImeAdapter.AdapterInputConnection mInputConnection; + + private SelectionHandleController mSelectionHandleController; + private InsertionHandleController mInsertionHandleController; + + // Tracks whether a selection is currently active. When applied to selected text, indicates + // whether the last selected text is still highlighted. + private boolean mHasSelection; + private String mLastSelectedText; + private boolean mSelectionEditable; + private ActionMode mActionMode; + + // The legacy webview DownloadListener. + private DownloadListener mDownloadListener; + // ContentViewDownloadDelegate adds support for authenticated downloads + // and POST downloads. Embedders should prefer ContentViewDownloadDelegate + // over DownloadListener. + private ContentViewDownloadDelegate mDownloadDelegate; + + // Whether a physical keyboard is connected. + private boolean mKeyboardConnected; + + private final VSyncMonitor.Listener mVSyncListener = new VSyncMonitor.Listener() { + @Override + public void onVSync(VSyncMonitor monitor, long vsyncTimeMicros) { + TraceEvent.instant("VSync"); + if (mNativeContentViewCore == 0 || mVSyncMonitor == null) { + return; + } + // Compensate for input event lag. Input events are delivered immediately on pre-JB + // releases, so this adjustment is only done for later versions. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + vsyncTimeMicros += INPUT_EVENT_LAG_FROM_VSYNC_MICROSECONDS; + } + nativeUpdateVSyncParameters(mNativeContentViewCore, vsyncTimeMicros, + mVSyncMonitor.getVSyncPeriodInMicroseconds()); + } + }; + + // The AccessibilityInjector that handles loading Accessibility scripts into the web page. + private AccessibilityInjector mAccessibilityInjector; + + // Temporary notification to tell onSizeChanged to focus a form element, + // because the OSK was just brought up. + private boolean mFocusOnNextSizeChanged = false; + private boolean mUnfocusOnNextSizeChanged = false; + + private boolean mNeedUpdateOrientationChanged; + + // Used to keep track of whether we should try to undo the last zoom-to-textfield operation. + private boolean mScrolledAndZoomedFocusedEditableNode = false; + + // Whether we use hardware-accelerated drawing. + private boolean mHardwareAccelerated = false; + + /** + * Enable multi-process ContentView. This should be called by the application before + * constructing any ContentView instances. If enabled, ContentView will run renderers in + * separate processes up to the number of processes specified by maxRenderProcesses. If this is + * not called then the default is to run the renderer in the main application on a separate + * thread. + * + * @param context Context used to obtain the application context. + * @param maxRendererProcesses Limit on the number of renderers to use. Each tab runs in its own + * process until the maximum number of processes is reached. The special value of + * MAX_RENDERERS_SINGLE_PROCESS requests single-process mode where the renderer will run in the + * application process in a separate thread. If the special value MAX_RENDERERS_AUTOMATIC is + * used then the number of renderers will be determined based on the device memory class. The + * maximum number of allowed renderers is capped by MAX_RENDERERS_LIMIT. + * @return Whether the process actually needed to be initialized (false if already running). + */ + public static boolean enableMultiProcess(Context context, int maxRendererProcesses) { + return AndroidBrowserProcess.initContentViewProcess(context, maxRendererProcesses); + } + + /** + * Initialize the process as the platform browser. This must be called before accessing + * ContentView in order to treat this as a Chromium browser process. + * + * @param context Context used to obtain the application context. + * @param maxRendererProcesses Same as ContentView.enableMultiProcess() + * @return Whether the process actually needed to be initialized (false if already running). + */ + public static boolean initChromiumBrowserProcess(Context context, int maxRendererProcesses) { + return AndroidBrowserProcess.initChromiumBrowserProcess(context, maxRendererProcesses); + } + + /** + * Constructs a new ContentViewCore. Embedders must call initialize() after constructing + * a ContentViewCore and before using it. + * + * @param context The context used to create this. + * @param personality The type of ContentViewCore being created. + */ + public ContentViewCore(Context context, int personality) { + mContext = context; + + WeakContext.initializeWeakContext(context); + // By default, ContentView will initialize single process mode. The call to + // initContentViewProcess below is ignored if either the ContentView host called + // enableMultiProcess() or the platform browser called initChromiumBrowserProcess(). + AndroidBrowserProcess.initContentViewProcess( + context, AndroidBrowserProcess.MAX_RENDERERS_SINGLE_PROCESS); + + mPersonality = personality; + HeapStatsLogger.init(mContext.getApplicationContext()); + } + + /** + * @return The context used for creating this ContentViewCore. + */ + public Context getContext() { + return mContext; + } + + /** + * @return The ViewGroup that all view actions of this ContentViewCore should interact with. + */ + protected ViewGroup getContainerView() { + return mContainerView; + } + + private ImeAdapter createImeAdapter(Context context) { + return new ImeAdapter(context, getSelectionHandleController(), + getInsertionHandleController(), + new ImeAdapter.ViewEmbedder() { + @Override + public void onImeEvent(boolean isFinish) { + getContentViewClient().onImeEvent(); + if (!isFinish) { + undoScrollFocusedEditableNodeIntoViewIfNeeded(false); + } + } + + @Override + public void onSetFieldValue() { + scrollFocusedEditableNodeIntoView(); + } + + @Override + public void onDismissInput() { + } + + @Override + public View getAttachedView() { + return mContainerView; + } + + @Override + public ResultReceiver getNewShowKeyboardReceiver() { + return new ResultReceiver(new Handler()) { + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode == InputMethodManager.RESULT_SHOWN) { + // If OSK is newly shown, delay the form focus until + // the onSizeChanged (in order to adjust relative to the + // new size). + mFocusOnNextSizeChanged = true; + } else if (resultCode == + InputMethodManager.RESULT_UNCHANGED_SHOWN) { + // If the OSK was already there, focus the form immediately. + scrollFocusedEditableNodeIntoView(); + } else { + undoScrollFocusedEditableNodeIntoViewIfNeeded(false); + } + } + }; + } + } + ); + } + + /** + * Returns true if the given Activity has hardware acceleration enabled + * in its manifest, or in its foreground window. + * + * TODO(husky): Remove when initialize() is refactored (see TODO there) + * TODO(dtrainor) This is still used by other classes. Make sure to pull some version of this + * out before removing it. + */ + public static boolean hasHardwareAcceleration(Activity activity) { + // Has HW acceleration been enabled manually in the current window? + Window window = activity.getWindow(); + if (window != null) { + if ((window.getAttributes().flags + & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0) { + return true; + } + } + + // Has HW acceleration been enabled in the manifest? + try { + ActivityInfo info = activity.getPackageManager().getActivityInfo( + activity.getComponentName(), 0); + if ((info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0) { + return true; + } + } catch (PackageManager.NameNotFoundException e) { + Log.e("Chrome", "getActivityInfo(self) should not fail"); + } + + return false; + } + + /** + * Returns true if the given Context is a HW-accelerated Activity. + * + * TODO(husky): Remove when initialize() is refactored (see TODO there) + */ + private static boolean hasHardwareAcceleration(Context context) { + if (context instanceof Activity) { + return hasHardwareAcceleration((Activity) context); + } + return false; + } + + /** + * + * @param containerView The view that will act as a container for all views created by this. + * @param internalDispatcher Handles dispatching all hidden or super methods to the + * containerView. + * @param nativeWebContents A pointer to the native web contents. + * @param nativeWindow An instance of the NativeWindow. + * @param isAccessFromFileURLsGrantedByDefault Default WebSettings configuration. + */ + // Perform important post-construction set up of the ContentViewCore. + // We do not require the containing view in the constructor to allow embedders to create a + // ContentViewCore without having fully created its containing view. The containing view + // is a vital component of the ContentViewCore, so embedders must exercise caution in what + // they do with the ContentViewCore before calling initialize(). + // We supply the nativeWebContents pointer here rather than in the constructor to allow us + // to set the private browsing mode at a later point for the WebView implementation. + // Note that the caller remains the owner of the nativeWebContents and is responsible for + // deleting it after destroying the ContentViewCore. + public void initialize(ViewGroup containerView, InternalAccessDelegate internalDispatcher, + int nativeWebContents, NativeWindow nativeWindow, + boolean isAccessFromFileURLsGrantedByDefault) { + // Check whether to use hardware acceleration. This is a bit hacky, and + // only works if the Context is actually an Activity (as it is in the + // Chrome application). + // + // What we're doing here is checking whether the app has *requested* + // hardware acceleration by setting the appropriate flags. This does not + // necessarily mean we're going to *get* hardware acceleration -- that's + // up to the Android framework. + // + // TODO(husky): Once the native code has been updated so that the + // HW acceleration flag can be set dynamically (Grace is doing this), + // move this check into onAttachedToWindow(), where we can test for + // HW support directly. + mHardwareAccelerated = hasHardwareAcceleration(mContext); + mContainerView = containerView; + mNativeContentViewCore = nativeInit(mHardwareAccelerated, nativeWebContents, + nativeWindow.getNativePointer()); + mCleanupReference = new CleanupReference( + this, new DestroyRunnable(mNativeContentViewCore)); + mContentSettings = new ContentSettings( + this, mNativeContentViewCore, isAccessFromFileURLsGrantedByDefault); + initializeContainerView(internalDispatcher); + if (mPersonality == PERSONALITY_VIEW) { + setAllUserAgentOverridesInHistory(); + } + + mAccessibilityInjector = AccessibilityInjector.newInstance(this); + mAccessibilityInjector.addOrRemoveAccessibilityApisIfNecessary(); + + String contentDescription = "Web View"; + if (AppResource.STRING_CONTENT_VIEW_CONTENT_DESCRIPTION == 0) { + Log.w(TAG, "Setting contentDescription to 'Web View' as no value was specified."); + } else { + contentDescription = mContext.getResources().getString( + AppResource.STRING_CONTENT_VIEW_CONTENT_DESCRIPTION); + } + mContainerView.setContentDescription(contentDescription); + mWebContentsObserver = new WebContentsObserverAndroid(this) { + @Override + public void didStartLoading(String url) { + hidePopupDialog(); + } + }; + } + + /** + * Initializes the View that will contain all Views created by the ContentViewCore. + * + * @param internalDispatcher Handles dispatching all hidden or super methods to the + * containerView. + */ + private void initializeContainerView(InternalAccessDelegate internalDispatcher) { + TraceEvent.begin(); + mContainerViewInternals = internalDispatcher; + + mContainerView.setWillNotDraw(false); + mContainerView.setFocusable(true); + mContainerView.setFocusableInTouchMode(true); + mContainerView.setClickable(true); + + if (mPersonality == PERSONALITY_CHROME) { + // Doing this in PERSONALITY_VIEW mode causes rendering problems in our + // current WebView test case (the HTMLViewer application). + // TODO(benm): Figure out why this is the case. + if (mContainerView.getScrollBarStyle() == View.SCROLLBARS_INSIDE_OVERLAY) { + mContainerView.setHorizontalScrollBarEnabled(false); + mContainerView.setVerticalScrollBarEnabled(false); + } + } + + mZoomManager = new ZoomManager(mContext, this); + mZoomManager.updateMultiTouchSupport(); + mContentViewGestureHandler = new ContentViewGestureHandler(mContext, this, mZoomManager); + + mNativeScrollX = mNativeScrollY = 0; + mNativePageScaleFactor = 1.0f; + initPopupZoomer(mContext); + mImeAdapter = createImeAdapter(mContext); + mKeyboardConnected = mContainerView.getResources().getConfiguration().keyboard + != Configuration.KEYBOARD_NOKEYS; + mVSyncMonitor = new VSyncMonitor(mContext, mContainerView, mVSyncListener); + TraceEvent.end(); + } + + private void initPopupZoomer(Context context){ + mPopupZoomer = new PopupZoomer(context); + mContainerView.addView(mPopupZoomer); + PopupZoomer.OnTapListener listener = new PopupZoomer.OnTapListener() { + @Override + public boolean onSingleTap(View v, MotionEvent e) { + mContainerView.requestFocus(); + if (mNativeContentViewCore != 0) { + nativeSingleTap(mNativeContentViewCore, e.getEventTime(), (int) e.getX(), + (int) e.getY(), true); + } + return true; + } + + @Override + public boolean onLongPress(View v, MotionEvent e) { + if (mNativeContentViewCore != 0) { + nativeLongPress(mNativeContentViewCore, e.getEventTime(), (int) e.getX(), + (int) e.getY(), true); + } + return true; + } + }; + mPopupZoomer.setOnTapListener(listener); + } + + /** + * @return Whether the configured personality of this ContentView is {@link #PERSONALITY_VIEW}. + */ + boolean isPersonalityView() { + switch (mPersonality) { + case PERSONALITY_VIEW: + return true; + case PERSONALITY_CHROME: + return false; + default: + Log.e(TAG, "Unknown ContentView personality: " + mPersonality); + return false; + } + } + + /** + * Destroy the internal state of the ContentView. This method may only be + * called after the ContentView has been removed from the view system. No + * other methods may be called on this ContentView after this method has + * been called. + */ + public void destroy() { + hidePopupDialog(); + mCleanupReference.cleanupNow(); + mNativeContentViewCore = 0; + // Do not propagate the destroy() to settings, as the client may still hold a reference to + // that and could still be using it. + mContentSettings = null; + mVSyncMonitor.stop(); + } + + /** + * Returns true initially, false after destroy() has been called. + * It is illegal to call any other public method after destroy(). + */ + public boolean isAlive() { + return mNativeContentViewCore != 0; + } + + /** + * This is only useful for passing over JNI to native code that requires ContentViewCore*. + * @return native ContentViewCore pointer. + */ + public int getNativeContentViewCore() { + return mNativeContentViewCore; + } + + /** + * For internal use. Throws IllegalStateException if mNativeContentView is 0. + * Use this to ensure we get a useful Java stack trace, rather than a native + * crash dump, from use-after-destroy bugs in Java code. + */ + void checkIsAlive() throws IllegalStateException { + if (!isAlive()) { + throw new IllegalStateException("ContentView used after destroy() was called"); + } + } + + public void setContentViewClient(ContentViewClient client) { + if (client == null) { + throw new IllegalArgumentException("The client can't be null."); + } + mContentViewClient = client; + } + + ContentViewClient getContentViewClient() { + if (mContentViewClient == null) { + // We use the Null Object pattern to avoid having to perform a null check in this class. + // We create it lazily because most of the time a client will be set almost immediately + // after ContentView is created. + mContentViewClient = new ContentViewClient(); + // We don't set the native ContentViewClient pointer here on purpose. The native + // implementation doesn't mind a null delegate and using one is better than passing a + // Null Object, since we cut down on the number of JNI calls. + } + return mContentViewClient; + } + + public int getBackgroundColor() { + if (mNativeContentViewCore != 0) { + return nativeGetBackgroundColor(mNativeContentViewCore); + } + return Color.WHITE; + } + + public void setBackgroundColor(int color) { + if (mNativeContentViewCore != 0 && getBackgroundColor() != color) { + nativeSetBackgroundColor(mNativeContentViewCore, color); + } + } + + /** + * Load url without fixing up the url string. Consumers of ContentView are responsible for + * ensuring the URL passed in is properly formatted (i.e. the scheme has been added if left + * off during user input). + * + * @param pararms Parameters for this load. + */ + public void loadUrl(LoadUrlParams params) { + if (mNativeContentViewCore == 0) return; + if (isPersonalityView()) { + // For WebView, always use the user agent override, which is set + // every time the user agent in ContentSettings is modified. + params.setOverrideUserAgent(LoadUrlParams.UA_OVERRIDE_TRUE); + } + + nativeLoadUrl(mNativeContentViewCore, + params.mUrl, + params.mLoadUrlType, + params.mTransitionType, + params.mUaOverrideOption, + params.getExtraHeadersString(), + params.mPostData, + params.mBaseUrlForDataUrl, + params.mVirtualUrlForDataUrl); + } + + void setAllUserAgentOverridesInHistory() { + nativeSetAllUserAgentOverridesInHistory(mNativeContentViewCore, + mContentSettings.getUserAgentString()); + } + + /** + * Stops loading the current web contents. + */ + public void stopLoading() { + if (mNativeContentViewCore != 0) nativeStopLoading(mNativeContentViewCore); + } + + /** + * Get the URL of the current page. + * + * @return The URL of the current page. + */ + public String getUrl() { + if (mNativeContentViewCore != 0) return nativeGetURL(mNativeContentViewCore); + return null; + } + + /** + * Get the title of the current page. + * + * @return The title of the current page. + */ + public String getTitle() { + if (mNativeContentViewCore != 0) return nativeGetTitle(mNativeContentViewCore); + return null; + } + + public int getWidth() { + return mContainerView.getWidth(); + } + + public int getHeight() { + return mContainerView.getHeight(); + } + + /** + * @see android.webkit.WebView#getContentHeight() + */ + public int getContentHeight() { + return (int) (mContentHeight / mNativePageScaleFactor); + } + + /** + * @see android.webkit.WebView#getContentWidth() + */ + public int getContentWidth() { + return (int) (mContentWidth / mNativePageScaleFactor); + } + + public Bitmap getBitmap() { + return getBitmap(getWidth(), getHeight()); + } + + public Bitmap getBitmap(int width, int height) { + return getBitmap(width, height, Bitmap.Config.ARGB_8888); + } + + public Bitmap getBitmap(int width, int height, Bitmap.Config config) { + // TODO(nileshagrawal): Implement this. + return null; + } + + /** + * @return Whether the ContentView is covered by an overlay that is more than half + * of it's surface. This is used to determine if we need to do a slow bitmap capture or + * to show the ContentView without them. + */ + public boolean hasLargeOverlay() { + // TODO(nileshagrawal): Implement this. + return false; + } + + /** + * @return Whether the current WebContents has a previous navigation entry. + */ + public boolean canGoBack() { + return mNativeContentViewCore != 0 && nativeCanGoBack(mNativeContentViewCore); + } + + /** + * @return Whether the current WebContents has a navigation entry after the current one. + */ + public boolean canGoForward() { + return mNativeContentViewCore != 0 && nativeCanGoForward(mNativeContentViewCore); + } + + /** + * @param offset The offset into the navigation history. + * @return Whether we can move in history by given offset + */ + public boolean canGoToOffset(int offset) { + return mNativeContentViewCore != 0 && nativeCanGoToOffset(mNativeContentViewCore, offset); + } + + /** + * Navigates to the specified offset from the "current entry". Does nothing if the offset is out + * of bounds. + * @param offset The offset into the navigation history. + */ + public void goToOffset(int offset) { + if (mNativeContentViewCore != 0) nativeGoToOffset(mNativeContentViewCore, offset); + } + + /** + * Goes to the navigation entry before the current one. + */ + public void goBack() { + if (mNativeContentViewCore != 0) nativeGoBack(mNativeContentViewCore); + } + + /** + * Goes to the navigation entry following the current one. + */ + public void goForward() { + if (mNativeContentViewCore != 0) nativeGoForward(mNativeContentViewCore); + } + + /** + * Reload the current page. + */ + public void reload() { + mAccessibilityInjector.addOrRemoveAccessibilityApisIfNecessary(); + if (mNativeContentViewCore != 0) nativeReload(mNativeContentViewCore); + } + + /** + * Clears the ContentViewCore's page history in both the backwards and + * forwards directions. + */ + public void clearHistory() { + if (mNativeContentViewCore != 0) nativeClearHistory(mNativeContentViewCore); + } + + String getSelectedText() { + return mHasSelection ? mLastSelectedText : ""; + } + + // End FrameLayout overrides. + + /** + * @see {@link android.webkit.WebView#flingScroll(int, int)} + */ + public void flingScroll(int vx, int vy) { + // Notes: + // (1) Use large negative values for the x/y parameters so we don't accidentally scroll a + // nested frame. + // (2) vx and vy are inverted to match WebView behavior. + mContentViewGestureHandler.fling( + System.currentTimeMillis(), -Integer.MAX_VALUE, -Integer.MIN_VALUE, -vx, -vy); + } + + /** + * @see View#onTouchEvent(MotionEvent) + */ + public boolean onTouchEvent(MotionEvent event) { + undoScrollFocusedEditableNodeIntoViewIfNeeded(false); + return mContentViewGestureHandler.onTouchEvent(event); + } + + /** + * @return ContentViewGestureHandler for all MotionEvent and gesture related calls. + */ + ContentViewGestureHandler getContentViewGestureHandler() { + return mContentViewGestureHandler; + } + + @Override + public boolean sendTouchEvent(long timeMs, int action, TouchPoint[] pts) { + if (mNativeContentViewCore != 0) { + return nativeSendTouchEvent(mNativeContentViewCore, timeMs, action, pts); + } + return false; + } + + @SuppressWarnings("unused") + @CalledByNative + private void didSetNeedTouchEvents(boolean needTouchEvents) { + mContentViewGestureHandler.didSetNeedTouchEvents(needTouchEvents); + } + + @SuppressWarnings("unused") + @CalledByNative + private void confirmTouchEvent(boolean handled) { + mContentViewGestureHandler.confirmTouchEvent(handled); + } + + @Override + public boolean sendGesture(int type, long timeMs, int x, int y, Bundle b) { + if (mNativeContentViewCore == 0) return false; + + switch (type) { + case ContentViewGestureHandler.GESTURE_SHOW_PRESSED_STATE: + nativeShowPressState(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_DOUBLE_TAP: + nativeDoubleTap(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_SINGLE_TAP_UP: + nativeSingleTap(mNativeContentViewCore, timeMs, x, y, false); + return true; + case ContentViewGestureHandler.GESTURE_SINGLE_TAP_CONFIRMED: + handleTapOrPress(timeMs, x, y, false, + b.getBoolean(ContentViewGestureHandler.SHOW_PRESS, false)); + return true; + case ContentViewGestureHandler.GESTURE_LONG_PRESS: + handleTapOrPress(timeMs, x, y, true, false); + return true; + case ContentViewGestureHandler.GESTURE_SCROLL_START: + nativeScrollBegin(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_SCROLL_BY: { + int dx = b.getInt(ContentViewGestureHandler.DISTANCE_X); + int dy = b.getInt(ContentViewGestureHandler.DISTANCE_Y); + nativeScrollBy(mNativeContentViewCore, timeMs, x, y, dx, dy); + return true; + } + case ContentViewGestureHandler.GESTURE_SCROLL_END: + nativeScrollEnd(mNativeContentViewCore, timeMs); + return true; + case ContentViewGestureHandler.GESTURE_FLING_START: + nativeFlingStart(mNativeContentViewCore, timeMs, x, y, + clampFlingVelocityX(b.getInt(ContentViewGestureHandler.VELOCITY_X, 0)), + clampFlingVelocityY(b.getInt(ContentViewGestureHandler.VELOCITY_Y, 0))); + return true; + case ContentViewGestureHandler.GESTURE_FLING_CANCEL: + nativeFlingCancel(mNativeContentViewCore, timeMs); + return true; + case ContentViewGestureHandler.GESTURE_PINCH_BEGIN: + nativePinchBegin(mNativeContentViewCore, timeMs, x, y); + return true; + case ContentViewGestureHandler.GESTURE_PINCH_BY: + nativePinchBy(mNativeContentViewCore, timeMs, x, y, + b.getFloat(ContentViewGestureHandler.DELTA, 0)); + return true; + case ContentViewGestureHandler.GESTURE_PINCH_END: + nativePinchEnd(mNativeContentViewCore, timeMs); + return true; + default: + return false; + } + } + + /** + * Injects the passed JavaScript code in the current page and evaluates it. + * Once evaluated, an asynchronous call to + * ContentViewClient.onJavaScriptEvaluationResult is made. Used in automation + * tests. + * + * @return an id that is passed along in the asynchronous onJavaScriptEvaluationResult callback + * @throws IllegalStateException If the ContentView has been destroyed. + * @hide + */ + public int evaluateJavaScript(String script) throws IllegalStateException { + checkIsAlive(); + return nativeEvaluateJavaScript(script); + } + + /** + * This method should be called when the containing activity is paused. + */ + public void onActivityPause() { + TraceEvent.begin(); + hidePopupDialog(); + nativeOnHide(mNativeContentViewCore); + setAccessibilityState(false); + TraceEvent.end(); + mVSyncMonitor.stop(); + } + + /** + * This method should be called when the containing activity is resumed. + */ + public void onActivityResume() { + nativeOnShow(mNativeContentViewCore); + setAccessibilityState(true); + } + + /** + * To be called when the ContentView is shown. + */ + public void onShow() { + nativeOnShow(mNativeContentViewCore); + setAccessibilityState(true); + } + + /** + * To be called when the ContentView is hidden. + */ + public void onHide() { + hidePopupDialog(); + setAccessibilityState(false); + nativeOnHide(mNativeContentViewCore); + mVSyncMonitor.stop(); + } + + /** + * Return the ContentSettings object used to control the settings for this + * ContentViewCore. + * + * Note that when ContentView is used in the PERSONALITY_CHROME role, + * ContentSettings can only be used for retrieving settings values. For + * modifications, ChromeNativePreferences is to be used. + * @return A ContentSettings object that can be used to control this + * ContentViewCore's settings. + */ + public ContentSettings getContentSettings() { + return mContentSettings; + } + + @Override + public boolean didUIStealScroll(float x, float y) { + return getContentViewClient().shouldOverrideScroll( + x, y, computeHorizontalScrollOffset(), computeVerticalScrollOffset()); + } + + private void hidePopupDialog() { + SelectPopupDialog.hide(this); + } + + void hideSelectActionBar() { + if (mActionMode != null) { + mActionMode.finish(); + } + } + + /** + * @see View#onAttachedToWindow() + */ + @SuppressWarnings("javadoc") + protected void onAttachedToWindow() { + setAccessibilityState(true); + } + + /** + * @see View#onDetachedFromWindow() + */ + @SuppressWarnings("javadoc") + protected void onDetachedFromWindow() { + setAccessibilityState(false); + } + + @CalledByNative + private void onWebPreferencesUpdated() { + // TODO(nileshagrawal): Implement this. + } + + /** + * @see View#onCreateInputConnection(EditorInfo) + */ + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + if (!mImeAdapter.hasTextInputType()) { + // Although onCheckIsTextEditor will return false in this case, the EditorInfo + // is still used by the InputMethodService. Need to make sure the IME doesn't + // enter fullscreen mode. + outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; + } + mInputConnection = ImeAdapter.AdapterInputConnection.getInstance( + mContainerView, mImeAdapter, outAttrs); + return mInputConnection; + } + + /** + * @see View#onCheckIsTextEditor() + */ + public boolean onCheckIsTextEditor() { + return mImeAdapter.hasTextInputType(); + } + + /** + * @see View#onConfigurationChanged(Configuration) + */ + @SuppressWarnings("javadoc") + public void onConfigurationChanged(Configuration newConfig) { + TraceEvent.begin(); + + mKeyboardConnected = newConfig.keyboard != Configuration.KEYBOARD_NOKEYS; + + if (mKeyboardConnected) { + mImeAdapter.attach(nativeGetNativeImeAdapter(mNativeContentViewCore), + ImeAdapter.sTextInputTypeNone); + InputMethodManager manager = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + manager.restartInput(mContainerView); + } + mContainerViewInternals.super_onConfigurationChanged(newConfig); + mNeedUpdateOrientationChanged = true; + TraceEvent.end(); + } + + /** + * @see View#onSizeChanged(int, int, int, int) + */ + @SuppressWarnings("javadoc") + public void onSizeChanged(int w, int h, int ow, int oh) { + mPopupZoomer.hide(false); + // Update the content size to make sure it is at least the View size + if (mContentWidth < w) mContentWidth = w; + if (mContentHeight < h) mContentHeight = h; + } + + public void updateAfterSizeChanged() { + // Execute a delayed form focus operation because the OSK was brought + // up earlier. + if (mFocusOnNextSizeChanged) { + scrollFocusedEditableNodeIntoView(); + mFocusOnNextSizeChanged = false; + } else if (mUnfocusOnNextSizeChanged) { + undoScrollFocusedEditableNodeIntoViewIfNeeded(true); + mUnfocusOnNextSizeChanged = false; + } + } + + private void scrollFocusedEditableNodeIntoView() { + if (mNativeContentViewCore != 0) { + Runnable scrollTask = new Runnable() { + @Override + public void run() { + if (mNativeContentViewCore != 0) { + nativeScrollFocusedEditableNodeIntoView(mNativeContentViewCore); + } + } + }; + + scrollTask.run(); + + // The native side keeps track of whether the zoom and scroll actually occurred. It is + // more efficient to do it this way and sometimes fire an unnecessary message rather + // than synchronize with the renderer and always have an additional message. + mScrolledAndZoomedFocusedEditableNode = true; + } + } + + private void undoScrollFocusedEditableNodeIntoViewIfNeeded(boolean backButtonPressed) { + // The only call to this function that matters is the first call after the + // scrollFocusedEditableNodeIntoView function call. + // If the first call to this function is a result of a back button press we want to undo the + // preceding scroll. If the call is a result of some other action we don't want to perform + // an undo. + // All subsequent calls are ignored since only the scroll function sets + // mScrolledAndZoomedFocusedEditableNode to true. + if (mScrolledAndZoomedFocusedEditableNode && backButtonPressed && + mNativeContentViewCore != 0) { + Runnable scrollTask = new Runnable() { + @Override + public void run() { + if (mNativeContentViewCore != 0) { + nativeUndoScrollFocusedEditableNodeIntoView(mNativeContentViewCore); + } + } + }; + + scrollTask.run(); + } + mScrolledAndZoomedFocusedEditableNode = false; + } + + /** + * @see View#onFocusedChanged(boolean, int, Rect) + */ + @SuppressWarnings("javadoc") + public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + if (mNativeContentViewCore != 0) nativeSetFocus(mNativeContentViewCore, gainFocus); + } + + /** + * @see View#onKeyUp(int, KeyEvent) + */ + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (mPopupZoomer.isShowing() && keyCode == KeyEvent.KEYCODE_BACK) { + mPopupZoomer.hide(true); + return true; + } + return mContainerViewInternals.super_onKeyUp(keyCode, event); + } + + /** + * @see View#dispatchKeyEventPreIme(KeyEvent) + */ + public boolean dispatchKeyEventPreIme(KeyEvent event) { + try { + TraceEvent.begin(); + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && mImeAdapter.isActive()) { + mUnfocusOnNextSizeChanged = true; + } else { + undoScrollFocusedEditableNodeIntoViewIfNeeded(false); + } + mImeAdapter.dispatchKeyEventPreIme(event); + return mContainerViewInternals.super_dispatchKeyEventPreIme(event); + } finally { + TraceEvent.end(); + } + } + + /** + * @see View#dispatchKeyEvent(KeyEvent) + */ + public boolean dispatchKeyEvent(KeyEvent event) { + if (mImeAdapter != null && + !mImeAdapter.isNativeImeAdapterAttached() && mNativeContentViewCore != 0) { + mImeAdapter.attach(nativeGetNativeImeAdapter(mNativeContentViewCore)); + } + // The key handling logic is kind of confusing here. + // The purpose of shouldOverrideKeyEvent() is to filter out some keys that is critical + // to browser function but useless in renderer process (for example, the back button), + // so the browser can still respond to these keys in a timely manner when the renderer + // process is busy/blocked/busted. mImeAdapter.dispatchKeyEvent() forwards the key event + // to the renderer process. If mImeAdapter is bypassed or is not interested to the event, + // fall back to the default dispatcher to propagate the event to sub-views. + return (!getContentViewClient().shouldOverrideKeyEvent(event) + && mImeAdapter.dispatchKeyEvent(event)) + || mContainerViewInternals.super_dispatchKeyEvent(event); + } + + /** + * @see View#onHoverEvent(MotionEvent) + * Mouse move events are sent on hover enter, hover move and hover exit. + * They are sent on hover exit because sometimes it acts as both a hover + * move and hover exit. + */ + public boolean onHoverEvent(MotionEvent event) { + TraceEvent.begin("onHoverEvent"); + mContainerView.removeCallbacks(mFakeMouseMoveRunnable); + if (mNativeContentViewCore != 0) { + nativeSendMouseMoveEvent(mNativeContentViewCore, event.getEventTime(), + (int) event.getX(), (int) event.getY()); + } + TraceEvent.end("onHoverEvent"); + return true; + } + + /** + * @see View#onGenericMotionEvent(MotionEvent) + */ + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: + nativeSendMouseWheelEvent(mNativeContentViewCore, event.getEventTime(), + (int) event.getX(), (int) event.getY(), + event.getAxisValue(MotionEvent.AXIS_VSCROLL)); + + mContainerView.removeCallbacks(mFakeMouseMoveRunnable); + // Send a delayed onMouseMove event so that we end + // up hovering over the right position after the scroll. + final MotionEvent eventFakeMouseMove = MotionEvent.obtain(event); + mFakeMouseMoveRunnable = new Runnable() { + @Override + public void run() { + onHoverEvent(eventFakeMouseMove); + } + }; + mContainerView.postDelayed(mFakeMouseMoveRunnable, 250); + return true; + } + } + return mContainerViewInternals.super_onGenericMotionEvent(event); + } + + /** + * @see View#scrollBy(int, int) + * Currently the ContentView scrolling happens in the native side. In + * the Java view system, it is always pinned at (0, 0). scrollBy() and scrollTo() + * are overridden, so that View's mScrollX and mScrollY will be unchanged at + * (0, 0). This is critical for drawing ContentView correctly. + */ + public void scrollBy(int x, int y) { + if (mNativeContentViewCore != 0) { + nativeScrollBy(mNativeContentViewCore, System.currentTimeMillis(), 0, 0, x, y); + } + } + + /** + * @see View#scrollTo(int, int) + */ + public void scrollTo(int x, int y) { + if (mNativeContentViewCore == 0) return; + int dx = x - mNativeScrollX, dy = y - mNativeScrollY; + if (dx != 0 || dy != 0) { + long time = System.currentTimeMillis(); + nativeScrollBegin(mNativeContentViewCore, time, mNativeScrollX, mNativeScrollY); + nativeScrollBy(mNativeContentViewCore, time, mNativeScrollX, mNativeScrollY, + dx, dy); + nativeScrollEnd(mNativeContentViewCore, time); + } + } + + // NOTE: this can go away once ContentView.getScrollX() reports correct values. + // see: b/6029133 + public int getNativeScrollXForTest() { + return mNativeScrollX; + } + + // NOTE: this can go away once ContentView.getScrollY() reports correct values. + // see: b/6029133 + public int getNativeScrollYForTest() { + return mNativeScrollY; + } + + /** + * @see View#computeHorizontalScrollExtent() + */ + @SuppressWarnings("javadoc") + protected int computeHorizontalScrollExtent() { + // TODO (dtrainor): Need to expose scroll events properly to public. Either make getScroll* + // work or expose computeHorizontalScrollOffset()/computeVerticalScrollOffset as public. + return getWidth(); + } + + /** + * @see View#computeHorizontalScrollOffset() + */ + @SuppressWarnings("javadoc") + public int computeHorizontalScrollOffset() { + return mNativeScrollX; + } + + /** + * @see View#computeHorizontalScrollRange() + */ + @SuppressWarnings("javadoc") + public int computeHorizontalScrollRange() { + return mContentWidth; + } + + /** + * @see View#computeVerticalScrollExtent() + */ + @SuppressWarnings("javadoc") + public int computeVerticalScrollExtent() { + return getHeight(); + } + + /** + * @see View#computeVerticalScrollOffset() + */ + @SuppressWarnings("javadoc") + public int computeVerticalScrollOffset() { + return mNativeScrollY; + } + + /** + * @see View#computeVerticalScrollRange() + */ + @SuppressWarnings("javadoc") + public int computeVerticalScrollRange() { + return mContentHeight; + } + + // End FrameLayout overrides. + + /** + * @see View#awakenScrollBars(int, boolean) + */ + @SuppressWarnings("javadoc") + protected boolean awakenScrollBars(int startDelay, boolean invalidate) { + // For the default implementation of ContentView which draws the scrollBars on the native + // side, calling this function may get us into a bad state where we keep drawing the + // scrollBars, so disable it by always returning false. + if (mContainerView.getScrollBarStyle() == View.SCROLLBARS_INSIDE_OVERLAY) { + return false; + } else { + return mContainerViewInternals.super_awakenScrollBars(startDelay, invalidate); + } + } + + private void handleTapOrPress( + long timeMs, int x, int y, boolean isLongPress, boolean showPress) { + //TODO(yusufo):Upstream the rest of the bits about handlerControllers. + if (!mContainerView.isFocused()) mContainerView.requestFocus(); + + if (!mPopupZoomer.isShowing()) mPopupZoomer.setLastTouch(x, y); + + if (isLongPress) { + if (mNativeContentViewCore != 0) { + nativeLongPress(mNativeContentViewCore, timeMs, x, y, false); + } + } else { + if (!showPress && mNativeContentViewCore != 0) { + nativeShowPressState(mNativeContentViewCore, timeMs, x, y); + } + if (mNativeContentViewCore != 0) { + nativeSingleTap(mNativeContentViewCore, timeMs, x, y, false); + } + } + } + + void updateMultiTouchZoomSupport() { + mZoomManager.updateMultiTouchSupport(); + } + + public boolean isMultiTouchZoomSupported() { + return mZoomManager.isMultiTouchZoomSupported(); + } + + void selectPopupMenuItems(int[] indices) { + if (mNativeContentViewCore != 0) { + nativeSelectPopupMenuItems(mNativeContentViewCore, indices); + } + } + + /* + * To avoid checkerboard, we clamp the fling velocity based on the maximum number of tiles + * allowed to be uploaded per 100ms. Calculation is limited to one direction. We assume the + * tile size is 256x256. The precise distance / velocity should be calculated based on the + * logic in Scroller.java. As it is almost linear for the first 100ms, we use a simple math. + */ + private int clampFlingVelocityX(int velocity) { + int cols = mMaxNumUploadTiles / (int) (Math.ceil((float) getHeight() / 256) + 1); + int maxVelocity = cols > 0 ? cols * 2560 : 1000; + if (Math.abs(velocity) > maxVelocity) { + return velocity > 0 ? maxVelocity : -maxVelocity; + } else { + return velocity; + } + } + + private int clampFlingVelocityY(int velocity) { + int rows = mMaxNumUploadTiles / (int) (Math.ceil((float) getWidth() / 256) + 1); + int maxVelocity = rows > 0 ? rows * 2560 : 1000; + if (Math.abs(velocity) > maxVelocity) { + return velocity > 0 ? maxVelocity : -maxVelocity; + } else { + return velocity; + } + } + + /** + * Register the listener to be used when content can not be handled by the + * rendering engine, and should be downloaded instead. This will replace the + * current listener. + * @param listener An implementation of DownloadListener. + */ + // TODO(nileshagrawal): decide if setDownloadDelegate will be public API. If so, + // this method should be deprecated and the javadoc should make reference to the + // fact that a ContentViewDownloadDelegate will be used in preference to a + // DownloadListener. + public void setDownloadListener(DownloadListener listener) { + mDownloadListener = listener; + } + + // Called by DownloadController. + DownloadListener downloadListener() { + return mDownloadListener; + } + + /** + * Register the delegate to be used when content can not be handled by + * the rendering engine, and should be downloaded instead. This will replace + * the current delegate or existing DownloadListner. + * Embedders should prefer this over the legacy DownloadListener. + * @param listener An implementation of ContentViewDownloadDelegate. + */ + public void setDownloadDelegate(ContentViewDownloadDelegate delegate) { + mDownloadDelegate = delegate; + } + + // Called by DownloadController. + ContentViewDownloadDelegate getDownloadDelegate() { + return mDownloadDelegate; + } + + private SelectionHandleController getSelectionHandleController() { + if (mSelectionHandleController == null) { + mSelectionHandleController = new SelectionHandleController(getContainerView()); + // TODO(olilan): add specific method implementations. + + mSelectionHandleController.hideAndDisallowAutomaticShowing(); + } + + return mSelectionHandleController; + } + + private InsertionHandleController getInsertionHandleController() { + if (mInsertionHandleController == null) { + mInsertionHandleController = new InsertionHandleController(getContainerView()); + // TODO(olilan): add specific method implementations. + + mInsertionHandleController.hideAndDisallowAutomaticShowing(); + } + + return mInsertionHandleController; + } + + private void showSelectActionBar() { + if (mActionMode != null) { + mActionMode.invalidate(); + return; + } + + // Start a new action mode with a SelectActionModeCallback. + SelectActionModeCallback.ActionHandler actionHandler = + new SelectActionModeCallback.ActionHandler() { + @Override + public boolean selectAll() { + return mImeAdapter.selectAll(); + } + + @Override + public boolean cut() { + return mImeAdapter.cut(); + } + + @Override + public boolean copy() { + return mImeAdapter.copy(); + } + + @Override + public boolean paste() { + return mImeAdapter.paste(); + } + + @Override + public boolean isSelectionEditable() { + return mSelectionEditable; + } + + @Override + public String getSelectedText() { + return ContentViewCore.this.getSelectedText(); + } + + @Override + public void onDestroyActionMode() { + mActionMode = null; + mImeAdapter.unselect(); + getContentViewClient().onContextualActionBarHidden(); + } + }; + mActionMode = mContainerView.startActionMode( + getContentViewClient().getSelectActionModeCallback(getContext(), actionHandler, + nativeIsIncognito(mNativeContentViewCore))); + if (mActionMode == null) { + // There is no ActionMode, so remove the selection. + mImeAdapter.unselect(); + } else { + getContentViewClient().onContextualActionBarShown(); + } + } + + public boolean getUseDesktopUserAgent() { + if (mNativeContentViewCore != 0) { + return nativeGetUseDesktopUserAgent(mNativeContentViewCore); + } + return false; + } + + /** + * Set whether or not we're using a desktop user agent for the currently loaded page. + * @param override If true, use a desktop user agent. Use a mobile one otherwise. + * @param reloadOnChange Reload the page if the UA has changed. + */ + public void setUseDesktopUserAgent(boolean override, boolean reloadOnChange) { + if (mNativeContentViewCore != 0) { + nativeSetUseDesktopUserAgent(mNativeContentViewCore, override, reloadOnChange); + } + } + + /** + * @return Whether the native ContentView has crashed. + */ + public boolean isCrashed() { + if (mNativeContentViewCore == 0) return false; + return nativeCrashed(mNativeContentViewCore); + } + + @SuppressWarnings("unused") + @CalledByNative + private void updateContentSize(int width, int height) { + if (mContentWidth != width || mContentHeight != height) { + mPopupZoomer.hide(true); + } + // Make sure the content size is at least the View size + mContentWidth = Math.max(width, getWidth()); + mContentHeight = Math.max(height, getHeight()); + } + + @SuppressWarnings("unused") + @CalledByNative + private void updateScrollOffsetAndPageScaleFactor(int x, int y, float scale) { + if (mNativeScrollX == x && mNativeScrollY == y && mNativePageScaleFactor == scale) return; + + mContainerViewInternals.onScrollChanged(x, y, mNativeScrollX, mNativeScrollY); + + // This function should be called back from native as soon + // as the scroll is applied to the backbuffer. We should only + // update mNativeScrollX/Y here for consistency. + mNativeScrollX = x; + mNativeScrollY = y; + mNativePageScaleFactor = scale; + + mPopupZoomer.hide(true); + + mZoomManager.updateZoomControls(); + } + + @SuppressWarnings("unused") + @CalledByNative + private void updatePageScaleLimits(float minimumScale, float maximumScale) { + mNativeMinimumScale = minimumScale; + mNativeMaximumScale = maximumScale; + mZoomManager.updateZoomControls(); + } + + @SuppressWarnings("unused") + @CalledByNative + private void imeUpdateAdapter(int nativeImeAdapterAndroid, int textInputType, + String text, int selectionStart, int selectionEnd, + int compositionStart, int compositionEnd, boolean showImeIfNeeded) { + TraceEvent.begin(); + + // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces. + text = text.replace('\u00A0', ' '); + + mSelectionEditable = (textInputType != ImeAdapter.sTextInputTypeNone); + + mImeAdapter.attachAndShowIfNeeded(nativeImeAdapterAndroid, textInputType, + text, showImeIfNeeded); + + if (mInputConnection != null) { + // In WebKit if there's a composition then the selection will usually be the + // same as the composition, whereas Android IMEs expect the selection to be + // just a caret at the end of the composition. + if (selectionStart == compositionStart && selectionEnd == compositionEnd) { + selectionStart = selectionEnd; + } + mInputConnection.setEditableText(text, selectionStart, selectionEnd, + compositionStart, compositionEnd); + } + TraceEvent.end(); + } + + @SuppressWarnings("unused") + @CalledByNative + private void setTitle(String title) { + getContentViewClient().onUpdateTitle(title); + } + + /** + * Called (from native) when the <select> popup needs to be shown. + * @param items Items to show. + * @param enabled POPUP_ITEM_TYPEs for items. + * @param multiple Whether the popup menu should support multi-select. + * @param selectedIndices Indices of selected items. + */ + @SuppressWarnings("unused") + @CalledByNative + private void showSelectPopup(String[] items, int[] enabled, boolean multiple, + int[] selectedIndices) { + SelectPopupDialog.show(this, items, enabled, multiple, selectedIndices); + } + + @SuppressWarnings("unused") + @CalledByNative + private void onEvaluateJavaScriptResult(int id, String jsonResult) { + getContentViewClient().onEvaluateJavaScriptResult(id, jsonResult); + } + + /** + * @return Whether a reload happens when this ContentView is activated. + */ + public boolean needsReload() { + return mNativeContentViewCore != 0 && nativeNeedsReload(mNativeContentViewCore); + } + + /** + * @see View#hasFocus() + */ + @CalledByNative + public boolean hasFocus() { + return mContainerView.hasFocus(); + } + + /** + * Checks whether the ContentViewCore can be zoomed in. + * + * @return True if the ContentViewCore can be zoomed in. + */ + // This method uses the term 'zoom' for legacy reasons, but relates + // to what chrome calls the 'page scale factor'. + public boolean canZoomIn() { + return mNativeMaximumScale - mNativePageScaleFactor > ZOOM_CONTROLS_EPSILON; + } + + /** + * Checks whether the ContentViewCore can be zoomed out. + * + * @return True if the ContentViewCore can be zoomed out. + */ + // This method uses the term 'zoom' for legacy reasons, but relates + // to what chrome calls the 'page scale factor'. + public boolean canZoomOut() { + return mNativePageScaleFactor - mNativeMinimumScale > ZOOM_CONTROLS_EPSILON; + } + + /** + * Zooms in the ContentViewCore by 25% (or less if that would result in + * zooming in more than possible). + * + * @return True if there was a zoom change, false otherwise. + */ + // This method uses the term 'zoom' for legacy reasons, but relates + // to what chrome calls the 'page scale factor'. + public boolean zoomIn() { + if (!canZoomIn()) { + return false; + } + return zoomByDelta(1.25f); + } + + /** + * Zooms out the ContentViewCore by 20% (or less if that would result in + * zooming out more than possible). + * + * @return True if there was a zoom change, false otherwise. + */ + // This method uses the term 'zoom' for legacy reasons, but relates + // to what chrome calls the 'page scale factor'. + public boolean zoomOut() { + if (!canZoomOut()) { + return false; + } + return zoomByDelta(0.8f); + } + + /** + * Resets the zoom factor of the ContentViewCore. + * + * @return True if there was a zoom change, false otherwise. + */ + // This method uses the term 'zoom' for legacy reasons, but relates + // to what chrome calls the 'page scale factor'. + public boolean zoomReset() { + // The page scale factor is initialized to mNativeMinimumScale when + // the page finishes loading. Thus sets it back to mNativeMinimumScale. + if (mNativePageScaleFactor - mNativeMinimumScale < ZOOM_CONTROLS_EPSILON) { + return false; + } + return zoomByDelta(mNativeMinimumScale / mNativePageScaleFactor); + } + + private boolean zoomByDelta(float delta) { + if (mNativeContentViewCore == 0) { + return false; + } + + long timeMs = System.currentTimeMillis(); + int x = getWidth() / 2; + int y = getHeight() / 2; + + getContentViewGestureHandler().pinchBegin(timeMs, x, y); + getContentViewGestureHandler().pinchBy(timeMs, x, y, delta); + getContentViewGestureHandler().pinchEnd(timeMs); + + return true; + } + + /** + * Invokes the graphical zoom picker widget for this ContentView. + */ + @Override + public void invokeZoomPicker() { + if (mContentSettings.supportZoom()) { + mZoomManager.invokeZoomPicker(); + } + } + + // Unlike legacy WebView getZoomControls which returns external zoom controls, + // this method returns built-in zoom controls. This method is used in tests. + public View getZoomControlsForTest() { + return mZoomManager.getZoomControlsViewForTest(); + } + + /** + * This method injects the supplied Java object into the ContentViewCore. + * The object is injected into the JavaScript context of the main frame, + * using the supplied name. This allows the Java object to be accessed from + * JavaScript. Note that that injected objects will not appear in + * JavaScript until the page is next (re)loaded. For example: + * <pre> view.addJavascriptInterface(new Object(), "injectedObject"); + * view.loadData("<!DOCTYPE html><title></title>", "text/html", null); + * view.loadUrl("javascript:alert(injectedObject.toString())");</pre> + * <p><strong>IMPORTANT:</strong> + * <ul> + * <li> addJavascriptInterface() can be used to allow JavaScript to control + * the host application. This is a powerful feature, but also presents a + * security risk. Use of this method in a ContentViewCore containing + * untrusted content could allow an attacker to manipulate the host + * application in unintended ways, executing Java code with the permissions + * of the host application. Use extreme care when using this method in a + * ContentViewCore which could contain untrusted content. Particular care + * should be taken to avoid unintentional access to inherited methods, such + * as {@link Object#getClass()}. To prevent access to inherited methods, + * set {@code allowInheritedMethods} to {@code false}. In addition, ensure + * that the injected object's public methods return only objects designed + * to be used by untrusted code, and never return a raw Object instance. + * <li> JavaScript interacts with Java objects on a private, background + * thread of the ContentViewCore. Care is therefore required to maintain + * thread safety.</li> + * </ul></p> + * + * @param object The Java object to inject into the ContentViewCore's + * JavaScript context. Null values are ignored. + * @param name The name used to expose the instance in JavaScript. + * @param requireAnnotation Restrict exposed methods to ones with the + * {@link JavascriptInterface} annotation. + */ + public void addJavascriptInterface(Object object, String name, boolean requireAnnotation) { + if (mNativeContentViewCore != 0 && object != null) { + nativeAddJavascriptInterface(mNativeContentViewCore, object, name, requireAnnotation); + } + } + + /** + * Removes a previously added JavaScript interface with the given name. + * + * @param name The name of the interface to remove. + */ + public void removeJavascriptInterface(String name) { + if (mNativeContentViewCore != 0) { + nativeRemoveJavascriptInterface(mNativeContentViewCore, name); + } + } + + /** + * Return the current scale of the ContentView. + * @return The current scale. + */ + public float getScale() { + return mNativePageScaleFactor; + } + + /** + * If the view is ready to draw contents to the screen. In hardware mode, + * the initialization of the surface texture may not occur until after the + * view has been added to the layout. This method will return {@code true} + * once the texture is actually ready. + */ + public boolean isReady() { + // TODO(nileshagrawal): Implement this. + return false; + } + + /** + * @return Whether or not the texture view is available or not. + */ + public boolean isAvailable() { + // TODO(nileshagrawal): Implement this. + return false; + } + + @CalledByNative + private void startContentIntent(String contentUrl) { + getContentViewClient().onStartContentIntent(getContext(), contentUrl); + } + + /** + * Determines whether or not this ContentViewCore can handle this accessibility action. + * @param action The action to perform. + * @return Whether or not this action is supported. + */ + public boolean supportsAccessibilityAction(int action) { + return mAccessibilityInjector.supportsAccessibilityAction(action); + } + + /** + * Attempts to perform an accessibility action on the web content. If the accessibility action + * cannot be processed, it returns {@code null}, allowing the caller to know to call the + * super {@link View#performAccessibilityAction(int, Bundle)} method and use that return value. + * Otherwise the return value from this method should be used. + * @param action The action to perform. + * @param arguments Optional action arguments. + * @return Whether the action was performed or {@code null} if the call should be delegated to + * the super {@link View} class. + */ + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (mAccessibilityInjector.supportsAccessibilityAction(action)) { + return mAccessibilityInjector.performAccessibilityAction(action, arguments); + } + + return false; + } + + /** + * @see View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo) + */ + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + mAccessibilityInjector.onInitializeAccessibilityNodeInfo(info); + } + + /** + * @see View#onInitializeAccessibilityEvent(AccessibilityEvent) + */ + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(this.getClass().getName()); + + // Identify where the top-left of the screen currently points to. + event.setScrollX(mNativeScrollX); + event.setScrollY(mNativeScrollY); + + // The maximum scroll values are determined by taking the content dimensions and + // subtracting off the actual dimensions of the ChromeView. + int maxScrollX = Math.max(0, mContentWidth - getWidth()); + int maxScrollY = Math.max(0, mContentHeight - getHeight()); + event.setScrollable(maxScrollX > 0 || maxScrollY > 0); + + // Setting the maximum scroll values requires API level 15 or higher. + final int SDK_VERSION_REQUIRED_TO_SET_SCROLL = 15; + if (Build.VERSION.SDK_INT >= SDK_VERSION_REQUIRED_TO_SET_SCROLL) { + event.setMaxScrollX(maxScrollX); + event.setMaxScrollY(maxScrollY); + } + } + + /** + * Returns whether or not accessibility injection is being used. + */ + public boolean isInjectingAccessibilityScript() { + return mAccessibilityInjector.accessibilityIsAvailable(); + } + + /** + * Enable or disable accessibility features. + */ + public void setAccessibilityState(boolean state) { + mAccessibilityInjector.setScriptEnabled(state); + } + + /** + * Stop any TTS notifications that are currently going on. + */ + public void stopCurrentAccessibilityNotifications() { + mAccessibilityInjector.onPageLostFocus(); + } + + /** + * @See android.webkit.WebView#pageDown(boolean) + */ + public boolean pageDown(boolean bottom) { + if (computeVerticalScrollOffset() >= mContentHeight - getHeight()) { + // We seem to already be at the bottom of the page, so no scrolling will occur. + return false; + } + + if (bottom) { + scrollTo(computeHorizontalScrollOffset(), mContentHeight - getHeight()); + } else { + dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PAGE_DOWN)); + dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PAGE_DOWN)); + } + return true; + } + + /** + * @See android.webkit.WebView#pageUp(boolean) + */ + public boolean pageUp(boolean top) { + if (computeVerticalScrollOffset() == 0) { + // We seem to already be at the top of the page, so no scrolling will occur. + return false; + } + + if (top) { + scrollTo(computeHorizontalScrollOffset(), 0); + } else { + dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PAGE_UP)); + dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PAGE_UP)); + } + return true; + } + + /** + * Callback factory method for nativeGetNavigationHistory(). + */ + @CalledByNative + private void addToNavigationHistory(Object history, String url, String virtualUrl, + String originalUrl, String title, Bitmap favicon) { + NavigationEntry entry = new NavigationEntry(url, virtualUrl, originalUrl, title, favicon); + ((NavigationHistory) history).addEntry(entry); + } + + /** + * Get a copy of the navigation history of the view. + */ + public NavigationHistory getNavigationHistory() { + NavigationHistory history = new NavigationHistory(); + int currentIndex = nativeGetNavigationHistory(mNativeContentViewCore, history); + history.setCurrentEntryIndex(currentIndex); + return history; + } + + private native int nativeInit(boolean hardwareAccelerated, int webContentsPtr, + int windowAndroidPtr); + + private static native void nativeDestroy(int nativeContentViewCore); + + private native void nativeLoadUrl( + int nativeContentViewCoreImpl, + String url, + int loadUrlType, + int transitionType, + int uaOverrideOption, + String extraHeaders, + byte[] postData, + String baseUrlForDataUrl, + String virtualUrlForDataUrl); + + private native void nativeSetAllUserAgentOverridesInHistory(int nativeContentViewCoreImpl, + String userAgentOverride); + + private native String nativeGetURL(int nativeContentViewCoreImpl); + + private native String nativeGetTitle(int nativeContentViewCoreImpl); + + private native boolean nativeIsIncognito(int nativeContentViewCoreImpl); + + // Returns true if the native side crashed so that java side can draw a sad tab. + private native boolean nativeCrashed(int nativeContentViewCoreImpl); + + private native void nativeSetFocus(int nativeContentViewCoreImpl, boolean focused); + + private native boolean nativeSendTouchEvent( + int nativeContentViewCoreImpl, long timeMs, int action, TouchPoint[] pts); + + private native int nativeSendMouseMoveEvent( + int nativeContentViewCoreImpl, long timeMs, int x, int y); + + private native int nativeSendMouseWheelEvent( + int nativeContentViewCoreImpl, long timeMs, int x, int y, float verticalAxis); + + private native void nativeScrollBegin(int nativeContentViewCoreImpl, long timeMs, int x, int y); + + private native void nativeScrollEnd(int nativeContentViewCoreImpl, long timeMs); + + private native void nativeScrollBy( + int nativeContentViewCoreImpl, long timeMs, int x, int y, int deltaX, int deltaY); + + private native void nativeFlingStart( + int nativeContentViewCoreImpl, long timeMs, int x, int y, int vx, int vy); + + private native void nativeFlingCancel(int nativeContentViewCoreImpl, long timeMs); + + private native void nativeSingleTap( + int nativeContentViewCoreImpl, long timeMs, int x, int y, boolean linkPreviewTap); + + private native void nativeShowPressState( + int nativeContentViewCoreImpl, long timeMs, int x, int y); + + private native void nativeDoubleTap(int nativeContentViewCoreImpl, long timeMs, int x, int y); + + private native void nativeLongPress(int nativeContentViewCoreImpl, long timeMs, int x, int y, + boolean linkPreviewTap); + + private native void nativePinchBegin(int nativeContentViewCoreImpl, long timeMs, int x, int y); + + private native void nativePinchEnd(int nativeContentViewCoreImpl, long timeMs); + + private native void nativePinchBy(int nativeContentViewCoreImpl, long timeMs, + int anchorX, int anchorY, float deltaScale); + + private native void nativeSelectBetweenCoordinates( + int nativeContentViewCoreImpl, int x1, int y1, int x2, int y2); + + private native boolean nativeCanGoBack(int nativeContentViewCoreImpl); + + private native boolean nativeCanGoForward(int nativeContentViewCoreImpl); + + private native boolean nativeCanGoToOffset(int nativeContentViewCoreImpl, int offset); + + private native void nativeGoToOffset(int nativeContentViewCoreImpl, int offset); + + private native void nativeGoBack(int nativeContentViewCoreImpl); + + private native void nativeGoForward(int nativeContentViewCoreImpl); + + private native void nativeStopLoading(int nativeContentViewCoreImpl); + + private native void nativeReload(int nativeContentViewCoreImpl); + + private native void nativeSelectPopupMenuItems(int nativeContentViewCoreImpl, int[] indices); + + private native void nativeScrollFocusedEditableNodeIntoView(int nativeContentViewCoreImpl); + private native void nativeUndoScrollFocusedEditableNodeIntoView(int nativeContentViewCoreImpl); + private native boolean nativeNeedsReload(int nativeContentViewCoreImpl); + + private native void nativeClearHistory(int nativeContentViewCoreImpl); + + private native int nativeEvaluateJavaScript(String script); + + private native int nativeGetNativeImeAdapter(int nativeContentViewCoreImpl); + + private native int nativeGetCurrentRenderProcessId(int nativeContentViewCoreImpl); + + private native int nativeGetBackgroundColor(int nativeContentViewCoreImpl); + + private native void nativeSetBackgroundColor(int nativeContentViewCoreImpl, int color); + + private native void nativeOnShow(int nativeContentViewCoreImpl); + private native void nativeOnHide(int nativeContentViewCoreImpl); + + private native void nativeSetUseDesktopUserAgent(int nativeContentViewCoreImpl, + boolean enabled, boolean reloadOnChange); + private native boolean nativeGetUseDesktopUserAgent(int nativeContentViewCoreImpl); + + private native void nativeAddJavascriptInterface(int nativeContentViewCoreImpl, Object object, + String name, boolean requireAnnotation); + + private native void nativeRemoveJavascriptInterface(int nativeContentViewCoreImpl, String name); + + private native int nativeGetNavigationHistory(int nativeContentViewCoreImpl, Object context); + + private native void nativeUpdateVSyncParameters(int nativeContentViewCoreImpl, + long timebaseMicros, long intervalMicros); +} diff --git a/content/public/android/java/src/org/chromium/content/browser/CursorController.java b/content/public/android/java/src/org/chromium/content/browser/CursorController.java new file mode 100644 index 0000000..6e572c8 --- /dev/null +++ b/content/public/android/java/src/org/chromium/content/browser/CursorController.java @@ -0,0 +1,41 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import android.view.ViewTreeObserver; + +/** + * A CursorController instance can be used to control a cursor in the text. + */ +interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { + + /** + * Hide the cursor controller from screen. + */ + void hide(); + + /** + * @return true if the CursorController is currently visible + */ + boolean isShowing(); + + /** + * Called when the handle is about to start updating its position. + * @param handle + */ + void beforeStartUpdatingPosition(HandleView handle); + + /** + * Update the controller's position. + */ + void updatePosition(HandleView handle, int x, int y); + + /** + * Called when the view is detached from window. Perform house keeping task, such as + * stopping Runnable thread that would otherwise keep a reference on the context, thus + * preventing the activity to be recycled. + */ + void onDetached(); +} diff --git a/content/public/android/java/src/org/chromium/content/browser/HandleView.java b/content/public/android/java/src/org/chromium/content/browser/HandleView.java new file mode 100644 index 0000000..bf36b56 --- /dev/null +++ b/content/public/android/java/src/org/chromium/content/browser/HandleView.java @@ -0,0 +1,370 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.WindowManager; +import android.view.View.OnClickListener; +import android.view.ViewGroup.LayoutParams; +import android.widget.PopupWindow; +import android.widget.TextView; + +/** + * View that displays a selection or insertion handle for text editing. + */ +class HandleView extends View { + private Drawable mDrawable; + private final PopupWindow mContainer; + private int mPositionX; + private int mPositionY; + private final CursorController mController; + private boolean mIsDragging; + private float mTouchToWindowOffsetX; + private float mTouchToWindowOffsetY; + private float mHotspotX; + private float mHotspotY; + private int mLineOffsetY; + private int mHeight; + private int mLastParentX; + private int mLastParentY; + private float mDownPositionX, mDownPositionY; + private int mContainerPositionX, mContainerPositionY; + private long mTouchTimer; + private boolean mIsInsertionHandle = false; + private Runnable mLongPressCallback; + + private View mParent; + private InsertionHandleController.PastePopupMenu mPastePopupWindow; + + private final int mTextSelectHandleLeftRes; + private final int mTextSelectHandleRightRes; + private final int mTextSelectHandleRes; + + private Drawable mSelectHandleLeft; + private Drawable mSelectHandleRight; + private Drawable mSelectHandleCenter; + + private final int[] mTempCoords = new int[2]; + private final Rect mTempRect = new Rect(); + + static final int LEFT = 0; + static final int CENTER = 1; + static final int RIGHT = 2; + + // Number of dips to subtract from the handle's y position to give a suitable + // y coordinate for the corresponding text position. This is to compensate for the fact + // that the handle position is at the base of the line of text. + private static final float LINE_OFFSET_Y_DIP = 5.0f; + + private static final int[] TEXT_VIEW_HANDLE_ATTRS = { + android.R.attr.textSelectHandleLeft, + android.R.attr.textSelectHandle, + android.R.attr.textSelectHandleRight, + }; + + HandleView(CursorController controller, int pos, View parent) { + super(parent.getContext()); + Context context = parent.getContext(); + mParent = parent; + mController = controller; + mContainer = new PopupWindow(context, null, android.R.attr.textSelectHandleWindowStyle); + mContainer.setSplitTouchEnabled(true); + mContainer.setClippingEnabled(false); + + TypedArray a = context.obtainStyledAttributes(TEXT_VIEW_HANDLE_ATTRS); + mTextSelectHandleLeftRes = a.getResourceId(a.getIndex(LEFT), 0); + mTextSelectHandleRes = a.getResourceId(a.getIndex(CENTER), 0); + mTextSelectHandleRightRes = a.getResourceId(a.getIndex(RIGHT), 0); + a.recycle(); + + setOrientation(pos); + + // Convert line offset dips to pixels. + mLineOffsetY = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + LINE_OFFSET_Y_DIP, context.getResources().getDisplayMetrics()); + } + + void setOrientation(int pos) { + int handleWidth; + switch (pos) { + case LEFT: { + if (mSelectHandleLeft == null) { + mSelectHandleLeft = getContext().getResources().getDrawable( + mTextSelectHandleLeftRes); + } + mDrawable = mSelectHandleLeft; + handleWidth = mDrawable.getIntrinsicWidth(); + mHotspotX = (handleWidth * 3) / 4; + break; + } + + case RIGHT: { + if (mSelectHandleRight == null) { + mSelectHandleRight = getContext().getResources().getDrawable( + mTextSelectHandleRightRes); + } + mDrawable = mSelectHandleRight; + handleWidth = mDrawable.getIntrinsicWidth(); + mHotspotX = handleWidth / 4; + break; + } + + case CENTER: + default: { + if (mSelectHandleCenter == null) { + mSelectHandleCenter = getContext().getResources().getDrawable( + mTextSelectHandleRes); + } + mDrawable = mSelectHandleCenter; + handleWidth = mDrawable.getIntrinsicWidth(); + mHotspotX = handleWidth / 2; + mIsInsertionHandle = true; + break; + } + } + + final int handleHeight = mDrawable.getIntrinsicHeight(); + mHotspotY = 0; + mHeight = handleHeight; + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(mDrawable.getIntrinsicWidth(), + mDrawable.getIntrinsicHeight()); + } + + void show() { + if (!isPositionVisible()) { + hide(); + return; + } + mContainer.setContentView(this); + final int[] coords = mTempCoords; + mParent.getLocationInWindow(coords); + mContainerPositionX = coords[0] + mPositionX; + mContainerPositionY = coords[1] + mPositionY; + mContainer.showAtLocation(mParent, 0, mContainerPositionX, mContainerPositionY); + + // Hide paste view when handle is moved on screen. + if (mPastePopupWindow != null) { + mPastePopupWindow.hide(); + } + } + + void hide() { + mIsDragging = false; + mContainer.dismiss(); + if (mPastePopupWindow != null) { + mPastePopupWindow.hide(); + } + } + + boolean isShowing() { + return mContainer.isShowing(); + } + + private boolean isPositionVisible() { + // Always show a dragging handle. + if (mIsDragging) { + return true; + } + + final Rect clip = mTempRect; + clip.left = 0; + clip.top = 0; + clip.right = mParent.getWidth(); + clip.bottom = mParent.getHeight(); + + final ViewParent parent = mParent.getParent(); + if (parent == null || !parent.getChildVisibleRect(mParent, clip, null)) { + return false; + } + + final int[] coords = mTempCoords; + mParent.getLocationInWindow(coords); + final int posX = coords[0] + mPositionX + (int) mHotspotX; + final int posY = coords[1] + mPositionY + (int) mHotspotY; + + return posX >= clip.left && posX <= clip.right && + posY >= clip.top && posY <= clip.bottom; + } + + void moveTo(int x, int y) { + mPositionX = x; + mPositionY = y; + if (isPositionVisible()) { + int[] coords = null; + if (mContainer.isShowing()) { + coords = mTempCoords; + mParent.getLocationInWindow(coords); + final int containerPositionX = coords[0] + mPositionX; + final int containerPositionY = coords[1] + mPositionY; + + if (containerPositionX != mContainerPositionX || + containerPositionY != mContainerPositionY) { + mContainerPositionX = containerPositionX; + mContainerPositionY = containerPositionY; + + mContainer.update(mContainerPositionX, mContainerPositionY, + getRight() - getLeft(), getBottom() - getTop()); + + // Hide paste popup window as soon as a scroll occurs. + if (mPastePopupWindow != null) { + mPastePopupWindow.hide(); + } + } + } else { + show(); + } + + if (mIsDragging) { + if (coords == null) { + coords = mTempCoords; + mParent.getLocationInWindow(coords); + } + if (coords[0] != mLastParentX || coords[1] != mLastParentY) { + mTouchToWindowOffsetX += coords[0] - mLastParentX; + mTouchToWindowOffsetY += coords[1] - mLastParentY; + mLastParentX = coords[0]; + mLastParentY = coords[1]; + } + // Hide paste popup window as soon as the handle is dragged. + if (mPastePopupWindow != null) { + mPastePopupWindow.hide(); + } + } + } else { + hide(); + } + } + + @Override + protected void onDraw(Canvas c) { + mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop()); + mDrawable.draw(c); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + mDownPositionX = ev.getRawX(); + mDownPositionY = ev.getRawY(); + mTouchToWindowOffsetX = mDownPositionX - mPositionX; + mTouchToWindowOffsetY = mDownPositionY - mPositionY; + final int[] coords = mTempCoords; + mParent.getLocationInWindow(coords); + mLastParentX = coords[0]; + mLastParentY = coords[1]; + mIsDragging = true; + mController.beforeStartUpdatingPosition(this); + mTouchTimer = SystemClock.uptimeMillis(); + break; + } + + case MotionEvent.ACTION_MOVE: { + updatePosition(ev.getRawX(), ev.getRawY()); + break; + } + + case MotionEvent.ACTION_UP: + if (mIsInsertionHandle) { + long delay = SystemClock.uptimeMillis() - mTouchTimer; + if (delay < ViewConfiguration.getTapTimeout()) { + if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) { + // Tapping on the handle dismisses the displayed paste view, + mPastePopupWindow.hide(); + } else { + showPastePopupWindow(); + } + } + } + mIsDragging = false; + break; + + case MotionEvent.ACTION_CANCEL: + mIsDragging = false; + break; + } + return true; + } + + boolean isDragging() { + return mIsDragging; + } + + /** + * @return Returns the x position of the handle + */ + int getPositionX() { + return mPositionX; + } + + /** + * @return Returns the y position of the handle + */ + int getPositionY() { + return mPositionY; + } + + private void updatePosition(float rawX, float rawY) { + final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; + final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY - mLineOffsetY; + + mController.updatePosition(this, Math.round(newPosX), Math.round(newPosY)); + } + + void positionAt(int x, int y) { + moveTo((int)(x - mHotspotX), (int)(y - mHotspotY)); + } + + // Returns the x coordinate of the position that the handle appears to be pointing to. + int getAdjustedPositionX() { + return (int) (mPositionX + mHotspotX); + } + + // Returns the y coordinate of the position that the handle appears to be pointing to. + int getAdjustedPositionY() { + return (int) (mPositionY + mHotspotY); + } + + // Returns a suitable y coordinate for the text position corresponding to the handle. + // As the handle points to a position on the base of the line of text, this method + // returns a coordinate a small number of pixels higher (i.e. a slightly smaller number) + // than getAdjustedPositionY. + int getLineAdjustedPositionY() { + return (int) (mPositionY + mHotspotY - mLineOffsetY); + } + + Drawable getDrawable() { + return mDrawable; + } + + void showPastePopupWindow() { + InsertionHandleController ihc = (InsertionHandleController) mController; + if (mIsInsertionHandle && ihc.canPaste()) { + if (mPastePopupWindow == null) { + // Lazy initialization: create when actually shown only. + mPastePopupWindow = ihc.new PastePopupMenu(); + } + mPastePopupWindow.show(); + } + } +} diff --git a/content/public/android/java/src/org/chromium/content/browser/InsertionHandleController.java b/content/public/android/java/src/org/chromium/content/browser/InsertionHandleController.java index 8d84fde..2dd4008 100644 --- a/content/public/android/java/src/org/chromium/content/browser/InsertionHandleController.java +++ b/content/public/android/java/src/org/chromium/content/browser/InsertionHandleController.java @@ -4,14 +4,35 @@ package org.chromium.content.browser; +import android.content.ClipboardManager; import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.view.Gravity; +import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.PopupWindow; -class InsertionHandleController { +/** + * CursorController for inserting text at the cursor position. + */ +abstract class InsertionHandleController implements CursorController { + + /** The handle view, lazily created when first shown */ + private HandleView mHandle; /** The view over which the insertion handle should be shown */ private View mParent; + /** True iff the insertion handle is currently showing */ + private boolean mIsShowing; + + /** True iff the insertion handle can be shown automatically when selection changes */ + private boolean mAllowAutomaticShowing; + private Context mContext; InsertionHandleController(View parent) { @@ -19,8 +40,251 @@ class InsertionHandleController { mContext = parent.getContext(); } + /** Allows the handle to be shown automatically when cursor position changes */ + void allowAutomaticShowing() { + mAllowAutomaticShowing = true; + } + /** Disallows the handle from being shown automatically when cursor position changes */ - public void hideAndDisallowAutomaticShowing() { - // TODO(olilan): add method implementation (this is just a stub for ImeAdapter). + void hideAndDisallowAutomaticShowing() { + hide(); + mAllowAutomaticShowing = false; + } + + /** + * Sets the position and shows the handle. + * @param x1 + * @param y1 + */ + void showHandleAt(int x, int y) { + createHandleIfNeeded(); + setHandlePosition(x, y); + showHandleIfNeeded(); + } + + void showPastePopup() { + if (mIsShowing) { + mHandle.showPastePopupWindow(); + } + } + + void showHandleWithPastePopupAt(int x, int y) { + showHandleAt(x, y); + showPastePopup(); + } + + /** Shows the handle at the given coordinates, as long as automatic showing is allowed */ + void onCursorPositionChanged(int x, int y) { + if (mAllowAutomaticShowing) { + showHandleAt(x, y); + } + } + + /** + * Moves the handle so that it points at the given coordinates. + * @param x + * @param y + */ + void setHandlePosition(int x, int y) { + mHandle.positionAt(x, y); + } + + @Override + public void onTouchModeChanged(boolean isInTouchMode) { + if (!isInTouchMode) { + hide(); + } + } + + @Override + public void hide() { + if (mIsShowing) { + if (mHandle != null) mHandle.hide(); + mIsShowing = false; + } + } + + @Override + public boolean isShowing() { + return mIsShowing; + } + + @Override + public void beforeStartUpdatingPosition(HandleView handle) {} + + @Override + public void updatePosition(HandleView handle, int x, int y) { + setCursorPosition(x, y); + } + + /** + * The concrete implementation must cause the cursor position to move to the given + * coordinates and (possibly asynchronously) set the insertion handle position + * after the cursor position change is made via showHandleAt(x,y). + * @param x + * @param y + */ + protected abstract void setCursorPosition(int x, int y); + + /** Pastes the contents of clipboard at the current insertion point */ + protected abstract void paste(); + + /** Returns the current line height in pixels */ + protected abstract int getLineHeight(); + + @Override + public void onDetached() {} + + boolean canPaste() { + return ((ClipboardManager)mContext.getSystemService( + Context.CLIPBOARD_SERVICE)).hasPrimaryClip(); + } + + private void createHandleIfNeeded() { + if (mHandle == null) mHandle = new HandleView(this, HandleView.CENTER, mParent); + } + + private void showHandleIfNeeded() { + if (!mIsShowing) { + mIsShowing = true; + mHandle.show(); + } + } + + /* + * This class is based on TextView.PastePopupMenu. + */ + class PastePopupMenu implements OnClickListener { + private final PopupWindow mContainer; + private int mPositionX; + private int mPositionY; + private View[] mPasteViews; + private int[] mPasteViewLayouts; + + public PastePopupMenu() { + mContainer = new PopupWindow(mContext, null, + android.R.attr.textSelectHandleWindowStyle); + mContainer.setSplitTouchEnabled(true); + mContainer.setClippingEnabled(false); + + mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + + final int[] POPUP_LAYOUT_ATTRS = { + android.R.attr.textEditPasteWindowLayout, + android.R.attr.textEditNoPasteWindowLayout, + android.R.attr.textEditSidePasteWindowLayout, + android.R.attr.textEditSideNoPasteWindowLayout, + }; + + mPasteViews = new View[POPUP_LAYOUT_ATTRS.length]; + mPasteViewLayouts = new int[POPUP_LAYOUT_ATTRS.length]; + + TypedArray attrs = mContext.obtainStyledAttributes(POPUP_LAYOUT_ATTRS); + for (int i = 0; i < attrs.length(); ++i) { + mPasteViewLayouts[i] = attrs.getResourceId(attrs.getIndex(i), 0); + } + attrs.recycle(); + } + + private int viewIndex(boolean onTop) { + return (onTop ? 0 : 1<<1) + (canPaste() ? 0 : 1 << 0); + } + + private void updateContent(boolean onTop) { + final int viewIndex = viewIndex(onTop); + View view = mPasteViews[viewIndex]; + + if (view == null) { + final int layout = mPasteViewLayouts[viewIndex]; + LayoutInflater inflater = (LayoutInflater)mContext. + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + if (inflater != null) { + view = inflater.inflate(layout, null); + } + + if (view == null) { + throw new IllegalArgumentException("Unable to inflate TextEdit paste window"); + } + + final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + view.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + view.measure(size, size); + + view.setOnClickListener(this); + + mPasteViews[viewIndex] = view; + } + + mContainer.setContentView(view); + } + + void show() { + updateContent(true); + positionAtCursor(); + } + + void hide() { + mContainer.dismiss(); + } + + boolean isShowing() { + return mContainer.isShowing(); + } + + @Override + public void onClick(View v) { + if (canPaste()) { + paste(); + } + hide(); + } + + void positionAtCursor() { + View contentView = mContainer.getContentView(); + int width = contentView.getMeasuredWidth(); + int height = contentView.getMeasuredHeight(); + + int lineHeight = getLineHeight(); + + mPositionX = (int) (mHandle.getAdjustedPositionX() - width / 2.0f); + mPositionY = mHandle.getAdjustedPositionY() - height - lineHeight; + + final int[] coords = new int[2]; + mParent.getLocationInWindow(coords); + coords[0] += mPositionX; + coords[1] += mPositionY; + + final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels; + if (coords[1] < 0) { + updateContent(false); + // Update dimensions from new view + contentView = mContainer.getContentView(); + width = contentView.getMeasuredWidth(); + height = contentView.getMeasuredHeight(); + + // Vertical clipping, move under edited line and to the side of insertion cursor + // TODO bottom clipping in case there is no system bar + coords[1] += height; + coords[1] += lineHeight; + + // Move to right hand side of insertion cursor by default. TODO RTL text. + final Drawable handle = mHandle.getDrawable(); + final int handleHalfWidth = handle.getIntrinsicWidth() / 2; + + if (mHandle.getAdjustedPositionX() + width < screenWidth) { + coords[0] += handleHalfWidth + width / 2; + } else { + coords[0] -= handleHalfWidth + width / 2; + } + } else { + // Horizontal clipping + coords[0] = Math.max(0, coords[0]); + coords[0] = Math.min(screenWidth - width, coords[0]); + } + + mContainer.showAtLocation(mParent, Gravity.NO_GRAVITY, coords[0], coords[1]); + } } } diff --git a/content/public/android/java/src/org/chromium/content/browser/SelectionHandleController.java b/content/public/android/java/src/org/chromium/content/browser/SelectionHandleController.java index 8e6b0bf..f89332a 100644 --- a/content/public/android/java/src/org/chromium/content/browser/SelectionHandleController.java +++ b/content/public/android/java/src/org/chromium/content/browser/SelectionHandleController.java @@ -6,16 +6,192 @@ package org.chromium.content.browser; import android.view.View; -class SelectionHandleController { +/** + * CursorController for selecting a range of text. + */ +abstract class SelectionHandleController implements CursorController { + + // The following constants match the ones in base/i18n/rtl.h. + private static final int TEXT_DIRECTION_RTL = 1; + private static final int TEXT_DIRECTION_LTR = 2; + + /** The cursor controller images, lazily created when shown. */ + private HandleView mStartHandle, mEndHandle; + + /** Whether handles should show automatically when text is selected. */ + private boolean mAllowAutomaticShowing = true; + + /** Whether selection anchors are active. */ + private boolean mIsShowing; private View mParent; + private int mFixedHandleX; + private int mFixedHandleY; + SelectionHandleController(View parent) { mParent = parent; } + /** Automatically show selection anchors when text is selected. */ + void allowAutomaticShowing() { + mAllowAutomaticShowing = true; + } + /** Hide selection anchors, and don't automatically show them. */ - public void hideAndDisallowAutomaticShowing() { - // TODO(olilan): add method implementation (this is just a stub for ImeAdapter). + void hideAndDisallowAutomaticShowing() { + hide(); + mAllowAutomaticShowing = false; + } + + @Override + public boolean isShowing() { + return mIsShowing; + } + + @Override + public void hide() { + if (mIsShowing) { + if (mStartHandle != null) mStartHandle.hide(); + if (mEndHandle != null) mEndHandle.hide(); + mIsShowing = false; + } + } + + void cancelFadeOutAnimation() { + hide(); + } + + /** + * Updates the selection for a movement of the given handle (which + * should be the start handle or end handle) to coordinates x,y. + * Note that this will not actually result in the handle moving to (x,y): + * selectBetweenCoordinates(x1,y1,x2,y2) will trigger the selection and set the + * actual coordinates later via showHandlesAt. + */ + @Override + public void updatePosition(HandleView handle, int x, int y) { + selectBetweenCoordinates(mFixedHandleX, mFixedHandleY, x, y); + } + + @Override + public void beforeStartUpdatingPosition(HandleView handle) { + HandleView fixedHandle = (handle == mStartHandle) ? mEndHandle : mStartHandle; + mFixedHandleX = fixedHandle.getAdjustedPositionX(); + mFixedHandleY = fixedHandle.getLineAdjustedPositionY(); + } + + /** + * The concrete implementation must trigger a selection between the given + * coordinates and (possibly asynchronously) set the actual handle positions + * after the selection is made via showHandlesAt(x1,y1,x2,y2). + * @param x1 + * @param y1 + * @param x2 + * @param y2 + */ + protected abstract void selectBetweenCoordinates(int x1, int y1, int x2, int y2); + + /** + * @return true iff this controller is being used to move the selection start. + */ + boolean isSelectionStartDragged() { + return mStartHandle != null && mStartHandle.isDragging(); + } + + /** + * @return true iff this controller is being used to drag either the selection start or end. + */ + boolean isDragging() { + return (mStartHandle != null && mStartHandle.isDragging()) || + (mEndHandle != null && mEndHandle.isDragging()); + } + + @Override + public void onTouchModeChanged(boolean isInTouchMode) { + if (!isInTouchMode) { + hide(); + } + } + + @Override + public void onDetached() {} + + /** + * Moves the start handle so that it points at the given coordinates. + * @param x + * @param y + */ + void setStartHandlePosition(int x, int y) { + mStartHandle.positionAt(x, y); + } + + /** + * Moves the end handle so that it points at the given coordinates. + * @param x + * @param y + */ + void setEndHandlePosition(int x, int y) { + mEndHandle.positionAt(x, y); + } + + /** + * Moves the start handle by x pixels horizontally and y pixels vertically. + * @param x + * @param y + */ + void moveStartHandleBy(int x, int y) { + mStartHandle.moveTo(mStartHandle.getPositionX() + x, mStartHandle.getPositionY() + y); + } + + /** + * Moves the end handle by x pixels horizontally and y pixels vertically. + * @param x + * @param y + */ + void moveEndHandleBy(int x, int y) { + mEndHandle.moveTo(mEndHandle.getPositionX() + x, mEndHandle.getPositionY() + y); + } + + void onSelectionChanged(int x1, int y1, int dir1, int x2, int y2, int dir2) { + if (mAllowAutomaticShowing) { + showHandlesAt(x1, y1, dir1, x2, y2, dir2); + } + } + + /** + * Sets both start and end position and show the handles. + * Note: this method does not trigger a selection, see + * selectBetweenCoordinates() + * + * @param x1 + * @param y1 + * @param x2 + * @param y2 + */ + void showHandlesAt(int x1, int y1, int dir1, int x2, int y2, int dir2) { + createHandlesIfNeeded(dir1, dir2); + setStartHandlePosition(x1, y1); + setEndHandlePosition(x2, y2); + showHandlesIfNeeded(); + } + + private void createHandlesIfNeeded(int start_dir, int end_dir) { + if (mStartHandle == null) { + mStartHandle = new HandleView(this, + start_dir == TEXT_DIRECTION_LTR ? HandleView.LEFT : HandleView.RIGHT, mParent); + } + if (mEndHandle == null) { + mEndHandle = new HandleView(this, + end_dir == TEXT_DIRECTION_LTR ? HandleView.RIGHT : HandleView.LEFT, mParent); + } + } + + private void showHandlesIfNeeded() { + if (!mIsShowing) { + mIsShowing = true; + mStartHandle.show(); + mEndHandle.show(); + } } } |