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

Задний план

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

Проблема

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

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

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

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

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);
    }
}

layout / 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;
    }
}

Вопросы

Ну это о проблемах:

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

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


РЕДАКТИРОВАТЬ: я заметил, что на веб-сайте CommonsWare (здесь ), говорится, что растровое изображение вывода по какой-то причине больше, но в отличие от того, что я заметил (черный край в начале И в конце), он говорит, что он должен быть в конце:

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

Я пробовал то, что там предлагали, но вылетает с исключением «java.lang.RuntimeException: Buffer not enough for Pixels».


person android developer    schedule 30.04.2017    source источник
comment
Я не рассматривал это подробно, но вам может не хватать реального размера экрана (DisplayMetrics.getRealMetrics) и вы можете получить только размер окна. Вот рабочий пример, который может помочь: github.com/mattprecious/telescope/blob/   -  person Eric Cochran    schedule 08.05.2017
comment
Извините, я не понимаю вашу проблему с уведомлениями. Значок проекции отображается в строке состояния, когда используется Media Projection API. Также пользователю предоставляется диалоговое окно, чтобы УВЕДОМЛЕНИЕ о том, что их экран будет захвачен приложением, которое запускает объект намерения захвата экрана, полученный от службы проекции мультимедиа.   -  person Ankit Batra    schedule 08.05.2017
comment
@AnkitBatra Да, я хочу, чтобы значок проекции остался, потому что я думаю, что это позволит избежать повторной инициализации всего процесса. Можно ли избежать повторной инициализации между снимками экрана? В настоящее время показывает, скрывает, показывает, скрывает ...   -  person android developer    schedule 08.05.2017


Ответы (1)


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

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

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


    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.

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

Насколько медленно это оправдывает ваши ожидания? Мой вариант использования Media projection API - это сделать снимок экрана и предоставить его пользователю для редактирования. По мне скорость вполне приличная. Я считаю, что стоит упомянуть, что класс ImageReader может принимать обработчик потока в setOnImageAvailableListener. Если вы предоставите обработчик там, обратный вызов onImageAvailable будет запущен в потоке обработчика вместо того, который создал 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());
        }


person Ankit Batra    schedule 08.05.2017
comment
Проблема с полями решается как за счет использования лучшего размера, так и за счет обрезки. Насчет скорости, использование обработчика не помогло. Однако не могли бы вы показать, как оставить уведомление между снимками экрана? Например, я хочу запустить снимок экрана в определенное время, а затем, когда его запустит что-то еще. Я думаю, что это, по крайней мере, улучшит скорость захвата после первого захвата, потому что не нужно будет каждый раз повторно инициализировать. - person android developer; 08.05.2017
comment
Решение newInstance не позволяет иметь слишком много изображений (пример 1000), и даже с несколькими изображениями (10) уведомление все равно удаляется. - person android developer; 08.05.2017
comment
Кажется, это действительно быстро. Просто для пользователя индикация довольно медленная и не исчезает сразу. Если вы знаете, как избежать этого странного UX (оставив индикацию на месте, пока я не закончу со всеми скриншотами), сообщите мне. А пока это правильный ответ, и я отмечу его. - person android developer; 08.05.2017
comment
Если вы хотите, чтобы значок проекции оставался, вам придется запретить вызов mediaProjection.stop () в своем коде, который останавливает проекцию и в конечном итоге скрывает значок. Тем не менее, я предполагаю, что могут возникнуть некоторые проблемы (например, медленная работа, разряд батареи и т. Д.), Если вы продолжите проецирование мультимедиа в фоновом режиме. Я не думаю, что есть способ контролировать значок проекции, отображаемый в строке состояния. Но я недостаточно исследовал в этом направлении. Кроме того, я считаю, что вы можете улучшить пользовательский интерфейс, объясняя / намекая пользователю значок, чтобы не потерять слишком много в пользовательском интерфейсе. - person Ankit Batra; 08.05.2017
comment
Не могли бы вы продемонстрировать, как это сделать хорошо? Я думаю, что делаю что-то не так, пытаясь это сделать. - person android developer; 09.05.2017
comment
Вставьте свой код или поделитесь ссылкой, чтобы сообщество могло посмотреть. - person Ankit Batra; 09.05.2017
comment
@androiddeveloper, вы уже знали, что этот ваш класс не работает на Android 7.0 (появляется черное изображение)? может исправить это пожалуйста? Я думаю, что этот класс очень интересен, в основном из-за AsyncTask. - person ; 29.12.2017
comment
@MarcioGomes Он должен работать с API 21, то есть Android 5.0. Пожалуйста, попробуйте эмулятор. - person android developer; 29.12.2017
comment
@androiddeveloper, то вы сделали этот класс только для работы на версиях 5.0 и 5.1? Я тестировал на своем Moto G5s Plus v.7.1.1 и не работал, появляется черный экран. - person ; 29.12.2017
comment
@androiddeveloper, здесь - это код, который я тестировал и работает в версиях 7.0 и 7.1. .1, но не знаю, работает ли он также на 5.1 или 6.0. Даже в этом случае этот код не соответствует моей потребности, потому что использует создание бесконечного цикла (например, сбой на стороне сервера, когда мы хотим отправить эти снимки экрана через сокет из-за этого бесконечного цикла). Попробуйте исправить свой код (чтобы он работал в последних версиях Android) на основе этого кода, который я связал и обновил на Github ;-). - person ; 29.12.2017
comment
@MarcioGomes К сожалению, у меня есть одно устройство, и оно на 8.1, и эмуляторы не работают на моем компьютере, потому что у меня процессор AMD. Не могу тебе помочь. Я помню, что мне удалось заставить его работать с другими версиями, но не помню, в какой. - person android developer; 30.12.2017