前言

这一周又遇到个麻烦事儿,公司对接了一个老外客户,要求在现有的USDK密码键盘界面自定义密码界面风格,但密码按键的处理以及获取密码数据需要USDK去实现,也就是说老外只负责画界面,把绘制出的界面中的控件坐标传下来(控件的左上角和右下即角坐标),不处理点击事件,让USDK把click事件屏蔽并返回密码数据给他们(这需求也是没谁了~~无力吐槽),so开动吧!谁让我是苦逼的程序猿呢~

正文

首先我们先整理下需要做的事儿:

  1. 如何屏蔽应用层点击事件并获取点击的位置;
  2. 考虑到交互的安全性,输入密码界面需屏蔽最近任务键,Home键,Back键以及下拉任务栏;

针对上面问题1,我首先想到的是setOnTouchListener方法,获取点击的位置不正合适吗?可问题来了,不是一个进程的如何获取到界面的MotionEvent方法呢?那我可以在USDK中添加一个空白的View上去,这样不久可以获取了吗?而且通过设置View的属性也正好可以消耗点击事件,一举两得呀~至于问题2,这个修改系统就OK了,so方案有了:

  1. 添加透明空白View,通过setOnTouchListener方法获取用户点击位置;
  2. 修改系统,找到Home键,Back键以及下拉任务栏的事件处理位置,使用SystemProperties类设置自定义系统属性,在事件处理位置截取判断使能即可;

获取点击位置坐标

这里我定义了一个空的View,layout文件如下:代码块

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/offlinepin_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@android:color/transparent">

    <!--显示透明View可见 方便调试,后续调试OK屏蔽即可-->
    <TextView
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="test"/>

</LinearLayout>

这里通过设置android:background="@android:color/transparent" 将其设置为透明;然后在USDK中添加本View,主要代码如下:代码块

LinearLayout mLayout = (LinearLayout) LayoutInflater.from(TopwiseApplication.getContext()).
	                inflate(R.layout.view_offlinepin, null);
	WindowManager.LayoutParams params = new WindowManager.LayoutParams();
	// 设置框口属性为系统提示级别,属于最上层显示
	params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
	// 设置View颜色支持属性
	params.format = PixelFormat.RGBA_8888;
	params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
	        WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
	//宽 高显示限制
	params.width = WindowManager.LayoutParams.MATCH_PARENT;
	params.height = WindowManager.LayoutParams.MATCH_PARENT;
	SDKLog.d(TAG, "addView");
	//if (manager != null) {
    //    manager.addView(layout, params);
    //    isAddView = true;
    //}
	mHandler = new ViewHandler(mWindowManager, mLayout, params);

此处先获取layout实例,通过WindowManager服务去addView,WindowManager可通过mWindowManager = (WindowManager) TopwiseApplication.getContext().
getSystemService(Context.WINDOW_SERVICE);获取;
解释下WindowManager.LayoutParams布局属性,此属性用于设置当前添加的View的显示,活动包括事件处理等相关配置,其中flags值比较重要,介绍几个常用的:

FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
//Constant Value: 1 (0x00000001)
只要这个window对用户是可见的,则允许在屏幕开启的时候锁定屏幕
这个flag可以单独的使用,也可以配合FLAG_KEEP_SCREEN_ON和(或者) FLAG_SHOW_WHEN_LOCKED使用

FLAG_DIM_BEHIND
//Constant Value: 2 (0x00000002)
所有在这个window之后的会变暗,
使用dimAmount属性来控制变暗的程度(1.0不透明,0.0完全透明)

FLAG_NOT_FOCUSABLE
//Constant Value: 8 (0x00000008)
设置之后window永远不会获取焦点,所以用户不能给此window发送点击事件
焦点会传递给在其下面的可获取焦点的window
这个flag同时会启用 FLAG_NOT_TOUCH_MODAL flag , 不管你有没有手动设置
设置这个flag同时表明了这个window不会和软键盘交互

FLAG_NOT_TOUCHABLE
//Constant Value: 16 (0x00000010)
这个window永远无法获取点击事件

FLAG_NOT_TOUCH_MODAL
//Constant Value: 32 (0x00000020)
即使这个window是可获取焦点的,
也允许window之外点击事件传递给其他在其之后的window
如果不设置这个值,则window消费掉所有点击事件,不管这些点击事件是不是在window的范围之内
这个flag简而言之就是说,当前window区域以外的点击事件传递给下层window,当前window区域以内的点击事件自己处理

FLAG_KEEP_SCREEN_ON
//Constant Value: 128 (0x00000080)
当这个window对用户是可见状态,则保持设备屏幕不关闭且不变暗

所有的flags值详细介绍可参考链接:https://www.jianshu.com/p/c91448e1c7d1

ps:这里我踩了一个小坑,本来我以为客户在调用我这个接口的时候是在主线程中调用的,所以刚开始直接如注释代码那样写了,后面发现addView的时候没成功,catch了一下addView的异常,才发现是非主线程,so自定义了一个ViewHandler,代码如下:代码块

private static class ViewHandler extends Handler {
        private WindowManager manager;
        private LinearLayout layout;
        private WindowManager.LayoutParams params;
        private boolean isAddView;

        ViewHandler(WindowManager manager, LinearLayout layout, WindowManager.LayoutParams params) {
            //保证为主线程处理,如果在主线程中创建Handler,可不加此行代码
            super(Looper.getMainLooper());
            this.manager = manager;
            this.layout = layout;
            this.params = params;
            isAddView = false;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case ADD_VIEW_TAG:
                    if (manager != null) {
                        manager.addView(layout, params);
                        isAddView = true;
                    }
                    break;
                case DELETE_VIEW_TAG:
                    try {
                        if (manager != null && isAddView) {
                            isAddView = false;
                            manager.removeView(layout);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        SDKLog.d(TAG, "remove view fail!");
                    }
                    break;
                default:
                    break;
            }
        }
    }

添加好View以后设置监听事件就简单了,代码块

mLayout.setOnTouchListener(this);

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            SDKLog.d(TAG, "touch x: " + event.getX() + ";y: " + event.getY());
        }
        return true;
    }

这里有个小点需要提一下,如果这里单单实现onTouch或者onClick点击事件的话,会提示你缺少事件分配方法,大概提示如下:

onTouch should call View#performClick when a click is detected less… (Ctrl+F1)
 …


如果覆盖onTouchEvent或使用OnTouchListener的View没有实现performClick方法,并且在检测到click事件时调用它,则View可能无法正确地处理可访问性操作。处理单击操作的逻辑理想情况下应该放在View#performClick中,因为某些可访问性服务在应该发生单击操作时调用performClick。

如果想细查问题原因,就得看Android的View事件分发机制了,这里不多讲,只告诉你添加*@SuppressLint(“ClickableViewAccessibility”)*这行注解就可以;

屏蔽最近任务键,Home键以及下拉任务栏

这里是使用SystemProperties类去设置读取自定义字段值,在功能实现代码处进行判断屏蔽,其实只要找到各个按键的功能实现位置,基本就没什么问题;

SystemProperties.set("persist.sys.usdk.home.enable", String.valueOf(flag));
SystemProperties.getBoolean("persist.sys.usdk.home.enable",false);

此处屏蔽的是虚拟按键,实体键通过KeyEvent可获取,不需要改系统代码,源码:MTK7.0

Home键代码位置

PhoneWindowManager类中的interceptKeyBeforeDispatching方法,此方法为按键事件分发前拦截,部分代码如下:代码块

@Override
    public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) {
        final boolean keyguardOn = keyguardOn();
        final int keyCode = event.getKeyCode();
        final int repeatCount = event.getRepeatCount();
        final int metaState = event.getMetaState();
        final int flags = event.getFlags();
        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
        final boolean canceled = event.isCanceled();
        // 拦截Home按键代码块 --start--
        if(keyCode == KeyEvent.KEYCODE_HOME &&
		SystemProperties.getBoolean("persist.sys.usdk.home.enable",false)){
            String pkgName = win.getAttrs().packageName;
            if (!"com.centerm.frame".equals(pkgName)) {
                return -1;
            }
        }
        // -- end --
        / ...
          ...
          ... /
    }

下拉任务栏代码位置

SystemUI中的PanelBar类中的onTouchEvent方法,部分代码如下:代码块

@Override
    public boolean onTouchEvent(MotionEvent event) {
	    / ...
	      ...
	      ... /
	    if(SystemProperties.getBoolean("persist.sys.usdk.drop.enable",false)){
		    return false;
	    }
    }

RecentApp键代码位置

PhoneStatusBar类中相关代码:代码块

// init clickListenter
    private void prepareNavigationBarView() {
        mNavigationBarView.reorient();

        ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton();
        recentsButton.setOnClickListener(mRecentsClickListener);
        recentsButton.setOnTouchListener(mRecentsPreloadOnTouchListener);
        recentsButton.setLongClickable(true);
        recentsButton.setOnLongClickListener(mRecentsLongClickListener);

        ButtonDispatcher backButton = mNavigationBarView.getBackButton();
        backButton.setLongClickable(true);
        backButton.setOnLongClickListener(mLongPressBackListener);

        ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
        homeButton.setOnTouchListener(mHomeActionListener);
        homeButton.setOnLongClickListener(mLongPressHomeListener);

        ButtonDispatcher hideButton = mNavigationBarView.getHideButton();
        hideButton.setOnClickListener(mHideClickListener);

        /// M: BMW  restore button @{
        if (MultiWindowManager.isSupported()) {
            ButtonDispatcher restoreButton = mNavigationBarView.getRestoreButton();
            restoreButton.setOnClickListener(mRestoreClickListener);

        }
        /// @}

        mAssistManager.onConfigurationChanged();
    }

    private View.OnClickListener mRecentsClickListener = new View.OnClickListener() {
        public void onClick(View v) {
            / ...
              ...
              ... /
            boolean isEnable = true;
            try{
                isEnable = mWindowManagerService.isRecentAppsKeyEnable();
            }catch(RemoteException e) {
                e.printStackTrace();
            }
            if(isEnable && SystemProperties.getBoolean("persist.sys.usdk.appbtn.enable",false)){
                Log.d(TAG, "disable recentApp btn");
                return;
            }
        }
    };

关于系统代码这块的讲解就不多写了,不然写起来很多,后面学习到相关的再统一归类整理;

PPS

WindowManager.LayoutParams params = new WindowManager.LayoutParams();
	// 设置框口属性为系统提示级别,属于最上层显示
	params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;

关于这个type设置,中间还搞了我一手,客户在实现自定义密码键盘界面的时候,使用的是继承Dialog的自定义View,调用USDK接口的位置如下:代码块

@Override
	@Override
	public void onWindowFocusChanged(boolean hasFocus) {
		// TODO Auto-generated method stub
		super.onWindowFocusChanged(hasFocus);
		// 1
		if (!hasFocus) {
			return;
		}
		try {
			pinpad.getPin(param, new GetPinListener.Stub(){
				@Override
				public void onInputKey(int i, String s) throws RemoteException {
					listener.onInputKey(i,s);
				}

				@Override
				public void onError(int i) throws RemoteException {
					listener.onError(i);
					onClose();
				}

				@Override
				public void onConfirmInput(byte[] bytes) throws RemoteException {
					listener.onConfirmInput(bytes);
					onClose();
				}

				@Override
				public void onCancelKeyPress() throws RemoteException {
					listener.onCancelKeyPress();
					onClose();
				}

				@Override
				public void onStopGetPin() throws RemoteException {
					listener.onStopGetPin();
					onClose();
				}
			});
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			onClose();
		}
	}

之前是没有注释1的判断条件的,所以在创建这个Dialog的时候会走onWindowFocusChanged方法,调用USDK的getPin方法,然后USDK中的透明View起来的时候又会走onWindowFocusChanged方法,结果又调用了一次getPin方法,然后程序崩溃;
onWindowFocusChanged方法在这里稍微解释一下,这个方法是在生命周期过程中调用的一个方法,其作用是:从onWindowFocusChanged被执行起,用户可以与应用进行交互了,而这之前,对用户的操作需要做一点限制;具体在生命周期的位置为:
1 entry: onStart---->onResume---->onAttachedToWindow----------->onWindowVisibilityChanged–visibility=0---------->onWindowFocusChanged(true)-------> …
2 exit: onPause---->onStop---->onWindowFocusChanged(false) ---------------------- (lockscreen)
3 exit : onPause----->onWindowFocusChanged(false)-------->onWindowVisibilityChanged–visibility=8------------>onStop(to another activity)
复写这个方法的时候一定注意hasFocus值的判断!

OK,到这里此功能基本就可以满足客户要求了!开心撒花~~~

后记

也不知道之前是写代码少了手生还是自己基础知识不牢固,有些基础知识的使用总感觉不难,知道其原理,但写起来就是漏这漏那,感觉后面还是多看一些基础,有必要多写一些基础性的文章,巩固知识,备战面试!!^ - ^

本文到此结束,欢迎同行(xing)大佬们评论指正,觉得写的还不错滴麻烦点个赞哟~~