|
1 | 1 | package com.httpsms.receivers |
2 | 2 |
|
| 3 | +import android.annotation.SuppressLint |
3 | 4 | import android.content.BroadcastReceiver |
4 | 5 | import android.content.Context |
5 | 6 | import android.content.Intent |
| 7 | +import android.os.Build |
| 8 | +import android.provider.CallLog |
| 9 | +import android.telephony.SubscriptionManager |
6 | 10 | import android.telephony.TelephonyManager |
| 11 | +import androidx.work.Constraints |
| 12 | +import androidx.work.Data |
| 13 | +import androidx.work.NetworkType |
| 14 | +import androidx.work.OneTimeWorkRequest |
| 15 | +import androidx.work.WorkManager |
| 16 | +import androidx.work.Worker |
| 17 | +import androidx.work.WorkerParameters |
| 18 | +import androidx.work.workDataOf |
| 19 | +import com.httpsms.Constants |
| 20 | +import com.httpsms.HttpSmsApiService |
| 21 | +import com.httpsms.Settings |
7 | 22 | import timber.log.Timber |
| 23 | +import java.time.Instant |
| 24 | +import java.time.ZoneOffset |
| 25 | +import java.time.ZonedDateTime |
| 26 | +import java.time.format.DateTimeFormatter |
8 | 27 |
|
9 | 28 |
|
10 | 29 | class PhoneStateReceiver : BroadcastReceiver() { |
11 | 30 | override fun onReceive(context: Context, intent: Intent) { |
12 | 31 | Timber.d("onReceive: ${intent.action}") |
13 | 32 | val stateStr = intent.extras!!.getString(TelephonyManager.EXTRA_STATE) |
14 | | - val subscriptionId = intent.extras!!.getString(TelephonyManager.EXTRA_SUBSCRIPTION_ID) |
| 33 | + |
| 34 | + @Suppress("DEPRECATION") |
15 | 35 | val number = intent.extras!!.getString(TelephonyManager.EXTRA_INCOMING_NUMBER) |
16 | | - Timber.w("state = [${stateStr}] number = [${number}], subscriptionID = [${subscriptionId}]") |
17 | | - val bundle = intent.extras |
18 | | - if (bundle != null) { |
19 | | - for (key in bundle.keySet()) { |
20 | | - Timber.w(key + " : " + if (bundle[key] != null) bundle[key] else "NULL") |
| 36 | + if (stateStr != "IDLE" || number == null) { |
| 37 | + Timber.d("event is not a missed call or permission is not granted state = [${stateStr}]") |
| 38 | + return |
| 39 | + } |
| 40 | + |
| 41 | + // Sleep so that the call gets added into the call log |
| 42 | + Thread.sleep(200) |
| 43 | + |
| 44 | + val lastCall = getCallLog(context, number) |
| 45 | + if (lastCall == null) { |
| 46 | + Timber.d("The call from [${number}] was not a missed call.") |
| 47 | + return |
| 48 | + } |
| 49 | + |
| 50 | + handleMissedCallEvent(context, number, lastCall) |
| 51 | + } |
| 52 | + |
| 53 | + private fun handleMissedCallEvent(context: Context, contact: String, callLog: Pair<ZonedDateTime, String>) { |
| 54 | + val (timestamp, sim) = callLog |
| 55 | + val owner = Settings.getPhoneNumber(context, sim) |
| 56 | + |
| 57 | + if (!Settings.isLoggedIn(context)) { |
| 58 | + Timber.w("[${sim}] user is not logged in") |
| 59 | + return |
| 60 | + } |
| 61 | + |
| 62 | + if (!Settings.isIncomingCallEventsEnabled(context, callLog.second)) { |
| 63 | + Timber.w("[${sim}] incoming call events is not enabled") |
| 64 | + return |
| 65 | + } |
| 66 | + |
| 67 | + val constraints = Constraints.Builder() |
| 68 | + .setRequiredNetworkType(NetworkType.CONNECTED) |
| 69 | + .build() |
| 70 | + |
| 71 | + val inputData: Data = workDataOf( |
| 72 | + Constants.KEY_MESSAGE_FROM to contact, |
| 73 | + Constants.KEY_MESSAGE_SIM to sim, |
| 74 | + Constants.KEY_MESSAGE_TO to owner, |
| 75 | + Constants.KEY_MESSAGE_TIMESTAMP to DateTimeFormatter.ofPattern(Constants.TIMESTAMP_PATTERN).format(timestamp).replace("+", "Z") |
| 76 | + ) |
| 77 | + |
| 78 | + val work = OneTimeWorkRequest |
| 79 | + .Builder(MissedCallWorker::class.java) |
| 80 | + .setConstraints(constraints) |
| 81 | + .setInputData(inputData) |
| 82 | + .build() |
| 83 | + |
| 84 | + WorkManager |
| 85 | + .getInstance(context) |
| 86 | + .enqueue(work) |
| 87 | + |
| 88 | + Timber.d("work enqueued with ID [${work.id}] for missed phone call from [${contact}] to [${owner}] in [${sim}]") |
| 89 | + } |
| 90 | + |
| 91 | + @SuppressLint("MissingPermission") |
| 92 | + private fun getSlotIndexFromSubscriptionId(context: Context, subscriptionId: Int): String { |
| 93 | + val localSubscriptionManager: SubscriptionManager = if (Build.VERSION.SDK_INT < 31) { |
| 94 | + @Suppress("DEPRECATION") |
| 95 | + SubscriptionManager.from(context) |
| 96 | + } else { |
| 97 | + context.getSystemService(SubscriptionManager::class.java) |
| 98 | + } |
| 99 | + |
| 100 | + var sim = Constants.SIM1 |
| 101 | + localSubscriptionManager.activeSubscriptionInfoList.forEach { |
| 102 | + if (it.subscriptionId == subscriptionId) { |
| 103 | + if (it.simSlotIndex > 0){ |
| 104 | + sim = Constants.SIM2 |
| 105 | + } |
21 | 106 | } |
22 | 107 | } |
| 108 | + return sim |
| 109 | + } |
| 110 | + |
| 111 | + private fun getCallLog(context: Context, phoneNumber: String): Pair<ZonedDateTime, String>? { |
| 112 | + // Specify the columns you want to retrieve from the call log |
| 113 | + val projection = arrayOf(CallLog.Calls.NUMBER, CallLog.Calls.DATE, CallLog.Calls.TYPE, CallLog.Calls.PHONE_ACCOUNT_ID) |
| 114 | + |
| 115 | + // Query the call log content provider |
| 116 | + val cursor = context.contentResolver.query( |
| 117 | + CallLog.Calls.CONTENT_URI, |
| 118 | + projection, |
| 119 | + null, |
| 120 | + null, |
| 121 | + CallLog.Calls.DATE + " DESC" // Order by date in descending order |
| 122 | + ) |
| 123 | + |
| 124 | + // Check if the cursor is not null and contains at least one entry |
| 125 | + if (cursor != null && cursor.moveToFirst()) { |
| 126 | + val number = cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.NUMBER)) |
| 127 | + if (number != phoneNumber) { |
| 128 | + Timber.w("last phone call has phone number [${number}] but the expected phone number was [${phoneNumber}]") |
| 129 | + return null |
| 130 | + } |
| 131 | + |
| 132 | + if (cursor.getInt(cursor.getColumnIndexOrThrow(CallLog.Calls.TYPE)) != CallLog.Calls.MISSED_TYPE) { |
| 133 | + Timber.w("last phone call from phone number was [${phoneNumber}] was not a missed call") |
| 134 | + return null |
| 135 | + } |
| 136 | + |
| 137 | + val date = cursor.getLong(cursor.getColumnIndexOrThrow(CallLog.Calls.DATE)) |
| 138 | + val sim = getSlotIndexFromSubscriptionId(context, cursor.getInt(cursor.getColumnIndexOrThrow(CallLog.Calls.PHONE_ACCOUNT_ID))) |
| 139 | + |
| 140 | + // Convert date to a readable format (optional) |
| 141 | + val dateString = java.text.DateFormat.getDateTimeInstance().format(date) |
| 142 | + Timber.d("missed call date is [${dateString}], SIM = [${sim}]") |
| 143 | + |
| 144 | + // Close the cursor to free up resources |
| 145 | + cursor.close() |
| 146 | + |
| 147 | + // Construct a string representing the last call |
| 148 | + return Pair(ZonedDateTime.ofInstant(Instant.ofEpochMilli(date), ZoneOffset.UTC), sim) |
| 149 | + } |
| 150 | + |
| 151 | + // Close the cursor if it's not null even if it doesn't contain any data |
| 152 | + cursor?.close() |
| 153 | + |
| 154 | + // Return null if no calls are found |
| 155 | + return null |
| 156 | + } |
| 157 | + |
| 158 | + internal class MissedCallWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { |
| 159 | + override fun doWork(): Result { |
| 160 | + Timber.i("[${this.inputData.getString(Constants.KEY_MESSAGE_SIM)}] forwarding missed call from [${this.inputData.getString(Constants.KEY_MESSAGE_FROM)}] to [${this.inputData.getString(Constants.KEY_MESSAGE_TO)}]") |
| 161 | + |
| 162 | + if (HttpSmsApiService.create(applicationContext).sendMissedCallEvent( |
| 163 | + this.inputData.getString(Constants.KEY_MESSAGE_SIM)!!, |
| 164 | + this.inputData.getString(Constants.KEY_MESSAGE_FROM)!!, |
| 165 | + this.inputData.getString(Constants.KEY_MESSAGE_TO)!!, |
| 166 | + this.inputData.getString(Constants.KEY_MESSAGE_TIMESTAMP)!!, |
| 167 | + )) { |
| 168 | + return Result.success() |
| 169 | + } |
| 170 | + |
| 171 | + return Result.retry() |
| 172 | + } |
23 | 173 | } |
24 | 174 | } |
0 commit comments