Как правильно сделать снимок экрана во всем мире?

Фон

Начиная с Android API 21, приложения могут делать скриншоты по всему миру и записывать экран.

Проблема

Я сделал образец кода из всего, что я нашел в Интернете, но у него есть несколько проблем:

  • Это довольно медленно. Возможно, это возможно избежать, по крайней мере, для нескольких скриншотов, избегая уведомления об удалении, пока оно действительно не понадобится.
  • Он имеет черные поля слева и справа, что означает, что с расчетами что-то может быть не так:

введите описание изображения здесь

Что я пробовал

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_ID = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.checkIfPossibleToRecordButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                ScreenshotManager.INSTANCE.requestScreenshotPermission(MainActivity.this, REQUEST_ID);
            }
        });
        findViewById(R.id.takeScreenshotButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                ScreenshotManager.INSTANCE.takeScreenshot(MainActivity.this);
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_ID)
            ScreenshotManager.INSTANCE.onActivityResult(resultCode, data);
    }
}

макет /activity _main.xml

<LinearLayout
    android:id="@+id/rootView"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">


    <Button
        android:id="@+id/checkIfPossibleToRecordButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="request if possible"/>

    <Button
        android:id="@+id/takeScreenshotButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="take screenshot"/>

</LinearLayout>

ScreenshotManager

public class ScreenshotManager {
    private static final String SCREENCAP_NAME = "screencap";
    private static final int VIRTUAL_DISPLAY_FLAGS = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
    public static final ScreenshotManager INSTANCE = new ScreenshotManager();
    private Intent mIntent;

    private ScreenshotManager() {
    }

    public void requestScreenshotPermission(@NonNull Activity activity, int requestId) {
        MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        activity.startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), requestId);
    }


    public void onActivityResult(int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK && data != null)
            mIntent = data;
        else mIntent = null;
    }

    @UiThread
    public boolean takeScreenshot(@NonNull Context context) {
        if (mIntent == null)
            return false;
        final MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        final MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, mIntent);
        if (mediaProjection == null)
            return false;
        final int density = context.getResources().getDisplayMetrics().densityDpi;
        final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        final Point size = new Point();
        display.getSize(size);
        final int width = size.x, height = size.y;

        // start capture reader
        final ImageReader imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1);
        final VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay(SCREENCAP_NAME, width, height, density, VIRTUAL_DISPLAY_FLAGS, imageReader.getSurface(), null, null);
        imageReader.setOnImageAvailableListener(new OnImageAvailableListener() {
            @Override
            public void onImageAvailable(final ImageReader reader) {
                Log.d("AppLog", "onImageAvailable");
                mediaProjection.stop();
                new AsyncTask<Void, Void, Bitmap>() {
                    @Override
                    protected Bitmap doInBackground(final Void... params) {
                        Image image = null;
                        Bitmap bitmap = null;
                        try {
                            image = reader.acquireLatestImage();
                            if (image != null) {
                                Plane[] planes = image.getPlanes();
                                ByteBuffer buffer = planes[0].getBuffer();
                                int pixelStride = planes[0].getPixelStride(), rowStride = planes[0].getRowStride(), rowPadding = rowStride - pixelStride * width;
                                bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Config.ARGB_8888);
                                bitmap.copyPixelsFromBuffer(buffer);
                                return bitmap;
                            }
                        } catch (Exception e) {
                            if (bitmap != null)
                                bitmap.recycle();
                            e.printStackTrace();
                        }
                        if (image != null)
                            image.close();
                        reader.close();
                        return null;
                    }

                    @Override
                    protected void onPostExecute(final Bitmap bitmap) {
                        super.onPostExecute(bitmap);
                        Log.d("AppLog", "Got bitmap?" + (bitmap != null));
                    }
                }.execute();
            }
        }, null);
        mediaProjection.registerCallback(new Callback() {
            @Override
            public void onStop() {
                super.onStop();
                if (virtualDisplay != null)
                    virtualDisplay.release();
                imageReader.setOnImageAvailableListener(null, null);
                mediaProjection.unregisterCallback(this);
            }
        }, null);
        return true;
    }
}

Вопросы

Хорошо, о проблемах:

  • Почему это так медленно? Есть ли способ его улучшить?
  • Как я могу избежать, снимая скриншоты, удаление уведомлений о них? Когда я могу удалить уведомление? Значит ли это, что он постоянно принимает скриншоты?
  • Почему выходной растровый файл (в настоящее время я ничего не делаю с ним, потому что он все еще POC) имеет черные поля? Что не так с кодом в этом вопросе?

ПРИМЕЧАНИЕ. Я не хочу снимать снимок только с текущего приложения. Я хочу знать, как использовать его во всем мире, для всех приложений, что возможно официально только с использованием этого API, насколько я знаю.


EDIT: Я заметил, что на веб-сайте CommonsWare ( здесь) говорится, что выходное растровое изображение по какой-то причине больше, но в отличие от того, что я заметил (черный край в начале и конце), он говорит, что он должен быть в конце:

По необъяснимым причинам он будет немного больше, с избытком неиспользованных пикселей на каждой строке на конце.

Я пробовал то, что предлагалось там, но он выходит из строя с исключением "java.lang.RuntimeException: буфер не достаточно большой для пикселей".

Ответ 1

Почему выходной растровый файл (в настоящее время я ничего не делаю с ним, потому что он все еще POC) имеет черное поле? Что не так с кодом в этом вопросе?

У вас есть черные поля вокруг экрана, потому что вы не используете realSize окна, в котором вы находитесь. Чтобы решить эту проблему:

  • Получить реальный размер окна:


    final Point windowSize = new Point();
    WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    windowManager.getDefaultDisplay().getRealSize(windowSize);


  1. Используйте это для создания своего устройства чтения изображений:


    imageReader = ImageReader.newInstance(windowSize.x, windowSize.y, PixelFormat.RGBA_8888, MAX_IMAGES);


  1. Этот третий шаг может не потребоваться, но я видел в коде приложения (который работает на разных устройствах Android). Когда вы приобретаете изображение для ImageReader и создаете из него растровое изображение. Обрезайте это растровое изображение, используя размер окна, используя приведенный ниже код.


    // fix the extra width from Image
    Bitmap croppedBitmap;
    try {
        croppedBitmap = Bitmap.createBitmap(bitmap, 0, 0, windowSize.x, windowSize.y);
    } catch (OutOfMemoryError e) {
        Timber.d(e, "Out of memory when cropping bitmap of screen size");
        croppedBitmap = bitmap;
    }
    if (croppedBitmap != bitmap) {
        bitmap.recycle();
    }


Я не хочу снимать снимок только с текущего приложения. Я хочу знать, как использовать его во всем мире, для всех приложений, что возможно официально только с использованием этого API, насколько я знаю.

Чтобы захватить экран/сделать снимок экрана, вам нужен объект MediaProjection. Чтобы создать такой объект, вам нужна пара resultCode (int) и Intent. Вы уже знаете, как эти объекты будут получены, и кешируйте их в классе ScreenshotManager.

Возвращаясь к скриншотам любого приложения, вам нужно следовать той же процедуре получения этих переменных resultCode и Intent, но вместо того, чтобы кэшировать его локально в ваших переменных класса, запустите фоновый сервис и передайте эти переменные таким же, как любые другие нормальные параметры. Посмотрите, как Telecine делает это здесь. Когда эта фоновая служба запущена, он может предоставить триггер (кнопку уведомления) пользователю, который при нажатии будет выполнять те же операции захвата экрана/снятия снимков экрана, как вы делаете в своем классе ScreenshotManager.

Почему это так медленно? Есть ли способ улучшить его?

Насколько медленны ваши ожидания? Мой прецедент для API медиапроекции - сделать снимок экрана и представить его пользователю для редактирования. Для меня скорость достаточно приличная. Одна вещь, о которой я хотел бы упомянуть, это то, что класс ImageReader может принимать обработчик для потока в setOnImageAvailableListener. Если вы предоставите обработчик, onImageAvailable callback будет запускаться в потоке обработчика вместо того, который создал ImageReader. Это поможет вам НЕ создавать AsyncTask (и запускать его), когда изображение доступно, вместо этого сам обратный вызов будет происходить в фоновом потоке. Вот как я создаю свой ImageReader:



    private void createImageReader() {
            startBackgroundThread();
            imageReader = ImageReader.newInstance(windowSize.x, windowSize.y, PixelFormat.RGBA_8888, MAX_IMAGES);
            ImageHandler imageHandler = new ImageHandler(context, domainModel, windowSize, this, notificationManager, analytics);
            imageReader.setOnImageAvailableListener(imageHandler, backgroundHandler);
        }

    private void startBackgroundThread() {
            backgroundThread = new HandlerThread(NAME_VIRTUAL_DISPLAY);
            backgroundThread.start();
            backgroundHandler = new Handler(backgroundThread.getLooper());
        }