上周六加班在解决一个关于SystemUI内嵌的DVR录像与系统截屏操作冲突的问题,介于问题的复杂性,所以我把这个分享出来便
于以后自己更加的理解,又方便以后遇到此问题的同行能够提供一些帮助,若有疑问可向鄙人的博客提供你的宝贵意见!
首先我们需要找到系统截屏的按键定义,并且知道它在哪里执行的,先摈弃从硬件底层的协议,我们直接从framework层开始
讲,因为底层底层硬件返回的结果由.c.o.h这些文件,再由Binder aidl
将结果给到framework,所以我们就从开始从framework
开始,如果有兴趣的可以下载源码查看整个流程的实现过程。
首先我们查看
frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
在这个类下面,有一个方法 interceptKeyBeforeQueueing
这个方法来自interface WindowManagerPolicy,而interface
WindowManagerPolicy 的回调结果经过几多辗转最终由底层给到framework层interface WindowManagerPolicy,让
PhoneWindowManager来处理,因为底层返回的那流程涉及的文件和协议比较复杂,即使说了,不懂的也很难一下子掌握和理
解,所以笔者从framework开始作介绍,因为最后截屏的操作也会通过 native 由更底层的C来实现
下面继续看到 interceptKeyBeforeQueueing 这个函数,在这个函数下有一个switch (keyCode) 里面有一个按键监听,其实在
这个PhoneWindowManager下面截屏的方法被调用了2次,相信到这里大家都应该明白了吧?因为安卓系统原生的截屏操作是一
个按键组合,即 KeyEvent.KEYCODE_POWER | KeyEvent.KEYCODE_VOLUME_DOWN) 这两个按键,当我们按下这两个按键
系统会调一个函数做一个判断处理
<span style="font-size:14px;color:#3333ff;"><strong> switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_MUTE: { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { if (down) { if (interactive && !mScreenshotChordVolumeDownKeyTriggered && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { mScreenshotChordVolumeDownKeyTriggered = true; mScreenshotChordVolumeDownKeyTime = event.getDownTime(); mScreenshotChordVolumeDownKeyConsumed = false; cancelPendingPowerKeyAction(); interceptScreenshotChord(); } } else { mScreenshotChordVolumeDownKeyTriggered = false; cancelPendingScreenshotChordAction(); } }</strong></span>
这个函数就是interceptScreenshotChord()
<span style="font-size:14px;color:#3333ff;"><strong> private void interceptScreenshotChord() { if (mScreenshotChordEnabled && mScreenshotChordVolumeDownKeyTriggered && mScreenshotChordPowerKeyTriggered && !mScreenshotChordVolumeUpKeyTriggered) { final long now = SystemClock.uptimeMillis(); // 按键组合按下的误差小于 150 毫秒视为截图操作 if (now <= mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS && now <= mScreenshotChordPowerKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) { mScreenshotChordVolumeDownKeyConsumed = true; cancelPendingPowerKeyAction(); mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay()); } } }</strong></span>
这个函数或启动一个Runable ,执行takeScreenshot()函数
<span style="font-size:14px;color:#3333ff;"><strong> private final Runnable mScreenshotRunnable = new Runnable() { @Override public void run() { takeScreenshot(); } };</strong></span>
takeScreenshot()函数
<span style="font-size:14px;color:#3333ff;"><strong> private void takeScreenshot() { synchronized (mScreenshotLock) { if (mScreenshotConnection != null) { return; } ComponentName cn = new ComponentName("com.android.systemui", "com.android.systemui.screenshot.TakeScreenshotService"); Intent intent = new Intent(); intent.setComponent(cn); ServiceConnection conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized (mScreenshotLock) { if (mScreenshotConnection != this) { return; } Messenger messenger = new Messenger(service); Message msg = Message.obtain(null, 1); final ServiceConnection myConn = this; Handler h = new Handler(mHandler.getLooper()) { @Override public void handleMessage(Message msg) { synchronized (mScreenshotLock) { if (mScreenshotConnection == myConn) { mContext.unbindService(mScreenshotConnection); mScreenshotConnection = null; mHandler.removeCallbacks(mScreenshotTimeout); } } } }; msg.replyTo = new Messenger(h); msg.arg1 = msg.arg2 = 0; if (mStatusBar != null && mStatusBar.isVisibleLw()) msg.arg1 = 1; if (mNavigationBar != null && mNavigationBar.isVisibleLw()) msg.arg2 = 1; try { messenger.send(msg); } catch (RemoteException e) { } } } @Override public void onServiceDisconnected(ComponentName name) {} }; if (mContext.bindServiceAsUser( intent, conn, Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) { mScreenshotConnection = conn; mHandler.postDelayed(mScreenshotTimeout, 10000); } } }</strong></span>
该函数bind TakeScreenshotService
<span style="font-size:14px;color:#3333ff;"><strong>public class TakeScreenshotService extends Service { private static final String TAG = "TakeScreenshotService"; private static GlobalScreenshot mScreenshot; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 1: final Messenger callback = msg.replyTo; if (mScreenshot == null) { mScreenshot = new GlobalScreenshot(TakeScreenshotService.this); } mScreenshot.takeScreenshot(new Runnable() { @Override public void run() { Message reply = Message.obtain(null, 1); try { callback.send(reply); } catch (RemoteException e) { } } }, msg.arg1 > 0, msg.arg2 > 0); } } }; @Override public IBinder onBind(Intent intent) { return new Messenger(mHandler).getBinder(); }</strong></span>
GlobalScreenshot takeScreenshot 就是开始截屏framework执行截屏的那个动画,正在意义的截屏最后丢给了 native 去执行了
<span style="font-size:14px;color:#3333ff;"><strong> /** * Takes a screenshot of the current display and shows an animation. */ void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) { // We need to orient the screenshot correctly (and the Surface api seems to take screenshots // only in the natural orientation of the device :!) mDisplay.getRealMetrics(mDisplayMetrics); float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels}; /// M: [SystemUI] Support Smartbook Feature. @{ boolean isPlugIn = com.mediatek.systemui.statusbar.util.SIMHelper.isSmartBookPluggedIn(mContext); if (isPlugIn) { dims[0] = mDisplayMetrics.heightPixels; dims[1] = mDisplayMetrics.widthPixels; } /// @} float degrees = getDegreesForRotation(mDisplay.getRotation()); Xlog.d("takeScreenshot", "dims = " + dims[0] + "," + dims[1] + " of " + degrees); boolean requiresRotation = (degrees > 0); if (requiresRotation) { // Get the dimensions of the device in its native orientation mDisplayMatrix.reset(); mDisplayMatrix.preRotate(-degrees); mDisplayMatrix.mapPoints(dims); dims[0] = Math.abs(dims[0]); dims[1] = Math.abs(dims[1]); Xlog.d("takeScreenshot", "reqRotate, dims = " + dims[0] + "," + dims[1]); } // Take the screenshot /// M: [SystemUI] Support Smartbook Feature. @{ if (isPlugIn) { mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1], SurfaceControl.BUILT_IN_DISPLAY_ID_HDMI); degrees = 270f - degrees; } /// @} else { mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]); } if (mScreenBitmap == null) { Xlog.d("takeScreenshot", "mScreenBitmap == null, " + dims[0] + "," + dims[1]); notifyScreenshotError(mContext, mNotificationManager); finisher.run(); return; } if (requiresRotation) { // Rotate the screenshot to the current orientation Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(ss); c.translate(ss.getWidth() / 2, ss.getHeight() / 2); c.rotate(degrees); c.translate(-dims[0] / 2, -dims[1] / 2); c.drawBitmap(mScreenBitmap, 0, 0, null); c.setBitmap(null); // Recycle the previous bitmap mScreenBitmap.recycle(); mScreenBitmap = ss; } // Optimizations mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); // Start the post-screenshot animation startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels, statusBarVisible, navBarVisible); }</strong></span>
于是我在debug的时候将截屏的操作屏蔽掉了,测试看这个系统的BUG是否受到截屏的影响
<span style="font-size:14px;color:#3333ff;"><strong>public class TakeScreenshotService extends Service { private static final String TAG = "TakeScreenshotService"; private static GlobalScreenshot mScreenshot; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { // case 1: // final Messenger callback = msg.replyTo; // if (mScreenshot == null) { // mScreenshot = new GlobalScreenshot(TakeScreenshotService.this); // } // mScreenshot.takeScreenshot(new Runnable() { // @Override public void run() { // Message reply = Message.obtain(null, 1); // try { // callback.send(reply); // } catch (RemoteException e) { // } // } // }, msg.arg1 > 0, msg.arg2 > 0); } } }; @Override public IBinder onBind(Intent intent) { return new Messenger(mHandler).getBinder(); } }</strong></span>
屏蔽之后,我在make 一把debug,本地跟踪打印,发现这个截屏操作虽然没有做截屏操作了,但是BUG依旧还在,排除了BUG
是由截屏本身引起的,有可能是截屏的需要某个组件导致他跟DVR冲突,DVR在open camera的时候
Faild ,因为BUG只是在每
一次的重启截屏才会出现,我又不想深入去追到底是那个组件被多次调用,因为这个操作不需要同步,而是用户手动去操作,而
DVR必须在开机的时候由SystemUI开启,所以加 synchronized 也无济于事,所以只能走兼容的处理了,于是我在截屏操作的时
候做了一个简单的结果通知,即
inient receiver 我在 SystemUI得到结果并作标记处理
<span style="font-size:14px;color:#3333ff;"><strong> private void interceptScreenshotChord() { if (mScreenshotChordEnabled && mScreenshotChordVolumeDownKeyTriggered && mScreenshotChordPowerKeyTriggered && !mScreenshotChordVolumeUpKeyTriggered) { final long now = SystemClock.uptimeMillis(); if (now <= mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS && now <= mScreenshotChordPowerKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) { mScreenshotChordVolumeDownKeyConsumed = true; cancelPendingPowerKeyAction(); mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay()); // engineer-jsp add method ScreenshotNotifyTion(); } } } // engineer-jsp add method private void ScreenshotNotifyTion(){ Intent intent = new Intent("rmt.screenshot.notifytion.action"); mContext.sendBroadcast(intent); }</strong></span>
在SystemUI接收,但是这个接收必须存一个比 static 更持久,但是在BUG逻辑块程序执行完后我需要update这个flags,因为我
在SystemUI注册BroadcastReceiver收到onReceiver 保存的标志位即使存全局 application
也会改变,因为截屏的这个操作导致
某个组件跟SystemUI的 DVR 冲突,所以SystemUI会被重启很多次,最终application下的这个用来标记截屏的flags也会被重
置,所以static也是无济于事的,这时候我想到了利用file 节点和mysql
和SharedPreferences 方案,最终选定轻量级的
SharedPreferences,将BroadcaseReceiver 注册在了
frameworks\base\packages\SystemUI\src\com\cars\recorder\media\RecorderStateManager.java
<span style="font-size:14px;color:#3333ff;"><strong>// engineer-jsp add method private void LoadScreenShotReceiver(){ mReceiver = new ScreenShotReceiver(); IntentFilter filter = new IntentFilter("rmt.screenshot.notifytion.action"); filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); mContext.registerReceiver(mReceiver, filter); } GotoStealStateReceiver mGotoStealStateReceiver; CrashControlReceiver mCrashControlReceiver; ScreenShotReceiver mReceiver; Context mContext; public RecorderStateManager(SurfaceHolder holder,Context context) { mContext = context; // engineer-jsp add method LoadScreenShotReceiver(); mRecorder = new RecordSurfaceThread(holder); ......</strong></span>
在 onReceive 由ContextUtil 单例调用自定义类 ScreenShotUtil 单例,执行flags标记
,ContextUtil extends Application
<span style="font-size:14px;color:#3333ff;"><strong> class ScreenShotReceiver extends BroadcastReceiver { @Override public void onReceive(Context arg0, Intent arg1) { ContextUtil.getInstance().setScreenShotFlags(true); } }</strong></span>
<span style="font-size:14px;color:#3333ff;"><strong>public class ContextUtil extends Application { ...... public boolean getScreenShotFlags(){ return ScreenShotUtil.getScreenShotInstance(this).getScreenShotFlags(); } public void setScreenShotFlags(boolean flags){ ScreenShotUtil.getScreenShotInstance(this).setScreenShotFlags(flags); } ...... </strong></span>
ScreenShotUtil 自定义类
<span style="font-size:14px;color:#3333ff;"><strong>package com.cars.recorder.media; /** * @author engineer-jsp * @date 2016.06.18 * ScreenShotUtil * */ import android.content.Context; import android.content.SharedPreferences; import android.text.TextUtils; public class ScreenShotUtil { private static ScreenShotUtil mScreenShotUtil = null; private static SharedPreferences mSharedPreferences = null; private static String SCREENSHOT_FILE = "screenshot_file"; private static String SCREENSHOT_FLAGS = "screenshot_flags"; public static ScreenShotUtil getScreenShotInstance(Context context) { if (mScreenShotUtil == null) { mScreenShotUtil = new ScreenShotUtil(); } if (mSharedPreferences == null) { mSharedPreferences = context.getSharedPreferences(SCREENSHOT_FILE, Context.MODE_PRIVATE); } return mScreenShotUtil; } public void setScreenShotFlags(boolean flags) { if (mSharedPreferences == null) { return; } mSharedPreferences.edit() .putString(SCREENSHOT_FLAGS, String.valueOf(flags)).commit(); } public boolean getScreenShotFlags() { return isScreenShotFlags(); } public boolean isScreenShotFlags() { if (mSharedPreferences == null) { return false; } if (TextUtils.isEmpty(mSharedPreferences .getString(SCREENSHOT_FLAGS, ""))) { return false; } else { return Boolean.valueOf(mSharedPreferences.getString( SCREENSHOT_FLAGS, "")); } } } </strong></span>
本来考虑到两个影响因素,但是这两个因素都被我排除了,即开始截屏跟冲突导致SystemUI最后一次重新启动的误差,还有一个
是截屏冲突导致SystemUI最后一次重新启动flags在截屏前重置,因为笔者做的是MTK的方案,6735的平台,加载4G网络没那么
快,如果用户在网络不正常的情况下截屏的话,我会存下这个时间戳,然后等待冲突导致SystemUI重启再次获取时间戳,存在轻
量级下,但是这个可能是ANT在网络正常的情况下获取的,所以这两个时间戳根本无法比较,因为ANT在没有网络的情况下默认
是节点文件下的默认时间,是不标准的,一旦加载了网络,时间就会从google获取北京时间,所以这个想法不成立,还有一个就
是刚说的第二个方案,即在收到截屏广播通知我就存下标记,执行完我就设为 false,不成功默认false,如果在截屏的中途断点
或ACC断开,执行了按键处逻辑,没有执行截屏,这时候flags视为true,我只需要在每次SystemUI重启的时候设为原始默认的
false即可,即使在SystemUI重启的那几次继续截屏也会适用,所以选择第二个方案是非常可行的!
<span style="font-size:14px;color:#3333ff;"><strong>public class NoCameraState extends RecorderState { ...... @Override public boolean canChangeTo(RecorderState state) { if(ContextUtil.getInstance().getSleepAndZdfdValues()){ return true; } else { // engineer-jsp add method // 根据标志位执行如下逻辑,执行完后还原,中途失败,会在SystemUI下次init重置,所以不冲突 // ContextUtil.getInstance().getScreenShotFlags()?截屏:没有截屏 if(!ContextUtil.getInstance().getScreenShotFlags()){ TTSHelper.ttsReport(TheRecorderPlugin.getInstacne().getPluginContext(). getString(R.string.carmerafailed), 0, TTSHelper.mExprieForever); } // 重置 ContextUtil.getInstance().setScreenShotFlags(false); return false; } } ......</strong></span>
修改完之后make烧录新的固件,测试了N次,OK!没任何问题!