first commit
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user