1/*
2 * Copyright (C) 2012 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.server.policy;
18
19import android.accessibilityservice.AccessibilityServiceInfo;
20import android.app.ActivityManager;
21import android.content.ComponentName;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.pm.ServiceInfo;
25import android.media.AudioManager;
26import android.media.Ringtone;
27import android.media.RingtoneManager;
28import android.os.Handler;
29import android.os.Message;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.os.UserManager;
33import android.provider.Settings;
34import android.speech.tts.TextToSpeech;
35import android.util.MathUtils;
36import android.view.IWindowManager;
37import android.view.MotionEvent;
38import android.view.accessibility.AccessibilityManager;
39import android.view.accessibility.IAccessibilityManager;
40
41import com.android.internal.R;
42
43import java.util.ArrayList;
44import java.util.Iterator;
45import java.util.List;
46
47public class EnableAccessibilityController {
48
49    private static final int SPEAK_WARNING_DELAY_MILLIS = 2000;
50    private static final int ENABLE_ACCESSIBILITY_DELAY_MILLIS = 6000;
51
52    public static final int MESSAGE_SPEAK_WARNING = 1;
53    public static final int MESSAGE_SPEAK_ENABLE_CANCELED = 2;
54    public static final int MESSAGE_ENABLE_ACCESSIBILITY = 3;
55
56    private final Handler mHandler = new Handler() {
57        @Override
58        public void handleMessage(Message message) {
59            switch (message.what) {
60                case MESSAGE_SPEAK_WARNING: {
61                    String text = mContext.getString(R.string.continue_to_enable_accessibility);
62                    mTts.speak(text, TextToSpeech.QUEUE_FLUSH, null);
63                } break;
64                case MESSAGE_SPEAK_ENABLE_CANCELED: {
65                    String text = mContext.getString(R.string.enable_accessibility_canceled);
66                    mTts.speak(text, TextToSpeech.QUEUE_FLUSH, null);
67                } break;
68                case MESSAGE_ENABLE_ACCESSIBILITY: {
69                    enableAccessibility();
70                    mTone.play();
71                    mTts.speak(mContext.getString(R.string.accessibility_enabled),
72                            TextToSpeech.QUEUE_FLUSH, null);
73                } break;
74            }
75        }
76    };
77
78    private final IWindowManager mWindowManager = IWindowManager.Stub.asInterface(
79            ServiceManager.getService("window"));
80
81    private final IAccessibilityManager mAccessibilityManager = IAccessibilityManager
82            .Stub.asInterface(ServiceManager.getService("accessibility"));
83
84
85    private final Context mContext;
86    private final Runnable mOnAccessibilityEnabledCallback;
87    private final UserManager mUserManager;
88    private final TextToSpeech mTts;
89    private final Ringtone mTone;
90
91    private final float mTouchSlop;
92
93    private boolean mDestroyed;
94    private boolean mCanceled;
95
96    private float mFirstPointerDownX;
97    private float mFirstPointerDownY;
98    private float mSecondPointerDownX;
99    private float mSecondPointerDownY;
100
101    public EnableAccessibilityController(Context context, Runnable onAccessibilityEnabledCallback) {
102        mContext = context;
103        mOnAccessibilityEnabledCallback = onAccessibilityEnabledCallback;
104        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
105        mTts = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
106            @Override
107            public void onInit(int status) {
108                if (mDestroyed) {
109                    mTts.shutdown();
110                }
111            }
112        });
113        mTone = RingtoneManager.getRingtone(context, Settings.System.DEFAULT_NOTIFICATION_URI);
114        mTone.setStreamType(AudioManager.STREAM_MUSIC);
115        mTouchSlop = context.getResources().getDimensionPixelSize(
116                R.dimen.accessibility_touch_slop);
117    }
118
119    public static boolean canEnableAccessibilityViaGesture(Context context) {
120        AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(context);
121        // Accessibility is enabled and there is an enabled speaking
122        // accessibility service, then we have nothing to do.
123        if (accessibilityManager.isEnabled()
124                && !accessibilityManager.getEnabledAccessibilityServiceList(
125                        AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty()) {
126            return false;
127        }
128        // If the global gesture is enabled and there is a speaking service
129        // installed we are good to go, otherwise there is nothing to do.
130        return Settings.Global.getInt(context.getContentResolver(),
131                Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1
132                && !getInstalledSpeakingAccessibilityServices(context).isEmpty();
133    }
134
135    private static List<AccessibilityServiceInfo> getInstalledSpeakingAccessibilityServices(
136            Context context) {
137        List<AccessibilityServiceInfo> services = new ArrayList<AccessibilityServiceInfo>();
138        services.addAll(AccessibilityManager.getInstance(context)
139                .getInstalledAccessibilityServiceList());
140        Iterator<AccessibilityServiceInfo> iterator = services.iterator();
141        while (iterator.hasNext()) {
142            AccessibilityServiceInfo service = iterator.next();
143            if ((service.feedbackType & AccessibilityServiceInfo.FEEDBACK_SPOKEN) == 0) {
144                iterator.remove();
145            }
146        }
147        return services;
148    }
149
150    public void onDestroy() {
151        mDestroyed = true;
152    }
153
154    public boolean onInterceptTouchEvent(MotionEvent event) {
155        if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN
156                && event.getPointerCount() == 2) {
157            mFirstPointerDownX = event.getX(0);
158            mFirstPointerDownY = event.getY(0);
159            mSecondPointerDownX = event.getX(1);
160            mSecondPointerDownY = event.getY(1);
161            mHandler.sendEmptyMessageDelayed(MESSAGE_SPEAK_WARNING,
162                    SPEAK_WARNING_DELAY_MILLIS);
163            mHandler.sendEmptyMessageDelayed(MESSAGE_ENABLE_ACCESSIBILITY,
164                   ENABLE_ACCESSIBILITY_DELAY_MILLIS);
165            return true;
166        }
167        return false;
168    }
169
170    public boolean onTouchEvent(MotionEvent event) {
171        final int pointerCount = event.getPointerCount();
172        final int action = event.getActionMasked();
173        if (mCanceled) {
174            if (action == MotionEvent.ACTION_UP) {
175                mCanceled = false;
176            }
177            return true;
178        }
179        switch (action) {
180            case MotionEvent.ACTION_POINTER_DOWN: {
181                if (pointerCount > 2) {
182                    cancel();
183                }
184            } break;
185            case MotionEvent.ACTION_MOVE: {
186                final float firstPointerMove = MathUtils.dist(event.getX(0),
187                        event.getY(0), mFirstPointerDownX, mFirstPointerDownY);
188                if (Math.abs(firstPointerMove) > mTouchSlop) {
189                    cancel();
190                }
191                final float secondPointerMove = MathUtils.dist(event.getX(1),
192                        event.getY(1), mSecondPointerDownX, mSecondPointerDownY);
193                if (Math.abs(secondPointerMove) > mTouchSlop) {
194                    cancel();
195                }
196            } break;
197            case MotionEvent.ACTION_POINTER_UP:
198            case MotionEvent.ACTION_CANCEL: {
199                cancel();
200            } break;
201        }
202        return true;
203    }
204
205    private void cancel() {
206        mCanceled = true;
207        if (mHandler.hasMessages(MESSAGE_SPEAK_WARNING)) {
208            mHandler.removeMessages(MESSAGE_SPEAK_WARNING);
209        } else if (mHandler.hasMessages(MESSAGE_ENABLE_ACCESSIBILITY)) {
210            mHandler.sendEmptyMessage(MESSAGE_SPEAK_ENABLE_CANCELED);
211        }
212        mHandler.removeMessages(MESSAGE_ENABLE_ACCESSIBILITY);
213    }
214
215    private void enableAccessibility() {
216        List<AccessibilityServiceInfo> services = getInstalledSpeakingAccessibilityServices(
217                mContext);
218        if (services.isEmpty()) {
219            return;
220        }
221        boolean keyguardLocked = false;
222        try {
223            keyguardLocked = mWindowManager.isKeyguardLocked();
224        } catch (RemoteException re) {
225            /* ignore */
226        }
227
228        final boolean hasMoreThanOneUser = mUserManager.getUsers().size() > 1;
229
230        AccessibilityServiceInfo service = services.get(0);
231        boolean enableTouchExploration = (service.flags
232                & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0;
233        // Try to find a service supporting explore by touch.
234        if (!enableTouchExploration) {
235            final int serviceCount = services.size();
236            for (int i = 1; i < serviceCount; i++) {
237                AccessibilityServiceInfo candidate = services.get(i);
238                if ((candidate.flags & AccessibilityServiceInfo
239                        .FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0) {
240                    enableTouchExploration = true;
241                    service = candidate;
242                    break;
243                }
244            }
245        }
246
247        ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo;
248        ComponentName componentName = new ComponentName(serviceInfo.packageName, serviceInfo.name);
249        if (!keyguardLocked || !hasMoreThanOneUser) {
250            final int userId = ActivityManager.getCurrentUser();
251            String enabledServiceString = componentName.flattenToString();
252            ContentResolver resolver = mContext.getContentResolver();
253            // Enable one speaking accessibility service.
254            Settings.Secure.putStringForUser(resolver,
255                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
256                    enabledServiceString, userId);
257            // Allow the services we just enabled to toggle touch exploration.
258            Settings.Secure.putStringForUser(resolver,
259                    Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
260                    enabledServiceString, userId);
261            // Enable touch exploration.
262            if (enableTouchExploration) {
263                Settings.Secure.putIntForUser(resolver, Settings.Secure.TOUCH_EXPLORATION_ENABLED,
264                        1, userId);
265            }
266            // Enable accessibility script injection (AndroidVox) for web content.
267            Settings.Secure.putIntForUser(resolver, Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION,
268                    1, userId);
269            // Turn on accessibility mode last.
270            Settings.Secure.putIntForUser(resolver, Settings.Secure.ACCESSIBILITY_ENABLED,
271                    1, userId);
272        } else if (keyguardLocked) {
273            try {
274                mAccessibilityManager.temporaryEnableAccessibilityStateUntilKeyguardRemoved(
275                        componentName, enableTouchExploration);
276            } catch (RemoteException re) {
277                /* ignore */
278            }
279        }
280
281        mOnAccessibilityEnabledCallback.run();
282    }
283}
284