2019-11-30
关键字:最近应用列表、SystemUI、Android拦截杀应用事件
Android 系统里有个“最近应用列表”功能,就是触发以后会将当前正在运行或处于后台运行的应用进程展示出来。用户既可以在这个列表里快速切换应用也可以杀掉某个应用进程。
笔者今天遇到个需求:给应用添加“白名单”功能,白名单中的应用将不能通过“最近应用列表”杀掉。
换句话说,就是拦截杀应用事件。
这个最近应用列表功能是在 SystemUI APK 中的。这个APK可复杂了,笔者花了好几个小时才算摸到这个“最近应用列表”的一点门路,并最终实现了这个应用白名单功能。
现将该需求的实现要点记录下来,希望能帮到有同样需求的其他同学。
另外提一句,笔者是在运行着 Android5.1 操作系统的 rk3288 开发板实现的该需求。
首先,关于这个最近应用列表的核心架构,大致如下简图所示:
SystemUI APK 中似乎有两套 RecentsActivity 。我们需要关心的是 com.android.systemui.recents 包下的。
从 RecentsView 开始都是 View 控件。
每一个应用进程记录都对应着一个 TaskView,如下图所示:
一个 TaskStackView 上包含着若干个 TaskView,而若干个 TaskStackView 又被加载在一个共同的 RecentsView 布局上。
为什么会有若干个 TaskStackView 呢?这是因为 Android 会将最近应用进程记录做“缓存”。就是你在本次使用中所有的应用记录都会被缓存起来,在你重启系统后直接点击查看最近应用记录是能发现上一次关机前的记录数据的。这有点难简单表达清楚,我们只要记住,这多个 TaskStackView 不会影响我们的拦截就好了。
好,有了前面的基本认识,就可以来着手改动代码了。
我们需要拦截的事件主要有两种:
1、点击记录卡片右上角的关闭按钮事件;
2、滑动记录卡片事件;
其中滑动事件又可分为“左右滑动事件”与“上下滑动事件”。显然我们只能拦截“左右滑动”事件。
首先,我们要如何来区分哪个卡片是哪个应用呢?这些卡片默认是不记录与它相关的应用进程信息的。
./SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
在 boolean synchronizeStackViewsWithModel() 方法中。
这个方法比较长,但判断卡片与进程信息的关系的代码可不能随便加,笔者将完整代码贴出来:
1 boolean synchronizeStackViewsWithModel() { 2 if (mStackViewsDirty) { 3 RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); 4 SystemServicesProxy ssp = loader.getSystemServicesProxy(); 5 6 // Get all the task transforms 7 ArrayList<Task> tasks = mStack.getTasks(); 8 float stackScroll = mStackScroller.getStackScroll(); 9 int[] visibleRange = mTmpVisibleRange; 10 boolean isValidVisibleRange = updateStackTransforms(mCurrentTaskTransforms, tasks, 11 stackScroll, visibleRange, false); 12 if (mDebugOverlay != null) { 13 mDebugOverlay.setText("vis[" + visibleRange[1] + "-" + visibleRange[0] + "]"); 14 } 15 16 // Return all the invisible children to the pool 17 mTmpTaskViewMap.clear(); 18 int childCount = getChildCount(); 19 for (int i = childCount - 1; i >= 0; i--) { 20 TaskView tv = (TaskView) getChildAt(i); 21 Task task = tv.getTask(); 22 int taskIndex = mStack.indexOfTask(task); 23 if (visibleRange[1] <= taskIndex && taskIndex <= visibleRange[0]) { 24 mTmpTaskViewMap.put(task, tv); 25 } else { 26 mViewPool.returnViewToPool(tv); 27 } 28 } 29 30 // Pick up all the newly visible children and update all the existing children 31 for (int i = visibleRange[0]; isValidVisibleRange && i >= visibleRange[1]; i--) { 32 Task task = tasks.get(i); 33 TaskViewTransform transform = mCurrentTaskTransforms.get(i); 34 TaskView tv = mTmpTaskViewMap.get(task); 35 int taskIndex = mStack.indexOfTask(task); 36 37 if (tv == null) { 38 tv = mViewPool.pickUpViewFromPool(task, task); 39 40 if (mStackViewsAnimationDuration > 0) { 41 // For items in the list, put them in start animating them from the 42 // approriate ends of the list where they are expected to appear 43 if (Float.compare(transform.p, 0f) <= 0) { 44 mLayoutAlgorithm.getStackTransform(0f, 0f, mTmpTransform, null); 45 } else { 46 mLayoutAlgorithm.getStackTransform(1f, 0f, mTmpTransform, null); 47 } 48 tv.updateViewPropertiesToTaskTransform(mTmpTransform, 0); 49 } 50 } 51 52 try { 53 tv.setTag(null); 54 String pkg = task.key.baseIntent.getComponent().getPackageName(); 55 for(String pkg2 : RecentsActivity.whiteList) { 56 if(pkg2.equals(pkg)) { 57 tv.setTag(false); 58 break; 59 } 60 } 61 }catch(Exception e) { 62 e.printStackTrace(); 63 } 64 65 // Animate the task into place 66 tv.updateViewPropertiesToTaskTransform(mCurrentTaskTransforms.get(taskIndex), 67 mStackViewsAnimationDuration, mRequestUpdateClippingListener); 68 69 // Request accessibility focus on the next view if we removed the task 70 // that previously held accessibility focus 71 childCount = getChildCount(); 72 if (childCount > 0 && ssp.isTouchExplorationEnabled()) { 73 TaskView atv = (TaskView) getChildAt(childCount - 1); 74 int indexOfTask = mStack.indexOfTask(atv.getTask()); 75 if (mPrevAccessibilityFocusedIndex != indexOfTask) { 76 tv.requestAccessibilityFocus(); 77 mPrevAccessibilityFocusedIndex = indexOfTask; 78 } 79 } 80 } 81 82 // Reset the request-synchronize params 83 mStackViewsAnimationDuration = 0; 84 mStackViewsDirty = false; 85 mStackViewsClipDirty = true; 86 return true; 87 } 88 return false; 89 }
上述代码第 52 行 ~ 第 63 行就是笔者用来判断关系的代码。需要强调的是:只能在那代码代码或它之后去判断。因为在这之前,卡片视图有可能还未实例化。
至于要如何记录卡片代表的进程信息,你们自由发挥即可。笔者这里是直接利用卡片View的 setTag() 方法。笔者直接将不能被杀掉的应用的卡片上保存一个 boolean 实例,可以杀的应用则是放空。这样待到后续通过点击卡片右上角的关闭按钮或者左右滑动时只需要简单地判断卡片视图的 getTag() 是否为空就行了。
点击右上角的关闭按钮的事件在哪呢?
./SystemUI/src/com/android/systemui/recents/views/TaskView.java
就在这个代码的 onClick() 方法中:
在上图红框处的代码就是笔者的拦截代码,只要不让它执行 dismissTask() 就不会杀掉这个进程。
滑动事件的流程要稍微复杂一点,但这里只讲核心部分:
./SystemUI/src/com/android/systemui/recents/views/SwiperHelper.java
在这个代码的 onTouchEvent() 方法中:
上图红框处是笔者的拦截处理代码。只要不让它执行红框代码后面的语句,应用记录卡片就不会左右方向滑动。