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

来源:https://www.cnblogs.com/cczheng-666/archive/2019/08/16/11365869.html
-Advertisement-
Play Games

前言 涉及到的源碼有 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;

常用按鍵對應值

ebFZ5R.png

這樣文章開頭提到的三指截屏操作,我們就可以加在 PhoneWindowManager 中,當手勢監聽獲取到三指時,只需調用截屏的兩行代碼即可

總結

  • 在 PhoneWindowManager 的 dispatchUnhandledKey 方法中處理App無法處理的按鍵事件,當然也包括音量減少鍵和電源按鍵的組合按鍵

  • 通過一系列的調用啟動 TakeScreenshotService 服務,並通過其執行截屏的操作。

  • 具體的截屏代碼是在 native 層實現的。

  • 截屏操作時候,若截屏失敗則直接發送截屏失敗的 notification 通知。

  • 截屏之後,若截屏成功,則先執行截屏的動畫,併在動畫效果執行完畢之後,發送截屏成功的 notification 的通知。

參考文章

Android 截屏方法總結
Android KeyCode列表


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、相關文檔老規矩,為了避免我的解釋誤導大家,請大家務必通過官網瞭解一波SQL SERVER的相關功能。文檔地址:整體介紹文檔:https://docs.microsoft.com/en-us/sql/relational-databases/track-changes/about-change-t... ...
  • 用Google搜異常信息,肯定都訪問過 "Stack Overflow網站" 全球最大的程式員問答網站,名字來自於一個常見的報錯,就是棧溢出(stack overflow) 從函數調用開始,在電腦指令層面函數間的相互調用是怎麼實現的,以及什麼情況下會發生棧溢出 1 棧的意義 先看一個簡單的C程式 ...
  • 1、關閉防火牆 systemctl stop firewalld.service 停止firewall systemctl disable firewalld.service 禁止firewall開機啟動 2、切換用戶 3、編輯靜默安裝文件 4、修改配置文件 以下參數不要更改 [GENERAL] R ...
  • 相信大家都接觸過Mysql資料庫,而且也肯定都會寫sql。我不知道大家有沒有這樣的感受,反正我是有過這樣的想法。就是當我把一條sql語句寫完了,並且執行完得到想要的結果。這時我就在想為什麼我寫這樣的一條sql語句,就能給我查詢出我想要的結果,為什麼我寫了update就能更新一條語句?它們的執行過程是 ...
  • 1.首先安裝scala(找到合適版本的具體地址下載) 在/usr/local/目錄下 wget https://www.scala-lang.org/download/**** 2.安裝spark (由於我的Hadoop是2.7.6版本的,因此我所用的spark是在官網上的適用hadoop-2以上版 ...
  • Elasticsearch中的腳本(script)有什麼作用? 如何創建、搜索、使用腳本? 腳本的緩存又是什麼? 對於腳本的使用, 有哪些高效的實踐策略? 本篇博文對這些內容作個簡單的探討. ...
  • 轉載、節選於 https://dev.mysql.com/doc/refman/8.0/en/innodb-tablespace.html This section covers topics related to InnoDB tablespaces. 1.The System Tablespac ...
  • 簡單來說GPDB是一個分散式資料庫軟體,其可以管理和處理分佈在多個不同主機上的海量數據。對於GPDB來說,一個DB實例實際上是由多個獨立的PostgreSQL實例組成的,它們分佈在不同的物理主機上,協同工作,呈現給用戶的是一個DB的效果。Master是GPDB系統的訪問入口,其負責處理客戶端的連接及 ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...