android随笔之截屏分析

提示:本文将对framework收到截屏按键到保存图片流程进行分析,并分享一个有趣的案例



前言

本文是基于android 12.0源码上进行分析的


1. 流程图

先上流程图,截屏事件是从PhoneWindowManager->interceptKeyBeforeDispatching()接收到KEYCODE_SYSRQ按键开始

PhoneWindowManager DisplayPolicy ScreenshotHelper TakeScreenshotService ScreenshotController SaveImageInBackgroundTask ImageExporter ContentResolver MediaProvider DatabaseHelper ScreenshotRunnable() FULLSCREEN,KEY_OTHER takeScreenshot() takeScreenshot() bindService() config_screenshotServiceComponent handleMessage() FULLSCREEN takeScreenshotFullscreen() takeScreenshotInternal() captureScreenshot() saveScreenshot() saveScreenshotInWorkerThread() task.execute() doInBackground() export() task.execute() createEntry() insert() insert() insertInternal() insertFile() insertAllowingUpsert() sql.insert() OnFilesChangeListener.onInsert() updateQuotaTypeForUri() writeImage() openOutputStream() writeExif() openFile() updateExifAttributes() publishEntry() update() PhoneWindowManager DisplayPolicy ScreenshotHelper TakeScreenshotService ScreenshotController SaveImageInBackgroundTask ImageExporter ContentResolver MediaProvider DatabaseHelper

2. 代码详解

frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java

先接收到截屏按键KEYCODE_SYSRQ

    public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
            int policyFlags) {
    
    
        ******
            case KeyEvent.KEYCODE_SYSRQ:
                if (down && repeatCount == 0) {
    
    
                    mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
                    mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_OTHER);
                    mHandler.post(mScreenshotRunnable);
                }
        ******
    }

    private class ScreenshotRunnable implements Runnable {
    
    
        ******
        @Override
        public void run() {
    
    
            mDefaultDisplayPolicy.takeScreenshot(mScreenshotType, mScreenshotSource);
        }
    }

frameworks/base/services/core/java/com/android/server/wm/DisplayPolicy.java

    public void takeScreenshot(int screenshotType, int source) {
    
    
        if (mScreenshotHelper != null) {
    
    
            mScreenshotHelper.takeScreenshot(screenshotType,
                    getStatusBar() != null && getStatusBar().isVisible(),
                    getNavigationBar() != null && getNavigationBar().isVisible(),
                    source, mHandler, null /* completionConsumer */);
        }
    }

frameworks/base/core/java/com/android/internal/util/ScreenshotHelper.java

    public void takeScreenshot(final int screenshotType, final boolean hasStatus,
            final boolean hasNav, int source, @NonNull Handler handler,
            @Nullable Consumer<Uri> completionConsumer) {
    
    
        ScreenshotRequest screenshotRequest = new ScreenshotRequest(source, hasStatus, hasNav);
        takeScreenshot(screenshotType, SCREENSHOT_TIMEOUT_MS, handler, screenshotRequest,
                completionConsumer);
    }

    private void takeScreenshot(final int screenshotType, long timeoutMs, @NonNull Handler handler,
            ScreenshotRequest screenshotRequest, @Nullable Consumer<Uri> completionConsumer) {
    
    
        ******
            if (mScreenshotConnection == null || mScreenshotService == null) {
    
    
                final ComponentName serviceComponent = ComponentName.unflattenFromString(
                        mContext.getResources().getString(
                                com.android.internal.R.string.config_screenshotServiceComponent));
                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;
                            }
                            mScreenshotService = service;
                            Messenger messenger = new Messenger(mScreenshotService);

                            try {
    
    
                                messenger.send(msg);
                            } catch (RemoteException e) {
    
    
        ******
    }

这里binder上com.android.systemui/com.android.systemui.screenshot.TakeScreenshotService服务,再send msg过去。

frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java

    private boolean handleMessage(Message msg) {
    
    
        ******
        switch (msg.what) {
    
    
            case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
                if (DEBUG_SERVICE) {
    
    
                    Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_FULLSCREEN");
                }
                mScreenshot.takeScreenshotFullscreen(uriConsumer, requestCallback);
                break;
        ******
    }

frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java

    void takeScreenshotFullscreen(Consumer<Uri> finisher, RequestCallback requestCallback) {
    
    
        ******
        takeScreenshotInternal(
                finisher,
                new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
    }

    private void takeScreenshotInternal(Consumer<Uri> finisher, Rect crop) {
    
    
        ******
        Bitmap screenshot = captureScreenshot(crop);//进行抓图生成Bitmap,生成过程这里就不做分析了
        ******
        saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true);
        //把Bitmap信息保存起来,我们重点看下保存过程。
    }

    private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
            Insets screenInsets, boolean showFlash) {
    
    
        ******
        saveScreenshotInWorkerThread(finisher, this::showUiOnActionsReady,
                this::showUiOnQuickShareActionReady);
        ******
    }

    private void saveScreenshotInWorkerThread(Consumer<Uri> finisher,
            @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener,
            @Nullable ScreenshotController.QuickShareActionReadyListener
                    quickShareActionsReadyListener) {
    
    
        ScreenshotController.SaveImageInBackgroundData
                data = new ScreenshotController.SaveImageInBackgroundData();
        ******
        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter,
                mScreenshotSmartActions, data, getActionTransitionSupplier());
        mSaveInBgTask.execute();
    }

frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java

    protected Void doInBackground(Void... paramsUnused) {
    
    
        ******
            ListenableFuture<ImageExporter.Result> future =
                    mImageExporter.export(Runnable::run, requestId, image);
            ImageExporter.Result result = future.get();
        ******
    }

frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java

    ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap) {
    
    
        return export(executor, requestId, bitmap, ZonedDateTime.now());
    }

    ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
            ZonedDateTime captureTime) {
    
    

        final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
                mQuality, /* publish */ true);

        return CallbackToFutureAdapter.getFuture(
                (completer) -> {
    
    
                    executor.execute(() -> {
    
    
                        try {
    
    
                            completer.set(task.execute());
                        } catch (ImageExportException | InterruptedException e) {
    
    
        ******
    }

    private static class Task {
    
    
        public Result execute() throws ImageExportException, InterruptedException {
    
    
            ******
                uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName);
                throwIfInterrupted();

                writeImage(mResolver, mBitmap, mFormat, mQuality, uri);
                throwIfInterrupted();

                int width = mBitmap.getWidth();
                int height = mBitmap.getHeight();
                writeExif(mResolver, uri, mRequestId, width, height, mCaptureTime);
                throwIfInterrupted();

                if (mPublish) {
    
    
                    publishEntry(mResolver, uri);
                    result.published = true;
                }
            ******
        }
    }

    private static Uri createEntry(ContentResolver resolver, CompressFormat format,
            ZonedDateTime time, String fileName) throws ImageExportException {
    
    
        ******
            final ContentValues values = createMetadata(time, format, fileName);
            Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        ******
    }

至此截屏文件信息第一次插入到媒体数据库里。

frameworks/base/core/java/android/content/ContentResolver.java

    public final @Nullable Uri insert(@RequiresPermission.Write @NonNull Uri url,
                @Nullable ContentValues values) {
    
    
        return insert(url, values, null);
    }

    public final @Nullable Uri insert(@RequiresPermission.Write @NonNull Uri url,
            @Nullable ContentValues values, @Nullable Bundle extras) {
    
    
        Objects.requireNonNull(url, "url");

        try {
    
    
            if (mWrapped != null) return mWrapped.insert(url, values, extras);
        } catch (RemoteException e) {
    
    
            return null;
        }
        ******
    }

这里的mWrapped是调用ContentResolver.wrap()静态函数得到的。因为我们这是media数据,所以是MediaProvider。

packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java

    public @Nullable Uri insert(@NonNull Uri uri, @Nullable ContentValues values,
            @Nullable Bundle extras) {
    
    
        ******
                        return insertInternal(uri, values, extras);
        ******
    }

    private @Nullable Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
            @Nullable Bundle extras) throws FallbackException {
    
    
        ******
        switch (match) {
    
    
            case IMAGES_MEDIA: {
    
    
                maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
                final boolean isDownload = maybeMarkAsDownload(initialValues);
                newUri = insertFile(qb, helper, match, uri, extras, initialValues,
                        FileColumns.MEDIA_TYPE_IMAGE);
                break;
            }
        ******
    }
        
    private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper,
            int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
            int mediaType) throws VolumeArgumentException, VolumeNotFoundException {
    
    
        ******
            rowId = insertAllowingUpsert(qb, helper, values, path);
        ******
    }
        
    private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb,
            @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)
            throws SQLiteConstraintException {
    
    
        return helper.runWithTransaction((db) -> {
    
    
            Long parent = values.getAsLong(FileColumns.PARENT);
            if (parent == null) {
    
    
                if (path != null) {
    
    
                    final long parentId = getParent(db, path);
                    values.put(FileColumns.PARENT, parentId);
                }
            }

            try {
    
    
                return qb.insert(helper, values);
            } catch (SQLiteConstraintException e) {
    
    
        ******
    }

    private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() {
    
    
        @Override
        public void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
                int mediaType, boolean isDownload) {
    
    
            handleInsertedRowForFuse(id);
            acceptWithExpansion(helper::notifyInsert, volumeName, id, mediaType, isDownload);

            helper.postBackground(() -> {
    
    
                if (helper.isExternal()) {
    
    
                    // Update the quota type on the filesystem
                    Uri fileUri = MediaStore.Files.getContentUri(volumeName, id);
                    updateQuotaTypeForUri(fileUri, mediaType);
                }

                // Tell our SAF provider so it knows when views are no longer empty
                MediaDocumentsProvider.onMediaStoreInsert(getContext(), volumeName, mediaType, id);
            });
        }
    }

这里就已经保存到sql数据库,后面就不继续追了,感兴趣的小伙伴可以往下看。

下面分享下案例

3. 案例

3.1 需求

截屏文件默认优先保存在外设usb中,如果没有则保存在内部external里

3.2 分析思路

其实从上面的分析过程中我们可以看到,android 12.0相比以前的低版本android 9.0,MediaProvider有了非常大的改动。

9.0的时候使用MediaStore.Images.ImageColumns.DATA保存image的绝对路径,然后通过ContentValues把路径传给MediaProvider。而12.0可以看到MediaProvider都是通过Uri去访问文件,没有使用绝对路径这个概念了,而Uri是通过设备卷名(即uuid)+文件号转化而成。

所以我们想优先保存到外设,可以在SaveImageInBackgroundTask先获取到外设的卷名,然后转化为对应的Uri传下来。

frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java

    public static String mVolumeName = "external";//MediaStore.Images.Media.EXTERNAL_CONTENT_URI

    SaveImageInBackgroundTask(Context context, ImageExporter exporter,
            ScreenshotSmartActions screenshotSmartActions,
            ScreenshotController.SaveImageInBackgroundData data,
            Supplier<ActionTransition> sharedElementTransition) {
    
    
        ******
            mVolumeName = "external";
            for (String resolvedVolumeName : MediaStore.getExternalVolumeNames(mContext)) {
    
    
                if (resolvedVolumeName.startsWith("internal") ||
                    resolvedVolumeName.startsWith("external")){
    
    
                    continue;//排除内部设备
                }
                mVolumeName = resolvedVolumeName;
            }
        ******
    }

然后insert的时候把这个卷名的Uri传下去

frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java

    private static Uri createEntry(ContentResolver resolver, CompressFormat format,
            ZonedDateTime time, String fileName) throws ImageExportException {
    
    
        ******
            Uri uri = resolver.insert(MediaStore.Images.Media.getContentUri(SaveImageInBackgroundTask.mVolumeName), values);
        ******
    }

至此,需求已完成。SaveImageInBackgroundTask会先去拿外设卷名,如果拿到则保存在外设,如果没拿到则还是保存在内部存储

3.3 有趣的问题

我在某些平台上发现,外设挂载的目录名不是外设卷名。这里补充一下,android原生外设挂载的目录名默认都是卷名。

而android 12.0 MediaProvider拿到的所谓卷名实际是设备挂载目录名。

那么就会造成卷名没找到。

03-21 16:40:28.816  2759  2833 E MediaProvider: Volume sda not found, calling identity: UserHandle{0}, attached volumes: {MediaVolume name: [internal] id: [null] user: [null] path: [null], MediaVolume name: [189a-0881] id: [public:8,0] user: [UserHandle{0}] path: [/storage/sda], MediaVolume name: [external_primary] id: [emulated;0] user: [UserHandle{0}] path: [/storage/emulated/0]}

上面可以看出设备卷名是189a-0881,但MediaProvider拿到的是sda,也就是挂载的目录名/storage/sda

那这种情况下,肯定是基于某种原因改了挂载目录名。如果我们直接还原目录名,可能会造成之前的问题发生,亦或者会造成现有平台某些场景下访问外设的绝对路径失效。改这个风险太高。

另外还有种解决方案,可以直接打开截屏生成的Uri,保存文件到外设。但只能解决这一种场景,其他MediaProvider的movie等文件insert到外设依旧会有问题。

那我们还需要分析MediaProvider为什么拿到的是目录名。

分析代码不难发现,都是通过FileUtils.extractVolumeName()获取卷名的

packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java

    public static @Nullable String extractVolumeName(@Nullable String data) {
        if (data == null) return null;
        final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data);
        if (matcher.find()) {
            final String volumeName = matcher.group(1);
            if (volumeName.equals("emulated")) {
                return MediaStore.VOLUME_EXTERNAL_PRIMARY;
            } else {
                return normalizeUuid(volumeName);
            }
        } else {
            return MediaStore.VOLUME_INTERNAL;
        }
    }

    private static @Nullable String normalizeUuid(@Nullable String fsUuid) {
        return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null;
    }

原因就在于此,那怎么解决呢。

正确的卷名都是通过StorageManager来获取,android 12隐藏了很多获取卷名的方法,无法直接使用,能获取卷名的函数又都需要传递context,而FileUtils类没有自己的context,extractVolumeName()也没有传下来。那只能借助其他类来间接访问了
我们可以用android.os.Environment新增一个静态函数
frameworks/base/core/java/android/os/Environment.java

    public static @NonNull StorageVolume getExternalStorageVolume(@NonNull File path) {
    
    
        final StorageVolume volume = StorageManager.getStorageVolume(path, UserHandle.myUserId());
        if (volume != null) {
    
    
            return volume;
        } else {
    
    
            return null;
        }
    }

然后修改packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java

    public static @Nullable String extractVolumeName(@Nullable String data) {
        if (data == null) return null;
        final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data);
        if (matcher.find()) {
            final String volumeName = matcher.group(1);
            if (volumeName.equals("emulated")) {
                return MediaStore.VOLUME_EXTERNAL_PRIMARY;
            } else {
                File file = new File(data);
                StorageVolume volume = Environment.getExternalStorageVolume(file);
                if (volume != null) {
                    return volume.getMediaStoreVolumeName();
                } else {
                    return normalizeUuid(volumeName);
                }
            }
        } else {
            return MediaStore.VOLUME_INTERNAL;
        }
    }

这样就解决了MediaProvider insert文件到外设失败的问题


总结

截屏保存到外设不难,难点在于过程中遇到的这个卷名拿错的坑。当时想了很多种方法在MediaProvider里去拿卷名,最终都失败了。只能借助Environment类间接访问了。这也给我提供一个思路,有时候没必要纠结一定要直接访问某个类,可能权限或者接口隐藏等问题都行不通,那我们可以借助android.os.xxxx的这些类,暴露一个接口出来去间接访问达到目的。

另外,这个案例也说明一个问题,不要轻易修改外设挂载的目录名,改了以后android高版本ContentProvider类型数据库插入到外设会失败。上面案例也只解决了MediaProvider数据库问题,其他ContentProvider类型的数据库依旧有同样的现象,解决方案类似,这里就不列举了。

猜你喜欢

转载自blog.csdn.net/hmz0303hf/article/details/129771208
今日推荐