Шпионская камера на базе андроид

Мир Android развивается очень бурно, и мы, разработчики, имеем счастье каждый день узнавать что-то новое и тем самым становиться лучше. Технологии, которые были передовыми год назад, уже могут быть не так эффективны. К примеру, начиная с API версии 21 был введен новый класс Camera2, предоставляющий возможность управлять камерой. Старому классу Camera присвоен статус deprecated: это значит, что он все еще доступен для использования, но уже очень скоро его исключат и на новых устройствах этот класс работать не будет. Мы ориентируемся на перспективу, для этого сегодня мы изучим новые возможности, разобравшись, зачем же он же пришел на смену старому.

Конвейер как концепция

Паттерн pipeline и класс Camera2

Паттерн pipeline и клaсс Camera2

Хоть названия классов и схожи, концепция работы серьезно изменилась. Модель работы камеры представлена в виде конвейера (pipeline). Это паттерн ООП, который предполагает использование многопоточной разработки с целью последовательного преобразования объекта, выводимого как конечный результат. В нашем случае мы подключаемся к камере, задаем параметры съемки и получателя сформированного изображения. Камера — это физический объект, поэтому для повышения производительности и экономии ресурсов устройства целесообразно все вычисления возложить на дополнительные потоки. Сейчас ты увидишь, что это не только не страшно, но и полезно!

Шпионская камера

Настало время применить на практике новый API. Интересно будет разобраться, как теперь решается одна из наших любимых задач: создать приложение, которое в фоне может делать фотографии без ведома пользователя.

Как обычно в Android, начнем мы с добавления пермишенов в манифест-файл. Сегодня нам нужен доступ к камере.

<uses-permission android:name="android.permission.CAMERA" />

Оснoвную работу начнем с получения доступа к камерам на нашем устройстве, в этом нам поможет класс CameraManager. Это менеджер системного сервиса, который позволяет найти доступные камеры, подсоединиться к любой из них и задать для нее настройки съемки.

CameraManager manager = (CameraManager)
getApplicationContext().getSystemService(Context.CAMERA_SERVICE);

Вообще, максимально широкое использование многопоточности — это тренд современной разработки под Android. Работа с сетью, физичeскими датчиками на устройстве, обработка полученной информации — это все очень ресурсозатратные и часто долговременные операции. Порождение дополнительных потоков позволяет не только по максимуму использовать все ресурсы аппарата (все современные мобильные устройства обладают многоядерными процессорами), но и создать у пользователя ощущение «легкости»: в основном потоке ведется только отображение элементов интерфейса.

Формирование картинки начнем с создания новых потоков и обработчиков событий. Сперва создадим новый поток, который будет висеть в фоне и ждать команды на выполнение какой-либо операции. Он будет нам полезен при задании настроек камеры и передаче картинки между объектами.

mBackgroundThread = new HandlerThread("CameraBackground");
mBackgroundThread.start();
mBackgroundHandler = new Handler(mBackgroundThread.getLooper

Камера — довольно сложное и не самое быстро реагирующее физическое устройство. По сравнению с вычислениями в памяти устройства перевод камеры из состояния в состояние может занимать целую вечность. Поэтому операции с камерой будут выполняться вне главного потока, а изменения состояния камеры мы будем отлавливать с помощью обработчиков событий. К примеру, инициализируем в нашем приложении объект CameraDevice.StateCallback, один из методов которого будет вызван после того, как на устройстве откроется камера.

private final CameraDevice.StateCallback mStateCallback =
new CameraDevice.StateCallback() {
  @Override
  public void onOpened(@NonNull CameraDevice cameraDevice){...}

Еще нам потребуется отслеживать тот момент, когда нам cтанет доступна картинка, снятая камерой. Организация передачи картинки с камеры другим объектам — довольно ресурсозатратный процесс, поэтому он тоже будет выполняться в фоне. Для этого существует объект CameraCaptureSession.CaptureCallback, его метод onCaptureCompleted будет вызван после захвата камерой изображения.

private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
  @Override
  public void onCaptureCompleted(...);

Теперь определимся, как мы будем обрабатывать полученное изображение. Сформированное с помощью класса CameraCaptureSession изображение будет доступно в виде объекта Surface. Это обертка для фотографий, сделанных с помощью класса Camera2. Специально для работы с ним создан класс ImageReader. Воспользуемся интерфейсом ImageReader.OnImageAvailableListener, метод onImageAvailable в созданном объекте будет вызван сразу, как только картинка станет доступна.

ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
  @Override
  public void onImageAvailable(ImageReader reader) {...};

Фотографируем

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

for (String cameraId : manager.getCameraIdList()) {
  CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);

Объект класса CameraCharacteristics содержит в себе физические параметры камеры под номером cameraId. Нас сейчас интересует, в какую сторону направлена камера. Камеры могут быть трех типов: фронтальная (LENTS_FACING_FRONT), задняя (LENS_FACING_BACK) и внешняя (LENS_FACING_EXTERNAL). Камеру не для селфи мы будем искать методом исключения.

Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {continue;}

Теперь нужно разобраться, какого качества фотографии можно получить с камеры. Объект класса SteamConfigurationMap будет содержать в себе информацию о поддерживаемых разрешениях изображения для выбранного режима съемки. Тут мы воспользовались данными из класса CameraCharacteristics, он позволяет управлять многими другими параметрами камеры: фокусом, чувствительностью и так далее. Сегодня мы к нему еще вернемся.

StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

Для организации конвейера нужно обязательно задать конечного получателя, это будет объект уже знакомого нам класса ImageReader. Зададим размеры фотографии (тут как раз пригодился объект map), а как только картинка сформируется, вызовем уже созданный нами обработчик mOnImageAvailableListener. Чтобы интерфейс приложения не ждал завершения этой операции, передадим все потоку mBackgroundHandler.

mImageReader = ImageReader.newInstance(width,height,ImageFormat.JPEG, 2);
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);

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

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

manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);

Как только камера откроется, мы попадаем в мир многопоточного программирования под Android: системой будет вызван описанный выше метод onOpened класса CameraDevice.StateCallback. При желании уже на этом этапе можно получить картинку с камеры и сбросить ее в файл. Но результат будет плачевный: камера не сфокусирована, светочувствительность низкая… Как в настоящем фотоаппарате, камерам в планшетах и смартфонах нужно некоторое время, чтобы осмотреть окружающий мир и настроиться на работу. Дадим ей эту возможность, создав «тренировочный» снимок.

MpreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
PreviewRequestBuilder.addTarget(mImageReader.getSurface());

Сейчас мы указали, что требуется организовать передачу картинки с камеры в слабом качестве (TEMPLATE_PREVIEW). По замыслу разработчиков, сформированный объект Surface следует вывести на экран с помощью классов SurfaceView и TextureView. Поскольку изображение не может уходить «в никуда» (мы получим ошибку NullPointerException), направим все в mImageReader.

шпионская камера
Предпоказ изображения с TextureView

Теперь нам нужно дождаться момента, когда камера будет готова к работе и начнет передавать данные на конвейер. Да, у нас опять асинхронность, и сейчас эстафета переходит следующему объекту. Мы задействуем CameraCaptureSession.StateCallback, методы которого вызываются при изменении состояния потоков данных с камеры.

mCameraDevice.createCaptureSession(..., new CameraCaptureSession.StateCallback() {...}

В частности, нас интересует ситуация, когда камера уже включилась и может отправлять данные. Как настоящие профи-фотографы, мы выставим параметры съемки, так как сейчас камера передает совершенно не настроенное изображение. У нас будет включен автофокус, при желании можно добавить вспышку, настроить баланс белого и прочее.

@Override
onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
  mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
    CameraMetadata.CONTROL_AF_TRIGGER_START);
  mCaptureSession.capture(mPreviewRequestBuilder.build(),
    mCaptureCallback, mBackgroundHandler)
  ...
};

Метод capture отправляет обновленные настройки на камеру с указанием конечного получателя изображения. Как ты уже понял, все действия с камерой ресурсозатратны, поэтому выполнять их будет наш поток mBackgroundHandler. Фотокамера — вещь капризная, изображение, полученное на данном этапе, еще не будет достаточно качественным. Хоть мы и указали, что нам нужен автофокус, он не выставляется моментально, поэтому первые кадры с камеры можно назвать пристрелочными. Нужно уловить момент, когда камера окончательно настроится, и только тогда сохранить получившееся изображение. В этом нам поможет метод onCaptureCompleted объекта CameraCaptureSession.CaptureCallback. Он вызывается каждый раз, когда произошел захват изображения камерой. Мы удостоверимся, что камера захватила фокус, затем запустим процесс захвата и сохранения картинки в файл.

if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED==afState) {
  Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
  processImageToFile();
  ...
}

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

public void onImageAvailable(ImageReader reader) {
  mBackgroundHandler.post(new ImageSaver(reader.acquireLatestImage(), mFile));
};

Запись фотографии мы уже привычно поручили объекту mBackgroudHandler. Камера нам выдала несколько изображений, последняя будет самой качественной, именно ее мы вытащим методом acquireLatestImage, а предыдущие фотографии будут выброшены из памяти.

Заключение

У нас получилось! Сегодня мы пробрались сквозь дебри многопоточного программирования под Android и получили качественную фотографию с камеры. Весь исходный код созданного нами приложения есть на сайте, рекомендую его тоже посмотреть. Да, класс Camera2 требует вдумчивого изучения и тщательной проработки действий. Но зато теперь у нас есть инструмент для полного контроля над камерой, что позволяет создавать более гибкие и удобные приложения. Надо сказать, что механизмы организации многопоточности в Android не самые удобные, но это не значит, что они неэффективны. Уверен, теперь ты сможешь по максимуму использовать ресурсы устройства, создавая сбалансированные приложения на радость пользователям. Если остались какие-то вопросы, обязательно пиши. Удачи!

Click to rate this post!
[Total: 12 Average: 4.1]

Специалист в области кибер-безопасности. Работал в ведущих компаниях занимающихся защитой и аналитикой компьютерных угроз. Цель данного блога - простым языком рассказать о сложных моментах защиты IT инфраструктур и сетей.

1 comments On Шпионская камера на базе андроид

Leave a reply:

Your email address will not be published.