diff options
author | Amith Yamasani <yamasani@google.com> | 2013-12-19 23:30:35 +0000 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2013-12-19 23:30:35 +0000 |
commit | 49782e46c0eb85a25ae2abcf80880c48dbab5aea (patch) | |
tree | 9fab2a40c41004d78b7001dc766d85f61d24f582 /services/accessibility | |
parent | 4dace6f66d498c2d119adf265776aa83b28452af (diff) | |
parent | 9158825f9c41869689d6b1786d7c7aa8bdd524ce (diff) | |
download | frameworks_base-49782e46c0eb85a25ae2abcf80880c48dbab5aea.zip frameworks_base-49782e46c0eb85a25ae2abcf80880c48dbab5aea.tar.gz frameworks_base-49782e46c0eb85a25ae2abcf80880c48dbab5aea.tar.bz2 |
am 9158825f: Move some system services to separate directories
* commit '9158825f9c41869689d6b1786d7c7aa8bdd524ce':
Move some system services to separate directories
Diffstat (limited to 'services/accessibility')
7 files changed, 6923 insertions, 0 deletions
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java new file mode 100644 index 0000000..9e893da --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2011 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 com.android.server.accessibility; + +import android.content.Context; +import android.os.PowerManager; +import android.util.Pools.SimplePool; +import android.util.Slog; +import android.view.Choreographer; +import android.view.Display; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputFilter; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.WindowManagerPolicy; +import android.view.accessibility.AccessibilityEvent; + +/** + * This class is an input filter for implementing accessibility features such + * as display magnification and explore by touch. + * + * NOTE: This class has to be created and poked only from the main thread. + */ +class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation { + + private static final String TAG = AccessibilityInputFilter.class.getSimpleName(); + + private static final boolean DEBUG = false; + + /** + * Flag for enabling the screen magnification feature. + * + * @see #setEnabledFeatures(int) + */ + static final int FLAG_FEATURE_SCREEN_MAGNIFIER = 0x00000001; + + /** + * Flag for enabling the touch exploration feature. + * + * @see #setEnabledFeatures(int) + */ + static final int FLAG_FEATURE_TOUCH_EXPLORATION = 0x00000002; + + /** + * Flag for enabling the filtering key events feature. + * + * @see #setEnabledFeatures(int) + */ + static final int FLAG_FEATURE_FILTER_KEY_EVENTS = 0x00000004; + + private final Runnable mProcessBatchedEventsRunnable = new Runnable() { + @Override + public void run() { + final long frameTimeNanos = mChoreographer.getFrameTimeNanos(); + if (DEBUG) { + Slog.i(TAG, "Begin batch processing for frame: " + frameTimeNanos); + } + processBatchedEvents(frameTimeNanos); + if (DEBUG) { + Slog.i(TAG, "End batch processing."); + } + if (mEventQueue != null) { + scheduleProcessBatchedEvents(); + } + } + }; + + private final Context mContext; + + private final PowerManager mPm; + + private final AccessibilityManagerService mAms; + + private final Choreographer mChoreographer; + + private int mCurrentTouchDeviceId; + + private boolean mInstalled; + + private int mEnabledFeatures; + + private TouchExplorer mTouchExplorer; + + private ScreenMagnifier mScreenMagnifier; + + private EventStreamTransformation mEventHandler; + + private MotionEventHolder mEventQueue; + + private boolean mMotionEventSequenceStarted; + + private boolean mHoverEventSequenceStarted; + + private boolean mKeyEventSequenceStarted; + + private boolean mFilterKeyEvents; + + AccessibilityInputFilter(Context context, AccessibilityManagerService service) { + super(context.getMainLooper()); + mContext = context; + mAms = service; + mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mChoreographer = Choreographer.getInstance(); + } + + @Override + public void onInstalled() { + if (DEBUG) { + Slog.d(TAG, "Accessibility input filter installed."); + } + mInstalled = true; + disableFeatures(); + enableFeatures(); + super.onInstalled(); + } + + @Override + public void onUninstalled() { + if (DEBUG) { + Slog.d(TAG, "Accessibility input filter uninstalled."); + } + mInstalled = false; + disableFeatures(); + super.onUninstalled(); + } + + @Override + public void onInputEvent(InputEvent event, int policyFlags) { + if (DEBUG) { + Slog.d(TAG, "Received event: " + event + ", policyFlags=0x" + + Integer.toHexString(policyFlags)); + } + if (event instanceof MotionEvent + && event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) { + MotionEvent motionEvent = (MotionEvent) event; + onMotionEvent(motionEvent, policyFlags); + } else if (event instanceof KeyEvent + && event.isFromSource(InputDevice.SOURCE_KEYBOARD)) { + KeyEvent keyEvent = (KeyEvent) event; + onKeyEvent(keyEvent, policyFlags); + } else { + super.onInputEvent(event, policyFlags); + } + } + + private void onMotionEvent(MotionEvent event, int policyFlags) { + if (mEventHandler == null) { + super.onInputEvent(event, policyFlags); + return; + } + if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) { + mMotionEventSequenceStarted = false; + mHoverEventSequenceStarted = false; + mEventHandler.clear(); + super.onInputEvent(event, policyFlags); + return; + } + final int deviceId = event.getDeviceId(); + if (mCurrentTouchDeviceId != deviceId) { + mCurrentTouchDeviceId = deviceId; + mMotionEventSequenceStarted = false; + mHoverEventSequenceStarted = false; + mEventHandler.clear(); + } + if (mCurrentTouchDeviceId < 0) { + super.onInputEvent(event, policyFlags); + return; + } + // We do not handle scroll events. + if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) { + super.onInputEvent(event, policyFlags); + return; + } + // Wait for a down touch event to start processing. + if (event.isTouchEvent()) { + if (!mMotionEventSequenceStarted) { + if (event.getActionMasked() != MotionEvent.ACTION_DOWN) { + return; + } + mMotionEventSequenceStarted = true; + } + } else { + // Wait for an enter hover event to start processing. + if (!mHoverEventSequenceStarted) { + if (event.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER) { + return; + } + mHoverEventSequenceStarted = true; + } + } + batchMotionEvent((MotionEvent) event, policyFlags); + } + + private void onKeyEvent(KeyEvent event, int policyFlags) { + if (!mFilterKeyEvents) { + super.onInputEvent(event, policyFlags); + return; + } + if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) { + mKeyEventSequenceStarted = false; + super.onInputEvent(event, policyFlags); + return; + } + // Wait for a down key event to start processing. + if (!mKeyEventSequenceStarted) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return; + } + mKeyEventSequenceStarted = true; + } + mAms.notifyKeyEvent(event, policyFlags); + } + + private void scheduleProcessBatchedEvents() { + mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, + mProcessBatchedEventsRunnable, null); + } + + private void batchMotionEvent(MotionEvent event, int policyFlags) { + if (DEBUG) { + Slog.i(TAG, "Batching event: " + event + ", policyFlags: " + policyFlags); + } + if (mEventQueue == null) { + mEventQueue = MotionEventHolder.obtain(event, policyFlags); + scheduleProcessBatchedEvents(); + return; + } + if (mEventQueue.event.addBatch(event)) { + return; + } + MotionEventHolder holder = MotionEventHolder.obtain(event, policyFlags); + holder.next = mEventQueue; + mEventQueue.previous = holder; + mEventQueue = holder; + } + + private void processBatchedEvents(long frameNanos) { + MotionEventHolder current = mEventQueue; + while (current.next != null) { + current = current.next; + } + while (true) { + if (current == null) { + mEventQueue = null; + break; + } + if (current.event.getEventTimeNano() >= frameNanos) { + // Finished with this choreographer frame. Do the rest on the next one. + current.next = null; + break; + } + handleMotionEvent(current.event, current.policyFlags); + MotionEventHolder prior = current; + current = current.previous; + prior.recycle(); + } + } + + private void handleMotionEvent(MotionEvent event, int policyFlags) { + if (DEBUG) { + Slog.i(TAG, "Handling batched event: " + event + ", policyFlags: " + policyFlags); + } + // Since we do batch processing it is possible that by the time the + // next batch is processed the event handle had been set to null. + if (mEventHandler != null) { + mPm.userActivity(event.getEventTime(), false); + MotionEvent transformedEvent = MotionEvent.obtain(event); + mEventHandler.onMotionEvent(transformedEvent, event, policyFlags); + transformedEvent.recycle(); + } + } + + @Override + public void onMotionEvent(MotionEvent transformedEvent, MotionEvent rawEvent, + int policyFlags) { + sendInputEvent(transformedEvent, policyFlags); + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + // TODO Implement this to inject the accessibility event + // into the accessibility manager service similarly + // to how this is done for input events. + } + + @Override + public void setNext(EventStreamTransformation sink) { + /* do nothing */ + } + + @Override + public void clear() { + /* do nothing */ + } + + void setEnabledFeatures(int enabledFeatures) { + if (mEnabledFeatures == enabledFeatures) { + return; + } + if (mInstalled) { + disableFeatures(); + } + mEnabledFeatures = enabledFeatures; + if (mInstalled) { + enableFeatures(); + } + } + + void notifyAccessibilityEvent(AccessibilityEvent event) { + if (mEventHandler != null) { + mEventHandler.onAccessibilityEvent(event); + } + } + + private void enableFeatures() { + mMotionEventSequenceStarted = false; + mHoverEventSequenceStarted = false; + if ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0) { + mEventHandler = mScreenMagnifier = new ScreenMagnifier(mContext, + Display.DEFAULT_DISPLAY, mAms); + mEventHandler.setNext(this); + } + if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) { + mTouchExplorer = new TouchExplorer(mContext, mAms); + mTouchExplorer.setNext(this); + if (mEventHandler != null) { + mEventHandler.setNext(mTouchExplorer); + } else { + mEventHandler = mTouchExplorer; + } + } + if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) { + mFilterKeyEvents = true; + } + } + + void disableFeatures() { + if (mTouchExplorer != null) { + mTouchExplorer.clear(); + mTouchExplorer.onDestroy(); + mTouchExplorer = null; + } + if (mScreenMagnifier != null) { + mScreenMagnifier.clear(); + mScreenMagnifier.onDestroy(); + mScreenMagnifier = null; + } + mEventHandler = null; + mKeyEventSequenceStarted = false; + mMotionEventSequenceStarted = false; + mHoverEventSequenceStarted = false; + mFilterKeyEvents = false; + } + + @Override + public void onDestroy() { + /* ignore */ + } + + private static class MotionEventHolder { + private static final int MAX_POOL_SIZE = 32; + private static final SimplePool<MotionEventHolder> sPool = + new SimplePool<MotionEventHolder>(MAX_POOL_SIZE); + + public int policyFlags; + public MotionEvent event; + public MotionEventHolder next; + public MotionEventHolder previous; + + public static MotionEventHolder obtain(MotionEvent event, int policyFlags) { + MotionEventHolder holder = sPool.acquire(); + if (holder == null) { + holder = new MotionEventHolder(); + } + holder.event = MotionEvent.obtain(event); + holder.policyFlags = policyFlags; + return holder; + } + + public void recycle() { + event.recycle(); + event = null; + policyFlags = 0; + next = null; + previous = null; + sPool.release(this); + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java new file mode 100644 index 0000000..43e1f12 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -0,0 +1,3199 @@ +/* + ** Copyright 2009, 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 com.android.server.accessibility; + +import static android.accessibilityservice.AccessibilityServiceInfo.DEFAULT; + +import android.Manifest; +import android.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IAccessibilityServiceClient; +import android.accessibilityservice.IAccessibilityServiceConnection; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.StatusBarManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.database.ContentObserver; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.hardware.input.InputManager; +import android.net.Uri; +import android.opengl.Matrix; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; +import android.os.Process; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.text.TextUtils.SimpleStringSplitter; +import android.util.Pools.Pool; +import android.util.Pools.SimplePool; +import android.util.Slog; +import android.util.SparseArray; +import android.view.Display; +import android.view.IWindow; +import android.view.IWindowManager; +import android.view.InputDevice; +import android.view.InputEventConsistencyVerifier; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MagnificationSpec; +import android.view.WindowManager; +import android.view.WindowManagerPolicy; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityInteractionClient; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.view.accessibility.IAccessibilityManager; +import android.view.accessibility.IAccessibilityManagerClient; + +import com.android.internal.R; +import com.android.internal.content.PackageMonitor; +import com.android.internal.statusbar.IStatusBarService; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This class is instantiated by the system as a system level service and can be + * accessed only by the system. The task of this service is to be a centralized + * event dispatch for {@link AccessibilityEvent}s generated across all processes + * on the device. Events are dispatched to {@link AccessibilityService}s. + * + * @hide + */ +public class AccessibilityManagerService extends IAccessibilityManager.Stub { + + private static final boolean DEBUG = false; + + private static final String LOG_TAG = "AccessibilityManagerService"; + + // TODO: This is arbitrary. When there is time implement this by watching + // when that accessibility services are bound. + private static final int WAIT_FOR_USER_STATE_FULLY_INITIALIZED_MILLIS = 3000; + + private static final String FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE = + "registerUiTestAutomationService"; + + private static final String TEMPORARY_ENABLE_ACCESSIBILITY_UNTIL_KEYGUARD_REMOVED = + "temporaryEnableAccessibilityStateUntilKeyguardRemoved"; + + private static final ComponentName sFakeAccessibilityServiceComponentName = + new ComponentName("foo.bar", "FakeService"); + + private static final String FUNCTION_DUMP = "dump"; + + private static final char COMPONENT_NAME_SEPARATOR = ':'; + + private static final int OWN_PROCESS_ID = android.os.Process.myPid(); + + private static final int MAX_POOL_SIZE = 10; + + private static int sIdCounter = 0; + + private static int sNextWindowId; + + private final Context mContext; + + private final Object mLock = new Object(); + + private final Pool<PendingEvent> mPendingEventPool = + new SimplePool<PendingEvent>(MAX_POOL_SIZE); + + private final SimpleStringSplitter mStringColonSplitter = + new SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); + + private final List<AccessibilityServiceInfo> mEnabledServicesForFeedbackTempList = + new ArrayList<AccessibilityServiceInfo>(); + + private final Rect mTempRect = new Rect(); + + private final Point mTempPoint = new Point(); + + private final Display mDefaultDisplay; + + private final PackageManager mPackageManager; + + private final IWindowManager mWindowManagerService; + + private final SecurityPolicy mSecurityPolicy; + + private final MainHandler mMainHandler; + + private Service mQueryBridge; + + private AlertDialog mEnableTouchExplorationDialog; + + private AccessibilityInputFilter mInputFilter; + + private boolean mHasInputFilter; + + private final Set<ComponentName> mTempComponentNameSet = new HashSet<ComponentName>(); + + private final List<AccessibilityServiceInfo> mTempAccessibilityServiceInfoList = + new ArrayList<AccessibilityServiceInfo>(); + + private final RemoteCallbackList<IAccessibilityManagerClient> mGlobalClients = + new RemoteCallbackList<IAccessibilityManagerClient>(); + + private final SparseArray<AccessibilityConnectionWrapper> mGlobalInteractionConnections = + new SparseArray<AccessibilityConnectionWrapper>(); + + private final SparseArray<IBinder> mGlobalWindowTokens = new SparseArray<IBinder>(); + + private final SparseArray<UserState> mUserStates = new SparseArray<UserState>(); + + private int mCurrentUserId = UserHandle.USER_OWNER; + + //TODO: Remove this hack + private boolean mInitialized; + + private UserState getCurrentUserStateLocked() { + return getUserStateLocked(mCurrentUserId); + } + + private UserState getUserStateLocked(int userId) { + UserState state = mUserStates.get(userId); + if (state == null) { + state = new UserState(userId); + mUserStates.put(userId, state); + } + return state; + } + + /** + * Creates a new instance. + * + * @param context A {@link Context} instance. + */ + public AccessibilityManagerService(Context context) { + mContext = context; + mPackageManager = mContext.getPackageManager(); + mWindowManagerService = (IWindowManager) ServiceManager.getService(Context.WINDOW_SERVICE); + mSecurityPolicy = new SecurityPolicy(); + mMainHandler = new MainHandler(mContext.getMainLooper()); + //TODO: (multi-display) We need to support multiple displays. + DisplayManager displayManager = (DisplayManager) + mContext.getSystemService(Context.DISPLAY_SERVICE); + mDefaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + registerBroadcastReceivers(); + new AccessibilityContentObserver(mMainHandler).register( + context.getContentResolver()); + } + + private void registerBroadcastReceivers() { + PackageMonitor monitor = new PackageMonitor() { + @Override + public void onSomePackagesChanged() { + synchronized (mLock) { + if (getChangingUserId() != mCurrentUserId) { + return; + } + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + // We have to reload the installed services since some services may + // have different attributes, resolve info (does not support equals), + // etc. Remove them then to force reload. Do it even if automation is + // running since when it goes away, we will have to reload as well. + userState.mInstalledServices.clear(); + if (userState.mUiAutomationService == null) { + if (readConfigurationForUserStateLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } + + @Override + public void onPackageRemoved(String packageName, int uid) { + synchronized (mLock) { + final int userId = getChangingUserId(); + if (userId != mCurrentUserId) { + return; + } + UserState userState = getUserStateLocked(userId); + Iterator<ComponentName> it = userState.mEnabledServices.iterator(); + while (it.hasNext()) { + ComponentName comp = it.next(); + String compPkg = comp.getPackageName(); + if (compPkg.equals(packageName)) { + it.remove(); + // Update the enabled services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + userState.mEnabledServices, userId); + // Update the touch exploration granted services setting. + userState.mTouchExplorationGrantedServices.remove(comp); + persistComponentNamesToSettingLocked( + Settings.Secure. + TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + userState.mTouchExplorationGrantedServices, userId); + // We will update when the automation service dies. + if (userState.mUiAutomationService == null) { + onUserStateChangedLocked(userState); + } + return; + } + } + } + } + + @Override + public boolean onHandleForceStop(Intent intent, String[] packages, + int uid, boolean doit) { + synchronized (mLock) { + final int userId = getChangingUserId(); + if (userId != mCurrentUserId) { + return false; + } + UserState userState = getUserStateLocked(userId); + Iterator<ComponentName> it = userState.mEnabledServices.iterator(); + while (it.hasNext()) { + ComponentName comp = it.next(); + String compPkg = comp.getPackageName(); + for (String pkg : packages) { + if (compPkg.equals(pkg)) { + if (!doit) { + return true; + } + it.remove(); + persistComponentNamesToSettingLocked( + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + userState.mEnabledServices, userId); + // We will update when the automation service dies. + if (userState.mUiAutomationService == null) { + onUserStateChangedLocked(userState); + } + } + } + } + return false; + } + } + }; + + // package changes + monitor.register(mContext, null, UserHandle.ALL, true); + + // user change and unlock + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_USER_SWITCHED); + intentFilter.addAction(Intent.ACTION_USER_REMOVED); + intentFilter.addAction(Intent.ACTION_USER_PRESENT); + + mContext.registerReceiverAsUser(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_USER_SWITCHED.equals(action)) { + switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + } else if (Intent.ACTION_USER_REMOVED.equals(action)) { + removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + } else if (Intent.ACTION_USER_PRESENT.equals(action)) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readConfigurationForUserStateLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } + }, UserHandle.ALL, intentFilter, null, null); + } + + public int addClient(IAccessibilityManagerClient client, int userId) { + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + // If the client is from a process that runs across users such as + // the system UI or the system we add it to the global state that + // is shared across users. + UserState userState = getUserStateLocked(resolvedUserId); + if (mSecurityPolicy.isCallerInteractingAcrossUsers(userId)) { + mGlobalClients.register(client); + if (DEBUG) { + Slog.i(LOG_TAG, "Added global client for pid:" + Binder.getCallingPid()); + } + return userState.getClientState(); + } else { + userState.mClients.register(client); + // If this client is not for the current user we do not + // return a state since it is not for the foreground user. + // We will send the state to the client on a user switch. + if (DEBUG) { + Slog.i(LOG_TAG, "Added user client for pid:" + Binder.getCallingPid() + + " and userId:" + mCurrentUserId); + } + return (resolvedUserId == mCurrentUserId) ? userState.getClientState() : 0; + } + } + } + + public boolean sendAccessibilityEvent(AccessibilityEvent event, int userId) { + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + // This method does nothing for a background user. + if (resolvedUserId != mCurrentUserId) { + return true; // yes, recycle the event + } + if (mSecurityPolicy.canDispatchAccessibilityEvent(event)) { + mSecurityPolicy.updateEventSourceLocked(event); + mMainHandler.obtainMessage(MainHandler.MSG_UPDATE_ACTIVE_WINDOW, + event.getWindowId(), event.getEventType()).sendToTarget(); + notifyAccessibilityServicesDelayedLocked(event, false); + notifyAccessibilityServicesDelayedLocked(event, true); + } + if (mHasInputFilter && mInputFilter != null) { + mMainHandler.obtainMessage(MainHandler.MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER, + AccessibilityEvent.obtain(event)).sendToTarget(); + } + event.recycle(); + getUserStateLocked(resolvedUserId).mHandledFeedbackTypes = 0; + } + return (OWN_PROCESS_ID != Binder.getCallingPid()); + } + + public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(int userId) { + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + // The automation service is a fake one and should not be reported + // to clients as being installed - it really is not. + UserState userState = getUserStateLocked(resolvedUserId); + if (userState.mUiAutomationService != null) { + List<AccessibilityServiceInfo> installedServices = + new ArrayList<AccessibilityServiceInfo>(); + installedServices.addAll(userState.mInstalledServices); + installedServices.remove(userState.mUiAutomationService); + return installedServices; + } + return userState.mInstalledServices; + } + } + + public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackType, + int userId) { + List<AccessibilityServiceInfo> result = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + + // The automation service is a fake one and should not be reported + // to clients as being enabled. The automation service is always the + // only active one, if it exists. + UserState userState = getUserStateLocked(resolvedUserId); + if (userState.mUiAutomationService != null) { + return Collections.emptyList(); + } + + result = mEnabledServicesForFeedbackTempList; + result.clear(); + List<Service> services = userState.mBoundServices; + while (feedbackType != 0) { + final int feedbackTypeBit = (1 << Integer.numberOfTrailingZeros(feedbackType)); + feedbackType &= ~feedbackTypeBit; + final int serviceCount = services.size(); + for (int i = 0; i < serviceCount; i++) { + Service service = services.get(i); + if ((service.mFeedbackType & feedbackTypeBit) != 0) { + result.add(service.mAccessibilityServiceInfo); + } + } + } + } + return result; + } + + public void interrupt(int userId) { + CopyOnWriteArrayList<Service> services; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + // This method does nothing for a background user. + if (resolvedUserId != mCurrentUserId) { + return; + } + services = getUserStateLocked(resolvedUserId).mBoundServices; + } + for (int i = 0, count = services.size(); i < count; i++) { + Service service = services.get(i); + try { + service.mServiceInterface.onInterrupt(); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error during sending interrupt request to " + + service.mService, re); + } + } + } + + public int addAccessibilityInteractionConnection(IWindow windowToken, + IAccessibilityInteractionConnection connection, int userId) throws RemoteException { + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + final int windowId = sNextWindowId++; + // If the window is from a process that runs across users such as + // the system UI or the system we add it to the global state that + // is shared across users. + if (mSecurityPolicy.isCallerInteractingAcrossUsers(userId)) { + AccessibilityConnectionWrapper wrapper = new AccessibilityConnectionWrapper( + windowId, connection, UserHandle.USER_ALL); + wrapper.linkToDeath(); + mGlobalInteractionConnections.put(windowId, wrapper); + mGlobalWindowTokens.put(windowId, windowToken.asBinder()); + if (DEBUG) { + Slog.i(LOG_TAG, "Added global connection for pid:" + Binder.getCallingPid() + + " with windowId: " + windowId); + } + } else { + AccessibilityConnectionWrapper wrapper = new AccessibilityConnectionWrapper( + windowId, connection, resolvedUserId); + wrapper.linkToDeath(); + UserState userState = getUserStateLocked(resolvedUserId); + userState.mInteractionConnections.put(windowId, wrapper); + userState.mWindowTokens.put(windowId, windowToken.asBinder()); + if (DEBUG) { + Slog.i(LOG_TAG, "Added user connection for pid:" + Binder.getCallingPid() + + " with windowId: " + windowId + " and userId:" + mCurrentUserId); + } + } + if (DEBUG) { + Slog.i(LOG_TAG, "Adding interaction connection to windowId: " + windowId); + } + return windowId; + } + } + + public void removeAccessibilityInteractionConnection(IWindow window) { + synchronized (mLock) { + mSecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + IBinder token = window.asBinder(); + final int removedWindowId = removeAccessibilityInteractionConnectionInternalLocked( + token, mGlobalWindowTokens, mGlobalInteractionConnections); + if (removedWindowId >= 0) { + if (DEBUG) { + Slog.i(LOG_TAG, "Removed global connection for pid:" + Binder.getCallingPid() + + " with windowId: " + removedWindowId); + } + return; + } + final int userCount = mUserStates.size(); + for (int i = 0; i < userCount; i++) { + UserState userState = mUserStates.valueAt(i); + final int removedWindowIdForUser = + removeAccessibilityInteractionConnectionInternalLocked( + token, userState.mWindowTokens, userState.mInteractionConnections); + if (removedWindowIdForUser >= 0) { + if (DEBUG) { + Slog.i(LOG_TAG, "Removed user connection for pid:" + Binder.getCallingPid() + + " with windowId: " + removedWindowIdForUser + " and userId:" + + mUserStates.keyAt(i)); + } + return; + } + } + } + } + + private int removeAccessibilityInteractionConnectionInternalLocked(IBinder windowToken, + SparseArray<IBinder> windowTokens, + SparseArray<AccessibilityConnectionWrapper> interactionConnections) { + final int count = windowTokens.size(); + for (int i = 0; i < count; i++) { + if (windowTokens.valueAt(i) == windowToken) { + final int windowId = windowTokens.keyAt(i); + windowTokens.removeAt(i); + AccessibilityConnectionWrapper wrapper = interactionConnections.get(windowId); + wrapper.unlinkToDeath(); + interactionConnections.remove(windowId); + return windowId; + } + } + return -1; + } + + public void registerUiTestAutomationService(IBinder owner, + IAccessibilityServiceClient serviceClient, + AccessibilityServiceInfo accessibilityServiceInfo) { + mSecurityPolicy.enforceCallingPermission(Manifest.permission.RETRIEVE_WINDOW_CONTENT, + FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE); + + accessibilityServiceInfo.setComponentName(sFakeAccessibilityServiceComponentName); + + synchronized (mLock) { + UserState userState = getCurrentUserStateLocked(); + + if (userState.mUiAutomationService != null) { + throw new IllegalStateException("UiAutomationService " + serviceClient + + "already registered!"); + } + + try { + owner.linkToDeath(userState.mUiAutomationSerivceOnwerDeathRecipient, 0); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Couldn't register for the death of a" + + " UiTestAutomationService!", re); + return; + } + + userState.mUiAutomationServiceOwner = owner; + userState.mUiAutomationServiceClient = serviceClient; + + // Set the temporary state. + userState.mIsAccessibilityEnabled = true; + userState.mIsTouchExplorationEnabled = false; + userState.mIsEnhancedWebAccessibilityEnabled = false; + userState.mIsDisplayMagnificationEnabled = false; + userState.mInstalledServices.add(accessibilityServiceInfo); + userState.mEnabledServices.clear(); + userState.mEnabledServices.add(sFakeAccessibilityServiceComponentName); + userState.mTouchExplorationGrantedServices.add(sFakeAccessibilityServiceComponentName); + + // Use the new state instead of settings. + onUserStateChangedLocked(userState); + } + } + + public void unregisterUiTestAutomationService(IAccessibilityServiceClient serviceClient) { + synchronized (mLock) { + UserState userState = getCurrentUserStateLocked(); + // Automation service is not bound, so pretend it died to perform clean up. + if (userState.mUiAutomationService != null + && serviceClient != null + && userState.mUiAutomationService != null + && userState.mUiAutomationService.mServiceInterface != null + && userState.mUiAutomationService.mServiceInterface.asBinder() + == serviceClient.asBinder()) { + userState.mUiAutomationService.binderDied(); + } else { + throw new IllegalStateException("UiAutomationService " + serviceClient + + " not registered!"); + } + } + } + + public void temporaryEnableAccessibilityStateUntilKeyguardRemoved( + ComponentName service, boolean touchExplorationEnabled) { + mSecurityPolicy.enforceCallingPermission( + Manifest.permission.TEMPORARY_ENABLE_ACCESSIBILITY, + TEMPORARY_ENABLE_ACCESSIBILITY_UNTIL_KEYGUARD_REMOVED); + try { + if (!mWindowManagerService.isKeyguardLocked()) { + return; + } + } catch (RemoteException re) { + return; + } + synchronized (mLock) { + // Set the temporary state. + UserState userState = getCurrentUserStateLocked(); + + // This is a nop if UI automation is enabled. + if (userState.mUiAutomationService != null) { + return; + } + + userState.mIsAccessibilityEnabled = true; + userState.mIsTouchExplorationEnabled = touchExplorationEnabled; + userState.mIsEnhancedWebAccessibilityEnabled = false; + userState.mIsDisplayMagnificationEnabled = false; + userState.mEnabledServices.clear(); + userState.mEnabledServices.add(service); + userState.mBindingServices.clear(); + userState.mTouchExplorationGrantedServices.clear(); + userState.mTouchExplorationGrantedServices.add(service); + + // User the current state instead settings. + onUserStateChangedLocked(userState); + } + } + + boolean onGesture(int gestureId) { + synchronized (mLock) { + boolean handled = notifyGestureLocked(gestureId, false); + if (!handled) { + handled = notifyGestureLocked(gestureId, true); + } + return handled; + } + } + + boolean notifyKeyEvent(KeyEvent event, int policyFlags) { + synchronized (mLock) { + KeyEvent localClone = KeyEvent.obtain(event); + boolean handled = notifyKeyEventLocked(localClone, policyFlags, false); + if (!handled) { + handled = notifyKeyEventLocked(localClone, policyFlags, true); + } + return handled; + } + } + + /** + * Gets the bounds of the accessibility focus in the active window. + * + * @param outBounds The output to which to write the focus bounds. + * @return Whether accessibility focus was found and the bounds are populated. + */ + // TODO: (multi-display) Make sure this works for multiple displays. + boolean getAccessibilityFocusBoundsInActiveWindow(Rect outBounds) { + // Instead of keeping track of accessibility focus events per + // window to be able to find the focus in the active window, + // we take a stateless approach and look it up. This is fine + // since we do this only when the user clicks/long presses. + Service service = getQueryBridge(); + final int connectionId = service.mId; + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + client.addConnection(connectionId, service); + try { + AccessibilityNodeInfo root = AccessibilityInteractionClient.getInstance() + .getRootInActiveWindow(connectionId); + if (root == null) { + return false; + } + AccessibilityNodeInfo focus = root.findFocus( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY); + if (focus == null) { + return false; + } + focus.getBoundsInScreen(outBounds); + + MagnificationSpec spec = service.getCompatibleMagnificationSpec(focus.getWindowId()); + if (spec != null && !spec.isNop()) { + outBounds.offset((int) -spec.offsetX, (int) -spec.offsetY); + outBounds.scale(1 / spec.scale); + } + + // Clip to the window rectangle. + Rect windowBounds = mTempRect; + getActiveWindowBounds(windowBounds); + outBounds.intersect(windowBounds); + // Clip to the screen rectangle. + mDefaultDisplay.getRealSize(mTempPoint); + outBounds.intersect(0, 0, mTempPoint.x, mTempPoint.y); + + return true; + } finally { + client.removeConnection(connectionId); + } + } + + /** + * Gets the bounds of the active window. + * + * @param outBounds The output to which to write the bounds. + */ + boolean getActiveWindowBounds(Rect outBounds) { + IBinder token; + synchronized (mLock) { + final int windowId = mSecurityPolicy.mActiveWindowId; + token = mGlobalWindowTokens.get(windowId); + if (token == null) { + token = getCurrentUserStateLocked().mWindowTokens.get(windowId); + } + } + try { + mWindowManagerService.getWindowFrame(token, outBounds); + if (!outBounds.isEmpty()) { + return true; + } + } catch (RemoteException re) { + /* ignore */ + } + return false; + } + + int getActiveWindowId() { + return mSecurityPolicy.mActiveWindowId; + } + + void onTouchInteractionStart() { + mSecurityPolicy.onTouchInteractionStart(); + } + + void onTouchInteractionEnd() { + mSecurityPolicy.onTouchInteractionEnd(); + } + + void onMagnificationStateChanged() { + notifyClearAccessibilityNodeInfoCacheLocked(); + } + + private void switchUser(int userId) { + synchronized (mLock) { + if (mCurrentUserId == userId && mInitialized) { + return; + } + + // Disconnect from services for the old user. + UserState oldUserState = getUserStateLocked(mCurrentUserId); + oldUserState.onSwitchToAnotherUser(); + + // Disable the local managers for the old user. + if (oldUserState.mClients.getRegisteredCallbackCount() > 0) { + mMainHandler.obtainMessage(MainHandler.MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER, + oldUserState.mUserId, 0).sendToTarget(); + } + + // Announce user changes only if more that one exist. + UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + final boolean announceNewUser = userManager.getUsers().size() > 1; + + // The user changed. + mCurrentUserId = userId; + + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService != null) { + // Switching users disables the UI automation service. + userState.mUiAutomationService.binderDied(); + } + + readConfigurationForUserStateLocked(userState); + // Even if reading did not yield change, we have to update + // the state since the context in which the current user + // state was used has changed since it was inactive. + onUserStateChangedLocked(userState); + + if (announceNewUser) { + // Schedule announcement of the current user if needed. + mMainHandler.sendEmptyMessageDelayed(MainHandler.MSG_ANNOUNCE_NEW_USER_IF_NEEDED, + WAIT_FOR_USER_STATE_FULLY_INITIALIZED_MILLIS); + } + } + } + + private void removeUser(int userId) { + synchronized (mLock) { + mUserStates.remove(userId); + } + } + + private Service getQueryBridge() { + if (mQueryBridge == null) { + AccessibilityServiceInfo info = new AccessibilityServiceInfo(); + info.setCapabilities(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT); + mQueryBridge = new Service(UserHandle.USER_NULL, + sFakeAccessibilityServiceComponentName, info); + } + return mQueryBridge; + } + + private boolean notifyGestureLocked(int gestureId, boolean isDefault) { + // TODO: Now we are giving the gestures to the last enabled + // service that can handle them which is the last one + // in our list since we write the last enabled as the + // last record in the enabled services setting. Ideally, + // the user should make the call which service handles + // gestures. However, only one service should handle + // gestures to avoid user frustration when different + // behavior is observed from different combinations of + // enabled accessibility services. + UserState state = getCurrentUserStateLocked(); + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + Service service = state.mBoundServices.get(i); + if (service.mRequestTouchExplorationMode && service.mIsDefault == isDefault) { + service.notifyGesture(gestureId); + return true; + } + } + return false; + } + + private boolean notifyKeyEventLocked(KeyEvent event, int policyFlags, boolean isDefault) { + // TODO: Now we are giving the key events to the last enabled + // service that can handle them Ideally, the user should + // make the call which service handles key events. However, + // only one service should handle key events to avoid user + // frustration when different behavior is observed from + // different combinations of enabled accessibility services. + UserState state = getCurrentUserStateLocked(); + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + Service service = state.mBoundServices.get(i); + // Key events are handled only by services that declared + // this capability and requested to filter key events. + if (!service.mRequestFilterKeyEvents || + (service.mAccessibilityServiceInfo.getCapabilities() & AccessibilityServiceInfo + .CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS) == 0) { + continue; + } + if (service.mIsDefault == isDefault) { + service.notifyKeyEvent(event, policyFlags); + return true; + } + } + return false; + } + + private void notifyClearAccessibilityNodeInfoCacheLocked() { + UserState state = getCurrentUserStateLocked(); + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + Service service = state.mBoundServices.get(i); + service.notifyClearAccessibilityNodeInfoCache(); + } + } + + /** + * Removes an AccessibilityInteractionConnection. + * + * @param windowId The id of the window to which the connection is targeted. + * @param userId The id of the user owning the connection. UserHandle.USER_ALL + * if global. + */ + private void removeAccessibilityInteractionConnectionLocked(int windowId, int userId) { + if (userId == UserHandle.USER_ALL) { + mGlobalWindowTokens.remove(windowId); + mGlobalInteractionConnections.remove(windowId); + } else { + UserState userState = getCurrentUserStateLocked(); + userState.mWindowTokens.remove(windowId); + userState.mInteractionConnections.remove(windowId); + } + if (DEBUG) { + Slog.i(LOG_TAG, "Removing interaction connection to windowId: " + windowId); + } + } + + private boolean readInstalledAccessibilityServiceLocked(UserState userState) { + mTempAccessibilityServiceInfoList.clear(); + + List<ResolveInfo> installedServices = mPackageManager.queryIntentServicesAsUser( + new Intent(AccessibilityService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, + mCurrentUserId); + + for (int i = 0, count = installedServices.size(); i < count; i++) { + ResolveInfo resolveInfo = installedServices.get(i); + ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (!android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE.equals( + serviceInfo.permission)) { + Slog.w(LOG_TAG, "Skipping accessibilty service " + new ComponentName( + serviceInfo.packageName, serviceInfo.name).flattenToShortString() + + ": it does not require the permission " + + android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE); + continue; + } + AccessibilityServiceInfo accessibilityServiceInfo; + try { + accessibilityServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext); + mTempAccessibilityServiceInfoList.add(accessibilityServiceInfo); + } catch (XmlPullParserException xppe) { + Slog.e(LOG_TAG, "Error while initializing AccessibilityServiceInfo", xppe); + } catch (IOException ioe) { + Slog.e(LOG_TAG, "Error while initializing AccessibilityServiceInfo", ioe); + } + } + + if (!mTempAccessibilityServiceInfoList.equals(userState.mInstalledServices)) { + userState.mInstalledServices.clear(); + userState.mInstalledServices.addAll(mTempAccessibilityServiceInfoList); + mTempAccessibilityServiceInfoList.clear(); + return true; + } + + mTempAccessibilityServiceInfoList.clear(); + return false; + } + + private boolean readEnabledAccessibilityServicesLocked(UserState userState) { + mTempComponentNameSet.clear(); + readComponentNamesFromSettingLocked(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + userState.mUserId, mTempComponentNameSet); + if (!mTempComponentNameSet.equals(userState.mEnabledServices)) { + userState.mEnabledServices.clear(); + userState.mEnabledServices.addAll(mTempComponentNameSet); + mTempComponentNameSet.clear(); + return true; + } + mTempComponentNameSet.clear(); + return false; + } + + private boolean readTouchExplorationGrantedAccessibilityServicesLocked( + UserState userState) { + mTempComponentNameSet.clear(); + readComponentNamesFromSettingLocked( + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + userState.mUserId, mTempComponentNameSet); + if (!mTempComponentNameSet.equals(userState.mTouchExplorationGrantedServices)) { + userState.mTouchExplorationGrantedServices.clear(); + userState.mTouchExplorationGrantedServices.addAll(mTempComponentNameSet); + mTempComponentNameSet.clear(); + return true; + } + mTempComponentNameSet.clear(); + return false; + } + + /** + * Performs {@link AccessibilityService}s delayed notification. The delay is configurable + * and denotes the period after the last event before notifying the service. + * + * @param event The event. + * @param isDefault True to notify default listeners, not default services. + */ + private void notifyAccessibilityServicesDelayedLocked(AccessibilityEvent event, + boolean isDefault) { + try { + UserState state = getCurrentUserStateLocked(); + for (int i = 0, count = state.mBoundServices.size(); i < count; i++) { + Service service = state.mBoundServices.get(i); + + if (service.mIsDefault == isDefault) { + if (canDispathEventLocked(service, event, state.mHandledFeedbackTypes)) { + state.mHandledFeedbackTypes |= service.mFeedbackType; + service.notifyAccessibilityEvent(event); + } + } + } + } catch (IndexOutOfBoundsException oobe) { + // An out of bounds exception can happen if services are going away + // as the for loop is running. If that happens, just bail because + // there are no more services to notify. + return; + } + } + + private void addServiceLocked(Service service, UserState userState) { + try { + service.linkToOwnDeathLocked(); + userState.mBoundServices.add(service); + userState.mComponentNameToServiceMap.put(service.mComponentName, service); + } catch (RemoteException re) { + /* do nothing */ + } + } + + /** + * Removes a service. + * + * @param service The service. + * @return True if the service was removed, false otherwise. + */ + private void removeServiceLocked(Service service, UserState userState) { + userState.mBoundServices.remove(service); + userState.mComponentNameToServiceMap.remove(service.mComponentName); + service.unlinkToOwnDeathLocked(); + } + + /** + * Determines if given event can be dispatched to a service based on the package of the + * event source and already notified services for that event type. Specifically, a + * service is notified if it is interested in events from the package and no other service + * providing the same feedback type has been notified. Exception are services the + * provide generic feedback (feedback type left as a safety net for unforeseen feedback + * types) which are always notified. + * + * @param service The potential receiver. + * @param event The event. + * @param handledFeedbackTypes The feedback types for which services have been notified. + * @return True if the listener should be notified, false otherwise. + */ + private boolean canDispathEventLocked(Service service, AccessibilityEvent event, + int handledFeedbackTypes) { + + if (!service.canReceiveEventsLocked()) { + return false; + } + + if (!event.isImportantForAccessibility() + && (service.mFetchFlags + & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) == 0) { + return false; + } + + int eventType = event.getEventType(); + if ((service.mEventTypes & eventType) != eventType) { + return false; + } + + Set<String> packageNames = service.mPackageNames; + CharSequence packageName = event.getPackageName(); + + if (packageNames.isEmpty() || packageNames.contains(packageName)) { + int feedbackType = service.mFeedbackType; + if ((handledFeedbackTypes & feedbackType) != feedbackType + || feedbackType == AccessibilityServiceInfo.FEEDBACK_GENERIC) { + return true; + } + } + + return false; + } + + private void unbindAllServicesLocked(UserState userState) { + List<Service> services = userState.mBoundServices; + for (int i = 0, count = services.size(); i < count; i++) { + Service service = services.get(i); + if (service.unbindLocked()) { + i--; + count--; + } + } + } + + /** + * Populates a set with the {@link ComponentName}s stored in a colon + * separated value setting for a given user. + * + * @param settingName The setting to parse. + * @param userId The user id. + * @param outComponentNames The output component names. + */ + private void readComponentNamesFromSettingLocked(String settingName, int userId, + Set<ComponentName> outComponentNames) { + String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(), + settingName, userId); + outComponentNames.clear(); + if (settingValue != null) { + TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; + splitter.setString(settingValue); + while (splitter.hasNext()) { + String str = splitter.next(); + if (str == null || str.length() <= 0) { + continue; + } + ComponentName enabledService = ComponentName.unflattenFromString(str); + if (enabledService != null) { + outComponentNames.add(enabledService); + } + } + } + } + + /** + * Persists the component names in the specified setting in a + * colon separated fashion. + * + * @param settingName The setting name. + * @param componentNames The component names. + */ + private void persistComponentNamesToSettingLocked(String settingName, + Set<ComponentName> componentNames, int userId) { + StringBuilder builder = new StringBuilder(); + for (ComponentName componentName : componentNames) { + if (builder.length() > 0) { + builder.append(COMPONENT_NAME_SEPARATOR); + } + builder.append(componentName.flattenToShortString()); + } + Settings.Secure.putStringForUser(mContext.getContentResolver(), + settingName, builder.toString(), userId); + } + + private void manageServicesLocked(UserState userState) { + Map<ComponentName, Service> componentNameToServiceMap = + userState.mComponentNameToServiceMap; + boolean isEnabled = userState.mIsAccessibilityEnabled; + + for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) { + AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i); + ComponentName componentName = ComponentName.unflattenFromString( + installedService.getId()); + Service service = componentNameToServiceMap.get(componentName); + + if (isEnabled) { + // Wait for the binding if it is in process. + if (userState.mBindingServices.contains(componentName)) { + continue; + } + if (userState.mEnabledServices.contains(componentName)) { + if (service == null) { + service = new Service(userState.mUserId, componentName, installedService); + } else if (userState.mBoundServices.contains(service)) { + continue; + } + service.bindLocked(); + } else { + if (service != null) { + service.unbindLocked(); + } + } + } else { + if (service != null) { + service.unbindLocked(); + } else { + userState.mBindingServices.remove(componentName); + } + } + } + + // No enabled installed services => disable accessibility to avoid + // sending accessibility events with no recipient across processes. + if (isEnabled && userState.mEnabledServices.isEmpty()) { + userState.mIsAccessibilityEnabled = false; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED, 0, userState.mUserId); + } + } + + private void scheduleUpdateClientsIfNeededLocked(UserState userState) { + final int clientState = userState.getClientState(); + if (userState.mLastSentClientState != clientState + && (mGlobalClients.getRegisteredCallbackCount() > 0 + || userState.mClients.getRegisteredCallbackCount() > 0)) { + userState.mLastSentClientState = clientState; + mMainHandler.obtainMessage(MainHandler.MSG_SEND_STATE_TO_CLIENTS, + clientState, userState.mUserId) .sendToTarget(); + } + } + + private void scheduleUpdateInputFilter(UserState userState) { + mMainHandler.obtainMessage(MainHandler.MSG_UPDATE_INPUT_FILTER, userState).sendToTarget(); + } + + private void updateInputFilter(UserState userState) { + boolean setInputFilter = false; + AccessibilityInputFilter inputFilter = null; + synchronized (mLock) { + int flags = 0; + if (userState.mIsDisplayMagnificationEnabled) { + flags |= AccessibilityInputFilter.FLAG_FEATURE_SCREEN_MAGNIFIER; + } + // Touch exploration without accessibility makes no sense. + if (userState.mIsAccessibilityEnabled && userState.mIsTouchExplorationEnabled) { + flags |= AccessibilityInputFilter.FLAG_FEATURE_TOUCH_EXPLORATION; + } + if (userState.mIsFilterKeyEventsEnabled) { + flags |= AccessibilityInputFilter.FLAG_FEATURE_FILTER_KEY_EVENTS; + } + if (flags != 0) { + if (!mHasInputFilter) { + mHasInputFilter = true; + if (mInputFilter == null) { + mInputFilter = new AccessibilityInputFilter(mContext, + AccessibilityManagerService.this); + } + inputFilter = mInputFilter; + setInputFilter = true; + } + mInputFilter.setEnabledFeatures(flags); + } else { + if (mHasInputFilter) { + mHasInputFilter = false; + mInputFilter.disableFeatures(); + inputFilter = null; + setInputFilter = true; + } + } + } + if (setInputFilter) { + try { + mWindowManagerService.setInputFilter(inputFilter); + } catch (RemoteException re) { + /* ignore */ + } + } + } + + private void showEnableTouchExplorationDialog(final Service service) { + synchronized (mLock) { + String label = service.mResolveInfo.loadLabel( + mContext.getPackageManager()).toString(); + + final UserState state = getCurrentUserStateLocked(); + if (state.mIsTouchExplorationEnabled) { + return; + } + if (mEnableTouchExplorationDialog != null + && mEnableTouchExplorationDialog.isShowing()) { + return; + } + mEnableTouchExplorationDialog = new AlertDialog.Builder(mContext) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setPositiveButton(android.R.string.ok, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // The user allowed the service to toggle touch exploration. + state.mTouchExplorationGrantedServices.add(service.mComponentName); + persistComponentNamesToSettingLocked( + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + state.mTouchExplorationGrantedServices, state.mUserId); + // Enable touch exploration. + UserState userState = getUserStateLocked(service.mUserId); + userState.mIsTouchExplorationEnabled = true; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, 1, + service.mUserId); + onUserStateChangedLocked(userState); + } + }) + .setNegativeButton(android.R.string.cancel, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .setTitle(R.string.enable_explore_by_touch_warning_title) + .setMessage(mContext.getString( + R.string.enable_explore_by_touch_warning_message, label)) + .create(); + mEnableTouchExplorationDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + mEnableTouchExplorationDialog.getWindow().getAttributes().privateFlags + |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; + mEnableTouchExplorationDialog.setCanceledOnTouchOutside(true); + mEnableTouchExplorationDialog.show(); + } + } + + private void onUserStateChangedLocked(UserState userState) { + // TODO: Remove this hack + mInitialized = true; + updateLegacyCapabilities(userState); + updateServicesLocked(userState); + updateFilterKeyEventsLocked(userState); + updateTouchExplorationLocked(userState); + updateEnhancedWebAccessibilityLocked(userState); + updateDisplayColorAdjustmentSettingsLocked(userState); + scheduleUpdateInputFilter(userState); + scheduleUpdateClientsIfNeededLocked(userState); + } + + private void updateLegacyCapabilities(UserState userState) { + // Up to JB-MR1 we had a white list with services that can enable touch + // exploration. When a service is first started we show a dialog to the + // use to get a permission to white list the service. + final int installedServiceCount = userState.mInstalledServices.size(); + for (int i = 0; i < installedServiceCount; i++) { + AccessibilityServiceInfo serviceInfo = userState.mInstalledServices.get(i); + ResolveInfo resolveInfo = serviceInfo.getResolveInfo(); + if ((serviceInfo.getCapabilities() + & AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION) == 0 + && resolveInfo.serviceInfo.applicationInfo.targetSdkVersion + <= Build.VERSION_CODES.JELLY_BEAN_MR1) { + ComponentName componentName = new ComponentName( + resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name); + if (userState.mTouchExplorationGrantedServices.contains(componentName)) { + serviceInfo.setCapabilities(serviceInfo.getCapabilities() + | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION); + } + } + } + } + + private void updateFilterKeyEventsLocked(UserState userState) { + final int serviceCount = userState.mBoundServices.size(); + for (int i = 0; i < serviceCount; i++) { + Service service = userState.mBoundServices.get(i); + if (service.mRequestFilterKeyEvents + && (service.mAccessibilityServiceInfo.getCapabilities() + & AccessibilityServiceInfo + .CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS) != 0) { + userState.mIsFilterKeyEventsEnabled = true; + return; + } + } + userState.mIsFilterKeyEventsEnabled = false; + } + + private void updateServicesLocked(UserState userState) { + if (userState.mIsAccessibilityEnabled) { + manageServicesLocked(userState); + } else { + unbindAllServicesLocked(userState); + } + } + + private boolean readConfigurationForUserStateLocked(UserState userState) { + boolean somthingChanged = false; + somthingChanged |= readAccessibilityEnabledSettingLocked(userState); + somthingChanged |= readInstalledAccessibilityServiceLocked(userState); + somthingChanged |= readEnabledAccessibilityServicesLocked(userState); + somthingChanged |= readTouchExplorationGrantedAccessibilityServicesLocked(userState); + somthingChanged |= readTouchExplorationEnabledSettingLocked(userState); + somthingChanged |= readEnhancedWebAccessibilityEnabledChangedLocked(userState); + somthingChanged |= readDisplayMagnificationEnabledSettingLocked(userState); + somthingChanged |= readDisplayColorAdjustmentSettingsLocked(userState); + return somthingChanged; + } + + private boolean readAccessibilityEnabledSettingLocked(UserState userState) { + final boolean accessibilityEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED, 0, userState.mUserId) == 1; + if (accessibilityEnabled != userState.mIsAccessibilityEnabled) { + userState.mIsAccessibilityEnabled = accessibilityEnabled; + return true; + } + return false; + } + + private boolean readTouchExplorationEnabledSettingLocked(UserState userState) { + final boolean touchExplorationEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0, userState.mUserId) == 1; + if (touchExplorationEnabled != userState.mIsTouchExplorationEnabled) { + userState.mIsTouchExplorationEnabled = touchExplorationEnabled; + return true; + } + return false; + } + + private boolean readDisplayMagnificationEnabledSettingLocked(UserState userState) { + final boolean displayMagnificationEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, + 0, userState.mUserId) == 1; + if (displayMagnificationEnabled != userState.mIsDisplayMagnificationEnabled) { + userState.mIsDisplayMagnificationEnabled = displayMagnificationEnabled; + return true; + } + return false; + } + + private boolean readEnhancedWebAccessibilityEnabledChangedLocked(UserState userState) { + final boolean enhancedWeAccessibilityEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, + 0, userState.mUserId) == 1; + if (enhancedWeAccessibilityEnabled != userState.mIsEnhancedWebAccessibilityEnabled) { + userState.mIsEnhancedWebAccessibilityEnabled = enhancedWeAccessibilityEnabled; + return true; + } + return false; + } + + private boolean readDisplayColorAdjustmentSettingsLocked(UserState userState) { + final boolean displayAdjustmentsEnabled = DisplayAdjustmentUtils.hasAdjustments(mContext, + userState.mUserId); + if (displayAdjustmentsEnabled != userState.mHasDisplayColorAdjustment) { + userState.mHasDisplayColorAdjustment = displayAdjustmentsEnabled; + return true; + } + // If display adjustment is enabled, always assume there was a change in + // the adjustment settings. + return displayAdjustmentsEnabled; + } + + private void updateTouchExplorationLocked(UserState userState) { + boolean enabled = false; + final int serviceCount = userState.mBoundServices.size(); + for (int i = 0; i < serviceCount; i++) { + Service service = userState.mBoundServices.get(i); + if (canRequestAndRequestsTouchExplorationLocked(service)) { + enabled = true; + break; + } + } + if (enabled != userState.mIsTouchExplorationEnabled) { + userState.mIsTouchExplorationEnabled = enabled; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, enabled ? 1 : 0, + userState.mUserId); + } + try { + mWindowManagerService.setTouchExplorationEnabled(enabled); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + private boolean canRequestAndRequestsTouchExplorationLocked(Service service) { + // Service not ready or cannot request the feature - well nothing to do. + if (!service.canReceiveEventsLocked() || !service.mRequestTouchExplorationMode) { + return false; + } + // UI test automation service can always enable it. + if (service.mIsAutomation) { + return true; + } + if (service.mResolveInfo.serviceInfo.applicationInfo.targetSdkVersion + <= Build.VERSION_CODES.JELLY_BEAN_MR1) { + // Up to JB-MR1 we had a white list with services that can enable touch + // exploration. When a service is first started we show a dialog to the + // use to get a permission to white list the service. + UserState userState = getUserStateLocked(service.mUserId); + if (userState.mTouchExplorationGrantedServices.contains(service.mComponentName)) { + return true; + } else if (mEnableTouchExplorationDialog == null + || !mEnableTouchExplorationDialog.isShowing()) { + mMainHandler.obtainMessage( + MainHandler.MSG_SHOW_ENABLED_TOUCH_EXPLORATION_DIALOG, + service).sendToTarget(); + } + } else { + // Starting in JB-MR2 we request an accessibility service to declare + // certain capabilities in its meta-data to allow it to enable the + // corresponding features. + if ((service.mAccessibilityServiceInfo.getCapabilities() + & AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION) != 0) { + return true; + } + } + return false; + } + + private void updateEnhancedWebAccessibilityLocked(UserState userState) { + boolean enabled = false; + final int serviceCount = userState.mBoundServices.size(); + for (int i = 0; i < serviceCount; i++) { + Service service = userState.mBoundServices.get(i); + if (canRequestAndRequestsEnhancedWebAccessibilityLocked(service)) { + enabled = true; + break; + } + } + if (enabled != userState.mIsEnhancedWebAccessibilityEnabled) { + userState.mIsEnhancedWebAccessibilityEnabled = enabled; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, enabled ? 1 : 0, + userState.mUserId); + } + } + + private boolean canRequestAndRequestsEnhancedWebAccessibilityLocked(Service service) { + if (!service.canReceiveEventsLocked() || !service.mRequestEnhancedWebAccessibility ) { + return false; + } + if (service.mIsAutomation || (service.mAccessibilityServiceInfo.getCapabilities() + & AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY) != 0) { + return true; + } + return false; + } + + private void updateDisplayColorAdjustmentSettingsLocked(UserState userState) { + DisplayAdjustmentUtils.applyAdjustments(mContext, userState.mUserId); + } + + @Override + public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { + mSecurityPolicy.enforceCallingPermission(Manifest.permission.DUMP, FUNCTION_DUMP); + synchronized (mLock) { + pw.println("ACCESSIBILITY MANAGER (dumpsys accessibility)"); + pw.println(); + final int userCount = mUserStates.size(); + for (int i = 0; i < userCount; i++) { + UserState userState = mUserStates.valueAt(i); + pw.append("User state[attributes:{id=" + userState.mUserId); + pw.append(", currentUser=" + (userState.mUserId == mCurrentUserId)); + pw.append(", accessibilityEnabled=" + userState.mIsAccessibilityEnabled); + pw.append(", touchExplorationEnabled=" + userState.mIsTouchExplorationEnabled); + pw.append(", displayMagnificationEnabled=" + + userState.mIsDisplayMagnificationEnabled); + if (userState.mUiAutomationService != null) { + pw.append(", "); + userState.mUiAutomationService.dump(fd, pw, args); + pw.println(); + } + pw.append("}"); + pw.println(); + pw.append(" services:{"); + final int serviceCount = userState.mBoundServices.size(); + for (int j = 0; j < serviceCount; j++) { + if (j > 0) { + pw.append(", "); + pw.println(); + pw.append(" "); + } + Service service = userState.mBoundServices.get(j); + service.dump(fd, pw, args); + } + pw.println("}]"); + pw.println(); + } + } + } + + private class AccessibilityConnectionWrapper implements DeathRecipient { + private final int mWindowId; + private final int mUserId; + private final IAccessibilityInteractionConnection mConnection; + + public AccessibilityConnectionWrapper(int windowId, + IAccessibilityInteractionConnection connection, int userId) { + mWindowId = windowId; + mUserId = userId; + mConnection = connection; + } + + public void linkToDeath() throws RemoteException { + mConnection.asBinder().linkToDeath(this, 0); + } + + public void unlinkToDeath() { + mConnection.asBinder().unlinkToDeath(this, 0); + } + + @Override + public void binderDied() { + unlinkToDeath(); + synchronized (mLock) { + removeAccessibilityInteractionConnectionLocked(mWindowId, mUserId); + } + } + } + + private final class MainHandler extends Handler { + public static final int MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER = 1; + public static final int MSG_SEND_STATE_TO_CLIENTS = 2; + public static final int MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER = 3; + public static final int MSG_UPDATE_ACTIVE_WINDOW = 4; + public static final int MSG_ANNOUNCE_NEW_USER_IF_NEEDED = 5; + public static final int MSG_UPDATE_INPUT_FILTER = 6; + public static final int MSG_SHOW_ENABLED_TOUCH_EXPLORATION_DIALOG = 7; + public static final int MSG_SEND_KEY_EVENT_TO_INPUT_FILTER = 8; + + public MainHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + final int type = msg.what; + switch (type) { + case MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER: { + AccessibilityEvent event = (AccessibilityEvent) msg.obj; + synchronized (mLock) { + if (mHasInputFilter && mInputFilter != null) { + mInputFilter.notifyAccessibilityEvent(event); + } + } + event.recycle(); + } break; + case MSG_SEND_KEY_EVENT_TO_INPUT_FILTER: { + KeyEvent event = (KeyEvent) msg.obj; + final int policyFlags = msg.arg1; + synchronized (mLock) { + if (mHasInputFilter && mInputFilter != null) { + mInputFilter.sendInputEvent(event, policyFlags); + } + } + event.recycle(); + } break; + case MSG_SEND_STATE_TO_CLIENTS: { + final int clientState = msg.arg1; + final int userId = msg.arg2; + sendStateToClients(clientState, mGlobalClients); + sendStateToClientsForUser(clientState, userId); + } break; + case MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER: { + final int userId = msg.arg1; + sendStateToClientsForUser(0, userId); + } break; + case MSG_UPDATE_ACTIVE_WINDOW: { + final int windowId = msg.arg1; + final int eventType = msg.arg2; + mSecurityPolicy.updateActiveWindow(windowId, eventType); + } break; + case MSG_ANNOUNCE_NEW_USER_IF_NEEDED: { + announceNewUserIfNeeded(); + } break; + case MSG_UPDATE_INPUT_FILTER: { + UserState userState = (UserState) msg.obj; + updateInputFilter(userState); + } break; + case MSG_SHOW_ENABLED_TOUCH_EXPLORATION_DIALOG: { + Service service = (Service) msg.obj; + showEnableTouchExplorationDialog(service); + } break; + } + } + + private void announceNewUserIfNeeded() { + synchronized (mLock) { + UserState userState = getCurrentUserStateLocked(); + if (userState.mIsAccessibilityEnabled) { + UserManager userManager = (UserManager) mContext.getSystemService( + Context.USER_SERVICE); + String message = mContext.getString(R.string.user_switched, + userManager.getUserInfo(mCurrentUserId).name); + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_ANNOUNCEMENT); + event.getText().add(message); + event.setWindowId(mSecurityPolicy.getRetrievalAllowingWindowLocked()); + sendAccessibilityEvent(event, mCurrentUserId); + } + } + } + + private void sendStateToClientsForUser(int clientState, int userId) { + final UserState userState; + synchronized (mLock) { + userState = getUserStateLocked(userId); + } + sendStateToClients(clientState, userState.mClients); + } + + private void sendStateToClients(int clientState, + RemoteCallbackList<IAccessibilityManagerClient> clients) { + try { + final int userClientCount = clients.beginBroadcast(); + for (int i = 0; i < userClientCount; i++) { + IAccessibilityManagerClient client = clients.getBroadcastItem(i); + try { + client.setState(clientState); + } catch (RemoteException re) { + /* ignore */ + } + } + } finally { + clients.finishBroadcast(); + } + } + } + + private PendingEvent obtainPendingEventLocked(KeyEvent event, int policyFlags, int sequence) { + PendingEvent pendingEvent = mPendingEventPool.acquire(); + if (pendingEvent == null) { + pendingEvent = new PendingEvent(); + } + pendingEvent.event = event; + pendingEvent.policyFlags = policyFlags; + pendingEvent.sequence = sequence; + return pendingEvent; + } + + private void recyclePendingEventLocked(PendingEvent pendingEvent) { + pendingEvent.clear(); + mPendingEventPool.release(pendingEvent); + } + + /** + * This class represents an accessibility service. It stores all per service + * data required for the service management, provides API for starting/stopping the + * service and is responsible for adding/removing the service in the data structures + * for service management. The class also exposes configuration interface that is + * passed to the service it represents as soon it is bound. It also serves as the + * connection for the service. + */ + class Service extends IAccessibilityServiceConnection.Stub + implements ServiceConnection, DeathRecipient {; + + final int mUserId; + + int mId = 0; + + AccessibilityServiceInfo mAccessibilityServiceInfo; + + IBinder mService; + + IAccessibilityServiceClient mServiceInterface; + + int mEventTypes; + + int mFeedbackType; + + Set<String> mPackageNames = new HashSet<String>(); + + boolean mIsDefault; + + boolean mRequestTouchExplorationMode; + + boolean mRequestEnhancedWebAccessibility; + + boolean mRequestFilterKeyEvents; + + int mFetchFlags; + + long mNotificationTimeout; + + ComponentName mComponentName; + + Intent mIntent; + + boolean mIsAutomation; + + final Rect mTempBounds = new Rect(); + + final ResolveInfo mResolveInfo; + + // the events pending events to be dispatched to this service + final SparseArray<AccessibilityEvent> mPendingEvents = + new SparseArray<AccessibilityEvent>(); + + final KeyEventDispatcher mKeyEventDispatcher = new KeyEventDispatcher(); + + boolean mWasConnectedAndDied; + + // Handler only for dispatching accessibility events since we use event + // types as message types allowing us to remove messages per event type. + public Handler mEventDispatchHandler = new Handler(mMainHandler.getLooper()) { + @Override + public void handleMessage(Message message) { + final int eventType = message.what; + notifyAccessibilityEventInternal(eventType); + } + }; + + // Handler for scheduling method invocations on the main thread. + public InvocationHandler mInvocationHandler = new InvocationHandler( + mMainHandler.getLooper()); + + public Service(int userId, ComponentName componentName, + AccessibilityServiceInfo accessibilityServiceInfo) { + mUserId = userId; + mResolveInfo = accessibilityServiceInfo.getResolveInfo(); + mId = sIdCounter++; + mComponentName = componentName; + mAccessibilityServiceInfo = accessibilityServiceInfo; + mIsAutomation = (sFakeAccessibilityServiceComponentName.equals(componentName)); + if (!mIsAutomation) { + mIntent = new Intent().setComponent(mComponentName); + mIntent.putExtra(Intent.EXTRA_CLIENT_LABEL, + com.android.internal.R.string.accessibility_binding_label); + mIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity( + mContext, 0, new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS), 0)); + } + setDynamicallyConfigurableProperties(accessibilityServiceInfo); + } + + public void setDynamicallyConfigurableProperties(AccessibilityServiceInfo info) { + mEventTypes = info.eventTypes; + mFeedbackType = info.feedbackType; + String[] packageNames = info.packageNames; + if (packageNames != null) { + mPackageNames.addAll(Arrays.asList(packageNames)); + } + mNotificationTimeout = info.notificationTimeout; + mIsDefault = (info.flags & DEFAULT) != 0; + + if (mIsAutomation || info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion + >= Build.VERSION_CODES.JELLY_BEAN) { + if ((info.flags & AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0) { + mFetchFlags |= AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + } else { + mFetchFlags &= ~AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + } + } + + if ((info.flags & AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS) != 0) { + mFetchFlags |= AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS; + } else { + mFetchFlags &= ~AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS; + } + + mRequestTouchExplorationMode = (info.flags + & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0; + mRequestEnhancedWebAccessibility = (info.flags + & AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY) != 0; + mRequestFilterKeyEvents = (info.flags + & AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS) != 0; + } + + /** + * Binds to the accessibility service. + * + * @return True if binding is successful. + */ + public boolean bindLocked() { + UserState userState = getUserStateLocked(mUserId); + if (!mIsAutomation) { + if (mService == null && mContext.bindServiceAsUser( + mIntent, this, Context.BIND_AUTO_CREATE, new UserHandle(mUserId))) { + userState.mBindingServices.add(mComponentName); + } + } else { + userState.mBindingServices.add(mComponentName); + mService = userState.mUiAutomationServiceClient.asBinder(); + onServiceConnected(mComponentName, mService); + userState.mUiAutomationService = this; + } + return false; + } + + /** + * Unbinds form the accessibility service and removes it from the data + * structures for service management. + * + * @return True if unbinding is successful. + */ + public boolean unbindLocked() { + if (mService == null) { + return false; + } + UserState userState = getUserStateLocked(mUserId); + mKeyEventDispatcher.flush(); + if (!mIsAutomation) { + mContext.unbindService(this); + } else { + userState.destroyUiAutomationService(); + } + removeServiceLocked(this, userState); + resetLocked(); + return true; + } + + public boolean canReceiveEventsLocked() { + return (mEventTypes != 0 && mFeedbackType != 0 && mService != null); + } + + @Override + public void setOnKeyEventResult(boolean handled, int sequence) { + mKeyEventDispatcher.setOnKeyEventResult(handled, sequence); + } + + @Override + public AccessibilityServiceInfo getServiceInfo() { + synchronized (mLock) { + return mAccessibilityServiceInfo; + } + } + + @Override + public void setServiceInfo(AccessibilityServiceInfo info) { + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + // If the XML manifest had data to configure the service its info + // should be already set. In such a case update only the dynamically + // configurable properties. + AccessibilityServiceInfo oldInfo = mAccessibilityServiceInfo; + if (oldInfo != null) { + oldInfo.updateDynamicallyConfigurableProperties(info); + setDynamicallyConfigurableProperties(oldInfo); + } else { + setDynamicallyConfigurableProperties(info); + } + UserState userState = getUserStateLocked(mUserId); + onUserStateChangedLocked(userState); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void onServiceConnected(ComponentName componentName, IBinder service) { + synchronized (mLock) { + mService = service; + mServiceInterface = IAccessibilityServiceClient.Stub.asInterface(service); + UserState userState = getUserStateLocked(mUserId); + addServiceLocked(this, userState); + if (userState.mBindingServices.contains(mComponentName) || mWasConnectedAndDied) { + userState.mBindingServices.remove(mComponentName); + mWasConnectedAndDied = false; + try { + mServiceInterface.setConnection(this, mId); + onUserStateChangedLocked(userState); + } catch (RemoteException re) { + Slog.w(LOG_TAG, "Error while setting connection for service: " + + service, re); + binderDied(); + } + } else { + binderDied(); + } + } + } + + @Override + public boolean findAccessibilityNodeInfosByViewId(int accessibilityWindowId, + long accessibilityNodeId, String viewIdResName, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + final boolean permissionGranted = mSecurityPolicy.canRetrieveWindowContent(this); + if (!permissionGranted) { + return false; + } else { + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); + try { + connection.findAccessibilityNodeInfosByViewId(accessibilityNodeId, + viewIdResName, interactionId, callback, mFetchFlags, interrogatingPid, + interrogatingTid, spec); + return true; + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error findAccessibilityNodeInfoByViewId()."); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return false; + } + + @Override + public boolean findAccessibilityNodeInfosByText(int accessibilityWindowId, + long accessibilityNodeId, String text, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + final boolean permissionGranted = + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); + if (!permissionGranted) { + return false; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); + try { + connection.findAccessibilityNodeInfosByText(accessibilityNodeId, text, + interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid, + spec); + return true; + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfosByText()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return false; + } + + @Override + public boolean findAccessibilityNodeInfoByAccessibilityId( + int accessibilityWindowId, long accessibilityNodeId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + long interrogatingTid) throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + final boolean permissionGranted = + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); + if (!permissionGranted) { + return false; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); + try { + connection.findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId, + interactionId, callback, mFetchFlags | flags, interrogatingPid, + interrogatingTid, spec); + return true; + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return false; + } + + @Override + public boolean findFocus(int accessibilityWindowId, long accessibilityNodeId, + int focusType, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + final boolean permissionGranted = + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); + if (!permissionGranted) { + return false; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); + try { + connection.findFocus(accessibilityNodeId, focusType, interactionId, callback, + mFetchFlags, interrogatingPid, interrogatingTid, spec); + return true; + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling findAccessibilityFocus()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return false; + } + + @Override + public boolean focusSearch(int accessibilityWindowId, long accessibilityNodeId, + int direction, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + final boolean permissionGranted = + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); + if (!permissionGranted) { + return false; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); + try { + connection.focusSearch(accessibilityNodeId, direction, interactionId, callback, + mFetchFlags, interrogatingPid, interrogatingTid, spec); + return true; + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling accessibilityFocusSearch()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return false; + } + + @Override + public boolean performAccessibilityAction(int accessibilityWindowId, + long accessibilityNodeId, int action, Bundle arguments, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + final boolean permissionGranted = mSecurityPolicy.canPerformActionLocked(this, + resolvedWindowId, action, arguments); + if (!permissionGranted) { + return false; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + try { + connection.performAccessibilityAction(accessibilityNodeId, action, arguments, + interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid); + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling performAccessibilityAction()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return true; + } + + public boolean performGlobalAction(int action) { + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + } + final long identity = Binder.clearCallingIdentity(); + try { + switch (action) { + case AccessibilityService.GLOBAL_ACTION_BACK: { + sendDownAndUpKeyEvents(KeyEvent.KEYCODE_BACK); + } return true; + case AccessibilityService.GLOBAL_ACTION_HOME: { + sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HOME); + } return true; + case AccessibilityService.GLOBAL_ACTION_RECENTS: { + openRecents(); + } return true; + case AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS: { + expandNotifications(); + } return true; + case AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS: { + expandQuickSettings(); + } return true; + } + return false; + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { + mSecurityPolicy.enforceCallingPermission(Manifest.permission.DUMP, FUNCTION_DUMP); + synchronized (mLock) { + pw.append("Service[label=" + mAccessibilityServiceInfo.getResolveInfo() + .loadLabel(mContext.getPackageManager())); + pw.append(", feedbackType" + + AccessibilityServiceInfo.feedbackTypeToString(mFeedbackType)); + pw.append(", capabilities=" + mAccessibilityServiceInfo.getCapabilities()); + pw.append(", eventTypes=" + + AccessibilityEvent.eventTypeToString(mEventTypes)); + pw.append(", notificationTimeout=" + mNotificationTimeout); + pw.append("]"); + } + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + /* do nothing - #binderDied takes care */ + } + + public void linkToOwnDeathLocked() throws RemoteException { + mService.linkToDeath(this, 0); + } + + public void unlinkToOwnDeathLocked() { + mService.unlinkToDeath(this, 0); + } + + public void resetLocked() { + try { + // Clear the proxy in the other process so this + // IAccessibilityServiceConnection can be garbage collected. + mServiceInterface.setConnection(null, mId); + } catch (RemoteException re) { + /* ignore */ + } + mService = null; + mServiceInterface = null; + } + + public boolean isConnectedLocked() { + return (mService != null); + } + + public void binderDied() { + synchronized (mLock) { + // It is possible that this service's package was force stopped during + // whose handling the death recipient is unlinked and still get a call + // on binderDied since the call was made before we unlink but was + // waiting on the lock we held during the force stop handling. + if (!isConnectedLocked()) { + return; + } + mWasConnectedAndDied = true; + mKeyEventDispatcher.flush(); + UserState userState = getUserStateLocked(mUserId); + // The death recipient is unregistered in removeServiceLocked + removeServiceLocked(this, userState); + resetLocked(); + if (mIsAutomation) { + // We no longer have an automation service, so restore + // the state based on values in the settings database. + userState.mInstalledServices.remove(mAccessibilityServiceInfo); + userState.mEnabledServices.remove(mComponentName); + userState.destroyUiAutomationService(); + } + } + } + + /** + * Performs a notification for an {@link AccessibilityEvent}. + * + * @param event The event. + */ + public void notifyAccessibilityEvent(AccessibilityEvent event) { + synchronized (mLock) { + final int eventType = event.getEventType(); + // Make a copy since during dispatch it is possible the event to + // be modified to remove its source if the receiving service does + // not have permission to access the window content. + AccessibilityEvent newEvent = AccessibilityEvent.obtain(event); + AccessibilityEvent oldEvent = mPendingEvents.get(eventType); + mPendingEvents.put(eventType, newEvent); + + final int what = eventType; + if (oldEvent != null) { + mEventDispatchHandler.removeMessages(what); + oldEvent.recycle(); + } + + Message message = mEventDispatchHandler.obtainMessage(what); + mEventDispatchHandler.sendMessageDelayed(message, mNotificationTimeout); + } + } + + /** + * Notifies an accessibility service client for a scheduled event given the event type. + * + * @param eventType The type of the event to dispatch. + */ + private void notifyAccessibilityEventInternal(int eventType) { + IAccessibilityServiceClient listener; + AccessibilityEvent event; + + synchronized (mLock) { + listener = mServiceInterface; + + // If the service died/was disabled while the message for dispatching + // the accessibility event was propagating the listener may be null. + if (listener == null) { + return; + } + + event = mPendingEvents.get(eventType); + + // Check for null here because there is a concurrent scenario in which this + // happens: 1) A binder thread calls notifyAccessibilityServiceDelayedLocked + // which posts a message for dispatching an event. 2) The message is pulled + // from the queue by the handler on the service thread and the latter is + // just about to acquire the lock and call this method. 3) Now another binder + // thread acquires the lock calling notifyAccessibilityServiceDelayedLocked + // so the service thread waits for the lock; 4) The binder thread replaces + // the event with a more recent one (assume the same event type) and posts a + // dispatch request releasing the lock. 5) Now the main thread is unblocked and + // dispatches the event which is removed from the pending ones. 6) And ... now + // the service thread handles the last message posted by the last binder call + // but the event is already dispatched and hence looking it up in the pending + // ones yields null. This check is much simpler that keeping count for each + // event type of each service to catch such a scenario since only one message + // is processed at a time. + if (event == null) { + return; + } + + mPendingEvents.remove(eventType); + if (mSecurityPolicy.canRetrieveWindowContent(this)) { + event.setConnectionId(mId); + } else { + event.setSource(null); + } + event.setSealed(true); + } + + try { + listener.onAccessibilityEvent(event); + if (DEBUG) { + Slog.i(LOG_TAG, "Event " + event + " sent to " + listener); + } + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error during sending " + event + " to " + listener, re); + } finally { + event.recycle(); + } + } + + public void notifyGesture(int gestureId) { + mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_GESTURE, + gestureId, 0).sendToTarget(); + } + + public void notifyKeyEvent(KeyEvent event, int policyFlags) { + mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_KEY_EVENT, + policyFlags, 0, event).sendToTarget(); + } + + public void notifyClearAccessibilityNodeInfoCache() { + mInvocationHandler.sendEmptyMessage( + InvocationHandler.MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE); + } + + private void notifyGestureInternal(int gestureId) { + IAccessibilityServiceClient listener = mServiceInterface; + if (listener != null) { + try { + listener.onGesture(gestureId); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error during sending gesture " + gestureId + + " to " + mService, re); + } + } + } + + private void notifyKeyEventInternal(KeyEvent event, int policyFlags) { + mKeyEventDispatcher.notifyKeyEvent(event, policyFlags); + } + + private void notifyClearAccessibilityNodeInfoCacheInternal() { + IAccessibilityServiceClient listener = mServiceInterface; + if (listener != null) { + try { + listener.clearAccessibilityNodeInfoCache(); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error during requesting accessibility info cache" + + " to be cleared.", re); + } + } + } + + private void sendDownAndUpKeyEvents(int keyCode) { + final long token = Binder.clearCallingIdentity(); + + // Inject down. + final long downTime = SystemClock.uptimeMillis(); + KeyEvent down = KeyEvent.obtain(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FROM_SYSTEM, + InputDevice.SOURCE_KEYBOARD, null); + InputManager.getInstance().injectInputEvent(down, + InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + down.recycle(); + + // Inject up. + final long upTime = SystemClock.uptimeMillis(); + KeyEvent up = KeyEvent.obtain(downTime, upTime, KeyEvent.ACTION_UP, keyCode, 0, 0, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FROM_SYSTEM, + InputDevice.SOURCE_KEYBOARD, null); + InputManager.getInstance().injectInputEvent(up, + InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + up.recycle(); + + Binder.restoreCallingIdentity(token); + } + + private void expandNotifications() { + final long token = Binder.clearCallingIdentity(); + + StatusBarManager statusBarManager = (StatusBarManager) mContext.getSystemService( + android.app.Service.STATUS_BAR_SERVICE); + statusBarManager.expandNotificationsPanel(); + + Binder.restoreCallingIdentity(token); + } + + private void expandQuickSettings() { + final long token = Binder.clearCallingIdentity(); + + StatusBarManager statusBarManager = (StatusBarManager) mContext.getSystemService( + android.app.Service.STATUS_BAR_SERVICE); + statusBarManager.expandSettingsPanel(); + + Binder.restoreCallingIdentity(token); + } + + private void openRecents() { + final long token = Binder.clearCallingIdentity(); + + IStatusBarService statusBarService = IStatusBarService.Stub.asInterface( + ServiceManager.getService("statusbar")); + try { + statusBarService.toggleRecentApps(); + } catch (RemoteException e) { + Slog.e(LOG_TAG, "Error toggling recent apps."); + } + + Binder.restoreCallingIdentity(token); + } + + private IAccessibilityInteractionConnection getConnectionLocked(int windowId) { + if (DEBUG) { + Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId); + } + AccessibilityConnectionWrapper wrapper = mGlobalInteractionConnections.get(windowId); + if (wrapper == null) { + wrapper = getCurrentUserStateLocked().mInteractionConnections.get(windowId); + } + if (wrapper != null && wrapper.mConnection != null) { + return wrapper.mConnection; + } + if (DEBUG) { + Slog.e(LOG_TAG, "No interaction connection to window: " + windowId); + } + return null; + } + + private int resolveAccessibilityWindowIdLocked(int accessibilityWindowId) { + if (accessibilityWindowId == AccessibilityNodeInfo.ACTIVE_WINDOW_ID) { + return mSecurityPolicy.mActiveWindowId; + } + return accessibilityWindowId; + } + + private MagnificationSpec getCompatibleMagnificationSpec(int windowId) { + try { + IBinder windowToken = mGlobalWindowTokens.get(windowId); + if (windowToken == null) { + windowToken = getCurrentUserStateLocked().mWindowTokens.get(windowId); + } + if (windowToken != null) { + return mWindowManagerService.getCompatibleMagnificationSpecForWindow( + windowToken); + } + } catch (RemoteException re) { + /* ignore */ + } + return null; + } + + private final class InvocationHandler extends Handler { + + public static final int MSG_ON_GESTURE = 1; + public static final int MSG_ON_KEY_EVENT = 2; + public static final int MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE = 3; + public static final int MSG_ON_KEY_EVENT_TIMEOUT = 4; + + public InvocationHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message message) { + final int type = message.what; + switch (type) { + case MSG_ON_GESTURE: { + final int gestureId = message.arg1; + notifyGestureInternal(gestureId); + } break; + case MSG_ON_KEY_EVENT: { + KeyEvent event = (KeyEvent) message.obj; + final int policyFlags = message.arg1; + notifyKeyEventInternal(event, policyFlags); + } break; + case MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE: { + notifyClearAccessibilityNodeInfoCacheInternal(); + } break; + case MSG_ON_KEY_EVENT_TIMEOUT: { + PendingEvent eventState = (PendingEvent) message.obj; + setOnKeyEventResult(false, eventState.sequence); + } break; + default: { + throw new IllegalArgumentException("Unknown message: " + type); + } + } + } + } + + private final class KeyEventDispatcher { + + private static final long ON_KEY_EVENT_TIMEOUT_MILLIS = 500; + + private PendingEvent mPendingEvents; + + private final InputEventConsistencyVerifier mSentEventsVerifier = + InputEventConsistencyVerifier.isInstrumentationEnabled() + ? new InputEventConsistencyVerifier( + this, 0, KeyEventDispatcher.class.getSimpleName()) : null; + + public void notifyKeyEvent(KeyEvent event, int policyFlags) { + final PendingEvent pendingEvent; + + synchronized (mLock) { + pendingEvent = addPendingEventLocked(event, policyFlags); + } + + Message message = mInvocationHandler.obtainMessage( + InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT, pendingEvent); + mInvocationHandler.sendMessageDelayed(message, ON_KEY_EVENT_TIMEOUT_MILLIS); + + try { + // Accessibility services are exclusively not in the system + // process, therefore no need to clone the motion event to + // prevent tampering. It will be cloned in the IPC call. + mServiceInterface.onKeyEvent(pendingEvent.event, pendingEvent.sequence); + } catch (RemoteException re) { + setOnKeyEventResult(false, pendingEvent.sequence); + } + } + + public void setOnKeyEventResult(boolean handled, int sequence) { + synchronized (mLock) { + PendingEvent pendingEvent = removePendingEventLocked(sequence); + if (pendingEvent != null) { + mInvocationHandler.removeMessages( + InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT, + pendingEvent); + pendingEvent.handled = handled; + finishPendingEventLocked(pendingEvent); + } + } + } + + public void flush() { + synchronized (mLock) { + cancelAllPendingEventsLocked(); + if (mSentEventsVerifier != null) { + mSentEventsVerifier.reset(); + } + } + } + + private PendingEvent addPendingEventLocked(KeyEvent event, int policyFlags) { + final int sequence = event.getSequenceNumber(); + PendingEvent pendingEvent = obtainPendingEventLocked(event, policyFlags, sequence); + pendingEvent.next = mPendingEvents; + mPendingEvents = pendingEvent; + return pendingEvent; + } + + private PendingEvent removePendingEventLocked(int sequence) { + PendingEvent previous = null; + PendingEvent current = mPendingEvents; + + while (current != null) { + if (current.sequence == sequence) { + if (previous != null) { + previous.next = current.next; + } else { + mPendingEvents = current.next; + } + current.next = null; + return current; + } + previous = current; + current = current.next; + } + return null; + } + + private void finishPendingEventLocked(PendingEvent pendingEvent) { + if (!pendingEvent.handled) { + sendKeyEventToInputFilter(pendingEvent.event, pendingEvent.policyFlags); + } + // Nullify the event since we do not want it to be + // recycled yet. It will be sent to the input filter. + pendingEvent.event = null; + recyclePendingEventLocked(pendingEvent); + } + + private void sendKeyEventToInputFilter(KeyEvent event, int policyFlags) { + if (DEBUG) { + Slog.i(LOG_TAG, "Injecting event: " + event); + } + if (mSentEventsVerifier != null) { + mSentEventsVerifier.onKeyEvent(event, 0); + } + policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER; + mMainHandler.obtainMessage(MainHandler.MSG_SEND_KEY_EVENT_TO_INPUT_FILTER, + policyFlags, 0, event).sendToTarget(); + } + + private void cancelAllPendingEventsLocked() { + while (mPendingEvents != null) { + PendingEvent pendingEvent = removePendingEventLocked(mPendingEvents.sequence); + pendingEvent.handled = false; + mInvocationHandler.removeMessages(InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT, + pendingEvent); + finishPendingEventLocked(pendingEvent); + } + } + } + } + + private static final class PendingEvent { + PendingEvent next; + + KeyEvent event; + int policyFlags; + int sequence; + boolean handled; + + public void clear() { + if (event != null) { + event.recycle(); + event = null; + } + next = null; + policyFlags = 0; + sequence = 0; + handled = false; + } + } + + final class SecurityPolicy { + private static final int VALID_ACTIONS = + AccessibilityNodeInfo.ACTION_CLICK + | AccessibilityNodeInfo.ACTION_LONG_CLICK + | AccessibilityNodeInfo.ACTION_FOCUS + | AccessibilityNodeInfo.ACTION_CLEAR_FOCUS + | AccessibilityNodeInfo.ACTION_SELECT + | AccessibilityNodeInfo.ACTION_CLEAR_SELECTION + | AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS + | AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS + | AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY + | AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY + | AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT + | AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT + | AccessibilityNodeInfo.ACTION_SCROLL_FORWARD + | AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD + | AccessibilityNodeInfo.ACTION_COPY + | AccessibilityNodeInfo.ACTION_PASTE + | AccessibilityNodeInfo.ACTION_CUT + | AccessibilityNodeInfo.ACTION_SET_SELECTION + | AccessibilityNodeInfo.ACTION_EXPAND + | AccessibilityNodeInfo.ACTION_COLLAPSE + | AccessibilityNodeInfo.ACTION_DISMISS; + + private static final int RETRIEVAL_ALLOWING_EVENT_TYPES = + AccessibilityEvent.TYPE_VIEW_CLICKED + | AccessibilityEvent.TYPE_VIEW_FOCUSED + | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER + | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT + | AccessibilityEvent.TYPE_VIEW_LONG_CLICKED + | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED + | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + | AccessibilityEvent.TYPE_VIEW_SELECTED + | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED + | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED + | AccessibilityEvent.TYPE_VIEW_SCROLLED + | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED + | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED; + + private int mActiveWindowId; + private boolean mTouchInteractionInProgress; + + private boolean canDispatchAccessibilityEvent(AccessibilityEvent event) { + final int eventType = event.getEventType(); + switch (eventType) { + // All events that are for changes in a global window + // state should *always* be dispatched. + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: + case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: + // All events generated by the user touching the + // screen should *always* be dispatched. + case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START: + case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END: + case AccessibilityEvent.TYPE_GESTURE_DETECTION_START: + case AccessibilityEvent.TYPE_GESTURE_DETECTION_END: + case AccessibilityEvent.TYPE_TOUCH_INTERACTION_START: + case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END: + // These will change the active window, so dispatch. + case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: + case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: { + return true; + } + // All events for changes in window content should be + // dispatched *only* if this window is the active one. + default: + return event.getWindowId() == mActiveWindowId; + } + } + + public void updateEventSourceLocked(AccessibilityEvent event) { + if ((event.getEventType() & RETRIEVAL_ALLOWING_EVENT_TYPES) == 0) { + event.setSource(null); + } + } + + public void updateActiveWindow(int windowId, int eventType) { + // The active window is either the window that has input focus or + // the window that the user is currently touching. If the user is + // touching a window that does not have input focus as soon as the + // the user stops touching that window the focused window becomes + // the active one. + switch (eventType) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: { + if (getFocusedWindowId() == windowId) { + mActiveWindowId = windowId; + } + } break; + case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: { + // Do not allow delayed hover events to confuse us + // which the active window is. + if (mTouchInteractionInProgress) { + mActiveWindowId = windowId; + } + } break; + } + } + + public void onTouchInteractionStart() { + mTouchInteractionInProgress = true; + } + + public void onTouchInteractionEnd() { + mTouchInteractionInProgress = false; + // We want to set the active window to be current immediately + // after the user has stopped touching the screen since if the + // user types with the IME he should get a feedback for the + // letter typed in the text view which is in the input focused + // window. Note that we always deliver hover accessibility events + // (they are a result of user touching the screen) so change of + // the active window before all hover accessibility events from + // the touched window are delivered is fine. + mActiveWindowId = getFocusedWindowId(); + } + + public int getRetrievalAllowingWindowLocked() { + return mActiveWindowId; + } + + public boolean canGetAccessibilityNodeInfoLocked(Service service, int windowId) { + return canRetrieveWindowContent(service) && isRetrievalAllowingWindow(windowId); + } + + public boolean canPerformActionLocked(Service service, int windowId, int action, + Bundle arguments) { + return canRetrieveWindowContent(service) + && isRetrievalAllowingWindow(windowId) + && isActionPermitted(action); + } + + public boolean canRetrieveWindowContent(Service service) { + return (service.mAccessibilityServiceInfo.getCapabilities() + & AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT) != 0; + } + + public void enforceCanRetrieveWindowContent(Service service) throws RemoteException { + // This happens due to incorrect registration so make it apparent. + if (!canRetrieveWindowContent(service)) { + Slog.e(LOG_TAG, "Accessibility serivce " + service.mComponentName + " does not " + + "declare android:canRetrieveWindowContent."); + throw new RemoteException(); + } + } + + public int resolveCallingUserIdEnforcingPermissionsLocked(int userId) { + final int callingUid = Binder.getCallingUid(); + if (callingUid == 0 + || callingUid == Process.SYSTEM_UID + || callingUid == Process.SHELL_UID) { + return mCurrentUserId; + } + final int callingUserId = UserHandle.getUserId(callingUid); + if (callingUserId == userId) { + return userId; + } + if (!hasPermission(Manifest.permission.INTERACT_ACROSS_USERS) + && !hasPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL)) { + throw new SecurityException("Call from user " + callingUserId + " as user " + + userId + " without permission INTERACT_ACROSS_USERS or " + + "INTERACT_ACROSS_USERS_FULL not allowed."); + } + if (userId == UserHandle.USER_CURRENT + || userId == UserHandle.USER_CURRENT_OR_SELF) { + return mCurrentUserId; + } + throw new IllegalArgumentException("Calling user can be changed to only " + + "UserHandle.USER_CURRENT or UserHandle.USER_CURRENT_OR_SELF."); + } + + public boolean isCallerInteractingAcrossUsers(int userId) { + final int callingUid = Binder.getCallingUid(); + return (Binder.getCallingPid() == android.os.Process.myPid() + || callingUid == Process.SHELL_UID + || userId == UserHandle.USER_CURRENT + || userId == UserHandle.USER_CURRENT_OR_SELF); + } + + private boolean isRetrievalAllowingWindow(int windowId) { + return (mActiveWindowId == windowId); + } + + private boolean isActionPermitted(int action) { + return (VALID_ACTIONS & action) != 0; + } + + private void enforceCallingPermission(String permission, String function) { + if (OWN_PROCESS_ID == Binder.getCallingPid()) { + return; + } + if (!hasPermission(permission)) { + throw new SecurityException("You do not have " + permission + + " required to call " + function + " from pid=" + + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()); + } + } + + private boolean hasPermission(String permission) { + return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED; + } + + private int getFocusedWindowId() { + try { + // We call this only on window focus change or after touch + // exploration gesture end and the shown windows are not that + // many, so the linear look up is just fine. + IBinder token = mWindowManagerService.getFocusedWindowToken(); + if (token != null) { + synchronized (mLock) { + int windowId = getFocusedWindowIdLocked(token, mGlobalWindowTokens); + if (windowId < 0) { + windowId = getFocusedWindowIdLocked(token, + getCurrentUserStateLocked().mWindowTokens); + } + return windowId; + } + } + } catch (RemoteException re) { + /* ignore */ + } + return -1; + } + + private int getFocusedWindowIdLocked(IBinder token, SparseArray<IBinder> windows) { + final int windowCount = windows.size(); + for (int i = 0; i < windowCount; i++) { + if (windows.valueAt(i) == token) { + return windows.keyAt(i); + } + } + return -1; + } + } + + private class UserState { + public final int mUserId; + + // Non-transient state. + + public final RemoteCallbackList<IAccessibilityManagerClient> mClients = + new RemoteCallbackList<IAccessibilityManagerClient>(); + + public final SparseArray<AccessibilityConnectionWrapper> mInteractionConnections = + new SparseArray<AccessibilityConnectionWrapper>(); + + public final SparseArray<IBinder> mWindowTokens = new SparseArray<IBinder>(); + + // Transient state. + + public final CopyOnWriteArrayList<Service> mBoundServices = + new CopyOnWriteArrayList<Service>(); + + public final Map<ComponentName, Service> mComponentNameToServiceMap = + new HashMap<ComponentName, Service>(); + + public final List<AccessibilityServiceInfo> mInstalledServices = + new ArrayList<AccessibilityServiceInfo>(); + + public final Set<ComponentName> mBindingServices = new HashSet<ComponentName>(); + + public final Set<ComponentName> mEnabledServices = new HashSet<ComponentName>(); + + public final Set<ComponentName> mTouchExplorationGrantedServices = + new HashSet<ComponentName>(); + + public int mHandledFeedbackTypes = 0; + + public int mLastSentClientState = -1; + + public boolean mIsAccessibilityEnabled; + public boolean mIsTouchExplorationEnabled; + public boolean mIsEnhancedWebAccessibilityEnabled; + public boolean mIsDisplayMagnificationEnabled; + public boolean mIsFilterKeyEventsEnabled; + public boolean mHasDisplayColorAdjustment; + + private Service mUiAutomationService; + private IAccessibilityServiceClient mUiAutomationServiceClient; + + private IBinder mUiAutomationServiceOwner; + private final DeathRecipient mUiAutomationSerivceOnwerDeathRecipient = + new DeathRecipient() { + @Override + public void binderDied() { + mUiAutomationServiceOwner.unlinkToDeath( + mUiAutomationSerivceOnwerDeathRecipient, 0); + mUiAutomationServiceOwner = null; + if (mUiAutomationService != null) { + mUiAutomationService.binderDied(); + } + } + }; + + public UserState(int userId) { + mUserId = userId; + } + + public int getClientState() { + int clientState = 0; + if (mIsAccessibilityEnabled) { + clientState |= AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED; + } + // Touch exploration relies on enabled accessibility. + if (mIsAccessibilityEnabled && mIsTouchExplorationEnabled) { + clientState |= AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED; + } + return clientState; + } + + public void onSwitchToAnotherUser() { + // Clear UI test automation state. + if (mUiAutomationService != null) { + mUiAutomationService.binderDied(); + } + + // Unbind all services. + unbindAllServicesLocked(this); + + // Clear service management state. + mBoundServices.clear(); + mBindingServices.clear(); + + // Clear event management state. + mHandledFeedbackTypes = 0; + mLastSentClientState = -1; + + // Clear state persisted in settings. + mEnabledServices.clear(); + mTouchExplorationGrantedServices.clear(); + mIsAccessibilityEnabled = false; + mIsTouchExplorationEnabled = false; + mIsEnhancedWebAccessibilityEnabled = false; + mIsDisplayMagnificationEnabled = false; + } + + public void destroyUiAutomationService() { + mUiAutomationService = null; + mUiAutomationServiceClient = null; + if (mUiAutomationServiceOwner != null) { + mUiAutomationServiceOwner.unlinkToDeath( + mUiAutomationSerivceOnwerDeathRecipient, 0); + mUiAutomationServiceOwner = null; + } + } + } + + private final class AccessibilityContentObserver extends ContentObserver { + + private final Uri mAccessibilityEnabledUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_ENABLED); + + private final Uri mTouchExplorationEnabledUri = Settings.Secure.getUriFor( + Settings.Secure.TOUCH_EXPLORATION_ENABLED); + + private final Uri mDisplayMagnificationEnabledUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED); + + private final Uri mEnabledAccessibilityServicesUri = Settings.Secure.getUriFor( + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); + + private final Uri mTouchExplorationGrantedAccessibilityServicesUri = Settings.Secure + .getUriFor(Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES); + + private final Uri mEnhancedWebAccessibilityUri = Settings.Secure + .getUriFor(Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION); + + private final Uri mDisplayContrastEnabledUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_CONTRAST_ENABLED); + private final Uri mDisplayContrastUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_CONTRAST); + private final Uri mDisplayBrightnessUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_BRIGHTNESS); + + private final Uri mDisplayInversionEnabledUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED); + private final Uri mDisplayInversionUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION); + + private final Uri mDisplayDaltonizerEnabledUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED); + private final Uri mDisplayDaltonizerUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER); + + public AccessibilityContentObserver(Handler handler) { + super(handler); + } + + public void register(ContentResolver contentResolver) { + contentResolver.registerContentObserver(mAccessibilityEnabledUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver(mTouchExplorationEnabledUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver(mDisplayMagnificationEnabledUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver(mEnabledAccessibilityServicesUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mTouchExplorationGrantedAccessibilityServicesUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver(mEnhancedWebAccessibilityUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mDisplayContrastEnabledUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mDisplayContrastUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mDisplayBrightnessUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mDisplayInversionEnabledUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mDisplayInversionUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mDisplayDaltonizerEnabledUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mDisplayDaltonizerUri, false, this, UserHandle.USER_ALL); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (mAccessibilityEnabledUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readAccessibilityEnabledSettingLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mTouchExplorationEnabledUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readTouchExplorationEnabledSettingLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mDisplayMagnificationEnabledUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readDisplayMagnificationEnabledSettingLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mEnabledAccessibilityServicesUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readEnabledAccessibilityServicesLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mTouchExplorationGrantedAccessibilityServicesUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readTouchExplorationGrantedAccessibilityServicesLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mEnhancedWebAccessibilityUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readEnhancedWebAccessibilityEnabledChangedLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mDisplayContrastEnabledUri.equals(uri) + || mDisplayInversionEnabledUri.equals(uri) + || mDisplayDaltonizerEnabledUri.equals(uri) + || mDisplayContrastUri.equals(uri) + || mDisplayBrightnessUri.equals(uri) + || mDisplayInversionUri.equals(uri) + || mDisplayDaltonizerUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readDisplayColorAdjustmentSettingsLocked(userState)) { + updateDisplayColorAdjustmentSettingsLocked(userState); + } + } + } + } + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java b/services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java new file mode 100644 index 0000000..8c93e7b --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java @@ -0,0 +1,93 @@ +/* + ** Copyright 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 com.android.server.accessibility; + +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.accessibility.AccessibilityEvent; + +/** + * Interface for classes that can handle and potentially transform a stream of + * motion and accessibility events. Instances implementing this interface are + * ordered in a sequence to implement a transformation chain. An instance may + * consume, modify, and generate events. It is responsible to deliver the + * output events to the next transformation in the sequence set via + * {@link #setNext(EventStreamTransformation)}. + * + * Note that since instances implementing this interface are transformations + * of the event stream, an instance should work against the event stream + * potentially modified by previous ones. Hence, the order of transformations + * is important. + * + * It is a responsibility of each handler that decides to react to an event + * sequence and prevent any subsequent ones from performing an action to send + * the appropriate cancel event given it has delegated a part of the events + * that belong to the current gesture. This will ensure that subsequent + * transformations will not be left in an inconsistent state and the applications + * see a consistent event stream. + * + * For example, to cancel a {@link KeyEvent} the handler has to emit an event + * with action {@link KeyEvent#ACTION_UP} with the additional flag + * {@link KeyEvent#FLAG_CANCELED}. To cancel a {@link MotionEvent} the handler + * has to send an event with action {@link MotionEvent#ACTION_CANCEL}. + * + * It is a responsibility of each handler that received a cancel event to clear its + * internal state and to propagate the event to the next one to enable subsequent + * transformations to clear their internal state. + * + * It is a responsibility for each transformation to start handling events only + * after an event that designates the start of a well-formed event sequence. + * For example, if it received a down motion event followed by a cancel motion + * event, it should not handle subsequent move and up events until it gets a down. + */ +interface EventStreamTransformation { + + /** + * Receives a motion event. Passed are the event transformed by previous + * transformations and the raw event to which no transformations have + * been applied. + * + * @param event The transformed motion event. + * @param rawEvent The raw motion event. + * @param policyFlags Policy flags for the event. + */ + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags); + + /** + * Receives an accessibility event. + * + * @param event The accessibility event. + */ + public void onAccessibilityEvent(AccessibilityEvent event); + + /** + * Sets the next transformation. + * + * @param next The next transformation. + */ + public void setNext(EventStreamTransformation next); + + /** + * Clears the internal state of this transformation. + */ + public void clear(); + + /** + * Destroys this transformation. + */ + public void onDestroy(); +} diff --git a/services/accessibility/java/com/android/server/accessibility/GestureUtils.java b/services/accessibility/java/com/android/server/accessibility/GestureUtils.java new file mode 100644 index 0000000..b68b09f --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/GestureUtils.java @@ -0,0 +1,102 @@ +package com.android.server.accessibility; + +import android.util.MathUtils; +import android.view.MotionEvent; + +/** + * Some helper functions for gesture detection. + */ +final class GestureUtils { + + private GestureUtils() { + /* cannot be instantiated */ + } + + public static boolean isTap(MotionEvent down, MotionEvent up, int tapTimeSlop, + int tapDistanceSlop, int actionIndex) { + return eventsWithinTimeAndDistanceSlop(down, up, tapTimeSlop, tapDistanceSlop, actionIndex); + } + + public static boolean isMultiTap(MotionEvent firstUp, MotionEvent secondUp, + int multiTapTimeSlop, int multiTapDistanceSlop, int actionIndex) { + return eventsWithinTimeAndDistanceSlop(firstUp, secondUp, multiTapTimeSlop, + multiTapDistanceSlop, actionIndex); + } + + private static boolean eventsWithinTimeAndDistanceSlop(MotionEvent first, MotionEvent second, + int timeout, int distance, int actionIndex) { + if (isTimedOut(first, second, timeout)) { + return false; + } + final double deltaMove = computeDistance(first, second, actionIndex); + if (deltaMove >= distance) { + return false; + } + return true; + } + + public static double computeDistance(MotionEvent first, MotionEvent second, int pointerIndex) { + return MathUtils.dist(first.getX(pointerIndex), first.getY(pointerIndex), + second.getX(pointerIndex), second.getY(pointerIndex)); + } + + public static boolean isTimedOut(MotionEvent firstUp, MotionEvent secondUp, int timeout) { + final long deltaTime = secondUp.getEventTime() - firstUp.getEventTime(); + return (deltaTime >= timeout); + } + + public static boolean isSamePointerContext(MotionEvent first, MotionEvent second) { + return (first.getPointerIdBits() == second.getPointerIdBits() + && first.getPointerId(first.getActionIndex()) + == second.getPointerId(second.getActionIndex())); + } + + /** + * Determines whether a two pointer gesture is a dragging one. + * + * @param event The event with the pointer data. + * @return True if the gesture is a dragging one. + */ + public static boolean isDraggingGesture(float firstPtrDownX, float firstPtrDownY, + float secondPtrDownX, float secondPtrDownY, float firstPtrX, float firstPtrY, + float secondPtrX, float secondPtrY, float maxDraggingAngleCos) { + + // Check if the pointers are moving in the same direction. + final float firstDeltaX = firstPtrX - firstPtrDownX; + final float firstDeltaY = firstPtrY - firstPtrDownY; + + if (firstDeltaX == 0 && firstDeltaY == 0) { + return true; + } + + final float firstMagnitude = + (float) Math.sqrt(firstDeltaX * firstDeltaX + firstDeltaY * firstDeltaY); + final float firstXNormalized = + (firstMagnitude > 0) ? firstDeltaX / firstMagnitude : firstDeltaX; + final float firstYNormalized = + (firstMagnitude > 0) ? firstDeltaY / firstMagnitude : firstDeltaY; + + final float secondDeltaX = secondPtrX - secondPtrDownX; + final float secondDeltaY = secondPtrY - secondPtrDownY; + + if (secondDeltaX == 0 && secondDeltaY == 0) { + return true; + } + + final float secondMagnitude = + (float) Math.sqrt(secondDeltaX * secondDeltaX + secondDeltaY * secondDeltaY); + final float secondXNormalized = + (secondMagnitude > 0) ? secondDeltaX / secondMagnitude : secondDeltaX; + final float secondYNormalized = + (secondMagnitude > 0) ? secondDeltaY / secondMagnitude : secondDeltaY; + + final float angleCos = + firstXNormalized * secondXNormalized + firstYNormalized * secondYNormalized; + + if (angleCos < maxDraggingAngleCos) { + return false; + } + + return true; + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java b/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java new file mode 100644 index 0000000..5f12cf4 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java @@ -0,0 +1,1177 @@ +/* + * 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 com.android.server.accessibility; + +import android.animation.ObjectAnimator; +import android.animation.TypeEvaluator; +import android.animation.ValueAnimator; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Rect; +import android.graphics.Region; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Property; +import android.util.Slog; +import android.view.GestureDetector; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.IMagnificationCallbacks; +import android.view.IWindowManager; +import android.view.MagnificationSpec; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; +import android.view.ScaleGestureDetector; +import android.view.ScaleGestureDetector.OnScaleGestureListener; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.DecelerateInterpolator; + +import com.android.internal.os.SomeArgs; + +import java.util.Locale; + +/** + * This class handles the screen magnification when accessibility is enabled. + * The behavior is as follows: + * + * 1. Triple tap toggles permanent screen magnification which is magnifying + * the area around the location of the triple tap. One can think of the + * location of the triple tap as the center of the magnified viewport. + * For example, a triple tap when not magnified would magnify the screen + * and leave it in a magnified state. A triple tapping when magnified would + * clear magnification and leave the screen in a not magnified state. + * + * 2. Triple tap and hold would magnify the screen if not magnified and enable + * viewport dragging mode until the finger goes up. One can think of this + * mode as a way to move the magnified viewport since the area around the + * moving finger will be magnified to fit the screen. For example, if the + * screen was not magnified and the user triple taps and holds the screen + * would magnify and the viewport will follow the user's finger. When the + * finger goes up the screen will zoom out. If the same user interaction + * is performed when the screen is magnified, the viewport movement will + * be the same but when the finger goes up the screen will stay magnified. + * In other words, the initial magnified state is sticky. + * + * 3. Pinching with any number of additional fingers when viewport dragging + * is enabled, i.e. the user triple tapped and holds, would adjust the + * magnification scale which will become the current default magnification + * scale. The next time the user magnifies the same magnification scale + * would be used. + * + * 4. When in a permanent magnified state the user can use two or more fingers + * to pan the viewport. Note that in this mode the content is panned as + * opposed to the viewport dragging mode in which the viewport is moved. + * + * 5. When in a permanent magnified state the user can use two or more + * fingers to change the magnification scale which will become the current + * default magnification scale. The next time the user magnifies the same + * magnification scale would be used. + * + * 6. The magnification scale will be persisted in settings and in the cloud. + */ +public final class ScreenMagnifier extends IMagnificationCallbacks.Stub + implements EventStreamTransformation { + + private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName(); + + private static final boolean DEBUG_STATE_TRANSITIONS = false; + private static final boolean DEBUG_DETECTING = false; + private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false; + private static final boolean DEBUG_PANNING = false; + private static final boolean DEBUG_SCALING = false; + private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false; + + private static final int STATE_DELEGATING = 1; + private static final int STATE_DETECTING = 2; + private static final int STATE_VIEWPORT_DRAGGING = 3; + private static final int STATE_MAGNIFIED_INTERACTION = 4; + + private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f; + private static final int MULTI_TAP_TIME_SLOP_ADJUSTMENT = 50; + + private static final int MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED = 1; + private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 2; + private static final int MESSAGE_ON_USER_CONTEXT_CHANGED = 3; + private static final int MESSAGE_ON_ROTATION_CHANGED = 4; + + private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1; + + private static final int MY_PID = android.os.Process.myPid(); + + private final Rect mTempRect = new Rect(); + private final Rect mTempRect1 = new Rect(); + + private final Context mContext; + private final IWindowManager mWindowManager; + private final MagnificationController mMagnificationController; + private final ScreenStateObserver mScreenStateObserver; + + private final DetectingStateHandler mDetectingStateHandler; + private final MagnifiedContentInteractonStateHandler mMagnifiedContentInteractonStateHandler; + private final StateViewportDraggingHandler mStateViewportDraggingHandler; + + private final AccessibilityManagerService mAms; + + private final int mTapTimeSlop = ViewConfiguration.getTapTimeout(); + private final int mMultiTapTimeSlop = + ViewConfiguration.getDoubleTapTimeout() - MULTI_TAP_TIME_SLOP_ADJUSTMENT; + private final int mTapDistanceSlop; + private final int mMultiTapDistanceSlop; + + private final long mLongAnimationDuration; + + private final Region mMagnifiedBounds = new Region(); + + private EventStreamTransformation mNext; + + private int mCurrentState; + private int mPreviousState; + private boolean mTranslationEnabledBeforePan; + + private PointerCoords[] mTempPointerCoords; + private PointerProperties[] mTempPointerProperties; + + private long mDelegatingStateDownTime; + + private boolean mUpdateMagnificationSpecOnNextBoundsChange; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED: { + Region bounds = (Region) message.obj; + handleOnMagnifiedBoundsChanged(bounds); + bounds.recycle(); + } break; + case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: { + SomeArgs args = (SomeArgs) message.obj; + final int left = args.argi1; + final int top = args.argi2; + final int right = args.argi3; + final int bottom = args.argi4; + handleOnRectangleOnScreenRequested(left, top, right, bottom); + args.recycle(); + } break; + case MESSAGE_ON_USER_CONTEXT_CHANGED: { + handleOnUserContextChanged(); + } break; + case MESSAGE_ON_ROTATION_CHANGED: { + final int rotation = message.arg1; + handleOnRotationChanged(rotation); + } break; + } + } + }; + + public ScreenMagnifier(Context context, int displayId, AccessibilityManagerService service) { + mContext = context; + mWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService("window")); + mAms = service; + + mLongAnimationDuration = context.getResources().getInteger( + com.android.internal.R.integer.config_longAnimTime); + mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); + + mDetectingStateHandler = new DetectingStateHandler(); + mStateViewportDraggingHandler = new StateViewportDraggingHandler(); + mMagnifiedContentInteractonStateHandler = new MagnifiedContentInteractonStateHandler( + context); + + mMagnificationController = new MagnificationController(mLongAnimationDuration); + mScreenStateObserver = new ScreenStateObserver(context, mMagnificationController); + + try { + mWindowManager.setMagnificationCallbacks(this); + } catch (RemoteException re) { + /* ignore */ + } + + transitionToState(STATE_DETECTING); + } + + @Override + public void onMagnifedBoundsChanged(Region bounds) { + Region newBounds = Region.obtain(bounds); + mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, newBounds).sendToTarget(); + if (MY_PID != Binder.getCallingPid()) { + bounds.recycle(); + } + } + + private void handleOnMagnifiedBoundsChanged(Region bounds) { + // If there was a rotation we have to update the center of the magnified + // region since the old offset X/Y may be out of its acceptable range for + // the new display width and height. + if (mUpdateMagnificationSpecOnNextBoundsChange) { + mUpdateMagnificationSpecOnNextBoundsChange = false; + MagnificationSpec spec = mMagnificationController.getMagnificationSpec(); + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + final float scale = spec.scale; + final float centerX = (-spec.offsetX + magnifiedFrame.width() / 2) / scale; + final float centerY = (-spec.offsetY + magnifiedFrame.height() / 2) / scale; + mMagnificationController.setScaleAndMagnifiedRegionCenter(scale, centerX, + centerY, false); + } + mMagnifiedBounds.set(bounds); + mAms.onMagnificationStateChanged(); + } + + @Override + public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) { + SomeArgs args = SomeArgs.obtain(); + args.argi1 = left; + args.argi2 = top; + args.argi3 = right; + args.argi4 = bottom; + mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, args).sendToTarget(); + } + + private void handleOnRectangleOnScreenRequested(int left, int top, int right, int bottom) { + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + if (!magnifiedFrame.intersects(left, top, right, bottom)) { + return; + } + Rect magnifFrameInScreenCoords = mTempRect1; + getMagnifiedFrameInContentCoords(magnifFrameInScreenCoords); + final float scrollX; + final float scrollY; + if (right - left > magnifFrameInScreenCoords.width()) { + final int direction = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()); + if (direction == View.LAYOUT_DIRECTION_LTR) { + scrollX = left - magnifFrameInScreenCoords.left; + } else { + scrollX = right - magnifFrameInScreenCoords.right; + } + } else if (left < magnifFrameInScreenCoords.left) { + scrollX = left - magnifFrameInScreenCoords.left; + } else if (right > magnifFrameInScreenCoords.right) { + scrollX = right - magnifFrameInScreenCoords.right; + } else { + scrollX = 0; + } + if (bottom - top > magnifFrameInScreenCoords.height()) { + scrollY = top - magnifFrameInScreenCoords.top; + } else if (top < magnifFrameInScreenCoords.top) { + scrollY = top - magnifFrameInScreenCoords.top; + } else if (bottom > magnifFrameInScreenCoords.bottom) { + scrollY = bottom - magnifFrameInScreenCoords.bottom; + } else { + scrollY = 0; + } + final float scale = mMagnificationController.getScale(); + mMagnificationController.offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale); + } + + @Override + public void onRotationChanged(int rotation) { + mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0).sendToTarget(); + } + + private void handleOnRotationChanged(int rotation) { + resetMagnificationIfNeeded(); + if (mMagnificationController.isMagnifying()) { + mUpdateMagnificationSpecOnNextBoundsChange = true; + } + } + + @Override + public void onUserContextChanged() { + mHandler.sendEmptyMessage(MESSAGE_ON_USER_CONTEXT_CHANGED); + } + + private void handleOnUserContextChanged() { + resetMagnificationIfNeeded(); + } + + private void getMagnifiedFrameInContentCoords(Rect rect) { + MagnificationSpec spec = mMagnificationController.getMagnificationSpec(); + mMagnifiedBounds.getBounds(rect); + rect.offset((int) -spec.offsetX, (int) -spec.offsetY); + rect.scale(1.0f / spec.scale); + } + + private void resetMagnificationIfNeeded() { + if (mMagnificationController.isMagnifying() + && isScreenMagnificationAutoUpdateEnabled(mContext)) { + mMagnificationController.reset(true); + } + } + + @Override + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, + int policyFlags) { + mMagnifiedContentInteractonStateHandler.onMotionEvent(event); + switch (mCurrentState) { + case STATE_DELEGATING: { + handleMotionEventStateDelegating(event, rawEvent, policyFlags); + } break; + case STATE_DETECTING: { + mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags); + } break; + case STATE_VIEWPORT_DRAGGING: { + mStateViewportDraggingHandler.onMotionEvent(event, policyFlags); + } break; + case STATE_MAGNIFIED_INTERACTION: { + // mMagnifiedContentInteractonStateHandler handles events only + // if this is the current state since it uses ScaleGestureDetecotr + // and a GestureDetector which need well formed event stream. + } break; + default: { + throw new IllegalStateException("Unknown state: " + mCurrentState); + } + } + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + if (mNext != null) { + mNext.onAccessibilityEvent(event); + } + } + + @Override + public void setNext(EventStreamTransformation next) { + mNext = next; + } + + @Override + public void clear() { + mCurrentState = STATE_DETECTING; + mDetectingStateHandler.clear(); + mStateViewportDraggingHandler.clear(); + mMagnifiedContentInteractonStateHandler.clear(); + if (mNext != null) { + mNext.clear(); + } + } + + @Override + public void onDestroy() { + mScreenStateObserver.destroy(); + try { + mWindowManager.setMagnificationCallbacks(null); + } catch (RemoteException re) { + /* ignore */ + } + } + + private void handleMotionEventStateDelegating(MotionEvent event, + MotionEvent rawEvent, int policyFlags) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + mDelegatingStateDownTime = event.getDownTime(); + } break; + case MotionEvent.ACTION_UP: { + if (mDetectingStateHandler.mDelayedEventQueue == null) { + transitionToState(STATE_DETECTING); + } + } break; + } + if (mNext != null) { + // If the event is within the magnified portion of the screen we have + // to change its location to be where the user thinks he is poking the + // UI which may have been magnified and panned. + final float eventX = event.getX(); + final float eventY = event.getY(); + if (mMagnificationController.isMagnifying() + && mMagnifiedBounds.contains((int) eventX, (int) eventY)) { + final float scale = mMagnificationController.getScale(); + final float scaledOffsetX = mMagnificationController.getOffsetX(); + final float scaledOffsetY = mMagnificationController.getOffsetY(); + final int pointerCount = event.getPointerCount(); + PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); + PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount); + for (int i = 0; i < pointerCount; i++) { + event.getPointerCoords(i, coords[i]); + coords[i].x = (coords[i].x - scaledOffsetX) / scale; + coords[i].y = (coords[i].y - scaledOffsetY) / scale; + event.getPointerProperties(i, properties[i]); + } + event = MotionEvent.obtain(event.getDownTime(), + event.getEventTime(), event.getAction(), pointerCount, properties, + coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(), + event.getFlags()); + } + // We cache some events to see if the user wants to trigger magnification. + // If no magnification is triggered we inject these events with adjusted + // time and down time to prevent subsequent transformations being confused + // by stale events. After the cached events, which always have a down, are + // injected we need to also update the down time of all subsequent non cached + // events. All delegated events cached and non-cached are delivered here. + event.setDownTime(mDelegatingStateDownTime); + mNext.onMotionEvent(event, rawEvent, policyFlags); + } + } + + private PointerCoords[] getTempPointerCoordsWithMinSize(int size) { + final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0; + if (oldSize < size) { + PointerCoords[] oldTempPointerCoords = mTempPointerCoords; + mTempPointerCoords = new PointerCoords[size]; + if (oldTempPointerCoords != null) { + System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize); + } + } + for (int i = oldSize; i < size; i++) { + mTempPointerCoords[i] = new PointerCoords(); + } + return mTempPointerCoords; + } + + private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) { + final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0; + if (oldSize < size) { + PointerProperties[] oldTempPointerProperties = mTempPointerProperties; + mTempPointerProperties = new PointerProperties[size]; + if (oldTempPointerProperties != null) { + System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize); + } + } + for (int i = oldSize; i < size; i++) { + mTempPointerProperties[i] = new PointerProperties(); + } + return mTempPointerProperties; + } + + private void transitionToState(int state) { + if (DEBUG_STATE_TRANSITIONS) { + switch (state) { + case STATE_DELEGATING: { + Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING"); + } break; + case STATE_DETECTING: { + Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING"); + } break; + case STATE_VIEWPORT_DRAGGING: { + Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING"); + } break; + case STATE_MAGNIFIED_INTERACTION: { + Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION"); + } break; + default: { + throw new IllegalArgumentException("Unknown state: " + state); + } + } + } + mPreviousState = mCurrentState; + mCurrentState = state; + } + + private final class MagnifiedContentInteractonStateHandler + extends SimpleOnGestureListener implements OnScaleGestureListener { + private static final float MIN_SCALE = 1.3f; + private static final float MAX_SCALE = 5.0f; + + private static final float SCALING_THRESHOLD = 0.3f; + + private final ScaleGestureDetector mScaleGestureDetector; + private final GestureDetector mGestureDetector; + + private float mInitialScaleFactor = -1; + private boolean mScaling; + + public MagnifiedContentInteractonStateHandler(Context context) { + mScaleGestureDetector = new ScaleGestureDetector(context, this); + mScaleGestureDetector.setQuickScaleEnabled(false); + mGestureDetector = new GestureDetector(context, this); + } + + public void onMotionEvent(MotionEvent event) { + mScaleGestureDetector.onTouchEvent(event); + mGestureDetector.onTouchEvent(event); + if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { + return; + } + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + clear(); + final float scale = Math.min(Math.max(mMagnificationController.getScale(), + MIN_SCALE), MAX_SCALE); + if (scale != getPersistedScale()) { + persistScale(scale); + } + if (mPreviousState == STATE_VIEWPORT_DRAGGING) { + transitionToState(STATE_VIEWPORT_DRAGGING); + } else { + transitionToState(STATE_DETECTING); + } + } + } + + @Override + public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX, + float distanceY) { + if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { + return true; + } + if (DEBUG_PANNING) { + Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX + + " scrollY: " + distanceY); + } + mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY); + return true; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + if (!mScaling) { + if (mInitialScaleFactor < 0) { + mInitialScaleFactor = detector.getScaleFactor(); + } else { + final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor; + if (Math.abs(deltaScale) > SCALING_THRESHOLD) { + mScaling = true; + return true; + } + } + return false; + } + final float newScale = mMagnificationController.getScale() + * detector.getScaleFactor(); + final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), MAX_SCALE); + if (DEBUG_SCALING) { + Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale); + } + mMagnificationController.setScale(normalizedNewScale, detector.getFocusX(), + detector.getFocusY(), false); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return (mCurrentState == STATE_MAGNIFIED_INTERACTION); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + clear(); + } + + private void clear() { + mInitialScaleFactor = -1; + mScaling = false; + } + } + + private final class StateViewportDraggingHandler { + private boolean mLastMoveOutsideMagnifiedRegion; + + private void onMotionEvent(MotionEvent event, int policyFlags) { + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN"); + } + case MotionEvent.ACTION_POINTER_DOWN: { + clear(); + transitionToState(STATE_MAGNIFIED_INTERACTION); + } break; + case MotionEvent.ACTION_MOVE: { + if (event.getPointerCount() != 1) { + throw new IllegalStateException("Should have one pointer down."); + } + final float eventX = event.getX(); + final float eventY = event.getY(); + if (mMagnifiedBounds.contains((int) eventX, (int) eventY)) { + if (mLastMoveOutsideMagnifiedRegion) { + mLastMoveOutsideMagnifiedRegion = false; + mMagnificationController.setMagnifiedRegionCenter(eventX, + eventY, true); + } else { + mMagnificationController.setMagnifiedRegionCenter(eventX, + eventY, false); + } + } else { + mLastMoveOutsideMagnifiedRegion = true; + } + } break; + case MotionEvent.ACTION_UP: { + if (!mTranslationEnabledBeforePan) { + mMagnificationController.reset(true); + } + clear(); + transitionToState(STATE_DETECTING); + } break; + case MotionEvent.ACTION_POINTER_UP: { + throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP"); + } + } + } + + public void clear() { + mLastMoveOutsideMagnifiedRegion = false; + } + } + + private final class DetectingStateHandler { + + private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1; + + private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; + + private static final int ACTION_TAP_COUNT = 3; + + private MotionEventInfo mDelayedEventQueue; + + private MotionEvent mLastDownEvent; + private MotionEvent mLastTapUpEvent; + private int mTapCount; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message message) { + final int type = message.what; + switch (type) { + case MESSAGE_ON_ACTION_TAP_AND_HOLD: { + MotionEvent event = (MotionEvent) message.obj; + final int policyFlags = message.arg1; + onActionTapAndHold(event, policyFlags); + } break; + case MESSAGE_TRANSITION_TO_DELEGATING_STATE: { + transitionToState(STATE_DELEGATING); + sendDelayedMotionEvents(); + clear(); + } break; + default: { + throw new IllegalArgumentException("Unknown message type: " + type); + } + } + } + }; + + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cacheDelayedMotionEvent(event, rawEvent, policyFlags); + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); + if (!mMagnifiedBounds.contains((int) event.getX(), + (int) event.getY())) { + transitionToDelegatingStateAndClear(); + return; + } + if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null + && GestureUtils.isMultiTap(mLastDownEvent, event, + mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { + Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD, + policyFlags, 0, event); + mHandler.sendMessageDelayed(message, + ViewConfiguration.getLongPressTimeout()); + } else if (mTapCount < ACTION_TAP_COUNT) { + Message message = mHandler.obtainMessage( + MESSAGE_TRANSITION_TO_DELEGATING_STATE); + mHandler.sendMessageDelayed(message, mMultiTapTimeSlop); + } + clearLastDownEvent(); + mLastDownEvent = MotionEvent.obtain(event); + } break; + case MotionEvent.ACTION_POINTER_DOWN: { + if (mMagnificationController.isMagnifying()) { + transitionToState(STATE_MAGNIFIED_INTERACTION); + clear(); + } else { + transitionToDelegatingStateAndClear(); + } + } break; + case MotionEvent.ACTION_MOVE: { + if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) { + final double distance = GestureUtils.computeDistance(mLastDownEvent, + event, 0); + if (Math.abs(distance) > mTapDistanceSlop) { + transitionToDelegatingStateAndClear(); + } + } + } break; + case MotionEvent.ACTION_UP: { + if (mLastDownEvent == null) { + return; + } + mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); + if (!mMagnifiedBounds.contains((int) event.getX(), (int) event.getY())) { + transitionToDelegatingStateAndClear(); + return; + } + if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop, + mTapDistanceSlop, 0)) { + transitionToDelegatingStateAndClear(); + return; + } + if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent, + event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { + transitionToDelegatingStateAndClear(); + return; + } + mTapCount++; + if (DEBUG_DETECTING) { + Slog.i(LOG_TAG, "Tap count:" + mTapCount); + } + if (mTapCount == ACTION_TAP_COUNT) { + clear(); + onActionTap(event, policyFlags); + return; + } + clearLastTapUpEvent(); + mLastTapUpEvent = MotionEvent.obtain(event); + } break; + case MotionEvent.ACTION_POINTER_UP: { + /* do nothing */ + } break; + } + } + + public void clear() { + mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); + mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); + clearTapDetectionState(); + clearDelayedMotionEvents(); + } + + private void clearTapDetectionState() { + mTapCount = 0; + clearLastTapUpEvent(); + clearLastDownEvent(); + } + + private void clearLastTapUpEvent() { + if (mLastTapUpEvent != null) { + mLastTapUpEvent.recycle(); + mLastTapUpEvent = null; + } + } + + private void clearLastDownEvent() { + if (mLastDownEvent != null) { + mLastDownEvent.recycle(); + mLastDownEvent = null; + } + } + + private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, + int policyFlags) { + MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent, + policyFlags); + if (mDelayedEventQueue == null) { + mDelayedEventQueue = info; + } else { + MotionEventInfo tail = mDelayedEventQueue; + while (tail.mNext != null) { + tail = tail.mNext; + } + tail.mNext = info; + } + } + + private void sendDelayedMotionEvents() { + while (mDelayedEventQueue != null) { + MotionEventInfo info = mDelayedEventQueue; + mDelayedEventQueue = info.mNext; + final long offset = SystemClock.uptimeMillis() - info.mCachedTimeMillis; + MotionEvent event = obtainEventWithOffsetTimeAndDownTime(info.mEvent, offset); + MotionEvent rawEvent = obtainEventWithOffsetTimeAndDownTime(info.mRawEvent, offset); + ScreenMagnifier.this.onMotionEvent(event, rawEvent, info.mPolicyFlags); + event.recycle(); + rawEvent.recycle(); + info.recycle(); + } + } + + private MotionEvent obtainEventWithOffsetTimeAndDownTime(MotionEvent event, long offset) { + final int pointerCount = event.getPointerCount(); + PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); + PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount); + for (int i = 0; i < pointerCount; i++) { + event.getPointerCoords(i, coords[i]); + event.getPointerProperties(i, properties[i]); + } + final long downTime = event.getDownTime() + offset; + final long eventTime = event.getEventTime() + offset; + return MotionEvent.obtain(downTime, eventTime, + event.getAction(), pointerCount, properties, coords, + event.getMetaState(), event.getButtonState(), + 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(), + event.getSource(), event.getFlags()); + } + + private void clearDelayedMotionEvents() { + while (mDelayedEventQueue != null) { + MotionEventInfo info = mDelayedEventQueue; + mDelayedEventQueue = info.mNext; + info.recycle(); + } + } + + private void transitionToDelegatingStateAndClear() { + transitionToState(STATE_DELEGATING); + sendDelayedMotionEvents(); + clear(); + } + + private void onActionTap(MotionEvent up, int policyFlags) { + if (DEBUG_DETECTING) { + Slog.i(LOG_TAG, "onActionTap()"); + } + if (!mMagnificationController.isMagnifying()) { + mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(), + up.getX(), up.getY(), true); + } else { + mMagnificationController.reset(true); + } + } + + private void onActionTapAndHold(MotionEvent down, int policyFlags) { + if (DEBUG_DETECTING) { + Slog.i(LOG_TAG, "onActionTapAndHold()"); + } + clear(); + mTranslationEnabledBeforePan = mMagnificationController.isMagnifying(); + mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(), + down.getX(), down.getY(), true); + transitionToState(STATE_VIEWPORT_DRAGGING); + } + } + + private void persistScale(final float scale) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + Settings.Secure.putFloat(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale); + return null; + } + }.execute(); + } + + private float getPersistedScale() { + return Settings.Secure.getFloat(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, + DEFAULT_MAGNIFICATION_SCALE); + } + + private static boolean isScreenMagnificationAutoUpdateEnabled(Context context) { + return (Settings.Secure.getInt(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE, + DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1); + } + + private static final class MotionEventInfo { + + private static final int MAX_POOL_SIZE = 10; + + private static final Object sLock = new Object(); + private static MotionEventInfo sPool; + private static int sPoolSize; + + private MotionEventInfo mNext; + private boolean mInPool; + + public MotionEvent mEvent; + public MotionEvent mRawEvent; + public int mPolicyFlags; + public long mCachedTimeMillis; + + public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent, + int policyFlags) { + synchronized (sLock) { + MotionEventInfo info; + if (sPoolSize > 0) { + sPoolSize--; + info = sPool; + sPool = info.mNext; + info.mNext = null; + info.mInPool = false; + } else { + info = new MotionEventInfo(); + } + info.initialize(event, rawEvent, policyFlags); + return info; + } + } + + private void initialize(MotionEvent event, MotionEvent rawEvent, + int policyFlags) { + mEvent = MotionEvent.obtain(event); + mRawEvent = MotionEvent.obtain(rawEvent); + mPolicyFlags = policyFlags; + mCachedTimeMillis = SystemClock.uptimeMillis(); + } + + public void recycle() { + synchronized (sLock) { + if (mInPool) { + throw new IllegalStateException("Already recycled."); + } + clear(); + if (sPoolSize < MAX_POOL_SIZE) { + sPoolSize++; + mNext = sPool; + sPool = this; + mInPool = true; + } + } + } + + private void clear() { + mEvent.recycle(); + mEvent = null; + mRawEvent.recycle(); + mRawEvent = null; + mPolicyFlags = 0; + mCachedTimeMillis = 0; + } + } + + private final class MagnificationController { + + private static final String PROPERTY_NAME_MAGNIFICATION_SPEC = + "magnificationSpec"; + + private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain(); + + private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain(); + + private final Rect mTempRect = new Rect(); + + private final ValueAnimator mTransformationAnimator; + + public MagnificationController(long animationDuration) { + Property<MagnificationController, MagnificationSpec> property = + Property.of(MagnificationController.class, MagnificationSpec.class, + PROPERTY_NAME_MAGNIFICATION_SPEC); + TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() { + private final MagnificationSpec mTempTransformationSpec = + MagnificationSpec.obtain(); + @Override + public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec, + MagnificationSpec toSpec) { + MagnificationSpec result = mTempTransformationSpec; + result.scale = fromSpec.scale + + (toSpec.scale - fromSpec.scale) * fraction; + result.offsetX = fromSpec.offsetX + (toSpec.offsetX - fromSpec.offsetX) + * fraction; + result.offsetY = fromSpec.offsetY + (toSpec.offsetY - fromSpec.offsetY) + * fraction; + return result; + } + }; + mTransformationAnimator = ObjectAnimator.ofObject(this, property, + evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec); + mTransformationAnimator.setDuration((long) (animationDuration)); + mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f)); + } + + public boolean isMagnifying() { + return mCurrentMagnificationSpec.scale > 1.0f; + } + + public void reset(boolean animate) { + if (mTransformationAnimator.isRunning()) { + mTransformationAnimator.cancel(); + } + mCurrentMagnificationSpec.clear(); + if (animate) { + animateMangificationSpec(mSentMagnificationSpec, + mCurrentMagnificationSpec); + } else { + setMagnificationSpec(mCurrentMagnificationSpec); + } + Rect bounds = mTempRect; + bounds.setEmpty(); + mAms.onMagnificationStateChanged(); + } + + public float getScale() { + return mCurrentMagnificationSpec.scale; + } + + public float getOffsetX() { + return mCurrentMagnificationSpec.offsetX; + } + + public float getOffsetY() { + return mCurrentMagnificationSpec.offsetY; + } + + public void setScale(float scale, float pivotX, float pivotY, boolean animate) { + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + MagnificationSpec spec = mCurrentMagnificationSpec; + final float oldScale = spec.scale; + final float oldCenterX = (-spec.offsetX + magnifiedFrame.width() / 2) / oldScale; + final float oldCenterY = (-spec.offsetY + magnifiedFrame.height() / 2) / oldScale; + final float normPivotX = (-spec.offsetX + pivotX) / oldScale; + final float normPivotY = (-spec.offsetY + pivotY) / oldScale; + final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale); + final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale); + final float centerX = normPivotX + offsetX; + final float centerY = normPivotY + offsetY; + setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate); + } + + public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) { + setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.scale, centerX, centerY, + animate); + } + + public void offsetMagnifiedRegionCenter(float offsetX, float offsetY) { + final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX; + mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX, + getMinOffsetX()), 0); + final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY; + mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY, + getMinOffsetY()), 0); + setMagnificationSpec(mCurrentMagnificationSpec); + } + + public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY, + boolean animate) { + if (Float.compare(mCurrentMagnificationSpec.scale, scale) == 0 + && Float.compare(mCurrentMagnificationSpec.offsetX, + centerX) == 0 + && Float.compare(mCurrentMagnificationSpec.offsetY, + centerY) == 0) { + return; + } + if (mTransformationAnimator.isRunning()) { + mTransformationAnimator.cancel(); + } + if (DEBUG_MAGNIFICATION_CONTROLLER) { + Slog.i(LOG_TAG, "scale: " + scale + " offsetX: " + centerX + + " offsetY: " + centerY); + } + updateMagnificationSpec(scale, centerX, centerY); + if (animate) { + animateMangificationSpec(mSentMagnificationSpec, + mCurrentMagnificationSpec); + } else { + setMagnificationSpec(mCurrentMagnificationSpec); + } + mAms.onMagnificationStateChanged(); + } + + public void updateMagnificationSpec(float scale, float magnifiedCenterX, + float magnifiedCenterY) { + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + mCurrentMagnificationSpec.scale = scale; + final int viewportWidth = magnifiedFrame.width(); + final float nonNormOffsetX = viewportWidth / 2 - magnifiedCenterX * scale; + mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX, + getMinOffsetX()), 0); + final int viewportHeight = magnifiedFrame.height(); + final float nonNormOffsetY = viewportHeight / 2 - magnifiedCenterY * scale; + mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY, + getMinOffsetY()), 0); + } + + private float getMinOffsetX() { + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + final float viewportWidth = magnifiedFrame.width(); + return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale; + } + + private float getMinOffsetY() { + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + final float viewportHeight = magnifiedFrame.height(); + return viewportHeight - viewportHeight * mCurrentMagnificationSpec.scale; + } + + private void animateMangificationSpec(MagnificationSpec fromSpec, + MagnificationSpec toSpec) { + mTransformationAnimator.setObjectValues(fromSpec, toSpec); + mTransformationAnimator.start(); + } + + public MagnificationSpec getMagnificationSpec() { + return mSentMagnificationSpec; + } + + public void setMagnificationSpec(MagnificationSpec spec) { + if (DEBUG_SET_MAGNIFICATION_SPEC) { + Slog.i(LOG_TAG, "Sending: " + spec); + } + try { + mSentMagnificationSpec.scale = spec.scale; + mSentMagnificationSpec.offsetX = spec.offsetX; + mSentMagnificationSpec.offsetY = spec.offsetY; + mWindowManager.setMagnificationSpec( + MagnificationSpec.obtain(spec)); + } catch (RemoteException re) { + /* ignore */ + } + } + } + + private final class ScreenStateObserver extends BroadcastReceiver { + private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1; + + private final Context mContext; + private final MagnificationController mMagnificationController; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MESSAGE_ON_SCREEN_STATE_CHANGE: { + String action = (String) message.obj; + handleOnScreenStateChange(action); + } break; + } + } + }; + + public ScreenStateObserver(Context context, + MagnificationController magnificationController) { + mContext = context; + mMagnificationController = magnificationController; + mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF)); + } + + public void destroy() { + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE, + intent.getAction()).sendToTarget(); + } + + private void handleOnScreenStateChange(String action) { + if (mMagnificationController.isMagnifying() + && isScreenMagnificationAutoUpdateEnabled(mContext)) { + mMagnificationController.reset(false); + } + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java new file mode 100644 index 0000000..43f12eb --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java @@ -0,0 +1,1937 @@ +/* + ** Copyright 2011, 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 com.android.server.accessibility; + +import android.content.Context; +import android.gesture.Gesture; +import android.gesture.GestureLibraries; +import android.gesture.GestureLibrary; +import android.gesture.GesturePoint; +import android.gesture.GestureStore; +import android.gesture.GestureStroke; +import android.gesture.Prediction; +import android.graphics.Rect; +import android.os.Handler; +import android.os.SystemClock; +import android.util.Slog; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.view.WindowManagerPolicy; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This class is a strategy for performing touch exploration. It + * transforms the motion event stream by modifying, adding, replacing, + * and consuming certain events. The interaction model is: + * + * <ol> + * <li>1. One finger moving slow around performs touch exploration.</li> + * <li>2. One finger moving fast around performs gestures.</li> + * <li>3. Two close fingers moving in the same direction perform a drag.</li> + * <li>4. Multi-finger gestures are delivered to view hierarchy.</li> + * <li>5. Two fingers moving in different directions are considered a multi-finger gesture.</li> + * <li>7. Double tapping clicks on the on the last touch explored location if it was in + * a window that does not take focus, otherwise the click is within the accessibility + * focused rectangle.</li> + * <li>7. Tapping and holding for a while performs a long press in a similar fashion + * as the click above.</li> + * <ol> + * + * @hide + */ +class TouchExplorer implements EventStreamTransformation { + + private static final boolean DEBUG = false; + + // Tag for logging received events. + private static final String LOG_TAG = "TouchExplorer"; + + // States this explorer can be in. + private static final int STATE_TOUCH_EXPLORING = 0x00000001; + private static final int STATE_DRAGGING = 0x00000002; + private static final int STATE_DELEGATING = 0x00000004; + private static final int STATE_GESTURE_DETECTING = 0x00000005; + + // The maximum of the cosine between the vectors of two moving + // pointers so they can be considered moving in the same direction. + private static final float MAX_DRAGGING_ANGLE_COS = 0.525321989f; // cos(pi/4) + + // Constant referring to the ids bits of all pointers. + private static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF; + + // This constant captures the current implementation detail that + // pointer IDs are between 0 and 31 inclusive (subject to change). + // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) + private static final int MAX_POINTER_COUNT = 32; + + // Invalid pointer ID. + private static final int INVALID_POINTER_ID = -1; + + // The velocity above which we detect gestures. + private static final int GESTURE_DETECTION_VELOCITY_DIP = 1000; + + // The minimal distance before we take the middle of the distance between + // the two dragging pointers as opposed to use the location of the primary one. + private static final int MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP = 200; + + // The timeout after which we are no longer trying to detect a gesture. + private static final int EXIT_GESTURE_DETECTION_TIMEOUT = 2000; + + // Timeout before trying to decide what the user is trying to do. + private final int mDetermineUserIntentTimeout; + + // Timeout within which we try to detect a tap. + private final int mTapTimeout; + + // Timeout within which we try to detect a double tap. + private final int mDoubleTapTimeout; + + // Slop between the down and up tap to be a tap. + private final int mTouchSlop; + + // Slop between the first and second tap to be a double tap. + private final int mDoubleTapSlop; + + // The current state of the touch explorer. + private int mCurrentState = STATE_TOUCH_EXPLORING; + + // The ID of the pointer used for dragging. + private int mDraggingPointerId; + + // Handler for performing asynchronous operations. + private final Handler mHandler; + + // Command for delayed sending of a hover enter and move event. + private final SendHoverEnterAndMoveDelayed mSendHoverEnterAndMoveDelayed; + + // Command for delayed sending of a hover exit event. + private final SendHoverExitDelayed mSendHoverExitDelayed; + + // Command for delayed sending of touch exploration end events. + private final SendAccessibilityEventDelayed mSendTouchExplorationEndDelayed; + + // Command for delayed sending of touch interaction end events. + private final SendAccessibilityEventDelayed mSendTouchInteractionEndDelayed; + + // Command for delayed sending of a long press. + private final PerformLongPressDelayed mPerformLongPressDelayed; + + // Command for exiting gesture detection mode after a timeout. + private final ExitGestureDetectionModeDelayed mExitGestureDetectionModeDelayed; + + // Helper to detect and react to double tap in touch explore mode. + private final DoubleTapDetector mDoubleTapDetector; + + // The scaled minimal distance before we take the middle of the distance between + // the two dragging pointers as opposed to use the location of the primary one. + private final int mScaledMinPointerDistanceToUseMiddleLocation; + + // The scaled velocity above which we detect gestures. + private final int mScaledGestureDetectionVelocity; + + // The handler to which to delegate events. + private EventStreamTransformation mNext; + + // Helper to track gesture velocity. + private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); + + // Helper class to track received pointers. + private final ReceivedPointerTracker mReceivedPointerTracker; + + // Helper class to track injected pointers. + private final InjectedPointerTracker mInjectedPointerTracker; + + // Handle to the accessibility manager service. + private final AccessibilityManagerService mAms; + + // Temporary rectangle to avoid instantiation. + private final Rect mTempRect = new Rect(); + + // Context in which this explorer operates. + private final Context mContext; + + // The X of the previous event. + private float mPreviousX; + + // The Y of the previous event. + private float mPreviousY; + + // Buffer for storing points for gesture detection. + private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); + + // The minimal delta between moves to add a gesture point. + private static final int TOUCH_TOLERANCE = 3; + + // The minimal score for accepting a predicted gesture. + private static final float MIN_PREDICTION_SCORE = 2.0f; + + // The library for gesture detection. + private GestureLibrary mGestureLibrary; + + // The long pressing pointer id if coordinate remapping is needed. + private int mLongPressingPointerId = -1; + + // The long pressing pointer X if coordinate remapping is needed. + private int mLongPressingPointerDeltaX; + + // The long pressing pointer Y if coordinate remapping is needed. + private int mLongPressingPointerDeltaY; + + // The id of the last touch explored window. + private int mLastTouchedWindowId; + + // Whether touch exploration is in progress. + private boolean mTouchExplorationInProgress; + + /** + * Creates a new instance. + * + * @param inputFilter The input filter associated with this explorer. + * @param context A context handle for accessing resources. + */ + public TouchExplorer(Context context, AccessibilityManagerService service) { + mContext = context; + mAms = service; + mReceivedPointerTracker = new ReceivedPointerTracker(); + mInjectedPointerTracker = new InjectedPointerTracker(); + mTapTimeout = ViewConfiguration.getTapTimeout(); + mDetermineUserIntentTimeout = ViewConfiguration.getDoubleTapTimeout(); + mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout(); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); + mHandler = new Handler(context.getMainLooper()); + mPerformLongPressDelayed = new PerformLongPressDelayed(); + mExitGestureDetectionModeDelayed = new ExitGestureDetectionModeDelayed(); + mGestureLibrary = GestureLibraries.fromRawResource(context, R.raw.accessibility_gestures); + mGestureLibrary.setOrientationStyle(8); + mGestureLibrary.setSequenceType(GestureStore.SEQUENCE_SENSITIVE); + mGestureLibrary.load(); + mSendHoverEnterAndMoveDelayed = new SendHoverEnterAndMoveDelayed(); + mSendHoverExitDelayed = new SendHoverExitDelayed(); + mSendTouchExplorationEndDelayed = new SendAccessibilityEventDelayed( + AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END, + mDetermineUserIntentTimeout); + mSendTouchInteractionEndDelayed = new SendAccessibilityEventDelayed( + AccessibilityEvent.TYPE_TOUCH_INTERACTION_END, + mDetermineUserIntentTimeout); + mDoubleTapDetector = new DoubleTapDetector(); + final float density = context.getResources().getDisplayMetrics().density; + mScaledMinPointerDistanceToUseMiddleLocation = + (int) (MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP * density); + mScaledGestureDetectionVelocity = (int) (GESTURE_DETECTION_VELOCITY_DIP * density); + } + + public void clear() { + // If we have not received an event then we are in initial + // state. Therefore, there is not need to clean anything. + MotionEvent event = mReceivedPointerTracker.getLastReceivedEvent(); + if (event != null) { + clear(mReceivedPointerTracker.getLastReceivedEvent(), WindowManagerPolicy.FLAG_TRUSTED); + } + } + + public void onDestroy() { + // TODO: Implement + } + + private void clear(MotionEvent event, int policyFlags) { + switch (mCurrentState) { + case STATE_TOUCH_EXPLORING: { + // If a touch exploration gesture is in progress send events for its end. + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + } break; + case STATE_DRAGGING: { + mDraggingPointerId = INVALID_POINTER_ID; + // Send exit to all pointers that we have delivered. + sendUpForInjectedDownPointers(event, policyFlags); + } break; + case STATE_DELEGATING: { + // Send exit to all pointers that we have delivered. + sendUpForInjectedDownPointers(event, policyFlags); + } break; + case STATE_GESTURE_DETECTING: { + // Clear the current stroke. + mStrokeBuffer.clear(); + } break; + } + // Remove all pending callbacks. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + mExitGestureDetectionModeDelayed.cancel(); + mSendTouchExplorationEndDelayed.cancel(); + mSendTouchInteractionEndDelayed.cancel(); + // Reset the pointer trackers. + mReceivedPointerTracker.clear(); + mInjectedPointerTracker.clear(); + // Clear the double tap detector + mDoubleTapDetector.clear(); + // Go to initial state. + // Clear the long pressing pointer remap data. + mLongPressingPointerId = -1; + mLongPressingPointerDeltaX = 0; + mLongPressingPointerDeltaY = 0; + mCurrentState = STATE_TOUCH_EXPLORING; + if (mNext != null) { + mNext.clear(); + } + mTouchExplorationInProgress = false; + mAms.onTouchInteractionEnd(); + } + + @Override + public void setNext(EventStreamTransformation next) { + mNext = next; + } + + @Override + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (DEBUG) { + Slog.d(LOG_TAG, "Received event: " + event + ", policyFlags=0x" + + Integer.toHexString(policyFlags)); + Slog.d(LOG_TAG, getStateSymbolicName(mCurrentState)); + } + + mReceivedPointerTracker.onMotionEvent(rawEvent); + + switch(mCurrentState) { + case STATE_TOUCH_EXPLORING: { + handleMotionEventStateTouchExploring(event, rawEvent, policyFlags); + } break; + case STATE_DRAGGING: { + handleMotionEventStateDragging(event, policyFlags); + } break; + case STATE_DELEGATING: { + handleMotionEventStateDelegating(event, policyFlags); + } break; + case STATE_GESTURE_DETECTING: { + handleMotionEventGestureDetecting(rawEvent, policyFlags); + } break; + default: + throw new IllegalStateException("Illegal state: " + mCurrentState); + } + } + + public void onAccessibilityEvent(AccessibilityEvent event) { + final int eventType = event.getEventType(); + + // The event for gesture end should be strictly after the + // last hover exit event. + if (mSendTouchExplorationEndDelayed.isPending() + && eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) { + mSendTouchExplorationEndDelayed.cancel(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END); + } + + // The event for touch interaction end should be strictly after the + // last hover exit and the touch exploration gesture end events. + if (mSendTouchInteractionEndDelayed.isPending() + && eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) { + mSendTouchInteractionEndDelayed.cancel(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); + } + + // If a new window opens or the accessibility focus moves we no longer + // want to click/long press on the last touch explored location. + switch (eventType) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: + case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: { + if (mInjectedPointerTracker.mLastInjectedHoverEventForClick != null) { + mInjectedPointerTracker.mLastInjectedHoverEventForClick.recycle(); + mInjectedPointerTracker.mLastInjectedHoverEventForClick = null; + } + mLastTouchedWindowId = -1; + } break; + case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: + case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: { + mLastTouchedWindowId = event.getWindowId(); + } break; + } + if (mNext != null) { + mNext.onAccessibilityEvent(event); + } + } + + /** + * Handles a motion event in touch exploring state. + * + * @param event The event to be handled. + * @param rawEvent The raw (unmodified) motion event. + * @param policyFlags The policy flags associated with the event. + */ + private void handleMotionEventStateTouchExploring(MotionEvent event, MotionEvent rawEvent, + int policyFlags) { + ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; + + mVelocityTracker.addMovement(rawEvent); + + mDoubleTapDetector.onMotionEvent(event, policyFlags); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + mAms.onTouchInteractionStart(); + + // Pre-feed the motion events to the gesture detector since we + // have a distance slop before getting into gesture detection + // mode and not using the points within this slop significantly + // decreases the quality of gesture recognition. + handleMotionEventGestureDetecting(rawEvent, policyFlags); + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_START); + + // If we still have not notified the user for the last + // touch, we figure out what to do. If were waiting + // we resent the delayed callback and wait again. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + + if (mSendTouchExplorationEndDelayed.isPending()) { + mSendTouchExplorationEndDelayed.forceSendAndRemove(); + } + + if (mSendTouchInteractionEndDelayed.isPending()) { + mSendTouchInteractionEndDelayed.forceSendAndRemove(); + } + + // If we have the first tap, schedule a long press and break + // since we do not want to schedule hover enter because + // the delayed callback will kick in before the long click. + // This would lead to a state transition resulting in long + // pressing the item below the double taped area which is + // not necessary where accessibility focus is. + if (mDoubleTapDetector.firstTapDetected()) { + // We got a tap now post a long press action. + mPerformLongPressDelayed.post(event, policyFlags); + break; + } + if (!mTouchExplorationInProgress) { + if (!mSendHoverEnterAndMoveDelayed.isPending()) { + // Deliver hover enter with a delay to have a chance + // to detect what the user is trying to do. + final int pointerId = receivedTracker.getPrimaryPointerId(); + final int pointerIdBits = (1 << pointerId); + mSendHoverEnterAndMoveDelayed.post(event, true, pointerIdBits, + policyFlags); + } else { + // Cache the event until we discern exploration from gesturing. + mSendHoverEnterAndMoveDelayed.addEvent(event); + } + } + } break; + case MotionEvent.ACTION_POINTER_DOWN: { + // Another finger down means that if we have not started to deliver + // hover events, we will not have to. The code for ACTION_MOVE will + // decide what we will actually do next. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + } break; + case MotionEvent.ACTION_MOVE: { + final int pointerId = receivedTracker.getPrimaryPointerId(); + final int pointerIndex = event.findPointerIndex(pointerId); + final int pointerIdBits = (1 << pointerId); + switch (event.getPointerCount()) { + case 1: { + // We have not started sending events since we try to + // figure out what the user is doing. + if (mSendHoverEnterAndMoveDelayed.isPending()) { + // Pre-feed the motion events to the gesture detector since we + // have a distance slop before getting into gesture detection + // mode and not using the points within this slop significantly + // decreases the quality of gesture recognition. + handleMotionEventGestureDetecting(rawEvent, policyFlags); + + // Cache the event until we discern exploration from gesturing. + mSendHoverEnterAndMoveDelayed.addEvent(event); + + // It is *important* to use the distance traveled by the pointers + // on the screen which may or may not be magnified. + final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId) + - rawEvent.getX(pointerIndex); + final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId) + - rawEvent.getY(pointerIndex); + final double moveDelta = Math.hypot(deltaX, deltaY); + // The user has moved enough for us to decide. + if (moveDelta > mDoubleTapSlop) { + // Check whether the user is performing a gesture. We + // detect gestures if the pointer is moving above a + // given velocity. + mVelocityTracker.computeCurrentVelocity(1000); + final float maxAbsVelocity = Math.max( + Math.abs(mVelocityTracker.getXVelocity(pointerId)), + Math.abs(mVelocityTracker.getYVelocity(pointerId))); + if (maxAbsVelocity > mScaledGestureDetectionVelocity) { + // We have to perform gesture detection, so + // clear the current state and try to detect. + mCurrentState = STATE_GESTURE_DETECTING; + mVelocityTracker.clear(); + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + mExitGestureDetectionModeDelayed.post(); + // Send accessibility event to announce the start + // of gesture recognition. + sendAccessibilityEvent( + AccessibilityEvent.TYPE_GESTURE_DETECTION_START); + } else { + // We have just decided that the user is touch, + // exploring so start sending events. + mSendHoverEnterAndMoveDelayed.forceSendAndRemove(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, + pointerIdBits, policyFlags); + } + break; + } + } else { + // Cancel the long press if pending and the user + // moved more than the slop. + if (mPerformLongPressDelayed.isPending()) { + final float deltaX = + receivedTracker.getReceivedPointerDownX(pointerId) + - rawEvent.getX(pointerIndex); + final float deltaY = + receivedTracker.getReceivedPointerDownY(pointerId) + - rawEvent.getY(pointerIndex); + final double moveDelta = Math.hypot(deltaX, deltaY); + // The user has moved enough for us to decide. + if (moveDelta > mTouchSlop) { + mPerformLongPressDelayed.cancel(); + } + } + if (mTouchExplorationInProgress) { + sendTouchExplorationGestureStartAndHoverEnterIfNeeded(policyFlags); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, + policyFlags); + } + } + } break; + case 2: { + // More than one pointer so the user is not touch exploring + // and now we have to decide whether to delegate or drag. + if (mSendHoverEnterAndMoveDelayed.isPending()) { + // We have not started sending events so cancel + // scheduled sending events. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + } else { + mPerformLongPressDelayed.cancel(); + if (mTouchExplorationInProgress) { + // If the user is touch exploring the second pointer may be + // performing a double tap to activate an item without need + // for the user to lift his exploring finger. + // It is *important* to use the distance traveled by the pointers + // on the screen which may or may not be magnified. + final float deltaX = receivedTracker.getReceivedPointerDownX( + pointerId) - rawEvent.getX(pointerIndex); + final float deltaY = receivedTracker.getReceivedPointerDownY( + pointerId) - rawEvent.getY(pointerIndex); + final double moveDelta = Math.hypot(deltaX, deltaY); + if (moveDelta < mDoubleTapSlop) { + break; + } + // We are sending events so send exit and gesture + // end since we transition to another state. + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + } + } + + // We know that a new state transition is to happen and the + // new state will not be gesture recognition, so clear the + // stashed gesture strokes. + mStrokeBuffer.clear(); + + if (isDraggingGesture(event)) { + // Two pointers moving in the same direction within + // a given distance perform a drag. + mCurrentState = STATE_DRAGGING; + mDraggingPointerId = pointerId; + event.setEdgeFlags(receivedTracker.getLastReceivedDownEdgeFlags()); + sendMotionEvent(event, MotionEvent.ACTION_DOWN, pointerIdBits, + policyFlags); + } else { + // Two pointers moving arbitrary are delegated to the view hierarchy. + mCurrentState = STATE_DELEGATING; + sendDownForAllNotInjectedPointers(event, policyFlags); + } + mVelocityTracker.clear(); + } break; + default: { + // More than one pointer so the user is not touch exploring + // and now we have to decide whether to delegate or drag. + if (mSendHoverEnterAndMoveDelayed.isPending()) { + // We have not started sending events so cancel + // scheduled sending events. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + } else { + mPerformLongPressDelayed.cancel(); + // We are sending events so send exit and gesture + // end since we transition to another state. + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + } + + // More than two pointers are delegated to the view hierarchy. + mCurrentState = STATE_DELEGATING; + sendDownForAllNotInjectedPointers(event, policyFlags); + mVelocityTracker.clear(); + } + } + } break; + case MotionEvent.ACTION_UP: { + mAms.onTouchInteractionEnd(); + // We know that we do not need the pre-fed gesture points are not + // needed anymore since the last pointer just went up. + mStrokeBuffer.clear(); + final int pointerId = event.getPointerId(event.getActionIndex()); + final int pointerIdBits = (1 << pointerId); + + mPerformLongPressDelayed.cancel(); + mVelocityTracker.clear(); + + if (mSendHoverEnterAndMoveDelayed.isPending()) { + // If we have not delivered the enter schedule an exit. + mSendHoverExitDelayed.post(event, pointerIdBits, policyFlags); + } else { + // The user is touch exploring so we send events for end. + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + } + + if (!mSendTouchInteractionEndDelayed.isPending()) { + mSendTouchInteractionEndDelayed.post(); + } + + } break; + case MotionEvent.ACTION_CANCEL: { + clear(event, policyFlags); + } break; + } + } + + /** + * Handles a motion event in dragging state. + * + * @param event The event to be handled. + * @param policyFlags The policy flags associated with the event. + */ + private void handleMotionEventStateDragging(MotionEvent event, int policyFlags) { + final int pointerIdBits = (1 << mDraggingPointerId); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + throw new IllegalStateException("Dragging state can be reached only if two " + + "pointers are already down"); + } + case MotionEvent.ACTION_POINTER_DOWN: { + // We are in dragging state so we have two pointers and another one + // goes down => delegate the three pointers to the view hierarchy + mCurrentState = STATE_DELEGATING; + if (mDraggingPointerId != INVALID_POINTER_ID) { + sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); + } + sendDownForAllNotInjectedPointers(event, policyFlags); + } break; + case MotionEvent.ACTION_MOVE: { + switch (event.getPointerCount()) { + case 1: { + // do nothing + } break; + case 2: { + if (isDraggingGesture(event)) { + final float firstPtrX = event.getX(0); + final float firstPtrY = event.getY(0); + final float secondPtrX = event.getX(1); + final float secondPtrY = event.getY(1); + + final float deltaX = firstPtrX - secondPtrX; + final float deltaY = firstPtrY - secondPtrY; + final double distance = Math.hypot(deltaX, deltaY); + + if (distance > mScaledMinPointerDistanceToUseMiddleLocation) { + event.setLocation(deltaX / 2, deltaY / 2); + } + + // If still dragging send a drag event. + sendMotionEvent(event, MotionEvent.ACTION_MOVE, pointerIdBits, + policyFlags); + } else { + // The two pointers are moving either in different directions or + // no close enough => delegate the gesture to the view hierarchy. + mCurrentState = STATE_DELEGATING; + // Send an event to the end of the drag gesture. + sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, + policyFlags); + // Deliver all pointers to the view hierarchy. + sendDownForAllNotInjectedPointers(event, policyFlags); + } + } break; + default: { + mCurrentState = STATE_DELEGATING; + // Send an event to the end of the drag gesture. + sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, + policyFlags); + // Deliver all pointers to the view hierarchy. + sendDownForAllNotInjectedPointers(event, policyFlags); + } + } + } break; + case MotionEvent.ACTION_POINTER_UP: { + final int pointerId = event.getPointerId(event.getActionIndex()); + if (pointerId == mDraggingPointerId) { + mDraggingPointerId = INVALID_POINTER_ID; + // Send an event to the end of the drag gesture. + sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); + } + } break; + case MotionEvent.ACTION_UP: { + mAms.onTouchInteractionEnd(); + // Announce the end of a new touch interaction. + sendAccessibilityEvent( + AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); + final int pointerId = event.getPointerId(event.getActionIndex()); + if (pointerId == mDraggingPointerId) { + mDraggingPointerId = INVALID_POINTER_ID; + // Send an event to the end of the drag gesture. + sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); + } + mCurrentState = STATE_TOUCH_EXPLORING; + } break; + case MotionEvent.ACTION_CANCEL: { + clear(event, policyFlags); + } break; + } + } + + /** + * Handles a motion event in delegating state. + * + * @param event The event to be handled. + * @param policyFlags The policy flags associated with the event. + */ + private void handleMotionEventStateDelegating(MotionEvent event, int policyFlags) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + throw new IllegalStateException("Delegating state can only be reached if " + + "there is at least one pointer down!"); + } + case MotionEvent.ACTION_UP: { + // Offset the event if we are doing a long press as the + // target is not necessarily under the user's finger. + if (mLongPressingPointerId >= 0) { + event = offsetEvent(event, - mLongPressingPointerDeltaX, + - mLongPressingPointerDeltaY); + // Clear the long press state. + mLongPressingPointerId = -1; + mLongPressingPointerDeltaX = 0; + mLongPressingPointerDeltaY = 0; + } + + // Deliver the event. + sendMotionEvent(event, event.getAction(), ALL_POINTER_ID_BITS, policyFlags); + + // Announce the end of a the touch interaction. + mAms.onTouchInteractionEnd(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); + + mCurrentState = STATE_TOUCH_EXPLORING; + } break; + case MotionEvent.ACTION_CANCEL: { + clear(event, policyFlags); + } break; + default: { + // Deliver the event. + sendMotionEvent(event, event.getAction(), ALL_POINTER_ID_BITS, policyFlags); + } + } + } + + private void handleMotionEventGestureDetecting(MotionEvent event, int policyFlags) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + final float x = event.getX(); + final float y = event.getY(); + mPreviousX = x; + mPreviousY = y; + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + } break; + case MotionEvent.ACTION_MOVE: { + final float x = event.getX(); + final float y = event.getY(); + final float dX = Math.abs(x - mPreviousX); + final float dY = Math.abs(y - mPreviousY); + if (dX >= TOUCH_TOLERANCE || dY >= TOUCH_TOLERANCE) { + mPreviousX = x; + mPreviousY = y; + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + } + } break; + case MotionEvent.ACTION_UP: { + mAms.onTouchInteractionEnd(); + // Announce the end of the gesture recognition. + sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END); + // Announce the end of a the touch interaction. + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); + + float x = event.getX(); + float y = event.getY(); + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + + Gesture gesture = new Gesture(); + gesture.addStroke(new GestureStroke(mStrokeBuffer)); + + ArrayList<Prediction> predictions = mGestureLibrary.recognize(gesture); + if (!predictions.isEmpty()) { + Prediction bestPrediction = predictions.get(0); + if (bestPrediction.score >= MIN_PREDICTION_SCORE) { + if (DEBUG) { + Slog.i(LOG_TAG, "gesture: " + bestPrediction.name + " score: " + + bestPrediction.score); + } + try { + final int gestureId = Integer.parseInt(bestPrediction.name); + mAms.onGesture(gestureId); + } catch (NumberFormatException nfe) { + Slog.w(LOG_TAG, "Non numeric gesture id:" + bestPrediction.name); + } + } + } + + mStrokeBuffer.clear(); + mExitGestureDetectionModeDelayed.cancel(); + mCurrentState = STATE_TOUCH_EXPLORING; + } break; + case MotionEvent.ACTION_CANCEL: { + clear(event, policyFlags); + } break; + } + } + + /** + * Sends an accessibility event of the given type. + * + * @param type The event type. + */ + private void sendAccessibilityEvent(int type) { + AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mContext); + if (accessibilityManager.isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(type); + event.setWindowId(mAms.getActiveWindowId()); + accessibilityManager.sendAccessibilityEvent(event); + switch (type) { + case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START: { + mTouchExplorationInProgress = true; + } break; + case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END: { + mTouchExplorationInProgress = false; + } break; + } + } + } + + /** + * Sends down events to the view hierarchy for all pointers which are + * not already being delivered i.e. pointers that are not yet injected. + * + * @param prototype The prototype from which to create the injected events. + * @param policyFlags The policy flags associated with the event. + */ + private void sendDownForAllNotInjectedPointers(MotionEvent prototype, int policyFlags) { + InjectedPointerTracker injectedPointers = mInjectedPointerTracker; + + // Inject the injected pointers. + int pointerIdBits = 0; + final int pointerCount = prototype.getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = prototype.getPointerId(i); + // Do not send event for already delivered pointers. + if (!injectedPointers.isInjectedPointerDown(pointerId)) { + pointerIdBits |= (1 << pointerId); + final int action = computeInjectionAction(MotionEvent.ACTION_DOWN, i); + sendMotionEvent(prototype, action, pointerIdBits, policyFlags); + } + } + } + + /** + * Sends the exit events if needed. Such events are hover exit and touch explore + * gesture end. + * + * @param policyFlags The policy flags associated with the event. + */ + private void sendHoverExitAndTouchExplorationGestureEndIfNeeded(int policyFlags) { + MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent(); + if (event != null && event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) { + final int pointerIdBits = event.getPointerIdBits(); + if (!mSendTouchExplorationEndDelayed.isPending()) { + mSendTouchExplorationEndDelayed.post(); + } + sendMotionEvent(event, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, policyFlags); + } + } + + /** + * Sends the enter events if needed. Such events are hover enter and touch explore + * gesture start. + * + * @param policyFlags The policy flags associated with the event. + */ + private void sendTouchExplorationGestureStartAndHoverEnterIfNeeded(int policyFlags) { + MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent(); + if (event != null && event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) { + final int pointerIdBits = event.getPointerIdBits(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, pointerIdBits, policyFlags); + } + } + + /** + * Sends up events to the view hierarchy for all pointers which are + * already being delivered i.e. pointers that are injected. + * + * @param prototype The prototype from which to create the injected events. + * @param policyFlags The policy flags associated with the event. + */ + private void sendUpForInjectedDownPointers(MotionEvent prototype, int policyFlags) { + final InjectedPointerTracker injectedTracked = mInjectedPointerTracker; + int pointerIdBits = 0; + final int pointerCount = prototype.getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = prototype.getPointerId(i); + // Skip non injected down pointers. + if (!injectedTracked.isInjectedPointerDown(pointerId)) { + continue; + } + pointerIdBits |= (1 << pointerId); + final int action = computeInjectionAction(MotionEvent.ACTION_UP, i); + sendMotionEvent(prototype, action, pointerIdBits, policyFlags); + } + } + + /** + * Sends an up and down events. + * + * @param prototype The prototype from which to create the injected events. + * @param policyFlags The policy flags associated with the event. + */ + private void sendActionDownAndUp(MotionEvent prototype, int policyFlags) { + // Tap with the pointer that last explored. + final int pointerId = prototype.getPointerId(prototype.getActionIndex()); + final int pointerIdBits = (1 << pointerId); + sendMotionEvent(prototype, MotionEvent.ACTION_DOWN, pointerIdBits, policyFlags); + sendMotionEvent(prototype, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); + } + + /** + * Sends an event. + * + * @param prototype The prototype from which to create the injected events. + * @param action The action of the event. + * @param pointerIdBits The bits of the pointers to send. + * @param policyFlags The policy flags associated with the event. + */ + private void sendMotionEvent(MotionEvent prototype, int action, int pointerIdBits, + int policyFlags) { + prototype.setAction(action); + + MotionEvent event = null; + if (pointerIdBits == ALL_POINTER_ID_BITS) { + event = prototype; + } else { + event = prototype.split(pointerIdBits); + } + if (action == MotionEvent.ACTION_DOWN) { + event.setDownTime(event.getEventTime()); + } else { + event.setDownTime(mInjectedPointerTracker.getLastInjectedDownEventTime()); + } + + // If the user is long pressing but the long pressing pointer + // was not exactly over the accessibility focused item we need + // to remap the location of that pointer so the user does not + // have to explicitly touch explore something to be able to + // long press it, or even worse to avoid the user long pressing + // on the wrong item since click and long press behave differently. + if (mLongPressingPointerId >= 0) { + event = offsetEvent(event, - mLongPressingPointerDeltaX, + - mLongPressingPointerDeltaY); + } + + if (DEBUG) { + Slog.d(LOG_TAG, "Injecting event: " + event + ", policyFlags=0x" + + Integer.toHexString(policyFlags)); + } + + // Make sure that the user will see the event. + policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER; + if (mNext != null) { + // TODO: For now pass null for the raw event since the touch + // explorer is the last event transformation and it does + // not care about the raw event. + mNext.onMotionEvent(event, null, policyFlags); + } + + mInjectedPointerTracker.onMotionEvent(event); + + if (event != prototype) { + event.recycle(); + } + } + + /** + * Offsets all pointers in the given event by adding the specified X and Y + * offsets. + * + * @param event The event to offset. + * @param offsetX The X offset. + * @param offsetY The Y offset. + * @return An event with the offset pointers or the original event if both + * offsets are zero. + */ + private MotionEvent offsetEvent(MotionEvent event, int offsetX, int offsetY) { + if (offsetX == 0 && offsetY == 0) { + return event; + } + final int remappedIndex = event.findPointerIndex(mLongPressingPointerId); + final int pointerCount = event.getPointerCount(); + PointerProperties[] props = PointerProperties.createArray(pointerCount); + PointerCoords[] coords = PointerCoords.createArray(pointerCount); + for (int i = 0; i < pointerCount; i++) { + event.getPointerProperties(i, props[i]); + event.getPointerCoords(i, coords[i]); + if (i == remappedIndex) { + coords[i].x += offsetX; + coords[i].y += offsetY; + } + } + return MotionEvent.obtain(event.getDownTime(), + event.getEventTime(), event.getAction(), event.getPointerCount(), + props, coords, event.getMetaState(), event.getButtonState(), + 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(), + event.getSource(), event.getFlags()); + } + + /** + * Computes the action for an injected event based on a masked action + * and a pointer index. + * + * @param actionMasked The masked action. + * @param pointerIndex The index of the pointer which has changed. + * @return The action to be used for injection. + */ + private int computeInjectionAction(int actionMasked, int pointerIndex) { + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + InjectedPointerTracker injectedTracker = mInjectedPointerTracker; + // Compute the action based on how many down pointers are injected. + if (injectedTracker.getInjectedPointerDownCount() == 0) { + return MotionEvent.ACTION_DOWN; + } else { + return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) + | MotionEvent.ACTION_POINTER_DOWN; + } + } + case MotionEvent.ACTION_POINTER_UP: { + InjectedPointerTracker injectedTracker = mInjectedPointerTracker; + // Compute the action based on how many down pointers are injected. + if (injectedTracker.getInjectedPointerDownCount() == 1) { + return MotionEvent.ACTION_UP; + } else { + return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) + | MotionEvent.ACTION_POINTER_UP; + } + } + default: + return actionMasked; + } + } + + private class DoubleTapDetector { + private MotionEvent mDownEvent; + private MotionEvent mFirstTapEvent; + + public void onMotionEvent(MotionEvent event, int policyFlags) { + final int actionIndex = event.getActionIndex(); + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + if (mFirstTapEvent != null + && !GestureUtils.isSamePointerContext(mFirstTapEvent, event)) { + clear(); + } + mDownEvent = MotionEvent.obtain(event); + } break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: { + if (mDownEvent == null) { + return; + } + if (!GestureUtils.isSamePointerContext(mDownEvent, event)) { + clear(); + return; + } + if (GestureUtils.isTap(mDownEvent, event, mTapTimeout, mTouchSlop, + actionIndex)) { + if (mFirstTapEvent == null || GestureUtils.isTimedOut(mFirstTapEvent, + event, mDoubleTapTimeout)) { + mFirstTapEvent = MotionEvent.obtain(event); + mDownEvent.recycle(); + mDownEvent = null; + return; + } + if (GestureUtils.isMultiTap(mFirstTapEvent, event, mDoubleTapTimeout, + mDoubleTapSlop, actionIndex)) { + onDoubleTap(event, policyFlags); + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + mDownEvent.recycle(); + mDownEvent = null; + return; + } + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } else { + if (mFirstTapEvent != null) { + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } + } + mDownEvent.recycle(); + mDownEvent = null; + } break; + } + } + + public void onDoubleTap(MotionEvent secondTapUp, int policyFlags) { + // This should never be called when more than two pointers are down. + if (secondTapUp.getPointerCount() > 2) { + return; + } + + // Remove pending event deliveries. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + + if (mSendTouchExplorationEndDelayed.isPending()) { + mSendTouchExplorationEndDelayed.forceSendAndRemove(); + } + if (mSendTouchInteractionEndDelayed.isPending()) { + mSendTouchInteractionEndDelayed.forceSendAndRemove(); + } + + int clickLocationX; + int clickLocationY; + + final int pointerId = secondTapUp.getPointerId(secondTapUp.getActionIndex()); + final int pointerIndex = secondTapUp.findPointerIndex(pointerId); + + MotionEvent lastExploreEvent = + mInjectedPointerTracker.getLastInjectedHoverEventForClick(); + if (lastExploreEvent == null) { + // No last touch explored event but there is accessibility focus in + // the active window. We click in the middle of the focus bounds. + Rect focusBounds = mTempRect; + if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) { + clickLocationX = focusBounds.centerX(); + clickLocationY = focusBounds.centerY(); + } else { + // Out of luck - do nothing. + return; + } + } else { + // If the click is within the active window but not within the + // accessibility focus bounds we click in the focus center. + final int lastExplorePointerIndex = lastExploreEvent.getActionIndex(); + clickLocationX = (int) lastExploreEvent.getX(lastExplorePointerIndex); + clickLocationY = (int) lastExploreEvent.getY(lastExplorePointerIndex); + Rect activeWindowBounds = mTempRect; + if (mLastTouchedWindowId == mAms.getActiveWindowId()) { + mAms.getActiveWindowBounds(activeWindowBounds); + if (activeWindowBounds.contains(clickLocationX, clickLocationY)) { + Rect focusBounds = mTempRect; + if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) { + if (!focusBounds.contains(clickLocationX, clickLocationY)) { + clickLocationX = focusBounds.centerX(); + clickLocationY = focusBounds.centerY(); + } + } + } + } + } + + // Do the click. + PointerProperties[] properties = new PointerProperties[1]; + properties[0] = new PointerProperties(); + secondTapUp.getPointerProperties(pointerIndex, properties[0]); + PointerCoords[] coords = new PointerCoords[1]; + coords[0] = new PointerCoords(); + coords[0].x = clickLocationX; + coords[0].y = clickLocationY; + MotionEvent event = MotionEvent.obtain(secondTapUp.getDownTime(), + secondTapUp.getEventTime(), MotionEvent.ACTION_DOWN, 1, properties, + coords, 0, 0, 1.0f, 1.0f, secondTapUp.getDeviceId(), 0, + secondTapUp.getSource(), secondTapUp.getFlags()); + sendActionDownAndUp(event, policyFlags); + event.recycle(); + } + + public void clear() { + if (mDownEvent != null) { + mDownEvent.recycle(); + mDownEvent = null; + } + if (mFirstTapEvent != null) { + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } + } + + public boolean firstTapDetected() { + return mFirstTapEvent != null + && SystemClock.uptimeMillis() - mFirstTapEvent.getEventTime() < mDoubleTapTimeout; + } + } + + /** + * Determines whether a two pointer gesture is a dragging one. + * + * @param event The event with the pointer data. + * @return True if the gesture is a dragging one. + */ + private boolean isDraggingGesture(MotionEvent event) { + ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; + + final float firstPtrX = event.getX(0); + final float firstPtrY = event.getY(0); + final float secondPtrX = event.getX(1); + final float secondPtrY = event.getY(1); + + final float firstPtrDownX = receivedTracker.getReceivedPointerDownX(0); + final float firstPtrDownY = receivedTracker.getReceivedPointerDownY(0); + final float secondPtrDownX = receivedTracker.getReceivedPointerDownX(1); + final float secondPtrDownY = receivedTracker.getReceivedPointerDownY(1); + + return GestureUtils.isDraggingGesture(firstPtrDownX, firstPtrDownY, secondPtrDownX, + secondPtrDownY, firstPtrX, firstPtrY, secondPtrX, secondPtrY, + MAX_DRAGGING_ANGLE_COS); + } + + /** + * Gets the symbolic name of a state. + * + * @param state A state. + * @return The state symbolic name. + */ + private static String getStateSymbolicName(int state) { + switch (state) { + case STATE_TOUCH_EXPLORING: + return "STATE_TOUCH_EXPLORING"; + case STATE_DRAGGING: + return "STATE_DRAGGING"; + case STATE_DELEGATING: + return "STATE_DELEGATING"; + case STATE_GESTURE_DETECTING: + return "STATE_GESTURE_DETECTING"; + default: + throw new IllegalArgumentException("Unknown state: " + state); + } + } + + /** + * Class for delayed exiting from gesture detecting mode. + */ + private final class ExitGestureDetectionModeDelayed implements Runnable { + + public void post() { + mHandler.postDelayed(this, EXIT_GESTURE_DETECTION_TIMEOUT); + } + + public void cancel() { + mHandler.removeCallbacks(this); + } + + @Override + public void run() { + // Announce the end of gesture recognition. + sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END); + // Clearing puts is in touch exploration state with a finger already + // down, so announce the transition to exploration state. + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); + clear(); + } + } + + /** + * Class for delayed sending of long press. + */ + private final class PerformLongPressDelayed implements Runnable { + private MotionEvent mEvent; + private int mPolicyFlags; + + public void post(MotionEvent prototype, int policyFlags) { + mEvent = MotionEvent.obtain(prototype); + mPolicyFlags = policyFlags; + mHandler.postDelayed(this, ViewConfiguration.getLongPressTimeout()); + } + + public void cancel() { + if (mEvent != null) { + mHandler.removeCallbacks(this); + clear(); + } + } + + private boolean isPending() { + return mHandler.hasCallbacks(this); + } + + @Override + public void run() { + // Pointers should not be zero when running this command. + if (mReceivedPointerTracker.getLastReceivedEvent().getPointerCount() == 0) { + return; + } + + int clickLocationX; + int clickLocationY; + + final int pointerId = mEvent.getPointerId(mEvent.getActionIndex()); + final int pointerIndex = mEvent.findPointerIndex(pointerId); + + MotionEvent lastExploreEvent = + mInjectedPointerTracker.getLastInjectedHoverEventForClick(); + if (lastExploreEvent == null) { + // No last touch explored event but there is accessibility focus in + // the active window. We click in the middle of the focus bounds. + Rect focusBounds = mTempRect; + if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) { + clickLocationX = focusBounds.centerX(); + clickLocationY = focusBounds.centerY(); + } else { + // Out of luck - do nothing. + return; + } + } else { + // If the click is within the active window but not within the + // accessibility focus bounds we click in the focus center. + final int lastExplorePointerIndex = lastExploreEvent.getActionIndex(); + clickLocationX = (int) lastExploreEvent.getX(lastExplorePointerIndex); + clickLocationY = (int) lastExploreEvent.getY(lastExplorePointerIndex); + Rect activeWindowBounds = mTempRect; + if (mLastTouchedWindowId == mAms.getActiveWindowId()) { + mAms.getActiveWindowBounds(activeWindowBounds); + if (activeWindowBounds.contains(clickLocationX, clickLocationY)) { + Rect focusBounds = mTempRect; + if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) { + if (!focusBounds.contains(clickLocationX, clickLocationY)) { + clickLocationX = focusBounds.centerX(); + clickLocationY = focusBounds.centerY(); + } + } + } + } + } + + mLongPressingPointerId = pointerId; + mLongPressingPointerDeltaX = (int) mEvent.getX(pointerIndex) - clickLocationX; + mLongPressingPointerDeltaY = (int) mEvent.getY(pointerIndex) - clickLocationY; + + sendHoverExitAndTouchExplorationGestureEndIfNeeded(mPolicyFlags); + + mCurrentState = STATE_DELEGATING; + sendDownForAllNotInjectedPointers(mEvent, mPolicyFlags); + clear(); + } + + private void clear() { + mEvent.recycle(); + mEvent = null; + mPolicyFlags = 0; + } + } + + /** + * Class for delayed sending of hover enter and move events. + */ + class SendHoverEnterAndMoveDelayed implements Runnable { + private final String LOG_TAG_SEND_HOVER_DELAYED = "SendHoverEnterAndMoveDelayed"; + + private final List<MotionEvent> mEvents = new ArrayList<MotionEvent>(); + + private int mPointerIdBits; + private int mPolicyFlags; + + public void post(MotionEvent event, boolean touchExplorationInProgress, + int pointerIdBits, int policyFlags) { + cancel(); + addEvent(event); + mPointerIdBits = pointerIdBits; + mPolicyFlags = policyFlags; + mHandler.postDelayed(this, mDetermineUserIntentTimeout); + } + + public void addEvent(MotionEvent event) { + mEvents.add(MotionEvent.obtain(event)); + } + + public void cancel() { + if (isPending()) { + mHandler.removeCallbacks(this); + clear(); + } + } + + private boolean isPending() { + return mHandler.hasCallbacks(this); + } + + private void clear() { + mPointerIdBits = -1; + mPolicyFlags = 0; + final int eventCount = mEvents.size(); + for (int i = eventCount - 1; i >= 0; i--) { + mEvents.remove(i).recycle(); + } + } + + public void forceSendAndRemove() { + if (isPending()) { + run(); + cancel(); + } + } + + public void run() { + // Send an accessibility event to announce the touch exploration start. + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); + + if (!mEvents.isEmpty()) { + // Deliver a down event. + sendMotionEvent(mEvents.get(0), MotionEvent.ACTION_HOVER_ENTER, + mPointerIdBits, mPolicyFlags); + if (DEBUG) { + Slog.d(LOG_TAG_SEND_HOVER_DELAYED, + "Injecting motion event: ACTION_HOVER_ENTER"); + } + + // Deliver move events. + final int eventCount = mEvents.size(); + for (int i = 1; i < eventCount; i++) { + sendMotionEvent(mEvents.get(i), MotionEvent.ACTION_HOVER_MOVE, + mPointerIdBits, mPolicyFlags); + if (DEBUG) { + Slog.d(LOG_TAG_SEND_HOVER_DELAYED, + "Injecting motion event: ACTION_HOVER_MOVE"); + } + } + } + clear(); + } + } + + /** + * Class for delayed sending of hover exit events. + */ + class SendHoverExitDelayed implements Runnable { + private final String LOG_TAG_SEND_HOVER_DELAYED = "SendHoverExitDelayed"; + + private MotionEvent mPrototype; + private int mPointerIdBits; + private int mPolicyFlags; + + public void post(MotionEvent prototype, int pointerIdBits, int policyFlags) { + cancel(); + mPrototype = MotionEvent.obtain(prototype); + mPointerIdBits = pointerIdBits; + mPolicyFlags = policyFlags; + mHandler.postDelayed(this, mDetermineUserIntentTimeout); + } + + public void cancel() { + if (isPending()) { + mHandler.removeCallbacks(this); + clear(); + } + } + + private boolean isPending() { + return mHandler.hasCallbacks(this); + } + + private void clear() { + mPrototype.recycle(); + mPrototype = null; + mPointerIdBits = -1; + mPolicyFlags = 0; + } + + public void forceSendAndRemove() { + if (isPending()) { + run(); + cancel(); + } + } + + public void run() { + if (DEBUG) { + Slog.d(LOG_TAG_SEND_HOVER_DELAYED, "Injecting motion event:" + + " ACTION_HOVER_EXIT"); + } + sendMotionEvent(mPrototype, MotionEvent.ACTION_HOVER_EXIT, + mPointerIdBits, mPolicyFlags); + if (!mSendTouchExplorationEndDelayed.isPending()) { + mSendTouchExplorationEndDelayed.cancel(); + mSendTouchExplorationEndDelayed.post(); + } + if (mSendTouchInteractionEndDelayed.isPending()) { + mSendTouchInteractionEndDelayed.cancel(); + mSendTouchInteractionEndDelayed.post(); + } + clear(); + } + } + + private class SendAccessibilityEventDelayed implements Runnable { + private final int mEventType; + private final int mDelay; + + public SendAccessibilityEventDelayed(int eventType, int delay) { + mEventType = eventType; + mDelay = delay; + } + + public void cancel() { + mHandler.removeCallbacks(this); + } + + public void post() { + mHandler.postDelayed(this, mDelay); + } + + public boolean isPending() { + return mHandler.hasCallbacks(this); + } + + public void forceSendAndRemove() { + if (isPending()) { + run(); + cancel(); + } + } + + @Override + public void run() { + sendAccessibilityEvent(mEventType); + } + } + + @Override + public String toString() { + return LOG_TAG; + } + + class InjectedPointerTracker { + private static final String LOG_TAG_INJECTED_POINTER_TRACKER = "InjectedPointerTracker"; + + // Keep track of which pointers sent to the system are down. + private int mInjectedPointersDown; + + // The time of the last injected down. + private long mLastInjectedDownEventTime; + + // The last injected hover event. + private MotionEvent mLastInjectedHoverEvent; + + // The last injected hover event used for performing clicks. + private MotionEvent mLastInjectedHoverEventForClick; + + /** + * Processes an injected {@link MotionEvent} event. + * + * @param event The event to process. + */ + public void onMotionEvent(MotionEvent event) { + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + final int pointerId = event.getPointerId(event.getActionIndex()); + final int pointerFlag = (1 << pointerId); + mInjectedPointersDown |= pointerFlag; + mLastInjectedDownEventTime = event.getDownTime(); + } break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: { + final int pointerId = event.getPointerId(event.getActionIndex()); + final int pointerFlag = (1 << pointerId); + mInjectedPointersDown &= ~pointerFlag; + if (mInjectedPointersDown == 0) { + mLastInjectedDownEventTime = 0; + } + } break; + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_HOVER_EXIT: { + if (mLastInjectedHoverEvent != null) { + mLastInjectedHoverEvent.recycle(); + } + mLastInjectedHoverEvent = MotionEvent.obtain(event); + if (mLastInjectedHoverEventForClick != null) { + mLastInjectedHoverEventForClick.recycle(); + } + mLastInjectedHoverEventForClick = MotionEvent.obtain(event); + } break; + } + if (DEBUG) { + Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer:\n" + toString()); + } + } + + /** + * Clears the internals state. + */ + public void clear() { + mInjectedPointersDown = 0; + } + + /** + * @return The time of the last injected down event. + */ + public long getLastInjectedDownEventTime() { + return mLastInjectedDownEventTime; + } + + /** + * @return The number of down pointers injected to the view hierarchy. + */ + public int getInjectedPointerDownCount() { + return Integer.bitCount(mInjectedPointersDown); + } + + /** + * @return The bits of the injected pointers that are down. + */ + public int getInjectedPointersDown() { + return mInjectedPointersDown; + } + + /** + * Whether an injected pointer is down. + * + * @param pointerId The unique pointer id. + * @return True if the pointer is down. + */ + public boolean isInjectedPointerDown(int pointerId) { + final int pointerFlag = (1 << pointerId); + return (mInjectedPointersDown & pointerFlag) != 0; + } + + /** + * @return The the last injected hover event. + */ + public MotionEvent getLastInjectedHoverEvent() { + return mLastInjectedHoverEvent; + } + + /** + * @return The the last injected hover event. + */ + public MotionEvent getLastInjectedHoverEventForClick() { + return mLastInjectedHoverEventForClick; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("========================="); + builder.append("\nDown pointers #"); + builder.append(Integer.bitCount(mInjectedPointersDown)); + builder.append(" [ "); + for (int i = 0; i < MAX_POINTER_COUNT; i++) { + if ((mInjectedPointersDown & i) != 0) { + builder.append(i); + builder.append(" "); + } + } + builder.append("]"); + builder.append("\n========================="); + return builder.toString(); + } + } + + class ReceivedPointerTracker { + private static final String LOG_TAG_RECEIVED_POINTER_TRACKER = "ReceivedPointerTracker"; + + // Keep track of where and when a pointer went down. + private final float[] mReceivedPointerDownX = new float[MAX_POINTER_COUNT]; + private final float[] mReceivedPointerDownY = new float[MAX_POINTER_COUNT]; + private final long[] mReceivedPointerDownTime = new long[MAX_POINTER_COUNT]; + + // Which pointers are down. + private int mReceivedPointersDown; + + // The edge flags of the last received down event. + private int mLastReceivedDownEdgeFlags; + + // Primary pointer which is either the first that went down + // or if it goes up the next one that most recently went down. + private int mPrimaryPointerId; + + // Keep track of the last up pointer data. + private long mLastReceivedUpPointerDownTime; + private float mLastReceivedUpPointerDownX; + private float mLastReceivedUpPointerDownY; + + private MotionEvent mLastReceivedEvent; + + /** + * Clears the internals state. + */ + public void clear() { + Arrays.fill(mReceivedPointerDownX, 0); + Arrays.fill(mReceivedPointerDownY, 0); + Arrays.fill(mReceivedPointerDownTime, 0); + mReceivedPointersDown = 0; + mPrimaryPointerId = 0; + mLastReceivedUpPointerDownTime = 0; + mLastReceivedUpPointerDownX = 0; + mLastReceivedUpPointerDownY = 0; + } + + /** + * Processes a received {@link MotionEvent} event. + * + * @param event The event to process. + */ + public void onMotionEvent(MotionEvent event) { + if (mLastReceivedEvent != null) { + mLastReceivedEvent.recycle(); + } + mLastReceivedEvent = MotionEvent.obtain(event); + + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + handleReceivedPointerDown(event.getActionIndex(), event); + } break; + case MotionEvent.ACTION_POINTER_DOWN: { + handleReceivedPointerDown(event.getActionIndex(), event); + } break; + case MotionEvent.ACTION_UP: { + handleReceivedPointerUp(event.getActionIndex(), event); + } break; + case MotionEvent.ACTION_POINTER_UP: { + handleReceivedPointerUp(event.getActionIndex(), event); + } break; + } + if (DEBUG) { + Slog.i(LOG_TAG_RECEIVED_POINTER_TRACKER, "Received pointer:\n" + toString()); + } + } + + /** + * @return The last received event. + */ + public MotionEvent getLastReceivedEvent() { + return mLastReceivedEvent; + } + + /** + * @return The number of received pointers that are down. + */ + public int getReceivedPointerDownCount() { + return Integer.bitCount(mReceivedPointersDown); + } + + /** + * Whether an received pointer is down. + * + * @param pointerId The unique pointer id. + * @return True if the pointer is down. + */ + public boolean isReceivedPointerDown(int pointerId) { + final int pointerFlag = (1 << pointerId); + return (mReceivedPointersDown & pointerFlag) != 0; + } + + /** + * @param pointerId The unique pointer id. + * @return The X coordinate where the pointer went down. + */ + public float getReceivedPointerDownX(int pointerId) { + return mReceivedPointerDownX[pointerId]; + } + + /** + * @param pointerId The unique pointer id. + * @return The Y coordinate where the pointer went down. + */ + public float getReceivedPointerDownY(int pointerId) { + return mReceivedPointerDownY[pointerId]; + } + + /** + * @param pointerId The unique pointer id. + * @return The time when the pointer went down. + */ + public long getReceivedPointerDownTime(int pointerId) { + return mReceivedPointerDownTime[pointerId]; + } + + /** + * @return The id of the primary pointer. + */ + public int getPrimaryPointerId() { + if (mPrimaryPointerId == INVALID_POINTER_ID) { + mPrimaryPointerId = findPrimaryPointerId(); + } + return mPrimaryPointerId; + } + + /** + * @return The time when the last up received pointer went down. + */ + public long getLastReceivedUpPointerDownTime() { + return mLastReceivedUpPointerDownTime; + } + + /** + * @return The down X of the last received pointer that went up. + */ + public float getLastReceivedUpPointerDownX() { + return mLastReceivedUpPointerDownX; + } + + /** + * @return The down Y of the last received pointer that went up. + */ + public float getLastReceivedUpPointerDownY() { + return mLastReceivedUpPointerDownY; + } + + /** + * @return The edge flags of the last received down event. + */ + public int getLastReceivedDownEdgeFlags() { + return mLastReceivedDownEdgeFlags; + } + + /** + * Handles a received pointer down event. + * + * @param pointerIndex The index of the pointer that has changed. + * @param event The event to be handled. + */ + private void handleReceivedPointerDown(int pointerIndex, MotionEvent event) { + final int pointerId = event.getPointerId(pointerIndex); + final int pointerFlag = (1 << pointerId); + + mLastReceivedUpPointerDownTime = 0; + mLastReceivedUpPointerDownX = 0; + mLastReceivedUpPointerDownX = 0; + + mLastReceivedDownEdgeFlags = event.getEdgeFlags(); + + mReceivedPointersDown |= pointerFlag; + mReceivedPointerDownX[pointerId] = event.getX(pointerIndex); + mReceivedPointerDownY[pointerId] = event.getY(pointerIndex); + mReceivedPointerDownTime[pointerId] = event.getEventTime(); + + mPrimaryPointerId = pointerId; + } + + /** + * Handles a received pointer up event. + * + * @param pointerIndex The index of the pointer that has changed. + * @param event The event to be handled. + */ + private void handleReceivedPointerUp(int pointerIndex, MotionEvent event) { + final int pointerId = event.getPointerId(pointerIndex); + final int pointerFlag = (1 << pointerId); + + mLastReceivedUpPointerDownTime = getReceivedPointerDownTime(pointerId); + mLastReceivedUpPointerDownX = mReceivedPointerDownX[pointerId]; + mLastReceivedUpPointerDownY = mReceivedPointerDownY[pointerId]; + + mReceivedPointersDown &= ~pointerFlag; + mReceivedPointerDownX[pointerId] = 0; + mReceivedPointerDownY[pointerId] = 0; + mReceivedPointerDownTime[pointerId] = 0; + + if (mPrimaryPointerId == pointerId) { + mPrimaryPointerId = INVALID_POINTER_ID; + } + } + + /** + * @return The primary pointer id. + */ + private int findPrimaryPointerId() { + int primaryPointerId = INVALID_POINTER_ID; + long minDownTime = Long.MAX_VALUE; + + // Find the pointer that went down first. + int pointerIdBits = mReceivedPointersDown; + while (pointerIdBits > 0) { + final int pointerId = Integer.numberOfTrailingZeros(pointerIdBits); + pointerIdBits &= ~(1 << pointerId); + final long downPointerTime = mReceivedPointerDownTime[pointerId]; + if (downPointerTime < minDownTime) { + minDownTime = downPointerTime; + primaryPointerId = pointerId; + } + } + return primaryPointerId; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("========================="); + builder.append("\nDown pointers #"); + builder.append(getReceivedPointerDownCount()); + builder.append(" [ "); + for (int i = 0; i < MAX_POINTER_COUNT; i++) { + if (isReceivedPointerDown(i)) { + builder.append(i); + builder.append(" "); + } + } + builder.append("]"); + builder.append("\nPrimary pointer id [ "); + builder.append(getPrimaryPointerId()); + builder.append(" ]"); + builder.append("\n========================="); + return builder.toString(); + } + } +} diff --git a/services/accessibility/java/service.mk b/services/accessibility/java/service.mk new file mode 100644 index 0000000..5e8f375 --- /dev/null +++ b/services/accessibility/java/service.mk @@ -0,0 +1,11 @@ +# Include only if the service is required +ifneq ($(findstring accessibility,$(REQUIRED_SERVICES)),) + +SUB_DIR := accessibility/java + +LOCAL_SRC_FILES += \ + $(call all-java-files-under,$(SUB_DIR)) + +#DEFINED_SERVICES += com.android.server.accessibility.AccessibilityManagerService + +endif |