Files
flash-call-otp/caller-app/app/src/main/java/com/intaleq/flashcall/CallerService.kt
2026-05-23 16:17:20 +03:00

327 lines
11 KiB
Kotlin

package com.intaleq.flashcall
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.content.pm.ServiceInfo
import android.telephony.SmsManager
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.*
import java.text.SimpleDateFormat
import java.util.*
class CallerService : Service() {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var flashCallManager: FlashCallManager
private var callPollerJob: Job? = null
private var smsPollerJob: Job? = null
companion object {
var isRunning = false
private set
// Callback for MainActivity to receive log messages
var logListener: ((String) -> Unit)? = null
private const val CHANNEL_ID = "flash_call_service_channel"
private const val NOTIFICATION_ID = 1001
}
private val notificationManager by lazy {
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
override fun onCreate() {
super.onCreate()
isRunning = true
flashCallManager = FlashCallManager(this)
createNotificationChannel()
// startForeground(NOTIFICATION_ID, buildNotification("Flash OTP Caller — Active"))
// ... inside onCreate() ...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(
NOTIFICATION_ID,
buildNotification("Flash OTP Caller — Active"),
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
)
} else {
startForeground(NOTIFICATION_ID, buildNotification("Flash OTP Caller — Active"))
}
// Register SMS hardware status receiver
val filter = android.content.IntentFilter("SMS_SENT")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(smsReceiver, filter, Context.RECEIVER_EXPORTED)
} else {
registerReceiver(smsReceiver, filter)
}
startPolling()
addLog("Service started")
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
callPollerJob?.cancel()
smsPollerJob?.cancel()
serviceScope.cancel()
try {
unregisterReceiver(smsReceiver)
} catch (e: Exception) {
// Ignore if not registered
}
isRunning = false
addLog("Service stopped")
}
private fun startPolling() {
callPollerJob = serviceScope.launch {
while (isActive) {
try {
pollCallTask()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
addLog("Call poll error: ${e.message}")
}
delay(3000)
}
}
smsPollerJob = serviceScope.launch {
while (isActive) {
try {
pollSmsTask()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
addLog("SMS poll error: ${e.message}")
}
delay(3000)
}
}
}
private suspend fun pollCallTask() {
val prefs = getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
val deviceId = prefs.getString("device_id", null) ?: return
val appKey = prefs.getString("app_key", null) ?: return
RetrofitClient.setAppKey(appKey)
val api = RetrofitClient.apiService
val task = try {
val result = api.pendingCall(deviceId, appKey)
addLog("Call poll OK: taskId=${result.taskId}")
result
} catch (e: retrofit2.HttpException) {
addLog("Call poll HTTP ${e.code()}: ${e.message()}")
return
} catch (e: Exception) {
addLog("Call poll net error: ${e.javaClass.simpleName}: ${e.message}")
return
}
val taskId = task.taskId ?: return
val phone = task.phone ?: return
addLog("Call task #$taskId$phone")
val result = flashCallManager.makeFlashCall(phone)
addLog("Call #$taskId result: $result")
// Report result back to server
try {
api.callDone(
CallDoneRequest(
taskId = taskId,
deviceId = deviceId,
appKey = appKey,
result = result
)
)
addLog("Call #$taskId reported: $result")
} catch (e: Exception) {
addLog("Call #$taskId report failed: ${e.message}")
}
updateNotification("Last call: #$taskId$result")
}
private suspend fun pollSmsTask() {
val prefs = getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
val deviceId = prefs.getString("device_id", null) ?: return
val appKey = prefs.getString("app_key", null) ?: return
RetrofitClient.setAppKey(appKey)
val api = RetrofitClient.apiService
val task = try {
val result = api.pendingSms(deviceId, appKey)
addLog("SMS poll OK: taskId=${result.taskId}")
result
} catch (e: retrofit2.HttpException) {
addLog("SMS poll HTTP ${e.code()}: ${e.message()}")
return
} catch (e: Exception) {
addLog("SMS poll net error: ${e.javaClass.simpleName}: ${e.message}")
return
}
val taskId = task.taskId ?: return
val phone = task.phone ?: return
val otp = task.otp ?: return
addLog("SMS task #$taskId$phone (OTP: $otp)")
val result = sendSms(phone, otp)
addLog("SMS #$taskId result: $result")
// Report result back to server
try {
api.smsDone(
CallDoneRequest(
taskId = taskId,
deviceId = deviceId,
appKey = appKey,
result = result
)
)
addLog("SMS #$taskId reported: $result")
} catch (e: Exception) {
addLog("SMS #$taskId report failed: ${e.message}")
}
updateNotification("Last SMS: #$taskId$result")
}
private fun sendSms(phone: String, otp: String): String {
return try {
val message = "رمز التحقق في تطبيق انطلق هو: $otp"
// Format phone to local Jordan format if needed (+962 -> 0)
val formattedPhone = if (phone.startsWith("+962")) {
val suffix = phone.substring(4)
if (suffix.startsWith("0")) suffix else "0$suffix"
} else {
phone
}
addLog("SMS sending to $formattedPhone...")
// Create a PendingIntent to track if SMS was sent (Explicit intent for Android 14+)
val sentIntent = android.app.PendingIntent.getBroadcast(
this, 0,
Intent("SMS_SENT").apply { setPackage(packageName) },
android.app.PendingIntent.FLAG_IMMUTABLE
)
val smsManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getSystemService(SmsManager::class.java)
} else {
@Suppress("DEPRECATION")
SmsManager.getDefault()
}
smsManager.sendTextMessage(formattedPhone, null, message, sentIntent, null)
addLog("SMS handed to OS for $formattedPhone")
"success"
} catch (e: SecurityException) {
addLog("SMS SecurityException: ${e.message}")
"failed"
} catch (e: Exception) {
addLog("SMS Exception: ${e.javaClass.simpleName}: ${e.message}")
"failed"
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Flash OTP Caller Service",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Keeps the flash call service running"
setShowBadge(false)
}
notificationManager.createNotificationChannel(channel)
}
}
private fun buildNotification(text: String): Notification {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Flash OTP Caller")
.setContentText(text)
.setSmallIcon(android.R.drawable.ic_menu_call)
.setOngoing(true)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
private fun updateNotification(text: String) {
try {
val notification = buildNotification(text)
notificationManager.notify(NOTIFICATION_ID, notification)
} catch (e: Exception) {
// Ignore notification update errors
}
}
private fun addLog(message: String) {
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
val logLine = "[$timestamp] $message"
// Also log to system Logcat for Android Studio visibility
android.util.Log.d("CallerService", logLine)
// Save to SharedPreferences for MainActivity to read
val prefs = getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
val existingLogs = prefs.getString("service_logs", "") ?: ""
val logs = (existingLogs.split("\n") + logLine).takeLast(20)
prefs.edit().putString("service_logs", logs.joinToString("\n")).apply()
// Notify listener if attached
logListener?.invoke(logLine)
}
private val smsReceiver = object : android.content.BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "SMS_SENT") {
val result = when (resultCode) {
android.app.Activity.RESULT_OK -> "SUCCESS (Hardware confirmed)"
android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE -> "FAILED (Generic/No Credit)"
android.telephony.SmsManager.RESULT_ERROR_NO_SERVICE -> "FAILED (No Cell Service)"
android.telephony.SmsManager.RESULT_ERROR_NULL_PDU -> "FAILED (Null PDU)"
android.telephony.SmsManager.RESULT_ERROR_RADIO_OFF -> "FAILED (Radio Off/Airplane Mode)"
else -> "FAILED (Unknown code: $resultCode)"
}
addLog("SMS Hardware Result: $result")
}
}
}
}