[Android][Framework] 无障碍快捷方式相关代码

问题:无障碍快捷方式(Accessibility Shortcut)打开不生效。

如图,打开功能后,长按power键会出现振动,震动后双指放在屏幕上会打开无障碍。

无障碍的功能从来没有接触过,也不清楚在哪个模块修改,所以下面记录一下如何快速定位这种问题的思路:

在Opengrok检索"Accessibility Shortcut"找到字串accessibility_global_gesture_preference_title,可以确定两个地方:

  • Settings里Accessibility选项的入口 packages/apps/Settings/res/xml/accessibility_settings.xml
  • Accessibility的控制代码 packages/apps/Settings/src/com/android/settings/accessibility/AccessibilitySettings.java
AccessibilitySettings.java

通过阅读方法列表,知道这个类完全是用来控制Settings的Accessibility界面按钮的逻辑。

从控制代码,知道打开上图界面的代码是:

private void handleToggleEnableAccessibilityGesturePreferenceClick() {
    Bundle extras = mGlobalGesturePreferenceScreen.getExtras();
    extras.putString(EXTRA_TITLE, getString(
        R.string.accessibility_global_gesture_preference_title));
    extras.putString(EXTRA_SUMMARY, getString(
        R.string.accessibility_global_gesture_preference_description));
    extras.putBoolean(EXTRA_CHECKED, Settings.Global.getInt(getContentResolver(),Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1);
    super.onPreferenceTreeClick(mGlobalGesturePreferenceScreen);
}

这一步是设置Title和Summary,和设置SwichBar是否check。所以这个KEY是控制变量的关键。

搜索Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED,可以了解到几个地方:

ToggleGlobalGesturePreferenceFragment.java

这里通过KEY的命名和相关类的命名,可以知道,Accessibility Shortcut打开后,相关的手势被称为Global Gesture,全局手势。了解命名也很重要,这对于分析不熟悉的模块很有帮助。

这个Fragment是处理SwichBar的回调。每当SwichBar状态变化,就会更新相关的值:

@Override
protected void onPreferenceToggled(String preferenceKey, boolean enabled) {
    Settings.Global.putInt(getContentResolver(),
            Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, enabled ? 1 : 0);
}

前面两个类似乎并不是关键,后面的两个就不一样了。

PhoneWindowManager.java

这是个大类,涉及到非常多的控制逻辑,而且从命名就能知道它的核心功能。但实际上,对于这个问题,只需要看一部分逻辑:

Accessibility Shortcut功能是长按POWER键启用。在PWM中,有powerLongPress的处理逻辑:

private void powerLongPress() {
	final int behavior = getResolvedLongPressOnPowerBehavior();
	switch (behavior) {
		case LONG_PRESS_POWER_NOTHING:
			break;
		case LONG_PRESS_POWER_GLOBAL_ACTIONS:
			mPowerKeyHandled = true;
			if (!performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false)) {
				performAuditoryFeedbackForAccessibilityIfNeed();
			}
			showGlobalActionsInternal();
			break;
        case LONG_PRESS_POWER_SHUT_OFF:
        case LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM:
        	mPowerKeyHandled = true;
        	performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false);
        	sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS);
        	mWindowManagerFuncs.shutdown(behavior == LONG_PRESS_POWER_SHUT_OFF);
        	break;
    }
}

首先获取长按POWER键的行为:

private int getResolvedLongPressOnPowerBehavior() {
    if (FactoryTest.isLongPressOnPowerOffEnabled()) {
    	return LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM;
    }
    return mLongPressOnPowerBehavior;
}

正常情况下都会直接返回mLongPressOnPowerBehavior,这个变量初始化如下:

mLongPressOnPowerBehavior = mContext.getResources().getInteger(
                com.android.internal.R.integer.config_longPressOnPowerBehavior);

这个值在frameworks/base/core/res/res/values/config.xml配置为1。所以在Switch会走到LONG_PRESS_POWER_GLOBAL_ACTIONS这个case。

在这个case下,首先会调用performHapticFeedbackLW方法,从名称看,是perform触觉反馈LW方法,最终目的是发出振动。

@Override
public boolean performHapticFeedbackLw(WindowState win, int effectId, boolean always) {
    if (!mVibrator.hasVibrator()) {
    	return false;
    }
    final boolean hapticsDisabled = Settings.System.getIntForUser(mContext.getContentResolver(),
    Settings.System.HAPTIC_FEEDBACK_ENABLED, 0, UserHandle.USER_CURRENT) == 0;
    if (hapticsDisabled && !always) {
    	return false;
    }
    long[] pattern = null;
    switch (effectId) {
        case HapticFeedbackConstants.LONG_PRESS:
        	pattern = mLongPressVibePattern;
        	break;
        case HapticFeedbackConstants.VIRTUAL_KEY:
        	pattern = mVirtualKeyVibePattern;
        	break;
        case HapticFeedbackConstants.KEYBOARD_TAP:
        	pattern = mKeyboardTapVibePattern;
        	break;
        case HapticFeedbackConstants.CLOCK_TICK:
        	pattern = mClockTickVibePattern;
        	break;
        case HapticFeedbackConstants.CALENDAR_DATE:
        	pattern = mCalendarDateVibePattern;
        	break;
        case HapticFeedbackConstants.SAFE_MODE_DISABLED:
        	pattern = mSafeModeDisabledVibePattern;
        	break;
        case HapticFeedbackConstants.SAFE_MODE_ENABLED:
        	pattern = mSafeModeEnabledVibePattern;
        	break;
        case HapticFeedbackConstants.CONTEXT_CLICK:
        	pattern = mContextClickVibePattern;
        	break;
        default:
        	return false;
    }
    int owningUid;
    String owningPackage;
    if (win != null) {
    	owningUid = win.getOwningUid();
    	owningPackage = win.getOwningPackage();
    } else {
    	owningUid = android.os.Process.myUid();
    	owningPackage = mContext.getOpPackageName();
    }
    if (pattern.length == 1) {
    	// One-shot vibration
    	mVibrator.vibrate(owningUid, owningPackage, pattern[0], VIBRATION_ATTRIBUTES);
    } else {
    	// Pattern vibration
    	mVibrator.vibrate(owningUid, owningPackage, pattern, -1, VIBRATION_ATTRIBUTES);
    }
    return true;
}

如果上面返回为false,则没有振动,代码会进入performAuditoryFeedbackForAccessibilityIfNeed,即播放声音。

private void performAuditoryFeedbackForAccessibilityIfNeed() {
    if (!isGlobalAccessibilityGestureEnabled()) {
        return;
    }
    AudioManager audioManager = (AudioManager) mContext.getSystemService(
        Context.AUDIO_SERVICE);
    if (audioManager.isSilentMode()) {
        return;
    }
    Ringtone ringTone = RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI);
    ringTone.setStreamType(AudioManager.STREAM_MUSIC);
    ringTone.play();
}

播放声音前的判断就是 通过获取标志位判断。

private boolean isGlobalAccessibilityGestureEnabled() {
	return Settings.Global.getInt(mContext.getContentResolver(),
		Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1;
}

最后调用的是showGlobalActionsInternal方法

void showGlobalActionsInternal() {
    sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS);
    if (mGlobalActions == null) {
        mGlobalActions = new GlobalActions(mContext, mWindowManagerFuncs);
    }
    final boolean keyguardShowing = isKeyguardShowingAndNotOccluded();
    mGlobalActions.showDialog(keyguardShowing, isDeviceProvisioned());
    if (keyguardShowing) {
        // since it took two seconds of long press to bring this up,
        // poke the wake lock so they have some time to see the dialog.
        mPowerManager.userActivity(SystemClock.uptimeMillis(), false);
    }
}

所以这里看出,PWM只是处理了长按power键的逻辑。那长按之后,双指触摸屏幕的逻辑在哪控制呢?

GlobalActions.java

这个类蛮有意思的,通过类名知道他是全局的一个操作,里面出现大量的控制代码,而且很多和手势操作相关。有机会可以深入了解一下。

在onStart的地方,通过阅读注释,可以知道,这里处理打开Accessibility后的双指触摸,触摸时不销毁弹出的关机选项对话框。

代码中有一个非常重要的判断,它决定是否进入Accessibility模式。

@Override
protected void onStart() {
    // If global accessibility gesture can be performed, we will take care
    // of dismissing the dialog on touch outside. This is because the dialog
    // is dismissed on the first down while the global gesture is a long press
    // with two fingers anywhere on the screen.
    if (EnableAccessibilityController.canEnableAccessibilityViaGesture(mContext)) {
    	mEnableAccessibilityController = new EnableAccessibilityController(mContext,
    	new Runnable() {
    		@Override
    		public void run() {
    			dismiss();
    		}
    	});
    	super.setCanceledOnTouchOutside(false);
    } else {
    	mEnableAccessibilityController = null;
    	super.setCanceledOnTouchOutside(true);
    }

    super.onStart();
}
EnableAccessibilityController.java
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();
}

这里需要判断几层:

  • AccessibilityManager enable
  • AccessibilityManager 服务不为空。这里服务可以是自定义安装的。如果没有可用服务就无从打开。
  • ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED标志位打开,即前面界面的switch按钮打开。
  • talkbak必须安装。因为他需要语音播放一些内容,所以talkbak是必备的。

我遇到的问题就是手机没有集成GMS talkbak,导致Accessibility打开没反应。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android小菜鸡

解决横竖屏切换导致自定义View切换Fragment失败

  自定义的BottomSelectView,在切换横竖屏后,底部选择器点击后Fragment不切换,失去效果。

964
来自专栏Winter漫聊技术

Retrofit进阶

什么是Retrofit? 这类文章太多了,这里就不多做介绍,贴个官方链接: http://square.github.io/retrofit/

1112
来自专栏HansBug's Lab

算法模板——线性筛素数

实现功能:如题,筛出1——N内的所有素数 原理:如phile神犇所言,这次的才算是真正意义上的线性筛素数,其精髓在于if (i mod a[j])=0 then...

29412
来自专栏Android常用基础

应用自动更新封装-Android

应用更新应该是现在每个应用必备的一个功能。正是通过不断的更新,不断的调优,才使我们的应用更完善。当然在各大应用市场中,它们已经帮我们实现了这项功能,但是有一个问...

1781
来自专栏张善友的专栏

如何分析Performance Monitor (PerfMon) Log

  Windows 2003 和xp提供了性能监视器的几个命令行工具,他们是logman utility (logman.exe),relog utility ...

2048
来自专栏向治洪

android优化之省电

Android程序中耗电最多的地方在以下几个方面 : 1、 大数据量的传输。 2、 不停的在网络间切换。 3、 解析大量的文本数据。 那么我们怎么样来改...

19710
来自专栏向治洪

android PakageManagerService启动流程分析

PakageManagerService的启动流程图 ? 1.PakageManagerService概述 PakageManagerService是andro...

50810
来自专栏ASP.NET MVC5 后台权限管理系统

ASP.NET MVC5+EF6+EasyUI 后台管理系统(75)-微信公众平台开发-用户管理

前言 本节主要是关注者(即用户)和用户组的管理,微信公众号提供了用户和用户组的管理,我们可以在微信公众号官方里面进行操作,添加备注和标签,以及移动用户组别,同...

5596
来自专栏Android源码框架分析

十分钟了解Android触摸事件原理(InputManagerService)

从手指接触屏幕到MotionEvent被传送到Activity或者View,中间究竟经历了什么?Android中触摸事件到底是怎么来的呢?源头是哪呢?本文就直观...

4054
来自专栏技术小黑屋

Android性能调优利器StrictMode

作为Android开发,日常的开发工作中或多或少要接触到性能问题,比如我的Android程序运行缓慢卡顿,并且常常出现ANR对话框等等问题。既然有性能问题,就需...

1712

扫码关注云+社区

领取腾讯云代金券