/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.webkit; import android.accessibilityservice.AccessibilityServiceInfo; import android.animation.ObjectAnimator; import android.annotation.Widget; import android.app.ActivityManager; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.DataSetObserver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.DrawFilter; import android.graphics.Paint; import android.graphics.PaintFlagsDrawFilter; import android.graphics.Picture; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.RegionIterator; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.net.Proxy; import android.net.ProxyProperties; import android.net.Uri; import android.net.http.SslCertificate; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.security.KeyChain; import android.text.Editable; import android.text.InputType; import android.text.Selection; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.HardwareCanvas; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.SoundEffectConstants; import android.view.VelocityTracker; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewRootImpl; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.webkit.WebView.HitTestResult; import android.webkit.WebView.PictureListener; import android.webkit.WebViewCore.DrawData; import android.webkit.WebViewCore.EventHub; import android.webkit.WebViewCore.TextFieldInitData; import android.webkit.WebViewCore.TextSelectionData; import android.webkit.WebViewCore.WebKitHitTest; import android.widget.AbsoluteLayout; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.CheckedTextView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.OverScroller; import android.widget.PopupWindow; import android.widget.Scroller; import android.widget.TextView; import android.widget.Toast; import junit.framework.Assert; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.Vector; /** * Implements a backend provider for the {@link WebView} public API. * @hide */ // TODO: Check if any WebView published API methods are called from within here, and if so // we should bounce the call out via the proxy to enable any sub-class to override it. @Widget @SuppressWarnings("deprecation") public final class WebViewClassic implements WebViewProvider, WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate { /** * InputConnection used for ContentEditable. This captures changes * to the text and sends them either as key strokes or text changes. */ class WebViewInputConnection extends BaseInputConnection { // Used for mapping characters to keys typed. private KeyCharacterMap mKeyCharacterMap; private boolean mIsKeySentByMe; private int mInputType; private int mImeOptions; private String mHint; private int mMaxLength; private boolean mIsAutoFillable; private boolean mIsAutoCompleteEnabled; private String mName; private int mBatchLevel; public WebViewInputConnection() { super(mWebView, true); } public void setAutoFillable(int queryId) { mIsAutoFillable = getSettings().getAutoFillEnabled() && (queryId != WebTextView.FORM_NOT_AUTOFILLABLE); int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION; if (variation != EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD && (mIsAutoFillable || mIsAutoCompleteEnabled)) { if (mName != null && mName.length() > 0) { requestFormData(mName, mFieldPointer, mIsAutoFillable, mIsAutoCompleteEnabled); } } } @Override public boolean beginBatchEdit() { if (mBatchLevel == 0) { beginTextBatch(); } mBatchLevel++; return false; } @Override public boolean endBatchEdit() { mBatchLevel--; if (mBatchLevel == 0) { commitTextBatch(); } return false; } public boolean getIsAutoFillable() { return mIsAutoFillable; } @Override public boolean sendKeyEvent(KeyEvent event) { // Some IMEs send key events directly using sendKeyEvents. // WebViewInputConnection should treat these as text changes. if (!mIsKeySentByMe) { if (event.getAction() == KeyEvent.ACTION_UP) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { return deleteSurroundingText(1, 0); } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { return deleteSurroundingText(0, 1); } else if (event.getUnicodeChar() != 0){ String newComposingText = Character.toString((char)event.getUnicodeChar()); return commitText(newComposingText, 1); } } else if (event.getAction() == KeyEvent.ACTION_DOWN && (event.getKeyCode() == KeyEvent.KEYCODE_DEL || event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL || event.getUnicodeChar() != 0)) { return true; // only act on action_down } } return super.sendKeyEvent(event); } public void setTextAndKeepSelection(CharSequence text) { Editable editable = getEditable(); int selectionStart = Selection.getSelectionStart(editable); int selectionEnd = Selection.getSelectionEnd(editable); text = limitReplaceTextByMaxLength(text, editable.length()); editable.replace(0, editable.length(), text); restartInput(); // Keep the previous selection. selectionStart = Math.min(selectionStart, editable.length()); selectionEnd = Math.min(selectionEnd, editable.length()); setSelection(selectionStart, selectionEnd); finishComposingText(); } public void replaceSelection(CharSequence text) { Editable editable = getEditable(); int selectionStart = Selection.getSelectionStart(editable); int selectionEnd = Selection.getSelectionEnd(editable); text = limitReplaceTextByMaxLength(text, selectionEnd - selectionStart); setNewText(selectionStart, selectionEnd, text); editable.replace(selectionStart, selectionEnd, text); restartInput(); // Move caret to the end of the new text int newCaret = selectionStart + text.length(); setSelection(newCaret, newCaret); } @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { Editable editable = getEditable(); int start = getComposingSpanStart(editable); int end = getComposingSpanEnd(editable); if (start < 0 || end < 0) { start = Selection.getSelectionStart(editable); end = Selection.getSelectionEnd(editable); } if (end < start) { int temp = end; end = start; start = temp; } CharSequence limitedText = limitReplaceTextByMaxLength(text, end - start); setNewText(start, end, limitedText); if (limitedText != text) { newCursorPosition -= text.length() - limitedText.length(); } super.setComposingText(limitedText, newCursorPosition); updateSelection(); if (limitedText != text) { int lastCaret = start + limitedText.length(); finishComposingText(); setSelection(lastCaret, lastCaret); } return true; } @Override public boolean commitText(CharSequence text, int newCursorPosition) { setComposingText(text, newCursorPosition); finishComposingText(); return true; } @Override public boolean deleteSurroundingText(int leftLength, int rightLength) { // This code is from BaseInputConnection#deleteSurroundText. // We have to delete the same text in webkit. Editable content = getEditable(); int a = Selection.getSelectionStart(content); int b = Selection.getSelectionEnd(content); if (a > b) { int tmp = a; a = b; b = tmp; } int ca = getComposingSpanStart(content); int cb = getComposingSpanEnd(content); if (cb < ca) { int tmp = ca; ca = cb; cb = tmp; } if (ca != -1 && cb != -1) { if (ca < a) a = ca; if (cb > b) b = cb; } int endDelete = Math.min(content.length(), b + rightLength); if (endDelete > b) { setNewText(b, endDelete, ""); } int startDelete = Math.max(0, a - leftLength); if (startDelete < a) { setNewText(startDelete, a, ""); } return super.deleteSurroundingText(leftLength, rightLength); } @Override public boolean performEditorAction(int editorAction) { boolean handled = true; switch (editorAction) { case EditorInfo.IME_ACTION_NEXT: mWebView.requestFocus(View.FOCUS_FORWARD); break; case EditorInfo.IME_ACTION_PREVIOUS: mWebView.requestFocus(View.FOCUS_BACKWARD); break; case EditorInfo.IME_ACTION_DONE: WebViewClassic.this.hideSoftKeyboard(); break; case EditorInfo.IME_ACTION_GO: case EditorInfo.IME_ACTION_SEARCH: WebViewClassic.this.hideSoftKeyboard(); String text = getEditable().toString(); passToJavaScript(text, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); passToJavaScript(text, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)); break; default: handled = super.performEditorAction(editorAction); break; } return handled; } public void initEditorInfo(WebViewCore.TextFieldInitData initData) { int type = initData.mType; int inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; int imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN; if (!initData.mIsSpellCheckEnabled) { inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; } if (WebTextView.TEXT_AREA != type) { if (initData.mIsTextFieldNext) { imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT; } if (initData.mIsTextFieldPrev) { imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS; } } int action = EditorInfo.IME_ACTION_GO; switch (type) { case WebTextView.NORMAL_TEXT_FIELD: break; case WebTextView.TEXT_AREA: inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; action = EditorInfo.IME_ACTION_NONE; break; case WebTextView.PASSWORD: inputType |= EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD; break; case WebTextView.SEARCH: action = EditorInfo.IME_ACTION_SEARCH; break; case WebTextView.EMAIL: // inputType needs to be overwritten because of the different text variation. inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; break; case WebTextView.NUMBER: // inputType needs to be overwritten because of the different class. inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_NORMAL | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL; // Number and telephone do not have both a Tab key and an // action, so set the action to NEXT break; case WebTextView.TELEPHONE: // inputType needs to be overwritten because of the different class. inputType = InputType.TYPE_CLASS_PHONE; break; case WebTextView.URL: // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so // exclude it for now. inputType |= InputType.TYPE_TEXT_VARIATION_URI; break; default: break; } imeOptions |= action; mHint = initData.mLabel; mInputType = inputType; mImeOptions = imeOptions; mMaxLength = initData.mMaxLength; mIsAutoCompleteEnabled = initData.mIsAutoCompleteEnabled; mName = initData.mName; mAutoCompletePopup.clearAdapter(); } public void setupEditorInfo(EditorInfo outAttrs) { outAttrs.inputType = mInputType; outAttrs.imeOptions = mImeOptions; outAttrs.hintText = mHint; outAttrs.initialCapsMode = getCursorCapsMode(InputType.TYPE_CLASS_TEXT); Editable editable = getEditable(); int selectionStart = Selection.getSelectionStart(editable); int selectionEnd = Selection.getSelectionEnd(editable); if (selectionStart < 0 || selectionEnd < 0) { selectionStart = editable.length(); selectionEnd = selectionStart; } outAttrs.initialSelStart = selectionStart; outAttrs.initialSelEnd = selectionEnd; } @Override public boolean setSelection(int start, int end) { boolean result = super.setSelection(start, end); updateSelection(); return result; } @Override public boolean setComposingRegion(int start, int end) { boolean result = super.setComposingRegion(start, end); updateSelection(); return result; } /** * Send the selection and composing spans to the IME. */ private void updateSelection() { Editable editable = getEditable(); int selectionStart = Selection.getSelectionStart(editable); int selectionEnd = Selection.getSelectionEnd(editable); int composingStart = getComposingSpanStart(editable); int composingEnd = getComposingSpanEnd(editable); InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { imm.updateSelection(mWebView, selectionStart, selectionEnd, composingStart, composingEnd); } } /** * Sends a text change to webkit indirectly. If it is a single- * character add or delete, it sends it as a key stroke. If it cannot * be represented as a key stroke, it sends it as a field change. * @param start The start offset (inclusive) of the text being changed. * @param end The end offset (exclusive) of the text being changed. * @param text The new text to replace the changed text. */ private void setNewText(int start, int end, CharSequence text) { mIsKeySentByMe = true; Editable editable = getEditable(); CharSequence original = editable.subSequence(start, end); boolean isCharacterAdd = false; boolean isCharacterDelete = false; int textLength = text.length(); int originalLength = original.length(); int selectionStart = Selection.getSelectionStart(editable); int selectionEnd = Selection.getSelectionEnd(editable); if (selectionStart == selectionEnd) { if (textLength > originalLength) { isCharacterAdd = (textLength == originalLength + 1) && TextUtils.regionMatches(text, 0, original, 0, originalLength); } else if (originalLength > textLength) { isCharacterDelete = (textLength == originalLength - 1) && TextUtils.regionMatches(text, 0, original, 0, textLength); } } if (isCharacterAdd) { sendCharacter(text.charAt(textLength - 1)); } else if (isCharacterDelete) { sendKey(KeyEvent.KEYCODE_DEL); } else if ((textLength != originalLength) || !TextUtils.regionMatches(text, 0, original, 0, textLength)) { // Send a message so that key strokes and text replacement // do not come out of order. Message replaceMessage = mPrivateHandler.obtainMessage( REPLACE_TEXT, start, end, text.toString()); mPrivateHandler.sendMessage(replaceMessage); } if (mAutoCompletePopup != null) { StringBuilder newText = new StringBuilder(); newText.append(editable.subSequence(0, start)); newText.append(text); newText.append(editable.subSequence(end, editable.length())); mAutoCompletePopup.setText(newText.toString()); } mIsKeySentByMe = false; } /** * Send a single character to the WebView as a key down and up event. * @param c The character to be sent. */ private void sendCharacter(char c) { if (mKeyCharacterMap == null) { mKeyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); } char[] chars = new char[1]; chars[0] = c; KeyEvent[] events = mKeyCharacterMap.getEvents(chars); if (events != null) { for (KeyEvent event : events) { sendKeyEvent(event); } } else { Message msg = mPrivateHandler.obtainMessage(KEY_PRESS, (int) c, 0); mPrivateHandler.sendMessage(msg); } } /** * Send a key event for a specific key code, not a standard * unicode character. * @param keyCode The key code to send. */ private void sendKey(int keyCode) { long eventTime = SystemClock.uptimeMillis(); sendKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD)); sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD)); } private CharSequence limitReplaceTextByMaxLength(CharSequence text, int numReplaced) { if (mMaxLength > 0) { Editable editable = getEditable(); int maxReplace = mMaxLength - editable.length() + numReplaced; if (maxReplace < text.length()) { maxReplace = Math.max(maxReplace, 0); // New length is greater than the maximum. trim it down. text = text.subSequence(0, maxReplace); } } return text; } private void restartInput() { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { // Since the text has changed, do not allow the IME to replace the // existing text as though it were a completion. imm.restartInput(mWebView); } } } private class PastePopupWindow extends PopupWindow implements View.OnClickListener { private ViewGroup mContentView; private TextView mPasteTextView; public PastePopupWindow() { super(mContext, null, com.android.internal.R.attr.textSelectHandleWindowStyle); setClippingEnabled(true); LinearLayout linearLayout = new LinearLayout(mContext); linearLayout.setOrientation(LinearLayout.HORIZONTAL); mContentView = linearLayout; mContentView.setBackgroundResource( com.android.internal.R.drawable.text_edit_paste_window); LayoutInflater inflater = (LayoutInflater)mContext. getSystemService(Context.LAYOUT_INFLATER_SERVICE); ViewGroup.LayoutParams wrapContent = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); mPasteTextView = (TextView) inflater.inflate( com.android.internal.R.layout.text_edit_action_popup_text, null); mPasteTextView.setLayoutParams(wrapContent); mContentView.addView(mPasteTextView); mPasteTextView.setText(com.android.internal.R.string.paste); mPasteTextView.setOnClickListener(this); this.setContentView(mContentView); } public void show(Point cursorBottom, Point cursorTop, int windowLeft, int windowTop) { measureContent(); int width = mContentView.getMeasuredWidth(); int height = mContentView.getMeasuredHeight(); int y = cursorTop.y - height; int x = cursorTop.x - (width / 2); if (y < windowTop) { // There's not enough room vertically, move it below the // handle. ensureSelectionHandles(); y = cursorBottom.y + mSelectHandleCenter.getIntrinsicHeight(); x = cursorBottom.x - (width / 2); } if (x < windowLeft) { x = windowLeft; } if (!isShowing()) { showAtLocation(mWebView, Gravity.NO_GRAVITY, x, y); } update(x, y, width, height); } public void hide() { dismiss(); } @Override public void onClick(View view) { pasteFromClipboard(); selectionDone(); } protected void measureContent() { final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); mContentView.measure( View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, View.MeasureSpec.AT_MOST), View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, View.MeasureSpec.AT_MOST)); } } // if AUTO_REDRAW_HACK is true, then the CALL key will toggle redrawing // the screen all-the-time. Good for profiling our drawing code static private final boolean AUTO_REDRAW_HACK = false; // The rate at which edit text is scrolled in content pixels per millisecond static private final float TEXT_SCROLL_RATE = 0.01f; // The presumed scroll rate for the first scroll of edit text static private final long TEXT_SCROLL_FIRST_SCROLL_MS = 16; // Buffer pixels of the caret rectangle when moving edit text into view // after resize. static private final int EDIT_RECT_BUFFER = 10; static private final long SELECTION_HANDLE_ANIMATION_MS = 150; // true means redraw the screen all-the-time. Only with AUTO_REDRAW_HACK private boolean mAutoRedraw; // Reference to the AlertDialog displayed by InvokeListBox. // It's used to dismiss the dialog in destroy if not done before. private AlertDialog mListBoxDialog = null; // Reference to the save password dialog so it can be dimissed in // destroy if not done before. private AlertDialog mSavePasswordDialog = null; static final String LOGTAG = "webview"; private ZoomManager mZoomManager; private final Rect mInvScreenRect = new Rect(); private final Rect mScreenRect = new Rect(); private final RectF mVisibleContentRect = new RectF(); private boolean mIsWebViewVisible = true; WebViewInputConnection mInputConnection = null; private int mFieldPointer; private PastePopupWindow mPasteWindow; private AutoCompletePopup mAutoCompletePopup; Rect mEditTextContentBounds = new Rect(); Rect mEditTextContent = new Rect(); int mEditTextLayerId; boolean mIsEditingText = false; ArrayList mBatchedTextChanges = new ArrayList(); boolean mIsBatchingTextChanges = false; private long mLastEditScroll = 0; private static class OnTrimMemoryListener implements ComponentCallbacks2 { private static OnTrimMemoryListener sInstance = null; static void init(Context c) { if (sInstance == null) { sInstance = new OnTrimMemoryListener(c.getApplicationContext()); } } private OnTrimMemoryListener(Context c) { c.registerComponentCallbacks(this); } @Override public void onConfigurationChanged(Configuration newConfig) { // Ignore } @Override public void onLowMemory() { // Ignore } @Override public void onTrimMemory(int level) { if (DebugFlags.WEB_VIEW) { Log.d("WebView", "onTrimMemory: " + level); } // When framework reset EGL context during high memory pressure, all // the existing GL resources for the html5 video will be destroyed // at native side. // Here we just need to clean up the Surface Texture which is static. if (level > TRIM_MEMORY_UI_HIDDEN) { HTML5VideoInline.cleanupSurfaceTexture(); HTML5VideoView.release(); } WebViewClassic.nativeOnTrimMemory(level); } } // A final CallbackProxy shared by WebViewCore and BrowserFrame. private CallbackProxy mCallbackProxy; private WebViewDatabaseClassic mDatabase; // SSL certificate for the main top-level page (if secure) private SslCertificate mCertificate; // Native WebView pointer that is 0 until the native object has been // created. private int mNativeClass; // This would be final but it needs to be set to null when the WebView is // destroyed. private WebViewCore mWebViewCore; // Handler for dispatching UI messages. /* package */ final Handler mPrivateHandler = new PrivateHandler(); // Used to ignore changes to webkit text that arrives to the UI side after // more key events. private int mTextGeneration; /* package */ void incrementTextGeneration() { mTextGeneration++; } // Used by WebViewCore to create child views. /* package */ ViewManager mViewManager; // Used to display in full screen mode PluginFullScreenHolder mFullScreenHolder; /** * Position of the last touch event in pixels. * Use integer to prevent loss of dragging delta calculation accuracy; * which was done in float and converted to integer, and resulted in gradual * and compounding touch position and view dragging mismatch. */ private int mLastTouchX; private int mLastTouchY; private int mStartTouchX; private int mStartTouchY; private float mAverageAngle; /** * Time of the last touch event. */ private long mLastTouchTime; /** * Time of the last time sending touch event to WebViewCore */ private long mLastSentTouchTime; /** * The minimum elapsed time before sending another ACTION_MOVE event to * WebViewCore. This really should be tuned for each type of the devices. * For example in Google Map api test case, it takes Dream device at least * 150ms to do a full cycle in the WebViewCore by processing a touch event, * triggering the layout and drawing the picture. While the same process * takes 60+ms on the current high speed device. If we make * TOUCH_SENT_INTERVAL too small, there will be multiple touch events sent * to WebViewCore queue and the real layout and draw events will be pushed * to further, which slows down the refresh rate. Choose 50 to favor the * current high speed devices. For Dream like devices, 100 is a better * choice. Maybe make this in the buildspec later. * (Update 12/14/2010: changed to 0 since current device should be able to * handle the raw events and Map team voted to have the raw events too. */ private static final int TOUCH_SENT_INTERVAL = 0; private int mCurrentTouchInterval = TOUCH_SENT_INTERVAL; /** * Helper class to get velocity for fling */ VelocityTracker mVelocityTracker; private int mMaximumFling; private float mLastVelocity; private float mLastVelX; private float mLastVelY; // The id of the native layer being scrolled. private int mCurrentScrollingLayerId; private Rect mScrollingLayerRect = new Rect(); // only trigger accelerated fling if the new velocity is at least // MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION times of the previous velocity private static final float MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION = 0.2f; /** * Touch mode * TODO: Some of this is now unnecessary as it is handled by * WebInputTouchDispatcher (such as click, long press, and double tap). */ private int mTouchMode = TOUCH_DONE_MODE; private static final int TOUCH_INIT_MODE = 1; private static final int TOUCH_DRAG_START_MODE = 2; private static final int TOUCH_DRAG_MODE = 3; private static final int TOUCH_SHORTPRESS_START_MODE = 4; private static final int TOUCH_SHORTPRESS_MODE = 5; private static final int TOUCH_DOUBLE_TAP_MODE = 6; private static final int TOUCH_DONE_MODE = 7; private static final int TOUCH_PINCH_DRAG = 8; private static final int TOUCH_DRAG_LAYER_MODE = 9; private static final int TOUCH_DRAG_TEXT_MODE = 10; // true when the touch movement exceeds the slop private boolean mConfirmMove; private boolean mTouchInEditText; // Whether or not to draw the cursor ring. private boolean mDrawCursorRing = true; // true if onPause has been called (and not onResume) private boolean mIsPaused; private HitTestResult mInitialHitTestResult; private WebKitHitTest mFocusedNode; /** * Customizable constant */ // pre-computed square of ViewConfiguration.getScaledTouchSlop() private int mTouchSlopSquare; // pre-computed square of ViewConfiguration.getScaledDoubleTapSlop() private int mDoubleTapSlopSquare; // pre-computed density adjusted navigation slop private int mNavSlop; // This should be ViewConfiguration.getTapTimeout() // But system time out is 100ms, which is too short for the browser. // In the browser, if it switches out of tap too soon, jump tap won't work. // In addition, a double tap on a trackpad will always have a duration of // 300ms, so this value must be at least that (otherwise we will timeout the // first tap and convert it to a long press). private static final int TAP_TIMEOUT = 300; // This should be ViewConfiguration.getLongPressTimeout() // But system time out is 500ms, which is too short for the browser. // With a short timeout, it's difficult to treat trigger a short press. private static final int LONG_PRESS_TIMEOUT = 1000; // needed to avoid flinging after a pause of no movement private static final int MIN_FLING_TIME = 250; // draw unfiltered after drag is held without movement private static final int MOTIONLESS_TIME = 100; // The amount of content to overlap between two screens when going through // pages with the space bar, in pixels. private static final int PAGE_SCROLL_OVERLAP = 24; /** * These prevent calling requestLayout if either dimension is fixed. This * depends on the layout parameters and the measure specs. */ boolean mWidthCanMeasure; boolean mHeightCanMeasure; // Remember the last dimensions we sent to the native side so we can avoid // sending the same dimensions more than once. int mLastWidthSent; int mLastHeightSent; // Since view height sent to webkit could be fixed to avoid relayout, this // value records the last sent actual view height. int mLastActualHeightSent; private int mContentWidth; // cache of value from WebViewCore private int mContentHeight; // cache of value from WebViewCore // Need to have the separate control for horizontal and vertical scrollbar // style than the View's single scrollbar style private boolean mOverlayHorizontalScrollbar = true; private boolean mOverlayVerticalScrollbar = false; // our standard speed. this way small distances will be traversed in less // time than large distances, but we cap the duration, so that very large // distances won't take too long to get there. private static final int STD_SPEED = 480; // pixels per second // time for the longest scroll animation private static final int MAX_DURATION = 750; // milliseconds // Used by OverScrollGlow OverScroller mScroller; Scroller mEditTextScroller; private boolean mInOverScrollMode = false; private static Paint mOverScrollBackground; private static Paint mOverScrollBorder; private boolean mWrapContent; private static final int MOTIONLESS_FALSE = 0; private static final int MOTIONLESS_PENDING = 1; private static final int MOTIONLESS_TRUE = 2; private static final int MOTIONLESS_IGNORE = 3; private int mHeldMotionless; // Lazily-instantiated instance for injecting accessibility. private AccessibilityInjector mAccessibilityInjector; /** * How long the caret handle will last without being touched. */ private static final long CARET_HANDLE_STAMINA_MS = 3000; private Drawable mSelectHandleLeft; private Drawable mSelectHandleRight; private Drawable mSelectHandleCenter; private Point mSelectOffset; private Point mSelectCursorBase = new Point(); private Rect mSelectHandleBaseBounds = new Rect(); private int mSelectCursorBaseLayerId; private QuadF mSelectCursorBaseTextQuad = new QuadF(); private Point mSelectCursorExtent = new Point(); private Rect mSelectHandleExtentBounds = new Rect(); private int mSelectCursorExtentLayerId; private QuadF mSelectCursorExtentTextQuad = new QuadF(); private Point mSelectDraggingCursor; private QuadF mSelectDraggingTextQuad; private boolean mIsCaretSelection; static final int HANDLE_ID_BASE = 0; static final int HANDLE_ID_EXTENT = 1; // the color used to highlight the touch rectangles static final int HIGHLIGHT_COLOR = 0x6633b5e5; // the region indicating where the user touched on the screen private Region mTouchHighlightRegion = new Region(); // the paint for the touch highlight private Paint mTouchHightlightPaint = new Paint(); // debug only private static final boolean DEBUG_TOUCH_HIGHLIGHT = true; private static final int TOUCH_HIGHLIGHT_ELAPSE_TIME = 2000; private Paint mTouchCrossHairColor; private int mTouchHighlightX; private int mTouchHighlightY; private boolean mShowTapHighlight; // Basically this proxy is used to tell the Video to update layer tree at // SetBaseLayer time and to pause when WebView paused. private HTML5VideoViewProxy mHTML5VideoViewProxy; // If we are using a set picture, don't send view updates to webkit private boolean mBlockWebkitViewMessages = false; // cached value used to determine if we need to switch drawing models private boolean mHardwareAccelSkia = false; /* * Private message ids */ private static final int REMEMBER_PASSWORD = 1; private static final int NEVER_REMEMBER_PASSWORD = 2; private static final int SWITCH_TO_SHORTPRESS = 3; private static final int SWITCH_TO_LONGPRESS = 4; private static final int RELEASE_SINGLE_TAP = 5; private static final int REQUEST_FORM_DATA = 6; private static final int DRAG_HELD_MOTIONLESS = 8; private static final int PREVENT_DEFAULT_TIMEOUT = 10; private static final int SCROLL_SELECT_TEXT = 11; private static final int FIRST_PRIVATE_MSG_ID = REMEMBER_PASSWORD; private static final int LAST_PRIVATE_MSG_ID = SCROLL_SELECT_TEXT; /* * Package message ids */ static final int SCROLL_TO_MSG_ID = 101; static final int NEW_PICTURE_MSG_ID = 105; static final int WEBCORE_INITIALIZED_MSG_ID = 107; static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 108; static final int UPDATE_ZOOM_RANGE = 109; static final int TAKE_FOCUS = 110; static final int CLEAR_TEXT_ENTRY = 111; static final int UPDATE_TEXT_SELECTION_MSG_ID = 112; static final int SHOW_RECT_MSG_ID = 113; static final int LONG_PRESS_CENTER = 114; static final int PREVENT_TOUCH_ID = 115; static final int WEBCORE_NEED_TOUCH_EVENTS = 116; // obj=Rect in doc coordinates static final int INVAL_RECT_MSG_ID = 117; static final int REQUEST_KEYBOARD = 118; static final int SHOW_FULLSCREEN = 120; static final int HIDE_FULLSCREEN = 121; static final int UPDATE_MATCH_COUNT = 126; static final int CENTER_FIT_RECT = 127; static final int SET_SCROLLBAR_MODES = 129; static final int SELECTION_STRING_CHANGED = 130; static final int HIT_TEST_RESULT = 131; static final int SAVE_WEBARCHIVE_FINISHED = 132; static final int SET_AUTOFILLABLE = 133; static final int AUTOFILL_COMPLETE = 134; static final int SCREEN_ON = 136; static final int UPDATE_ZOOM_DENSITY = 139; static final int EXIT_FULLSCREEN_VIDEO = 140; static final int COPY_TO_CLIPBOARD = 141; static final int INIT_EDIT_FIELD = 142; static final int REPLACE_TEXT = 143; static final int CLEAR_CARET_HANDLE = 144; static final int KEY_PRESS = 145; static final int RELOCATE_AUTO_COMPLETE_POPUP = 146; static final int FOCUS_NODE_CHANGED = 147; static final int AUTOFILL_FORM = 148; static final int SCROLL_EDIT_TEXT = 149; static final int EDIT_TEXT_SIZE_CHANGED = 150; static final int SHOW_CARET_HANDLE = 151; static final int UPDATE_CONTENT_BOUNDS = 152; static final int SCROLL_HANDLE_INTO_VIEW = 153; private static final int FIRST_PACKAGE_MSG_ID = SCROLL_TO_MSG_ID; private static final int LAST_PACKAGE_MSG_ID = HIT_TEST_RESULT; static final String[] HandlerPrivateDebugString = { "REMEMBER_PASSWORD", // = 1; "NEVER_REMEMBER_PASSWORD", // = 2; "SWITCH_TO_SHORTPRESS", // = 3; "SWITCH_TO_LONGPRESS", // = 4; "RELEASE_SINGLE_TAP", // = 5; "REQUEST_FORM_DATA", // = 6; "RESUME_WEBCORE_PRIORITY", // = 7; "DRAG_HELD_MOTIONLESS", // = 8; "", // = 9; "PREVENT_DEFAULT_TIMEOUT", // = 10; "SCROLL_SELECT_TEXT" // = 11; }; static final String[] HandlerPackageDebugString = { "SCROLL_TO_MSG_ID", // = 101; "102", // = 102; "103", // = 103; "104", // = 104; "NEW_PICTURE_MSG_ID", // = 105; "UPDATE_TEXT_ENTRY_MSG_ID", // = 106; "WEBCORE_INITIALIZED_MSG_ID", // = 107; "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 108; "UPDATE_ZOOM_RANGE", // = 109; "UNHANDLED_NAV_KEY", // = 110; "CLEAR_TEXT_ENTRY", // = 111; "UPDATE_TEXT_SELECTION_MSG_ID", // = 112; "SHOW_RECT_MSG_ID", // = 113; "LONG_PRESS_CENTER", // = 114; "PREVENT_TOUCH_ID", // = 115; "WEBCORE_NEED_TOUCH_EVENTS", // = 116; "INVAL_RECT_MSG_ID", // = 117; "REQUEST_KEYBOARD", // = 118; "DO_MOTION_UP", // = 119; "SHOW_FULLSCREEN", // = 120; "HIDE_FULLSCREEN", // = 121; "DOM_FOCUS_CHANGED", // = 122; "REPLACE_BASE_CONTENT", // = 123; "RETURN_LABEL", // = 125; "UPDATE_MATCH_COUNT", // = 126; "CENTER_FIT_RECT", // = 127; "REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID", // = 128; "SET_SCROLLBAR_MODES", // = 129; "SELECTION_STRING_CHANGED", // = 130; "SET_TOUCH_HIGHLIGHT_RECTS", // = 131; "SAVE_WEBARCHIVE_FINISHED", // = 132; "SET_AUTOFILLABLE", // = 133; "AUTOFILL_COMPLETE", // = 134; "SELECT_AT", // = 135; "SCREEN_ON", // = 136; "ENTER_FULLSCREEN_VIDEO", // = 137; "UPDATE_SELECTION", // = 138; "UPDATE_ZOOM_DENSITY" // = 139; }; // If the site doesn't use the viewport meta tag to specify the viewport, // use DEFAULT_VIEWPORT_WIDTH as the default viewport width static final int DEFAULT_VIEWPORT_WIDTH = 980; // normally we try to fit the content to the minimum preferred width // calculated by the Webkit. To avoid the bad behavior when some site's // minimum preferred width keeps growing when changing the viewport width or // the minimum preferred width is huge, an upper limit is needed. static int sMaxViewportWidth = DEFAULT_VIEWPORT_WIDTH; // initial scale in percent. 0 means using default. private int mInitialScaleInPercent = 0; // Whether or not a scroll event should be sent to webkit. This is only set // to false when restoring the scroll position. private boolean mSendScrollEvent = true; private int mSnapScrollMode = SNAP_NONE; private static final int SNAP_NONE = 0; private static final int SNAP_LOCK = 1; // not a separate state private static final int SNAP_X = 2; // may be combined with SNAP_LOCK private static final int SNAP_Y = 4; // may be combined with SNAP_LOCK private boolean mSnapPositive; // keep these in sync with their counterparts in WebView.cpp private static final int DRAW_EXTRAS_NONE = 0; private static final int DRAW_EXTRAS_SELECTION = 1; private static final int DRAW_EXTRAS_CURSOR_RING = 2; // keep this in sync with WebCore:ScrollbarMode in WebKit private static final int SCROLLBAR_AUTO = 0; private static final int SCROLLBAR_ALWAYSOFF = 1; // as we auto fade scrollbar, this is ignored. private static final int SCROLLBAR_ALWAYSON = 2; private int mHorizontalScrollBarMode = SCROLLBAR_AUTO; private int mVerticalScrollBarMode = SCROLLBAR_AUTO; /** * Max distance to overscroll by in pixels. * This how far content can be pulled beyond its normal bounds by the user. */ private int mOverscrollDistance; /** * Max distance to overfling by in pixels. * This is how far flinged content can move beyond the end of its normal bounds. */ private int mOverflingDistance; private OverScrollGlow mOverScrollGlow; // Used to match key downs and key ups private Vector mKeysPressed; /* package */ static boolean mLogEvent = true; // for event log private long mLastTouchUpTime = 0; private WebViewCore.AutoFillData mAutoFillData; private static boolean sNotificationsEnabled = true; /** * URI scheme for telephone number */ public static final String SCHEME_TEL = "tel:"; /** * URI scheme for email address */ public static final String SCHEME_MAILTO = "mailto:"; /** * URI scheme for map address */ public static final String SCHEME_GEO = "geo:0,0?q="; private int mBackgroundColor = Color.WHITE; private static final long SELECT_SCROLL_INTERVAL = 1000 / 60; // 60 / second private int mAutoScrollX = 0; private int mAutoScrollY = 0; private int mMinAutoScrollX = 0; private int mMaxAutoScrollX = 0; private int mMinAutoScrollY = 0; private int mMaxAutoScrollY = 0; private Rect mScrollingLayerBounds = new Rect(); private boolean mSentAutoScrollMessage = false; // used for serializing asynchronously handled touch events. private WebViewInputDispatcher mInputDispatcher; // Used to track whether picture updating was paused due to a window focus change. private boolean mPictureUpdatePausedForFocusChange = false; // Used to notify listeners of a new picture. private PictureListener mPictureListener; // Used to notify listeners about find-on-page results. private WebView.FindListener mFindListener; // Used to prevent resending save password message private Message mResumeMsg; /** * Refer to {@link WebView#requestFocusNodeHref(Message)} for more information */ static class FocusNodeHref { static final String TITLE = "title"; static final String URL = "url"; static final String SRC = "src"; } public WebViewClassic(WebView webView, WebView.PrivateAccess privateAccess) { mWebView = webView; mWebViewPrivate = privateAccess; mContext = webView.getContext(); } /** * See {@link WebViewProvider#init(Map, boolean)} */ @Override public void init(Map javaScriptInterfaces, boolean privateBrowsing) { Context context = mContext; // Used by the chrome stack to find application paths JniUtil.setContext(context); mCallbackProxy = new CallbackProxy(context, this); mViewManager = new ViewManager(this); L10nUtils.setApplicationContext(context.getApplicationContext()); mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javaScriptInterfaces); mDatabase = WebViewDatabaseClassic.getInstance(context); mScroller = new OverScroller(context, null, 0, 0, false); //TODO Use OverScroller's flywheel mZoomManager = new ZoomManager(this, mCallbackProxy); /* The init method must follow the creation of certain member variables, * such as the mZoomManager. */ init(); setupPackageListener(context); setupProxyListener(context); setupTrustStorageListener(context); updateMultiTouchSupport(context); if (privateBrowsing) { startPrivateBrowsing(); } mAutoFillData = new WebViewCore.AutoFillData(); mEditTextScroller = new Scroller(context); } // WebViewProvider bindings static class Factory implements WebViewFactoryProvider, WebViewFactoryProvider.Statics { @Override public String findAddress(String addr) { return WebViewClassic.findAddress(addr); } @Override public void setPlatformNotificationsEnabled(boolean enable) { if (enable) { WebViewClassic.enablePlatformNotifications(); } else { WebViewClassic.disablePlatformNotifications(); } } @Override public Statics getStatics() { return this; } @Override public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) { return new WebViewClassic(webView, privateAccess); } @Override public GeolocationPermissions getGeolocationPermissions() { return GeolocationPermissionsClassic.getInstance(); } @Override public CookieManager getCookieManager() { return CookieManagerClassic.getInstance(); } @Override public WebIconDatabase getWebIconDatabase() { return WebIconDatabaseClassic.getInstance(); } @Override public WebStorage getWebStorage() { return WebStorageClassic.getInstance(); } @Override public WebViewDatabase getWebViewDatabase(Context context) { return WebViewDatabaseClassic.getInstance(context); } @Override public String getDefaultUserAgent(Context context) { return WebSettingsClassic.getDefaultUserAgentForLocale(context, Locale.getDefault()); } } private void onHandleUiEvent(MotionEvent event, int eventType, int flags) { switch (eventType) { case WebViewInputDispatcher.EVENT_TYPE_LONG_PRESS: HitTestResult hitTest = getHitTestResult(); if (hitTest != null) { mWebView.performLongClick(); } break; case WebViewInputDispatcher.EVENT_TYPE_DOUBLE_TAP: mZoomManager.handleDoubleTap(event.getX(), event.getY()); break; case WebViewInputDispatcher.EVENT_TYPE_TOUCH: onHandleUiTouchEvent(event); break; case WebViewInputDispatcher.EVENT_TYPE_CLICK: if (mFocusedNode != null && mFocusedNode.mIntentUrl != null) { mWebView.playSoundEffect(SoundEffectConstants.CLICK); overrideLoading(mFocusedNode.mIntentUrl); } break; } } private void onHandleUiTouchEvent(MotionEvent ev) { final ScaleGestureDetector detector = mZoomManager.getScaleGestureDetector(); int action = ev.getActionMasked(); final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; final boolean configChanged = action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN; final int skipIndex = pointerUp ? ev.getActionIndex() : -1; // Determine focal point float sumX = 0, sumY = 0; final int count = ev.getPointerCount(); for (int i = 0; i < count; i++) { if (skipIndex == i) continue; sumX += ev.getX(i); sumY += ev.getY(i); } final int div = pointerUp ? count - 1 : count; float x = sumX / div; float y = sumY / div; if (configChanged) { mLastTouchX = Math.round(x); mLastTouchY = Math.round(y); mLastTouchTime = ev.getEventTime(); mWebView.cancelLongPress(); mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); } if (detector != null) { detector.onTouchEvent(ev); if (detector.isInProgress()) { mLastTouchTime = ev.getEventTime(); if (!mZoomManager.supportsPanDuringZoom()) { return; } mTouchMode = TOUCH_DRAG_MODE; if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } } } if (action == MotionEvent.ACTION_POINTER_DOWN) { cancelTouch(); action = MotionEvent.ACTION_DOWN; } else if (action == MotionEvent.ACTION_MOVE) { // negative x or y indicate it is on the edge, skip it. if (x < 0 || y < 0) { return; } } handleTouchEventCommon(ev, action, Math.round(x), Math.round(y)); } // The webview that is bound to this WebViewClassic instance. Primarily needed for supplying // as the first param in the WebViewClient and WebChromeClient callbacks. final private WebView mWebView; // Callback interface, provides priviledged access into the WebView instance. final private WebView.PrivateAccess mWebViewPrivate; // Cached reference to mWebView.getContext(), for convenience. final private Context mContext; /** * @return The webview proxy that this classic webview is bound to. */ public WebView getWebView() { return mWebView; } @Override public ViewDelegate getViewDelegate() { return this; } @Override public ScrollDelegate getScrollDelegate() { return this; } public static WebViewClassic fromWebView(WebView webView) { return webView == null ? null : (WebViewClassic) webView.getWebViewProvider(); } // Accessors, purely for convenience (and to reduce code churn during webview proxy migration). int getScrollX() { return mWebView.getScrollX(); } int getScrollY() { return mWebView.getScrollY(); } int getWidth() { return mWebView.getWidth(); } int getHeight() { return mWebView.getHeight(); } Context getContext() { return mContext; } void invalidate() { mWebView.invalidate(); } // Setters for the Scroll X & Y, without invoking the onScrollChanged etc code paths. void setScrollXRaw(int mScrollX) { mWebViewPrivate.setScrollXRaw(mScrollX); } void setScrollYRaw(int mScrollY) { mWebViewPrivate.setScrollYRaw(mScrollY); } private static class TrustStorageListener extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(KeyChain.ACTION_STORAGE_CHANGED)) { handleCertTrustChanged(); } } } private static TrustStorageListener sTrustStorageListener; /** * Handles update to the trust storage. */ private static void handleCertTrustChanged() { // send a message for indicating trust storage change WebViewCore.sendStaticMessage(EventHub.TRUST_STORAGE_UPDATED, null); } /* * @param context This method expects this to be a valid context. */ private static void setupTrustStorageListener(Context context) { if (sTrustStorageListener != null ) { return; } IntentFilter filter = new IntentFilter(); filter.addAction(KeyChain.ACTION_STORAGE_CHANGED); sTrustStorageListener = new TrustStorageListener(); Intent current = context.getApplicationContext().registerReceiver(sTrustStorageListener, filter); if (current != null) { handleCertTrustChanged(); } } private static class ProxyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Proxy.PROXY_CHANGE_ACTION)) { handleProxyBroadcast(intent); } } } /* * Receiver for PROXY_CHANGE_ACTION, will be null when it is not added handling broadcasts. */ private static ProxyReceiver sProxyReceiver; /* * @param context This method expects this to be a valid context */ private static synchronized void setupProxyListener(Context context) { if (sProxyReceiver != null || sNotificationsEnabled == false) { return; } IntentFilter filter = new IntentFilter(); filter.addAction(Proxy.PROXY_CHANGE_ACTION); sProxyReceiver = new ProxyReceiver(); Intent currentProxy = context.getApplicationContext().registerReceiver( sProxyReceiver, filter); if (currentProxy != null) { handleProxyBroadcast(currentProxy); } } /* * @param context This method expects this to be a valid context */ private static synchronized void disableProxyListener(Context context) { if (sProxyReceiver == null) return; context.getApplicationContext().unregisterReceiver(sProxyReceiver); sProxyReceiver = null; } private static void handleProxyBroadcast(Intent intent) { ProxyProperties proxyProperties = (ProxyProperties)intent.getExtra(Proxy.EXTRA_PROXY_INFO); if (proxyProperties == null || proxyProperties.getHost() == null) { WebViewCore.sendStaticMessage(EventHub.PROXY_CHANGED, null); return; } WebViewCore.sendStaticMessage(EventHub.PROXY_CHANGED, proxyProperties); } /* * A variable to track if there is a receiver added for ACTION_PACKAGE_ADDED * or ACTION_PACKAGE_REMOVED. */ private static boolean sPackageInstallationReceiverAdded = false; /* * A set of Google packages we monitor for the * navigator.isApplicationInstalled() API. Add additional packages as * needed. */ private static Set sGoogleApps; static { sGoogleApps = new HashSet(); sGoogleApps.add("com.google.android.youtube"); } private static class PackageListener extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); final String packageName = intent.getData().getSchemeSpecificPart(); final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); if (Intent.ACTION_PACKAGE_REMOVED.equals(action) && replacing) { // if it is replacing, refreshPlugins() when adding return; } if (sGoogleApps.contains(packageName)) { if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { WebViewCore.sendStaticMessage(EventHub.ADD_PACKAGE_NAME, packageName); } else { WebViewCore.sendStaticMessage(EventHub.REMOVE_PACKAGE_NAME, packageName); } } PluginManager pm = PluginManager.getInstance(context); if (pm.containsPluginPermissionAndSignatures(packageName)) { pm.refreshPlugins(Intent.ACTION_PACKAGE_ADDED.equals(action)); } } } private void setupPackageListener(Context context) { /* * we must synchronize the instance check and the creation of the * receiver to ensure that only ONE receiver exists for all WebView * instances. */ synchronized (WebViewClassic.class) { // if the receiver already exists then we do not need to register it // again if (sPackageInstallationReceiverAdded) { return; } IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addDataScheme("package"); BroadcastReceiver packageListener = new PackageListener(); context.getApplicationContext().registerReceiver(packageListener, filter); sPackageInstallationReceiverAdded = true; } // check if any of the monitored apps are already installed AsyncTask> task = new AsyncTask>() { @Override protected Set doInBackground(Void... unused) { Set installedPackages = new HashSet(); PackageManager pm = mContext.getPackageManager(); for (String name : sGoogleApps) { try { pm.getPackageInfo(name, PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES); installedPackages.add(name); } catch (PackageManager.NameNotFoundException e) { // package not found } } return installedPackages; } // Executes on the UI thread @Override protected void onPostExecute(Set installedPackages) { if (mWebViewCore != null) { mWebViewCore.sendMessage(EventHub.ADD_PACKAGE_NAMES, installedPackages); } } }; task.execute(); } void updateMultiTouchSupport(Context context) { mZoomManager.updateMultiTouchSupport(context); } void updateJavaScriptEnabled(boolean enabled) { if (isAccessibilityInjectionEnabled()) { getAccessibilityInjector().updateJavaScriptEnabled(enabled); } } private void init() { OnTrimMemoryListener.init(mContext); mWebView.setWillNotDraw(false); mWebView.setClickable(true); mWebView.setLongClickable(true); final ViewConfiguration configuration = ViewConfiguration.get(mContext); int slop = configuration.getScaledTouchSlop(); mTouchSlopSquare = slop * slop; slop = configuration.getScaledDoubleTapSlop(); mDoubleTapSlopSquare = slop * slop; final float density = WebViewCore.getFixedDisplayDensity(mContext); // use one line height, 16 based on our current default font, for how // far we allow a touch be away from the edge of a link mNavSlop = (int) (16 * density); mZoomManager.init(density); mMaximumFling = configuration.getScaledMaximumFlingVelocity(); // Compute the inverse of the density squared. DRAG_LAYER_INVERSE_DENSITY_SQUARED = 1 / (density * density); mOverscrollDistance = configuration.getScaledOverscrollDistance(); mOverflingDistance = configuration.getScaledOverflingDistance(); setScrollBarStyle(mWebViewPrivate.super_getScrollBarStyle()); // Initially use a size of two, since the user is likely to only hold // down two keys at a time (shift + another key) mKeysPressed = new Vector(2); mHTML5VideoViewProxy = null ; } @Override public boolean shouldDelayChildPressedState() { return true; } @Override public boolean performAccessibilityAction(int action, Bundle arguments) { if (!mWebView.isEnabled()) { // Only default actions are supported while disabled. return mWebViewPrivate.super_performAccessibilityAction(action, arguments); } if (getAccessibilityInjector().supportsAccessibilityAction(action)) { return getAccessibilityInjector().performAccessibilityAction(action, arguments); } switch (action) { case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { final int convertedContentHeight = contentToViewY(getContentHeight()); final int adjustedViewHeight = getHeight() - mWebView.getPaddingTop() - mWebView.getPaddingBottom(); final int maxScrollY = Math.max(convertedContentHeight - adjustedViewHeight, 0); final boolean canScrollBackward = (getScrollY() > 0); final boolean canScrollForward = ((getScrollY() - maxScrollY) > 0); if ((action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) && canScrollBackward) { mWebView.scrollBy(0, adjustedViewHeight); return true; } if ((action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) && canScrollForward) { mWebView.scrollBy(0, -adjustedViewHeight); return true; } return false; } } return mWebViewPrivate.super_performAccessibilityAction(action, arguments); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { if (!mWebView.isEnabled()) { // Only default actions are supported while disabled. return; } info.setScrollable(isScrollableForAccessibility()); final int convertedContentHeight = contentToViewY(getContentHeight()); final int adjustedViewHeight = getHeight() - mWebView.getPaddingTop() - mWebView.getPaddingBottom(); final int maxScrollY = Math.max(convertedContentHeight - adjustedViewHeight, 0); final boolean canScrollBackward = (getScrollY() > 0); final boolean canScrollForward = ((getScrollY() - maxScrollY) > 0); if (canScrollForward) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } if (canScrollForward) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } getAccessibilityInjector().onInitializeAccessibilityNodeInfo(info); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { event.setScrollable(isScrollableForAccessibility()); event.setScrollX(getScrollX()); event.setScrollY(getScrollY()); final int convertedContentWidth = contentToViewX(getContentWidth()); final int adjustedViewWidth = getWidth() - mWebView.getPaddingLeft() - mWebView.getPaddingLeft(); event.setMaxScrollX(Math.max(convertedContentWidth - adjustedViewWidth, 0)); final int convertedContentHeight = contentToViewY(getContentHeight()); final int adjustedViewHeight = getHeight() - mWebView.getPaddingTop() - mWebView.getPaddingBottom(); event.setMaxScrollY(Math.max(convertedContentHeight - adjustedViewHeight, 0)); } private boolean isAccessibilityInjectionEnabled() { final AccessibilityManager manager = AccessibilityManager.getInstance(mContext); if (!manager.isEnabled()) { return false; } // Accessibility scripts should be injected only when a speaking service // is enabled. This may need to change later to accommodate Braille. final List services = manager.getEnabledAccessibilityServiceList( AccessibilityServiceInfo.FEEDBACK_SPOKEN); if (services.isEmpty()) { return false; } return true; } private AccessibilityInjector getAccessibilityInjector() { if (mAccessibilityInjector == null) { mAccessibilityInjector = new AccessibilityInjector(this); } return mAccessibilityInjector; } private boolean isScrollableForAccessibility() { return (contentToViewX(getContentWidth()) > getWidth() - mWebView.getPaddingLeft() - mWebView.getPaddingRight() || contentToViewY(getContentHeight()) > getHeight() - mWebView.getPaddingTop() - mWebView.getPaddingBottom()); } @Override public void setOverScrollMode(int mode) { if (mode != View.OVER_SCROLL_NEVER) { if (mOverScrollGlow == null) { mOverScrollGlow = new OverScrollGlow(this); } } else { mOverScrollGlow = null; } } /* package */ void adjustDefaultZoomDensity(int zoomDensity) { final float density = WebViewCore.getFixedDisplayDensity(mContext) * 100 / zoomDensity; updateDefaultZoomDensity(density); } /* package */ void updateDefaultZoomDensity(float density) { mNavSlop = (int) (16 * density); mZoomManager.updateDefaultZoomDensity(density); } /* package */ int getScaledNavSlop() { return viewToContentDimension(mNavSlop); } /* package */ boolean onSavePassword(String schemePlusHost, String username, String password, final Message resumeMsg) { boolean rVal = false; if (resumeMsg == null) { // null resumeMsg implies saving password silently mDatabase.setUsernamePassword(schemePlusHost, username, password); } else { if (mResumeMsg != null) { Log.w(LOGTAG, "onSavePassword should not be called while dialog is up"); resumeMsg.sendToTarget(); return true; } mResumeMsg = resumeMsg; final Message remember = mPrivateHandler.obtainMessage( REMEMBER_PASSWORD); remember.getData().putString("host", schemePlusHost); remember.getData().putString("username", username); remember.getData().putString("password", password); remember.obj = resumeMsg; final Message neverRemember = mPrivateHandler.obtainMessage( NEVER_REMEMBER_PASSWORD); neverRemember.getData().putString("host", schemePlusHost); neverRemember.getData().putString("username", username); neverRemember.getData().putString("password", password); neverRemember.obj = resumeMsg; mSavePasswordDialog = new AlertDialog.Builder(mContext) .setTitle(com.android.internal.R.string.save_password_label) .setMessage(com.android.internal.R.string.save_password_message) .setPositiveButton(com.android.internal.R.string.save_password_notnow, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (mResumeMsg != null) { resumeMsg.sendToTarget(); mResumeMsg = null; } mSavePasswordDialog = null; } }) .setNeutralButton(com.android.internal.R.string.save_password_remember, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (mResumeMsg != null) { remember.sendToTarget(); mResumeMsg = null; } mSavePasswordDialog = null; } }) .setNegativeButton(com.android.internal.R.string.save_password_never, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (mResumeMsg != null) { neverRemember.sendToTarget(); mResumeMsg = null; } mSavePasswordDialog = null; } }) .setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { if (mResumeMsg != null) { resumeMsg.sendToTarget(); mResumeMsg = null; } mSavePasswordDialog = null; } }).show(); // Return true so that WebViewCore will pause while the dialog is // up. rVal = true; } return rVal; } @Override public void setScrollBarStyle(int style) { if (style == View.SCROLLBARS_INSIDE_INSET || style == View.SCROLLBARS_OUTSIDE_INSET) { mOverlayHorizontalScrollbar = mOverlayVerticalScrollbar = false; } else { mOverlayHorizontalScrollbar = mOverlayVerticalScrollbar = true; } } /** * See {@link WebView#setHorizontalScrollbarOverlay(boolean)} */ @Override public void setHorizontalScrollbarOverlay(boolean overlay) { mOverlayHorizontalScrollbar = overlay; } /** * See {@link WebView#setVerticalScrollbarOverlay(boolean) */ @Override public void setVerticalScrollbarOverlay(boolean overlay) { mOverlayVerticalScrollbar = overlay; } /** * See {@link WebView#overlayHorizontalScrollbar()} */ @Override public boolean overlayHorizontalScrollbar() { return mOverlayHorizontalScrollbar; } /** * See {@link WebView#overlayVerticalScrollbar()} */ @Override public boolean overlayVerticalScrollbar() { return mOverlayVerticalScrollbar; } /* * Return the width of the view where the content of WebView should render * to. * Note: this can be called from WebCoreThread. */ /* package */ int getViewWidth() { if (!mWebView.isVerticalScrollBarEnabled() || mOverlayVerticalScrollbar) { return getWidth(); } else { return Math.max(0, getWidth() - mWebView.getVerticalScrollbarWidth()); } } // Interface to enable the browser to override title bar handling. public interface TitleBarDelegate { int getTitleHeight(); public void onSetEmbeddedTitleBar(final View title); } /** * Returns the height (in pixels) of the embedded title bar (if any). Does not care about * scrolling */ protected int getTitleHeight() { if (mWebView instanceof TitleBarDelegate) { return ((TitleBarDelegate) mWebView).getTitleHeight(); } return 0; } /** * See {@link WebView#getVisibleTitleHeight()} */ @Override @Deprecated public int getVisibleTitleHeight() { // Actually, this method returns the height of the embedded title bar if one is set via the // hidden setEmbeddedTitleBar method. return getVisibleTitleHeightImpl(); } private int getVisibleTitleHeightImpl() { // need to restrict mScrollY due to over scroll return Math.max(getTitleHeight() - Math.max(0, getScrollY()), getOverlappingActionModeHeight()); } private int mCachedOverlappingActionModeHeight = -1; private int getOverlappingActionModeHeight() { if (mFindCallback == null) { return 0; } if (mCachedOverlappingActionModeHeight < 0) { mWebView.getGlobalVisibleRect(mGlobalVisibleRect, mGlobalVisibleOffset); mCachedOverlappingActionModeHeight = Math.max(0, mFindCallback.getActionModeGlobalBottom() - mGlobalVisibleRect.top); } return mCachedOverlappingActionModeHeight; } /* * Return the height of the view where the content of WebView should render * to. Note that this excludes mTitleBar, if there is one. * Note: this can be called from WebCoreThread. */ /* package */ int getViewHeight() { return getViewHeightWithTitle() - getVisibleTitleHeightImpl(); } int getViewHeightWithTitle() { int height = getHeight(); if (mWebView.isHorizontalScrollBarEnabled() && !mOverlayHorizontalScrollbar) { height -= mWebViewPrivate.getHorizontalScrollbarHeight(); } return height; } /** * See {@link WebView#getCertificate()} */ @Override public SslCertificate getCertificate() { return mCertificate; } /** * See {@link WebView#setCertificate(SslCertificate)} */ @Override public void setCertificate(SslCertificate certificate) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "setCertificate=" + certificate); } // here, the certificate can be null (if the site is not secure) mCertificate = certificate; } //------------------------------------------------------------------------- // Methods called by activity //------------------------------------------------------------------------- /** * See {@link WebView#savePassword(String, String, String)} */ @Override public void savePassword(String host, String username, String password) { mDatabase.setUsernamePassword(host, username, password); } /** * See {@link WebView#setHttpAuthUsernamePassword(String, String, String, String)} */ @Override public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { mDatabase.setHttpAuthUsernamePassword(host, realm, username, password); } /** * See {@link WebView#getHttpAuthUsernamePassword(String, String)} */ @Override public String[] getHttpAuthUsernamePassword(String host, String realm) { return mDatabase.getHttpAuthUsernamePassword(host, realm); } /** * Remove Find or Select ActionModes, if active. */ private void clearActionModes() { if (mSelectCallback != null) { mSelectCallback.finish(); } if (mFindCallback != null) { mFindCallback.finish(); } } /** * Called to clear state when moving from one page to another, or changing * in some other way that makes elements associated with the current page * (such as ActionModes) no longer relevant. */ private void clearHelpers() { hideSoftKeyboard(); clearActionModes(); dismissFullScreenMode(); cancelDialogs(); } private void cancelDialogs() { if (mListBoxDialog != null) { mListBoxDialog.cancel(); mListBoxDialog = null; } if (mSavePasswordDialog != null) { mSavePasswordDialog.dismiss(); mSavePasswordDialog = null; } } /** * See {@link WebView#destroy()} */ @Override public void destroy() { if (mWebView.getViewRootImpl() != null) { Log.e(LOGTAG, "Error: WebView.destroy() called while still attached!"); } ensureFunctorDetached(); destroyJava(); destroyNative(); } private void ensureFunctorDetached() { if (mWebView.isHardwareAccelerated()) { int drawGLFunction = nativeGetDrawGLFunction(mNativeClass); ViewRootImpl viewRoot = mWebView.getViewRootImpl(); if (drawGLFunction != 0 && viewRoot != null) { viewRoot.detachFunctor(drawGLFunction); } } } private void destroyJava() { mCallbackProxy.blockMessages(); if (mAccessibilityInjector != null) { mAccessibilityInjector.destroy(); mAccessibilityInjector = null; } if (mWebViewCore != null) { // Tell WebViewCore to destroy itself synchronized (this) { WebViewCore webViewCore = mWebViewCore; mWebViewCore = null; // prevent using partial webViewCore webViewCore.destroy(); } // Remove any pending messages that might not be serviced yet. mPrivateHandler.removeCallbacksAndMessages(null); } } private void destroyNative() { if (mNativeClass == 0) return; int nptr = mNativeClass; mNativeClass = 0; if (Thread.currentThread() == mPrivateHandler.getLooper().getThread()) { // We are on the main thread and can safely delete nativeDestroy(nptr); } else { mPrivateHandler.post(new DestroyNativeRunnable(nptr)); } } private static class DestroyNativeRunnable implements Runnable { private int mNativePtr; public DestroyNativeRunnable(int nativePtr) { mNativePtr = nativePtr; } @Override public void run() { // nativeDestroy also does a stopGL() nativeDestroy(mNativePtr); } } /** * See {@link WebView#enablePlatformNotifications()} */ @Deprecated public static void enablePlatformNotifications() { synchronized (WebViewClassic.class) { sNotificationsEnabled = true; Context context = JniUtil.getContext(); if (context != null) setupProxyListener(context); } } /** * See {@link WebView#disablePlatformNotifications()} */ @Deprecated public static void disablePlatformNotifications() { synchronized (WebViewClassic.class) { sNotificationsEnabled = false; Context context = JniUtil.getContext(); if (context != null) disableProxyListener(context); } } /** * Sets JavaScript engine flags. * * @param flags JS engine flags in a String * * This is an implementation detail. */ public void setJsFlags(String flags) { mWebViewCore.sendMessage(EventHub.SET_JS_FLAGS, flags); } /** * See {@link WebView#setNetworkAvailable(boolean)} */ @Override public void setNetworkAvailable(boolean networkUp) { mWebViewCore.sendMessage(EventHub.SET_NETWORK_STATE, networkUp ? 1 : 0, 0); } /** * Inform WebView about the current network type. */ public void setNetworkType(String type, String subtype) { Map map = new HashMap(); map.put("type", type); map.put("subtype", subtype); mWebViewCore.sendMessage(EventHub.SET_NETWORK_TYPE, map); } /** * See {@link WebView#saveState(Bundle)} */ @Override public WebBackForwardList saveState(Bundle outState) { if (outState == null) { return null; } // We grab a copy of the back/forward list because a client of WebView // may have invalidated the history list by calling clearHistory. WebBackForwardListClassic list = copyBackForwardList(); final int currentIndex = list.getCurrentIndex(); final int size = list.getSize(); // We should fail saving the state if the list is empty or the index is // not in a valid range. if (currentIndex < 0 || currentIndex >= size || size == 0) { return null; } outState.putInt("index", currentIndex); // FIXME: This should just be a byte[][] instead of ArrayList but // Parcel.java does not have the code to handle multi-dimensional // arrays. ArrayList history = new ArrayList(size); for (int i = 0; i < size; i++) { WebHistoryItemClassic item = list.getItemAtIndex(i); if (null == item) { // FIXME: this shouldn't happen // need to determine how item got set to null Log.w(LOGTAG, "saveState: Unexpected null history item."); return null; } byte[] data = item.getFlattenedData(); if (data == null) { // It would be very odd to not have any data for a given history // item. And we will fail to rebuild the history list without // flattened data. return null; } history.add(data); } outState.putSerializable("history", history); if (mCertificate != null) { outState.putBundle("certificate", SslCertificate.saveState(mCertificate)); } outState.putBoolean("privateBrowsingEnabled", isPrivateBrowsingEnabled()); mZoomManager.saveZoomState(outState); return list; } /** * See {@link WebView#savePicture(Bundle, File)} */ @Override @Deprecated public boolean savePicture(Bundle b, final File dest) { if (dest == null || b == null) { return false; } final Picture p = capturePicture(); // Use a temporary file while writing to ensure the destination file // contains valid data. final File temp = new File(dest.getPath() + ".writing"); new Thread(new Runnable() { @Override public void run() { FileOutputStream out = null; try { out = new FileOutputStream(temp); p.writeToStream(out); // Writing the picture succeeded, rename the temporary file // to the destination. temp.renameTo(dest); } catch (Exception e) { // too late to do anything about it. } finally { if (out != null) { try { out.close(); } catch (Exception e) { // Can't do anything about that } } temp.delete(); } } }).start(); // now update the bundle b.putInt("scrollX", getScrollX()); b.putInt("scrollY", getScrollY()); mZoomManager.saveZoomState(b); return true; } private void restoreHistoryPictureFields(Picture p, Bundle b) { int sx = b.getInt("scrollX", 0); int sy = b.getInt("scrollY", 0); mDrawHistory = true; mHistoryPicture = p; setScrollXRaw(sx); setScrollYRaw(sy); mZoomManager.restoreZoomState(b); final float scale = mZoomManager.getScale(); mHistoryWidth = Math.round(p.getWidth() * scale); mHistoryHeight = Math.round(p.getHeight() * scale); invalidate(); } /** * See {@link WebView#restorePicture(Bundle, File)}; */ @Override @Deprecated public boolean restorePicture(Bundle b, File src) { if (src == null || b == null) { return false; } if (!src.exists()) { return false; } try { final FileInputStream in = new FileInputStream(src); final Bundle copy = new Bundle(b); new Thread(new Runnable() { @Override public void run() { try { final Picture p = Picture.createFromStream(in); if (p != null) { // Post a runnable on the main thread to update the // history picture fields. mPrivateHandler.post(new Runnable() { @Override public void run() { restoreHistoryPictureFields(p, copy); } }); } } finally { try { in.close(); } catch (Exception e) { // Nothing we can do now. } } } }).start(); } catch (FileNotFoundException e){ e.printStackTrace(); } return true; } /** * Saves the view data to the output stream. The output is highly * version specific, and may not be able to be loaded by newer versions * of WebView. * @param stream The {@link OutputStream} to save to * @param callback The {@link ValueCallback} to call with the result */ public void saveViewState(OutputStream stream, ValueCallback callback) { if (mWebViewCore == null) { callback.onReceiveValue(false); return; } mWebViewCore.sendMessageAtFrontOfQueue(EventHub.SAVE_VIEW_STATE, new WebViewCore.SaveViewStateRequest(stream, callback)); } /** * Loads the view data from the input stream. See * {@link #saveViewState(java.io.OutputStream, ValueCallback)} for more information. * @param stream The {@link InputStream} to load from */ public void loadViewState(InputStream stream) { mBlockWebkitViewMessages = true; new AsyncTask() { @Override protected DrawData doInBackground(InputStream... params) { try { return ViewStateSerializer.deserializeViewState(params[0]); } catch (IOException e) { return null; } } @Override protected void onPostExecute(DrawData draw) { if (draw == null) { Log.e(LOGTAG, "Failed to load view state!"); return; } int viewWidth = getViewWidth(); int viewHeight = getViewHeightWithTitle() - getTitleHeight(); draw.mViewSize = new Point(viewWidth, viewHeight); draw.mViewState.mDefaultScale = getDefaultZoomScale(); mLoadedPicture = draw; setNewPicture(mLoadedPicture, true); mLoadedPicture.mViewState = null; } }.execute(stream); } /** * Clears the view state set with {@link #loadViewState(InputStream)}. * This WebView will then switch to showing the content from webkit */ public void clearViewState() { mBlockWebkitViewMessages = false; mLoadedPicture = null; invalidate(); } /** * See {@link WebView#restoreState(Bundle)} */ @Override public WebBackForwardList restoreState(Bundle inState) { WebBackForwardListClassic returnList = null; if (inState == null) { return returnList; } if (inState.containsKey("index") && inState.containsKey("history")) { mCertificate = SslCertificate.restoreState( inState.getBundle("certificate")); final WebBackForwardListClassic list = mCallbackProxy.getBackForwardList(); final int index = inState.getInt("index"); // We can't use a clone of the list because we need to modify the // shared copy, so synchronize instead to prevent concurrent // modifications. synchronized (list) { final List history = (List) inState.getSerializable("history"); final int size = history.size(); // Check the index bounds so we don't crash in native code while // restoring the history index. if (index < 0 || index >= size) { return null; } for (int i = 0; i < size; i++) { byte[] data = history.remove(0); if (data == null) { // If we somehow have null data, we cannot reconstruct // the item and thus our history list cannot be rebuilt. return null; } WebHistoryItem item = new WebHistoryItemClassic(data); list.addHistoryItem(item); } // Grab the most recent copy to return to the caller. returnList = copyBackForwardList(); // Update the copy to have the correct index. returnList.setCurrentIndex(index); } // Restore private browsing setting. if (inState.getBoolean("privateBrowsingEnabled")) { getSettings().setPrivateBrowsingEnabled(true); } mZoomManager.restoreZoomState(inState); // Remove all pending messages because we are restoring previous // state. mWebViewCore.removeMessages(); if (isAccessibilityInjectionEnabled()) { getAccessibilityInjector().addAccessibilityApisIfNecessary(); } // Send a restore state message. mWebViewCore.sendMessage(EventHub.RESTORE_STATE, index); } return returnList; } /** * See {@link WebView#loadUrl(String, Map)} */ @Override public void loadUrl(String url, Map additionalHttpHeaders) { loadUrlImpl(url, additionalHttpHeaders); } private void loadUrlImpl(String url, Map extraHeaders) { switchOutDrawHistory(); WebViewCore.GetUrlData arg = new WebViewCore.GetUrlData(); arg.mUrl = url; arg.mExtraHeaders = extraHeaders; mWebViewCore.sendMessage(EventHub.LOAD_URL, arg); clearHelpers(); } /** * See {@link WebView#loadUrl(String)} */ @Override public void loadUrl(String url) { loadUrlImpl(url); } private void loadUrlImpl(String url) { if (url == null) { return; } loadUrlImpl(url, null); } /** * See {@link WebView#postUrl(String, byte[])} */ @Override public void postUrl(String url, byte[] postData) { if (URLUtil.isNetworkUrl(url)) { switchOutDrawHistory(); WebViewCore.PostUrlData arg = new WebViewCore.PostUrlData(); arg.mUrl = url; arg.mPostData = postData; mWebViewCore.sendMessage(EventHub.POST_URL, arg); clearHelpers(); } else { loadUrlImpl(url); } } /** * See {@link WebView#loadData(String, String, String)} */ @Override public void loadData(String data, String mimeType, String encoding) { loadDataImpl(data, mimeType, encoding); } private void loadDataImpl(String data, String mimeType, String encoding) { StringBuilder dataUrl = new StringBuilder("data:"); dataUrl.append(mimeType); if ("base64".equals(encoding)) { dataUrl.append(";base64"); } dataUrl.append(","); dataUrl.append(data); loadUrlImpl(dataUrl.toString()); } /** * See {@link WebView#loadDataWithBaseURL(String, String, String, String, String)} */ @Override public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) { loadDataImpl(data, mimeType, encoding); return; } switchOutDrawHistory(); WebViewCore.BaseUrlData arg = new WebViewCore.BaseUrlData(); arg.mBaseUrl = baseUrl; arg.mData = data; arg.mMimeType = mimeType; arg.mEncoding = encoding; arg.mHistoryUrl = historyUrl; mWebViewCore.sendMessage(EventHub.LOAD_DATA, arg); clearHelpers(); } /** * See {@link WebView#saveWebArchive(String)} */ @Override public void saveWebArchive(String filename) { saveWebArchiveImpl(filename, false, null); } /* package */ static class SaveWebArchiveMessage { SaveWebArchiveMessage (String basename, boolean autoname, ValueCallback callback) { mBasename = basename; mAutoname = autoname; mCallback = callback; } /* package */ final String mBasename; /* package */ final boolean mAutoname; /* package */ final ValueCallback mCallback; /* package */ String mResultFile; } /** * See {@link WebView#saveWebArchive(String, boolean, ValueCallback)} */ @Override public void saveWebArchive(String basename, boolean autoname, ValueCallback callback) { saveWebArchiveImpl(basename, autoname, callback); } private void saveWebArchiveImpl(String basename, boolean autoname, ValueCallback callback) { mWebViewCore.sendMessage(EventHub.SAVE_WEBARCHIVE, new SaveWebArchiveMessage(basename, autoname, callback)); } /** * See {@link WebView#stopLoading()} */ @Override public void stopLoading() { // TODO: should we clear all the messages in the queue before sending // STOP_LOADING? switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.STOP_LOADING); } /** * See {@link WebView#reload()} */ @Override public void reload() { clearHelpers(); switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.RELOAD); } /** * See {@link WebView#canGoBack()} */ @Override public boolean canGoBack() { WebBackForwardListClassic l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { return false; } else { return l.getCurrentIndex() > 0; } } } /** * See {@link WebView#goBack()} */ @Override public void goBack() { goBackOrForwardImpl(-1); } /** * See {@link WebView#canGoForward()} */ @Override public boolean canGoForward() { WebBackForwardListClassic l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { return false; } else { return l.getCurrentIndex() < l.getSize() - 1; } } } /** * See {@link WebView#goForward()} */ @Override public void goForward() { goBackOrForwardImpl(1); } /** * See {@link WebView#canGoBackOrForward(int)} */ @Override public boolean canGoBackOrForward(int steps) { WebBackForwardListClassic l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { return false; } else { int newIndex = l.getCurrentIndex() + steps; return newIndex >= 0 && newIndex < l.getSize(); } } } /** * See {@link WebView#goBackOrForward(int)} */ @Override public void goBackOrForward(int steps) { goBackOrForwardImpl(steps); } private void goBackOrForwardImpl(int steps) { goBackOrForward(steps, false); } private void goBackOrForward(int steps, boolean ignoreSnapshot) { if (steps != 0) { clearHelpers(); mWebViewCore.sendMessage(EventHub.GO_BACK_FORWARD, steps, ignoreSnapshot ? 1 : 0); } } /** * See {@link WebView#isPrivateBrowsingEnabled()} */ @Override public boolean isPrivateBrowsingEnabled() { WebSettingsClassic settings = getSettings(); return (settings != null) ? settings.isPrivateBrowsingEnabled() : false; } private void startPrivateBrowsing() { getSettings().setPrivateBrowsingEnabled(true); } private boolean extendScroll(int y) { int finalY = mScroller.getFinalY(); int newY = pinLocY(finalY + y); if (newY == finalY) return false; mScroller.setFinalY(newY); mScroller.extendDuration(computeDuration(0, y)); return true; } /** * See {@link WebView#pageUp(boolean)} */ @Override public boolean pageUp(boolean top) { if (mNativeClass == 0) { return false; } if (top) { // go to the top of the document return pinScrollTo(getScrollX(), 0, true, 0); } // Page up int h = getHeight(); int y; if (h > 2 * PAGE_SCROLL_OVERLAP) { y = -h + PAGE_SCROLL_OVERLAP; } else { y = -h / 2; } return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) : extendScroll(y); } /** * See {@link WebView#pageDown(boolean)} */ @Override public boolean pageDown(boolean bottom) { if (mNativeClass == 0) { return false; } if (bottom) { return pinScrollTo(getScrollX(), computeRealVerticalScrollRange(), true, 0); } // Page down. int h = getHeight(); int y; if (h > 2 * PAGE_SCROLL_OVERLAP) { y = h - PAGE_SCROLL_OVERLAP; } else { y = h / 2; } return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) : extendScroll(y); } /** * See {@link WebView#clearView()} */ @Override public void clearView() { mContentWidth = 0; mContentHeight = 0; setBaseLayer(0, false, false); mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT); } /** * See {@link WebView#capturePicture()} */ @Override public Picture capturePicture() { if (mNativeClass == 0) return null; Picture result = new Picture(); nativeCopyBaseContentToPicture(result); return result; } /** * See {@link WebView#getScale()} */ @Override public float getScale() { return mZoomManager.getScale(); } /** * Compute the reading level scale of the WebView * @param scale The current scale. * @return The reading level scale. */ /*package*/ float computeReadingLevelScale(float scale) { return mZoomManager.computeReadingLevelScale(scale); } /** * See {@link WebView#setInitialScale(int)} */ @Override public void setInitialScale(int scaleInPercent) { mZoomManager.setInitialScaleInPercent(scaleInPercent); } /** * See {@link WebView#invokeZoomPicker()} */ @Override public void invokeZoomPicker() { if (!getSettings().supportZoom()) { Log.w(LOGTAG, "This WebView doesn't support zoom."); return; } clearHelpers(); mZoomManager.invokeZoomPicker(); } /** * See {@link WebView#getHitTestResult()} */ @Override public HitTestResult getHitTestResult() { return mInitialHitTestResult; } // No left edge for double-tap zoom alignment static final int NO_LEFTEDGE = -1; int getBlockLeftEdge(int x, int y, float readingScale) { float invReadingScale = 1.0f / readingScale; int readingWidth = (int) (getViewWidth() * invReadingScale); int left = NO_LEFTEDGE; if (mFocusedNode != null) { final int length = mFocusedNode.mEnclosingParentRects.length; for (int i = 0; i < length; i++) { Rect rect = mFocusedNode.mEnclosingParentRects[i]; if (rect.width() < mFocusedNode.mHitTestSlop) { // ignore bounding boxes that are too small continue; } else if (rect.width() > readingWidth) { // stop when bounding box doesn't fit the screen width // at reading scale break; } left = rect.left; } } return left; } /** * See {@link WebView#requestFocusNodeHref(Message)} */ @Override public void requestFocusNodeHref(Message hrefMsg) { if (hrefMsg == null) { return; } int contentX = viewToContentX(mLastTouchX + getScrollX()); int contentY = viewToContentY(mLastTouchY + getScrollY()); if (mFocusedNode != null && mFocusedNode.mHitTestX == contentX && mFocusedNode.mHitTestY == contentY) { hrefMsg.getData().putString(FocusNodeHref.URL, mFocusedNode.mLinkUrl); hrefMsg.getData().putString(FocusNodeHref.TITLE, mFocusedNode.mAnchorText); hrefMsg.getData().putString(FocusNodeHref.SRC, mFocusedNode.mImageUrl); hrefMsg.sendToTarget(); return; } mWebViewCore.sendMessage(EventHub.REQUEST_CURSOR_HREF, contentX, contentY, hrefMsg); } /** * See {@link WebView#requestImageRef(Message)} */ @Override public void requestImageRef(Message msg) { if (0 == mNativeClass) return; // client isn't initialized String url = mFocusedNode != null ? mFocusedNode.mImageUrl : null; Bundle data = msg.getData(); data.putString("url", url); msg.setData(data); msg.sendToTarget(); } static int pinLoc(int x, int viewMax, int docMax) { // Log.d(LOGTAG, "-- pinLoc " + x + " " + viewMax + " " + docMax); if (docMax < viewMax) { // the doc has room on the sides for "blank" // pin the short document to the top/left of the screen x = 0; // Log.d(LOGTAG, "--- center " + x); } else if (x < 0) { x = 0; // Log.d(LOGTAG, "--- zero"); } else if (x + viewMax > docMax) { x = docMax - viewMax; // Log.d(LOGTAG, "--- pin " + x); } return x; } // Expects x in view coordinates int pinLocX(int x) { if (mInOverScrollMode) return x; return pinLoc(x, getViewWidth(), computeRealHorizontalScrollRange()); } // Expects y in view coordinates int pinLocY(int y) { if (mInOverScrollMode) return y; return pinLoc(y, getViewHeightWithTitle(), computeRealVerticalScrollRange() + getTitleHeight()); } /** * Given a distance in view space, convert it to content space. Note: this * does not reflect translation, just scaling, so this should not be called * with coordinates, but should be called for dimensions like width or * height. */ private int viewToContentDimension(int d) { return Math.round(d * mZoomManager.getInvScale()); } /** * Given an x coordinate in view space, convert it to content space. Also * may be used for absolute heights. */ /*package*/ int viewToContentX(int x) { return viewToContentDimension(x); } /** * Given a y coordinate in view space, convert it to content space. * Takes into account the height of the title bar if there is one * embedded into the WebView. */ /*package*/ int viewToContentY(int y) { return viewToContentDimension(y - getTitleHeight()); } /** * Given a x coordinate in view space, convert it to content space. * Returns the result as a float. */ private float viewToContentXf(int x) { return x * mZoomManager.getInvScale(); } /** * Given a y coordinate in view space, convert it to content space. * Takes into account the height of the title bar if there is one * embedded into the WebView. Returns the result as a float. */ private float viewToContentYf(int y) { return (y - getTitleHeight()) * mZoomManager.getInvScale(); } /** * Given a distance in content space, convert it to view space. Note: this * does not reflect translation, just scaling, so this should not be called * with coordinates, but should be called for dimensions like width or * height. */ /*package*/ int contentToViewDimension(int d) { return Math.round(d * mZoomManager.getScale()); } /** * Given an x coordinate in content space, convert it to view * space. */ /*package*/ int contentToViewX(int x) { return contentToViewDimension(x); } /** * Given a y coordinate in content space, convert it to view * space. Takes into account the height of the title bar. */ /*package*/ int contentToViewY(int y) { return contentToViewDimension(y) + getTitleHeight(); } private Rect contentToViewRect(Rect x) { return new Rect(contentToViewX(x.left), contentToViewY(x.top), contentToViewX(x.right), contentToViewY(x.bottom)); } /* To invalidate a rectangle in content coordinates, we need to transform the rect into view coordinates, so we can then call invalidate(...). Normally, we would just call contentToView[XY](...), which eventually calls Math.round(coordinate * mActualScale). However, for invalidates, we need to account for the slop that occurs with antialiasing. To address that, we are a little more liberal in the size of the rect that we invalidate. This liberal calculation calls floor() for the top/left, and ceil() for the bottom/right coordinates. This catches the possible extra pixels of antialiasing that we might have missed with just round(). */ // Called by JNI to invalidate the View, given rectangle coordinates in // content space private void viewInvalidate(int l, int t, int r, int b) { final float scale = mZoomManager.getScale(); final int dy = getTitleHeight(); mWebView.invalidate((int)Math.floor(l * scale), (int)Math.floor(t * scale) + dy, (int)Math.ceil(r * scale), (int)Math.ceil(b * scale) + dy); } // Called by JNI to invalidate the View after a delay, given rectangle // coordinates in content space private void viewInvalidateDelayed(long delay, int l, int t, int r, int b) { final float scale = mZoomManager.getScale(); final int dy = getTitleHeight(); mWebView.postInvalidateDelayed(delay, (int)Math.floor(l * scale), (int)Math.floor(t * scale) + dy, (int)Math.ceil(r * scale), (int)Math.ceil(b * scale) + dy); } private void invalidateContentRect(Rect r) { viewInvalidate(r.left, r.top, r.right, r.bottom); } // stop the scroll animation, and don't let a subsequent fling add // to the existing velocity private void abortAnimation() { mScroller.abortAnimation(); mLastVelocity = 0; } /* call from webcoreview.draw(), so we're still executing in the UI thread */ private void recordNewContentSize(int w, int h, boolean updateLayout) { // premature data from webkit, ignore if ((w | h) == 0) { invalidate(); return; } // don't abort a scroll animation if we didn't change anything if (mContentWidth != w || mContentHeight != h) { // record new dimensions mContentWidth = w; mContentHeight = h; // If history Picture is drawn, don't update scroll. They will be // updated when we get out of that mode. if (!mDrawHistory) { // repin our scroll, taking into account the new content size updateScrollCoordinates(pinLocX(getScrollX()), pinLocY(getScrollY())); if (!mScroller.isFinished()) { // We are in the middle of a scroll. Repin the final scroll // position. mScroller.setFinalX(pinLocX(mScroller.getFinalX())); mScroller.setFinalY(pinLocY(mScroller.getFinalY())); } } invalidate(); } contentSizeChanged(updateLayout); } // Used to avoid sending many visible rect messages. private Rect mLastVisibleRectSent = new Rect(); private Rect mLastGlobalRect = new Rect(); private Rect mVisibleRect = new Rect(); private Rect mGlobalVisibleRect = new Rect(); private Point mScrollOffset = new Point(); Rect sendOurVisibleRect() { if (mZoomManager.isPreventingWebkitUpdates()) return mLastVisibleRectSent; calcOurContentVisibleRect(mVisibleRect); // Rect.equals() checks for null input. if (!mVisibleRect.equals(mLastVisibleRectSent)) { if (!mBlockWebkitViewMessages) { mScrollOffset.set(mVisibleRect.left, mVisibleRect.top); mWebViewCore.removeMessages(EventHub.SET_SCROLL_OFFSET); mWebViewCore.sendMessage(EventHub.SET_SCROLL_OFFSET, mSendScrollEvent ? 1 : 0, mScrollOffset); } mLastVisibleRectSent.set(mVisibleRect); mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); } if (mWebView.getGlobalVisibleRect(mGlobalVisibleRect) && !mGlobalVisibleRect.equals(mLastGlobalRect)) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "sendOurVisibleRect=(" + mGlobalVisibleRect.left + "," + mGlobalVisibleRect.top + ",r=" + mGlobalVisibleRect.right + ",b=" + mGlobalVisibleRect.bottom); } // TODO: the global offset is only used by windowRect() // in ChromeClientAndroid ; other clients such as touch // and mouse events could return view + screen relative points. if (!mBlockWebkitViewMessages) { mWebViewCore.sendMessage(EventHub.SET_GLOBAL_BOUNDS, mGlobalVisibleRect); } mLastGlobalRect.set(mGlobalVisibleRect); } return mVisibleRect; } private Point mGlobalVisibleOffset = new Point(); // Sets r to be the visible rectangle of our webview in view coordinates private void calcOurVisibleRect(Rect r) { mWebView.getGlobalVisibleRect(r, mGlobalVisibleOffset); r.offset(-mGlobalVisibleOffset.x, -mGlobalVisibleOffset.y); } // Sets r to be our visible rectangle in content coordinates private void calcOurContentVisibleRect(Rect r) { calcOurVisibleRect(r); r.left = viewToContentX(r.left); // viewToContentY will remove the total height of the title bar. Add // the visible height back in to account for the fact that if the title // bar is partially visible, the part of the visible rect which is // displaying our content is displaced by that amount. r.top = viewToContentY(r.top + getVisibleTitleHeightImpl()); r.right = viewToContentX(r.right); r.bottom = viewToContentY(r.bottom); } private final Rect mTempContentVisibleRect = new Rect(); // Sets r to be our visible rectangle in content coordinates. We use this // method on the native side to compute the position of the fixed layers. // Uses floating coordinates (necessary to correctly place elements when // the scale factor is not 1) private void calcOurContentVisibleRectF(RectF r) { calcOurVisibleRect(mTempContentVisibleRect); viewToContentVisibleRect(r, mTempContentVisibleRect); } static class ViewSizeData { int mWidth; int mHeight; float mHeightWidthRatio; int mActualViewHeight; int mTextWrapWidth; int mAnchorX; int mAnchorY; float mScale; boolean mIgnoreHeight; } /** * Compute unzoomed width and height, and if they differ from the last * values we sent, send them to webkit (to be used as new viewport) * * @param force ensures that the message is sent to webkit even if the width * or height has not changed since the last message * * @return true if new values were sent */ boolean sendViewSizeZoom(boolean force) { if (mBlockWebkitViewMessages) return false; if (mZoomManager.isPreventingWebkitUpdates()) return false; int viewWidth = getViewWidth(); int newWidth = Math.round(viewWidth * mZoomManager.getInvScale()); // This height could be fixed and be different from actual visible height. int viewHeight = getViewHeightWithTitle() - getTitleHeight(); int newHeight = Math.round(viewHeight * mZoomManager.getInvScale()); // Make the ratio more accurate than (newHeight / newWidth), since the // latter both are calculated and rounded. float heightWidthRatio = (float) viewHeight / viewWidth; /* * Because the native side may have already done a layout before the * View system was able to measure us, we have to send a height of 0 to * remove excess whitespace when we grow our width. This will trigger a * layout and a change in content size. This content size change will * mean that contentSizeChanged will either call this method directly or * indirectly from onSizeChanged. */ if (newWidth > mLastWidthSent && mWrapContent) { newHeight = 0; heightWidthRatio = 0; } // Actual visible content height. int actualViewHeight = Math.round(getViewHeight() * mZoomManager.getInvScale()); // Avoid sending another message if the dimensions have not changed. if (newWidth != mLastWidthSent || newHeight != mLastHeightSent || force || actualViewHeight != mLastActualHeightSent) { ViewSizeData data = new ViewSizeData(); data.mWidth = newWidth; data.mHeight = newHeight; data.mHeightWidthRatio = heightWidthRatio; data.mActualViewHeight = actualViewHeight; data.mTextWrapWidth = Math.round(viewWidth / mZoomManager.getTextWrapScale()); data.mScale = mZoomManager.getScale(); data.mIgnoreHeight = mZoomManager.isFixedLengthAnimationInProgress() && !mHeightCanMeasure; data.mAnchorX = mZoomManager.getDocumentAnchorX(); data.mAnchorY = mZoomManager.getDocumentAnchorY(); mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, data); mLastWidthSent = newWidth; mLastHeightSent = newHeight; mLastActualHeightSent = actualViewHeight; mZoomManager.clearDocumentAnchor(); return true; } return false; } /** * Update the double-tap zoom. */ /* package */ void updateDoubleTapZoom(int doubleTapZoom) { mZoomManager.updateDoubleTapZoom(doubleTapZoom); } private int computeRealHorizontalScrollRange() { if (mDrawHistory) { return mHistoryWidth; } else { // to avoid rounding error caused unnecessary scrollbar, use floor return (int) Math.floor(mContentWidth * mZoomManager.getScale()); } } @Override public int computeHorizontalScrollRange() { int range = computeRealHorizontalScrollRange(); // Adjust reported range if overscrolled to compress the scroll bars final int scrollX = getScrollX(); final int overscrollRight = computeMaxScrollX(); if (scrollX < 0) { range -= scrollX; } else if (scrollX > overscrollRight) { range += scrollX - overscrollRight; } return range; } @Override public int computeHorizontalScrollOffset() { return Math.max(getScrollX(), 0); } private int computeRealVerticalScrollRange() { if (mDrawHistory) { return mHistoryHeight; } else { // to avoid rounding error caused unnecessary scrollbar, use floor return (int) Math.floor(mContentHeight * mZoomManager.getScale()); } } @Override public int computeVerticalScrollRange() { int range = computeRealVerticalScrollRange(); // Adjust reported range if overscrolled to compress the scroll bars final int scrollY = getScrollY(); final int overscrollBottom = computeMaxScrollY(); if (scrollY < 0) { range -= scrollY; } else if (scrollY > overscrollBottom) { range += scrollY - overscrollBottom; } return range; } @Override public int computeVerticalScrollOffset() { return Math.max(getScrollY() - getTitleHeight(), 0); } @Override public int computeVerticalScrollExtent() { return getViewHeight(); } @Override public void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, int l, int t, int r, int b) { if (getScrollY() < 0) { t -= getScrollY(); } scrollBar.setBounds(l, t + getVisibleTitleHeightImpl(), r, b); scrollBar.draw(canvas); } @Override public void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { // Special-case layer scrolling so that we do not trigger normal scroll // updating. if (mTouchMode == TOUCH_DRAG_TEXT_MODE) { scrollEditText(scrollX, scrollY); return; } if (mTouchMode == TOUCH_DRAG_LAYER_MODE) { scrollLayerTo(scrollX, scrollY); animateHandles(); return; } mInOverScrollMode = false; int maxX = computeMaxScrollX(); int maxY = computeMaxScrollY(); if (maxX == 0) { // do not over scroll x if the page just fits the screen scrollX = pinLocX(scrollX); } else if (scrollX < 0 || scrollX > maxX) { mInOverScrollMode = true; } if (scrollY < 0 || scrollY > maxY) { mInOverScrollMode = true; } int oldX = getScrollX(); int oldY = getScrollY(); mWebViewPrivate.super_scrollTo(scrollX, scrollY); animateHandles(); if (mOverScrollGlow != null) { mOverScrollGlow.pullGlow(getScrollX(), getScrollY(), oldX, oldY, maxX, maxY); } } /** * See {@link WebView#getUrl()} */ @Override public String getUrl() { WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getUrl() : null; } /** * See {@link WebView#getOriginalUrl()} */ @Override public String getOriginalUrl() { WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getOriginalUrl() : null; } /** * See {@link WebView#getTitle()} */ @Override public String getTitle() { WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getTitle() : null; } /** * See {@link WebView#getFavicon()} */ @Override public Bitmap getFavicon() { WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getFavicon() : null; } /** * See {@link WebView#getTouchIconUrl()} */ @Override public String getTouchIconUrl() { WebHistoryItemClassic h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getTouchIconUrl() : null; } /** * See {@link WebView#getProgress()} */ @Override public int getProgress() { return mCallbackProxy.getProgress(); } /** * See {@link WebView#getContentHeight()} */ @Override public int getContentHeight() { return mContentHeight; } /** * See {@link WebView#getContentWidth()} */ @Override public int getContentWidth() { return mContentWidth; } public int getPageBackgroundColor() { if (mNativeClass == 0) return Color.WHITE; return nativeGetBackgroundColor(mNativeClass); } /** * See {@link WebView#pauseTimers()} */ @Override public void pauseTimers() { mWebViewCore.sendMessage(EventHub.PAUSE_TIMERS); } /** * See {@link WebView#resumeTimers()} */ @Override public void resumeTimers() { mWebViewCore.sendMessage(EventHub.RESUME_TIMERS); } /** * See {@link WebView#onPause()} */ @Override public void onPause() { if (!mIsPaused) { mIsPaused = true; mWebViewCore.sendMessage(EventHub.ON_PAUSE); // We want to pause the current playing video when switching out // from the current WebView/tab. if (mHTML5VideoViewProxy != null) { mHTML5VideoViewProxy.pauseAndDispatch(); } if (mNativeClass != 0) { nativeSetPauseDrawing(mNativeClass, true); } cancelDialogs(); WebCoreThreadWatchdog.pause(); } } @Override public void onWindowVisibilityChanged(int visibility) { updateDrawingState(); } void updateDrawingState() { if (mNativeClass == 0 || mIsPaused) return; if (mWebView.getWindowVisibility() != View.VISIBLE) { nativeSetPauseDrawing(mNativeClass, true); } else if (mWebView.getVisibility() != View.VISIBLE) { nativeSetPauseDrawing(mNativeClass, true); } else { nativeSetPauseDrawing(mNativeClass, false); } } /** * See {@link WebView#onResume()} */ @Override public void onResume() { if (mIsPaused) { mIsPaused = false; mWebViewCore.sendMessage(EventHub.ON_RESUME); if (mNativeClass != 0) { nativeSetPauseDrawing(mNativeClass, false); } } // We get a call to onResume for new WebViews (i.e. mIsPaused will be false). We need // to ensure that the Watchdog thread is running for the new WebView, so call // it outside the if block above. WebCoreThreadWatchdog.resume(); } /** * See {@link WebView#isPaused()} */ @Override public boolean isPaused() { return mIsPaused; } /** * See {@link WebView#freeMemory()} */ @Override public void freeMemory() { mWebViewCore.sendMessage(EventHub.FREE_MEMORY); } /** * See {@link WebView#clearCache(boolean)} */ @Override public void clearCache(boolean includeDiskFiles) { // Note: this really needs to be a static method as it clears cache for all // WebView. But we need mWebViewCore to send message to WebCore thread, so // we can't make this static. mWebViewCore.sendMessage(EventHub.CLEAR_CACHE, includeDiskFiles ? 1 : 0, 0); } /** * See {@link WebView#clearFormData()} */ @Override public void clearFormData() { if (mAutoCompletePopup != null) { mAutoCompletePopup.clearAdapter(); } } /** * See {@link WebView#clearHistory()} */ @Override public void clearHistory() { mCallbackProxy.getBackForwardList().setClearPending(); mWebViewCore.sendMessage(EventHub.CLEAR_HISTORY); } /** * See {@link WebView#clearSslPreferences()} */ @Override public void clearSslPreferences() { mWebViewCore.sendMessage(EventHub.CLEAR_SSL_PREF_TABLE); } /** * See {@link WebView#copyBackForwardList()} */ @Override public WebBackForwardListClassic copyBackForwardList() { return mCallbackProxy.getBackForwardList().clone(); } /** * See {@link WebView#setFindListener(WebView.FindListener)}. * @hide */ @Override public void setFindListener(WebView.FindListener listener) { mFindListener = listener; } /** * See {@link WebView#findNext(boolean)} */ @Override public void findNext(boolean forward) { if (0 == mNativeClass) return; // client isn't initialized if (mFindRequest != null) { mWebViewCore.sendMessage(EventHub.FIND_NEXT, forward ? 1 : 0, mFindRequest); } } /** * See {@link WebView#findAll(String)} */ @Override public int findAll(String find) { return findAllBody(find, false); } @Override public void findAllAsync(String find) { findAllBody(find, true); } private int findAllBody(String find, boolean isAsync) { if (0 == mNativeClass) return 0; // client isn't initialized mFindRequest = null; if (find == null) return 0; mWebViewCore.removeMessages(EventHub.FIND_ALL); mFindRequest = new WebViewCore.FindAllRequest(find); if (isAsync) { mWebViewCore.sendMessage(EventHub.FIND_ALL, mFindRequest); return 0; // no need to wait for response } synchronized(mFindRequest) { try { mWebViewCore.sendMessageAtFrontOfQueue(EventHub.FIND_ALL, mFindRequest); while (mFindRequest.mMatchCount == -1) { mFindRequest.wait(); } } catch (InterruptedException e) { return 0; } return mFindRequest.mMatchCount; } } /** * Start an ActionMode for finding text in this WebView. Only works if this * WebView is attached to the view system. * @param text If non-null, will be the initial text to search for. * Otherwise, the last String searched for in this WebView will * be used to start. * @param showIme If true, show the IME, assuming the user will begin typing. * If false and text is non-null, perform a find all. * @return boolean True if the find dialog is shown, false otherwise. */ @Override public boolean showFindDialog(String text, boolean showIme) { FindActionModeCallback callback = new FindActionModeCallback(mContext); if (mWebView.getParent() == null || mWebView.startActionMode(callback) == null) { // Could not start the action mode, so end Find on page return false; } mCachedOverlappingActionModeHeight = -1; mFindCallback = callback; setFindIsUp(true); mFindCallback.setWebView(this); if (showIme) { mFindCallback.showSoftInput(); } else if (text != null) { mFindCallback.setText(text); mFindCallback.findAll(); return true; } if (text == null) { text = mFindRequest == null ? null : mFindRequest.mSearchText; } if (text != null) { mFindCallback.setText(text); mFindCallback.findAll(); } return true; } /** * Keep track of the find callback so that we can remove its titlebar if * necessary. */ private FindActionModeCallback mFindCallback; /** * Toggle whether the find dialog is showing, for both native and Java. */ private void setFindIsUp(boolean isUp) { mFindIsUp = isUp; } // Used to know whether the find dialog is open. Affects whether // or not we draw the highlights for matches. private boolean mFindIsUp; // Keep track of the last find request sent. private WebViewCore.FindAllRequest mFindRequest = null; /** * Return the first substring consisting of the address of a physical * location. Currently, only addresses in the United States are detected, * and consist of: * - a house number * - a street name * - a street type (Road, Circle, etc), either spelled out or abbreviated * - a city name * - a state or territory, either spelled out or two-letter abbr. * - an optional 5 digit or 9 digit zip code. * * All names must be correctly capitalized, and the zip code, if present, * must be valid for the state. The street type must be a standard USPS * spelling or abbreviation. The state or territory must also be spelled * or abbreviated using USPS standards. The house number may not exceed * five digits. * @param addr The string to search for addresses. * * @return the address, or if no address is found, return null. */ public static String findAddress(String addr) { return findAddress(addr, false); } /** * Return the first substring consisting of the address of a physical * location. Currently, only addresses in the United States are detected, * and consist of: * - a house number * - a street name * - a street type (Road, Circle, etc), either spelled out or abbreviated * - a city name * - a state or territory, either spelled out or two-letter abbr. * - an optional 5 digit or 9 digit zip code. * * Names are optionally capitalized, and the zip code, if present, * must be valid for the state. The street type must be a standard USPS * spelling or abbreviation. The state or territory must also be spelled * or abbreviated using USPS standards. The house number may not exceed * five digits. * @param addr The string to search for addresses. * @param caseInsensitive addr Set to true to make search ignore case. * * @return the address, or if no address is found, return null. */ public static String findAddress(String addr, boolean caseInsensitive) { return WebViewCore.nativeFindAddress(addr, caseInsensitive); } /** * See {@link WebView#clearMatches()} */ @Override public void clearMatches() { if (mNativeClass == 0) return; mWebViewCore.removeMessages(EventHub.FIND_ALL); mWebViewCore.sendMessage(EventHub.FIND_ALL, null); } /** * Called when the find ActionMode ends. */ void notifyFindDialogDismissed() { mFindCallback = null; mCachedOverlappingActionModeHeight = -1; if (mWebViewCore == null) { return; } clearMatches(); setFindIsUp(false); // Now that the dialog has been removed, ensure that we scroll to a // location that is not beyond the end of the page. pinScrollTo(getScrollX(), getScrollY(), false, 0); invalidate(); } /** * See {@link WebView#documentHasImages(Message)} */ @Override public void documentHasImages(Message response) { if (response == null) { return; } mWebViewCore.sendMessage(EventHub.DOC_HAS_IMAGES, response); } /** * Request the scroller to abort any ongoing animation */ public void stopScroll() { mScroller.forceFinished(true); mLastVelocity = 0; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { int oldX = getScrollX(); int oldY = getScrollY(); int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); invalidate(); // So we draw again if (!mScroller.isFinished()) { int rangeX = computeMaxScrollX(); int rangeY = computeMaxScrollY(); int overflingDistance = mOverflingDistance; // Use the layer's scroll data if needed. if (mTouchMode == TOUCH_DRAG_LAYER_MODE) { oldX = mScrollingLayerRect.left; oldY = mScrollingLayerRect.top; rangeX = mScrollingLayerRect.right; rangeY = mScrollingLayerRect.bottom; // No overscrolling for layers. overflingDistance = 0; } else if (mTouchMode == TOUCH_DRAG_TEXT_MODE) { oldX = getTextScrollX(); oldY = getTextScrollY(); rangeX = getMaxTextScrollX(); rangeY = getMaxTextScrollY(); overflingDistance = 0; } mWebViewPrivate.overScrollBy(x - oldX, y - oldY, oldX, oldY, rangeX, rangeY, overflingDistance, overflingDistance, false); if (mOverScrollGlow != null) { mOverScrollGlow.absorbGlow(x, y, oldX, oldY, rangeX, rangeY); } } else { if (mTouchMode == TOUCH_DRAG_LAYER_MODE) { // Update the layer position instead of WebView. scrollLayerTo(x, y); } else if (mTouchMode == TOUCH_DRAG_TEXT_MODE) { scrollEditText(x, y); } else { setScrollXRaw(x); setScrollYRaw(y); } abortAnimation(); nativeSetIsScrolling(false); if (!mBlockWebkitViewMessages) { WebViewCore.resumePriority(); if (!mSelectingText) { WebViewCore.resumeUpdatePicture(mWebViewCore); } } if (oldX != getScrollX() || oldY != getScrollY()) { sendOurVisibleRect(); } } } else { mWebViewPrivate.super_computeScroll(); } } private void scrollLayerTo(int x, int y) { int dx = mScrollingLayerRect.left - x; int dy = mScrollingLayerRect.top - y; if ((dx == 0 && dy == 0) || mNativeClass == 0) { return; } if (mSelectingText) { if (mSelectCursorBaseLayerId == mCurrentScrollingLayerId) { mSelectCursorBase.offset(dx, dy); mSelectCursorBaseTextQuad.offset(dx, dy); } if (mSelectCursorExtentLayerId == mCurrentScrollingLayerId) { mSelectCursorExtent.offset(dx, dy); mSelectCursorExtentTextQuad.offset(dx, dy); } } if (mAutoCompletePopup != null && mCurrentScrollingLayerId == mEditTextLayerId) { mEditTextContentBounds.offset(dx, dy); mAutoCompletePopup.resetRect(); } nativeScrollLayer(mNativeClass, mCurrentScrollingLayerId, x, y); mScrollingLayerRect.left = x; mScrollingLayerRect.top = y; mWebViewCore.sendMessage(WebViewCore.EventHub.SCROLL_LAYER, mCurrentScrollingLayerId, mScrollingLayerRect); mWebViewPrivate.onScrollChanged(getScrollX(), getScrollY(), getScrollX(), getScrollY()); invalidate(); } private static int computeDuration(int dx, int dy) { int distance = Math.max(Math.abs(dx), Math.abs(dy)); int duration = distance * 1000 / STD_SPEED; return Math.min(duration, MAX_DURATION); } // helper to pin the scrollBy parameters (already in view coordinates) // returns true if the scroll was changed private boolean pinScrollBy(int dx, int dy, boolean animate, int animationDuration) { return pinScrollTo(getScrollX() + dx, getScrollY() + dy, animate, animationDuration); } // helper to pin the scrollTo parameters (already in view coordinates) // returns true if the scroll was changed private boolean pinScrollTo(int x, int y, boolean animate, int animationDuration) { abortAnimation(); x = pinLocX(x); y = pinLocY(y); int dx = x - getScrollX(); int dy = y - getScrollY(); if ((dx | dy) == 0) { return false; } if (animate) { // Log.d(LOGTAG, "startScroll: " + dx + " " + dy); mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, animationDuration > 0 ? animationDuration : computeDuration(dx, dy)); invalidate(); } else { mWebView.scrollTo(x, y); } return true; } // Scale from content to view coordinates, and pin. // Also called by jni webview.cpp private boolean setContentScrollBy(int cx, int cy, boolean animate) { if (mDrawHistory) { // disallow WebView to change the scroll position as History Picture // is used in the view system. // TODO: as we switchOutDrawHistory when trackball or navigation // keys are hit, this should be safe. Right? return false; } cx = contentToViewDimension(cx); cy = contentToViewDimension(cy); if (mHeightCanMeasure) { // move our visible rect according to scroll request if (cy != 0) { Rect tempRect = new Rect(); calcOurVisibleRect(tempRect); tempRect.offset(cx, cy); mWebView.requestRectangleOnScreen(tempRect); } // FIXME: We scroll horizontally no matter what because currently // ScrollView and ListView will not scroll horizontally. // FIXME: Why do we only scroll horizontally if there is no // vertical scroll? // Log.d(LOGTAG, "setContentScrollBy cy=" + cy); return cy == 0 && cx != 0 && pinScrollBy(cx, 0, animate, 0); } else { return pinScrollBy(cx, cy, animate, 0); } } /** * Called by CallbackProxy when the page starts loading. * @param url The URL of the page which has started loading. */ /* package */ void onPageStarted(String url) { // every time we start a new page, we want to reset the // WebView certificate: if the new site is secure, we // will reload it and get a new certificate set; // if the new site is not secure, the certificate must be // null, and that will be the case mWebView.setCertificate(null); if (isAccessibilityInjectionEnabled()) { getAccessibilityInjector().onPageStarted(url); } // Don't start out editing. mIsEditingText = false; } /** * Called by CallbackProxy when the page finishes loading. * @param url The URL of the page which has finished loading. */ /* package */ void onPageFinished(String url) { mZoomManager.onPageFinished(url); if (isAccessibilityInjectionEnabled()) { getAccessibilityInjector().onPageFinished(url); } } // scale from content to view coordinates, and pin private void contentScrollTo(int cx, int cy, boolean animate) { if (mDrawHistory) { // disallow WebView to change the scroll position as History Picture // is used in the view system. return; } int vx = contentToViewX(cx); int vy = contentToViewY(cy); pinScrollTo(vx, vy, animate, 0); } /** * These are from webkit, and are in content coordinate system (unzoomed) */ private void contentSizeChanged(boolean updateLayout) { // suppress 0,0 since we usually see real dimensions soon after // this avoids drawing the prev content in a funny place. If we find a // way to consolidate these notifications, this check may become // obsolete if ((mContentWidth | mContentHeight) == 0) { return; } if (mHeightCanMeasure) { if (mWebView.getMeasuredHeight() != contentToViewDimension(mContentHeight) || updateLayout) { mWebView.requestLayout(); } } else if (mWidthCanMeasure) { if (mWebView.getMeasuredWidth() != contentToViewDimension(mContentWidth) || updateLayout) { mWebView.requestLayout(); } } else { // If we don't request a layout, try to send our view size to the // native side to ensure that WebCore has the correct dimensions. sendViewSizeZoom(false); } } /** * See {@link WebView#setWebViewClient(WebViewClient)} */ @Override public void setWebViewClient(WebViewClient client) { mCallbackProxy.setWebViewClient(client); } /** * Gets the WebViewClient * @return the current WebViewClient instance. * * This is an implementation detail. */ public WebViewClient getWebViewClient() { return mCallbackProxy.getWebViewClient(); } /** * See {@link WebView#setDownloadListener(DownloadListener)} */ @Override public void setDownloadListener(DownloadListener listener) { mCallbackProxy.setDownloadListener(listener); } /** * See {@link WebView#setWebChromeClient(WebChromeClient)} */ @Override public void setWebChromeClient(WebChromeClient client) { mCallbackProxy.setWebChromeClient(client); } /** * Gets the chrome handler. * @return the current WebChromeClient instance. * * This is an implementation detail. */ public WebChromeClient getWebChromeClient() { return mCallbackProxy.getWebChromeClient(); } /** * Set the back/forward list client. This is an implementation of * WebBackForwardListClient for handling new items and changes in the * history index. * @param client An implementation of WebBackForwardListClient. */ public void setWebBackForwardListClient(WebBackForwardListClient client) { mCallbackProxy.setWebBackForwardListClient(client); } /** * Gets the WebBackForwardListClient. */ public WebBackForwardListClient getWebBackForwardListClient() { return mCallbackProxy.getWebBackForwardListClient(); } /** * See {@link WebView#setPictureListener(PictureListener)} */ @Override @Deprecated public void setPictureListener(PictureListener listener) { mPictureListener = listener; } /* FIXME: Debug only! Remove for SDK! */ public void externalRepresentation(Message callback) { mWebViewCore.sendMessage(EventHub.REQUEST_EXT_REPRESENTATION, callback); } /* FIXME: Debug only! Remove for SDK! */ public void documentAsText(Message callback) { mWebViewCore.sendMessage(EventHub.REQUEST_DOC_AS_TEXT, callback); } /** * See {@link WebView#addJavascriptInterface(Object, String)} */ @Override public void addJavascriptInterface(Object object, String name) { if (object == null) { return; } WebViewCore.JSInterfaceData arg = new WebViewCore.JSInterfaceData(); arg.mObject = object; arg.mInterfaceName = name; // starting with JELLY_BEAN_MR1, annotations are mandatory for enabling access to // methods that are accessible from JS. if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR1) { arg.mRequireAnnotation = true; } else { arg.mRequireAnnotation = false; } mWebViewCore.sendMessage(EventHub.ADD_JS_INTERFACE, arg); } /** * See {@link WebView#removeJavascriptInterface(String)} */ @Override public void removeJavascriptInterface(String interfaceName) { if (mWebViewCore != null) { WebViewCore.JSInterfaceData arg = new WebViewCore.JSInterfaceData(); arg.mInterfaceName = interfaceName; mWebViewCore.sendMessage(EventHub.REMOVE_JS_INTERFACE, arg); } } /** * See {@link WebView#getSettings()} * Note this returns WebSettingsClassic, a sub-class of WebSettings, which can be used * to access extension APIs. */ @Override public WebSettingsClassic getSettings() { return (mWebViewCore != null) ? mWebViewCore.getSettings() : null; } /** * See {@link WebView#getPluginList()} */ @Deprecated public static synchronized PluginList getPluginList() { return new PluginList(); } /** * See {@link WebView#refreshPlugins(boolean)} */ @Deprecated public void refreshPlugins(boolean reloadOpenPages) { } //------------------------------------------------------------------------- // Override View methods //------------------------------------------------------------------------- @Override protected void finalize() throws Throwable { try { destroy(); } finally { super.finalize(); } } private void drawContent(Canvas canvas) { if (mDrawHistory) { canvas.scale(mZoomManager.getScale(), mZoomManager.getScale()); canvas.drawPicture(mHistoryPicture); return; } if (mNativeClass == 0) return; boolean animateZoom = mZoomManager.isFixedLengthAnimationInProgress(); boolean animateScroll = ((!mScroller.isFinished() || mVelocityTracker != null) && (mTouchMode != TOUCH_DRAG_MODE || mHeldMotionless != MOTIONLESS_TRUE)); if (mTouchMode == TOUCH_DRAG_MODE) { if (mHeldMotionless == MOTIONLESS_PENDING) { mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); mHeldMotionless = MOTIONLESS_FALSE; } if (mHeldMotionless == MOTIONLESS_FALSE) { mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(DRAG_HELD_MOTIONLESS), MOTIONLESS_TIME); mHeldMotionless = MOTIONLESS_PENDING; } } int saveCount = canvas.save(); if (animateZoom) { mZoomManager.animateZoom(canvas); } else if (!canvas.isHardwareAccelerated()) { canvas.scale(mZoomManager.getScale(), mZoomManager.getScale()); } boolean UIAnimationsRunning = false; // Currently for each draw we compute the animation values; // We may in the future decide to do that independently. if (mNativeClass != 0 && !canvas.isHardwareAccelerated() && nativeEvaluateLayersAnimations(mNativeClass)) { UIAnimationsRunning = true; // If we have unfinished (or unstarted) animations, // we ask for a repaint. We only need to do this in software // rendering (with hardware rendering we already have a different // method of requesting a repaint) mWebViewCore.sendMessage(EventHub.NOTIFY_ANIMATION_STARTED); invalidate(); } // decide which adornments to draw int extras = DRAW_EXTRAS_NONE; if (!mFindIsUp && mShowTextSelectionExtra) { extras = DRAW_EXTRAS_SELECTION; } calcOurContentVisibleRectF(mVisibleContentRect); if (canvas.isHardwareAccelerated()) { Rect invScreenRect = mIsWebViewVisible ? mInvScreenRect : null; Rect screenRect = mIsWebViewVisible ? mScreenRect : null; int functor = nativeCreateDrawGLFunction(mNativeClass, invScreenRect, screenRect, mVisibleContentRect, getScale(), extras); ((HardwareCanvas) canvas).callDrawGLFunction(functor); if (mHardwareAccelSkia != getSettings().getHardwareAccelSkiaEnabled()) { mHardwareAccelSkia = getSettings().getHardwareAccelSkiaEnabled(); nativeUseHardwareAccelSkia(mHardwareAccelSkia); } } else { DrawFilter df = null; if (mZoomManager.isZoomAnimating() || UIAnimationsRunning) { df = mZoomFilter; } else if (animateScroll) { df = mScrollFilter; } canvas.setDrawFilter(df); nativeDraw(canvas, mVisibleContentRect, mBackgroundColor, extras); canvas.setDrawFilter(null); } canvas.restoreToCount(saveCount); drawTextSelectionHandles(canvas); if (extras == DRAW_EXTRAS_CURSOR_RING) { if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) { mTouchMode = TOUCH_SHORTPRESS_MODE; } } } /** * Draw the background when beyond bounds * @param canvas Canvas to draw into */ private void drawOverScrollBackground(Canvas canvas) { if (mOverScrollBackground == null) { mOverScrollBackground = new Paint(); Bitmap bm = BitmapFactory.decodeResource( mContext.getResources(), com.android.internal.R.drawable.status_bar_background); mOverScrollBackground.setShader(new BitmapShader(bm, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); mOverScrollBorder = new Paint(); mOverScrollBorder.setStyle(Paint.Style.STROKE); mOverScrollBorder.setStrokeWidth(0); mOverScrollBorder.setColor(0xffbbbbbb); } int top = 0; int right = computeRealHorizontalScrollRange(); int bottom = top + computeRealVerticalScrollRange(); // first draw the background and anchor to the top of the view canvas.save(); canvas.translate(getScrollX(), getScrollY()); canvas.clipRect(-getScrollX(), top - getScrollY(), right - getScrollX(), bottom - getScrollY(), Region.Op.DIFFERENCE); canvas.drawPaint(mOverScrollBackground); canvas.restore(); // then draw the border canvas.drawRect(-1, top - 1, right, bottom, mOverScrollBorder); // next clip the region for the content canvas.clipRect(0, top, right, bottom); } @Override public void onDraw(Canvas canvas) { if (inFullScreenMode()) { return; // no need to draw anything if we aren't visible. } // if mNativeClass is 0, the WebView is either destroyed or not // initialized. In either case, just draw the background color and return if (mNativeClass == 0) { canvas.drawColor(mBackgroundColor); return; } // if both mContentWidth and mContentHeight are 0, it means there is no // valid Picture passed to WebView yet. This can happen when WebView // just starts. Draw the background and return. if ((mContentWidth | mContentHeight) == 0 && mHistoryPicture == null) { canvas.drawColor(mBackgroundColor); return; } if (canvas.isHardwareAccelerated()) { mZoomManager.setHardwareAccelerated(); } else { mWebViewCore.resumeWebKitDraw(); } int saveCount = canvas.save(); if (mInOverScrollMode && !getSettings() .getUseWebViewBackgroundForOverscrollBackground()) { drawOverScrollBackground(canvas); } canvas.translate(0, getTitleHeight()); drawContent(canvas); canvas.restoreToCount(saveCount); if (AUTO_REDRAW_HACK && mAutoRedraw) { invalidate(); } mWebViewCore.signalRepaintDone(); if (mOverScrollGlow != null && mOverScrollGlow.drawEdgeGlows(canvas)) { invalidate(); } if (mFocusTransition != null) { mFocusTransition.draw(canvas); } else if (shouldDrawHighlightRect()) { RegionIterator iter = new RegionIterator(mTouchHighlightRegion); Rect r = new Rect(); while (iter.next(r)) { canvas.drawRect(r, mTouchHightlightPaint); } } if (DEBUG_TOUCH_HIGHLIGHT) { if (getSettings().getNavDump()) { if ((mTouchHighlightX | mTouchHighlightY) != 0) { if (mTouchCrossHairColor == null) { mTouchCrossHairColor = new Paint(); mTouchCrossHairColor.setColor(Color.RED); } canvas.drawLine(mTouchHighlightX - mNavSlop, mTouchHighlightY - mNavSlop, mTouchHighlightX + mNavSlop + 1, mTouchHighlightY + mNavSlop + 1, mTouchCrossHairColor); canvas.drawLine(mTouchHighlightX + mNavSlop + 1, mTouchHighlightY - mNavSlop, mTouchHighlightX - mNavSlop, mTouchHighlightY + mNavSlop + 1, mTouchCrossHairColor); } } } } private void removeTouchHighlight() { setTouchHighlightRects(null); } @Override public void setLayoutParams(ViewGroup.LayoutParams params) { if (params.height == AbsoluteLayout.LayoutParams.WRAP_CONTENT) { mWrapContent = true; } mWebViewPrivate.super_setLayoutParams(params); } @Override public boolean performLongClick() { // performLongClick() is the result of a delayed message. If we switch // to windows overview, the WebView will be temporarily removed from the // view system. In that case, do nothing. if (mWebView.getParent() == null) return false; // A multi-finger gesture can look like a long press; make sure we don't take // long press actions if we're scaling. final ScaleGestureDetector detector = mZoomManager.getScaleGestureDetector(); if (detector != null && detector.isInProgress()) { return false; } if (mSelectingText) return false; // long click does nothing on selection /* if long click brings up a context menu, the super function * returns true and we're done. Otherwise, nothing happened when * the user clicked. */ if (mWebViewPrivate.super_performLongClick()) { return true; } /* In the case where the application hasn't already handled the long * click action, look for a word under the click. If one is found, * animate the text selection into view. * FIXME: no animation code yet */ final boolean isSelecting = selectText(); if (isSelecting) { mWebView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } else if (focusCandidateIsEditableText()) { mSelectCallback = new SelectActionModeCallback(); mSelectCallback.setWebView(this); mSelectCallback.setTextSelected(false); mWebView.startActionMode(mSelectCallback); } return isSelecting; } /** * Select the word at the last click point. * * This is an implementation detail. */ public boolean selectText() { int x = viewToContentX(mLastTouchX + getScrollX()); int y = viewToContentY(mLastTouchY + getScrollY()); return selectText(x, y); } /** * Select the word at the indicated content coordinates. */ boolean selectText(int x, int y) { if (mWebViewCore == null) { return false; } mWebViewCore.sendMessage(EventHub.SELECT_WORD_AT, x, y); return true; } private int mOrientation = Configuration.ORIENTATION_UNDEFINED; @Override public void onConfigurationChanged(Configuration newConfig) { mCachedOverlappingActionModeHeight = -1; if (mSelectingText && mOrientation != newConfig.orientation) { selectionDone(); } mOrientation = newConfig.orientation; if (mWebViewCore != null && !mBlockWebkitViewMessages) { mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT); } } /** * Keep track of the Callback so we can end its ActionMode or remove its * titlebar. */ private SelectActionModeCallback mSelectCallback; void setBaseLayer(int layer, boolean showVisualIndicator, boolean isPictureAfterFirstLayout) { if (mNativeClass == 0) return; boolean queueFull; final int scrollingLayer = (mTouchMode == TOUCH_DRAG_LAYER_MODE) ? mCurrentScrollingLayerId : 0; queueFull = nativeSetBaseLayer(mNativeClass, layer, showVisualIndicator, isPictureAfterFirstLayout, scrollingLayer); if (queueFull) { mWebViewCore.pauseWebKitDraw(); } else { mWebViewCore.resumeWebKitDraw(); } if (mHTML5VideoViewProxy != null) { mHTML5VideoViewProxy.setBaseLayer(layer); } } int getBaseLayer() { if (mNativeClass == 0) { return 0; } return nativeGetBaseLayer(mNativeClass); } private void onZoomAnimationStart() { } private void onZoomAnimationEnd() { mPrivateHandler.sendEmptyMessage(RELOCATE_AUTO_COMPLETE_POPUP); } void onFixedLengthZoomAnimationStart() { WebViewCore.pauseUpdatePicture(getWebViewCore()); onZoomAnimationStart(); } void onFixedLengthZoomAnimationEnd() { if (!mBlockWebkitViewMessages && !mSelectingText) { WebViewCore.resumeUpdatePicture(mWebViewCore); } onZoomAnimationEnd(); } private static final int ZOOM_BITS = Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG | Paint.SUBPIXEL_TEXT_FLAG; private static final int SCROLL_BITS = Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG; private final DrawFilter mZoomFilter = new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG); // If we need to trade better quality for speed, set mScrollFilter to null private final DrawFilter mScrollFilter = new PaintFlagsDrawFilter(SCROLL_BITS, 0); private class SelectionHandleAlpha { private int mAlpha = 0; private int mTargetAlpha = 0; public void setAlpha(int alpha) { mAlpha = alpha; // TODO: Use partial invalidate invalidate(); } public int getAlpha() { return mAlpha; } public void setTargetAlpha(int alpha) { mTargetAlpha = alpha; } public int getTargetAlpha() { return mTargetAlpha; } } private void startSelectingText() { mSelectingText = true; mShowTextSelectionExtra = true; animateHandles(); } private void animateHandle(boolean canShow, ObjectAnimator animator, Point selectionPoint, int selectionLayerId, SelectionHandleAlpha alpha) { boolean isVisible = canShow && mSelectingText && ((mSelectionStarted && mSelectDraggingCursor == selectionPoint) || isHandleVisible(selectionPoint, selectionLayerId)); int targetValue = isVisible ? 255 : 0; if (targetValue != alpha.getTargetAlpha()) { alpha.setTargetAlpha(targetValue); animator.setIntValues(targetValue); animator.setDuration(SELECTION_HANDLE_ANIMATION_MS); animator.start(); } } private void animateHandles() { boolean canShowBase = mSelectingText; boolean canShowExtent = mSelectingText && !mIsCaretSelection; animateHandle(canShowBase, mBaseHandleAlphaAnimator, mSelectCursorBase, mSelectCursorBaseLayerId, mBaseAlpha); animateHandle(canShowExtent, mExtentHandleAlphaAnimator, mSelectCursorExtent, mSelectCursorExtentLayerId, mExtentAlpha); } private void endSelectingText() { mSelectingText = false; mShowTextSelectionExtra = false; animateHandles(); } private void ensureSelectionHandles() { if (mSelectHandleCenter == null) { mSelectHandleCenter = mContext.getResources().getDrawable( com.android.internal.R.drawable.text_select_handle_middle).mutate(); mSelectHandleLeft = mContext.getResources().getDrawable( com.android.internal.R.drawable.text_select_handle_left).mutate(); mSelectHandleRight = mContext.getResources().getDrawable( com.android.internal.R.drawable.text_select_handle_right).mutate(); // All handles have the same height, so we can save effort with // this assumption. mSelectOffset = new Point(0, -mSelectHandleLeft.getIntrinsicHeight()); } } private void drawHandle(Point point, int handleId, Rect bounds, int alpha, Canvas canvas) { int offset; int width; int height; Drawable drawable; boolean isLeft = nativeIsHandleLeft(mNativeClass, handleId); if (isLeft) { drawable = mSelectHandleLeft; width = mSelectHandleLeft.getIntrinsicWidth(); height = mSelectHandleLeft.getIntrinsicHeight(); // Magic formula copied from TextView offset = (width * 3) / 4; } else { drawable = mSelectHandleRight; width = mSelectHandleRight.getIntrinsicWidth(); height = mSelectHandleRight.getIntrinsicHeight(); // Magic formula copied from TextView offset = width / 4; } int x = contentToViewDimension(point.x); int y = contentToViewDimension(point.y); bounds.set(x - offset, y, x - offset + width, y + height); drawable.setBounds(bounds); drawable.setAlpha(alpha); drawable.draw(canvas); } private void drawTextSelectionHandles(Canvas canvas) { if (mBaseAlpha.getAlpha() == 0 && mExtentAlpha.getAlpha() == 0) { return; } ensureSelectionHandles(); if (mIsCaretSelection) { // Caret handle is centered int x = contentToViewDimension(mSelectCursorBase.x) - (mSelectHandleCenter.getIntrinsicWidth() / 2); int y = contentToViewDimension(mSelectCursorBase.y); mSelectHandleBaseBounds.set(x, y, x + mSelectHandleCenter.getIntrinsicWidth(), y + mSelectHandleCenter.getIntrinsicHeight()); mSelectHandleCenter.setBounds(mSelectHandleBaseBounds); mSelectHandleCenter.setAlpha(mBaseAlpha.getAlpha()); mSelectHandleCenter.draw(canvas); } else { drawHandle(mSelectCursorBase, HANDLE_ID_BASE, mSelectHandleBaseBounds, mBaseAlpha.getAlpha(), canvas); drawHandle(mSelectCursorExtent, HANDLE_ID_EXTENT, mSelectHandleExtentBounds, mExtentAlpha.getAlpha(), canvas); } } private boolean isHandleVisible(Point selectionPoint, int layerId) { boolean isVisible = true; if (mIsEditingText) { isVisible = mEditTextContentBounds.contains(selectionPoint.x, selectionPoint.y); } if (isVisible) { isVisible = nativeIsPointVisible(mNativeClass, layerId, selectionPoint.x, selectionPoint.y); } return isVisible; } /** * Takes an int[4] array as an output param with the values being * startX, startY, endX, endY */ private void getSelectionHandles(int[] handles) { handles[0] = mSelectCursorBase.x; handles[1] = mSelectCursorBase.y; handles[2] = mSelectCursorExtent.x; handles[3] = mSelectCursorExtent.y; } // draw history private boolean mDrawHistory = false; private Picture mHistoryPicture = null; private int mHistoryWidth = 0; private int mHistoryHeight = 0; // Only check the flag, can be called from WebCore thread boolean drawHistory() { return mDrawHistory; } int getHistoryPictureWidth() { return (mHistoryPicture != null) ? mHistoryPicture.getWidth() : 0; } // Should only be called in UI thread void switchOutDrawHistory() { if (null == mWebViewCore) return; // CallbackProxy may trigger this if (mDrawHistory && (getProgress() == 100 || nativeHasContent())) { mDrawHistory = false; mHistoryPicture = null; invalidate(); int oldScrollX = getScrollX(); int oldScrollY = getScrollY(); setScrollXRaw(pinLocX(getScrollX())); setScrollYRaw(pinLocY(getScrollY())); if (oldScrollX != getScrollX() || oldScrollY != getScrollY()) { mWebViewPrivate.onScrollChanged(getScrollX(), getScrollY(), oldScrollX, oldScrollY); } else { sendOurVisibleRect(); } } } /** * Delete text from start to end in the focused textfield. If there is no * focus, or if start == end, silently fail. If start and end are out of * order, swap them. * @param start Beginning of selection to delete. * @param end End of selection to delete. */ /* package */ void deleteSelection(int start, int end) { mTextGeneration++; WebViewCore.TextSelectionData data = new WebViewCore.TextSelectionData(start, end, 0); mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, mTextGeneration, 0, data); } /** * Set the selection to (start, end) in the focused textfield. If start and * end are out of order, swap them. * @param start Beginning of selection. * @param end End of selection. */ /* package */ void setSelection(int start, int end) { if (mWebViewCore != null) { mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end); } } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (mInputConnection == null) { mInputConnection = new WebViewInputConnection(); mAutoCompletePopup = new AutoCompletePopup(this, mInputConnection); } mInputConnection.setupEditorInfo(outAttrs); return mInputConnection; } private void relocateAutoCompletePopup() { if (mAutoCompletePopup != null) { mAutoCompletePopup.resetRect(); mAutoCompletePopup.setText(mInputConnection.getEditable()); } } /** * Called in response to a message from webkit telling us that the soft * keyboard should be launched. */ private void displaySoftKeyboard(boolean isTextView) { InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); // bring it back to the default level scale so that user can enter text boolean zoom = mZoomManager.getScale() < mZoomManager.getDefaultScale(); if (zoom) { mZoomManager.setZoomCenter(mLastTouchX, mLastTouchY); mZoomManager.setZoomScale(mZoomManager.getDefaultScale(), false); } // Used by plugins and contentEditable. // Also used if the navigation cache is out of date, and // does not recognize that a textfield is in focus. In that // case, use WebView as the targeted view. // see http://b/issue?id=2457459 imm.showSoftInput(mWebView, 0); } // Called by WebKit to instruct the UI to hide the keyboard private void hideSoftKeyboard() { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null && (imm.isActive(mWebView))) { imm.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); } } /** * Called by AutoCompletePopup to find saved form data associated with the * textfield * @param name Name of the textfield. * @param nodePointer Pointer to the node of the textfield, so it can be * compared to the currently focused textfield when the data is * retrieved. * @param autoFillable true if WebKit has determined this field is part of * a form that can be auto filled. * @param autoComplete true if the attribute "autocomplete" is set to true * on the textfield. */ /* package */ void requestFormData(String name, int nodePointer, boolean autoFillable, boolean autoComplete) { if (mWebViewCore.getSettings().getSaveFormData()) { Message update = mPrivateHandler.obtainMessage(REQUEST_FORM_DATA); update.arg1 = nodePointer; RequestFormData updater = new RequestFormData(name, getUrl(), update, autoFillable, autoComplete); Thread t = new Thread(updater); t.start(); } } /* * This class requests an Adapter for the AutoCompletePopup which shows past * entries stored in the database. It is a Runnable so that it can be done * in its own thread, without slowing down the UI. */ private class RequestFormData implements Runnable { private String mName; private String mUrl; private Message mUpdateMessage; private boolean mAutoFillable; private boolean mAutoComplete; private WebSettingsClassic mWebSettings; public RequestFormData(String name, String url, Message msg, boolean autoFillable, boolean autoComplete) { mName = name; mUrl = WebTextView.urlForAutoCompleteData(url); mUpdateMessage = msg; mAutoFillable = autoFillable; mAutoComplete = autoComplete; mWebSettings = getSettings(); } @Override public void run() { ArrayList pastEntries = new ArrayList(); if (mAutoFillable) { // Note that code inside the adapter click handler in AutoCompletePopup depends // on the AutoFill item being at the top of the drop down list. If you change // the order, make sure to do it there too! if (mWebSettings != null && mWebSettings.getAutoFillProfile() != null) { pastEntries.add(mWebView.getResources().getText( com.android.internal.R.string.autofill_this_form).toString() + " " + mAutoFillData.getPreviewString()); mAutoCompletePopup.setIsAutoFillProfileSet(true); } else { // There is no autofill profile set up yet, so add an option that // will invite the user to set their profile up. pastEntries.add(mWebView.getResources().getText( com.android.internal.R.string.setup_autofill).toString()); mAutoCompletePopup.setIsAutoFillProfileSet(false); } } if (mAutoComplete) { pastEntries.addAll(mDatabase.getFormData(mUrl, mName)); } if (pastEntries.size() > 0) { ArrayAdapter adapter = new ArrayAdapter( mContext, com.android.internal.R.layout.web_text_view_dropdown, pastEntries); mUpdateMessage.obj = adapter; mUpdateMessage.sendToTarget(); } } } /** * Dump the display tree to "/sdcard/displayTree.txt" * * debug only */ public void dumpDisplayTree() { nativeDumpDisplayTree(getUrl()); } /** * Dump the dom tree to adb shell if "toFile" is False, otherwise dump it to * "/sdcard/domTree.txt" * * debug only */ public void dumpDomTree(boolean toFile) { mWebViewCore.sendMessage(EventHub.DUMP_DOMTREE, toFile ? 1 : 0, 0); } /** * Dump the render tree to adb shell if "toFile" is False, otherwise dump it * to "/sdcard/renderTree.txt" * * debug only */ public void dumpRenderTree(boolean toFile) { mWebViewCore.sendMessage(EventHub.DUMP_RENDERTREE, toFile ? 1 : 0, 0); } /** * Called by DRT on UI thread, need to proxy to WebCore thread. * * debug only */ public void setUseMockDeviceOrientation() { mWebViewCore.sendMessage(EventHub.SET_USE_MOCK_DEVICE_ORIENTATION); } /** * Sets use of the Geolocation mock client. Also resets that client. Called * by DRT on UI thread, need to proxy to WebCore thread. * * debug only */ public void setUseMockGeolocation() { mWebViewCore.sendMessage(EventHub.SET_USE_MOCK_GEOLOCATION); } /** * Called by DRT on WebCore thread. * * debug only */ public void setMockGeolocationPosition(double latitude, double longitude, double accuracy) { mWebViewCore.setMockGeolocationPosition(latitude, longitude, accuracy); } /** * Called by DRT on WebCore thread. * * debug only */ public void setMockGeolocationError(int code, String message) { mWebViewCore.setMockGeolocationError(code, message); } /** * Called by DRT on WebCore thread. * * debug only */ public void setMockGeolocationPermission(boolean allow) { mWebViewCore.setMockGeolocationPermission(allow); } /** * Called by DRT on WebCore thread. * * debug only */ public void setMockDeviceOrientation(boolean canProvideAlpha, double alpha, boolean canProvideBeta, double beta, boolean canProvideGamma, double gamma) { mWebViewCore.setMockDeviceOrientation(canProvideAlpha, alpha, canProvideBeta, beta, canProvideGamma, gamma); } // This is used to determine long press with the center key. Does not // affect long press with the trackball/touch. private boolean mGotCenterDown = false; @Override public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { if (mBlockWebkitViewMessages) { return false; } // send complex characters to webkit for use by JS and plugins if (keyCode == KeyEvent.KEYCODE_UNKNOWN && event.getCharacters() != null) { // pass the key to DOM sendBatchableInputMessage(EventHub.KEY_DOWN, 0, 0, event); sendBatchableInputMessage(EventHub.KEY_UP, 0, 0, event); // return true as DOM handles the key return true; } return false; } private boolean isEnterActionKey(int keyCode) { return keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER; } public boolean onKeyPreIme(int keyCode, KeyEvent event) { if (mAutoCompletePopup != null) { return mAutoCompletePopup.onKeyPreIme(keyCode, event); } return false; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis() + "keyCode=" + keyCode + ", " + event + ", unicode=" + event.getUnicodeChar()); } if (mIsCaretSelection) { selectionDone(); } if (mBlockWebkitViewMessages) { return false; } // don't implement accelerator keys here; defer to host application if (event.isCtrlPressed()) { return false; } if (mNativeClass == 0) { return false; } // do this hack up front, so it always works, regardless of touch-mode if (AUTO_REDRAW_HACK && (keyCode == KeyEvent.KEYCODE_CALL)) { mAutoRedraw = !mAutoRedraw; if (mAutoRedraw) { invalidate(); } return true; } // Bubble up the key event if // 1. it is a system key; or // 2. the host application wants to handle it; if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) { return false; } // See if the accessibility injector needs to handle this event. if (isAccessibilityInjectionEnabled() && getAccessibilityInjector().handleKeyEventIfNecessary(event)) { return true; } if (keyCode == KeyEvent.KEYCODE_PAGE_UP) { if (event.hasNoModifiers()) { pageUp(false); return true; } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { pageUp(true); return true; } } if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) { if (event.hasNoModifiers()) { pageDown(false); return true; } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { pageDown(true); return true; } } if (keyCode == KeyEvent.KEYCODE_MOVE_HOME && event.hasNoModifiers()) { pageUp(true); return true; } if (keyCode == KeyEvent.KEYCODE_MOVE_END && event.hasNoModifiers()) { pageDown(true); return true; } if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { switchOutDrawHistory(); } if (isEnterActionKey(keyCode)) { switchOutDrawHistory(); if (event.getRepeatCount() == 0) { if (mSelectingText) { return true; // discard press if copy in progress } mGotCenterDown = true; mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(LONG_PRESS_CENTER), LONG_PRESS_TIMEOUT); } } if (getSettings().getNavDump()) { switch (keyCode) { case KeyEvent.KEYCODE_4: dumpDisplayTree(); break; case KeyEvent.KEYCODE_5: case KeyEvent.KEYCODE_6: dumpDomTree(keyCode == KeyEvent.KEYCODE_5); break; case KeyEvent.KEYCODE_7: case KeyEvent.KEYCODE_8: dumpRenderTree(keyCode == KeyEvent.KEYCODE_7); break; } } // pass the key to DOM sendKeyEvent(event); // return true as DOM handles the key return true; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "keyUp at " + System.currentTimeMillis() + ", " + event + ", unicode=" + event.getUnicodeChar()); } if (mBlockWebkitViewMessages) { return false; } if (mNativeClass == 0) { return false; } // special CALL handling when cursor node's href is "tel:XXX" if (keyCode == KeyEvent.KEYCODE_CALL && mInitialHitTestResult != null && mInitialHitTestResult.getType() == HitTestResult.PHONE_TYPE) { String text = mInitialHitTestResult.getExtra(); Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(text)); mContext.startActivity(intent); return true; } // Bubble up the key event if // 1. it is a system key; or // 2. the host application wants to handle it; if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) { return false; } // See if the accessibility injector needs to handle this event. if (isAccessibilityInjectionEnabled() && getAccessibilityInjector().handleKeyEventIfNecessary(event)) { return true; } if (isEnterActionKey(keyCode)) { // remove the long press message first mPrivateHandler.removeMessages(LONG_PRESS_CENTER); mGotCenterDown = false; if (mSelectingText) { copySelection(); selectionDone(); return true; // discard press if copy in progress } } // pass the key to DOM sendKeyEvent(event); // return true as DOM handles the key return true; } private boolean startSelectActionMode() { mSelectCallback = new SelectActionModeCallback(); mSelectCallback.setTextSelected(!mIsCaretSelection); mSelectCallback.setWebView(this); if (mWebView.startActionMode(mSelectCallback) == null) { // There is no ActionMode, so do not allow the user to modify a // selection. selectionDone(); return false; } mWebView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); return true; } private void showPasteWindow() { ClipboardManager cm = (ClipboardManager)(mContext .getSystemService(Context.CLIPBOARD_SERVICE)); if (cm.hasPrimaryClip()) { Point cursorPoint = new Point(contentToViewX(mSelectCursorBase.x), contentToViewY(mSelectCursorBase.y)); Point cursorTop = calculateBaseCaretTop(); cursorTop.set(contentToViewX(cursorTop.x), contentToViewY(cursorTop.y)); int[] location = new int[2]; mWebView.getLocationInWindow(location); int offsetX = location[0] - getScrollX(); int offsetY = location[1] - getScrollY(); cursorPoint.offset(offsetX, offsetY); cursorTop.offset(offsetX, offsetY); if (mPasteWindow == null) { mPasteWindow = new PastePopupWindow(); } mPasteWindow.show(cursorPoint, cursorTop, location[0], location[1]); } } /** * Given segment AB, this finds the point C along AB that is closest to * point and then returns it scale along AB. The scale factor is AC/AB. * * @param x The x coordinate of the point near segment AB that determines * the scale factor. * @param y The y coordinate of the point near segment AB that determines * the scale factor. * @param a The first point of the line segment. * @param b The second point of the line segment. * @return The scale factor AC/AB, where C is the point on AB closest to * point. */ private static float scaleAlongSegment(int x, int y, PointF a, PointF b) { // The bottom line of the text box is line AB float abX = b.x - a.x; float abY = b.y - a.y; float ab2 = (abX * abX) + (abY * abY); // The line from first point in text bounds to bottom is AP float apX = x - a.x; float apY = y - a.y; float abDotAP = (apX * abX) + (apY * abY); float scale = abDotAP / ab2; return scale; } private Point calculateBaseCaretTop() { return calculateCaretTop(mSelectCursorBase, mSelectCursorBaseTextQuad); } private Point calculateDraggingCaretTop() { return calculateCaretTop(mSelectDraggingCursor, mSelectDraggingTextQuad); } /** * Assuming arbitrary shape of a quadralateral forming text bounds, this * calculates the top of a caret. */ private static Point calculateCaretTop(Point base, QuadF quad) { float scale = scaleAlongSegment(base.x, base.y, quad.p4, quad.p3); int x = Math.round(scaleCoordinate(scale, quad.p1.x, quad.p2.x)); int y = Math.round(scaleCoordinate(scale, quad.p1.y, quad.p2.y)); return new Point(x, y); } private void hidePasteButton() { if (mPasteWindow != null) { mPasteWindow.hide(); } } private void syncSelectionCursors() { mSelectCursorBaseLayerId = nativeGetHandleLayerId(mNativeClass, HANDLE_ID_BASE, mSelectCursorBase, mSelectCursorBaseTextQuad); mSelectCursorExtentLayerId = nativeGetHandleLayerId(mNativeClass, HANDLE_ID_EXTENT, mSelectCursorExtent, mSelectCursorExtentTextQuad); } private boolean setupWebkitSelect() { syncSelectionCursors(); if (!mIsCaretSelection && !startSelectActionMode()) { selectionDone(); return false; } startSelectingText(); mTouchMode = TOUCH_DRAG_MODE; return true; } private void updateWebkitSelection(boolean isSnapped) { int handleId = (mSelectDraggingCursor == mSelectCursorBase) ? HANDLE_ID_BASE : HANDLE_ID_EXTENT; int x = mSelectDraggingCursor.x; int y = mSelectDraggingCursor.y; if (isSnapped) { // "center" the cursor in the snapping quad Point top = calculateDraggingCaretTop(); x = Math.round((top.x + x) / 2); y = Math.round((top.y + y) / 2); } mWebViewCore.removeMessages(EventHub.SELECT_TEXT); mWebViewCore.sendMessageAtFrontOfQueue(EventHub.SELECT_TEXT, x, y, (Integer)handleId); } private void resetCaretTimer() { mPrivateHandler.removeMessages(CLEAR_CARET_HANDLE); if (!mSelectionStarted) { mPrivateHandler.sendEmptyMessageDelayed(CLEAR_CARET_HANDLE, CARET_HANDLE_STAMINA_MS); } } /** * Select all of the text in this WebView. * * This is an implementation detail. */ public void selectAll() { mWebViewCore.sendMessage(EventHub.SELECT_ALL); } /** * Called when the selection has been removed. */ void selectionDone() { if (mSelectingText) { hidePasteButton(); endSelectingText(); // finish is idempotent, so this is fine even if selectionDone was // called by mSelectCallback.onDestroyActionMode if (mSelectCallback != null) { mSelectCallback.finish(); mSelectCallback = null; } invalidate(); // redraw without selection mAutoScrollX = 0; mAutoScrollY = 0; mSentAutoScrollMessage = false; } } /** * Copy the selection to the clipboard * * This is an implementation detail. */ public boolean copySelection() { boolean copiedSomething = false; String selection = getSelection(); if (selection != null && selection != "") { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "copySelection \"" + selection + "\""); } Toast.makeText(mContext , com.android.internal.R.string.text_copied , Toast.LENGTH_SHORT).show(); copiedSomething = true; ClipboardManager cm = (ClipboardManager)mContext .getSystemService(Context.CLIPBOARD_SERVICE); cm.setText(selection); int[] handles = new int[4]; getSelectionHandles(handles); mWebViewCore.sendMessage(EventHub.COPY_TEXT, handles); } invalidate(); // remove selection region and pointer return copiedSomething; } /** * Cut the selected text into the clipboard * * This is an implementation detail */ public void cutSelection() { copySelection(); int[] handles = new int[4]; getSelectionHandles(handles); mWebViewCore.sendMessage(EventHub.DELETE_TEXT, handles); } /** * Paste text from the clipboard to the cursor position. * * This is an implementation detail */ public void pasteFromClipboard() { ClipboardManager cm = (ClipboardManager)mContext .getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = cm.getPrimaryClip(); if (clipData != null) { ClipData.Item clipItem = clipData.getItemAt(0); CharSequence pasteText = clipItem.getText(); if (mInputConnection != null) { mInputConnection.replaceSelection(pasteText); } } } /** * Returns the currently highlighted text as a string. */ String getSelection() { if (mNativeClass == 0) return ""; return nativeGetSelection(); } @Override public void onAttachedToWindow() { if (mWebView.hasWindowFocus()) setActive(true); if (isAccessibilityInjectionEnabled()) { getAccessibilityInjector().toggleAccessibilityFeedback(true); } updateHwAccelerated(); } @Override public void onDetachedFromWindow() { clearHelpers(); mZoomManager.dismissZoomPicker(); if (mWebView.hasWindowFocus()) setActive(false); if (isAccessibilityInjectionEnabled()) { getAccessibilityInjector().toggleAccessibilityFeedback(false); } updateHwAccelerated(); ensureFunctorDetached(); } @Override public void onVisibilityChanged(View changedView, int visibility) { // The zoomManager may be null if the webview is created from XML that // specifies the view's visibility param as not visible (see http://b/2794841) if (visibility != View.VISIBLE && mZoomManager != null) { mZoomManager.dismissZoomPicker(); } updateDrawingState(); } void setActive(boolean active) { if (active) { if (mWebView.hasFocus()) { // If our window regained focus, and we have focus, then begin // drawing the cursor ring mDrawCursorRing = true; setFocusControllerActive(true); } else { mDrawCursorRing = false; setFocusControllerActive(false); } } else { if (!mZoomManager.isZoomPickerVisible()) { /* * The external zoom controls come in their own window, so our * window loses focus. Our policy is to not draw the cursor ring * if our window is not focused, but this is an exception since * the user can still navigate the web page with the zoom * controls showing. */ mDrawCursorRing = false; } mKeysPressed.clear(); mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); mTouchMode = TOUCH_DONE_MODE; setFocusControllerActive(false); } invalidate(); } // To avoid drawing the cursor ring, and remove the TextView when our window // loses focus. @Override public void onWindowFocusChanged(boolean hasWindowFocus) { setActive(hasWindowFocus); if (hasWindowFocus) { JWebCoreJavaBridge.setActiveWebView(this); if (mPictureUpdatePausedForFocusChange) { WebViewCore.resumeUpdatePicture(mWebViewCore); mPictureUpdatePausedForFocusChange = false; } } else { JWebCoreJavaBridge.removeActiveWebView(this); final WebSettings settings = getSettings(); if (settings != null && settings.enableSmoothTransition() && mWebViewCore != null && !WebViewCore.isUpdatePicturePaused(mWebViewCore)) { WebViewCore.pauseUpdatePicture(mWebViewCore); mPictureUpdatePausedForFocusChange = true; } } } /* * Pass a message to WebCore Thread, telling the WebCore::Page's * FocusController to be "inactive" so that it will * not draw the blinking cursor. It gets set to "active" to draw the cursor * in WebViewCore.cpp, when the WebCore thread receives key events/clicks. */ /* package */ void setFocusControllerActive(boolean active) { if (mWebViewCore == null) return; mWebViewCore.sendMessage(EventHub.SET_ACTIVE, active ? 1 : 0, 0); // Need to send this message after the document regains focus. if (active && mListBoxMessage != null) { mWebViewCore.sendMessage(mListBoxMessage); mListBoxMessage = null; } } @Override public void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "MT focusChanged " + focused + ", " + direction); } if (focused) { mDrawCursorRing = true; setFocusControllerActive(true); } else { mDrawCursorRing = false; setFocusControllerActive(false); mKeysPressed.clear(); } if (!mTouchHighlightRegion.isEmpty()) { mWebView.invalidate(mTouchHighlightRegion.getBounds()); } } // updateRectsForGL() happens almost every draw call, in order to avoid creating // any object in this code path, we move the local variable out to be a private // final member, and we marked them as mTemp*. private final Point mTempVisibleRectOffset = new Point(); private final Rect mTempVisibleRect = new Rect(); void updateRectsForGL() { // Use the getGlobalVisibleRect() to get the intersection among the parents // visible == false means we're clipped - send a null rect down to indicate that // we should not draw boolean visible = mWebView.getGlobalVisibleRect(mTempVisibleRect, mTempVisibleRectOffset); mInvScreenRect.set(mTempVisibleRect); if (visible) { // Then need to invert the Y axis, just for GL View rootView = mWebView.getRootView(); int rootViewHeight = rootView.getHeight(); mScreenRect.set(mInvScreenRect); int savedWebViewBottom = mInvScreenRect.bottom; mInvScreenRect.bottom = rootViewHeight - mInvScreenRect.top - getVisibleTitleHeightImpl(); mInvScreenRect.top = rootViewHeight - savedWebViewBottom; mIsWebViewVisible = true; } else { mIsWebViewVisible = false; } mTempVisibleRect.offset(-mTempVisibleRectOffset.x, -mTempVisibleRectOffset.y); viewToContentVisibleRect(mVisibleContentRect, mTempVisibleRect); nativeUpdateDrawGLFunction(mNativeClass, mIsWebViewVisible ? mInvScreenRect : null, mIsWebViewVisible ? mScreenRect : null, mVisibleContentRect, getScale()); } // Input : viewRect, rect in view/screen coordinate. // Output: contentRect, rect in content/document coordinate. private void viewToContentVisibleRect(RectF contentRect, Rect viewRect) { contentRect.left = viewToContentXf(viewRect.left) / mWebView.getScaleX(); // viewToContentY will remove the total height of the title bar. Add // the visible height back in to account for the fact that if the title // bar is partially visible, the part of the visible rect which is // displaying our content is displaced by that amount. contentRect.top = viewToContentYf(viewRect.top + getVisibleTitleHeightImpl()) / mWebView.getScaleY(); contentRect.right = viewToContentXf(viewRect.right) / mWebView.getScaleX(); contentRect.bottom = viewToContentYf(viewRect.bottom) / mWebView.getScaleY(); } @Override public boolean setFrame(int left, int top, int right, int bottom) { boolean changed = mWebViewPrivate.super_setFrame(left, top, right, bottom); if (!changed && mHeightCanMeasure) { // When mHeightCanMeasure is true, we will set mLastHeightSent to 0 // in WebViewCore after we get the first layout. We do call // requestLayout() when we get contentSizeChanged(). But the View // system won't call onSizeChanged if the dimension is not changed. // In this case, we need to call sendViewSizeZoom() explicitly to // notify the WebKit about the new dimensions. sendViewSizeZoom(false); } updateRectsForGL(); return changed; } @Override public void onSizeChanged(int w, int h, int ow, int oh) { // adjust the max viewport width depending on the view dimensions. This // is to ensure the scaling is not going insane. So do not shrink it if // the view size is temporarily smaller, e.g. when soft keyboard is up. int newMaxViewportWidth = (int) (Math.max(w, h) / mZoomManager.getDefaultMinZoomScale()); if (newMaxViewportWidth > sMaxViewportWidth) { sMaxViewportWidth = newMaxViewportWidth; } mZoomManager.onSizeChanged(w, h, ow, oh); if (mLoadedPicture != null && mDelaySetPicture == null) { // Size changes normally result in a new picture // Re-set the loaded picture to simulate that // However, do not update the base layer as that hasn't changed setNewPicture(mLoadedPicture, false); } if (mIsEditingText) { scrollEditIntoView(); } relocateAutoCompletePopup(); } /** * Scrolls the edit field into view using the minimum scrolling necessary. * If the edit field is too large to fit in the visible window, the caret * dimensions are used so that at least the caret is visible. * A buffer of EDIT_RECT_BUFFER in view pixels is used to offset the * edit rectangle to ensure a margin with the edge of the screen. */ private void scrollEditIntoView() { Rect visibleRect = new Rect(viewToContentX(getScrollX()), viewToContentY(getScrollY()), viewToContentX(getScrollX() + getWidth()), viewToContentY(getScrollY() + getViewHeightWithTitle())); if (visibleRect.contains(mEditTextContentBounds)) { return; // no need to scroll } syncSelectionCursors(); nativeFindMaxVisibleRect(mNativeClass, mEditTextLayerId, visibleRect); final int buffer = Math.max(1, viewToContentDimension(EDIT_RECT_BUFFER)); Rect showRect = new Rect( Math.max(0, mEditTextContentBounds.left - buffer), Math.max(0, mEditTextContentBounds.top - buffer), mEditTextContentBounds.right + buffer, mEditTextContentBounds.bottom + buffer); Point caretTop = calculateBaseCaretTop(); if (visibleRect.width() < mEditTextContentBounds.width()) { // The whole edit won't fit in the width, so use the caret rect if (mSelectCursorBase.x < caretTop.x) { showRect.left = Math.max(0, mSelectCursorBase.x - buffer); showRect.right = caretTop.x + buffer; } else { showRect.left = Math.max(0, caretTop.x - buffer); showRect.right = mSelectCursorBase.x + buffer; } } if (visibleRect.height() < mEditTextContentBounds.height()) { // The whole edit won't fit in the height, so use the caret rect if (mSelectCursorBase.y > caretTop.y) { showRect.top = Math.max(0, caretTop.y - buffer); showRect.bottom = mSelectCursorBase.y + buffer; } else { showRect.top = Math.max(0, mSelectCursorBase.y - buffer); showRect.bottom = caretTop.y + buffer; } } if (visibleRect.contains(showRect)) { return; // no need to scroll } int scrollX = viewToContentX(getScrollX()); if (visibleRect.left > showRect.left) { // We are scrolled too far scrollX += showRect.left - visibleRect.left; } else if (visibleRect.right < showRect.right) { // We aren't scrolled enough to include the right scrollX += showRect.right - visibleRect.right; } int scrollY = viewToContentY(getScrollY()); if (visibleRect.top > showRect.top) { scrollY += showRect.top - visibleRect.top; } else if (visibleRect.bottom < showRect.bottom) { scrollY += showRect.bottom - visibleRect.bottom; } contentScrollTo(scrollX, scrollY, false); } @Override public void onScrollChanged(int l, int t, int oldl, int oldt) { if (!mInOverScrollMode) { sendOurVisibleRect(); // update WebKit if visible title bar height changed. The logic is same // as getVisibleTitleHeightImpl. int titleHeight = getTitleHeight(); if (Math.max(titleHeight - t, 0) != Math.max(titleHeight - oldt, 0)) { sendViewSizeZoom(false); } } } @Override public boolean dispatchKeyEvent(KeyEvent event) { switch (event.getAction()) { case KeyEvent.ACTION_DOWN: mKeysPressed.add(Integer.valueOf(event.getKeyCode())); break; case KeyEvent.ACTION_MULTIPLE: // Always accept the action. break; case KeyEvent.ACTION_UP: int location = mKeysPressed.indexOf(Integer.valueOf(event.getKeyCode())); if (location == -1) { // We did not receive the key down for this key, so do not // handle the key up. return false; } else { // We did receive the key down. Handle the key up, and // remove it from our pressed keys. mKeysPressed.remove(location); } break; default: // Accept the action. This should not happen, unless a new // action is added to KeyEvent. break; } return mWebViewPrivate.super_dispatchKeyEvent(event); } /* * Here is the snap align logic: * 1. If it starts nearly horizontally or vertically, snap align; * 2. If there is a dramitic direction change, let it go; * * Adjustable parameters. Angle is the radians on a unit circle, limited * to quadrant 1. Values range from 0f (horizontal) to PI/2 (vertical) */ private static final float HSLOPE_TO_START_SNAP = .25f; private static final float HSLOPE_TO_BREAK_SNAP = .4f; private static final float VSLOPE_TO_START_SNAP = 1.25f; private static final float VSLOPE_TO_BREAK_SNAP = .95f; /* * These values are used to influence the average angle when entering * snap mode. If is is the first movement entering snap, we set the average * to the appropriate ideal. If the user is entering into snap after the * first movement, then we average the average angle with these values. */ private static final float ANGLE_VERT = 2f; private static final float ANGLE_HORIZ = 0f; /* * The modified moving average weight. * Formula: MAV[t]=MAV[t-1] + (P[t]-MAV[t-1])/n */ private static final float MMA_WEIGHT_N = 5; private boolean inFullScreenMode() { return mFullScreenHolder != null; } private void dismissFullScreenMode() { if (inFullScreenMode()) { mFullScreenHolder.hide(); mFullScreenHolder = null; invalidate(); } } void onPinchToZoomAnimationStart() { // cancel the single touch handling cancelTouch(); onZoomAnimationStart(); } void onPinchToZoomAnimationEnd(ScaleGestureDetector detector) { onZoomAnimationEnd(); // start a drag, TOUCH_PINCH_DRAG, can't use TOUCH_INIT_MODE as // it may trigger the unwanted click, can't use TOUCH_DRAG_MODE // as it may trigger the unwanted fling. mTouchMode = TOUCH_PINCH_DRAG; mConfirmMove = true; startTouch(detector.getFocusX(), detector.getFocusY(), mLastTouchTime); } // See if there is a layer at x, y and switch to TOUCH_DRAG_LAYER_MODE if a // layer is found. private void startScrollingLayer(float x, float y) { if (mNativeClass == 0) return; int contentX = viewToContentX((int) x + getScrollX()); int contentY = viewToContentY((int) y + getScrollY()); mCurrentScrollingLayerId = nativeScrollableLayer(mNativeClass, contentX, contentY, mScrollingLayerRect, mScrollingLayerBounds); if (mCurrentScrollingLayerId != 0) { mTouchMode = TOUCH_DRAG_LAYER_MODE; } } // 1/(density * density) used to compute the distance between points. // Computed in init(). private float DRAG_LAYER_INVERSE_DENSITY_SQUARED; // The distance between two points reported in onTouchEvent scaled by the // density of the screen. private static final int DRAG_LAYER_FINGER_DISTANCE = 20000; @Override public boolean onHoverEvent(MotionEvent event) { if (mNativeClass == 0) { return false; } int x = viewToContentX((int) event.getX() + getScrollX()); int y = viewToContentY((int) event.getY() + getScrollY()); mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, x, y); mWebViewPrivate.super_onHoverEvent(event); return true; } @Override public boolean onTouchEvent(MotionEvent ev) { if (mNativeClass == 0 || (!mWebView.isClickable() && !mWebView.isLongClickable())) { return false; } if (mInputDispatcher == null) { return false; } if (mWebView.isFocusable() && mWebView.isFocusableInTouchMode() && !mWebView.isFocused()) { mWebView.requestFocus(); } if (mInputDispatcher.postPointerEvent(ev, getScrollX(), getScrollY() - getTitleHeight(), mZoomManager.getInvScale())) { mInputDispatcher.dispatchUiEvents(); return true; } else { Log.w(LOGTAG, "mInputDispatcher rejected the event!"); return false; } } private float calculateDragAngle(int dx, int dy) { dx = Math.abs(dx); dy = Math.abs(dy); return (float) Math.atan2(dy, dx); } /* * Common code for single touch and multi-touch. * (x, y) denotes current focus point, which is the touch point for single touch * and the middle point for multi-touch. */ private void handleTouchEventCommon(MotionEvent event, int action, int x, int y) { ScaleGestureDetector detector = mZoomManager.getScaleGestureDetector(); long eventTime = event.getEventTime(); // Due to the touch screen edge effect, a touch closer to the edge // always snapped to the edge. As getViewWidth() can be different from // getWidth() due to the scrollbar, adjusting the point to match // getViewWidth(). Same applied to the height. x = Math.min(x, getViewWidth() - 1); y = Math.min(y, getViewHeightWithTitle() - 1); int deltaX = mLastTouchX - x; int deltaY = mLastTouchY - y; int contentX = viewToContentX(x + getScrollX()); int contentY = viewToContentY(y + getScrollY()); switch (action) { case MotionEvent.ACTION_DOWN: { mConfirmMove = false; if (!mEditTextScroller.isFinished()) { mEditTextScroller.abortAnimation(); } if (!mScroller.isFinished()) { // stop the current scroll animation, but if this is // the start of a fling, allow it to add to the current // fling's velocity mScroller.abortAnimation(); mTouchMode = TOUCH_DRAG_START_MODE; mConfirmMove = true; nativeSetIsScrolling(false); } else if (mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) { mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP); removeTouchHighlight(); if (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare) { mTouchMode = TOUCH_DOUBLE_TAP_MODE; } else { mTouchMode = TOUCH_INIT_MODE; } } else { // the normal case mTouchMode = TOUCH_INIT_MODE; if (mLogEvent && eventTime - mLastTouchUpTime < 1000) { EventLog.writeEvent(EventLogTags.BROWSER_DOUBLE_TAP_DURATION, (eventTime - mLastTouchUpTime), eventTime); } mSelectionStarted = false; if (mSelectingText) { ensureSelectionHandles(); int shiftedY = y - getTitleHeight() + getScrollY(); int shiftedX = x + getScrollX(); if (mSelectHandleBaseBounds.contains(shiftedX, shiftedY)) { mSelectionStarted = true; mSelectDraggingCursor = mSelectCursorBase; mSelectDraggingTextQuad = mSelectCursorBaseTextQuad; if (mIsCaretSelection) { mPrivateHandler.removeMessages(CLEAR_CARET_HANDLE); hidePasteButton(); } } else if (mSelectHandleExtentBounds .contains(shiftedX, shiftedY)) { mSelectionStarted = true; mSelectDraggingCursor = mSelectCursorExtent; mSelectDraggingTextQuad = mSelectCursorExtentTextQuad; } else if (mIsCaretSelection) { selectionDone(); } if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "select=" + contentX + "," + contentY); } } } // Trigger the link if (!mSelectingText && (mTouchMode == TOUCH_INIT_MODE || mTouchMode == TOUCH_DOUBLE_TAP_MODE)) { mPrivateHandler.sendEmptyMessageDelayed( SWITCH_TO_SHORTPRESS, TAP_TIMEOUT); mPrivateHandler.sendEmptyMessageDelayed( SWITCH_TO_LONGPRESS, LONG_PRESS_TIMEOUT); } startTouch(x, y, eventTime); if (mIsEditingText) { mTouchInEditText = mEditTextContentBounds .contains(contentX, contentY); } break; } case MotionEvent.ACTION_MOVE: { if (!mConfirmMove && (deltaX * deltaX + deltaY * deltaY) >= mTouchSlopSquare) { mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); mConfirmMove = true; if (mTouchMode == TOUCH_DOUBLE_TAP_MODE) { mTouchMode = TOUCH_INIT_MODE; } removeTouchHighlight(); } if (mSelectingText && mSelectionStarted) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "extend=" + contentX + "," + contentY); } ViewParent parent = mWebView.getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } if (deltaX != 0 || deltaY != 0) { int handleX = contentX + viewToContentDimension(mSelectOffset.x); int handleY = contentY + viewToContentDimension(mSelectOffset.y); mSelectDraggingCursor.set(handleX, handleY); boolean inCursorText = mSelectDraggingTextQuad.containsPoint(handleX, handleY); boolean inEditBounds = mEditTextContentBounds .contains(handleX, handleY); if (mIsEditingText && !inEditBounds) { beginScrollEdit(); } else { endScrollEdit(); } boolean snapped = false; if (inCursorText || (mIsEditingText && !inEditBounds)) { snapDraggingCursor(); snapped = true; } updateWebkitSelection(snapped); if (!inCursorText && mIsEditingText && inEditBounds) { // Visually snap even if we have moved the handle. snapDraggingCursor(); } mLastTouchX = x; mLastTouchY = y; invalidate(); } break; } if (mTouchMode == TOUCH_DONE_MODE) { // no dragging during scroll zoom animation, or when prevent // default is yes break; } if (mVelocityTracker == null) { Log.e(LOGTAG, "Got null mVelocityTracker when " + " mTouchMode = " + mTouchMode); } else { mVelocityTracker.addMovement(event); } if (mTouchMode != TOUCH_DRAG_MODE && mTouchMode != TOUCH_DRAG_LAYER_MODE && mTouchMode != TOUCH_DRAG_TEXT_MODE) { if (!mConfirmMove) { break; } // Only lock dragging to one axis if we don't have a scale in progress. // Scaling implies free-roaming movement. Note this is only ever a question // if mZoomManager.supportsPanDuringZoom() is true. mAverageAngle = calculateDragAngle(deltaX, deltaY); if (detector == null || !detector.isInProgress()) { // if it starts nearly horizontal or vertical, enforce it if (mAverageAngle < HSLOPE_TO_START_SNAP) { mSnapScrollMode = SNAP_X; mSnapPositive = deltaX > 0; mAverageAngle = ANGLE_HORIZ; } else if (mAverageAngle > VSLOPE_TO_START_SNAP) { mSnapScrollMode = SNAP_Y; mSnapPositive = deltaY > 0; mAverageAngle = ANGLE_VERT; } } mTouchMode = TOUCH_DRAG_MODE; mLastTouchX = x; mLastTouchY = y; deltaX = 0; deltaY = 0; startScrollingLayer(x, y); startDrag(); } // do pan boolean keepScrollBarsVisible = false; if (deltaX == 0 && deltaY == 0) { keepScrollBarsVisible = true; } else { mAverageAngle += (calculateDragAngle(deltaX, deltaY) - mAverageAngle) / MMA_WEIGHT_N; if (mSnapScrollMode != SNAP_NONE) { if (mSnapScrollMode == SNAP_Y) { // radical change means getting out of snap mode if (mAverageAngle < VSLOPE_TO_BREAK_SNAP) { mSnapScrollMode = SNAP_NONE; } } if (mSnapScrollMode == SNAP_X) { // radical change means getting out of snap mode if (mAverageAngle > HSLOPE_TO_BREAK_SNAP) { mSnapScrollMode = SNAP_NONE; } } } else { if (mAverageAngle < HSLOPE_TO_START_SNAP) { mSnapScrollMode = SNAP_X; mSnapPositive = deltaX > 0; mAverageAngle = (mAverageAngle + ANGLE_HORIZ) / 2; } else if (mAverageAngle > VSLOPE_TO_START_SNAP) { mSnapScrollMode = SNAP_Y; mSnapPositive = deltaY > 0; mAverageAngle = (mAverageAngle + ANGLE_VERT) / 2; } } if (mSnapScrollMode != SNAP_NONE) { if ((mSnapScrollMode & SNAP_X) == SNAP_X) { deltaY = 0; } else { deltaX = 0; } } if (deltaX * deltaX + deltaY * deltaY > mTouchSlopSquare) { mHeldMotionless = MOTIONLESS_FALSE; } else { mHeldMotionless = MOTIONLESS_TRUE; keepScrollBarsVisible = true; } mLastTouchTime = eventTime; boolean allDrag = doDrag(deltaX, deltaY); if (allDrag) { mLastTouchX = x; mLastTouchY = y; } else { int contentDeltaX = (int)Math.floor(deltaX * mZoomManager.getInvScale()); int roundedDeltaX = contentToViewDimension(contentDeltaX); int contentDeltaY = (int)Math.floor(deltaY * mZoomManager.getInvScale()); int roundedDeltaY = contentToViewDimension(contentDeltaY); mLastTouchX -= roundedDeltaX; mLastTouchY -= roundedDeltaY; } } break; } case MotionEvent.ACTION_UP: { if (mIsEditingText && mSelectionStarted) { endScrollEdit(); mPrivateHandler.sendEmptyMessageDelayed(SCROLL_HANDLE_INTO_VIEW, TEXT_SCROLL_FIRST_SCROLL_MS); if (!mConfirmMove && mIsCaretSelection) { showPasteWindow(); stopTouch(); break; } } mLastTouchUpTime = eventTime; if (mSentAutoScrollMessage) { mAutoScrollX = mAutoScrollY = 0; } switch (mTouchMode) { case TOUCH_DOUBLE_TAP_MODE: // double tap mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); mTouchMode = TOUCH_DONE_MODE; break; case TOUCH_INIT_MODE: // tap case TOUCH_SHORTPRESS_START_MODE: case TOUCH_SHORTPRESS_MODE: mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); if (!mConfirmMove) { if (mSelectingText) { // tapping on selection or controls does nothing if (!mSelectionStarted) { selectionDone(); } break; } // only trigger double tap if the WebView is // scalable if (mTouchMode == TOUCH_INIT_MODE && (canZoomIn() || canZoomOut())) { mPrivateHandler.sendEmptyMessageDelayed( RELEASE_SINGLE_TAP, ViewConfiguration .getDoubleTapTimeout()); } break; } case TOUCH_DRAG_MODE: case TOUCH_DRAG_LAYER_MODE: case TOUCH_DRAG_TEXT_MODE: mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); // if the user waits a while w/o moving before the // up, we don't want to do a fling if (eventTime - mLastTouchTime <= MIN_FLING_TIME) { if (mVelocityTracker == null) { Log.e(LOGTAG, "Got null mVelocityTracker"); } else { mVelocityTracker.addMovement(event); } // set to MOTIONLESS_IGNORE so that it won't keep // removing and sending message in // drawCoreAndCursorRing() mHeldMotionless = MOTIONLESS_IGNORE; doFling(); break; } else { if (mScroller.springBack(getScrollX(), getScrollY(), 0, computeMaxScrollX(), 0, computeMaxScrollY())) { invalidate(); } } // redraw in high-quality, as we're done dragging mHeldMotionless = MOTIONLESS_TRUE; invalidate(); // fall through case TOUCH_DRAG_START_MODE: // TOUCH_DRAG_START_MODE should not happen for the real // device as we almost certain will get a MOVE. But this // is possible on emulator. mLastVelocity = 0; WebViewCore.resumePriority(); if (!mSelectingText) { WebViewCore.resumeUpdatePicture(mWebViewCore); } break; } stopTouch(); break; } case MotionEvent.ACTION_CANCEL: { if (mTouchMode == TOUCH_DRAG_MODE) { mScroller.springBack(getScrollX(), getScrollY(), 0, computeMaxScrollX(), 0, computeMaxScrollY()); invalidate(); } cancelTouch(); break; } } } /** * Returns the text scroll speed in content pixels per millisecond based on * the touch location. * @param coordinate The x or y touch coordinate in content space * @param min The minimum coordinate (x or y) of the edit content bounds * @param max The maximum coordinate (x or y) of the edit content bounds */ private static float getTextScrollSpeed(int coordinate, int min, int max) { if (coordinate < min) { return (coordinate - min) * TEXT_SCROLL_RATE; } else if (coordinate >= max) { return (coordinate - max + 1) * TEXT_SCROLL_RATE; } else { return 0.0f; } } private static int getSelectionCoordinate(int coordinate, int min, int max) { return Math.max(Math.min(coordinate, max), min); } private void beginScrollEdit() { if (mLastEditScroll == 0) { mLastEditScroll = SystemClock.uptimeMillis() - TEXT_SCROLL_FIRST_SCROLL_MS; scrollEditWithCursor(); } } private void scrollDraggedSelectionHandleIntoView() { if (mSelectDraggingCursor == null) { return; } int x = mSelectDraggingCursor.x; int y = mSelectDraggingCursor.y; if (!mEditTextContentBounds.contains(x,y)) { int left = Math.min(0, x - mEditTextContentBounds.left - EDIT_RECT_BUFFER); int right = Math.max(0, x - mEditTextContentBounds.right + EDIT_RECT_BUFFER); int deltaX = left + right; int above = Math.min(0, y - mEditTextContentBounds.top - EDIT_RECT_BUFFER); int below = Math.max(0, y - mEditTextContentBounds.bottom + EDIT_RECT_BUFFER); int deltaY = above + below; if (deltaX != 0 || deltaY != 0) { int scrollX = getTextScrollX() + deltaX; int scrollY = getTextScrollY() + deltaY; scrollX = clampBetween(scrollX, 0, getMaxTextScrollX()); scrollY = clampBetween(scrollY, 0, getMaxTextScrollY()); scrollEditText(scrollX, scrollY); } } } private void endScrollEdit() { mLastEditScroll = 0; } private static int clampBetween(int value, int min, int max) { return Math.max(min, Math.min(value, max)); } private static int getTextScrollDelta(float speed, long deltaT) { float distance = speed * deltaT; int intDistance = (int)Math.floor(distance); float probability = distance - intDistance; if (Math.random() < probability) { intDistance++; } return intDistance; } /** * Scrolls edit text a distance based on the last touch point, * the last scroll time, and the edit text content bounds. */ private void scrollEditWithCursor() { if (mLastEditScroll != 0) { int x = viewToContentX(mLastTouchX + getScrollX() + mSelectOffset.x); float scrollSpeedX = getTextScrollSpeed(x, mEditTextContentBounds.left, mEditTextContentBounds.right); int y = viewToContentY(mLastTouchY + getScrollY() + mSelectOffset.y); float scrollSpeedY = getTextScrollSpeed(y, mEditTextContentBounds.top, mEditTextContentBounds.bottom); if (scrollSpeedX == 0.0f && scrollSpeedY == 0.0f) { endScrollEdit(); } else { long currentTime = SystemClock.uptimeMillis(); long timeSinceLastUpdate = currentTime - mLastEditScroll; int deltaX = getTextScrollDelta(scrollSpeedX, timeSinceLastUpdate); int deltaY = getTextScrollDelta(scrollSpeedY, timeSinceLastUpdate); int scrollX = getTextScrollX() + deltaX; scrollX = clampBetween(scrollX, 0, getMaxTextScrollX()); int scrollY = getTextScrollY() + deltaY; scrollY = clampBetween(scrollY, 0, getMaxTextScrollY()); mLastEditScroll = currentTime; if (scrollX == getTextScrollX() && scrollY == getTextScrollY()) { // By probability no text scroll this time. Try again later. mPrivateHandler.sendEmptyMessageDelayed(SCROLL_EDIT_TEXT, TEXT_SCROLL_FIRST_SCROLL_MS); } else { int selectionX = getSelectionCoordinate(x, mEditTextContentBounds.left, mEditTextContentBounds.right); int selectionY = getSelectionCoordinate(y, mEditTextContentBounds.top, mEditTextContentBounds.bottom); int oldX = mSelectDraggingCursor.x; int oldY = mSelectDraggingCursor.y; mSelectDraggingCursor.set(selectionX, selectionY); updateWebkitSelection(false); scrollEditText(scrollX, scrollY); mSelectDraggingCursor.set(oldX, oldY); } } } } private void startTouch(float x, float y, long eventTime) { // Remember where the motion event started mStartTouchX = mLastTouchX = Math.round(x); mStartTouchY = mLastTouchY = Math.round(y); mLastTouchTime = eventTime; mVelocityTracker = VelocityTracker.obtain(); mSnapScrollMode = SNAP_NONE; } private void startDrag() { WebViewCore.reducePriority(); // to get better performance, pause updating the picture WebViewCore.pauseUpdatePicture(mWebViewCore); nativeSetIsScrolling(true); if (mHorizontalScrollBarMode != SCROLLBAR_ALWAYSOFF || mVerticalScrollBarMode != SCROLLBAR_ALWAYSOFF) { mZoomManager.invokeZoomPicker(); } } private boolean doDrag(int deltaX, int deltaY) { boolean allDrag = true; if ((deltaX | deltaY) != 0) { int oldX = getScrollX(); int oldY = getScrollY(); int rangeX = computeMaxScrollX(); int rangeY = computeMaxScrollY(); final int contentX = (int)Math.floor(deltaX * mZoomManager.getInvScale()); final int contentY = (int)Math.floor(deltaY * mZoomManager.getInvScale()); // Assume page scrolling and change below if we're wrong mTouchMode = TOUCH_DRAG_MODE; // Check special scrolling before going to main page scrolling. if (mIsEditingText && mTouchInEditText && canTextScroll(deltaX, deltaY)) { // Edit text scrolling oldX = getTextScrollX(); rangeX = getMaxTextScrollX(); deltaX = contentX; oldY = getTextScrollY(); rangeY = getMaxTextScrollY(); deltaY = contentY; mTouchMode = TOUCH_DRAG_TEXT_MODE; allDrag = false; } else if (mCurrentScrollingLayerId != 0) { // Check the scrolling bounds to see if we will actually do any // scrolling. The rectangle is in document coordinates. final int maxX = mScrollingLayerRect.right; final int maxY = mScrollingLayerRect.bottom; final int resultX = clampBetween(maxX, 0, mScrollingLayerRect.left + contentX); final int resultY = clampBetween(maxY, 0, mScrollingLayerRect.top + contentY); if (resultX != mScrollingLayerRect.left || resultY != mScrollingLayerRect.top || (contentX | contentY) == 0) { // In case we switched to dragging the page. mTouchMode = TOUCH_DRAG_LAYER_MODE; deltaX = contentX; deltaY = contentY; oldX = mScrollingLayerRect.left; oldY = mScrollingLayerRect.top; rangeX = maxX; rangeY = maxY; allDrag = false; } } if (mOverScrollGlow != null) { mOverScrollGlow.setOverScrollDeltas(deltaX, deltaY); } mWebViewPrivate.overScrollBy(deltaX, deltaY, oldX, oldY, rangeX, rangeY, mOverscrollDistance, mOverscrollDistance, true); if (mOverScrollGlow != null && mOverScrollGlow.isAnimating()) { invalidate(); } } mZoomManager.keepZoomPickerVisible(); return allDrag; } private void stopTouch() { if (mScroller.isFinished() && !mSelectingText && (mTouchMode == TOUCH_DRAG_MODE || mTouchMode == TOUCH_DRAG_LAYER_MODE)) { WebViewCore.resumePriority(); WebViewCore.resumeUpdatePicture(mWebViewCore); nativeSetIsScrolling(false); } // we also use mVelocityTracker == null to tell us that we are // not "moving around", so we can take the slower/prettier // mode in the drawing code if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } // Release any pulled glows if (mOverScrollGlow != null) { mOverScrollGlow.releaseAll(); } if (mSelectingText) { mSelectionStarted = false; syncSelectionCursors(); if (mIsCaretSelection) { resetCaretTimer(); } invalidate(); } } private void cancelTouch() { // we also use mVelocityTracker == null to tell us that we are // not "moving around", so we can take the slower/prettier // mode in the drawing code if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } if ((mTouchMode == TOUCH_DRAG_MODE || mTouchMode == TOUCH_DRAG_LAYER_MODE) && !mSelectingText) { WebViewCore.resumePriority(); WebViewCore.resumeUpdatePicture(mWebViewCore); nativeSetIsScrolling(false); } mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); removeTouchHighlight(); mHeldMotionless = MOTIONLESS_TRUE; mTouchMode = TOUCH_DONE_MODE; } private void snapDraggingCursor() { float scale = scaleAlongSegment( mSelectDraggingCursor.x, mSelectDraggingCursor.y, mSelectDraggingTextQuad.p4, mSelectDraggingTextQuad.p3); // clamp scale to ensure point is on the bottom segment scale = Math.max(0.0f, scale); scale = Math.min(scale, 1.0f); float newX = scaleCoordinate(scale, mSelectDraggingTextQuad.p4.x, mSelectDraggingTextQuad.p3.x); float newY = scaleCoordinate(scale, mSelectDraggingTextQuad.p4.y, mSelectDraggingTextQuad.p3.y); int x = Math.round(newX); int y = Math.round(newY); if (mIsEditingText) { x = clampBetween(x, mEditTextContentBounds.left, mEditTextContentBounds.right); y = clampBetween(y, mEditTextContentBounds.top, mEditTextContentBounds.bottom); } mSelectDraggingCursor.set(x, y); } private static float scaleCoordinate(float scale, float coord1, float coord2) { float diff = coord2 - coord1; return coord1 + (scale * diff); } @Override public boolean onGenericMotionEvent(MotionEvent event) { if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { switch (event.getAction()) { case MotionEvent.ACTION_SCROLL: { final float vscroll; final float hscroll; if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { vscroll = 0; hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); } else { vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); } if (hscroll != 0 || vscroll != 0) { final int vdelta = (int) (vscroll * mWebViewPrivate.getVerticalScrollFactor()); final int hdelta = (int) (hscroll * mWebViewPrivate.getHorizontalScrollFactor()); abortAnimation(); int oldTouchMode = mTouchMode; startScrollingLayer(event.getX(), event.getY()); doDrag(hdelta, vdelta); mTouchMode = oldTouchMode; return true; } } } } return mWebViewPrivate.super_onGenericMotionEvent(event); } private long mTrackballFirstTime = 0; private long mTrackballLastTime = 0; private float mTrackballRemainsX = 0.0f; private float mTrackballRemainsY = 0.0f; private int mTrackballXMove = 0; private int mTrackballYMove = 0; private boolean mSelectingText = false; private boolean mShowTextSelectionExtra = false; private boolean mSelectionStarted = false; private static final int TRACKBALL_KEY_TIMEOUT = 1000; private static final int TRACKBALL_TIMEOUT = 200; private static final int TRACKBALL_WAIT = 100; private static final int TRACKBALL_SCALE = 400; private static final int TRACKBALL_SCROLL_COUNT = 5; private static final int TRACKBALL_MOVE_COUNT = 10; private static final int TRACKBALL_MULTIPLIER = 3; private static final int SELECT_CURSOR_OFFSET = 16; private static final int SELECT_SCROLL = 5; private int mSelectX = 0; private int mSelectY = 0; private boolean mTrackballDown = false; private long mTrackballUpTime = 0; private long mLastCursorTime = 0; private Rect mLastCursorBounds; private SelectionHandleAlpha mBaseAlpha = new SelectionHandleAlpha(); private SelectionHandleAlpha mExtentAlpha = new SelectionHandleAlpha(); private ObjectAnimator mBaseHandleAlphaAnimator = ObjectAnimator.ofInt(mBaseAlpha, "alpha", 0); private ObjectAnimator mExtentHandleAlphaAnimator = ObjectAnimator.ofInt(mExtentAlpha, "alpha", 0); // Set by default; BrowserActivity clears to interpret trackball data // directly for movement. Currently, the framework only passes // arrow key events, not trackball events, from one child to the next private boolean mMapTrackballToArrowKeys = true; private DrawData mDelaySetPicture; private DrawData mLoadedPicture; @Override public void setMapTrackballToArrowKeys(boolean setMap) { mMapTrackballToArrowKeys = setMap; } void resetTrackballTime() { mTrackballLastTime = 0; } @Override public boolean onTrackballEvent(MotionEvent ev) { long time = ev.getEventTime(); if ((ev.getMetaState() & KeyEvent.META_ALT_ON) != 0) { if (ev.getY() > 0) pageDown(true); if (ev.getY() < 0) pageUp(true); return true; } if (ev.getAction() == MotionEvent.ACTION_DOWN) { if (mSelectingText) { return true; // discard press if copy in progress } mTrackballDown = true; if (mNativeClass == 0) { return false; } if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent down ev=" + ev + " time=" + time + " mLastCursorTime=" + mLastCursorTime); } if (mWebView.isInTouchMode()) mWebView.requestFocusFromTouch(); return false; // let common code in onKeyDown at it } if (ev.getAction() == MotionEvent.ACTION_UP) { // LONG_PRESS_CENTER is set in common onKeyDown mPrivateHandler.removeMessages(LONG_PRESS_CENTER); mTrackballDown = false; mTrackballUpTime = time; if (mSelectingText) { copySelection(); selectionDone(); return true; // discard press if copy in progress } if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent up ev=" + ev + " time=" + time ); } return false; // let common code in onKeyUp at it } if ((mMapTrackballToArrowKeys && (ev.getMetaState() & KeyEvent.META_SHIFT_ON) == 0) || AccessibilityManager.getInstance(mContext).isEnabled()) { if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent gmail quit"); return false; } if (mTrackballDown) { if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent down quit"); return true; // discard move if trackball is down } if (time - mTrackballUpTime < TRACKBALL_TIMEOUT) { if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent up timeout quit"); return true; } // TODO: alternatively we can do panning as touch does switchOutDrawHistory(); if (time - mTrackballLastTime > TRACKBALL_TIMEOUT) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent time=" + time + " last=" + mTrackballLastTime); } mTrackballFirstTime = time; mTrackballXMove = mTrackballYMove = 0; } mTrackballLastTime = time; if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent ev=" + ev + " time=" + time); } mTrackballRemainsX += ev.getX(); mTrackballRemainsY += ev.getY(); doTrackball(time, ev.getMetaState()); return true; } private int scaleTrackballX(float xRate, int width) { int xMove = (int) (xRate / TRACKBALL_SCALE * width); int nextXMove = xMove; if (xMove > 0) { if (xMove > mTrackballXMove) { xMove -= mTrackballXMove; } } else if (xMove < mTrackballXMove) { xMove -= mTrackballXMove; } mTrackballXMove = nextXMove; return xMove; } private int scaleTrackballY(float yRate, int height) { int yMove = (int) (yRate / TRACKBALL_SCALE * height); int nextYMove = yMove; if (yMove > 0) { if (yMove > mTrackballYMove) { yMove -= mTrackballYMove; } } else if (yMove < mTrackballYMove) { yMove -= mTrackballYMove; } mTrackballYMove = nextYMove; return yMove; } private int keyCodeToSoundsEffect(int keyCode) { switch(keyCode) { case KeyEvent.KEYCODE_DPAD_UP: return SoundEffectConstants.NAVIGATION_UP; case KeyEvent.KEYCODE_DPAD_RIGHT: return SoundEffectConstants.NAVIGATION_RIGHT; case KeyEvent.KEYCODE_DPAD_DOWN: return SoundEffectConstants.NAVIGATION_DOWN; case KeyEvent.KEYCODE_DPAD_LEFT: return SoundEffectConstants.NAVIGATION_LEFT; } return 0; } private void doTrackball(long time, int metaState) { int elapsed = (int) (mTrackballLastTime - mTrackballFirstTime); if (elapsed == 0) { elapsed = TRACKBALL_TIMEOUT; } float xRate = mTrackballRemainsX * 1000 / elapsed; float yRate = mTrackballRemainsY * 1000 / elapsed; int viewWidth = getViewWidth(); int viewHeight = getViewHeight(); float ax = Math.abs(xRate); float ay = Math.abs(yRate); float maxA = Math.max(ax, ay); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doTrackball elapsed=" + elapsed + " xRate=" + xRate + " yRate=" + yRate + " mTrackballRemainsX=" + mTrackballRemainsX + " mTrackballRemainsY=" + mTrackballRemainsY); } int width = mContentWidth - viewWidth; int height = mContentHeight - viewHeight; if (width < 0) width = 0; if (height < 0) height = 0; ax = Math.abs(mTrackballRemainsX * TRACKBALL_MULTIPLIER); ay = Math.abs(mTrackballRemainsY * TRACKBALL_MULTIPLIER); maxA = Math.max(ax, ay); int count = Math.max(0, (int) maxA); int oldScrollX = getScrollX(); int oldScrollY = getScrollY(); if (count > 0) { int selectKeyCode = ax < ay ? mTrackballRemainsY < 0 ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN : mTrackballRemainsX < 0 ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT; count = Math.min(count, TRACKBALL_MOVE_COUNT); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doTrackball keyCode=" + selectKeyCode + " count=" + count + " mTrackballRemainsX=" + mTrackballRemainsX + " mTrackballRemainsY=" + mTrackballRemainsY); } if (mNativeClass != 0) { for (int i = 0; i < count; i++) { letPageHandleNavKey(selectKeyCode, time, true, metaState); } letPageHandleNavKey(selectKeyCode, time, false, metaState); } mTrackballRemainsX = mTrackballRemainsY = 0; } if (count >= TRACKBALL_SCROLL_COUNT) { int xMove = scaleTrackballX(xRate, width); int yMove = scaleTrackballY(yRate, height); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doTrackball pinScrollBy" + " count=" + count + " xMove=" + xMove + " yMove=" + yMove + " mScrollX-oldScrollX=" + (getScrollX()-oldScrollX) + " mScrollY-oldScrollY=" + (getScrollY()-oldScrollY) ); } if (Math.abs(getScrollX() - oldScrollX) > Math.abs(xMove)) { xMove = 0; } if (Math.abs(getScrollY() - oldScrollY) > Math.abs(yMove)) { yMove = 0; } if (xMove != 0 || yMove != 0) { pinScrollBy(xMove, yMove, true, 0); } } } /** * Compute the maximum horizontal scroll position. Used by {@link OverScrollGlow}. * @return Maximum horizontal scroll position within real content */ int computeMaxScrollX() { return Math.max(computeRealHorizontalScrollRange() - getViewWidth(), 0); } /** * Compute the maximum vertical scroll position. Used by {@link OverScrollGlow}. * @return Maximum vertical scroll position within real content */ int computeMaxScrollY() { return Math.max(computeRealVerticalScrollRange() + getTitleHeight() - getViewHeightWithTitle(), 0); } boolean updateScrollCoordinates(int x, int y) { int oldX = getScrollX(); int oldY = getScrollY(); setScrollXRaw(x); setScrollYRaw(y); if (oldX != getScrollX() || oldY != getScrollY()) { mWebViewPrivate.onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); return true; } else { return false; } } @Override public void flingScroll(int vx, int vy) { mScroller.fling(getScrollX(), getScrollY(), vx, vy, 0, computeMaxScrollX(), 0, computeMaxScrollY(), mOverflingDistance, mOverflingDistance); invalidate(); } private void doFling() { if (mVelocityTracker == null) { return; } int maxX = computeMaxScrollX(); int maxY = computeMaxScrollY(); mVelocityTracker.computeCurrentVelocity(1000, mMaximumFling); int vx = (int) mVelocityTracker.getXVelocity(); int vy = (int) mVelocityTracker.getYVelocity(); int scrollX = getScrollX(); int scrollY = getScrollY(); int overscrollDistance = mOverscrollDistance; int overflingDistance = mOverflingDistance; // Use the layer's scroll data if applicable. if (mTouchMode == TOUCH_DRAG_LAYER_MODE) { scrollX = mScrollingLayerRect.left; scrollY = mScrollingLayerRect.top; maxX = mScrollingLayerRect.right; maxY = mScrollingLayerRect.bottom; // No overscrolling for layers. overscrollDistance = overflingDistance = 0; } else if (mTouchMode == TOUCH_DRAG_TEXT_MODE) { scrollX = getTextScrollX(); scrollY = getTextScrollY(); maxX = getMaxTextScrollX(); maxY = getMaxTextScrollY(); // No overscrolling for edit text. overscrollDistance = overflingDistance = 0; } if (mSnapScrollMode != SNAP_NONE) { if ((mSnapScrollMode & SNAP_X) == SNAP_X) { vy = 0; } else { vx = 0; } } if ((maxX == 0 && vy == 0) || (maxY == 0 && vx == 0)) { WebViewCore.resumePriority(); if (!mSelectingText) { WebViewCore.resumeUpdatePicture(mWebViewCore); } if (mScroller.springBack(scrollX, scrollY, 0, maxX, 0, maxY)) { invalidate(); } return; } float currentVelocity = mScroller.getCurrVelocity(); float velocity = (float) Math.hypot(vx, vy); if (mLastVelocity > 0 && currentVelocity > 0 && velocity > mLastVelocity * MINIMUM_VELOCITY_RATIO_FOR_ACCELERATION) { float deltaR = (float) (Math.abs(Math.atan2(mLastVelY, mLastVelX) - Math.atan2(vy, vx))); final float circle = (float) (Math.PI) * 2.0f; if (deltaR > circle * 0.9f || deltaR < circle * 0.1f) { vx += currentVelocity * mLastVelX / mLastVelocity; vy += currentVelocity * mLastVelY / mLastVelocity; velocity = (float) Math.hypot(vx, vy); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doFling vx= " + vx + " vy=" + vy); } } else if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doFling missed " + deltaR / circle); } } else if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doFling start last=" + mLastVelocity + " current=" + currentVelocity + " vx=" + vx + " vy=" + vy + " maxX=" + maxX + " maxY=" + maxY + " scrollX=" + scrollX + " scrollY=" + scrollY + " layer=" + mCurrentScrollingLayerId); } // Allow sloppy flings without overscrolling at the edges. if ((scrollX == 0 || scrollX == maxX) && Math.abs(vx) < Math.abs(vy)) { vx = 0; } if ((scrollY == 0 || scrollY == maxY) && Math.abs(vy) < Math.abs(vx)) { vy = 0; } if (overscrollDistance < overflingDistance) { if ((vx > 0 && scrollX == -overscrollDistance) || (vx < 0 && scrollX == maxX + overscrollDistance)) { vx = 0; } if ((vy > 0 && scrollY == -overscrollDistance) || (vy < 0 && scrollY == maxY + overscrollDistance)) { vy = 0; } } mLastVelX = vx; mLastVelY = vy; mLastVelocity = velocity; // no horizontal overscroll if the content just fits mScroller.fling(scrollX, scrollY, -vx, -vy, 0, maxX, 0, maxY, maxX == 0 ? 0 : overflingDistance, overflingDistance); invalidate(); } /** * See {@link WebView#getZoomControls()} */ @Override @Deprecated public View getZoomControls() { if (!getSettings().supportZoom()) { Log.w(LOGTAG, "This WebView doesn't support zoom."); return null; } return mZoomManager.getExternalZoomPicker(); } void dismissZoomControl() { mZoomManager.dismissZoomPicker(); } float getDefaultZoomScale() { return mZoomManager.getDefaultScale(); } /** * Return the overview scale of the WebView * @return The overview scale. */ float getZoomOverviewScale() { return mZoomManager.getZoomOverviewScale(); } /** * See {@link WebView#canZoomIn()} */ @Override public boolean canZoomIn() { return mZoomManager.canZoomIn(); } /** * See {@link WebView#canZoomOut()} */ @Override public boolean canZoomOut() { return mZoomManager.canZoomOut(); } /** * See {@link WebView#zoomIn()} */ @Override public boolean zoomIn() { return mZoomManager.zoomIn(); } /** * See {@link WebView#zoomOut()} */ @Override public boolean zoomOut() { return mZoomManager.zoomOut(); } /* * Return true if the rect (e.g. plugin) is fully visible and maximized * inside the WebView. */ boolean isRectFitOnScreen(Rect rect) { final int rectWidth = rect.width(); final int rectHeight = rect.height(); final int viewWidth = getViewWidth(); final int viewHeight = getViewHeightWithTitle(); float scale = Math.min((float) viewWidth / rectWidth, (float) viewHeight / rectHeight); scale = mZoomManager.computeScaleWithLimits(scale); return !mZoomManager.willScaleTriggerZoom(scale) && contentToViewX(rect.left) >= getScrollX() && contentToViewX(rect.right) <= getScrollX() + viewWidth && contentToViewY(rect.top) >= getScrollY() && contentToViewY(rect.bottom) <= getScrollY() + viewHeight; } /* * Maximize and center the rectangle, specified in the document coordinate * space, inside the WebView. If the zoom doesn't need to be changed, do an * animated scroll to center it. If the zoom needs to be changed, find the * zoom center and do a smooth zoom transition. The rect is in document * coordinates */ void centerFitRect(Rect rect) { final int rectWidth = rect.width(); final int rectHeight = rect.height(); final int viewWidth = getViewWidth(); final int viewHeight = getViewHeightWithTitle(); float scale = Math.min((float) viewWidth / rectWidth, (float) viewHeight / rectHeight); scale = mZoomManager.computeScaleWithLimits(scale); if (!mZoomManager.willScaleTriggerZoom(scale)) { pinScrollTo(contentToViewX(rect.left + rectWidth / 2) - viewWidth / 2, contentToViewY(rect.top + rectHeight / 2) - viewHeight / 2, true, 0); } else { float actualScale = mZoomManager.getScale(); float oldScreenX = rect.left * actualScale - getScrollX(); float rectViewX = rect.left * scale; float rectViewWidth = rectWidth * scale; float newMaxWidth = mContentWidth * scale; float newScreenX = (viewWidth - rectViewWidth) / 2; // pin the newX to the WebView if (newScreenX > rectViewX) { newScreenX = rectViewX; } else if (newScreenX > (newMaxWidth - rectViewX - rectViewWidth)) { newScreenX = viewWidth - (newMaxWidth - rectViewX); } float zoomCenterX = (oldScreenX * scale - newScreenX * actualScale) / (scale - actualScale); float oldScreenY = rect.top * actualScale + getTitleHeight() - getScrollY(); float rectViewY = rect.top * scale + getTitleHeight(); float rectViewHeight = rectHeight * scale; float newMaxHeight = mContentHeight * scale + getTitleHeight(); float newScreenY = (viewHeight - rectViewHeight) / 2; // pin the newY to the WebView if (newScreenY > rectViewY) { newScreenY = rectViewY; } else if (newScreenY > (newMaxHeight - rectViewY - rectViewHeight)) { newScreenY = viewHeight - (newMaxHeight - rectViewY); } float zoomCenterY = (oldScreenY * scale - newScreenY * actualScale) / (scale - actualScale); mZoomManager.setZoomCenter(zoomCenterX, zoomCenterY); mZoomManager.startZoomAnimation(scale, false); } } // Called by JNI to handle a touch on a node representing an email address, // address, or phone number private void overrideLoading(String url) { mCallbackProxy.uiOverrideUrlLoading(url); } @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { // Check if we are destroyed if (mWebViewCore == null) return false; // FIXME: If a subwindow is showing find, and the user touches the // background window, it can steal focus. if (mFindIsUp) return false; boolean result = false; result = mWebViewPrivate.super_requestFocus(direction, previouslyFocusedRect); if (mWebViewCore.getSettings().getNeedInitialFocus() && !mWebView.isInTouchMode()) { // For cases such as GMail, where we gain focus from a direction, // we want to move to the first available link. // FIXME: If there are no visible links, we may not want to int fakeKeyDirection = 0; switch(direction) { case View.FOCUS_UP: fakeKeyDirection = KeyEvent.KEYCODE_DPAD_UP; break; case View.FOCUS_DOWN: fakeKeyDirection = KeyEvent.KEYCODE_DPAD_DOWN; break; case View.FOCUS_LEFT: fakeKeyDirection = KeyEvent.KEYCODE_DPAD_LEFT; break; case View.FOCUS_RIGHT: fakeKeyDirection = KeyEvent.KEYCODE_DPAD_RIGHT; break; default: return result; } mWebViewCore.sendMessage(EventHub.SET_INITIAL_FOCUS, fakeKeyDirection); } return result; } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int measuredHeight = heightSize; int measuredWidth = widthSize; // Grab the content size from WebViewCore. int contentHeight = contentToViewDimension(mContentHeight); int contentWidth = contentToViewDimension(mContentWidth); // Log.d(LOGTAG, "------- measure " + heightMode); if (heightMode != MeasureSpec.EXACTLY) { mHeightCanMeasure = true; measuredHeight = contentHeight; if (heightMode == MeasureSpec.AT_MOST) { // If we are larger than the AT_MOST height, then our height can // no longer be measured and we should scroll internally. if (measuredHeight > heightSize) { measuredHeight = heightSize; mHeightCanMeasure = false; measuredHeight |= View.MEASURED_STATE_TOO_SMALL; } } } else { mHeightCanMeasure = false; } if (mNativeClass != 0) { nativeSetHeightCanMeasure(mHeightCanMeasure); } // For the width, always use the given size unless unspecified. if (widthMode == MeasureSpec.UNSPECIFIED) { mWidthCanMeasure = true; measuredWidth = contentWidth; } else { if (measuredWidth < contentWidth) { measuredWidth |= View.MEASURED_STATE_TOO_SMALL; } mWidthCanMeasure = false; } synchronized (this) { mWebViewPrivate.setMeasuredDimension(measuredWidth, measuredHeight); } } @Override public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { if (mNativeClass == 0) { return false; } // don't scroll while in zoom animation. When it is done, we will adjust // the necessary components if (mZoomManager.isFixedLengthAnimationInProgress()) { return false; } rect.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY()); Rect content = new Rect(viewToContentX(getScrollX()), viewToContentY(getScrollY()), viewToContentX(getScrollX() + getWidth() - mWebView.getVerticalScrollbarWidth()), viewToContentY(getScrollY() + getViewHeightWithTitle())); int screenTop = contentToViewY(content.top); int screenBottom = contentToViewY(content.bottom); int height = screenBottom - screenTop; int scrollYDelta = 0; if (rect.bottom > screenBottom) { int oneThirdOfScreenHeight = height / 3; if (rect.height() > 2 * oneThirdOfScreenHeight) { // If the rectangle is too tall to fit in the bottom two thirds // of the screen, place it at the top. scrollYDelta = rect.top - screenTop; } else { // If the rectangle will still fit on screen, we want its // top to be in the top third of the screen. scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight); } } else if (rect.top < screenTop) { scrollYDelta = rect.top - screenTop; } int screenLeft = contentToViewX(content.left); int screenRight = contentToViewX(content.right); int width = screenRight - screenLeft; int scrollXDelta = 0; if (rect.right > screenRight && rect.left > screenLeft) { if (rect.width() > width) { scrollXDelta += (rect.left - screenLeft); } else { scrollXDelta += (rect.right - screenRight); } } else if (rect.left < screenLeft) { scrollXDelta -= (screenLeft - rect.left); } if ((scrollYDelta | scrollXDelta) != 0) { return pinScrollBy(scrollXDelta, scrollYDelta, !immediate, 0); } return false; } /* package */ void replaceTextfieldText(int oldStart, int oldEnd, String replace, int newStart, int newEnd) { WebViewCore.ReplaceTextData arg = new WebViewCore.ReplaceTextData(); arg.mReplace = replace; arg.mNewStart = newStart; arg.mNewEnd = newEnd; mTextGeneration++; arg.mTextGeneration = mTextGeneration; sendBatchableInputMessage(EventHub.REPLACE_TEXT, oldStart, oldEnd, arg); } /* package */ void passToJavaScript(String currentText, KeyEvent event) { // check if mWebViewCore has been destroyed if (mWebViewCore == null) { return; } WebViewCore.JSKeyData arg = new WebViewCore.JSKeyData(); arg.mEvent = event; arg.mCurrentText = currentText; // Increase our text generation number, and pass it to webcore thread mTextGeneration++; mWebViewCore.sendMessage(EventHub.PASS_TO_JS, mTextGeneration, 0, arg); // WebKit's document state is not saved until about to leave the page. // To make sure the host application, like Browser, has the up to date // document state when it goes to background, we force to save the // document state. mWebViewCore.removeMessages(EventHub.SAVE_DOCUMENT_STATE); mWebViewCore.sendMessageDelayed(EventHub.SAVE_DOCUMENT_STATE, null, 1000); } public synchronized WebViewCore getWebViewCore() { return mWebViewCore; } private boolean canTextScroll(int directionX, int directionY) { int scrollX = getTextScrollX(); int scrollY = getTextScrollY(); int maxScrollX = getMaxTextScrollX(); int maxScrollY = getMaxTextScrollY(); boolean canScrollX = (directionX > 0) ? (scrollX < maxScrollX) : (scrollX > 0); boolean canScrollY = (directionY > 0) ? (scrollY < maxScrollY) : (scrollY > 0); return canScrollX || canScrollY; } private int getTextScrollX() { return -mEditTextContent.left; } private int getTextScrollY() { return -mEditTextContent.top; } private int getMaxTextScrollX() { return Math.max(0, mEditTextContent.width() - mEditTextContentBounds.width()); } private int getMaxTextScrollY() { return Math.max(0, mEditTextContent.height() - mEditTextContentBounds.height()); } //------------------------------------------------------------------------- // Methods can be called from a separate thread, like WebViewCore // If it needs to call the View system, it has to send message. //------------------------------------------------------------------------- /** * General handler to receive message coming from webkit thread */ class PrivateHandler extends Handler implements WebViewInputDispatcher.UiCallbacks { @Override public void handleMessage(Message msg) { // exclude INVAL_RECT_MSG_ID since it is frequently output if (DebugFlags.WEB_VIEW && msg.what != INVAL_RECT_MSG_ID) { if (msg.what >= FIRST_PRIVATE_MSG_ID && msg.what <= LAST_PRIVATE_MSG_ID) { Log.v(LOGTAG, HandlerPrivateDebugString[msg.what - FIRST_PRIVATE_MSG_ID]); } else if (msg.what >= FIRST_PACKAGE_MSG_ID && msg.what <= LAST_PACKAGE_MSG_ID) { Log.v(LOGTAG, HandlerPackageDebugString[msg.what - FIRST_PACKAGE_MSG_ID]); } else { Log.v(LOGTAG, Integer.toString(msg.what)); } } if (mWebViewCore == null) { // after WebView's destroy() is called, skip handling messages. return; } if (mBlockWebkitViewMessages && msg.what != WEBCORE_INITIALIZED_MSG_ID) { // Blocking messages from webkit return; } switch (msg.what) { case REMEMBER_PASSWORD: { mDatabase.setUsernamePassword( msg.getData().getString("host"), msg.getData().getString("username"), msg.getData().getString("password")); ((Message) msg.obj).sendToTarget(); break; } case NEVER_REMEMBER_PASSWORD: { mDatabase.setUsernamePassword(msg.getData().getString("host"), null, null); ((Message) msg.obj).sendToTarget(); break; } case SCROLL_SELECT_TEXT: { if (mAutoScrollX == 0 && mAutoScrollY == 0) { mSentAutoScrollMessage = false; break; } if (mCurrentScrollingLayerId == 0) { pinScrollBy(mAutoScrollX, mAutoScrollY, true, 0); } else { scrollLayerTo(mScrollingLayerRect.left + mAutoScrollX, mScrollingLayerRect.top + mAutoScrollY); } sendEmptyMessageDelayed( SCROLL_SELECT_TEXT, SELECT_SCROLL_INTERVAL); break; } case SCROLL_TO_MSG_ID: { // arg1 = animate, arg2 = onlyIfImeIsShowing // obj = Point(x, y) if (msg.arg2 == 1) { // This scroll is intended to bring the textfield into // view, but is only necessary if the IME is showing InputMethodManager imm = InputMethodManager.peekInstance(); if (imm == null || !imm.isAcceptingText() || !imm.isActive(mWebView)) { break; } } final Point p = (Point) msg.obj; contentScrollTo(p.x, p.y, msg.arg1 == 1); break; } case UPDATE_ZOOM_RANGE: { WebViewCore.ViewState viewState = (WebViewCore.ViewState) msg.obj; // mScrollX contains the new minPrefWidth mZoomManager.updateZoomRange(viewState, getViewWidth(), viewState.mScrollX); break; } case UPDATE_ZOOM_DENSITY: { final float density = (Float) msg.obj; mZoomManager.updateDefaultZoomDensity(density); break; } case NEW_PICTURE_MSG_ID: { // called for new content final WebViewCore.DrawData draw = (WebViewCore.DrawData) msg.obj; setNewPicture(draw, true); break; } case WEBCORE_INITIALIZED_MSG_ID: // nativeCreate sets mNativeClass to a non-zero value String drawableDir = BrowserFrame.getRawResFilename( BrowserFrame.DRAWABLEDIR, mContext); nativeCreate(msg.arg1, drawableDir, ActivityManager.isHighEndGfx()); if (mDelaySetPicture != null) { setNewPicture(mDelaySetPicture, true); mDelaySetPicture = null; } if (mIsPaused) { nativeSetPauseDrawing(mNativeClass, true); } mInputDispatcher = new WebViewInputDispatcher(this, mWebViewCore.getInputDispatcherCallbacks()); break; case UPDATE_TEXTFIELD_TEXT_MSG_ID: // Make sure that the textfield is currently focused // and representing the same node as the pointer. if (msg.arg2 == mTextGeneration) { String text = (String) msg.obj; if (null == text) { text = ""; } if (mInputConnection != null && mFieldPointer == msg.arg1) { mInputConnection.setTextAndKeepSelection(text); } } break; case UPDATE_TEXT_SELECTION_MSG_ID: updateTextSelectionFromMessage(msg.arg1, msg.arg2, (WebViewCore.TextSelectionData) msg.obj); break; case TAKE_FOCUS: int direction = msg.arg1; View focusSearch = mWebView.focusSearch(direction); if (focusSearch != null && focusSearch != mWebView) { focusSearch.requestFocus(); } break; case CLEAR_TEXT_ENTRY: hideSoftKeyboard(); break; case INVAL_RECT_MSG_ID: { Rect r = (Rect)msg.obj; if (r == null) { invalidate(); } else { // we need to scale r from content into view coords, // which viewInvalidate() does for us viewInvalidate(r.left, r.top, r.right, r.bottom); } break; } case REQUEST_FORM_DATA: if (mFieldPointer == msg.arg1) { ArrayAdapter adapter = (ArrayAdapter)msg.obj; mAutoCompletePopup.setAdapter(adapter); } break; case LONG_PRESS_CENTER: // as this is shared by keydown and trackballdown, reset all // the states mGotCenterDown = false; mTrackballDown = false; mWebView.performLongClick(); break; case WEBCORE_NEED_TOUCH_EVENTS: mInputDispatcher.setWebKitWantsTouchEvents(msg.arg1 != 0); break; case REQUEST_KEYBOARD: if (msg.arg1 == 0) { hideSoftKeyboard(); } else { displaySoftKeyboard(false); } break; case DRAG_HELD_MOTIONLESS: mHeldMotionless = MOTIONLESS_TRUE; invalidate(); break; case SCREEN_ON: mWebView.setKeepScreenOn(msg.arg1 == 1); break; case EXIT_FULLSCREEN_VIDEO: if (mHTML5VideoViewProxy != null) { mHTML5VideoViewProxy.exitFullScreenVideo(); } break; case SHOW_FULLSCREEN: { View view = (View) msg.obj; int orientation = msg.arg1; int npp = msg.arg2; if (inFullScreenMode()) { Log.w(LOGTAG, "Should not have another full screen."); dismissFullScreenMode(); } mFullScreenHolder = new PluginFullScreenHolder(WebViewClassic.this, orientation, npp); mFullScreenHolder.setContentView(view); mFullScreenHolder.show(); invalidate(); break; } case HIDE_FULLSCREEN: dismissFullScreenMode(); break; case SHOW_RECT_MSG_ID: { WebViewCore.ShowRectData data = (WebViewCore.ShowRectData) msg.obj; int left = contentToViewX(data.mLeft); int width = contentToViewDimension(data.mWidth); int maxWidth = contentToViewDimension(data.mContentWidth); int viewWidth = getViewWidth(); int x = (int) (left + data.mXPercentInDoc * width - data.mXPercentInView * viewWidth); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "showRectMsg=(left=" + left + ",width=" + width + ",maxWidth=" + maxWidth + ",viewWidth=" + viewWidth + ",x=" + x + ",xPercentInDoc=" + data.mXPercentInDoc + ",xPercentInView=" + data.mXPercentInView+ ")"); } // use the passing content width to cap x as the current // mContentWidth may not be updated yet x = Math.max(0, (Math.min(maxWidth, x + viewWidth)) - viewWidth); int top = contentToViewY(data.mTop); int height = contentToViewDimension(data.mHeight); int maxHeight = contentToViewDimension(data.mContentHeight); int viewHeight = getViewHeight(); int y = (int) (top + data.mYPercentInDoc * height - data.mYPercentInView * viewHeight); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "showRectMsg=(top=" + top + ",height=" + height + ",maxHeight=" + maxHeight + ",viewHeight=" + viewHeight + ",y=" + y + ",yPercentInDoc=" + data.mYPercentInDoc + ",yPercentInView=" + data.mYPercentInView+ ")"); } // use the passing content height to cap y as the current // mContentHeight may not be updated yet y = Math.max(0, (Math.min(maxHeight, y + viewHeight) - viewHeight)); // We need to take into account the visible title height // when scrolling since y is an absolute view position. y = Math.max(0, y - getVisibleTitleHeightImpl()); mWebView.scrollTo(x, y); } break; case CENTER_FIT_RECT: centerFitRect((Rect)msg.obj); break; case SET_SCROLLBAR_MODES: mHorizontalScrollBarMode = msg.arg1; mVerticalScrollBarMode = msg.arg2; break; case SELECTION_STRING_CHANGED: if (isAccessibilityInjectionEnabled()) { getAccessibilityInjector() .handleSelectionChangedIfNecessary((String) msg.obj); } break; case FOCUS_NODE_CHANGED: mIsEditingText = (msg.arg1 == mFieldPointer); if (mAutoCompletePopup != null && !mIsEditingText) { mAutoCompletePopup.clearAdapter(); } // fall through to HIT_TEST_RESULT case HIT_TEST_RESULT: WebKitHitTest hit = (WebKitHitTest) msg.obj; mFocusedNode = hit; setTouchHighlightRects(hit); setHitTestResult(hit); break; case SAVE_WEBARCHIVE_FINISHED: SaveWebArchiveMessage saveMessage = (SaveWebArchiveMessage)msg.obj; if (saveMessage.mCallback != null) { saveMessage.mCallback.onReceiveValue(saveMessage.mResultFile); } break; case SET_AUTOFILLABLE: mAutoFillData = (WebViewCore.AutoFillData) msg.obj; if (mInputConnection != null) { mInputConnection.setAutoFillable(mAutoFillData.getQueryId()); mAutoCompletePopup.setAutoFillQueryId(mAutoFillData.getQueryId()); } break; case AUTOFILL_COMPLETE: if (mAutoCompletePopup != null) { ArrayList pastEntries = new ArrayList(); mAutoCompletePopup.setAdapter(new ArrayAdapter( mContext, com.android.internal.R.layout.web_text_view_dropdown, pastEntries)); } break; case COPY_TO_CLIPBOARD: copyToClipboard((String) msg.obj); break; case INIT_EDIT_FIELD: if (mInputConnection != null) { TextFieldInitData initData = (TextFieldInitData) msg.obj; mTextGeneration = 0; mFieldPointer = initData.mFieldPointer; mInputConnection.initEditorInfo(initData); mInputConnection.setTextAndKeepSelection(initData.mText); mEditTextContentBounds.set(initData.mContentBounds); mEditTextLayerId = initData.mNodeLayerId; nativeMapLayerRect(mNativeClass, mEditTextLayerId, mEditTextContentBounds); mEditTextContent.set(initData.mClientRect); relocateAutoCompletePopup(); } break; case REPLACE_TEXT:{ String text = (String)msg.obj; int start = msg.arg1; int end = msg.arg2; int cursorPosition = start + text.length(); replaceTextfieldText(start, end, text, cursorPosition, cursorPosition); selectionDone(); break; } case UPDATE_MATCH_COUNT: { WebViewCore.FindAllRequest request = (WebViewCore.FindAllRequest)msg.obj; if (request == null) { if (mFindCallback != null) { mFindCallback.updateMatchCount(0, 0, true); } } else if (request == mFindRequest) { int matchCount, matchIndex; synchronized (mFindRequest) { matchCount = request.mMatchCount; matchIndex = request.mMatchIndex; } if (mFindCallback != null) { mFindCallback.updateMatchCount(matchIndex, matchCount, false); } if (mFindListener != null) { mFindListener.onFindResultReceived(matchIndex, matchCount, true); } } break; } case CLEAR_CARET_HANDLE: if (mIsCaretSelection) { selectionDone(); } break; case KEY_PRESS: sendBatchableInputMessage(EventHub.KEY_PRESS, msg.arg1, 0, null); break; case RELOCATE_AUTO_COMPLETE_POPUP: relocateAutoCompletePopup(); break; case AUTOFILL_FORM: mWebViewCore.sendMessage(EventHub.AUTOFILL_FORM, msg.arg1, /* unused */0); break; case EDIT_TEXT_SIZE_CHANGED: if (msg.arg1 == mFieldPointer) { mEditTextContent.set((Rect)msg.obj); } break; case SHOW_CARET_HANDLE: if (!mSelectingText && mIsEditingText && mIsCaretSelection) { setupWebkitSelect(); resetCaretTimer(); showPasteWindow(); } break; case UPDATE_CONTENT_BOUNDS: mEditTextContentBounds.set((Rect) msg.obj); nativeMapLayerRect(mNativeClass, mEditTextLayerId, mEditTextContentBounds); break; case SCROLL_EDIT_TEXT: scrollEditWithCursor(); break; case SCROLL_HANDLE_INTO_VIEW: scrollDraggedSelectionHandleIntoView(); break; default: super.handleMessage(msg); break; } } @Override public Looper getUiLooper() { return getLooper(); } @Override public void dispatchUiEvent(MotionEvent event, int eventType, int flags) { onHandleUiEvent(event, eventType, flags); } @Override public Context getContext() { return WebViewClassic.this.getContext(); } @Override public boolean shouldInterceptTouchEvent(MotionEvent event) { if (!mSelectingText) { return false; } ensureSelectionHandles(); int y = Math.round(event.getY() - getTitleHeight() + getScrollY()); int x = Math.round(event.getX() + getScrollX()); boolean isPressingHandle; if (mIsCaretSelection) { isPressingHandle = mSelectHandleCenter.getBounds() .contains(x, y); } else { isPressingHandle = mSelectHandleBaseBounds.contains(x, y) || mSelectHandleExtentBounds.contains(x, y); } return isPressingHandle; } @Override public void showTapHighlight(boolean show) { if (mShowTapHighlight != show) { mShowTapHighlight = show; invalidate(); } } @Override public void clearPreviousHitTest() { setHitTestResult(null); } } private void setHitTestTypeFromUrl(String url) { String substr = null; if (url.startsWith(SCHEME_GEO)) { mInitialHitTestResult.setType(HitTestResult.GEO_TYPE); substr = url.substring(SCHEME_GEO.length()); } else if (url.startsWith(SCHEME_TEL)) { mInitialHitTestResult.setType(HitTestResult.PHONE_TYPE); substr = url.substring(SCHEME_TEL.length()); } else if (url.startsWith(SCHEME_MAILTO)) { mInitialHitTestResult.setType(HitTestResult.EMAIL_TYPE); substr = url.substring(SCHEME_MAILTO.length()); } else { mInitialHitTestResult.setType(HitTestResult.SRC_ANCHOR_TYPE); mInitialHitTestResult.setExtra(url); return; } try { mInitialHitTestResult.setExtra(URLDecoder.decode(substr, "UTF-8")); } catch (Throwable e) { Log.w(LOGTAG, "Failed to decode URL! " + substr, e); mInitialHitTestResult.setType(HitTestResult.UNKNOWN_TYPE); } } private void setHitTestResult(WebKitHitTest hit) { if (hit == null) { mInitialHitTestResult = null; return; } mInitialHitTestResult = new HitTestResult(); if (hit.mLinkUrl != null) { setHitTestTypeFromUrl(hit.mLinkUrl); if (hit.mImageUrl != null && mInitialHitTestResult.getType() == HitTestResult.SRC_ANCHOR_TYPE) { mInitialHitTestResult.setType(HitTestResult.SRC_IMAGE_ANCHOR_TYPE); mInitialHitTestResult.setExtra(hit.mImageUrl); } } else if (hit.mImageUrl != null) { mInitialHitTestResult.setType(HitTestResult.IMAGE_TYPE); mInitialHitTestResult.setExtra(hit.mImageUrl); } else if (hit.mEditable) { mInitialHitTestResult.setType(HitTestResult.EDIT_TEXT_TYPE); } else if (hit.mIntentUrl != null) { setHitTestTypeFromUrl(hit.mIntentUrl); } } private boolean shouldDrawHighlightRect() { if (mFocusedNode == null || mInitialHitTestResult == null) { return false; } if (mTouchHighlightRegion.isEmpty()) { return false; } if (mFocusedNode.mHasFocus && !mWebView.isInTouchMode()) { return mDrawCursorRing && !mFocusedNode.mEditable; } if (mFocusedNode.mHasFocus && mFocusedNode.mEditable) { return false; } return mShowTapHighlight; } private FocusTransitionDrawable mFocusTransition = null; static class FocusTransitionDrawable extends Drawable { Region mPreviousRegion; Region mNewRegion; float mProgress = 0; WebViewClassic mWebView; Paint mPaint; int mMaxAlpha; Point mTranslate; public FocusTransitionDrawable(WebViewClassic view) { mWebView = view; mPaint = new Paint(mWebView.mTouchHightlightPaint); mMaxAlpha = mPaint.getAlpha(); } @Override public void setColorFilter(ColorFilter cf) { } @Override public void setAlpha(int alpha) { } @Override public int getOpacity() { return 0; } public void setProgress(float p) { mProgress = p; if (mWebView.mFocusTransition == this) { if (mProgress == 1f) mWebView.mFocusTransition = null; mWebView.invalidate(); } } public float getProgress() { return mProgress; } @Override public void draw(Canvas canvas) { if (mTranslate == null) { Rect bounds = mPreviousRegion.getBounds(); Point from = new Point(bounds.centerX(), bounds.centerY()); mNewRegion.getBounds(bounds); Point to = new Point(bounds.centerX(), bounds.centerY()); mTranslate = new Point(from.x - to.x, from.y - to.y); } int alpha = (int) (mProgress * mMaxAlpha); RegionIterator iter = new RegionIterator(mPreviousRegion); Rect r = new Rect(); mPaint.setAlpha(mMaxAlpha - alpha); float tx = mTranslate.x * mProgress; float ty = mTranslate.y * mProgress; int save = canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.translate(-tx, -ty); while (iter.next(r)) { canvas.drawRect(r, mPaint); } canvas.restoreToCount(save); iter = new RegionIterator(mNewRegion); r = new Rect(); mPaint.setAlpha(alpha); save = canvas.save(Canvas.MATRIX_SAVE_FLAG); tx = mTranslate.x - tx; ty = mTranslate.y - ty; canvas.translate(tx, ty); while (iter.next(r)) { canvas.drawRect(r, mPaint); } canvas.restoreToCount(save); } }; private boolean shouldAnimateTo(WebKitHitTest hit) { // TODO: Don't be annoying or throw out the animation entirely return false; } private void setTouchHighlightRects(WebKitHitTest hit) { FocusTransitionDrawable transition = null; if (shouldAnimateTo(hit)) { transition = new FocusTransitionDrawable(this); } Rect[] rects = hit != null ? hit.mTouchRects : null; if (!mTouchHighlightRegion.isEmpty()) { mWebView.invalidate(mTouchHighlightRegion.getBounds()); if (transition != null) { transition.mPreviousRegion = new Region(mTouchHighlightRegion); } mTouchHighlightRegion.setEmpty(); } if (rects != null) { mTouchHightlightPaint.setColor(hit.mTapHighlightColor); for (Rect rect : rects) { Rect viewRect = contentToViewRect(rect); // some sites, like stories in nytimes.com, set // mouse event handler in the top div. It is not // user friendly to highlight the div if it covers // more than half of the screen. if (viewRect.width() < getWidth() >> 1 || viewRect.height() < getHeight() >> 1) { mTouchHighlightRegion.union(viewRect); } else if (DebugFlags.WEB_VIEW) { Log.d(LOGTAG, "Skip the huge selection rect:" + viewRect); } } mWebView.invalidate(mTouchHighlightRegion.getBounds()); if (transition != null && transition.mPreviousRegion != null) { transition.mNewRegion = new Region(mTouchHighlightRegion); mFocusTransition = transition; ObjectAnimator animator = ObjectAnimator.ofFloat( mFocusTransition, "progress", 1f); animator.start(); } } } // Interface to allow the profiled WebView to hook the page swap notifications. public interface PageSwapDelegate { void onPageSwapOccurred(boolean notifyAnimationStarted); } long mLastSwapTime; double mAverageSwapFps; /** Called by JNI when pages are swapped (only occurs with hardware * acceleration) */ protected void pageSwapCallback(boolean notifyAnimationStarted) { if (DebugFlags.MEASURE_PAGE_SWAP_FPS) { long now = System.currentTimeMillis(); long diff = now - mLastSwapTime; mAverageSwapFps = ((1000.0 / diff) + mAverageSwapFps) / 2; Log.d(LOGTAG, "page swap fps: " + mAverageSwapFps); mLastSwapTime = now; } mWebViewCore.resumeWebKitDraw(); if (notifyAnimationStarted) { mWebViewCore.sendMessage(EventHub.NOTIFY_ANIMATION_STARTED); } if (mWebView instanceof PageSwapDelegate) { // This provides a hook for ProfiledWebView to observe the tile page swaps. ((PageSwapDelegate) mWebView).onPageSwapOccurred(notifyAnimationStarted); } if (mPictureListener != null) { // trigger picture listener for hardware layers. Software layers are // triggered in setNewPicture mPictureListener.onNewPicture(getWebView(), capturePicture()); } } void setNewPicture(final WebViewCore.DrawData draw, boolean updateBaseLayer) { if (mNativeClass == 0) { if (mDelaySetPicture != null) { throw new IllegalStateException("Tried to setNewPicture with" + " a delay picture already set! (memory leak)"); } // Not initialized yet, delay set mDelaySetPicture = draw; return; } WebViewCore.ViewState viewState = draw.mViewState; boolean isPictureAfterFirstLayout = viewState != null; if (updateBaseLayer) { setBaseLayer(draw.mBaseLayer, getSettings().getShowVisualIndicator(), isPictureAfterFirstLayout); } final Point viewSize = draw.mViewSize; // We update the layout (i.e. request a layout from the // view system) if the last view size that we sent to // WebCore matches the view size of the picture we just // received in the fixed dimension. final boolean updateLayout = viewSize.x == mLastWidthSent && viewSize.y == mLastHeightSent; // Don't send scroll event for picture coming from webkit, // since the new picture may cause a scroll event to override // the saved history scroll position. mSendScrollEvent = false; recordNewContentSize(draw.mContentSize.x, draw.mContentSize.y, updateLayout); if (isPictureAfterFirstLayout) { // Reset the last sent data here since dealing with new page. mLastWidthSent = 0; mZoomManager.onFirstLayout(draw); int scrollX = viewState.mShouldStartScrolledRight ? getContentWidth() : viewState.mScrollX; int scrollY = viewState.mScrollY; contentScrollTo(scrollX, scrollY, false); if (!mDrawHistory) { // As we are on a new page, hide the keyboard hideSoftKeyboard(); } } mSendScrollEvent = true; int functor = 0; boolean forceInval = isPictureAfterFirstLayout; ViewRootImpl viewRoot = mWebView.getViewRootImpl(); if (mWebView.isHardwareAccelerated() && mWebView.getLayerType() != View.LAYER_TYPE_SOFTWARE && viewRoot != null) { functor = nativeGetDrawGLFunction(mNativeClass); if (functor != 0) { // force an invalidate if functor attach not successful forceInval |= !viewRoot.attachFunctor(functor); } } if (functor == 0 || forceInval || mWebView.getLayerType() != View.LAYER_TYPE_NONE) { // invalidate the screen so that the next repaint will show new content // TODO: partial invalidate mWebView.invalidate(); } // update the zoom information based on the new picture if (mZoomManager.onNewPicture(draw)) invalidate(); if (isPictureAfterFirstLayout) { mViewManager.postReadyToDrawAll(); } scrollEditWithCursor(); if (mPictureListener != null) { if (!mWebView.isHardwareAccelerated() || mWebView.getLayerType() == View.LAYER_TYPE_SOFTWARE) { // trigger picture listener for software layers. Hardware layers are // triggered in pageSwapCallback mPictureListener.onNewPicture(getWebView(), capturePicture()); } } } /** * Used when receiving messages for REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID * and UPDATE_TEXT_SELECTION_MSG_ID. */ private void updateTextSelectionFromMessage(int nodePointer, int textGeneration, WebViewCore.TextSelectionData data) { if (textGeneration == mTextGeneration) { if (mInputConnection != null && mFieldPointer == nodePointer) { mInputConnection.setSelection(data.mStart, data.mEnd); } } nativeSetTextSelection(mNativeClass, data.mSelectTextPtr); if ((data.mSelectionReason == TextSelectionData.REASON_ACCESSIBILITY_INJECTOR) || (!mSelectingText && data.mStart != data.mEnd && data.mSelectionReason != TextSelectionData.REASON_SELECT_WORD)) { selectionDone(); mShowTextSelectionExtra = true; invalidate(); return; } if (data.mSelectTextPtr != 0 && (data.mStart != data.mEnd || (mFieldPointer == nodePointer && mFieldPointer != 0) || (nodePointer == 0 && data.mStart == 0 && data.mEnd == 0))) { mIsEditingText = (mFieldPointer == nodePointer) && nodePointer != 0; mIsCaretSelection = (data.mStart == data.mEnd && nodePointer != 0); if (mIsCaretSelection && (mInputConnection == null || mInputConnection.getEditable().length() == 0)) { // There's no text, don't show caret handle. selectionDone(); } else { if (!mSelectingText) { setupWebkitSelect(); } else { syncSelectionCursors(); } animateHandles(); if (mIsCaretSelection) { resetCaretTimer(); } } } else { selectionDone(); } invalidate(); } private void scrollEditText(int scrollX, int scrollY) { // Scrollable edit text. Scroll it. float maxScrollX = getMaxTextScrollX(); float scrollPercentX = ((float)scrollX)/maxScrollX; mEditTextContent.offsetTo(-scrollX, -scrollY); mWebViewCore.removeMessages(EventHub.SCROLL_TEXT_INPUT); mWebViewCore.sendMessage(EventHub.SCROLL_TEXT_INPUT, 0, scrollY, (Float)scrollPercentX); animateHandles(); } private void beginTextBatch() { mIsBatchingTextChanges = true; } private void commitTextBatch() { if (mWebViewCore != null) { mWebViewCore.sendMessages(mBatchedTextChanges); } mBatchedTextChanges.clear(); mIsBatchingTextChanges = false; } void sendBatchableInputMessage(int what, int arg1, int arg2, Object obj) { if (mWebViewCore == null) { return; } Message message = Message.obtain(null, what, arg1, arg2, obj); if (mIsBatchingTextChanges) { mBatchedTextChanges.add(message); } else { mWebViewCore.sendMessage(message); } } // Class used to use a dropdown for a element. * * @param array Labels for the listbox. * @param enabledArray State for each element in the list. See static * integers in Container class. * @param selection Which position is initally selected. */ void requestListBox(String[] array, int[] enabledArray, int selection) { mPrivateHandler.post( new InvokeListBox(array, enabledArray, selection)); } private int getScaledMaxXScroll() { int width; if (mHeightCanMeasure == false) { width = getViewWidth() / 4; } else { Rect visRect = new Rect(); calcOurVisibleRect(visRect); width = visRect.width() / 2; } // FIXME the divisor should be retrieved from somewhere return viewToContentX(width); } private int getScaledMaxYScroll() { int height; if (mHeightCanMeasure == false) { height = getViewHeight() / 4; } else { Rect visRect = new Rect(); calcOurVisibleRect(visRect); height = visRect.height() / 2; } // FIXME the divisor should be retrieved from somewhere // the closest thing today is hard-coded into ScrollView.java // (from ScrollView.java, line 363) int maxJump = height/2; return Math.round(height * mZoomManager.getInvScale()); } /** * Called by JNI to invalidate view */ private void viewInvalidate() { invalidate(); } /** * Pass the key directly to the page. This assumes that * nativePageShouldHandleShiftAndArrows() returned true. */ private void letPageHandleNavKey(int keyCode, long time, boolean down, int metaState) { int keyEventAction; if (down) { keyEventAction = KeyEvent.ACTION_DOWN; } else { keyEventAction = KeyEvent.ACTION_UP; } KeyEvent event = new KeyEvent(time, time, keyEventAction, keyCode, 1, (metaState & KeyEvent.META_SHIFT_ON) | (metaState & KeyEvent.META_ALT_ON) | (metaState & KeyEvent.META_SYM_ON) , KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0); sendKeyEvent(event); } private void sendKeyEvent(KeyEvent event) { int direction = 0; switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_DOWN: direction = View.FOCUS_DOWN; break; case KeyEvent.KEYCODE_DPAD_UP: direction = View.FOCUS_UP; break; case KeyEvent.KEYCODE_DPAD_LEFT: direction = View.FOCUS_LEFT; break; case KeyEvent.KEYCODE_DPAD_RIGHT: direction = View.FOCUS_RIGHT; break; case KeyEvent.KEYCODE_TAB: direction = event.isShiftPressed() ? View.FOCUS_BACKWARD : View.FOCUS_FORWARD; break; } if (direction != 0 && mWebView.focusSearch(direction) == null) { // Can't take focus in that direction direction = 0; } int eventHubAction = EventHub.KEY_UP; if (event.getAction() == KeyEvent.ACTION_DOWN) { eventHubAction = EventHub.KEY_DOWN; int sound = keyCodeToSoundsEffect(event.getKeyCode()); if (sound != 0) { mWebView.playSoundEffect(sound); } } sendBatchableInputMessage(eventHubAction, direction, 0, event); } /** * See {@link WebView#setBackgroundColor(int)} */ @Override public void setBackgroundColor(int color) { mBackgroundColor = color; mWebViewCore.sendMessage(EventHub.SET_BACKGROUND_COLOR, color); } /** * Enable the communication b/t the webView and VideoViewProxy * * only used by the Browser */ public void setHTML5VideoViewProxy(HTML5VideoViewProxy proxy) { mHTML5VideoViewProxy = proxy; } /** * Set the time to wait between passing touches to WebCore. See also the * TOUCH_SENT_INTERVAL member for further discussion. * * This is only used by the DRT test application. */ public void setTouchInterval(int interval) { mCurrentTouchInterval = interval; } /** * Copy text into the clipboard. This is called indirectly from * WebViewCore. * @param text The text to put into the clipboard. */ private void copyToClipboard(String text) { ClipboardManager cm = (ClipboardManager)mContext .getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText(getTitle(), text); cm.setPrimaryClip(clip); } /*package*/ void autoFillForm(int autoFillQueryId) { mPrivateHandler.obtainMessage(AUTOFILL_FORM, autoFillQueryId, 0) .sendToTarget(); } /* package */ ViewManager getViewManager() { return mViewManager; } /** send content invalidate */ protected void contentInvalidateAll() { if (mWebViewCore != null && !mBlockWebkitViewMessages) { mWebViewCore.sendMessage(EventHub.CONTENT_INVALIDATE_ALL); } } /** discard all textures from tiles. Used in Profiled WebView */ public void discardAllTextures() { nativeDiscardAllTextures(); } @Override public void setLayerType(int layerType, Paint paint) { updateHwAccelerated(); } private void updateHwAccelerated() { if (mNativeClass == 0) { return; } boolean hwAccelerated = false; if (mWebView.isHardwareAccelerated() && mWebView.getLayerType() != View.LAYER_TYPE_SOFTWARE) { hwAccelerated = true; } // result is of type LayerAndroid::InvalidateFlags, non zero means invalidate/redraw int result = nativeSetHwAccelerated(mNativeClass, hwAccelerated); if (mWebViewCore != null && !mBlockWebkitViewMessages && result != 0) { mWebViewCore.contentDraw(); } } /** * Begin collecting per-tile profiling data * * only used by profiling tests */ public void tileProfilingStart() { nativeTileProfilingStart(); } /** * Return per-tile profiling data * * only used by profiling tests */ public float tileProfilingStop() { return nativeTileProfilingStop(); } /** only used by profiling tests */ public void tileProfilingClear() { nativeTileProfilingClear(); } /** only used by profiling tests */ public int tileProfilingNumFrames() { return nativeTileProfilingNumFrames(); } /** only used by profiling tests */ public int tileProfilingNumTilesInFrame(int frame) { return nativeTileProfilingNumTilesInFrame(frame); } /** only used by profiling tests */ public int tileProfilingGetInt(int frame, int tile, String key) { return nativeTileProfilingGetInt(frame, tile, key); } /** only used by profiling tests */ public float tileProfilingGetFloat(int frame, int tile, String key) { return nativeTileProfilingGetFloat(frame, tile, key); } /** * Checks the focused content for an editable text field. This can be * text input or ContentEditable. * @return true if the focused item is an editable text field. */ boolean focusCandidateIsEditableText() { if (mFocusedNode != null) { return mFocusedNode.mEditable; } return false; } // Called via JNI private void postInvalidate() { mWebView.postInvalidate(); } // Note: must be called before first WebViewClassic is created. public static void setShouldMonitorWebCoreThread() { WebViewCore.setShouldMonitorWebCoreThread(); } @Override public void dumpViewHierarchyWithProperties(BufferedWriter out, int level) { int layer = getBaseLayer(); if (layer != 0) { try { ByteArrayOutputStream stream = new ByteArrayOutputStream(); ViewStateSerializer.dumpLayerHierarchy(layer, stream, level); stream.close(); byte[] buf = stream.toByteArray(); out.write(new String(buf, "ascii")); } catch (IOException e) {} } } @Override public View findHierarchyView(String className, int hashCode) { if (mNativeClass == 0) return null; Picture pic = new Picture(); if (!nativeDumpLayerContentToPicture(mNativeClass, className, hashCode, pic)) { return null; } return new PictureWrapperView(getContext(), pic, mWebView); } private static class PictureWrapperView extends View { Picture mPicture; WebView mWebView; public PictureWrapperView(Context context, Picture picture, WebView parent) { super(context); mPicture = picture; mWebView = parent; setWillNotDraw(false); setRight(mPicture.getWidth()); setBottom(mPicture.getHeight()); } @Override protected void onDraw(Canvas canvas) { canvas.drawPicture(mPicture); } @Override public boolean post(Runnable action) { return mWebView.post(action); } } private native void nativeCreate(int ptr, String drawableDir, boolean isHighEndGfx); private native void nativeDebugDump(); private static native void nativeDestroy(int ptr); private native void nativeDraw(Canvas canvas, RectF visibleRect, int color, int extra); private native void nativeDumpDisplayTree(String urlOrNull); private native boolean nativeEvaluateLayersAnimations(int nativeInstance); private native int nativeCreateDrawGLFunction(int nativeInstance, Rect invScreenRect, Rect screenRect, RectF visibleContentRect, float scale, int extras); private native int nativeGetDrawGLFunction(int nativeInstance); private native void nativeUpdateDrawGLFunction(int nativeInstance, Rect invScreenRect, Rect screenRect, RectF visibleContentRect, float scale); private native String nativeGetSelection(); private native void nativeSetHeightCanMeasure(boolean measure); private native boolean nativeSetBaseLayer(int nativeInstance, int layer, boolean showVisualIndicator, boolean isPictureAfterFirstLayout, int scrollingLayer); private native int nativeGetBaseLayer(int nativeInstance); private native void nativeCopyBaseContentToPicture(Picture pict); private native boolean nativeDumpLayerContentToPicture(int nativeInstance, String className, int layerId, Picture pict); private native boolean nativeHasContent(); private native void nativeStopGL(int ptr); private native void nativeDiscardAllTextures(); private native void nativeTileProfilingStart(); private native float nativeTileProfilingStop(); private native void nativeTileProfilingClear(); private native int nativeTileProfilingNumFrames(); private native int nativeTileProfilingNumTilesInFrame(int frame); private native int nativeTileProfilingGetInt(int frame, int tile, String key); private native float nativeTileProfilingGetFloat(int frame, int tile, String key); private native void nativeUseHardwareAccelSkia(boolean enabled); // Returns a pointer to the scrollable LayerAndroid at the given point. private native int nativeScrollableLayer(int nativeInstance, int x, int y, Rect scrollRect, Rect scrollBounds); /** * Scroll the specified layer. * @param nativeInstance Native WebView instance * @param layer Id of the layer to scroll, as determined by nativeScrollableLayer. * @param newX Destination x position to which to scroll. * @param newY Destination y position to which to scroll. * @return True if the layer is successfully scrolled. */ private native boolean nativeScrollLayer(int nativeInstance, int layer, int newX, int newY); private native void nativeSetIsScrolling(boolean isScrolling); private native int nativeGetBackgroundColor(int nativeInstance); native boolean nativeSetProperty(String key, String value); native String nativeGetProperty(String key); /** * See {@link ComponentCallbacks2} for the trim levels and descriptions */ private static native void nativeOnTrimMemory(int level); private static native void nativeSetPauseDrawing(int instance, boolean pause); private static native void nativeSetTextSelection(int instance, int selection); private static native int nativeGetHandleLayerId(int instance, int handle, Point cursorLocation, QuadF textQuad); private static native void nativeMapLayerRect(int instance, int layerId, Rect rect); // Returns 1 if a layer sync is needed, else 0 private static native int nativeSetHwAccelerated(int instance, boolean hwAccelerated); private static native void nativeFindMaxVisibleRect(int instance, int layerId, Rect visibleContentRect); private static native boolean nativeIsHandleLeft(int instance, int handleId); private static native boolean nativeIsPointVisible(int instance, int layerId, int contentX, int contentY); }