summaryrefslogtreecommitdiffstats
path: root/services/java/com/android/server/accessibility/AccessibilityManagerService.java
diff options
context:
space:
mode:
Diffstat (limited to 'services/java/com/android/server/accessibility/AccessibilityManagerService.java')
-rw-r--r--services/java/com/android/server/accessibility/AccessibilityManagerService.java735
1 files changed, 735 insertions, 0 deletions
diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
new file mode 100644
index 0000000..ba74d86
--- /dev/null
+++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -0,0 +1,735 @@
+/*
+ ** 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 com.android.internal.content.PackageMonitor;
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.HandlerCaller.SomeArgs;
+
+import android.accessibilityservice.AccessibilityService;
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.accessibilityservice.IEventListener;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+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.net.Uri;
+import android.os.Binder;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.text.TextUtils.SimpleStringSplitter;
+import android.util.Config;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.IAccessibilityManager;
+import android.view.accessibility.IAccessibilityManagerClient;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 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
+ implements HandlerCaller.Callback {
+
+ private static final String LOG_TAG = "AccessibilityManagerService";
+
+ private static int sIdCounter = 0;
+
+ private static final int OWN_PROCESS_ID = android.os.Process.myPid();
+
+ private static final int DO_SET_SERVICE_INFO = 10;
+
+ final HandlerCaller mCaller;
+
+ final Context mContext;
+
+ final Object mLock = new Object();
+
+ final List<Service> mServices = new ArrayList<Service>();
+
+ final List<IAccessibilityManagerClient> mClients =
+ new ArrayList<IAccessibilityManagerClient>();
+
+ final Map<ComponentName, Service> mComponentNameToServiceMap =
+ new HashMap<ComponentName, Service>();
+
+ private final List<ServiceInfo> mInstalledServices = new ArrayList<ServiceInfo>();
+
+ private final Set<ComponentName> mEnabledServices = new HashSet<ComponentName>();
+
+ private final SimpleStringSplitter mStringColonSplitter = new SimpleStringSplitter(':');
+
+ private PackageManager mPackageManager;
+
+ private int mHandledFeedbackTypes = 0;
+
+ private boolean mIsEnabled;
+
+ /**
+ * Handler for delayed event dispatch.
+ */
+ private Handler mHandler = new Handler() {
+
+ @Override
+ public void handleMessage(Message message) {
+ Service service = (Service) message.obj;
+ int eventType = message.arg1;
+
+ synchronized (mLock) {
+ notifyEventListenerLocked(service, eventType);
+ AccessibilityEvent oldEvent = service.mPendingEvents.get(eventType);
+ service.mPendingEvents.remove(eventType);
+ tryRecycleLocked(oldEvent);
+ }
+ }
+ };
+
+ /**
+ * Creates a new instance.
+ *
+ * @param context A {@link Context} instance.
+ */
+ public AccessibilityManagerService(Context context) {
+ mContext = context;
+ mPackageManager = mContext.getPackageManager();
+ mCaller = new HandlerCaller(context, this);
+
+ registerPackageChangeAndBootCompletedBroadcastReceiver();
+ registerSettingsContentObservers();
+ }
+
+ /**
+ * Registers a {@link BroadcastReceiver} for the events of
+ * adding/changing/removing/restarting a package and boot completion.
+ */
+ private void registerPackageChangeAndBootCompletedBroadcastReceiver() {
+ Context context = mContext;
+
+ PackageMonitor monitor = new PackageMonitor() {
+ @Override
+ public void onSomePackagesChanged() {
+ synchronized (mLock) {
+ populateAccessibilityServiceListLocked();
+ manageServicesLocked();
+ }
+ }
+
+ @Override
+ public boolean onHandleForceStop(Intent intent, String[] packages,
+ int uid, boolean doit) {
+ synchronized (mLock) {
+ boolean changed = false;
+ Iterator<ComponentName> it = 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();
+ changed = true;
+ }
+ }
+ }
+ if (changed) {
+ it = mEnabledServices.iterator();
+ StringBuilder str = new StringBuilder();
+ while (it.hasNext()) {
+ if (str.length() > 0) {
+ str.append(':');
+ }
+ str.append(it.next().flattenToShortString());
+ }
+ Settings.Secure.putString(mContext.getContentResolver(),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ str.toString());
+ manageServicesLocked();
+ }
+ return false;
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() == Intent.ACTION_BOOT_COMPLETED) {
+ synchronized (mLock) {
+ populateAccessibilityServiceListLocked();
+
+ // get the accessibility enabled setting on boot
+ mIsEnabled = Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1;
+
+ // if accessibility is enabled inform our clients we are on
+ if (mIsEnabled) {
+ updateClientsLocked();
+ }
+
+ manageServicesLocked();
+ }
+
+ return;
+ }
+
+ super.onReceive(context, intent);
+ }
+ };
+
+ // package changes
+ monitor.register(context, true);
+
+ // boot completed
+ IntentFilter bootFiler = new IntentFilter(Intent.ACTION_BOOT_COMPLETED);
+ mContext.registerReceiver(monitor, bootFiler);
+ }
+
+ /**
+ * {@link ContentObserver}s for {@link Settings.Secure#ACCESSIBILITY_ENABLED}
+ * and {@link Settings.Secure#ENABLED_ACCESSIBILITY_SERVICES} settings.
+ */
+ private void registerSettingsContentObservers() {
+ ContentResolver contentResolver = mContext.getContentResolver();
+
+ Uri enabledUri = Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_ENABLED);
+ contentResolver.registerContentObserver(enabledUri, false,
+ new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+
+ synchronized (mLock) {
+ mIsEnabled = Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1;
+ if (mIsEnabled) {
+ manageServicesLocked();
+ } else {
+ unbindAllServicesLocked();
+ }
+ updateClientsLocked();
+ }
+ }
+ });
+
+ Uri providersUri =
+ Settings.Secure.getUriFor(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+ contentResolver.registerContentObserver(providersUri, false,
+ new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+
+ synchronized (mLock) {
+ manageServicesLocked();
+ }
+ }
+ });
+ }
+
+ public boolean addClient(IAccessibilityManagerClient client) {
+ synchronized (mLock) {
+ mClients.add(client);
+ return mIsEnabled;
+ }
+ }
+
+ public boolean sendAccessibilityEvent(AccessibilityEvent event) {
+ synchronized (mLock) {
+ notifyAccessibilityServicesDelayedLocked(event, false);
+ notifyAccessibilityServicesDelayedLocked(event, true);
+ }
+ // event not scheduled for dispatch => recycle
+ if (mHandledFeedbackTypes == 0) {
+ event.recycle();
+ } else {
+ mHandledFeedbackTypes = 0;
+ }
+
+ return (OWN_PROCESS_ID != Binder.getCallingPid());
+ }
+
+ public List<ServiceInfo> getAccessibilityServiceList() {
+ synchronized (mLock) {
+ return mInstalledServices;
+ }
+ }
+
+ public void interrupt() {
+ synchronized (mLock) {
+ for (int i = 0, count = mServices.size(); i < count; i++) {
+ Service service = mServices.get(i);
+ try {
+ service.mServiceInterface.onInterrupt();
+ } catch (RemoteException re) {
+ if (re instanceof DeadObjectException) {
+ Slog.w(LOG_TAG, "Dead " + service.mService + ". Cleaning up.");
+ if (removeDeadServiceLocked(service)) {
+ count--;
+ i--;
+ }
+ } else {
+ Slog.e(LOG_TAG, "Error during sending interrupt request to "
+ + service.mService, re);
+ }
+ }
+ }
+ }
+ }
+
+ public void executeMessage(Message message) {
+ switch (message.what) {
+ case DO_SET_SERVICE_INFO:
+ SomeArgs arguments = ((SomeArgs) message.obj);
+
+ AccessibilityServiceInfo info = (AccessibilityServiceInfo) arguments.arg1;
+ Service service = (Service) arguments.arg2;
+
+ synchronized (mLock) {
+ service.mEventTypes = info.eventTypes;
+ service.mFeedbackType = info.feedbackType;
+ String[] packageNames = info.packageNames;
+ if (packageNames != null) {
+ service.mPackageNames.addAll(Arrays.asList(packageNames));
+ }
+ service.mNotificationTimeout = info.notificationTimeout;
+ service.mIsDefault = (info.flags & AccessibilityServiceInfo.DEFAULT) != 0;
+ }
+ return;
+ default:
+ Slog.w(LOG_TAG, "Unknown message type: " + message.what);
+ }
+ }
+
+ /**
+ * Populates the cached list of installed {@link AccessibilityService}s.
+ */
+ private void populateAccessibilityServiceListLocked() {
+ mInstalledServices.clear();
+
+ List<ResolveInfo> installedServices = mPackageManager.queryIntentServices(
+ new Intent(AccessibilityService.SERVICE_INTERFACE), PackageManager.GET_SERVICES);
+
+ for (int i = 0, count = installedServices.size(); i < count; i++) {
+ mInstalledServices.add(installedServices.get(i).serviceInfo);
+ }
+ }
+
+ /**
+ * 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 {
+ for (int i = 0, count = mServices.size(); i < count; i++) {
+ Service service = mServices.get(i);
+
+ if (service.mIsDefault == isDefault) {
+ if (canDispathEventLocked(service, event, mHandledFeedbackTypes)) {
+ mHandledFeedbackTypes |= service.mFeedbackType;
+ notifyAccessibilityServiceDelayedLocked(service, 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;
+ }
+ }
+
+ /**
+ * Performs an {@link AccessibilityService} delayed notification. The delay is configurable
+ * and denotes the period after the last event before notifying the service.
+ *
+ * @param service The service.
+ * @param event The event.
+ */
+ private void notifyAccessibilityServiceDelayedLocked(Service service,
+ AccessibilityEvent event) {
+ synchronized (mLock) {
+ int eventType = event.getEventType();
+ AccessibilityEvent oldEvent = service.mPendingEvents.get(eventType);
+ service.mPendingEvents.put(eventType, event);
+
+ int what = eventType | (service.mId << 16);
+ if (oldEvent != null) {
+ mHandler.removeMessages(what);
+ tryRecycleLocked(oldEvent);
+ }
+
+ Message message = mHandler.obtainMessage(what, service);
+ message.arg1 = event.getEventType();
+ mHandler.sendMessageDelayed(message, service.mNotificationTimeout);
+ }
+ }
+
+ /**
+ * Recycles an event if it can be safely recycled. The condition is that no
+ * not notified service is interested in the event.
+ *
+ * @param event The event.
+ */
+ private void tryRecycleLocked(AccessibilityEvent event) {
+ if (event == null) {
+ return;
+ }
+ int eventType = event.getEventType();
+ List<Service> services = mServices;
+
+ // linear in the number of service which is not large
+ for (int i = 0, count = services.size(); i < count; i++) {
+ Service service = services.get(i);
+ if (service.mPendingEvents.get(eventType) == event) {
+ return;
+ }
+ }
+ event.recycle();
+ }
+
+ /**
+ * Notifies a service for a scheduled event given the event type.
+ *
+ * @param service The service.
+ * @param eventType The type of the event to dispatch.
+ */
+ private void notifyEventListenerLocked(Service service, int eventType) {
+ IEventListener listener = service.mServiceInterface;
+ AccessibilityEvent event = service.mPendingEvents.get(eventType);
+
+ try {
+ listener.onAccessibilityEvent(event);
+ if (Config.DEBUG) {
+ Slog.i(LOG_TAG, "Event " + event + " sent to " + listener);
+ }
+ } catch (RemoteException re) {
+ if (re instanceof DeadObjectException) {
+ Slog.w(LOG_TAG, "Dead " + service.mService + ". Cleaning up.");
+ removeDeadServiceLocked(service);
+ } else {
+ Slog.e(LOG_TAG, "Error during sending " + event + " to " + service.mService, re);
+ }
+ }
+ }
+
+ /**
+ * Removes a dead service.
+ *
+ * @param service The service.
+ * @return True if the service was removed, false otherwise.
+ */
+ private boolean removeDeadServiceLocked(Service service) {
+ if (Config.DEBUG) {
+ Slog.i(LOG_TAG, "Dead service " + service.mService + " removed");
+ }
+ mHandler.removeMessages(service.mId);
+ return mServices.remove(service);
+ }
+
+ /**
+ * 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.isConfigured()) {
+ return false;
+ }
+
+ if (!service.mService.isBinderAlive()) {
+ removeDeadServiceLocked(service);
+ 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;
+ }
+
+ /**
+ * Manages services by starting enabled ones and stopping disabled ones.
+ */
+ private void manageServicesLocked() {
+ populateEnabledServicesLocked(mEnabledServices);
+ updateServicesStateLocked(mInstalledServices, mEnabledServices);
+ }
+
+ /**
+ * Unbinds all bound services.
+ */
+ private void unbindAllServicesLocked() {
+ List<Service> services = mServices;
+
+ for (int i = 0, count = services.size(); i < count; i++) {
+ Service service = services.get(i);
+ if (service.unbind()) {
+ i--;
+ count--;
+ }
+ }
+ }
+
+ /**
+ * Populates a list with the {@link ComponentName}s of all enabled
+ * {@link AccessibilityService}s.
+ *
+ * @param enabledServices The list.
+ */
+ private void populateEnabledServicesLocked(Set<ComponentName> enabledServices) {
+ enabledServices.clear();
+
+ String servicesValue = Settings.Secure.getString(mContext.getContentResolver(),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+
+ if (servicesValue != null) {
+ TextUtils.SimpleStringSplitter splitter = mStringColonSplitter;
+ splitter.setString(servicesValue);
+ while (splitter.hasNext()) {
+ String str = splitter.next();
+ if (str == null || str.length() <= 0) {
+ continue;
+ }
+ ComponentName enabledService = ComponentName.unflattenFromString(str);
+ if (enabledService != null) {
+ enabledServices.add(enabledService);
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the state of each service by starting (or keeping running) enabled ones and
+ * stopping the rest.
+ *
+ * @param installedServices All installed {@link AccessibilityService}s.
+ * @param enabledServices The {@link ComponentName}s of the enabled services.
+ */
+ private void updateServicesStateLocked(List<ServiceInfo> installedServices,
+ Set<ComponentName> enabledServices) {
+
+ Map<ComponentName, Service> componentNameToServiceMap = mComponentNameToServiceMap;
+ boolean isEnabled = mIsEnabled;
+
+ for (int i = 0, count = installedServices.size(); i < count; i++) {
+ ServiceInfo intalledService = installedServices.get(i);
+ ComponentName componentName = new ComponentName(intalledService.packageName,
+ intalledService.name);
+ Service service = componentNameToServiceMap.get(componentName);
+
+ if (isEnabled) {
+ if (enabledServices.contains(componentName)) {
+ if (service == null) {
+ service = new Service(componentName);
+ }
+ service.bind();
+ } else if (!enabledServices.contains(componentName)) {
+ if (service != null) {
+ service.unbind();
+ }
+ }
+ } else {
+ if (service != null) {
+ service.unbind();
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the state of {@link android.view.accessibility.AccessibilityManager} clients.
+ */
+ private void updateClientsLocked() {
+ for (int i = 0, count = mClients.size(); i < count; i++) {
+ try {
+ mClients.get(i).setEnabled(mIsEnabled);
+ } catch (RemoteException re) {
+ mClients.remove(i);
+ count--;
+ i--;
+ }
+ }
+ }
+
+ /**
+ * 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 {
+ int mId = 0;
+
+ IBinder mService;
+
+ IEventListener mServiceInterface;
+
+ int mEventTypes;
+
+ int mFeedbackType;
+
+ Set<String> mPackageNames = new HashSet<String>();
+
+ boolean mIsDefault;
+
+ long mNotificationTimeout;
+
+ boolean mIsActive;
+
+ ComponentName mComponentName;
+
+ Intent mIntent;
+
+ // the events pending events to be dispatched to this service
+ final SparseArray<AccessibilityEvent> mPendingEvents =
+ new SparseArray<AccessibilityEvent>();
+
+ Service(ComponentName componentName) {
+ mId = sIdCounter++;
+ mComponentName = componentName;
+ 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));
+ }
+
+ /**
+ * Binds to the accessibility service.
+ *
+ * @return True if binding is successful.
+ */
+ public boolean bind() {
+ if (mService == null) {
+ return mContext.bindService(mIntent, this, Context.BIND_AUTO_CREATE);
+ }
+ 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 unbind() {
+ if (mService != null) {
+ mService = null;
+ mContext.unbindService(this);
+ mComponentNameToServiceMap.remove(mComponentName);
+ mServices.remove(this);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns if the service is configured i.e. at least event types of interest
+ * and feedback type must be set.
+ *
+ * @return True if the service is configured, false otherwise.
+ */
+ public boolean isConfigured() {
+ return (mEventTypes != 0 && mFeedbackType != 0);
+ }
+
+ public void setServiceInfo(AccessibilityServiceInfo info) {
+ mCaller.obtainMessageOO(DO_SET_SERVICE_INFO, info, this).sendToTarget();
+ }
+
+ public void onServiceConnected(ComponentName componentName, IBinder service) {
+ mService = service;
+ mServiceInterface = IEventListener.Stub.asInterface(service);
+
+ try {
+ mServiceInterface.setConnection(this);
+ synchronized (mLock) {
+ if (!mServices.contains(this)) {
+ mServices.add(this);
+ mComponentNameToServiceMap.put(componentName, this);
+ }
+ }
+ } catch (RemoteException re) {
+ Slog.w(LOG_TAG, "Error while setting Controller for service: " + service, re);
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName componentName) {
+ synchronized (mLock) {
+ Service service = mComponentNameToServiceMap.remove(componentName);
+ mServices.remove(service);
+ }
+ }
+ }
+}