Web-to-Phone SMS: Send Texts Using My Number

Introduction

Technology today offers us endless possibilities to connect and communicate. For this project, I wanted to leverage those possibilities by building a Django-based web app that communicates in real-time with an Android mobile application. The core idea was simple yet impactful: use WebSockets to facilitate instant messaging and direct SMS communication right from a web interface. My goal was to create an efficient, intuitive way to connect backend capabilities directly with mobile users, making interactions seamless and immediate.

How It Works: Django Backend

The backend relies on Django Channels to handle WebSocket connections. Django Channels allows the web app to manage asynchronous, event-driven interactions smoothly. This enables real-time communication, pushing data instantly from the backend straight to connected Android clients.

WebSocket Handling with Django Channels

The heart of the backend is the ChatConsumer class. It's straightforward but powerful, handling connections, messaging, and even a simple heartbeat to ensure connections stay alive and healthy:

  • Managing connections: It adds and removes client connections to a shared group (events).
  • Heartbeat mechanism: Responds to simple "ping" messages to keep connections active.
  • SMS messaging: Listens for and sends structured SMS messages to clients.

Here's a peek into the consumer code:

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        async_to_sync(self.channel_layer.group_add)("events", self.channel_name)
        self.accept()

    def disconnect(self, close_code):
        async_to_sync(self.channel_layer.group_discard)("events", self.channel_name)
        self.close()

    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message_type = text_data_json.get("type")

        if message_type == "ping":
            print("Received ping")
            sys.stdout.flush()  # This ensures the print statement is flushed to the console immediately
            self.send(text_data=json.dumps({"type": "pong"}))
        else:
            message = text_data_json.get("message")
            print(f"Received message: {message}")
            sys.stdout.flush()  # This ensures the print statement is flushed to the console immediately

    def send_sms(self, event):
        phone_number = event["phone"]
        message = event["message"]
        # Construct the message to send to the client
        response = {"type": "message", "phone": phone_number, "message": message}
        # Send the response to the client
        self.send(text_data=json.dumps(response))

Sending Messages from Django Views

The web interface makes use of standard Django views to push SMS messages through WebSockets:

def sms(request):
    if request.method == "POST":
        phone_number = request.POST.get("phone_number")
        message = request.POST.get("message")

        layer = get_channel_layer()
        async_to_sync(layer.group_send)(
            "events",
            {
                "type": "send_sms",  # The method in the consumer to handle the message
                "phone": phone_number,
                "message": message,
            },
        )

    return render(request, "sms.html")

With this setup, I can trigger SMS messages directly from the Django app to the connected Android devices in real time.

Android Client App

On the Android side, written in Kotlin, the app connects to the Django backend through WebSockets. It listens for instructions from Django, and when a message arrives, it automatically sends an SMS.

WebSocket Integration

The Android app uses Java’s WebSocket library to maintain a live connection. It includes heartbeat logic to maintain a stable connection, automatically reconnecting when necessary:

private fun connectWebSocket() {
        webSocketClient = object : WebSocketClient(serverUri) {
            override fun onOpen(handshakedata: ServerHandshake?) {
                startHeartbeat()
                runOnUiThread {
                    updateLog("Connected to the WebSocket server.")
                }
            }

            override fun onMessage(message: String?) {
                message?.let {
                    runOnUiThread {
                        val data = JSONObject(it)
                        if (data.optString("type") == "pong") {
                            runOnUiThread {
                                updateLog("Received a Pong")
                            }
                            missedHeartbeats = 0 // Reset missed heartbeats counter
                        } else {
                            val phoneNumber = data.optString("phone", "7577442518")
                            val chatMessage = data.optString("message", "Unknown message")
                            sendSms(phoneNumber, chatMessage) // Send SMS with chat message
                            runOnUiThread {
                                updateLog("Message received: $chatMessage for $phoneNumber")
                            }
                        }
                    }
                }
            }

            override fun onClose(code: Int, reason: String?, remote: Boolean) {
                handler.removeCallbacks(heartbeatRunnable) // Stop heartbeats
                runOnUiThread {
                    updateLog("Disconnected from WebSocket server.")
                }
                reconnectWebSocket()
            }

            override fun onError(ex: Exception?) {
                runOnUiThread {
                    updateLog("Error: ${ex?.message}")
                }
                reconnectWebSocket()
            }
        }
        webSocketClient.connect()
    }

Automated SMS Messaging

When the app gets instructions from Django, it securely sends out an SMS:

private fun sendSms(phoneNumber: String, message: String) {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED) {
            val smsManager = getSystemService(SmsManager::class.java)
            smsManager.sendTextMessage(phoneNumber, null, message, null, null)
        }
    }

Keeping Connections Alive: Heartbeat and Auto-Reconnect

The app maintains its connection by regularly sending ping messages. If the connection drops, the app automatically attempts to reconnect:

private val heartbeatRunnable = object : Runnable {
        override fun run() {
            if (missedHeartbeats >= maxMissedHeartbeats) {
                webSocketClient.close()
            } else if (webSocketClient.isOpen) {
                updateLog("Sending a Ping")
                sendPing()
                missedHeartbeats++
                handler.postDelayed(this, heartbeatInterval)
            }
        }
    }

Wrapping It Up

This project was all about using the power of real-time WebSocket connections to make backend and mobile interactions seamless. By integrating Django Channels with Android's native SMS capabilities, the app provides a robust, efficient, and user-friendly communication channel. It’s a practical example of how combining backend logic and mobile tech can lead to useful solutions that improve connectivity and communication.

Sources: