327 lines
11 KiB
Kotlin
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")
|
|
}
|
|
}
|
|
}
|
|
} |