diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b0290d0..03c7982 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -36,6 +36,8 @@ + + diff --git a/res/layout/setup_main.xml b/res/layout/setup_main.xml index 5fb5748..75644ab 100644 --- a/res/layout/setup_main.xml +++ b/res/layout/setup_main.xml @@ -18,14 +18,14 @@ android:id="@+id/root" android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:clickable="true"> - + diff --git a/src/com/cyanogenmod/setupwizard/ui/SetupWizardActivity.java b/src/com/cyanogenmod/setupwizard/ui/SetupWizardActivity.java index 6d05c26..d696854 100644 --- a/src/com/cyanogenmod/setupwizard/ui/SetupWizardActivity.java +++ b/src/com/cyanogenmod/setupwizard/ui/SetupWizardActivity.java @@ -24,6 +24,7 @@ import android.content.res.Resources; import android.os.Bundle; import android.provider.Settings; import android.util.Log; +import android.view.MotionEvent; import android.view.View; import android.widget.Button; @@ -35,6 +36,7 @@ import com.cyanogenmod.setupwizard.setup.CyanogenServicesPage; import com.cyanogenmod.setupwizard.setup.CyanogenSettingsPage; import com.cyanogenmod.setupwizard.setup.Page; import com.cyanogenmod.setupwizard.setup.SetupDataCallbacks; +import com.cyanogenmod.setupwizard.util.EnableAccessibilityController; import com.cyanogenmod.setupwizard.util.SetupWizardUtils; import com.cyanogenmod.setupwizard.util.WhisperPushUtils; @@ -81,6 +83,13 @@ public class SetupWizardActivity extends Activity implements SetupDataCallbacks if (savedInstanceState != null && savedInstanceState.containsKey("data")) { mSetupData.load(savedInstanceState.getBundle("data")); } + final EnableAccessibilityController acc = new EnableAccessibilityController(this); + mRootView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return acc.onInterceptTouchEvent(event); + } + }); // Since this is a new component, we need to disable here if the user // has already been through setup on a previous version. try { diff --git a/src/com/cyanogenmod/setupwizard/util/EnableAccessibilityController.java b/src/com/cyanogenmod/setupwizard/util/EnableAccessibilityController.java new file mode 100644 index 0000000..15cc540 --- /dev/null +++ b/src/com/cyanogenmod/setupwizard/util/EnableAccessibilityController.java @@ -0,0 +1,279 @@ +/* + * 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.cyanogenmod.setupwizard.util; + +import android.accessibilityservice.AccessibilityServiceInfo; +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.MathUtils; +import android.view.IWindowManager; +import android.view.MotionEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.IAccessibilityManager; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class 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 IWindowManager mWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService("window")); + + private final IAccessibilityManager mAccessibilityManager = IAccessibilityManager + .Stub.asInterface(ServiceManager.getService("accessibility")); + + + private final Context mContext; + 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) { + mContext = context; + 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(); + } + + private static List getInstalledSpeakingAccessibilityServices( + Context context) { + List services = new ArrayList(); + services.addAll(AccessibilityManager.getInstance(context) + .getInstalledAccessibilityServiceList()); + Iterator 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() { + List services = getInstalledSpeakingAccessibilityServices( + mContext); + if (services.isEmpty()) { + return; + } + boolean keyguardLocked = false; + try { + keyguardLocked = mWindowManager.isKeyguardLocked(); + } catch (RemoteException re) { + /* ignore */ + } + + final boolean hasMoreThanOneUser = mUserManager.getUsers().size() > 1; + + AccessibilityServiceInfo service = services.get(0); + boolean enableTouchExploration = (service.flags + & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0; + // Try to find a service supporting explore by touch. + if (!enableTouchExploration) { + final int serviceCount = services.size(); + for (int i = 1; i < serviceCount; i++) { + AccessibilityServiceInfo candidate = services.get(i); + if ((candidate.flags & AccessibilityServiceInfo + .FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0) { + enableTouchExploration = true; + service = candidate; + break; + } + } + } + + ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; + ComponentName componentName = new ComponentName(serviceInfo.packageName, serviceInfo.name); + if (!keyguardLocked || !hasMoreThanOneUser) { + final int userId = ActivityManager.getCurrentUser(); + String enabledServiceString = componentName.flattenToString(); + ContentResolver resolver = mContext.getContentResolver(); + // Enable one speaking accessibility service. + Settings.Secure.putStringForUser(resolver, + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + enabledServiceString, userId); + // Allow the services we just enabled to toggle touch exploration. + Settings.Secure.putStringForUser(resolver, + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + enabledServiceString, userId); + // Enable touch exploration. + if (enableTouchExploration) { + Settings.Secure.putIntForUser(resolver, Settings.Secure.TOUCH_EXPLORATION_ENABLED, + 1, userId); + } + // Enable accessibility script injection (AndroidVox) for web content. + Settings.Secure.putIntForUser(resolver, Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, + 1, userId); + // Turn on accessibility mode last. + Settings.Secure.putIntForUser(resolver, Settings.Secure.ACCESSIBILITY_ENABLED, + 1, userId); + } else if (keyguardLocked) { + try { + mAccessibilityManager.temporaryEnableAccessibilityStateUntilKeyguardRemoved( + componentName, enableTouchExploration); + } catch (RemoteException re) { + /* ignore */ + } + } + } +} \ No newline at end of file