Android 系统截屏与系统内置DVR录像冲突,导致SystemUI重启的问题解决与分享

上周六加班在解决一个关于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();
                    }
                }

这个函数就是interceptScreenshotChord()

    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());
            }
        }
    }

这个函数或启动一个Runable ,执行takeScreenshot()函数

    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);
            }
        }
    }

该函数bind TakeScreenshotService 

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();
    }


GlobalScreenshot  takeScreenshot 就是开始截屏framework执行截屏的那个动画,正在意义的截屏最后丢给了 native 去执行了


    /**
     * 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);
    }

于是我在debug的时候将截屏的操作屏蔽掉了,测试看这个系统的BUG是否受到截屏的影响
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();
    }
}

屏蔽之后,我在make 一把debug,本地跟踪打印,发现这个截屏操作虽然没有做截屏操作了,但是BUG依旧还在,排除了BUG

是由截屏本身引起的,有可能是截屏的需要某个组件导致他跟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接收,但是这个接收必须存一个比 static 更持久,但是在BUG逻辑块程序执行完后我需要update这个flags,因为我

在SystemUI注册BroadcastReceiver收到onReceiver 保存的标志位即使存全局 application也会改变,因为截屏的这个操作导致

某个组件跟SystemUI的 DVR 冲突,所以SystemUI会被重启很多次,最终application下的这个用来标记截屏的flags也会被重

置,所以static也是无济于事的,这时候我想到了利用file 节点sqlliteSharedPreferences 方案,最终选定轻量级的

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);
		......

onReceiveContextUtil 单例调用自定义类 ScreenShotUtil 单例,执行flags标记 ,ContextUtil extends Application

	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);
	}

......	

ScreenShotUtil 自定义类

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最后一次重新启动的误差,还有一个

是截屏冲突导致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;
		}
	}
	......

修改完之后make烧录新的固件,测试了N次,OK!没任何问题!


你可能感兴趣的:(Android系统编程)