欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android8.1 MTK平台 截屏功能分析

程序员文章站 2022-05-04 12:36:08
前言 涉及到的源码有 frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java vendor\mediatek\proprietary\packages\apps\SystemUI\src ......

前言

涉及到的源码有

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;

常用按键对应值

Android8.1 MTK平台 截屏功能分析

这样文章开头提到的三指截屏操作,我们就可以加在 phonewindowmanager 中,当手势监听获取到三指时,只需调用截屏的两行代码即可

总结

  • 在 phonewindowmanager 的 dispatchunhandledkey 方法中处理app无法处理的按键事件,当然也包括音量减少键和电源按键的组合按键

  • 通过一系列的调用启动 takescreenshotservice 服务,并通过其执行截屏的操作。

  • 具体的截屏代码是在 native 层实现的。

  • 截屏操作时候,若截屏失败则直接发送截屏失败的 notification 通知。

  • 截屏之后,若截屏成功,则先执行截屏的动画,并在动画效果执行完毕之后,发送截屏成功的 notification 的通知。

参考文章

android 截屏方法总结
android keycode列表