Переадресуем уведомления о входящих звонках и смс на компьютер

в 9:10, , рубрики: broadcastreceiver, linux, Разработка под android, уведомления, метки: , ,

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

А если еще и пересылать входящие СМС, то можно упростить работу с онлайн-сервисами, высылающими одноразовые коды подтверждения для проведения различных операций. Чтобы посмотреть этот код, не нужно будет искать телефон и разблокировать его. Вся информация будет доступна сразу на экране ноутбука.

Кроме того, это неплохая возможность попрактиковаться в разработке под андроид. В результате получилось маленькое приложение, которое перехватывает входящие звонки, СМС и отправляет их по локальной сети.

Для тех, кому интересно как у меня это получилось, добро пожаловать под кат.

Небольшая демонстрация работы, чтобы понять о чем речь (извиняюсь за качество, нет нормальной техники для видеозаписи)

Приемники широковещательных сообщений

Первое что нам нужно для перехвата звонков и СМС это добавить соответствующие разрешения в AndroidManifest.xml:

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

Но одних разрешений мало. Ещё понадобятся приемники широковещательных сообщений. Это специальные объекты, которые выполняются при получении какого либо специального сигнала. Для того, чтобы система знала что у нас есть объект, которому нужно послать сообщение, его необходимо зарегистрировать.

Конечно это можно сделать добавив соответствующие строчки в AndroidManifest.xml:

<receiver android:name="MessageReceiver" android:enabled="true">
    <intent-filter>
         <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
    </intent-filter>
</receiver>
<receiver android:name="CallReceiver">
    <intent-filter>
        <action android:name="android.intent.action.PHONE_STATE"/>
    </intent-filter>    
</receiver>

Но если мы хотим иметь возможность включать и выключать наши BroadcastReceiver, то можно их регистрировать динамически:

//создаем новый объект приемника оповещенийи 
myMessageReceiver = new MessageReceiver();
//создаем новый объект фильтра сообщений
IntentFilter e = new IntentFilter();
//выставляем наивысший приоритет нашего фильтра
e.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
//фильтр на прием СМС
e.addAction("android.provider.Telephony.SMS_RECEIVED");
//и регистрируем наш приемник в системе
registerReceiver(myMessageReceiver, e);

//для того чтобы перестать получать оповещения достаточно вызывать
unregisterReceiver(myMessageReceiver);

Но если мы будем создавать BroadcastReceiver в коде основной экранной формы (Activity), то они будут работать только когда приложение активно (не стоит в паузе и не остановлено). Наше же приложение всегда должно оставаться в фоне. Для этого существуют фоновые сервисы:

//подготавливаем уведомление о запущенном сервисе
Notification notif = new Notification(R.drawable.ic_launcher, "Notice service started",	System.currentTimeMillis());  	   
//сообщим фоновому сервису информацию о нашей форме (Activity)
Intent intent1 = new Intent(this, NoticeSetting.class);
PendingIntent pIntent = PendingIntent.getActivity(this, 0, intent1, 0);		   
//указываем текст уведомления
notif.setLatestEventInfo(this, "Notice service enabled", "For stopped it click me", pIntent);
// ставим флаг, чтобы уведомление попало в постоянную секцию
notif.flags |= Notification.FLAG_ONGOING_EVENT ;	
//и запускаем сервис
startForeground(NOTIFICATION_ID,  notif );

//для остановки сервиса используем 
stopForeground(true);

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

Стоит заметить, что указанный выше способ создания уведомления сервиса считается устаревшим, и начиная с версии API level 11 (Android 3.0.x) необходимо использовать конструктор Notification.Builder. Но я не стал его использовать для совместимости с более старыми версиями Android.

Так же будет полезно иметь возможность запускать фоновый сервис при запуске системы, для этого можно зарегистрировать ещё один BroadcastReceiver:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<receiver android:name=".BootBroadReceiv" android:enabled="true" android:exported="false" >
	<intent-filter>
		<action android:name="android.intent.action.BOOT_COMPLETED" />
	</intent-filter>
</receiver>

Теперь, когда мы разобрались как и где нужно регистрировать BroadcastReceiver, рассмотрим подробнее как их использовать. Необходимо унаследовать свой класс от BroadcastReceiver и определить в нем метод onReceive, который вызывается при приходе широковещательного сообщения:

public class MessageReceiver extends BroadcastReceiver {
	@Override
	public void onReceive(Context context, Intent intent) {
                //делаем что-то с сообщениями
        } 
}

Подготовка сообщения

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

phoneNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);

Существует несколько способов поиска имени звонящего по телефонной книге. Я воспользовался вот таким запросом:

String Name=" ";
phoneNumber = PhoneNumberUtils.stripSeparators(phoneNumber);
         
String[] projection = new String[]
  	{ ContactsContract.Data.CONTACT_ID,
       	 ContactsContract.Contacts.LOOKUP_KEY,
         ContactsContract.Contacts.DISPLAY_NAME,
         ContactsContract.Contacts.STARRED,
         ContactsContract.Contacts.CONTACT_STATUS,
         ContactsContract.Contacts.CONTACT_PRESENCE };

String selection = "PHONE_NUMBERS_EQUAL(" + 
         Phone.NUMBER + ",?) AND " + 
         Data.MIMETYPE + "='" + 
         Phone.CONTENT_ITEM_TYPE + "'";
         
String selectionArgs [] ={ phoneNumber };
        
Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, projection, selection, selectionArgs, null);
        
if(cursor.getCount()>0)
{
      	cursor.moveToFirst();
	Name = cursor.getString(2) + " ";
}		
else Name = "Unknow";

Для работы с книгой контактов нужно добавить в AndroidManifest.xml следующее разрешение

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

Получить входящие СМС можно поместив в onReceive следующий код:

Bundle bundle = intent.getExtras();        
if (bundle != null) {
	Object[] pdus = (Object[]) bundle.get("pdus");
	SmsMessage[] msgs = new SmsMessage[pdus.length];
	ArrayList<String> numbers = new ArrayList<String>();
	ArrayList<String> messages = new ArrayList<String>();
 	//пробегаемся по всем полученным сообщениям
		for (int i=0; i<msgs.length; i++){
			msgs[i] = SmsMessage.createFromPdu((byte[])pdus[i]);
			//получаем номер отправителя
			numbers.add(msgs[i].getOriginatingAddress()); 
			//получаем текст сообщения
			messages.add(msgs[i].getMessageBody().toString());
	}

Отправка сообщения

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

InetAddress serv_addr = null;
serv_addr = InetAddress.getByName("255.255.255.255");
int port= 35876;
DatagramSocket sock = null;
sock = new DatagramSocket();
//mess - сообщение на отправку
byte [] buf = mess.getBytes();
DatagramPacket pack= new DatagramPacket(buf, buf.length,serv_addr,port);
sock.send(pack);
sock.close();

Для возможности работы с сетью добавим разрешение в AndroidManifest.xml

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

И тут в ходе тестирования выяснилось, что метод onReceive в BroadcastReceiver живет очень небольшое время. Если поместить в него функцию определения имени абонента по номеру и функцию отправки сообщения по сети, то система убьет его раньше, чем обе функции будут выполнены. Что характерно, в версии Android 2.3.* этот код успешно успевал отработать, а в версиях 4.* нет. Поэтому длительные операции нужно выполнять в отдельном сервисе, запускаемом функцией startService().

Прием сообщений

Для приема сообщения напишем маленькую программу на C, использующую всё те же сокеты:

int main() {
	int z;
	char srvr_addr[8] = "0.0.0.0";
	struct sockaddr_in adr_inet; // AF_INET
	struct sockaddr_in adr_clnt; // AF_INET
	socklen_t len_inet;                
	int s;
	//буфер под входящее сообщение
	char dgram[512];            
	//создаем сокет
	s = socket(AF_INET,SOCK_DGRAM,0);
	if ( s == -1 ) {
		displayError("socket()");
	}
	memset(&adr_inet,0,sizeof(adr_inet));
	adr_inet.sin_family = AF_INET;
	adr_inet.sin_port = htons(35876);
	adr_inet.sin_addr.s_addr =inet_addr(srvr_addr);
 
	if ( adr_inet.sin_addr.s_addr == INADDR_NONE ) {
		displayError("bad address.");
	}
	len_inet = sizeof(adr_inet);
	//слушаем сокет
	z = bind(s, (struct sockaddr *)&adr_inet, len_inet);
	if ( z == -1 ) {
		displayError("bind()");
	}
	while(1) {
		len_inet = sizeof adr_clnt;
		//ожидаем входящего сообщения
		z = recvfrom(s,
			dgram,
			512,
			0,
 			(struct sockaddr *)&adr_clnt, 
			&len_inet); 
	if ( z < 0 ) {
		displayError("recvfrom(2)");
	}

	dgram[z] = 0; // null terminate
	if ( !strcasecmp(dgram,"QUIT") ) {
		break;     // Quit server
	}
	//dgram наше принятое сообщение
	printf("%sn",dgram);

Но получить сообщение мало, нужно еще как-то показать его себе. В linux существует удобная консольная утилита notify-send, которая принимает в качестве аргумента сообщение и выводит его пользователю через панель уведомления kde или gnome. Используя её можно вывести сообщение в пару строчек:

if (fork() == 0) {
	execl("/usr/bin/notify-send","notify-send","Сообщение с вашего телефона:",  	dgram, (char *) 0);
	perror("exec one failed");
	exit(1);
}

Полученное приложение Вы можете добавить в автозагрузку вашим любимым способом.

Ссылки в тему

Автор: Ermito

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js