Táto stránka pre svoju správnu funkčnosť vyžaduje súbory cookies. Slúžia na authentifikáciu návštevníka, analýzu návštevnosti a reklamnú personalizáciu.
logo
Prihlásenie / Registrácia
mobile

Zavrieť
 

Android ako SMS Gateway

V ďalšom "voľnom pokračovaní" mini-série článkov na tému ďalšieho využitia starých mobilných telefónov s Androidom, sa tento krát pokúsime urobiť z telefónu SMS bránu.
Už je to pár týždňov od vydania článku Bezpečnostná kamera z Android telefónu > link <, kde sme si jeden staručký telefón prerobili na improvizovanú bezpečnostnú kameru a nastal čas na nejaký ďalší projekt.

Pod pojmom SMS Gateway (SMS brána) si môžeme predstaviť službu, cez ktorú je možné posielať SMS správy (napríklad SMS notifikácie) pre aplikácie / skripty, ktoré túto možnosť samé o sebe nemajú. Pravdepodobne ste sa už stretli s tým, že nejaký WEB od Vás vyžadoval overenie cez SMS, prípadne ste dostali SMS notifikáciu o transakcii na účte od Vašej banky. Tieto a podobné riešenia používajú nejakú formu SMS brány.

V tomto článku sa samozrejme nebudem venovať tomu, ako si nejakú SMS bránu predplatiť, ale opäť siahneme do šuplíka so starými telefónmi a z jedného si SMS bránu spravíme.

Toľko na úvod, pusťme sa do toho.

Popis riešenia

Vytvoríme Android aplikáciu, ktorá bude slúžiť ako server počúvajúci na určenom porte a dáta od pripojených klientov bude odosielať ako SMS správy. Takémuto telefónu môžeme nastaviť statickú IP adresu, následne sa na neho vieme pripojiť z čohokoľvek čo umožňuje takúto komunikáciu a je na lokálnej sieti (v prípade potreby je vždy možné "otvoriť port" na routry a umožniť prístup aj mimo lokálnej siete, no treba zvážiť aj riziká s tým spojené). V našom prípade budeme používať nástroj telnet, ktorý je prítomný skoro na každom PC, prípadne je ľahko doinštalovateľný (napríklad > link <). Klient odošle riadok textu s inštrukciami v tvare sms adresát text, čiže napríklad sms +421900123456 Vas overovaci kod je: 123456" a server odpovie kódom 0 v prípade, ak bola jeho požiadavka úspešne zaznamenaná. V opačnom prípade odpovie číslom chyby. Telefón musí mať samozrejme funkčnú SIM kartu.

Technické riešenie

Otvoríme si Android Studio a vytvoríme novú aplikáciu s jednoduchou Activity. Môžeme ju nazvať SMS Gateway a ako package použiť sk.codeblog.smsgateway.

Na úvod si do súboru AndroidManifest.xml zapíšeme požadované oprávenia:

Zdrojový kód:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>

...

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

    <application ...>

...

Pokračujeme s layoutom hlavnej Activity, kde si na stred umiestníme TextView, v ktorom budeme zobrazovať aktuálne informácie k činnosti aplikácie:

Zdrojový kód:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <TextView
        android:id="@+id/tv_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="Waiting..." />

</RelativeLayout>

Vytvoríme súbor Constants.java, v ktorom zadefinujeme konštanty, ktoré budeme neskôr v kóde využívať. Ako ich mená napovedajú, budeme ich používať pri predávaní informácií medzi servisom na pozadí a vizuálnou častou aplikácie.

Zdrojový kód:
package sk.codeblog.smsgateway;

public class Constants {
    public static final String CALLBACK = "SK.CODEBLOG.SMSGATEWAY.ACTIVITY_CALLBACK";
    public static final String CALLBACK_TYPE = "SK.CODEBLOG.SMSGATEWAY.ACTIVITY_CALLBACK.TYPE";
    public static final String CALLBACK_MSG = "SK.CODEBLOG.SMSGATEWAY.ACTIVITY_CALLBACK.MSG";

    public static final int CALLBACK_TYPE_INFO = 0;
    public static final int CALLBACK_TYPE_ERROR = 1;
    public static final int CALLBACK_TYPE_RESULT = 2;
}

Prvé tri konštanty využijeme ako kľúče / názvy pre hodnoty v dátach Intentu, cez ktorý bude komunikácia prebiehať a zvyšné tri sú príznakmi, ktoré budú reprezentovať informáciu o type (CALLBACK_TYPE) udalosti o ktorej chceme aplikáciu informovať.

CALLBACK_TYPE_INFO - Všeobecná informácia na zobrazenie, bez nejakého špeciálneho významu
CALLBACK_TYPE_ERROR - Chybové hlásenie, ktoré by malo byť zvýraznené (bude červenou farbou)
CALLBACK_TYPE_RESULT - Hlásenie o výsledku odoslania SMS správy, ktoré dostaneme od triedy SmsManager a podľa výsledku zobrazíme ako informáciu alebo chybu.

Ďalší súbor na vytvorenie je Functions.java, ktorý bude obsahovať metódy sendSMS a sendInfoToApp, ktoré budú v aplikácii zabezpečovať samotné odoslanie SMS správy a posielanie správ do vizuálnej časti aplikácie.

Zdrojový kód:
package sk.codeblog.smsgateway;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.telephony.SmsManager;

public class Functions {
    public static void sendSMS(Context context, String address, String body, String clientIP)
    {

        Intent i = new Intent(Constants.CALLBACK);
        i.putExtra(Constants.CALLBACK_TYPE, Constants.CALLBACK_TYPE_RESULT);
        i.putExtra(Constants.CALLBACK_MSG, "SMS from " + clientIP);

        PendingIntent pi = PendingIntent.getBroadcast(context, 0, i, android.app.PendingIntent.FLAG_ONE_SHOT);

        SmsManager smsManager = SmsManager.getDefault();
        smsManager.sendTextMessage(address, null, body, pi, null);
    }

    public static void sendInfoToApp(Context context, int callbackType, String message)
    {
        try {

            Intent i = new Intent(Constants.CALLBACK);
            i.putExtra(Constants.CALLBACK_TYPE, callbackType);
            i.putExtra(Constants.CALLBACK_MSG, message);

            PendingIntent pi = PendingIntent.getBroadcast(context, 0, i, android.app.PendingIntent.FLAG_ONE_SHOT);
            pi.send();

        } catch (PendingIntent.CanceledException e) {
            e.printStackTrace();
        }
    }
}

Metóda sendSMS používa už spomenutú triedu SmsManager, kde pomocou metódy sendTextMessage odosiela samotnú SMS správu. Jedným z jej parametrov je aj PendingIntent, ktorý bude odoslaný po vykonaný pokusu o odoslanie správy. Intent bude smerovaný na rovnaké miesto ako v metóde sendInfoToApp, avšak bude obsahovať inú informáciu o svojom type CALLBACK_TYPE.

Nasleduje CommandThread.java, ktorý bude obsahovať rovnomennú triedu, vychádzajúcu z triedy Thread a bude slúžiť na spracovanie jednotlivých požiadaviek od klientov.

Zdrojový kód:
package sk.codeblog.smsgateway;

import android.content.Context;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class CommandThread extends Thread{

    Context context;
    Socket socket;
    BufferedReader input = null;
    BufferedWriter output = null;

    public CommandThread(Context context, Socket socket)
    {
        this.context = context;
        this.socket = socket;
    }

    @Override
    public void run() {
        super.run();

        try {

            openStreams();

            String command = readFromSocket();

            if (command.startsWith("sms")) {
                String[] params = command.split(" ", 3);
                if (params.length == 3) {
                    String phoneNum = params[1];
                    String message = params[2];

                    if (!phoneNum.equals("") && !message.equals(""))
                    {
                        String clientIP = this.socket.getRemoteSocketAddress().toString();
                        Functions.sendInfoToApp(this.context , Constants.CALLBACK_TYPE_INFO, "SMS request from " + clientIP);
                        Functions.sendSMS(this.context, phoneNum, message, clientIP);
                        writeToSocket("0: Request accepted");
                    }
                    else
                    {
                        writeToSocket("3: Invalid parameter count");
                    }
                }
                else
                {
                    writeToSocket("2: Invalid parameter count");
                }
            }
            else
            {
                writeToSocket("1: Invalid command");
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            closeSocket();
        }

    }

    private void openStreams() throws IOException {
        input = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
        output = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream()));
    }

    private void closeSocket() {
        try {
            if (input != null)
                input.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            if (output != null)
                output.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            this.socket.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    private String readFromSocket() throws IOException {
        return input.readLine();
    }

    private void writeToSocket(String Message) throws IOException {
        output.write(Message + "\n");
        output.flush();
    }
}

V preťaženej metóde run si otvoríme streamy pre načítanie požadavky od klienta a samozrejme aj na našu odpoveď. Následne si cez metódu readFromSocket načítame text požiadavky. Text požiadavky skontrolujeme, či obsahuje podporovaný príkaz (v našom prípade je to len jeden príkaz a to sms) a následne skontrolujeme počet zadaných parametrov. Ak nie sú všetky podmienky splnené, použijeme metódu writeToSocket na odoslanie chybového kódu klientovi. V prípade, ak je všetko menované splnené, pošleme informáciu o požiadavke do aplikácie (Functions.sendInfoToApp(), zadáme požiadavku o odoslanie SMS (Functions.sendInfoToApp) a nakoniec oznámime úspešné vytvorenie požiadavky klientovi (writeToSocket).

Nasleduje ServiceThread.java, v ktorom si zadefinujeme pracovné vlákno servisu, ktoré bude prijímať pripojenia od klientov a posúvať ich ďalej do už definovaného vlákna CommandThread:

Zdrojový kód:
package sk.codeblog.smsgateway;

import android.content.Context;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

class ServiceThread extends Thread {

    private Context context;
    private ServerSocket serverSocket = null;

    public ServiceThread(Context context)
    {
        this.context = context;
    }

    @Override
    public void run() {

        try {
            serverSocket = new ServerSocket(1234);
            Functions.sendInfoToApp(this.context , Constants.CALLBACK_TYPE_INFO,"Listening...");

            while (!this.isInterrupted()) {
                Socket socket = serverSocket.accept();
                processSocket(socket);
            }
        }
        catch (SocketException socExcp)
        {
            String errMsg = socExcp.getMessage();
            if (errMsg.equals("Socket closed")) {

                Functions.sendInfoToApp(this.context , Constants.CALLBACK_TYPE_ERROR, "Service error: " + errMsg);

                socExcp.printStackTrace();
            }

        }
        catch (IOException e) {

            this.cancel();

            e.printStackTrace();
        }
    }

    public void cancel()
    {
        if (this.serverSocket != null)
        {
            try {
                this.interrupt();

                this.serverSocket.close();
                this.serverSocket = null;

                Functions.sendInfoToApp(this.context , Constants.CALLBACK_TYPE_INFO, "Service interrupted");
            }
            catch (Exception ex) { ex.printStackTrace();}
        }
    }

    private void processSocket(Socket socket)
    {
        CommandThread th = new CommandThread( this.context, socket);
        th.start();
    }
}

Vlákno inicializuje ServerSocket na porte 1234, pošle informáciu do MainActivity a v cykle čaká na pripojenie od klientov cez metódu accept. Pripojení klienti sú následne metódou processSocket poslaný na spracovanie v novom vlákne typu CommandThread. A nakoniec je tu metóda cancel slúžiacia na ukončenie vlákna servisu.

Výkonný kód servisu máme vytvorený, potrebujeme ešte samotný servis, pre ktorý bol vytvorený.
Vytvoríme preto súbor SMSGatewayService.java

Zdrojový kód:
package sk.codeblog.smsgateway;

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

import androidx.annotation.Nullable;

public class SMSGatewayService extends Service {
    ServiceThread serviceThread;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        serviceThread = new ServiceThread(this);
        serviceThread.start();

        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        if (serviceThread != null)
            serviceThread.cancel();
    }
}

Servis samozrejme zaregistrujeme aj do AndroidManifest.xml pred MainActivity

Zdrojový kód:
...
<service android:name=".SMSGatewayService" />
...


A ako posledný krok nám ostalo už len doplnenie kódu MainActivity.java

Zdrojový kód:
package sk.codeblog.smsgateway;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private TextView tv_status = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        try {

            Intent service = new Intent(this.getApplicationContext(), SMSGatewayService.class);
            startService(service);

            setContentView(R.layout.activity_main);

            tv_status = (TextView) findViewById(R.id.tv_status);

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Constants.CALLBACK);
        registerReceiver(callbackReceiver, intentFilter);
    }

    @Override
    protected void onPause() {
        super.onPause();

        unregisterReceiver(callbackReceiver);
    }

    private BroadcastReceiver callbackReceiver =
            new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {

                    int callbackType = intent.getIntExtra(Constants.CALLBACK_TYPE, 0);
                    String message = intent.getStringExtra(Constants.CALLBACK_MSG);
                    int textColor = Color.BLACK;

                    switch (callbackType) {
                        case Constants.CALLBACK_TYPE_ERROR: {
                            textColor = Color.RED;
                        }
                        break;
                        case Constants.CALLBACK_TYPE_RESULT: {
                            int resultCode = getResultCode();
                            if (resultCode == Activity.RESULT_OK) {
                                message += "... OK";
                            } else {
                                textColor = Color.RED;
                                message += "... Failed";
                            }
                        }
                        break;
                    }

                    tv_status.setTextColor(textColor);
                    tv_status.setText(message);
                }
            };
}

V metóde onCreate cez startService naštartujeme servis, načítame layout samotnej Activity a referenciu na tv_status, na ktorom budeme zobrazovať aktuálnu činnosť servisu. Zároven si zadefinujeme BroadcastReceiver, cez ktorý budeme prijímať "hlásenia" o činnosti servisu a následne ich zobrazovať. Pretažená metóda onReceive BroadcastReceiveru vyhodnotí typ prijatého záznamu a prípadne upraví farbu a text hlásenia, ktoré následne zobrazí cez tv_status.

Aplikáciu môžeme skompilovať a vyskúšať na vašom telefóne. Pred tým si ale musíme pozriet, akú IP adresu má pridelenú. Dá sa to zistiť v nastaveniach sietí, v časti WIFI a v podponuke rozšírené nastavenia.

Test SMS brány cez telnet

Otvoríme si príkazový riadok / terminál a zavoláme telnet s príslušnými parametrami
Zdrojový kód:
telnet xxx.xxx.xxx.xxx 1234

Po úspešnom pripojení zadáme príkaz na odoslanie sms správy

Zdrojový kód:
sms +4219xxyyyzzz text spravy

Následne by sme mali dostať odpoveď z našej SMS brány.

Celá komunikácia by mohla vyzerať nasledovne

Zdrojový kód:
codeblog@codeblog-server:~$ telnet 192.168.1.78 1234
Trying 192.168.1.78...
Connected to 192.168.1.78.
Escape character is '^]'.
sms +421900123456 Test SMS brany
0: Request accepted
Connection closed by foreign host.

Záverom

Ak sme postupovali správne, mali by sme mať funkčný prototyp SMS brány, ktorý je následne možné rozšíriť, napríklad plnohodnotným ovládaním servisu, prípadne podrobnejším zaznamenávaním aktivít servisu aj mimo MainActivity, nejakou formou autentifikácie a pod. No, to je už nad rámec toho článku.

V prípade, ak ste našli nejakú chybu alebo máte návrh na zlepšenie neváhajte zanechať komentár pod článkom.

Zdrojové kódy projektu sú k dispozícii na > link <.

Použité zdroje

Sending and Receiving Data with Sockets in android - tutorialspoint.com > link <

Codeblog
Ostatné texty v seriály

Android ako SMS Gateway 0
Android ako SMS Gateway II - Klientské aplikácie 0
Diskusia

Žiadne príspevky v diskusii.

Nový príspevok

Na prispievanie do diskusie musíte byť prihlásený.