Pull to refresh

XMPP-SMS шлюз на Android

Reading time 11 min
Views 20K


Введение

Причиной написания данной статьи послужила необходимость создания программы для системы Android, с помощью которой можно отправлять данные заказа в виде SMS сообщений владельцам интернет магазинов о том, что был совершен заказ товаров или услуг. Ранее мною использовалась система включающая GSM-модем и программу написанную на языке С++, использовавшая AT-команды для общения с модемом и библиотеку gloox для получения сообщений по протоколу XMPP, на стороне web-сайта использовалась библиотека xmpphp, для отправки данных заказа. При такой схеме приходилось держать включенным компьютер постоянно, так как система приема заказов работала круглосуточно, соответственно отсюда дополнительный расход электроэнергии, шум от вентиляторов ночью и постоянный контроль интернет соединения.

Основной задачей программы, которую мы будем создавать на протяжении статьи, является получение сообщения определенного формата, по протоколу XMPP и последующая передача полученных данных через SMS. Средой разработки будет являться Eclipse с установленным плагином ADT и необходимыми SDK. Для взаимодействия по протоколу XMPP будет использоваться библиотека SMACK для Android устройств.

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

Сначала создадим каркас нашего приложения, который в последующем будем наращивать необходимым функционалом. Для этого создадим в Eclipse, Android Project (Ctrl+N – Android – Android Project) со следующими данными:



После создания нового проекта, добавим необходимое разрешение (Permission) в файле AndroidManifest.xml для возможности отправки SMS сообщений. Для этого в среде разработки Eclipse открываем файл AndroidManifest.xml, переходим на вкладку Permissions, нажимаем кнопку «Add…», в появившемся окне выбираем пункт «Uses Permission», нажимаем кнопку «OK», далее появиться возможность выбора разрешения, в списке находим и выбираем пункт android.permission.SEND_SMS, сохраняем наши действия. После всех манипуляций вкладка Permissions будет выглядеть следующим образом:



Теперь, для примера рассмотрим самый простой способ отправки SMS сообщения, протестировать который можно в обычном эмуляторе Android. Для этого создадим два новых виртуальных устройства, с помощью менеджера виртуальных устройств Android (Window – AVD Manager) со следующими параметрами:



В созданном нами проекте, в методе onCreate добавим следующий код:

package ru.blagin.xmppsmsgate;

import android.app.Activity;
import android.os.Bundle;
import android.telephony.SmsManager;

public class XMPPSMSGateActivity extends Activity 
{
    @Override
    public void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        SmsManager sms = SmsManager.getDefault();
        sms.sendTextMessage("5556",null,"Text SMS",null,null);
    }
}


Далее запускаем оба эмулятора Android, каждый из которых будет иметь собственный номер для проверки отправки и приема SMS сообщений, когда завершиться запуск и инициализации эмуляторов, нужно запустить наше приложение на эмуляторе с номером 5554, после запуска приложения, эмулятор с номером 5556 получит наше SMS сообщение.



Отправка SMS сообщения осуществлялась с помощью класс SmsManager, который позволяет в системе Android производить необходимые действия с SMS сообщениям. Для инициализации объекта данного класса, использовался статический метод SmsManager.getDefault(). Отправка SMS сообщения производится при помощи метода sendTextMessage, где параметрами метода являются:

destinationAddress – Номер, на который отправляется сообщение;
scAddress – Номер SMS-центра вашего оператора сотовой связи, через который происходит передача сообщения, если данный параметр имеет нулевое значение, тогда используется номер по умолчанию;
text – Текст SMS сообщения;
sentIntent – Если не нулевое значение, то в данный параметр передается объект PendingIntent, для получения сообщений о результате отправки сообщения;
deliveryIntent – Если не нулевое значение, то в данный параметр передается объект PendingIntent, для получения сообщений о результате доставки сообщения.

Эмулятор Android прекрасно справляется с возложенными на него задачами, но имеет ряд ограничений, например, проверить результат доставки сообщения на нем не возможно, для этого придется использовать реальное устройство, что и будет сделано в дальнейшем. Так же при отправке сообщения с помощью метода sendTextMessage, длина его не может превышать 160 символов. Для более длинных сообщений необходимо использовать метод sendMultipartTextMessage, который в свою очередь так же позволяет отправлять сообщения длиной менее 160 символов.
В приведенном выше примере, мы не получаем уведомлений об отправке SMS сообщения и его доставки получателю, поэтому расширим функционал приложения добавив необходимые обработки. Для этого в приложении необходимо зарегистрировать два приемника широковещательных намерений, которые будут обрабатывать необходимые намерения и выводит на экран соответствующие текстовые сообщения.

В редакторе ресурсов добавим к нашему основному окну приложения виджет TextView, для вывода информации на экран.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <TextView
        android:id="@+id/textView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" 
        android:gravity="top|left"/>
</LinearLayout>


Код приложения после внесения изменений примет следующий вид:

package ru.blagin.xmppsmsgate;

import java.util.ArrayList;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.telephony.SmsManager;
import android.widget.TextView;

public class XMPPSMSGateActivity extends Activity 
{

    TextView tv = null;
	
    String SENT      = "SMS_SENT";
    String DELIVERED = "SMS_DELIVERED";
    
    private BroadcastReceiver sent      = null;
    private BroadcastReceiver delivered = null;
    
    @Override
    public void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        //Объект TextView для вывода информации на экран
        tv = (TextView) findViewById(R.id.textView);
        
        //Регистрация широковещательного приемника: Отправка 
        IntentFilter in_sent = new IntentFilter(SENT);
        sent = new BroadcastReceiver()
        {
	@Override
	public void onReceive(Context context, Intent intent)
	{
	  tv.append(intent.getStringExtra("PARTS")+": ");
	  tv.append(intent.getStringExtra("MSG")+": ");
                switch(getResultCode())
                {
                    case Activity.RESULT_OK:
                    	tv.append("SMS Отправлено\n");
                    break;
                    case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
                    	tv.append("Общий сбой\n");
                    break;
                    case SmsManager.RESULT_ERROR_NO_SERVICE:
                    	tv.append("Нет сети\n");
                    break;
                    case SmsManager.RESULT_ERROR_NULL_PDU:
                    	tv.append("Null PDU\n");
                    break;
                    case SmsManager.RESULT_ERROR_RADIO_OFF:
                    	tv.append("Нет связи\n");
                    break;
                }
				
			}
        };
        registerReceiver(sent, in_sent);
        
        //Регистрация широковещательного приемника: Доставка 
        IntentFilter in_delivered = new IntentFilter(DELIVERED);
		delivered = new BroadcastReceiver()
        {
            @Override
            public void onReceive(Context context, Intent intent) 
            {
            	tv.append(intent.getStringExtra("PARTS")+": ");
				tv.append(intent.getStringExtra("MSG")+": ");
                switch (getResultCode())
                {
                    case Activity.RESULT_OK:
                    	tv.append("SMS Доставлено\n");
                    break;
                    case Activity.RESULT_CANCELED:
                    	tv.append("SMS Не доставлено\n");
                    break;                        
                }
            }
        };
        registerReceiver(delivered, in_delivered);
        
        SendSMS("Ваш_номер","Длинное сообщение > 160 символов.");
    }
    
    //Метод отправки SMS сообщения
    public void SendSMS(String phone, String message)
    {
    	SmsManager sms = SmsManager.getDefault();
    	
    	ArrayList<String> al_message = new ArrayList<String>();
    	al_message = sms.divideMessage(message);
    	
    	ArrayList<PendingIntent> al_piSent = new ArrayList<PendingIntent>();
    	ArrayList<PendingIntent> al_piDelivered = new ArrayList<PendingIntent>();
    	
    	for (int i = 0; i < al_message.size(); i++)
              {
    	    Intent sentIntent = new Intent(SENT);
    	    sentIntent.putExtra("PARTS", "Часть: "+i);
    	    sentIntent.putExtra("MSG", "Сообщение: "+al_message.get(i));
                  PendingIntent pi_sent = PendingIntent.getBroadcast(this, i, sentIntent,
                                            PendingIntent.FLAG_UPDATE_CURRENT);
                 al_piSent.add(pi_sent);
            
                 Intent deliveredIntent = new Intent(DELIVERED);
                deliveredIntent.putExtra("PARTS", "Часть: "+i);
                deliveredIntent.putExtra("MSG", "Сообщение: "+al_message.get(i));
                PendingIntent pi_delivered = PendingIntent.getBroadcast(this, i, deliveredIntent,
                                            PendingIntent.FLAG_UPDATE_CURRENT);
                al_piDelivered.add(pi_delivered);
	}
    	sms.sendMultipartTextMessage(phone, null, al_message, al_piSent, al_piDelivered);
    }
    
    @Override
    protected void onDestroy()
    {
    	if(sent != null)
    		unregisterReceiver(sent);
    	if(delivered != null)
    		unregisterReceiver(delivered);
    	super.onDestroy();
    }
}


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



2. Создание службы для работы по протоколу XMPP

Теперь необходимо добавим к нашему приложению возможность взаимодействия по протоколу XMPP. Для этих целей создадим службу (Service) которая будет работать в фоновом режиме. Служба при помощи библиотеки SMACK, будет принимать и обрабатывать сообщения. Далее с помощью широковещательных намерений данные из полученного сообщения буду передаваться в основной класс приложения, для вывода на экран и последующей передаче через SMS.

Для того чтобы добавить службу в наше приложение, необходимо определить для нее класс реализации, для этого в среде разработки Eclipse, необходимо нажать правой кнопки мыши по имени пакета проекта, в появившемся меню выбрать пункт New и Class, как показано на рисунке:



В появившемся окне заполняем необходимые пункты, указанные на изображении и нажимаем кнопку «Finish»:



После этих действий в проекте появиться реализация класса, далее необходимо зарегистрировать класс службы в файле AndroidManifest.xml, для этого открываем файл в среде разработки Eclipse, переходим на вкладку Application и в разделе Application Nodes нажимаем кнопку «Add», в появившемся окне выбираем пункт Service и нажимаем кнопку «OK». После необходимо указать имя класса службы, после всех манипуляций вкладка Application будет выглядеть следующим образом:



Теперь добавим еще одно разрешение для приложения, чтобы оно могло выходить в Интернет, для этого проделайте те же действия что и выше для разрешения по отправке SMS сообщения, только в этот раз выберите android.permission.INTERNET.

Следующим шагом будет добавление библиотеки SMACK к приложению, скачайте ее по адресу http://code.google.com/p/asmack/ сохраните в папке проекта, далее откройте свойства проекта, для этого нажмите правой кнопкой мыши на имени проекта в среде разработки Eclipse, в появившемся меню выберите пункт Properties. В появившемся окне настроек проекта, выберите в левом списке, пункт Java Build Path, после нажмите кнопку «Add External JARs…», найдите ранее сохраненную библиотеку в папке проекта и добавьте ее. После добавления внешней библиотеки окно настроек примет следующий вид:



Ниже приведен исходный код службы, как видно в методе onCreate создается отдельный поток, в котором происходит основная работа по взаимодействию по протоколу XMPP при помощи библиотеки SMACK. Полученные сообщения, а так же иные состояние службы передаются при помощи отправки широковещательных намерений.

package ru.blagin.xmppsmsgate;

import java.util.Collection;

import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ChatManager;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

public class XMPPSMSGateService extends Service
{
	private ConnectionConfiguration connConfig;
    private XMPPConnection connection;
    
	Thread th = null;
	Intent in = new Intent("SMSGate_Service");
	
	@Override
	public IBinder onBind(Intent arg0){return null;}
	@Override
	public int onStartCommand(Intent intent, int flags, int startId){return Service.START_STICKY;}
	@Override
	public void onCreate() 
	{
	    super.onCreate();
	    th= new Thread()
	    {
	    	public void run()
			{
           	 	in.putExtra("Message","The service is started");
                sendBroadcast(in);
                connConfig = new ConnectionConfiguration(/*domen*/,5222,/*server*/);
				SASLAuthentication.supportSASLMechanism("PLAIN");
				connConfig.setCompressionEnabled(false);
				connConfig.setSASLAuthenticationEnabled(true);
				connection = new XMPPConnection(connConfig);
				try
				{
	           	 	in.putExtra("Message","Connect to the XMPP server");
	                sendBroadcast(in);
					connection.connect();
					in.putExtra("Message","Login into the XMPP server");
	                sendBroadcast(in);
					connection.login(/*login*/,/*password*/);
					if(connection.isConnected())
					{
						in.putExtra("Message","SMS Gate online.");
		                sendBroadcast(in);
					}else
					{
						in.putExtra("Message","SMS Gate offline.");
		                sendBroadcast(in);
					}
					Presence presence = new Presence(Presence.Type.available);
					presence.setStatus("SMS Gate");
					presence.setPriority(30);
					connection.sendPacket(presence);
					PacketFilter filter = new AndFilter(new PacketTypeFilter(Message.class));
					PacketListener myListener = new PacketListener() 
					{
						public void processPacket(Packet packet) 
						{
							if(packet instanceof Message) 
							{
								Message message    = (Message) packet;
								String messageBody = message.getBody();
								String JID         = message.getFrom();
								if(messageBody == null)
								{
									messageBody = "";
									Collection<Message.Body> bodies = message.getBodies();
									for(Message.Body r:bodies){messageBody += r.getMessage();}
								}
								if(messageBody.equals("ping")){sendMessage(JID,"pong");}
								in.putExtra("Message",messageBody);
								sendBroadcast(in);
							}
						}
					};
					connection.addPacketListener(myListener, filter);
					while(connection.isConnected())
					{
						try{Thread.sleep(1000);}catch(Exception e){Log.e(this.getClass().getName(),e.getMessage());}							 
					}
					 
				}catch(Exception e)
				{
					Log.e(this.getClass().getName(),e.getMessage());
					in.putExtra("Message","ERROR: "+e.getMessage());
					sendBroadcast(in);
				}
			}
	    	public void sendMessage(String to, String message)
			 {
				 if(!message.equals(""))
				 {
					 ChatManager chatmanager = connection.getChatManager();
					 Chat newChat = chatmanager.createChat(to, null);
					 try{newChat.sendMessage(message);}
					 catch(Exception e)
					 {Log.e(this.getClass().getName(),e.getMessage());}
				 }
			 }
	    };
	    th.start();
	}
	@Override
	public void onDestroy() 
	{
		if(connection.isConnected()){connection.disconnect();th = null;}
	    in.putExtra("Message","The service is stopped");
        sendBroadcast(in);
	}
}


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

IntentFilter filter = new IntentFilter();
filter.addAction("SMSGate_Service");
service = new BroadcastReceiver() 
{
	@Override
	public void onReceive(Context context, Intent intent) 
	{
		if(intent.getAction().equals("SMSGate_Service"))
		{
			String message = intent.getStringExtra("Message"); 
			tv.append(message+"\n");
			int i = message.indexOf("@");
			if(i != -1)
			{
				String phone = message.substring(0, i);
				if(phone.length() != 0)
				{
					String text = message.substring(i+1,message.length());
					tv.append("Sending SMS...\n");
					SendSMS(phone,text);
							
				}else{/*phone: 0*/}
			}else{/*not sms message*/}
		}
    }
};
registerReceiver(service, filter);


Как видно из приведенного кода, для отправки SMS, обрабатываются сообщения, которые имеют тип номер_телефон@текст_сообщения, например:

5556@Текст сообщения

Для запуска службы, в основном классе приложения в метод onCreate, добавим строку:
startService(new Intent(this,XMPPSMSGateService.class));

Для остановки службы, в основном классе приложения в методе onDestroy, добавим строку:
stopService(new Intent(this,XMPPSMSGateService.class));

Теперь попробуем запустить приложение в эмуляторе и через любой IM-клиент отправим сообщение определенного типа. Результат показан на изображении:



Заключение

Данная статья является ознакомительной и рассчитана на начинающих программистов, коим являюсь сам. Созданное приложение имеет ряд недостатков и ограничений, например отправка SMS сообщения будет осуществляться только когда главное окно приложения активно. Отсутствует проверка на наличие доступа в Интернет, так же если выход в Интернет осуществляется только через WI-FI, то когда устройство переходит в спящий режим, происходит отключение WI-FI, для экономии заряда батареи. Данную проблему можно избежать при помощи приложения Wi-Fi Keep Alive либо добавив данный функционал к приложению самостоятельно. Отсутствует ведение журнала принятых и отправленных сообщений.

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

Исходный код приложения.

Список используемой литературы
  1. Алексей Голощапов, Google Android. Программирование для мобильных устройств, 2012 г.
  2. Алексей Голощапов, Google Android. Системные компоненты и сетевые коммуникации, 2012 г.
  3. XMPP-бот на Java с использованием Smack API (esin).
Tags:
Hubs:
+43
Comments 19
Comments Comments 19

Articles