红剑鱼,金山区haobc,亡羊补牢 史光辉
涉及到的源码有
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 的通知。
参考文章
android 截屏方法总结
android keycode列表
如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复
Android apk 项目一键打包并上传到蒲公英的实现方法
Android 自定义LineLayout实现满屏任意拖动功能的示例代码
android 限制某个操作每天只能操作指定的次数(示例代码详解)
Android 集成 google 登录并获取性别等隐私信息的实现代码
网友评论