blob: ac472048ec559804d48e01f79cb1d4b1dc67324a [file] [log] [blame]
// 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.Skin;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.Sets;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
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());
private final DrawableCache drawableCache = new DrawableCache(getResources());
private final PopUpPreview.Pool popupPreviewPool =
new PopUpPreview.Pool(
this, Looper.getMainLooper(), backgroundDrawableFactory, drawableCache);
private final long popupDismissDelay;
private Optional<Keyboard> keyboard = Optional.absent();
// Do not update directly. Use setMetaState instead.
@VisibleForTesting Set<MetaState> metaState;
@VisibleForTesting final KeyboardViewBackgroundSurface backgroundSurface =
new KeyboardViewBackgroundSurface(backgroundDrawableFactory, drawableCache);
@VisibleForTesting boolean isKeyPressed;
private final float scaledDensity;
private int flickSensitivity;
private boolean popupEnabled = true;
private final KeyboardAccessibilityDelegate accessibilityDelegate;
/**
* Decorator class for {@code Map} for {@code KeyEventContextMap}.
* <p>
* When the number of the content is changed, meta state "HANDLING_TOUCH_EVENT"
* is updated.
*/
private final class KeyEventContextMap extends ForwardingMap<Integer, KeyEventContext>{
private final Map<Integer, KeyEventContext> delegate;
private KeyEventContextMap(Map<Integer, KeyEventContext> delegate) {
this.delegate = delegate;
}
@Override
protected Map<Integer, KeyEventContext> delegate() {
return delegate;
}
@Override
public void clear() {
super.clear();
updateHandlingTouchEventMetaState();
}
@Override
public KeyEventContext put(Integer key, KeyEventContext value) {
KeyEventContext result = super.put(key, value);
updateHandlingTouchEventMetaState();
return result;
}
@Override
public void putAll(Map<? extends Integer, ? extends KeyEventContext> map) {
super.putAll(map);
updateHandlingTouchEventMetaState();
}
@Override
public KeyEventContext remove(Object object) {
KeyEventContext result = super.remove(object);
updateHandlingTouchEventMetaState();
return result;
}
private void updateHandlingTouchEventMetaState() {
if (keyEventContextMap.isEmpty()) {
updateMetaStates(Collections.<MetaState>emptySet(),
Collections.singleton(MetaState.HANDLING_TOUCH_EVENT));
} else {
updateMetaStates(Collections.singleton(MetaState.HANDLING_TOUCH_EVENT),
Collections.<MetaState>emptySet());
}
}
}
// 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 KeyEventContextMap(new LinkedHashMap<Integer, KeyEventContext>(16, 0.75f, false));
private Optional<KeyEventHandler> keyEventHandler = Optional.absent();
// 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);
scaledDensity = res.getDisplayMetrics().scaledDensity;
accessibilityDelegate = new KeyboardAccessibilityDelegate(
this, new KeyboardAccessibilityDelegate.TouchEventEmulator() {
@Override
public void emulateLongPress(Key key) {
Preconditions.checkNotNull(key);
emulateImpl(key, true);
}
@Override
public void emulateKeyInput(Key key) {
Preconditions.checkNotNull(key);
emulateImpl(key, false);
}
private void emulateImpl(Key key, boolean isLongPress) {
KeyEventContext keyEventContext = new KeyEventContext(key, 0, 0, 0, 0, 0, 0, metaState);
processKeyEventContextForOnDownEvent(keyEventContext);
if (isLongPress && keyEventHandler.isPresent()) {
keyEventHandler.get().handleMessageLongPress(keyEventContext);
}
processKeyEventContextForOnUpEvent(keyEventContext);
// Without the invalidation this view cannot know that its content
// has been updated.
invalidate();
}
});
ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate);
// Not sure if globe is really activated.
// However metastate requires GLOBE or NO_GLOBE state.
setMetaStates(EnumSet.of(MetaState.NO_GLOBE));
}
/**
* 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;
}
private Optional<KeyEventContext> getKeyEventContextByKey(Key key) {
Preconditions.checkNotNull(key);
for (KeyEventContext keyEventContext : keyEventContextMap.values()) {
if (key == keyEventContext.key) {
return Optional.of(keyEventContext);
}
}
return Optional.absent();
}
private void disposeKeyEventContext(KeyEventContext keyEventContext) {
Preconditions.checkNotNull(keyEventContext);
if (keyEventHandler.isPresent()) {
keyEventHandler.get().cancelDelayedKeyEvent(keyEventContext);
}
backgroundSurface.requestUpdateKey(keyEventContext.key, Optional.<Flick.Direction>absent());
if (popupEnabled || keyEventContext.longPressCallback.isPresent()) {
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(Optional<TouchEvent> relativeTouchEvent) {
Preconditions.checkNotNull(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();
for (KeyEventContext keyEventContext : keyEventContextArray) {
int keyCode = keyEventContext.getKeyCode();
int pressedKeyCode = keyEventContext.getPressedKeyCode();
disposeKeyEventContext(keyEventContext);
if (keyEventHandler.isPresent()) {
// Send relativeTouchEvent as well if exists.
// TODO(hsumita): Confirm that we can put null on touchEventList or not.
List<TouchEvent> touchEventList = relativeTouchEvent.isPresent()
? Arrays.asList(relativeTouchEvent.get(), keyEventContext.getTouchEvent().orNull())
: Collections.singletonList(keyEventContext.getTouchEvent().orNull());
keyEventHandler.get().sendKey(keyCode, touchEventList);
keyEventHandler.get().sendRelease(pressedKeyCode);
}
}
}
/** Set a given keyboard to this view, and send a request to update. */
public void setKeyboard(Keyboard keyboard) {
flushPendingKeyEvent(Optional.<TouchEvent>absent());
this.keyboard = Optional.of(keyboard);
updateMetaStates(Collections.<MetaState>emptySet(), MetaState.CHAR_TYPE_EXCLUSIVE_GROUP);
accessibilityDelegate.setKeyboard(this.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 Optional<Keyboard> getKeyboard() {
return keyboard;
}
@SuppressWarnings("deprecation")
public void setSkin(Skin skin) {
Preconditions.checkNotNull(skin);
drawableCache.setSkin(skin);
backgroundDrawableFactory.setSkin(skin);
if (keyboard.isPresent()) {
backgroundSurface.requestUpdateKeyboard(keyboard.get(), metaState);
}
setBackgroundDrawable(skin.windowBackgroundDrawable.getConstantState().newDrawable());
}
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.
Optional<KeyEventHandler> oldKeyEventHandler = this.keyEventHandler;
if (oldKeyEventHandler.isPresent()) {
// Cancel pending key event messages sent by this view.
for (KeyEventContext keyEventContext : keyEventContextMap.values()) {
oldKeyEventHandler.get().cancelDelayedKeyEvent(keyEventContext);
}
}
this.keyEventHandler = Optional.of(keyEventHandler);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!keyboard.isPresent()) {
// We have nothing to do.
return;
}
// Draw keyboard.
backgroundSurface.update();
backgroundSurface.draw(canvas);
}
@SuppressLint("InlinedApi")
private static int getPointerIndex(int action) {
return
(action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
}
private void onDown(MotionEvent event) {
Preconditions.checkState(keyboard.isPresent());
int pointerIndex = getPointerIndex(event.getAction());
float x = event.getX(pointerIndex);
float y = event.getY(pointerIndex);
Optional<Key> optionalKey = getKeyByCoord(x, y);
if (!optionalKey.isPresent()) {
// Just ignore if a key isn't found.
return;
}
if (getKeyEventContextByKey(optionalKey.get()).isPresent()) {
// 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.get().getFlickThreshold() + getFlickSensitivityInDip(), 1);
final KeyEventContext keyEventContext = new KeyEventContext(
optionalKey.get(), pointerId, x, y, getWidth(), getHeight(),
flickThreshold * flickThreshold, metaState);
// Show popup.
updatePopUp(keyEventContext, false);
Optional<KeyEntity> keyEntity =
KeyEventContext.getKeyEntity(keyEventContext.key, metaState,
Optional.of(Flick.Direction.CENTER));
if (keyEntity.isPresent() && keyEntity.get().getPopUp().isPresent()
&& !keyEntity.get().isLongPressTimeoutTrigger()) {
keyEventContext.setLongPressCallback(new Runnable() {
@Override
public void run() {
updatePopUp(keyEventContext, true);
}
});
}
// Process the KeyEventContext (e.g., sending messages to KeyEventHandler, updating the surface,
// flushing pending key events and so on)
processKeyEventContextForOnDownEvent(keyEventContext);
// keyEventContextMap contains older event.
// TODO(hidehiko): Switch to ignoring new event, or overwriting the old event
// not to show unknown exceptions to users.
Preconditions.checkState(
keyEventContextMap.put(pointerId, keyEventContext) == null,
"Conflicting keyEventContext is found: " + pointerId);
}
private void processKeyEventContextForOnDownEvent(
final KeyEventContext keyEventContext) {
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.get(), 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(keyEventContext.key,
Optional.of(keyEventContext.flickDirection));
}
if (keyEventHandler.isPresent()) {
// Clear pending key events and overwrite by this press key's one.
for (KeyEventContext context : keyEventContextMap.values()) {
keyEventHandler.get().cancelDelayedKeyEvent(context);
}
keyEventHandler.get().maybeStartDelayedKeyEvent(keyEventContext);
// Finally we send a notification to listeners.
keyEventHandler.get().sendPress(keyEventContext.getPressedKeyCode());
}
}
private void onUp(MotionEvent event) {
Preconditions.checkState(keyboard.isPresent());
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());
processKeyEventContextForOnUpEvent(keyEventContext);
}
private void processKeyEventContextForOnUpEvent(KeyEventContext keyEventContext) {
disposeKeyEventContext(keyEventContext);
int keyCode = keyEventContext.getKeyCode();
int pressedKeyCode = keyEventContext.getPressedKeyCode();
if (keyEventHandler.isPresent()) {
if (keyCode != KeyEntity.INVALID_KEY_CODE) {
// TODO(hsumita): Confirm that we can put null as a touch event or not.
keyEventHandler.get().sendKey(keyCode,
Collections.singletonList(keyEventContext.getTouchEvent().orNull()));
}
keyEventHandler.get().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.get(), 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.get(), 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())) {
if (keyEventHandler.isPresent()) {
// The key's state is updated from, at least, initial state, so we'll cancel the
// pending key events, and invoke new pending key events if necessary.
keyEventHandler.get().cancelDelayedKeyEvent(keyEventContext);
if (keyEventContext.flickDirection == Flick.Direction.CENTER) {
keyEventHandler.get().maybeStartDelayedKeyEvent(keyEventContext);
}
}
updatePopUp(keyEventContext, false);
}
backgroundSurface.requestUpdateKey(key, Optional.of(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.isPresent()) {
keyEventHandler.get().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 Optional.<Key>absent()} if not found.
*/
@VisibleForTesting Optional<Key> getKeyByCoord(float x, float y) {
if (y < 0 || !keyboard.isPresent() || keyboard.get().getRowList().isEmpty()) {
return Optional.absent();
}
List<Row> rowList = keyboard.get().getRowList();
int rowBottom = 0;
Row lastRow = rowList.get(rowList.size() - 1);
for (Row row : rowList) {
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.get().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 Optional.of(key); // Found a key.
}
switch (key.getStick()) {
case LEFT:
if (prevKey != null) {
return Optional.of(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 Optional.of(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 Optional.of(prevKey);
}
}
return Optional.absent(); // Not found.
}
@Override
public void trimMemory() {
backgroundSurface.trimMemory();
drawableCache.clear();
popupPreviewPool.releaseAll();
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Preconditions.checkNotNull(event);
if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
return accessibilityDelegate.dispatchTouchEvent(event);
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean dispatchHoverEvent(MotionEvent event) {
Preconditions.checkNotNull(event);
if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
return accessibilityDelegate.dispatchHoverEvent(event);
}
return false;
}
@VisibleForTesting
public 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);
backgroundSurface.requestMetaState(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());
invalidate();
}
public void setPasswordField(boolean isPasswordField) {
accessibilityDelegate.setPasswordField(isPasswordField);
}
public void setEditorInfo(EditorInfo editorInfo) {
Preconditions.checkNotNull(editorInfo);
Set<MetaState> metaStates = EnumSet.noneOf(MetaState.class);
// If IME_FLAG_NO_ENTER_ACTION is set, normal action icon should be shown.
if ((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0) {
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));
}
public void setGlobeButtonEnabled(boolean isGlobeButtonEnabled) {
if (isGlobeButtonEnabled) {
updateMetaStates(EnumSet.of(MetaState.GLOBE), EnumSet.of(MetaState.NO_GLOBE));
} else {
updateMetaStates(EnumSet.of(MetaState.NO_GLOBE), EnumSet.of(MetaState.GLOBE));
}
}
private void updatePopUp(KeyEventContext keyEventContext, boolean isDelayedPopUp) {
PopUpPreview popUpPreview = popupPreviewPool.getInstance(keyEventContext.pointerId);
// Even if popup is disabled by preference, delayed popup (== popup for long-press)
// is shown otherwise a user cannot know how long (s)he has to press the key
// to get a character corresponding to long-press.
if (popupEnabled || isDelayedPopUp) {
popUpPreview.showIfNecessary(
keyEventContext.key, keyEventContext.getCurrentPopUp(), isDelayedPopUp);
} else {
popUpPreview.dismiss();
}
}
}