first commit

This commit is contained in:
Hamza-Ayed
2026-05-23 16:17:20 +03:00
commit 2bbaa1ee16
195 changed files with 11126 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.intaleq.flashcall'
compileSdk 35
defaultConfig {
applicationId "com.intaleq.flashcall"
minSdk 26
targetSdk 35
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.work:work-runtime-ktx:2.10.0'
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
implementation 'androidx.lifecycle:lifecycle-service:2.8.7'
implementation 'com.google.code.gson:gson:2.11.0'
// The Kotlin plugin automatically adds the correct stdlib version.
// Invalid version 2.3.10 was removed.
}

View File

@@ -0,0 +1 @@
android.useAndroidX=true

34
caller-app/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,34 @@
# Retrofit
-keepattributes Signature
-keepattributes *Annotation*
-keep class retrofit2.** { *; }
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
# Gson
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.google.gson.** { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Keep data classes for Gson serialization
-keep class com.intaleq.flashcall.PendingTask { *; }
-keep class com.intaleq.flashcall.CallDoneRequest { *; }
-keep class com.intaleq.flashcall.RegisterRequest { *; }
-keep class com.intaleq.flashcall.ApiResponse { *; }
# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
# Kotlin Coroutines
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembers class kotlinx.coroutines.** {
volatile <fields>;
}

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="Flash OTP Caller"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
android:usesCleartextTraffic="false">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".CallerService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Automation of flash call verification for OTP purposes" />
</service>
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,75 @@
package com.intaleq.flashcall
import com.google.gson.annotations.SerializedName
import retrofit2.http.*
// --- Data Models ---
data class PendingTask(
@SerializedName("task_id") val taskId: Int?,
@SerializedName("phone") val phone: String?,
@SerializedName("caller_id") val callerId: String?,
@SerializedName("otp") val otp: String?,
@SerializedName("timeout_seconds") val timeoutSeconds: Int?
)
data class CallDoneRequest(
@SerializedName("task_id") val taskId: Int,
@SerializedName("device_id") val deviceId: String,
@SerializedName("app_key") val appKey: String,
@SerializedName("result") val result: String
)
data class RegisterRequest(
@SerializedName("device_id") val deviceId: String,
@SerializedName("phone_number") val phoneNumber: String,
@SerializedName("sim_slot") val simSlot: Int,
@SerializedName("app_key") val appKey: String
)
data class ApiResponse(
@SerializedName("success") val success: Boolean,
@SerializedName("message") val message: String?,
@SerializedName("device_id") val deviceId: String?
)
// --- API Interface ---
interface ApiService {
/**
* Polling for a new flash call task
*/
@GET("pending-call.php")
suspend fun pendingCall(
@Query("device_id") deviceId: String,
@Query("app_key") appKey: String
): PendingTask
/**
* Polling for a new SMS task
*/
@GET("pending-sms.php")
suspend fun pendingSms(
@Query("device_id") deviceId: String,
@Query("app_key") appKey: String
): PendingTask
/**
* Reporting the result of a flash call
*/
@POST("call-done.php")
suspend fun callDone(@Body request: CallDoneRequest): ApiResponse
/**
* Reporting the result of an SMS
*/
@POST("sms-done.php")
suspend fun smsDone(@Body request: CallDoneRequest): ApiResponse
/**
* Initial registration of the device
*/
@POST("register-device.php")
suspend fun registerDevice(@Body request: RegisterRequest): ApiResponse
}

View File

@@ -0,0 +1,26 @@
package com.intaleq.flashcall
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
intent.action == "android.intent.action.QUICKBOOT_POWERON") {
val prefs = context.getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE)
val isRegistered = prefs.getBoolean("is_registered", false)
if (isRegistered) {
val serviceIntent = Intent(context, CallerService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,78 @@
package com.intaleq.flashcall
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.telecom.TelecomManager
import android.telephony.TelephonyManager
import kotlinx.coroutines.delay
import java.lang.reflect.Method
class FlashCallManager(private val context: Context) {
/**
* Make a flash call: dial the number, wait 2500ms, then hang up.
* Returns result: "success", "failed", "busy", "no_answer"
*/
suspend fun makeFlashCall(phone: String): String {
return try {
// Place the call
android.util.Log.d("CallerService", "Initiating flash call to $phone")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
telecomManager.placeCall(Uri.parse("tel:$phone"), null)
} else {
val callIntent = Intent(Intent.ACTION_CALL, Uri.parse("tel:$phone")).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(callIntent)
}
// Wait 1000ms — enough for 1 quick ring
delay(1000)
// Hang up the call
val hungUp = endCall()
if (hungUp) "success" else "failed"
} catch (e: SecurityException) {
"failed"
} catch (e: Exception) {
"failed"
}
}
/**
* End the current active call.
* Primary: TelecomManager (Android 9+)
* Fallback: Reflection on TelephonyManager.endCall() (Android 8)
*/
@SuppressLint("NewApi")
fun endCall(): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Android 9+: Use TelecomManager
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
telecomManager.endCall()
} else {
// Android 8: Use reflection on TelephonyManager
endCallReflection()
}
} catch (e: Exception) {
// Try reflection as fallback
endCallReflection()
}
}
@Suppress("PrivateApi")
private fun endCallReflection(): Boolean {
return try {
val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val method: Method = TelephonyManager::class.java.getDeclaredMethod("endCall")
method.invoke(telephonyManager) as? Boolean ?: false
} catch (e: Exception) {
false
}
}
}

View File

@@ -0,0 +1,316 @@
package com.intaleq.flashcall
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.*
class MainActivity : AppCompatActivity() {
private lateinit var etDeviceId: EditText
private lateinit var etPhoneNumber: EditText
private lateinit var etAppKey: EditText
private lateinit var spinnerSimSlot: Spinner
private lateinit var btnRegister: Button
private lateinit var tvStatus: TextView
private lateinit var btnToggleService: Button
private lateinit var tvLog: TextView
private lateinit var logScrollView: ScrollView
private lateinit var progressBar: ProgressBar
private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val prefs by lazy { getSharedPreferences("flash_call_prefs", Context.MODE_PRIVATE) }
private val simSlotOptions = arrayOf("SIM 0", "SIM 1")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initViews()
loadSavedData()
setupSpinner()
setupClickListeners()
requestPermissionsIfNeeded()
updateServiceStatus()
loadServiceLogs()
}
override fun onResume() {
super.onResume()
updateServiceStatus()
loadServiceLogs()
// Attach log listener
CallerService.logListener = { logLine ->
runOnUiThread {
appendLogLine(logLine)
}
}
}
override fun onPause() {
super.onPause()
CallerService.logListener = null
}
private fun initViews() {
etDeviceId = findViewById(R.id.etDeviceId)
etPhoneNumber = findViewById(R.id.etPhoneNumber)
etAppKey = findViewById(R.id.etAppKey)
spinnerSimSlot = findViewById(R.id.spinnerSimSlot)
btnRegister = findViewById(R.id.btnRegister)
tvStatus = findViewById(R.id.tvStatus)
btnToggleService = findViewById(R.id.btnToggleService)
tvLog = findViewById(R.id.tvLog)
logScrollView = findViewById(R.id.logScrollView)
progressBar = findViewById(R.id.progressBar)
}
private fun loadSavedData() {
// Device ID — generate once and persist
var deviceId = prefs.getString("device_id", null)
if (deviceId == null) {
deviceId = UUID.randomUUID().toString()
prefs.edit().putString("device_id", deviceId).apply()
}
etDeviceId.setText(deviceId)
etDeviceId.isEnabled = false
etDeviceId.isFocusable = false
// Phone number
val savedPhone = prefs.getString("phone_number", null)
if (savedPhone != null) {
etPhoneNumber.setText(savedPhone)
}
// App key
val savedAppKey = prefs.getString("app_key", null)
if (savedAppKey != null) {
etAppKey.setText(savedAppKey)
}
// SIM slot
val savedSimSlot = prefs.getInt("sim_slot", 0)
spinnerSimSlot.setSelection(savedSimSlot)
}
private fun setupSpinner() {
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, simSlotOptions)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinnerSimSlot.adapter = adapter
}
private fun setupClickListeners() {
btnRegister.setOnClickListener {
registerDevice()
}
btnToggleService.setOnClickListener {
if (CallerService.isRunning) {
stopCallerService()
} else {
startCallerService()
}
}
}
private fun requestPermissionsIfNeeded() {
if (!PermissionHelper.areAllPermissionsGranted(this)) {
PermissionHelper.requestAllPermissions(this)
}
// Request battery optimization exemption on first launch
val hasRequestedBattery = prefs.getBoolean("requested_battery_opt", false)
if (!hasRequestedBattery) {
PermissionHelper.requestBatteryOptimizationExemption(this)
prefs.edit().putBoolean("requested_battery_opt", true).apply()
}
}
private fun registerDevice() {
val phoneNumber = etPhoneNumber.text.toString().trim()
val appKey = etAppKey.text.toString().trim()
val deviceId = etDeviceId.text.toString().trim()
val simSlot = spinnerSimSlot.selectedItemPosition
if (phoneNumber.isEmpty()) {
etPhoneNumber.error = "Phone number is required"
return
}
if (appKey.isEmpty()) {
etAppKey.error = "App key is required"
return
}
if (!PermissionHelper.areAllPermissionsGranted(this)) {
Toast.makeText(this, "Please grant all permissions first", Toast.LENGTH_LONG).show()
PermissionHelper.requestAllPermissions(this)
return
}
btnRegister.isEnabled = false
progressBar.visibility = View.VISIBLE
mainScope.launch {
try {
RetrofitClient.setAppKey(appKey)
val response = RetrofitClient.apiService.registerDevice(
RegisterRequest(
deviceId = deviceId,
phoneNumber = phoneNumber,
simSlot = simSlot,
appKey = appKey
)
)
if (response.success) {
// Save registration data
prefs.edit()
.putBoolean("is_registered", true)
.putString("phone_number", phoneNumber)
.putString("app_key", appKey)
.putInt("sim_slot", simSlot)
.apply()
Toast.makeText(this@MainActivity, "Device registered successfully!", Toast.LENGTH_SHORT).show()
appendLogLine("Device registered: $deviceId")
} else {
val message = response.message ?: "Registration failed"
Toast.makeText(this@MainActivity, message, Toast.LENGTH_LONG).show()
appendLogLine("Registration failed: $message")
}
} catch (e: Exception) {
Toast.makeText(this@MainActivity, "Error: ${e.message}", Toast.LENGTH_LONG).show()
appendLogLine("Registration error: ${e.message}")
} finally {
btnRegister.isEnabled = true
progressBar.visibility = View.GONE
}
}
}
private fun startCallerService() {
if (!PermissionHelper.areAllPermissionsGranted(this)) {
Toast.makeText(this, "Please grant all permissions first", Toast.LENGTH_LONG).show()
PermissionHelper.requestAllPermissions(this)
return
}
val isRegistered = prefs.getBoolean("is_registered", false)
if (!isRegistered) {
Toast.makeText(this, "Please register the device first", Toast.LENGTH_LONG).show()
return
}
val serviceIntent = Intent(this, CallerService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
// Update UI immediately (optimistic)
tvStatus.text = "Service Starting... ⏳"
tvStatus.setTextColor(getColor(android.R.color.holo_orange_dark))
btnToggleService.text = "Stop Service"
appendLogLine("Service start requested")
}
private fun stopCallerService() {
val serviceIntent = Intent(this, CallerService::class.java)
stopService(serviceIntent)
// Update UI immediately (optimistic)
tvStatus.text = "Service Stopping... ⏳"
tvStatus.setTextColor(getColor(android.R.color.holo_orange_dark))
btnToggleService.text = "Start Service"
appendLogLine("Service stop requested")
}
private fun updateServiceStatus() {
if (CallerService.isRunning) {
tvStatus.text = "Service Running ✅"
tvStatus.setTextColor(getColor(android.R.color.holo_green_dark))
btnToggleService.text = "Stop Service"
} else {
tvStatus.text = "Service Stopped ❌"
tvStatus.setTextColor(getColor(android.R.color.holo_red_dark))
btnToggleService.text = "Start Service"
}
}
private fun loadServiceLogs() {
val logs = prefs.getString("service_logs", "") ?: ""
tvLog.text = logs
logScrollView.post {
logScrollView.fullScroll(ScrollView.FOCUS_DOWN)
}
}
private fun appendLogLine(line: String) {
val current = tvLog.text.toString()
val lines = (current.split("\n") + line).takeLast(20)
tvLog.text = lines.joinToString("\n")
logScrollView.post {
logScrollView.fullScroll(ScrollView.FOCUS_DOWN)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PermissionHelper.REQUEST_CODE_PERMISSIONS) {
val allGranted = grantResults.all { it == android.content.pm.PackageManager.PERMISSION_GRANTED }
if (!allGranted) {
Toast.makeText(
this,
"Some permissions were denied. The app may not function correctly.",
Toast.LENGTH_LONG
).show()
// Show which permissions are missing
val denied = permissions.filterIndexed { index, _ ->
grantResults[index] != android.content.pm.PackageManager.PERMISSION_GRANTED
}
if (denied.isNotEmpty()) {
AlertDialog.Builder(this)
.setTitle("Permissions Required")
.setMessage("The following permissions are required for the app to work:\n\n" +
denied.joinToString("\n") { perm ->
when (perm) {
android.Manifest.permission.CALL_PHONE -> "• Phone - to make flash calls"
android.Manifest.permission.READ_PHONE_STATE -> "• Phone State - to read call status"
android.Manifest.permission.SEND_SMS -> "• SMS - to send OTP messages"
else -> "$perm"
}
} +
"\n\nPlease grant them in Settings.")
.setPositiveButton("Open Settings") { _, _ ->
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = android.net.Uri.fromParts("package", packageName, null)
startActivity(intent)
}
.setNegativeButton("Cancel", null)
.show()
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
package com.intaleq.flashcall
import android.Manifest
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
object PermissionHelper {
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CALL_PHONE,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.SEND_SMS
)
fun areAllPermissionsGranted(context: Context): Boolean {
return REQUIRED_PERMISSIONS.all { permission ->
ContextCompat.checkSelfPermission(context, permission) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
}
}
fun requestAllPermissions(activity: Activity) {
val permissionsToRequest = REQUIRED_PERMISSIONS.filter { permission ->
ContextCompat.checkSelfPermission(activity, permission) !=
android.content.pm.PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
// Show rationale for any permission that was previously denied
val shouldShowRationale = permissionsToRequest.any { permission ->
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
if (shouldShowRationale) {
// In a production app you'd show a dialog here explaining why
// For now, just request the permissions directly
ActivityCompat.requestPermissions(
activity,
permissionsToRequest.toTypedArray(),
REQUEST_CODE_PERMISSIONS
)
} else {
ActivityCompat.requestPermissions(
activity,
permissionsToRequest.toTypedArray(),
REQUEST_CODE_PERMISSIONS
)
}
}
// Also request battery optimization exemption
requestBatteryOptimizationExemption(activity)
// Request SYSTEM_ALERT_WINDOW (Draw over other apps) for Android 10+ background activity starts
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(activity)) {
try {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${activity.packageName}")
)
activity.startActivity(intent)
} catch (e: Exception) {
// Ignore if not available
}
}
}
fun requestBatteryOptimizationExemption(activity: Activity) {
val powerManager = activity.getSystemService(Context.POWER_SERVICE) as PowerManager
val packageName = activity.packageName
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
try {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:$packageName")
}
activity.startActivity(intent)
} catch (e: Exception) {
// Fallback: open battery optimization settings
try {
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
activity.startActivity(intent)
} catch (e2: Exception) {
// Cannot open settings, ignore
}
}
}
}
fun shouldShowCallPhoneRationale(activity: Activity): Boolean {
return ActivityCompat.shouldShowRequestPermissionRationale(
activity,
Manifest.permission.CALL_PHONE
)
}
fun shouldShowReadPhoneStateRationale(activity: Activity): Boolean {
return ActivityCompat.shouldShowRequestPermissionRationale(
activity,
Manifest.permission.READ_PHONE_STATE
)
}
fun shouldShowSendSmsRationale(activity: Activity): Boolean {
return ActivityCompat.shouldShowRequestPermissionRationale(
activity,
Manifest.permission.SEND_SMS
)
}
const val REQUEST_CODE_PERMISSIONS = 1001
}

View File

@@ -0,0 +1,44 @@
package com.intaleq.flashcall
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object RetrofitClient {
private const val BASE_URL = "https://otp.intaleqapp.com/api/"
private var appKey: String = ""
fun setAppKey(key: String) {
appKey = key
}
private val authInterceptor = Interceptor { chain ->
val original = chain.request()
val request = original.newBuilder()
.header("X-App-Key", appKey)
.build()
chain.proceed(request)
}
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build()
}
val apiService: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="49.59793"
android:startX="42.9492"
android:endY="92.4963"
android:endX="85.84757"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeWidth="1"
android:strokeColor="#00000000"/>
</vector>

View File

@@ -0,0 +1,220 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:background="#FAFAFA">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Device Setup Section -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:elevation="4dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Device Setup"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="12dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Device ID">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etDeviceId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:focusable="false"
android:textSize="12sp"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Phone Number">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPhoneNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="phone"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Server App Key">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAppKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SIM Slot:"
android:textSize="14sp"
android:textColor="#666666"
android:layout_marginEnd="12dp" />
<Spinner
android:id="@+id/spinnerSimSlot"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:spinnerMode="dropdown" />
</LinearLayout>
<Button
android:id="@+id/btnRegister"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="Register Device"
android:textAllCaps="false"
android:textSize="15sp"
android:backgroundTint="#4CAF50"
android:textColor="#FFFFFF" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:visibility="gone" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Service Control Section -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:elevation="4dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Service Control"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="12dp" />
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Service Stopped ❌"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="#F44336"
android:gravity="center"
android:layout_marginBottom="12dp" />
<Button
android:id="@+id/btnToggleService"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="Start Service"
android:textAllCaps="false"
android:textSize="15sp"
android:backgroundTint="#4CAF50"
android:textColor="#FFFFFF" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Live Log Section -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Live Log"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="8dp" />
<ScrollView
android:id="@+id/logScrollView"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#F5F5F5"
android:padding="8dp">
<TextView
android:id="@+id/tvLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="#555555"
android:fontFamily="monospace"
android:lineSpacingExtra="4dp" />
</ScrollView>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#4CAF50</color>
<color name="primary_dark">#388E3C</color>
<color name="primary_light">#C8E6C9</color>
<color name="accent">#4CAF50</color>
<color name="text_primary">#333333</color>
<color name="text_secondary">#666666</color>
<color name="text_hint">#999999</color>
<color name="background">#FAFAFA</color>
<color name="card_background">#FFFFFF</color>
<color name="success_green">#4CAF50</color>
<color name="error_red">#F44336</color>
<color name="log_background">#F5F5F5</color>
<color name="log_text">#555555</color>
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
</resources>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Flash OTP Caller</string>
<string name="device_setup">Device Setup</string>
<string name="service_control">Service Control</string>
<string name="device_id_hint">Device ID</string>
<string name="phone_number_hint">Phone Number</string>
<string name="app_key_hint">Server App Key</string>
<string name="sim_slot_label">SIM Slot</string>
<string name="register_button">Register Device</string>
<string name="start_service">Start Service</string>
<string name="stop_service">Stop Service</string>
<string name="service_running">Service Running ✅</string>
<string name="service_stopped">Service Stopped ❌</string>
<string name="live_log">Live Log</string>
<string name="permission_required">Permissions Required</string>
<string name="permission_rationale">This app requires Phone, SMS, and Phone State permissions to make flash calls and send OTP messages.</string>
<string name="registration_success">Device registered successfully!</string>
<string name="registration_failed">Registration failed</string>
<string name="permissions_denied">Some permissions were denied. The app may not function correctly.</string>
<string name="register_first">Please register the device first</string>
<string name="grant_permissions_first">Please grant all permissions first</string>
</resources>

15
caller-app/build.gradle Normal file
View File

@@ -0,0 +1,15 @@
buildscript {
ext.kotlin_version = '1.9.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.9.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
tasks.register('clean', Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,10 @@
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=512m
org.gradle.parallel=true
org.gradle.caching=true
# AndroidX Support
android.useAndroidX=true
android.enableJetifier=true
# Kotlin
kotlin.code.style=official

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "FlashOTPCaller"
include ':app'