Android8.1 MTK平台 截屏功能分析
前言
涉及到的源码有
frameworks\base\services\core\java\com\android\server\policy\phonewindowmanager.java
vendor\mediatek\proprietary\packages\apps\systemui\src\com\android\systemui\screenshot\takescreenshotservice.java
vendor\mediatek\proprietary\packages\apps\systemui\src\com\android\systemui\screenshot\globalscreenshot.java
按键处理都是在 phonewindowmanager 中,真正截屏的功能实现在 globalscreenshot 中, phonewindowmanager 和 systemui 通过 bind takescreenshotservice 来实现截屏功能
流程
一般未经过特殊定制的 android 系统,截屏都是通过同时按住音量下键和电源键来截屏,后来我们使用的一些华为、oppo等厂商的系统你会发现可以通过三指滑动来截屏,下一篇我们会定制此功能,而且截屏显示风格类似 iphone 在左下角显示截屏缩略图,点击可跳转放大查看,3s 无操作后向左自动滑动消失。
好了,现在我们先来理一下系统截屏的流程
system_process d/windowmanager: interceptkeyti keycode=25 down=true repeatcount=0 keyguardon=false mhomepressed=false canceled=false metastate:0 system_process d/windowmanager: interceptkeytq keycode=25 interactive=true keyguardactive=false policyflags=22000000 down =false canceled = false iswakekey=false mvolumedownkeytriggered =true result = 1 usehapticfeedback = false isinjected = false system_process d/windowmanager: interceptkeyti keycode=25 down=false repeatcount=0 keyguardon=false mhomepressed=false canceled=false metastate:0 system_process d/windowmanager: interceptkeytq keycode=26 interactive=true keyguardactive=false policyflags=22000000 down =false canceled = false iswakekey=false mvolumedownkeytriggered =false result = 1 usehapticfeedback = false isinjected = false
上面是按下音量下键和电源键的日志,音量下键对应 keycode=25 ,电源键对应 keycode=26,来看到 phonewindowmanager 中的 interceptkeybeforequeueing() 方法,在此处处理按键操作
/** {@inheritdoc} */ @override public int interceptkeybeforequeueing(keyevent event, int policyflags) { if (!msystembooted) { // if we have not yet booted, don't let key events do anything. return 0; } ..... if (debug_input) { log.d(tag, "interceptkeytq keycode=" + keycode + " interactive=" + interactive + " keyguardactive=" + keyguardactive + " policyflags=" + integer.tohexstring(policyflags)); } ..... // handle special keys. 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(); interceptaccessibilityshortcutchord(); } } else { mscreenshotchordvolumedownkeytriggered = false; cancelpendingscreenshotchordaction(); cancelpendingaccessibilityshortcutaction(); } } .... }
看到 keycode_volume_down 中,记录当前按下音量下键的时间 mscreenshotchordvolumedownkeytime,cancelpendingpowerkeyaction() 移除电源键长按消息 msg_power_long_press,来看下核心方法 interceptscreenshotchord()
// time to volume and power must be pressed within this interval of each other. private static final long screenshot_chord_debounce_delay_millis = 150; private void interceptscreenshotchord() { if (mscreenshotchordenabled && mscreenshotchordvolumedownkeytriggered && mscreenshotchordpowerkeytriggered && !ma11yshortcutchordvolumeupkeytriggered) { final long now = systemclock.uptimemillis(); if (now <= mscreenshotchordvolumedownkeytime + screenshot_chord_debounce_delay_millis && now <= mscreenshotchordpowerkeytime + screenshot_chord_debounce_delay_millis) { mscreenshotchordvolumedownkeyconsumed = true; cancelpendingpowerkeyaction(); mscreenshotrunnable.setscreenshottype(take_screenshot_fullscreen); mhandler.postdelayed(mscreenshotrunnable, getscreenshotchordlongpressdelay()); } } }
只有当电源键按下时 mscreenshotchordpowerkeytriggered 才为 true, 当两个按键的按下时间都大于 150 时,延时执行截屏任务 mscreenshotrunnable
private long getscreenshotchordlongpressdelay() { if (mkeyguarddelegate.isshowing()) { // double the time it takes to take a screenshot from the keyguard return (long) (keyguard_screenshot_chord_delay_multiplier * viewconfiguration.get(mcontext).getdeviceglobalactionkeytimeout()); } return viewconfiguration.get(mcontext).getdeviceglobalactionkeytimeout(); }
若当前输入框是打开状态,则延时时间为输入框关闭时间加上系统配置的按键超时时间,若当前输入框没有打开则直接是系统配置的按键超时处理时间
紧接着看下 mscreenshotrunnable 都做了什么操作
private class screenshotrunnable implements runnable { private int mscreenshottype = take_screenshot_fullscreen; public void setscreenshottype(int screenshottype) { mscreenshottype = screenshottype; } @override public void run() { takescreenshot(mscreenshottype); } } private final screenshotrunnable mscreenshotrunnable = new screenshotrunnable();
可以看到在线程中调用了 takescreenshot(),默认不设置截屏类型就是全屏,截屏类型有 take_screenshot_selected_region 选定的区域 和 take_screenshot_fullscreen 全屏两种类型
// assume this is called from the handler thread. private void takescreenshot(final int screenshottype) { synchronized (mscreenshotlock) { if (mscreenshotconnection != null) { return; } final componentname servicecomponent = new componentname(sysui_package, sysui_screenshot_service); final intent serviceintent = new intent(); serviceintent.setcomponent(servicecomponent); 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, screenshottype); 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) { synchronized (mscreenshotlock) { if (mscreenshotconnection != null) { mcontext.unbindservice(mscreenshotconnection); mscreenshotconnection = null; mhandler.removecallbacks(mscreenshottimeout); notifyscreenshoterror(); } } } }; if (mcontext.bindserviceasuser(serviceintent, conn, context.bind_auto_create | context.bind_foreground_service_while_awake, userhandle.current)) { mscreenshotconnection = conn; mhandler.postdelayed(mscreenshottimeout, 10000); } } }
takescreenshot 中通过 bind systemui中的 takescreenshotservice 建立连接,连接成功后通过 messenger 在两个进程中传递消息通行,有点类似 aidl,关于 messenger 的介绍可参考 android进程间通讯之 messenger messenger 主要传递当前的 mstatusbar 和 mnavigationbar 是否可见,再来看 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) { final messenger callback = msg.replyto; runnable finisher = new runnable() { @override public void run() { message reply = message.obtain(null, 1); try { callback.send(reply); } catch (remoteexception e) { } } }; // if the storage for this user is locked, we have no place to store // the screenshot, so skip taking it instead of showing a misleading // animation and error notification. if (!getsystemservice(usermanager.class).isuserunlocked()) { log.w(tag, "skipping screenshot because storage is locked!"); post(finisher); return; } if (mscreenshot == null) { mscreenshot = new globalscreenshot(takescreenshotservice.this); } switch (msg.what) { case windowmanager.take_screenshot_fullscreen: mscreenshot.takescreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0); break; case windowmanager.take_screenshot_selected_region: mscreenshot.takescreenshotpartial(finisher, msg.arg1 > 0, msg.arg2 > 0); break; } } }; @override public ibinder onbind(intent intent) { return new messenger(mhandler).getbinder(); } @override public boolean onunbind(intent intent) { if (mscreenshot != null) mscreenshot.stopscreenshot(); return true; } }
可以看到通过 mhandler 接收传递的消息,获取截屏类型和是否要包含状态栏、导航栏,通过创建 globalscreenshot 对象(真正干活的来了),调用 takescreenshot 执行截屏操作,继续跟进
void takescreenshot(runnable finisher, boolean statusbarvisible, boolean navbarvisible) { mdisplay.getrealmetrics(mdisplaymetrics); takescreenshot(finisher, statusbarvisible, navbarvisible, 0, 0, mdisplaymetrics.widthpixels, mdisplaymetrics.heightpixels); } /** * takes a screenshot of the current display and shows an animation. */ void takescreenshot(runnable finisher, boolean statusbarvisible, boolean navbarvisible, int x, int y, int width, int height) { // 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}; float degrees = getdegreesforrotation(mdisplay.getrotation()); 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]); } // take the screenshot mscreenbitmap = surfacecontrol.screenshot((int) dims[0], (int) dims[1]); if (mscreenbitmap == null) { notifyscreenshoterror(mcontext, mnotificationmanager, r.string.screenshot_failed_to_capture_text); finisher.run(); return; } if (requiresrotation) { // rotate the screenshot to the current orientation bitmap ss = bitmap.createbitmap(mdisplaymetrics.widthpixels, mdisplaymetrics.heightpixels, bitmap.config.argb_8888, mscreenbitmap.hasalpha(), mscreenbitmap.getcolorspace()); 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; } if (width != mdisplaymetrics.widthpixels || height != mdisplaymetrics.heightpixels) { // crop the screenshot to selected region bitmap cropped = bitmap.createbitmap(mscreenbitmap, x, y, width, height); mscreenbitmap.recycle(); mscreenbitmap = cropped; } // optimizations mscreenbitmap.sethasalpha(false); mscreenbitmap.preparetodraw(); // start the post-screenshot animation startanimation(finisher, mdisplaymetrics.widthpixels, mdisplaymetrics.heightpixels, statusbarvisible, navbarvisible); }
获取屏幕的宽高和当前屏幕方向以确定是否需要旋转图片,然后通过 surfacecontrol.screenshot 截屏,好吧,再继续往下看到
public static bitmap screenshot(int width, int height) { // todo: should take the display as a parameter ibinder displaytoken = surfacecontrol.getbuiltindisplay( surfacecontrol.built_in_display_id_main); return nativescreenshot(displaytoken, new rect(), width, height, 0, 0, true, false, surface.rotation_0); }
这里调用的是 nativescreenshot 方法,它是一个 native 方法,具体的实现在jni层,这里就不做过多的介绍了。继续回到我们的 takescreenshot 方法,在调用了截屏方法 screentshot 之后,判断是否截屏成功:
截屏失败则调用 notifyscreenshoterror 发送通知。截屏成功,则调用 startanimation 播放动画,来分析下动画,后面我们会改这个动画的效果
/** * starts the animation after taking the screenshot */ private void startanimation(final runnable finisher, int w, int h, boolean statusbarvisible, boolean navbarvisible) { // if power save is on, show a toast so there is some visual indication that a screenshot // has been taken. powermanager powermanager = (powermanager) mcontext.getsystemservice(context.power_service); if (powermanager.ispowersavemode()) { toast.maketext(mcontext, r.string.screenshot_saved_title, toast.length_short).show(); } // add the view for the animation mscreenshotview.setimagebitmap(mscreenbitmap); mscreenshotlayout.requestfocus(); // setup the animation with the screenshot just taken if (mscreenshotanimation != null) { if (mscreenshotanimation.isstarted()) { mscreenshotanimation.end(); } mscreenshotanimation.removealllisteners(); } mwindowmanager.addview(mscreenshotlayout, mwindowlayoutparams); valueanimator screenshotdropinanim = createscreenshotdropinanimation(); valueanimator screenshotfadeoutanim = createscreenshotdropoutanimation(w, h, statusbarvisible, navbarvisible); mscreenshotanimation = new animatorset(); mscreenshotanimation.playsequentially(screenshotdropinanim, screenshotfadeoutanim); mscreenshotanimation.addlistener(new animatorlisteneradapter() { @override public void onanimationend(animator animation) { // save the screenshot once we have a bit of time now savescreenshotinworkerthread(finisher); mwindowmanager.removeview(mscreenshotlayout); // clear any references to the bitmap mscreenbitmap = null; mscreenshotview.setimagebitmap(null); } }); mscreenshotlayout.post(new runnable() { @override public void run() { // play the shutter sound to notify that we've taken a screenshot mcamerasound.play(mediaactionsound.shutter_click); mscreenshotview.setlayertype(view.layer_type_hardware, null); mscreenshotview.buildlayer(); mscreenshotanimation.start(); } }); }
先判断是否是低电量模式,若是发出已抓取屏幕截图的 toast,然后通过 windowmanager 在屏幕中间添加一个装有截屏缩略图的 view,同时创建两个动画组合,通过 mcamerasound 播放截屏咔嚓声并执行动画,动画结束后移除刚刚添加的 view,同时调用 savescreenshotinworkerthread 保存图片到媒体库,我们直接来看 saveimageinbackgroundtask
class saveimageinbackgroundtask extends asynctask<void, void, void> { ..... saveimageinbackgroundtask(context context, saveimageinbackgrounddata data, notificationmanager nmanager) { ...... mnotificationbuilder = new notification.builder(context, notificationchannels.screenshots) .setticker(r.getstring(r.string.screenshot_saving_ticker) + (mtickeraddspace ? " " : "")) .setcontenttitle(r.getstring(r.string.screenshot_saving_title)) .setcontenttext(r.getstring(r.string.screenshot_saving_text)) .setsmallicon(r.drawable.stat_notify_image) .setwhen(now) .setshowwhen(true) .setcolor(r.getcolor(com.android.internal.r.color.system_notification_accent_color)) .setstyle(mnotificationstyle) .setpublicversion(mpublicnotificationbuilder.build()); mnotificationbuilder.setflag(notification.flag_no_clear, true); systemui.overridenotificationappname(context, mnotificationbuilder); mnotificationmanager.notify(systemmessage.note_global_screenshot, mnotificationbuilder.build()); } @override protected void doinbackground(void... params) { if (iscancelled()) { return null; } // by default, asynctask sets the worker thread to have background thread priority, so bump // it back up so that we save a little quicker. process.setthreadpriority(process.thread_priority_foreground); context context = mparams.context; bitmap image = mparams.image; resources r = context.getresources(); try { // create screenshot directory if it doesn't exist mscreenshotdir.mkdirs(); // media provider uses seconds for date_modified and date_added, but milliseconds // for date_taken long dateseconds = mimagetime / 1000; // save outputstream out = new fileoutputstream(mimagefilepath); image.compress(bitmap.compressformat.png, 100, out); out.flush(); out.close(); // save the screenshot to the mediastore contentvalues values = new contentvalues(); contentresolver resolver = context.getcontentresolver(); values.put(mediastore.images.imagecolumns.data, mimagefilepath); values.put(mediastore.images.imagecolumns.title, mimagefilename); values.put(mediastore.images.imagecolumns.display_name, mimagefilename); values.put(mediastore.images.imagecolumns.date_taken, mimagetime); values.put(mediastore.images.imagecolumns.date_added, dateseconds); values.put(mediastore.images.imagecolumns.date_modified, dateseconds); values.put(mediastore.images.imagecolumns.mime_type, "image/png"); values.put(mediastore.images.imagecolumns.width, mimagewidth); values.put(mediastore.images.imagecolumns.height, mimageheight); values.put(mediastore.images.imagecolumns.size, new file(mimagefilepath).length()); uri uri = resolver.insert(mediastore.images.media.external_content_uri, values); // create a share intent string subjectdate = dateformat.getdatetimeinstance().format(new date(mimagetime)); string subject = string.format(screenshot_share_subject_template, subjectdate); intent sharingintent = new intent(intent.action_send); sharingintent.settype("image/png"); sharingintent.putextra(intent.extra_stream, uri); sharingintent.putextra(intent.extra_subject, subject); // create a share action for the notification. note, we proxy the call to sharereceiver // because remoteviews currently forces an activity options on the pendingintent being // launched, and since we don't want to trigger the share sheet in this case, we will // start the chooser activitiy directly in sharereceiver. pendingintent shareaction = pendingintent.getbroadcast(context, 0, new intent(context, globalscreenshot.sharereceiver.class) .putextra(sharing_intent, sharingintent), pendingintent.flag_cancel_current); notification.action.builder shareactionbuilder = new notification.action.builder( r.drawable.ic_screenshot_share, r.getstring(com.android.internal.r.string.share), shareaction); mnotificationbuilder.addaction(shareactionbuilder.build()); // create a delete action for the notification pendingintent deleteaction = pendingintent.getbroadcast(context, 0, new intent(context, globalscreenshot.deletescreenshotreceiver.class) .putextra(globalscreenshot.screenshot_uri_id, uri.tostring()), pendingintent.flag_cancel_current | pendingintent.flag_one_shot); notification.action.builder deleteactionbuilder = new notification.action.builder( r.drawable.ic_screenshot_delete, r.getstring(com.android.internal.r.string.delete), deleteaction); mnotificationbuilder.addaction(deleteactionbuilder.build()); mparams.imageuri = uri; mparams.image = null; mparams.errormsgresid = 0; } catch (exception e) { // ioexception/unsupportedoperationexception may be thrown if external storage is not // mounted slog.e(tag, "unable to save screenshot", e); mparams.clearimage(); mparams.errormsgresid = r.string.screenshot_failed_to_save_text; } // recycle the bitmap data if (image != null) { image.recycle(); } return null; } @override protected void onpostexecute(void params) { if (mparams.errormsgresid != 0) { // show a message that we've failed to save the image to disk globalscreenshot.notifyscreenshoterror(mparams.context, mnotificationmanager, mparams.errormsgresid); } else { // show the final notification to indicate screenshot saved context context = mparams.context; resources r = context.getresources(); // create the intent to show the screenshot in gallery intent launchintent = new intent(intent.action_view); launchintent.setdataandtype(mparams.imageuri, "image/png"); launchintent.setflags( intent.flag_activity_new_task | intent.flag_grant_read_uri_permission); final long now = system.currenttimemillis(); // update the text and the icon for the existing notification mpublicnotificationbuilder .setcontenttitle(r.getstring(r.string.screenshot_saved_title)) .setcontenttext(r.getstring(r.string.screenshot_saved_text)) .setcontentintent(pendingintent.getactivity(mparams.context, 0, launchintent, 0)) .setwhen(now) .setautocancel(true) .setcolor(context.getcolor( com.android.internal.r.color.system_notification_accent_color)); mnotificationbuilder .setcontenttitle(r.getstring(r.string.screenshot_saved_title)) .setcontenttext(r.getstring(r.string.screenshot_saved_text)) .setcontentintent(pendingintent.getactivity(mparams.context, 0, launchintent, 0)) .setwhen(now) .setautocancel(true) .setcolor(context.getcolor( com.android.internal.r.color.system_notification_accent_color)) .setpublicversion(mpublicnotificationbuilder.build()) .setflag(notification.flag_no_clear, false); mnotificationmanager.notify(systemmessage.note_global_screenshot, mnotificationbuilder.build()); } mparams.finisher.run(); mparams.clearcontext(); } @override protected void oncancelled(void params) { // if we are cancelled while the task is running in the background, we may get null params. // the finisher is expected to always be called back, so just use the baked-in params from // the ctor in any case. mparams.finisher.run(); mparams.clearimage(); mparams.clearcontext(); // cancel the posted notification mnotificationmanager.cancel(systemmessage.note_global_screenshot); } }
简单说下, saveimageinbackgroundtask 构造方法中做了大量的准备工作,截屏图片的时间命名格式、截屏通知对象创建,在 doinbackground 中将截屏图片通过 contentresolver 存储至 mediastore,再创建两个 pendingintent,用于分享和删除截屏图片,在 onpostexecute 中发送刚刚创建的 notification 至 statubar 显示,到此截屏的流程就结束了。
其它
我们再回到 phonewindowmanager 中看下,通过上面我们知道要想截屏只需通过如下两行代码即可
mscreenshotrunnable.setscreenshottype(take_screenshot_fullscreen); mhandler.post(mscreenshotrunnable);
通过搜索上面的关键代码,我们发现还有另外两处也调用了截屏的代码,一起来看下
@override public long interceptkeybeforedispatching(windowstate win, keyevent event, int policyflags) { final boolean keyguardon = keyguardon(); final int keycode = event.getkeycode(); ..... else if (keycode == keyevent.keycode_s && event.ismetapressed() && event.isctrlpressed()) { if (down && repeatcount == 0) { int type = event.isshiftpressed() ? take_screenshot_selected_region : take_screenshot_fullscreen; mscreenshotrunnable.setscreenshottype(type); mhandler.post(mscreenshotrunnable); return -1; } } .... else if (keycode == keyevent.keycode_sysrq) { if (down && repeatcount == 0) { mscreenshotrunnable.setscreenshottype(take_screenshot_fullscreen); mhandler.post(mscreenshotrunnable); } return -1; } ...... }
也是在拦截按键消息分发之前的方法中,查看 keyevent 源码,第一种情况大概网上搜索了下,应该是接外设时,同时按下 s 键 + meta键 + ctrl键即可截屏,关于 meta 介绍可参考meta键始末 第二种情况是按下截屏键时,对应 keycode 为 120,可以用 adb shell input keyevent 120 模拟发现也能截屏
/** key code constant: 's' key. */ public static final int keycode_s = 47; /** key code constant: system request / print screen key. */ public static final int keycode_sysrq = 120;
常用按键对应值
这样文章开头提到的三指截屏操作,我们就可以加在 phonewindowmanager 中,当手势监听获取到三指时,只需调用截屏的两行代码即可
总结
在 phonewindowmanager 的 dispatchunhandledkey 方法中处理app无法处理的按键事件,当然也包括音量减少键和电源按键的组合按键
通过一系列的调用启动 takescreenshotservice 服务,并通过其执行截屏的操作。
具体的截屏代码是在 native 层实现的。
截屏操作时候,若截屏失败则直接发送截屏失败的 notification 通知。
截屏之后,若截屏成功,则先执行截屏的动画,并在动画效果执行完毕之后,发送截屏成功的 notification 的通知。
参考文章
下一篇: day27-python之迭代器协议