diff --git a/README.md b/README.md index f617e3c7..a69d33da 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ If the app rejects calls from contacts on Android 10, allow _Contacts_ permissio * CALL_SCREENING - block or allow call * READ_CALL_LOG - check you have called/answered the number and count times the number have called you in X minutes * READ_SMS - check you have sent a message to the number and you received a message from the number +* SEND_SMS - send a message to the number if the call is rejected * NOTIFICATION_LISTENER - find mobile numbers in messages * READ_PHONE_STATE - check on which SIM the number is calling * RECEIVE_BOOT_COMPLETED - persist clean expired numbers job across reboots diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c8d94e7f..e784c36a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,4 +83,10 @@ dependencies { ksp(libs.androidx.room.compiler) implementation(libs.google.libphonenumber) implementation(libs.guardianproject.panic) + implementation("androidx.compose.ui:ui:1.4.0") + implementation("androidx.compose.material3:material3:1.0.0-alpha13") + implementation("androidx.compose.ui:ui-tooling-preview:1.4.0") + implementation("androidx.compose.runtime:runtime-livedata:1.4.0") + implementation("androidx.activity:activity-compose:1.4.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e306151f..7416f128 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + @@ -110,5 +111,7 @@ + + \ No newline at end of file diff --git a/app/src/main/java/me/lucky/silence/MainActivity.kt b/app/src/main/java/me/lucky/silence/MainActivity.kt index f53eab68..6d0919ce 100644 --- a/app/src/main/java/me/lucky/silence/MainActivity.kt +++ b/app/src/main/java/me/lucky/silence/MainActivity.kt @@ -1,5 +1,8 @@ package me.lucky.silence +import android.Manifest +import android.app.AlertDialog +import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity @@ -10,13 +13,26 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.navigation.compose.rememberNavController import me.lucky.silence.ui.App +// Define a constant to identify the SEND_SMS permission request +private const val MY_PERMISSIONS_REQUEST_SEND_SMS = 0 + open class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) NotificationManager(this).createNotificationChannels() + + // Check and request SMS permission + if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) { + // If not, request the permission + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.SEND_SMS), MY_PERMISSIONS_REQUEST_SEND_SMS) + } + setContent { val isAndroid12OrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val colorScheme = when { @@ -30,4 +46,30 @@ open class MainActivity : ComponentActivity() { } } } -} + + // This method is called when the user responds to the permission request + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + // If the result is for the SEND_SMS permission request + MY_PERMISSIONS_REQUEST_SEND_SMS -> { + // If the permission is granted + if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { + // Show a dialog to check Settings + AlertDialog.Builder(this) + .setTitle("Check Settings") + .setMessage("You can set up an SMS notification in settings to be sent to a blocked number when it's blocked by the app.") + .setPositiveButton("OK") { dialog, _ -> + // Dismiss the dialog when the OK button is clicked + dialog.dismiss() + } + .show() + } + return + } + else -> { + // Ignore all other requests. + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/silence/NotificationManager.kt b/app/src/main/java/me/lucky/silence/NotificationManager.kt index 58076876..25ed588a 100644 --- a/app/src/main/java/me/lucky/silence/NotificationManager.kt +++ b/app/src/main/java/me/lucky/silence/NotificationManager.kt @@ -6,6 +6,8 @@ import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person +import android.telephony.SmsManager + class NotificationManager(private val ctx: Context) { companion object { @@ -43,4 +45,27 @@ class NotificationManager(private val ctx: Context) { ) } catch (_: SecurityException) {} } -} \ No newline at end of file + + +// Function to send an SMS message + fun sendSMS(phoneNumber: String, message: String) { + // Get the default instance of the SmsManager + val smsManager = SmsManager.getDefault() + // Send a text message to the provided phone number + smsManager.sendTextMessage(phoneNumber, null, message, null, null) + } + +// Function to send an SMS to a blocked call +fun smsBlockedCall(tel: String, sim: Sim?) { + // Get the SMS message from Preferences + val prefs = Preferences(ctx) + val message = prefs.smsMessage + + // If the message is not empty, send an SMS to the blocked number + if (!message.isNullOrEmpty()) { + sendSMS(tel, message) + } + + + +}} \ No newline at end of file diff --git a/app/src/main/java/me/lucky/silence/Preferences.kt b/app/src/main/java/me/lucky/silence/Preferences.kt index 893b6abc..b5f8eff9 100644 --- a/app/src/main/java/me/lucky/silence/Preferences.kt +++ b/app/src/main/java/me/lucky/silence/Preferences.kt @@ -20,6 +20,7 @@ class Preferences(ctx: Context) { const val REPEATED_BURST_TIMEOUT = "repeated_burst_timeout" const val MESSAGES = "messages" const val MESSAGES_TEXT_TTL = "messages_text_ttl" + const val SMS_MESSAGE = "sms_message" const val RESPONSE_OPTIONS = "call_screening_response_options" const val UNKNOWN_NUMBERS_CHECKED = "unknown_numbers_checked" @@ -131,6 +132,10 @@ class Preferences(ctx: Context) { var regexPattern: String? get() = prefs.getString(REGEX_PATTERN, "") set(value) = prefs.edit { putString(REGEX_PATTERN, value) } + + var smsMessage: String? + get() = prefs.getString(SMS_MESSAGE, "") + set(value) = prefs.edit { putString(SMS_MESSAGE, value) } } enum class Contact(val value: Int) { diff --git a/app/src/main/java/me/lucky/silence/screening/CallScreeningService.kt b/app/src/main/java/me/lucky/silence/screening/CallScreeningService.kt index bd2e2d2a..b4cc8bb5 100644 --- a/app/src/main/java/me/lucky/silence/screening/CallScreeningService.kt +++ b/app/src/main/java/me/lucky/silence/screening/CallScreeningService.kt @@ -113,6 +113,9 @@ class CallScreeningService : CallScreeningService() { } } notificationManager.notifyBlockedCall(tel ?: return, sim) + + // Here's where you would call smsBlockedCall + notificationManager.smsBlockedCall(tel ?: return, sim) } respondToCall(callDetails, response) } diff --git a/app/src/main/java/me/lucky/silence/ui/MainScreen.kt b/app/src/main/java/me/lucky/silence/ui/MainScreen.kt index 2d50539a..d34aba17 100644 --- a/app/src/main/java/me/lucky/silence/ui/MainScreen.kt +++ b/app/src/main/java/me/lucky/silence/ui/MainScreen.kt @@ -10,13 +10,19 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -37,6 +43,7 @@ import me.lucky.silence.ui.common.ToggleableButton @Composable fun ModuleList(modules: List) { + LazyColumn { items(modules) { module -> if ((module.getPreference != null) && (module.setPreference != null) && (module.navigation != null)) { @@ -83,6 +90,7 @@ fun MainScreen( onNavigateToSettings: () -> Unit, onNavigateToRegex: () -> Unit, ) { + fun getContactedPermissions(): Array { val contacted = prefs.contacted val permissions = mutableSetOf() @@ -196,6 +204,9 @@ fun MainScreen( setPreference = { prefs.isBlockEnabled = it }, ), ) + + + Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(R.string.app_name)) }, actions = { IconButton(onClick = onNavigateToSettings) { @@ -221,6 +232,8 @@ fun MainScreen( ) } }) + + } @Preview diff --git a/app/src/main/java/me/lucky/silence/ui/SettingsScreen.kt b/app/src/main/java/me/lucky/silence/ui/SettingsScreen.kt index 8f5fceab..018343be 100644 --- a/app/src/main/java/me/lucky/silence/ui/SettingsScreen.kt +++ b/app/src/main/java/me/lucky/silence/ui/SettingsScreen.kt @@ -1,9 +1,20 @@ package me.lucky.silence.ui +import android.app.AlertDialog import android.content.Context +import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import me.lucky.silence.ControlReceiver import me.lucky.silence.Preferences @@ -17,6 +28,7 @@ import me.lucky.silence.ui.common.Screen @Composable fun SettingsScreen(ctx: Context, prefs: Preferences, onBackPressed: () -> Boolean) { + var showSmsDialog by remember { mutableStateOf(false) } val preferenceList = listOf( Preference( getValue = { Utils.isComponentEnabled(ctx, ControlReceiver::class.java) }, @@ -73,17 +85,75 @@ fun SettingsScreen(ctx: Context, prefs: Preferences, onBackPressed: () -> Boolea }, name = R.string.settings_skip_notification, description = R.string.settings_skip_notification_description, + ), + Preference( + getValue = { prefs.smsMessage!!.isNotEmpty() }, + setValue = { isChecked -> + if (isChecked) { + showSmsDialog = true + } else { + prefs.smsMessage = "" + } + }, + name = R.string.enter_sms_message, + description = R.string.enter_sms_message_description, ) ) + Screen(title = R.string.settings, onBackPressed = onBackPressed, content = { PreferenceList(preferenceList) }) + if (showSmsDialog) { + SmsMessageDialog( + onDismiss = { showSmsDialog = false }, + onConfirm = { message -> + prefs.smsMessage = message + showSmsDialog = false + } + ) + } } + @Preview @Composable fun SettingsScreenPreview() { MaterialTheme { SettingsScreen(LocalContext.current, Preferences(LocalContext.current)) { true } } -} \ No newline at end of file + +} + +@Composable +fun SmsMessageDialog(onDismiss: () -> Unit, onConfirm: (String) -> Unit) { + val context = LocalContext.current + val input = remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { + Text(text = stringResource(id = R.string.enter_sms_message)) + }, + text = { + Column { + Text(text = stringResource(id = R.string.enter_sms_message_description)) + TextField( + value = input.value, + onValueChange = { input.value = it } + ) + } + }, + confirmButton = { + TextButton(onClick = { + onConfirm(input.value) + }) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = { onDismiss() }) { + Text(text = stringResource(id = android.R.string.cancel)) + } + } + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c01b3af..58e74c6f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,4 +86,6 @@ Back Off On + SMS Message + Enter the message to send when a call is blocked. Leave empty to not send an SMS. \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a4d74b0c..76e58826 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.3.2" +agp = "8.4.1" kotlin = "1.9.23" coreKtx = "1.12.0" junit = "4.13.2" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 645616a1..b5460a39 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jun 14 21:51:27 MSK 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME