上周六加班在解决一个关于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) 这两个按键,当我们按下这两个按键
系统会调一个函数做一个判断处理
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();
}
}
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());
}
}
}
private final Runnable mScreenshotRunnable = new Runnable() {
@Override
public void run() {
takeScreenshot();
}
};
takeScreenshot()函数
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);
}
}
}
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();
}
/**
* 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);
}
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();
}
}
是由截屏本身引起的,有可能是截屏的需要某个组件导致他跟DVR冲突,DVR在open camera的时候 Faild ,因为BUG只是在每
一次的重启截屏才会出现,我又不想深入去追到底是那个组件被多次调用,因为这个操作不需要同步,而是用户手动去操作,而
DVR必须在开机的时候由SystemUI开启,所以加 synchronized 也无济于事,所以只能走兼容的处理了,于是我在截屏操作的时
候做了一个简单的结果通知,即inient receiver 我在 SystemUI得到结果并作标记处理
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);
}
在SystemUI注册BroadcastReceiver收到onReceiver 保存的标志位即使存全局 application也会改变,因为截屏的这个操作导致
某个组件跟SystemUI的 DVR 冲突,所以SystemUI会被重启很多次,最终application下的这个用来标记截屏的flags也会被重
置,所以static也是无济于事的,这时候我想到了利用file 节点和sqllite和SharedPreferences 方案,最终选定轻量级的
SharedPreferences,将BroadcaseReceiver 注册在了
frameworks\base\packages\SystemUI\src\com\cars\recorder\media\RecorderStateManager.java
// 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);
......
class ScreenShotReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context arg0, Intent arg1) {
ContextUtil.getInstance().setScreenShotFlags(true);
}
}
public class ContextUtil extends Application {
......
public boolean getScreenShotFlags(){
return ScreenShotUtil.getScreenShotInstance(this).getScreenShotFlags();
}
public void setScreenShotFlags(boolean flags){
ScreenShotUtil.getScreenShotInstance(this).setScreenShotFlags(flags);
}
......
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, ""));
}
}
}
是截屏冲突导致SystemUI最后一次重新启动flags在截屏前重置,因为笔者做的是MTK的方案,6735的平台,加载4G网络没那么
快,如果用户在网络不正常的情况下截屏的话,我会存下这个时间戳,然后等待冲突导致SystemUI重启再次获取时间戳,存在轻
量级下,但是这个可能是ANT在网络正常的情况下获取的,所以这两个时间戳根本无法比较,因为ANT在没有网络的情况下默认
是节点文件下的默认时间,是不标准的,一旦加载了网络,时间就会从google获取北京时间,所以这个想法不成立,还有一个就
是刚说的第二个方案,即在收到截屏广播通知我就存下标记,执行完我就设为 false,不成功默认false,如果在截屏的中途断点
或ACC断开,执行了按键处逻辑,没有执行截屏,这时候flags视为true,我只需要在每次SystemUI重启的时候设为原始默认的
false即可,即使在SystemUI重启的那几次继续截屏也会适用,所以选择第二个方案是非常可行的!
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;
}
}
......