/* * Copyright (C) 2012 Google Inc. * * 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.policy; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.Nullable; import android.app.ActivityManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.pm.ServiceInfo; import android.media.AudioManager; import android.media.Ringtone; import android.media.RingtoneManager; import android.os.Handler; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserManager; import android.provider.Settings; import android.speech.tts.TextToSpeech; import android.util.Log; import android.util.MathUtils; import android.view.IWindowManager; import android.view.MotionEvent; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.WindowManagerInternal; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManager; import com.android.internal.R; import com.android.server.LocalServices; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class EnableAccessibilityController { private static final String TAG = "EnableAccessibilityController"; private static final int SPEAK_WARNING_DELAY_MILLIS = 2000; private static final int ENABLE_ACCESSIBILITY_DELAY_MILLIS = 6000; public static final int MESSAGE_SPEAK_WARNING = 1; public static final int MESSAGE_SPEAK_ENABLE_CANCELED = 2; public static final int MESSAGE_ENABLE_ACCESSIBILITY = 3; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message message) { switch (message.what) { case MESSAGE_SPEAK_WARNING: { String text = mContext.getString(R.string.continue_to_enable_accessibility); mTts.speak(text, TextToSpeech.QUEUE_FLUSH, null); } break; case MESSAGE_SPEAK_ENABLE_CANCELED: { String text = mContext.getString(R.string.enable_accessibility_canceled); mTts.speak(text, TextToSpeech.QUEUE_FLUSH, null); } break; case MESSAGE_ENABLE_ACCESSIBILITY: { enableAccessibility(); mTone.play(); mTts.speak(mContext.getString(R.string.accessibility_enabled), TextToSpeech.QUEUE_FLUSH, null); } break; } } }; private final IAccessibilityManager mAccessibilityManager = IAccessibilityManager .Stub.asInterface(ServiceManager.getService("accessibility")); private final Context mContext; private final Runnable mOnAccessibilityEnabledCallback; private final UserManager mUserManager; private final TextToSpeech mTts; private final Ringtone mTone; private final float mTouchSlop; private boolean mDestroyed; private boolean mCanceled; private float mFirstPointerDownX; private float mFirstPointerDownY; private float mSecondPointerDownX; private float mSecondPointerDownY; public EnableAccessibilityController(Context context, Runnable onAccessibilityEnabledCallback) { mContext = context; mOnAccessibilityEnabledCallback = onAccessibilityEnabledCallback; mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); mTts = new TextToSpeech(context, new TextToSpeech.OnInitListener() { @Override public void onInit(int status) { if (mDestroyed) { mTts.shutdown(); } } }); mTone = RingtoneManager.getRingtone(context, Settings.System.DEFAULT_NOTIFICATION_URI); mTone.setStreamType(AudioManager.STREAM_MUSIC); mTouchSlop = context.getResources().getDimensionPixelSize( R.dimen.accessibility_touch_slop); } public static boolean canEnableAccessibilityViaGesture(Context context) { AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(context); // Accessibility is enabled and there is an enabled speaking // accessibility service, then we have nothing to do. if (accessibilityManager.isEnabled() && !accessibilityManager.getEnabledAccessibilityServiceList( AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty()) { return false; } // If the global gesture is enabled and there is a speaking service // installed we are good to go, otherwise there is nothing to do. return Settings.Global.getInt(context.getContentResolver(), Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1 && !getInstalledSpeakingAccessibilityServices(context).isEmpty(); } public static List<AccessibilityServiceInfo> getInstalledSpeakingAccessibilityServices( Context context) { List<AccessibilityServiceInfo> services = new ArrayList<AccessibilityServiceInfo>(); services.addAll(AccessibilityManager.getInstance(context) .getInstalledAccessibilityServiceList()); Iterator<AccessibilityServiceInfo> iterator = services.iterator(); while (iterator.hasNext()) { AccessibilityServiceInfo service = iterator.next(); if ((service.feedbackType & AccessibilityServiceInfo.FEEDBACK_SPOKEN) == 0) { iterator.remove(); } } return services; } public void onDestroy() { mDestroyed = true; } public boolean onInterceptTouchEvent(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN && event.getPointerCount() == 2) { mFirstPointerDownX = event.getX(0); mFirstPointerDownY = event.getY(0); mSecondPointerDownX = event.getX(1); mSecondPointerDownY = event.getY(1); mHandler.sendEmptyMessageDelayed(MESSAGE_SPEAK_WARNING, SPEAK_WARNING_DELAY_MILLIS); mHandler.sendEmptyMessageDelayed(MESSAGE_ENABLE_ACCESSIBILITY, ENABLE_ACCESSIBILITY_DELAY_MILLIS); return true; } return false; } public boolean onTouchEvent(MotionEvent event) { final int pointerCount = event.getPointerCount(); final int action = event.getActionMasked(); if (mCanceled) { if (action == MotionEvent.ACTION_UP) { mCanceled = false; } return true; } switch (action) { case MotionEvent.ACTION_POINTER_DOWN: { if (pointerCount > 2) { cancel(); } } break; case MotionEvent.ACTION_MOVE: { final float firstPointerMove = MathUtils.dist(event.getX(0), event.getY(0), mFirstPointerDownX, mFirstPointerDownY); if (Math.abs(firstPointerMove) > mTouchSlop) { cancel(); } final float secondPointerMove = MathUtils.dist(event.getX(1), event.getY(1), mSecondPointerDownX, mSecondPointerDownY); if (Math.abs(secondPointerMove) > mTouchSlop) { cancel(); } } break; case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_CANCEL: { cancel(); } break; } return true; } private void cancel() { mCanceled = true; if (mHandler.hasMessages(MESSAGE_SPEAK_WARNING)) { mHandler.removeMessages(MESSAGE_SPEAK_WARNING); } else if (mHandler.hasMessages(MESSAGE_ENABLE_ACCESSIBILITY)) { mHandler.sendEmptyMessage(MESSAGE_SPEAK_ENABLE_CANCELED); } mHandler.removeMessages(MESSAGE_ENABLE_ACCESSIBILITY); } private void enableAccessibility() { if (enableAccessibility(mContext)) { mOnAccessibilityEnabledCallback.run(); } } public static boolean enableAccessibility(Context context) { final IAccessibilityManager accessibilityManager = IAccessibilityManager .Stub.asInterface(ServiceManager.getService("accessibility")); final WindowManagerInternal windowManager = LocalServices.getService( WindowManagerInternal.class); final UserManager userManager = (UserManager) context.getSystemService( Context.USER_SERVICE); ComponentName componentName = getInstalledSpeakingAccessibilityServiceComponent(context); if (componentName == null) { return false; } boolean keyguardLocked = windowManager.isKeyguardLocked(); final boolean hasMoreThanOneUser = userManager.getUsers().size() > 1; try { if (!keyguardLocked || !hasMoreThanOneUser) { final int userId = ActivityManager.getCurrentUser(); accessibilityManager.enableAccessibilityService(componentName, userId); } else if (keyguardLocked) { accessibilityManager.temporaryEnableAccessibilityStateUntilKeyguardRemoved( componentName, true /* enableTouchExploration */); } } catch (RemoteException e) { Log.e(TAG, "cannot enable accessibilty: " + e); } return true; } public static void disableAccessibility(Context context) { final IAccessibilityManager accessibilityManager = IAccessibilityManager .Stub.asInterface(ServiceManager.getService("accessibility")); ComponentName componentName = getInstalledSpeakingAccessibilityServiceComponent(context); if (componentName == null) { return; } final int userId = ActivityManager.getCurrentUser(); try { accessibilityManager.disableAccessibilityService(componentName, userId); } catch (RemoteException e) { Log.e(TAG, "cannot disable accessibility " + e); } } public static boolean isAccessibilityEnabled(Context context) { final AccessibilityManager accessibilityManager = context.getSystemService(AccessibilityManager.class); List enabledServices = accessibilityManager.getEnabledAccessibilityServiceList( AccessibilityServiceInfo.FEEDBACK_SPOKEN); return enabledServices != null && !enabledServices.isEmpty(); } @Nullable public static ComponentName getInstalledSpeakingAccessibilityServiceComponent( Context context) { List<AccessibilityServiceInfo> services = getInstalledSpeakingAccessibilityServices(context); if (services.isEmpty()) { return null; } ServiceInfo serviceInfo = services.get(0).getResolveInfo().serviceInfo; return new ComponentName(serviceInfo.packageName, serviceInfo.name); } }