| // Copyright 2010-2014, Google Inc. |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| package org.mozc.android.inputmethod.japanese.keyboard; |
| |
| import org.mozc.android.inputmethod.japanese.MemoryManageable; |
| import org.mozc.android.inputmethod.japanese.accessibility.AccessibilityUtil; |
| import org.mozc.android.inputmethod.japanese.accessibility.KeyboardAccessibilityDelegate; |
| import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchAction; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent; |
| import org.mozc.android.inputmethod.japanese.resources.R; |
| import org.mozc.android.inputmethod.japanese.view.DrawableCache; |
| import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory; |
| import org.mozc.android.inputmethod.japanese.view.SkinType; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.Sets; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Shader.TileMode; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.os.Looper; |
| import android.support.v4.view.ViewCompat; |
| import android.text.InputType; |
| import android.util.AttributeSet; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.inputmethod.EditorInfo; |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.EnumSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Basic implementation of a keyboard's view. |
| * This class supports taps and flicks. The clients of this class can handle them via |
| * {@code KeyboardActionListener}. |
| * |
| */ |
| public class KeyboardView extends View implements MemoryManageable { |
| private final BackgroundDrawableFactory backgroundDrawableFactory = |
| new BackgroundDrawableFactory(getResources().getDisplayMetrics().density); |
| private final DrawableCache drawableCache = |
| new DrawableCache(new MozcDrawableFactory(getResources())); |
| private final PopUpPreview.Pool popupPreviewPool = |
| new PopUpPreview.Pool( |
| this, Looper.getMainLooper(), backgroundDrawableFactory, drawableCache); |
| private final long popupDismissDelay; |
| |
| private Keyboard keyboard; |
| // Do not update directly. Use setMetaState instead. |
| @VisibleForTesting Set<MetaState> metaState; |
| @VisibleForTesting final KeyboardViewBackgroundSurface backgroundSurface = |
| new KeyboardViewBackgroundSurface(backgroundDrawableFactory, drawableCache); |
| @VisibleForTesting boolean isKeyPressed; |
| |
| private final int keycodeSymbol; |
| private final float scaledDensity; |
| |
| private int flickSensitivity; |
| private boolean popupEnabled = true; |
| private SkinType skinType = SkinType.ORANGE_LIGHTGRAY; |
| |
| private final KeyboardAccessibilityDelegate accessibilityDelegate; |
| |
| // A map from pointerId to KeyEventContext. |
| // Note: the pointerId should be small integers, e.g. 0, 1, 2... So if it turned out |
| // that the usage of Map is heavy, we probably can replace this map by a List or |
| // an array. |
| // We use LinkedHashMap with accessOrder=false here, in order to ensure sending key events |
| // in the pressing order in flushPendingKeyEvent. |
| // Its initial capacity (16) and load factor (0.75) are just heuristics. |
| @VisibleForTesting public final Map<Integer, KeyEventContext> keyEventContextMap = |
| new LinkedHashMap<Integer, KeyEventContext>(16, 0.75f, false); |
| |
| private KeyEventHandler keyEventHandler = null; |
| |
| // This constructor is package private for this unit test. |
| public KeyboardView(Context context) { |
| super(context); |
| } |
| |
| public KeyboardView(Context context, AttributeSet attrSet) { |
| super(context, attrSet); |
| } |
| |
| public KeyboardView(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| } |
| |
| // Initializer shared by constructors. |
| { |
| Context context = getContext(); |
| Resources res = context.getResources(); |
| popupDismissDelay = res.getInteger(R.integer.config_popup_dismiss_delay); |
| keycodeSymbol = res.getInteger(R.integer.key_symbol); |
| scaledDensity = res.getDisplayMetrics().scaledDensity; |
| accessibilityDelegate = new KeyboardAccessibilityDelegate(this); |
| ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate); |
| setMetaStates(Collections.<MetaState>emptySet()); |
| } |
| |
| /** |
| * At the moment, it is not limited to, but we expected the range of the flickSensitivity |
| * is [-10,+10] inclusive. 0 is the keyboard default sensitivity. |
| */ |
| public void setFlickSensitivity(int flickSensitivity) { |
| this.flickSensitivity = flickSensitivity; |
| } |
| |
| public int getFlickSensitivity() { |
| return flickSensitivity; |
| } |
| |
| private float getFlickSensitivityInDip() { |
| // To adapt the flickSensitiy Level to actual length, we scale 1.5f heuristically. |
| return -flickSensitivity * 1.5f * scaledDensity; |
| } |
| |
| KeyEventContext getKeyEventContextByKey(Key key) { |
| for (KeyEventContext keyEventContext : keyEventContextMap.values()) { |
| if (keyEventContext != null && key == keyEventContext.key) { |
| return keyEventContext; |
| } |
| } |
| return null; |
| } |
| |
| private void disposeKeyEventContext(KeyEventContext keyEventContext) { |
| if (keyEventContext == null) { |
| return; |
| } |
| if (keyEventHandler != null) { |
| keyEventHandler.cancelDelayedKeyEvent(keyEventContext); |
| } |
| backgroundSurface.requestUpdateKey(keyEventContext.key, null); |
| if (popupEnabled) { |
| popupPreviewPool.releaseDelayed(keyEventContext.pointerId, popupDismissDelay); |
| } |
| } |
| |
| /** |
| * Reset the internal state of this view. |
| */ |
| public void resetState() { |
| // To re-render the key in the normal state, notify the background surface about it. |
| for (KeyEventContext keyEventContext : keyEventContextMap.values()) { |
| disposeKeyEventContext(keyEventContext); |
| } |
| keyEventContextMap.clear(); |
| } |
| |
| private void flushPendingKeyEvent(TouchEvent relativeTouchEvent) { |
| // Back up values and clear the map first to avoid stack overflow |
| // in case this method is invoked recursively from the callback. |
| // TODO(hidehiko): Refactor around keyEventHandler and keyEventContext. Also we should be |
| // able to refactor this method with resetState. |
| KeyEventContext[] keyEventContextArray = |
| keyEventContextMap.values().toArray(new KeyEventContext[keyEventContextMap.size()]); |
| keyEventContextMap.clear(); |
| KeyEventHandler keyEventHandler = this.keyEventHandler; |
| |
| for (KeyEventContext keyEventContext : keyEventContextArray) { |
| int keyCode = keyEventContext.getKeyCode(); |
| int pressedKeyCode = keyEventContext.getPressedKeyCode(); |
| disposeKeyEventContext(keyEventContext); |
| if (keyEventHandler != null) { |
| // Send relativeTouchEvent as well if exists. |
| List<TouchEvent> touchEventList = relativeTouchEvent == null |
| ? Collections.singletonList(keyEventContext.getTouchEvent()) |
| : Arrays.asList(relativeTouchEvent, keyEventContext.getTouchEvent()); |
| keyEventHandler.sendKey(keyCode, touchEventList); |
| keyEventHandler.sendRelease(pressedKeyCode); |
| } |
| } |
| } |
| |
| /** Set a given keyboard to this view, and send a request to update. */ |
| public void setKeyboard(Keyboard keyboard) { |
| flushPendingKeyEvent(null); |
| |
| this.keyboard = keyboard; |
| updateMetaStates(Collections.<MetaState>emptySet(), MetaState.CHAR_TYPE_EXCLUSIVE_GROUP); |
| accessibilityDelegate.setKeyboard(Optional.fromNullable(keyboard)); |
| this.drawableCache.clear(); |
| backgroundSurface.requestUpdateKeyboard(keyboard, metaState); |
| backgroundSurface.requestUpdateSize(keyboard.contentRight - keyboard.contentLeft, |
| keyboard.contentBottom - keyboard.contentTop); |
| invalidate(); |
| } |
| |
| public void setPopupEnabled(boolean popupEnabled) { |
| this.popupEnabled = popupEnabled; |
| if (!popupEnabled) { |
| // When popup up is disabled, release all resources for popup immediately. |
| popupPreviewPool.releaseAll(); |
| } |
| } |
| |
| public boolean isPopupEnabled() { |
| return popupEnabled; |
| } |
| |
| @Override |
| public void onDetachedFromWindow() { |
| backgroundSurface.reset(); |
| super.onDetachedFromWindow(); |
| } |
| |
| /** @return the current keyboard instance */ |
| public Keyboard getKeyboard() { |
| return keyboard; |
| } |
| |
| public void setSkinType(SkinType skinType) { |
| this.skinType = skinType; |
| drawableCache.setSkinType(skinType); |
| backgroundDrawableFactory.setSkinType(skinType); |
| resetBackground(); |
| if (keyboard != null) { |
| backgroundSurface.requestUpdateKeyboard(keyboard, metaState); |
| } |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void resetBackground() { |
| Optional<Drawable> optionalKeyboardBackground = |
| drawableCache.getDrawable(skinType.windowBackgroundResourceId); |
| if (!optionalKeyboardBackground.isPresent()) { |
| setBackgroundColor(Color.BLACK); // Set default background color. |
| } else { |
| Drawable keyboardBackground = optionalKeyboardBackground.get(); |
| if (keyboardBackground instanceof BitmapDrawable) { |
| // If the background is bitmap resource, set repeat mode. |
| BitmapDrawable.class.cast(keyboardBackground).setTileModeXY( |
| TileMode.REPEAT, TileMode.REPEAT); |
| } |
| setBackgroundDrawable(keyboardBackground); |
| } |
| } |
| |
| public void setKeyEventHandler(KeyEventHandler keyEventHandler) { |
| // This method needs to be invoked from a thread which the looper held by older keyEventHandler |
| // points. Otherwise, there can be inconsistent state. |
| KeyEventHandler oldKeyEventHandler = this.keyEventHandler; |
| if (oldKeyEventHandler != null) { |
| // Cancel pending key event messages sent by this view. |
| for (KeyEventContext keyEventContext : keyEventContextMap.values()) { |
| oldKeyEventHandler.cancelDelayedKeyEvent(keyEventContext); |
| } |
| } |
| |
| this.keyEventHandler = keyEventHandler; |
| accessibilityDelegate.setKeyEventHandler(Optional.fromNullable(keyEventHandler)); |
| } |
| |
| @Override |
| public void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| |
| if (keyboard == null) { |
| // We have nothing to do. |
| return; |
| } |
| |
| backgroundSurface.update(); |
| backgroundSurface.draw(canvas); |
| } |
| |
| private static int getPointerIndex(int action) { |
| return |
| (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; |
| } |
| |
| private void onDown(MotionEvent event) { |
| int pointerIndex = getPointerIndex(event.getAction()); |
| float x = event.getX(pointerIndex); |
| float y = event.getY(pointerIndex); |
| |
| Key key = getKeyByCoord(x, y); |
| if (key == null) { |
| // Just ignore if a key isn't found. |
| return; |
| } |
| |
| if (getKeyEventContextByKey(key) != null) { |
| // If the key is already pressed, we simply ignore event sequence related to this press. |
| return; |
| } |
| |
| // Create a new key event context. |
| int pointerId = event.getPointerId(pointerIndex); |
| float flickThreshold = Math.max(keyboard.getFlickThreshold() + getFlickSensitivityInDip(), 1); |
| KeyEventContext keyEventContext = new KeyEventContext( |
| key, pointerId, x, y, getWidth(), getHeight(), |
| flickThreshold * flickThreshold, metaState); |
| |
| Set<MetaState> nextMetaStates = keyEventContext.getNextMetaStates(metaState); |
| |
| if (!nextMetaStates.equals(metaState)) { |
| // This is a modifier key, so we toggle the keyboard UI at the press timing instead of |
| // releasing timing. It is in order to support multi-touch with the modifier key. |
| // For example, "SHIFT + a" multi-touch will produce 'A' keycode, not 'a' keycode. |
| |
| // At first, we flush all pending key event under the current (older) keyboard. |
| flushPendingKeyEvent(keyEventContext.getTouchEvent()); |
| |
| // And then set isKeyPressed flag to false, in order to reset keyboard to unmodified state |
| // after multi-touch key events. |
| // For example, we expect the metaState will be back to unmodified if a user types |
| // "SHIFT + a". |
| isKeyPressed = false; |
| |
| // Update the metaState and request to update the full keyboard image |
| // to update all key icons. |
| setMetaStates(nextMetaStates); |
| backgroundSurface.requestUpdateKeyboard(keyboard, nextMetaStates); |
| } else { |
| // Remember if a non-modifier key is pressed. |
| isKeyPressed = true; |
| |
| // Request to update the image of only this key on the view. |
| backgroundSurface.requestUpdateKey(key, keyEventContext.flickDirection); |
| } |
| |
| if (keyEventContextMap.put(pointerId, keyEventContext) != null) { |
| // keyEventContextMap contains older event. |
| // TODO(hidehiko): Switch to ignoring new event, or overwriting the old event |
| // not to show unknown exceptions to users. |
| throw new IllegalStateException("Conflicting keyEventContext is found: " + pointerId); |
| } |
| |
| // Show popup. |
| if (popupEnabled) { |
| popupPreviewPool.getInstance(pointerId) |
| .showIfNecessary(key, keyEventContext.getCurrentPopUp()); |
| } |
| |
| if (keyEventHandler != null) { |
| // Clear pending key events and overwrite by this press key's one. |
| for (KeyEventContext context : keyEventContextMap.values()) { |
| keyEventHandler.cancelDelayedKeyEvent(context); |
| } |
| keyEventHandler.maybeStartDelayedKeyEvent(keyEventContext); |
| |
| // Finally we send a notification to listeners. |
| keyEventHandler.sendPress(keyEventContext.getPressedKeyCode()); |
| } |
| } |
| |
| private void onUp(MotionEvent event) { |
| int pointerIndex = getPointerIndex(event.getAction()); |
| KeyEventContext keyEventContext = keyEventContextMap.remove(event.getPointerId(pointerIndex)); |
| if (keyEventContext == null) { |
| // No corresponding event is found, so we have nothing to do. |
| return; |
| } |
| |
| float x = event.getX(pointerIndex); |
| float y = event.getY(pointerIndex); |
| keyEventContext.update(x, y, TouchAction.TOUCH_UP, event.getEventTime() - event.getDownTime()); |
| |
| int keyCode = keyEventContext.getKeyCode(); |
| int pressedKeyCode = keyEventContext.getPressedKeyCode(); |
| disposeKeyEventContext(keyEventContext); |
| |
| if (keyEventHandler != null) { |
| // In multi touch event, CursorView and SymbolInputView can't show by not primary touch event |
| // because user may intend to input characters rapidly by multi touch, not change mode. |
| // TODO(yoichio): Move this logic to ViewManager. "In theory" this should be done |
| // in the class. |
| if (keyCode != KeyEntity.INVALID_KEY_CODE && |
| (keyCode != keycodeSymbol || event.getAction() == MotionEvent.ACTION_UP)) { |
| keyEventHandler.sendKey(keyCode, |
| Collections.singletonList(keyEventContext.getTouchEvent())); |
| } |
| keyEventHandler.sendRelease(pressedKeyCode); |
| } |
| |
| if (keyEventContext.isMetaStateToggleEvent()) { |
| if (isKeyPressed) { |
| // A user pressed at least one key with pressing modifier key, and then the user |
| // released this modifier key. So, we flush all pending events here, and |
| // reset the keyboard's meta state to unmodified. |
| flushPendingKeyEvent(keyEventContext.getTouchEvent()); |
| updateMetaStates(Collections.<MetaState>emptySet(), MetaState.CHAR_TYPE_EXCLUSIVE_GROUP); |
| backgroundSurface.requestUpdateKeyboard(keyboard, Collections.<MetaState>emptySet()); |
| } |
| } else { |
| if (!metaState.isEmpty() && keyEventContextMap.isEmpty()) { |
| Set<MetaState> nextMetaState = Sets.newEnumSet(metaState, MetaState.class); |
| for (MetaState state : metaState) { |
| if (state.isOneTimeMetaState) { |
| // The current state is one time only, and we hit a release non-modifier key event here. |
| // So, we reset the meta state to unmodified. |
| nextMetaState.remove(state); |
| } |
| } |
| if (!nextMetaState.equals(metaState)) { |
| setMetaStates(nextMetaState); |
| backgroundSurface.requestUpdateKeyboard(keyboard, Collections.<MetaState>emptySet()); |
| } |
| } |
| } |
| } |
| |
| private void onMove(MotionEvent event) { |
| int pointerCount = event.getPointerCount(); |
| for (int i = 0; i < pointerCount; ++i) { |
| KeyEventContext keyEventContext = keyEventContextMap.get(event.getPointerId(i)); |
| if (keyEventContext == null) { |
| continue; |
| } |
| |
| Key key = keyEventContext.key; |
| if (keyEventContext.update(event.getX(i), event.getY(i), TouchAction.TOUCH_MOVE, |
| event.getEventTime() - event.getDownTime())) { |
| // The key's state is updated from, at least, initial state, so we'll cancel the |
| // pending key events. |
| if (keyEventHandler != null) { |
| keyEventHandler.cancelDelayedKeyEvent(keyEventContext); |
| } |
| if (popupEnabled) { |
| popupPreviewPool.getInstance(keyEventContext.pointerId).showIfNecessary( |
| key, keyEventContext.getCurrentPopUp()); |
| } |
| } |
| backgroundSurface.requestUpdateKey(key, keyEventContext.flickDirection); |
| } |
| } |
| |
| // Note that event is not used but this function takes it to standardize the signature to |
| // other onXXX methods defined above. |
| private void onCancel(@SuppressWarnings("unused") MotionEvent event) { |
| resetState(); |
| if (keyEventHandler != null) { |
| keyEventHandler.sendCancel(); |
| } |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN: |
| case MotionEvent.ACTION_POINTER_DOWN: |
| onDown(event); |
| break; |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_POINTER_UP: |
| onUp(event); |
| break; |
| case MotionEvent.ACTION_MOVE: |
| onMove(event); |
| break; |
| case MotionEvent.ACTION_CANCEL: |
| onCancel(event); |
| break; |
| default: |
| // The event is not handled by this class. |
| return false; |
| } |
| |
| // The keyboard's state is somehow changed. Update the view. |
| invalidate(); |
| return true; |
| } |
| |
| /** |
| * Finds a key containing the given coordinate. |
| * @param x {@code x}-coordinate. |
| * @param y {@code y}-coordinate. |
| * @return A corresponding {@code Key} instance, or {@code null} if not found. |
| */ |
| @VisibleForTesting Key getKeyByCoord(float x, float y) { |
| if (y < 0 || keyboard == null || keyboard.getRowList().isEmpty()) { |
| return null; |
| } |
| |
| int rowBottom = 0; |
| Row lastRow = keyboard.getRowList().get(keyboard.getRowList().size() - 1); |
| for (Row row : keyboard.getRowList()) { |
| rowBottom += row.getHeight() + row.getVerticalGap(); |
| Key prevKey = null; |
| for (Key key : row.getKeyList()) { |
| if ((// Stick vertical gaps to the keys above. |
| y < key.getY() + key.getHeight() + row.getVerticalGap() || |
| // Or the key is at the bottom of the keyboard. |
| // Note: Some devices sense touch events of out-side of screen. |
| // So, for better user experiences, we return the bottom row |
| // if a user touches below the screen bottom boundary. |
| row == lastRow || |
| key.getY() + key.getHeight() >= keyboard.contentBottom) && |
| // Horizontal gap is included in the width, |
| // so we don't need to calculate horizontal gap in addition to width. |
| x < key.getX() + key.getWidth() && |
| // The following condition selects a key hit in A, C, or D |
| // (C and D are on the same key), and excludes a key hit in B. |
| // +---+---+ |
| // current row -> | A | C | |
| // +---+ | |
| // next row -> | B | D | |
| // +---+---+ |
| // The condition y < rowBottom allows hits on A and C, and the other |
| // condition key.getX() <= x allows hits on C and D but not B. |
| // Hence, the hits on B are excluded. |
| (y < rowBottom || key.getX() <= x)) { |
| if (!key.isSpacer()) { |
| return key; // Found a key. |
| } |
| |
| switch (key.getStick()) { |
| case LEFT: |
| if (prevKey != null) { |
| return prevKey; |
| } |
| break; |
| case EVEN: |
| // Split the spacer evenly, assuming we don't have any consecutive spacers. |
| if (x < key.getX() + key.getWidth() / 2 && prevKey != null) { |
| return prevKey; |
| } |
| break; |
| case RIGHT: |
| // Do nothing to delegate the target to the next one. |
| } |
| } |
| |
| if (!key.isSpacer()) { |
| prevKey = key; |
| } |
| } |
| |
| if ((y < rowBottom || row == lastRow) && prevKey != null) { |
| return prevKey; |
| } |
| } |
| |
| return null; // Not found. |
| } |
| |
| @Override |
| public void trimMemory() { |
| backgroundSurface.trimMemory(); |
| drawableCache.clear(); |
| popupPreviewPool.releaseAll(); |
| } |
| |
| @Override |
| public boolean dispatchTouchEvent(MotionEvent event) { |
| if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { |
| return accessibilityDelegate.dispatchTouchEvent(event); |
| } |
| return super.dispatchTouchEvent(event); |
| } |
| |
| @Override |
| public boolean dispatchHoverEvent(MotionEvent event) { |
| if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { |
| return accessibilityDelegate.dispatchHoverEvent(event); |
| } |
| return false; |
| } |
| |
| @VisibleForTesting |
| Set<MetaState> getMetaStates() { |
| return this.metaState; |
| } |
| |
| private void setMetaStates(Set<MetaState> metaState) { |
| Preconditions.checkNotNull(metaState); |
| Preconditions.checkArgument(MetaState.isValidSet(metaState)); |
| this.metaState = metaState; |
| accessibilityDelegate.setMetaState(metaState); |
| } |
| |
| public void updateMetaStates(Set<MetaState> addedMetaStates, Set<MetaState> removedMetaStates) { |
| Preconditions.checkNotNull(addedMetaStates); |
| Preconditions.checkNotNull(removedMetaStates); |
| |
| setMetaStates(Sets.union(Sets.difference(metaState, removedMetaStates), |
| addedMetaStates).immutableCopy()); |
| } |
| |
| public void setPasswordField(boolean isPasswordField) { |
| accessibilityDelegate.setPasswordField(isPasswordField); |
| } |
| |
| public void setEditorInfo(EditorInfo editorInfo) { |
| Preconditions.checkNotNull(editorInfo); |
| |
| Set<MetaState> metaStates = EnumSet.noneOf(MetaState.class); |
| switch (editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION) { |
| case EditorInfo.IME_ACTION_DONE: |
| metaStates.add(MetaState.ACTION_DONE); |
| break; |
| case EditorInfo.IME_ACTION_GO: |
| metaStates.add(MetaState.ACTION_GO); |
| break; |
| case EditorInfo.IME_ACTION_NEXT: |
| metaStates.add(MetaState.ACTION_NEXT); |
| break; |
| case EditorInfo.IME_ACTION_NONE: |
| metaStates.add(MetaState.ACTION_NONE); |
| break; |
| case EditorInfo.IME_ACTION_PREVIOUS: |
| metaStates.add(MetaState.ACTION_PREVIOUS); |
| break; |
| case EditorInfo.IME_ACTION_SEARCH: |
| metaStates.add(MetaState.ACTION_SEARCH); |
| break; |
| case EditorInfo.IME_ACTION_SEND: |
| metaStates.add(MetaState.ACTION_SEND); |
| break; |
| default: |
| // Do nothing |
| } |
| // InputType variation is *NOT* bit-fields in fact. |
| int clazz = editorInfo.inputType & InputType.TYPE_MASK_CLASS; |
| int variation = editorInfo.inputType & InputType.TYPE_MASK_VARIATION; |
| switch (clazz) { |
| case InputType.TYPE_CLASS_TEXT: |
| switch (variation) { |
| case InputType.TYPE_TEXT_VARIATION_URI: |
| metaStates.add(MetaState.VARIATION_URI); |
| break; |
| case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: |
| case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: |
| metaStates.add(MetaState.VARIATION_EMAIL_ADDRESS); |
| break; |
| default: |
| // Do nothing |
| } |
| break; |
| default: |
| // Do nothing |
| } |
| |
| updateMetaStates(metaStates, Sets.union(MetaState.ACTION_EXCLUSIVE_GROUP, |
| MetaState.VARIATION_EXCLUSIVE_GROUP)); |
| } |
| } |