| // Copyright 2010-2015, 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.accessibility; |
| |
| import org.mozc.android.inputmethod.japanese.MozcLog; |
| import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction; |
| import org.mozc.android.inputmethod.japanese.keyboard.Key; |
| import org.mozc.android.inputmethod.japanese.keyboard.KeyState; |
| import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState; |
| import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; |
| import org.mozc.android.inputmethod.japanese.keyboard.Row; |
| import org.mozc.android.inputmethod.japanese.resources.R; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| |
| import android.content.Context; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.support.v4.view.ViewCompat; |
| import android.support.v4.view.accessibility.AccessibilityEventCompat; |
| import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; |
| import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; |
| import android.support.v4.view.accessibility.AccessibilityRecordCompat; |
| import android.view.View; |
| import android.view.accessibility.AccessibilityEvent; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Set; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Represents keyboard's virtual structure. |
| * |
| * <p>Note about virtual view ID: This class uses {@code Key}'s {@code sourceId} |
| * as virtual view ID. It is changed by metastate of the keyboard. |
| */ |
| class KeyboardAccessibilityNodeProvider extends AccessibilityNodeProviderCompat { |
| |
| @VisibleForTesting static final int UNDEFINED = Integer.MIN_VALUE; |
| @VisibleForTesting static final int PASSWORD_RESOURCE_ID = |
| R.string.cd_key_uchar_katakana_middle_dot; |
| |
| // View for keyboard. |
| private final View view; |
| // Keyboard model. |
| private Optional<Keyboard> keyboard = Optional.absent(); |
| // Keys in the keyboard. |
| // Don't access directly. Use #getKeys() instead for lazy creation. |
| private Optional<Collection<Key>> keys = Optional.absent(); |
| // Virtual ID of focused (in the light of accessibility) view. |
| private int virtualFocusedViewId = UNDEFINED; |
| private Set<MetaState> metaState = Collections.emptySet(); |
| // If true content description of printable characters are not spoken. |
| // For password field. |
| private boolean shouldObscureInput = false; |
| |
| KeyboardAccessibilityNodeProvider(View view) { |
| this.view = Preconditions.checkNotNull(view); |
| } |
| |
| private Context getContext() { |
| return view.getContext(); |
| } |
| |
| /** |
| * Returns all the keys in the {@code keyboard}. |
| * |
| * <p>Lazy creation is done inside. |
| * <p>If {@code keyboard} is not set, empty collection is returned. |
| */ |
| private Collection<Key> getKeys() { |
| if (keys.isPresent()) { |
| return keys.get(); |
| } |
| if (!keyboard.isPresent()) { |
| return Collections.emptyList(); |
| } |
| // Initial size is estimated roughly. |
| Collection<Key> tempKeys = new ArrayList<Key>(keyboard.get().getRowList().size() * 10); |
| for (Row row : keyboard.get().getRowList()) { |
| for (Key key : row.getKeyList()) { |
| if (!key.isSpacer()) { |
| tempKeys.add(key); |
| } |
| } |
| } |
| keys = Optional.of(tempKeys); |
| return tempKeys; |
| } |
| |
| /** |
| * Returns a {@code Key} based on given position. |
| * |
| * @param x horizontal location in screen coordinate (pixel) |
| * @param y vertical location in screen coordinate (pixel) |
| */ |
| Optional<Key> getKey(int x, int y) { |
| for (Key key : getKeys()) { |
| int left = key.getX(); |
| if (left > x) { |
| continue; |
| } |
| int right = left + key.getWidth(); |
| if (right <= x) { |
| continue; |
| } |
| int top = key.getY(); |
| if (top > y) { |
| continue; |
| } |
| int bottom = top + key.getHeight(); |
| if (bottom <= y) { |
| continue; |
| } |
| return Optional.of(key); |
| } |
| return Optional.absent(); |
| } |
| |
| |
| @Override |
| @Nullable |
| public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) { |
| if (virtualViewId == UNDEFINED) { |
| return null; |
| } |
| if (virtualViewId == View.NO_ID) { |
| // Required to return the information about keyboardView. |
| AccessibilityNodeInfoCompat info = |
| AccessibilityNodeInfoCompat.obtain(view); |
| if (info == null) { |
| // In old Android OS AccessibilityNodeInfoCompat.obtain() returns null. |
| return null; |
| } |
| Preconditions.checkNotNull(info); |
| ViewCompat.onInitializeAccessibilityNodeInfo(view, info); |
| // Add the virtual children of the root View. |
| for (Key key : getKeys()) { |
| info.addChild(view, getSourceId(key)); |
| } |
| return info; |
| } |
| // Required to return the information about child view (== key). |
| // Find the view that corresponds to the given id. |
| Optional<Key> optionalKey = getKeyFromSouceId(virtualViewId); |
| if (!optionalKey.isPresent()) { |
| MozcLog.e("Virtual view id " + virtualViewId + " is not found"); |
| return null; |
| } |
| Key key = optionalKey.get(); |
| Rect boundsInParent = |
| new Rect(key.getX(), key.getY(), |
| key.getX() + key.getWidth(), key.getY() + key.getHeight()); |
| int[] parentLocationOnScreen = new int[2]; |
| view.getLocationOnScreen(parentLocationOnScreen); |
| Rect boundsInScreen = new Rect(boundsInParent); |
| boundsInScreen.offset(parentLocationOnScreen[0], parentLocationOnScreen[1]); |
| |
| AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(); |
| if (info == null) { |
| // In old Android OS AccessibilityNodeInfoCompat.obtain() returns null. |
| return null; |
| } |
| info.setPackageName(getContext().getPackageName()); |
| info.setClassName(key.getClass().getName()); |
| info.setContentDescription(getContentDescription(key).orNull()); |
| info.setBoundsInParent(boundsInParent); |
| info.setBoundsInScreen(boundsInScreen); |
| info.setParent(view); |
| info.setSource(view, virtualViewId); |
| info.setEnabled(true); |
| info.setVisibleToUser(true); |
| |
| if (virtualFocusedViewId == virtualViewId) { |
| info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); |
| } else { |
| info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); |
| } |
| return info; |
| } |
| |
| private static KeyState getKeyState(Key key, Set<MetaState> metaState) { |
| Preconditions.checkArgument(!key.isSpacer()); |
| return key.getKeyState(metaState).get(); |
| } |
| |
| /** |
| * Returns source id of the given {@code key} unmodified-center |
| */ |
| private int getSourceId(Key key) { |
| Preconditions.checkNotNull(key); |
| if (key.isSpacer()) { |
| return UNDEFINED; |
| } |
| return getKeyState(key, metaState).getFlick( |
| Direction.CENTER).get().getKeyEntity().getSourceId(); |
| } |
| |
| private Optional<Integer> getKeyCode(Key key) { |
| Preconditions.checkNotNull(key); |
| if (key.isSpacer()) { |
| return Optional.absent(); |
| } |
| return Optional.of(getKeyState(key, metaState).getFlick( |
| Direction.CENTER).get().getKeyEntity().getKeyCode()); |
| } |
| |
| /** |
| * Returns {@code Key} from source Id. |
| */ |
| private Optional<Key> getKeyFromSouceId(int sourceId) { |
| if (!keyboard.isPresent()) { |
| return Optional.absent(); |
| } |
| for (Row row : keyboard.get().getRowList()) { |
| for (Key key : row.getKeyList()) { |
| if (sourceId == getSourceId(key)) { |
| return Optional.of(key); |
| } |
| } |
| } |
| return Optional.absent(); |
| } |
| |
| /** |
| * Creates a {@code AccessibilityEvent} from {@code Key} and {@code eventType}. |
| */ |
| private AccessibilityEvent createAccessibilityEvent(Key key, int eventType) { |
| Preconditions.checkNotNull(key); |
| |
| AccessibilityEvent event = AccessibilityEvent.obtain(eventType); |
| event.setPackageName(getContext().getPackageName()); |
| event.setClassName(key.getClass().getName()); |
| event.setContentDescription(getContentDescription(key).orNull()); |
| event.setEnabled(true); |
| AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); |
| record.setSource(view, getSourceId(key)); |
| return event; |
| } |
| |
| @Override |
| public boolean performAction(int virtualViewId, int action, Bundle arguments) { |
| Optional<Key> key = getKeyFromSouceId(virtualViewId); |
| return key.isPresent() |
| ? performActionForKeyInternal(key.get(), virtualViewId, action) |
| : false; |
| } |
| |
| boolean performActionForKey(Key key, int action) { |
| return performActionForKeyInternal(key, getSourceId(Preconditions.checkNotNull(key)), action); |
| } |
| |
| /** |
| * Processes accessibility action for key on virtual view structure. |
| */ |
| boolean performActionForKeyInternal(Key key, int virtualViewId, int action) { |
| switch (action) { |
| case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: |
| if (virtualFocusedViewId == virtualViewId) { |
| // If focused virtual view is unchanged, do nothing. |
| return false; |
| } |
| // Framework requires the keyboard to have focus. |
| // Return FOCUSED event to the framework as response. |
| virtualFocusedViewId = virtualViewId; |
| if (isAccessibilityEnabled()) { |
| AccessibilityUtil.sendAccessibilityEvent( |
| getContext(), |
| createAccessibilityEvent( |
| key, |
| AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED)); |
| } |
| return true; |
| case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: |
| // Framework requires the keyboard to clear focus. |
| // Return FOCUSE_CLEARED event to the framework as response. |
| if (virtualFocusedViewId != virtualViewId) { |
| return false; |
| } |
| virtualFocusedViewId = UNDEFINED; |
| if (isAccessibilityEnabled()) { |
| AccessibilityUtil.sendAccessibilityEvent( |
| getContext(), |
| createAccessibilityEvent( |
| key, |
| AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED)); |
| } |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| /** |
| * @return {@code true} if {@link android.view.accessibility.AccessibilityManager} is enabled |
| */ |
| private boolean isAccessibilityEnabled() { |
| return AccessibilityUtil.isAccessibilityEnabled(getContext()); |
| } |
| |
| void sendAccessibilityEventForKeyIfAccessibilityEnabled(Key key, int eventType) { |
| if (isAccessibilityEnabled()) { |
| AccessibilityEvent event = createAccessibilityEvent(key, eventType); |
| AccessibilityUtil.sendAccessibilityEvent(getContext(), event); |
| } |
| } |
| |
| void setKeyboard(Optional<Keyboard> keyboard) { |
| this.keyboard = Preconditions.checkNotNull(keyboard); |
| resetVirtualStructure(); |
| } |
| |
| void setMetaState(Set<MetaState> metaState) { |
| this.metaState = Preconditions.checkNotNull(metaState); |
| resetVirtualStructure(); |
| } |
| |
| void setObscureInput(boolean shouldObscureInput) { |
| this.shouldObscureInput = shouldObscureInput; |
| resetVirtualStructure(); |
| } |
| |
| private void resetVirtualStructure() { |
| keys = Optional.absent(); |
| if (isAccessibilityEnabled()) { |
| AccessibilityEvent event = AccessibilityEvent.obtain(); |
| ViewCompat.onInitializeAccessibilityEvent(view, event); |
| event.setEventType(AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); |
| AccessibilityUtil.sendAccessibilityEvent(getContext(), event); |
| } |
| } |
| |
| private Optional<String> getContentDescription(Key key) { |
| Preconditions.checkNotNull(key); |
| |
| if (!shouldObscureInput) { |
| return Optional.fromNullable(getKeyState(key, metaState).getContentDescription()); |
| } |
| Optional<Integer> optionalKeyCode = getKeyCode(key); |
| if (!optionalKeyCode.isPresent()) { |
| // Spacer |
| return null; |
| } |
| int code = optionalKeyCode.get(); |
| boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code); |
| |
| return isDefinedNonCtrl |
| ? Optional.of(getContext().getString(PASSWORD_RESOURCE_ID)) |
| : Optional.of(getKeyState(key, metaState).getContentDescription()); |
| } |
| } |