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") } } } }