up versions, remove legacy callblocker app

This commit is contained in:
2026-07-20 19:56:38 +03:00
parent b28eb4f662
commit 5c77892fac
46 changed files with 26 additions and 1118 deletions
-2
View File
@@ -1,2 +0,0 @@
/build
/bin
-46
View File
@@ -1,46 +0,0 @@
import com.android.build.api.dsl.ApplicationExtension
plugins {
alias(libs.plugins.convention.android.app)
alias(libs.plugins.convention.compose)
alias(libs.plugins.convention.koin)
}
extensions.getByType<ApplicationExtension>().apply {
namespace = "ru.shadowsparky.callblocker"
defaultConfig {
applicationId = namespace
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = true
}
}
buildFeatures {
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.datastore.preferences)
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
implementation(libs.activity.compose)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.appcompat)
implementation(project(":libs:base"))
implementation(project(":libs:ui"))
debugImplementation(libs.ui.tooling)
compileOnly(libs.ui.tooling.preview.android)
compileOnly(project(":libs:android-stub"))
testImplementation(libs.koin.test)
testImplementation(libs.mockk)
testImplementation(kotlin("test-junit"))
}
@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<application
android:name=".presentation.App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CallBlocker"
android:windowSoftInputMode="adjustResize"
tools:targetApi="31">
<activity
android:name=".presentation.ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.CallBlocker">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<receiver
android:name=".presentation.RejectCallReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>
</application>
</manifest>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,24 +0,0 @@
package ru.shadowsparky.callblocker
import android.app.Application
import android.os.Build
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Module
import ru.shadowsparky.callblocker.data.LRejectCallDelegate
import ru.shadowsparky.callblocker.data.PRejectCallDelegate
import ru.shadowsparky.callblocker.domain.RejectCallDelegate
@Module
@ComponentScan
class AppModule {
@Factory
fun provideAlwaysRejectCallDelegate(app: Application): RejectCallDelegate {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
PRejectCallDelegate(app)
} else {
LRejectCallDelegate(app)
}
}
}
@@ -1,51 +0,0 @@
package ru.shadowsparky.callblocker.data
import android.content.Context
import android.database.Cursor
import android.provider.ContactsContract
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.annotation.Factory
import ru.shadowsparky.callblocker.domain.ContactEntry
import ru.shadowsparky.callblocker.domain.ContactReader
@Factory
class ContentProviderContactReader(
private val context: Context
) : ContactReader {
override suspend fun read(): Set<ContactEntry> {
val projection = arrayOf(
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER
)
return withContext(Dispatchers.IO) {
context.contentResolver.query(
ContactsContract.Data.CONTENT_URI,
projection,
null,
null,
null
).use { queryContacts(it ?: return@withContext emptySet()) }
}
}
private fun queryContacts(cursor: Cursor): Set<ContactEntry> {
if (!cursor.moveToFirst()) return emptySet()
val result = mutableSetOf<ContactEntry>()
do {
val name = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME)
val phone = cursor.getString(ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER)
if (phone != null && name != null) {
result.add(ContactEntry(name, phone))
}
} while (cursor.moveToNext())
return result
}
private fun Cursor.getString(column: String): String? {
val idx = getColumnIndex(column)
if (idx < 0) return null
return getString(idx)
}
}
@@ -1,28 +0,0 @@
package ru.shadowsparky.callblocker.data
import android.app.Application
import android.content.Context
import android.telephony.TelephonyManager
import android.util.Log
import com.android.internal.telephony.ITelephony
import ru.shadowsparky.callblocker.domain.RejectCallDelegate
class LRejectCallDelegate(private val app: Application) : RejectCallDelegate {
override suspend fun reject() {
getTeleService()?.endCall()
}
private fun getTeleService(): ITelephony? {
val tm = app.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
try {
val c = Class.forName(tm.javaClass.name)
val m = c.getDeclaredMethod("getITelephony")
m.isAccessible = true
return m.invoke(tm) as ITelephony
} catch (e: Exception) {
Log.e("LRejectCallDelegate", "err", e)
}
return null
}
}
@@ -1,39 +0,0 @@
package ru.shadowsparky.callblocker.data
import android.util.Log
import ru.shadowsparky.callblocker.BuildConfig
import ru.shadowsparky.callblocker.domain.ContactReader
import ru.shadowsparky.callblocker.domain.RejectCallDelegate
class NonContactsRejectCallDelegate(
private val contactReader: ContactReader,
private val number: String?,
private val wrapper: RejectCallDelegate
) : RejectCallDelegate {
override suspend fun reject() {
log("reject called with number $number")
if (number == null) {
log("number is null!!! exiting...")
return
}
val contacts = contactReader.read()
log("contacts found ${contacts.joinToString()}")
if (!contacts.any { it.phoneNumber == number }) {
log("calling number not found in contacts. rejecting")
wrapper.reject()
} else {
log("calling number found in contacts!")
}
}
private fun log(msg: String) {
if (BuildConfig.DEBUG) {
Log.d(TAG, msg)
}
}
companion object {
private const val TAG = "NonContactsCallDelegate"
}
}
@@ -1,20 +0,0 @@
package ru.shadowsparky.callblocker.data
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.telecom.TelecomManager
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import ru.shadowsparky.callblocker.domain.RejectCallDelegate
@RequiresApi(Build.VERSION_CODES.P)
class PRejectCallDelegate(private val context: Context) : RejectCallDelegate {
@SuppressLint("MissingPermission") // пофиг
@Suppress("DEPRECATION") // еще больше пофиг, пока работает будем использовать!
override suspend fun reject() {
Log.e("PRejectCallDelegate", "reject called", RuntimeException())
context.getSystemService<TelecomManager>()?.endCall()
}
}
@@ -1,44 +0,0 @@
package ru.shadowsparky.callblocker.data
import org.koin.core.annotation.Factory
import ru.shadowsparky.callblocker.domain.ContactReader
import ru.shadowsparky.callblocker.domain.RejectCallDelegate
import ru.shadowsparky.callblocker.domain.RejectPolicy
import ru.shadowsparky.callblocker.domain.RejectPolicyManager
@Factory
class PolicyBasedRejectDelegateFactory(
private val rejectPolicyManager: RejectPolicyManager,
private val contactReader: ContactReader,
private val rejectDelegate: RejectCallDelegate
) {
fun create(callingNumber: String?): PolicyBasedRejectDelegate {
return PolicyBasedRejectDelegate(
rejectPolicyManager,
callingNumber,
contactReader,
rejectDelegate
)
}
}
class PolicyBasedRejectDelegate(
private val rejectPolicyManager: RejectPolicyManager,
private val callingNumber: String?,
private val contactReader: ContactReader,
private val rejectDelegate: RejectCallDelegate
) : RejectCallDelegate {
override suspend fun reject() {
val wrapper = when (rejectPolicyManager.get()) {
RejectPolicy.All -> rejectDelegate
RejectPolicy.NonContacts -> NonContactsRejectCallDelegate(
contactReader,
callingNumber,
rejectDelegate
)
RejectPolicy.None -> null
}
wrapper?.reject()
}
}
@@ -1,27 +0,0 @@
package ru.shadowsparky.callblocker.data
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Factory
import ru.shadowsparky.callblocker.domain.RejectPolicy
import ru.shadowsparky.callblocker.domain.RejectPolicyManager
import ru.shadowsparky.domain.KeyValueStorage
@Factory
class PreferenceRejectPolicyManager(
private val keyValueStorage: KeyValueStorage
) : RejectPolicyManager {
override val flow = keyValueStorage.getInt(POLICY_KEY).map {
RejectPolicy.findByCode(it ?: RejectPolicy.None.code)
}
override suspend fun set(newPolicy: RejectPolicy) {
keyValueStorage.putInt(POLICY_KEY, newPolicy.code)
}
override suspend fun get(): RejectPolicy = flow.first()
companion object {
private const val POLICY_KEY = "policy"
}
}
@@ -1,11 +0,0 @@
package ru.shadowsparky.callblocker.domain
interface ContactReader {
suspend fun read(): Set<ContactEntry>
}
data class ContactEntry(
val name: String,
val phoneNumber: String
)
@@ -1,5 +0,0 @@
package ru.shadowsparky.callblocker.domain
interface RejectCallDelegate {
suspend fun reject()
}
@@ -1,24 +0,0 @@
package ru.shadowsparky.callblocker.domain
import kotlinx.coroutines.flow.Flow
interface RejectPolicyManager {
val flow: Flow<RejectPolicy>
suspend fun set(newPolicy: RejectPolicy)
suspend fun get(): RejectPolicy
}
sealed class RejectPolicy(val code: Int) {
data object None : RejectPolicy(0)
data object All : RejectPolicy(1)
data object NonContacts : RejectPolicy(2)
companion object {
val values: Set<RejectPolicy> get() = setOf(None, All, NonContacts)
fun findByCode(code: Int): RejectPolicy {
return values.first { it.code == code }
}
}
}
@@ -1,19 +0,0 @@
package ru.shadowsparky.callblocker.presentation
import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.core.annotation.KoinApplication
import org.koin.plugin.module.dsl.startKoin
import ru.shadowsparky.callblocker.AppModule
@KoinApplication(modules = [AppModule::class])
object CallBlocker
class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin<CallBlocker> {
androidContext(this@App)
}
}
}
@@ -1,32 +0,0 @@
package ru.shadowsparky.callblocker.presentation
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.telephony.TelephonyManager
import android.util.Log
import ru.shadowsparky.callblocker.BuildConfig
class RejectCallReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val keys = intent?.extras?.keySet() ?: return
val values = keys.map { it to intent.extras?.get(it).toString() }
if (BuildConfig.DEBUG) {
Log.d("RejectCallReceiver", "start")
values.forEach {
Log.d("RejectCallReceiver", "key ${it.first}; value: ${it.second}")
}
Log.d("RejectCallReceiver", "end")
}
if (intent.action == TelephonyManager.ACTION_PHONE_STATE_CHANGED && context != null) {
val state = intent.getStringExtra(TelephonyManager.EXTRA_STATE)
if (state == TelephonyManager.EXTRA_STATE_RINGING) {
@Suppress("DEPRECATION")
val number = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER)
RejectCallWorker.start(context, number)
}
}
}
}
@@ -1,36 +0,0 @@
package ru.shadowsparky.callblocker.presentation
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import ru.shadowsparky.callblocker.data.PolicyBasedRejectDelegateFactory
import ru.shadowsparky.koin
class RejectCallWorker(
appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
private val delegateFactory by koin.inject<PolicyBasedRejectDelegateFactory>()
override suspend fun doWork(): Result {
val callingNumber = inputData.getString(NUMBER_KEY)
delegateFactory.create(callingNumber).reject()
return Result.success()
}
companion object {
private const val NUMBER_KEY = "number"
fun start(context: Context?, number: String?) {
context ?: return
val wm = WorkManager.getInstance(context)
val request = OneTimeWorkRequestBuilder<RejectCallWorker>()
.setInputData(workDataOf(NUMBER_KEY to number))
.build()
wm.enqueue(request)
}
}
}
@@ -1,42 +0,0 @@
package ru.shadowsparky.callblocker.presentation.ui
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import ru.shadowsparky.callblocker.R
import ru.shadowsparky.callblocker.domain.RejectPolicyManager
import ru.shadowsparky.koin
import ru.shadowsparky.ui.AppScaffold
class MainActivity : ComponentActivity() {
private val rejectPolicyManager by koin.inject<RejectPolicyManager>()
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val permissionRequest = newPermissionRequest()
setContent {
AppScaffold(
appBar = {
TopAppBar(title = { Text(stringResource(R.string.app_name)) })
}
) {
CompositionLocalProvider(LocalPermissionRequest provides permissionRequest) {
Box(modifier = Modifier.padding(it)) {
PolicyContent(rejectPolicyManager)
}
}
}
}
}
}
@@ -1,147 +0,0 @@
package ru.shadowsparky.callblocker.presentation.ui
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.core.content.ContextCompat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import ru.shadowsparky.callblocker.R
interface PermissionRequest {
val hasPermission: StateFlow<Boolean>
fun getTitle(): String
fun getDescription(): String
fun requestPermission()
}
abstract class MutablePermissionRequest(private val hasPermissionCallback: () -> Boolean) :
PermissionRequest {
override val hasPermission = MutableStateFlow(hasPermissionCallback())
init {
initState()
}
override fun requestPermission() {
if (!hasPermission.value) requestPermissionInternal()
}
private fun initState() {
if (!hasPermission.value) {
this.hasPermission.tryEmit(hasPermissionCallback())
}
}
protected abstract fun requestPermissionInternal()
}
class MultiplePermissionRequest(private val requests: List<PermissionRequest>) : PermissionRequest {
override val hasPermission
get() = getLatestDeclinedRequest()?.hasPermission ?: MutableStateFlow(true)
override fun getTitle(): String {
return getLatestDeclinedRequest()?.getTitle() ?: "null"
}
override fun getDescription(): String {
return getLatestDeclinedRequest()?.getDescription() ?: "null"
}
private fun getLatestDeclinedRequest(): PermissionRequest? {
return requests.firstOrNull { !it.hasPermission.value }
}
override fun requestPermission() {
getLatestDeclinedRequest()?.requestPermission()
}
}
val LocalPermissionRequest: ProvidableCompositionLocal<PermissionRequest> =
staticCompositionLocalOf { throw IllegalStateException("request must be initialized here") }
fun ComponentActivity.newPermissionRequest(): PermissionRequest {
val list = mutableListOf(
ReadContactsPermissionRequest(this),
ReadCallLogPermissionRequest(this),
newAnswerPhonePermissionRequest(),
ReadPhoneStatePermissionRequest(this),
)
return MultiplePermissionRequest(list)
}
abstract class AndroidPermissionRequest(
private val activity: ComponentActivity,
private val permission: String,
hasPermissionCallback: () -> Boolean = {
ContextCompat.checkSelfPermission(
activity,
permission
) == PackageManager.PERMISSION_GRANTED
}
) : MutablePermissionRequest(hasPermissionCallback) {
private val launcher =
activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
hasPermission.tryEmit(it)
}
override fun requestPermissionInternal() {
launcher.launch(permission)
}
protected fun getString(code: Int, vararg args: Any): String {
return activity.getString(code, args)
}
}
class ReadContactsPermissionRequest(
activity: ComponentActivity
) : AndroidPermissionRequest(activity, Manifest.permission.READ_CONTACTS) {
override fun getDescription(): String = getString(R.string.read_contacts_desc)
override fun getTitle(): String = getString(R.string.read_contacts)
}
class ReadCallLogPermissionRequest(
activity: ComponentActivity
) : AndroidPermissionRequest(activity, Manifest.permission.READ_CALL_LOG) {
override fun getDescription(): String = getString(R.string.read_call_log_desc)
override fun getTitle(): String = getString(R.string.read_call_log)
}
class ReadPhoneStatePermissionRequest(
activity: ComponentActivity
) : AndroidPermissionRequest(activity, Manifest.permission.READ_PHONE_STATE) {
override fun getDescription(): String = getString(R.string.read_phone_state_desc)
override fun getTitle(): String = getString(R.string.read_phone_state)
}
private fun ComponentActivity.newAnswerPhonePermissionRequest(): PermissionRequest {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AnswerPhoneCallPermissionRequest(this)
} else {
CallPhonePermissionRequest(this)
}
}
class CallPhonePermissionRequest(
activity: ComponentActivity
) : AndroidPermissionRequest(activity, Manifest.permission.CALL_PHONE) {
override fun getDescription(): String = getString(R.string.answer_phone_calls_desc)
override fun getTitle(): String = getString(R.string.answer_phone_calls)
}
@RequiresApi(Build.VERSION_CODES.O)
class AnswerPhoneCallPermissionRequest(
activity: ComponentActivity
) : AndroidPermissionRequest(activity, Manifest.permission.ANSWER_PHONE_CALLS) {
override fun getDescription(): String = getString(R.string.answer_phone_calls_desc)
override fun getTitle(): String = getString(R.string.answer_phone_calls)
}
@@ -1,159 +0,0 @@
package ru.shadowsparky.callblocker.presentation.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import ru.shadowsparky.callblocker.R
import ru.shadowsparky.callblocker.domain.RejectPolicy
import ru.shadowsparky.callblocker.domain.RejectPolicyManager
import ru.shadowsparky.ui.components.ListPreference
@Composable
fun PolicyContent(policyManager: RejectPolicyManager) {
val state = policyManager.flow.collectAsState(null)
if (state.value != null) {
InitializedPolicyContent(state, policyManager, LocalPermissionRequest.current)
}
}
@Preview(showBackground = true)
@Composable
fun InitializedPolicyScreenPreview() {
val state = remember { mutableStateOf<RejectPolicy>(RejectPolicy.None) }
InitializedPolicyContent(
state = state,
manager = object : RejectPolicyManager {
override val flow = MutableStateFlow<RejectPolicy>(RejectPolicy.None)
override suspend fun set(newPolicy: RejectPolicy) = flow.emit(newPolicy)
override suspend fun get(): RejectPolicy = flow.value
},
MultiplePermissionRequest(
listOf(
NoOpPermissionRequest("1"),
NoOpPermissionRequest("2"),
NoOpPermissionRequest("3")
)
)
)
}
private class NoOpPermissionRequest(private val prefix: String) : PermissionRequest {
override val hasPermission = MutableStateFlow(false)
override fun getTitle(): String {
return "$prefix noop"
}
override fun getDescription(): String {
return "$prefix noop"
}
override fun requestPermission() {
hasPermission.tryEmit(true)
}
}
@Composable
private fun InitializedPolicyContent(
state: State<RejectPolicy?>,
manager: RejectPolicyManager,
permissionRequest: PermissionRequest
) {
val coroutineScope = rememberCoroutineScope()
Column {
val items = RejectPolicy.values.associateWith { it.getTitle() }
val item = remember { mutableStateOf(state.value!!) }
val hasPermission by permissionRequest.hasPermission.collectAsState()
if (!hasPermission) {
MissingPermissionCard(
permissionRequest,
modifier = Modifier.padding(4.dp),
)
}
ListPreference(
title = stringResource(id = R.string.selected_policy),
items = items,
selectedItem = item.value,
onItemSelected = {
item.value = it
coroutineScope.launch {
manager.set(it)
}
}
)
}
}
@Composable
private fun RejectPolicy.getTitle(): String {
val code = when (this) {
RejectPolicy.All -> R.string.reject_all
RejectPolicy.NonContacts -> R.string.reject_non_contacts
RejectPolicy.None -> R.string.reject_none
}
return stringResource(code)
}
@Preview
@Composable
fun MissingPermissionCardPreview() {
MissingPermissionCard(NoOpPermissionRequest("1"))
}
@Composable
fun MissingPermissionCard(
request: PermissionRequest,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
)
) {
Column(modifier = Modifier.padding(8.dp)) {
Text(
stringResource(id = R.string.warning),
style = MaterialTheme.typography.titleMedium
)
Text(
stringResource(
id = R.string.mission_permission_text,
request.getTitle(),
request.getDescription()
)
)
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier.fillMaxWidth()
) {
OutlinedButton(
onClick = { request.requestPermission() },
) {
Text(stringResource(id = R.string.grant_permission))
}
}
}
}
}
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -1,16 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:scaleX="0.464"
android:scaleY="0.464"
android:translateX="6.432"
android:translateY="6.432">
<path
android:fillColor="@android:color/white"
android:pathData="M13,9h-2v2h2L13,9zM17,9h-2v2h2L17,9zM20,15.5c-1.25,0 -2.45,-0.2 -3.57,-0.57 -0.35,-0.11 -0.74,-0.03 -1.02,0.24l-2.2,2.2c-2.83,-1.44 -5.15,-3.75 -6.59,-6.58l2.2,-2.21c0.28,-0.27 0.36,-0.66 0.25,-1.01C8.7,6.45 8.5,5.25 8.5,4c0,-0.55 -0.45,-1 -1,-1L4,3c-0.55,0 -1,0.45 -1,1 0,9.39 7.61,17 17,17 0.55,0 1,-0.45 1,-1v-3.5c0,-0.55 -0.45,-1 -1,-1zM19,9v2h2L21,9h-2z" />
</group>
</vector>
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:tint="@android:color/white"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:scaleX="0.464"
android:scaleY="0.464"
android:translateX="6.432"
android:translateY="6.432">
<path
android:fillColor="@android:color/white"
android:pathData="M13,9h-2v2h2L13,9zM17,9h-2v2h2L17,9zM20,15.5c-1.25,0 -2.45,-0.2 -3.57,-0.57 -0.35,-0.11 -0.74,-0.03 -1.02,0.24l-2.2,2.2c-2.83,-1.44 -5.15,-3.75 -6.59,-6.58l2.2,-2.21c0.28,-0.27 0.36,-0.66 0.25,-1.01C8.7,6.45 8.5,5.25 8.5,4c0,-0.55 -0.45,-1 -1,-1L4,3c-0.55,0 -1,0.45 -1,1 0,9.39 7.61,17 17,17 0.55,0 1,-0.45 1,-1v-3.5c0,-0.55 -0.45,-1 -1,-1zM19,9v2h2L21,9h-2z" />
</group>
</vector>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -1,22 +0,0 @@
<resources>
<string name="app_name">Блокировка звонков</string>
<string name="permission_rejected">Права не выданы</string>
<string name="reject_none">Выключено</string>
<string name="reject_all">Отменять любые звонки</string>
<string name="reject_non_contacts">Отменять звонки не из контактов</string>
<string name="selected_policy">Режим работы</string>
<string name="read_contacts">чтения контактов</string>
<string name="read_contacts_desc">распознавания находится ли номер в списке контактов или нет</string>
<string name="warning">Внимание!</string>
<string name="mission_permission_text">Не выданы права для %1$s. Приложению эти права необходимы для %2$s. Пожалуйста, выдайте права, иначе приложение может работать некорректно"</string>
<string name="read_call_log_desc">просмотра номера звонящего</string>
<string name="read_call_log">просмотра информации о звонке</string>
<string name="read_phone_state_desc">получения уведомлений о звонках</string>
<string name="read_phone_state">чтение состояния телефона</string>
<string name="answer_phone_calls_desc">сброса входящего звонка</string>
<string name="answer_phone_calls">ответа на звонок</string>
<string name="grant_permission">Выдать права</string>
<string name="settings">Настройки</string>
<string name="about_app">О приложении</string>
<string name="version">Версия</string>
</resources>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.CallBlocker" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:statusBarColor">@color/black</item>
</style>
</resources>
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -30,9 +30,14 @@ class AndroidApplication : Plugin<Project> {
}
}
}
lint {
checkReleaseBuilds = false
abortOnError = false
}
buildTypes {
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("release")
}
+6 -5
View File
@@ -1,13 +1,14 @@
kotlin.code.style=official
xcodeproj=./iosApp
android.useAndroidX=true
org.gradle.jvmargs=-Xms512m -Xmx4096m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xms1g -Xmx3584m -XX:+UseParallelGC -Dfile.encoding=UTF-8
org.gradle.daemon.idletimeout=60000
org.jetbrains.compose.experimental.jscanvas.enabled=true
org.jetbrains.compose.experimental.uikit.enabled=true
android.nonTransitiveRClass=false
android.r8.strictFullModeForKeepRules=true
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.configuration-cache=true
android.uniquePackageNames=false
android.dependency.useConstraints=false
android.r8.strictFullModeForKeepRules=false
+14 -14
View File
@@ -2,21 +2,21 @@
activity-compose = "1.13.0"
appcompat = "1.7.1"
coil-compose = "3.4.0"
compose-gradle-plugin = "1.11.1"
compose-version = "1.11.0-alpha03"
coil-compose = "3.5.0"
compose-gradle-plugin = "1.12.0-beta01"
compose-version = "1.12.0-alpha03"
desugar_jdk_libs = "2.1.5"
haze = "1.7.2"
java-jwt = "4.5.2"
kotlin-version = "2.4.0"
agp = "9.2.1"
java-jwt = "4.6.0"
kotlin-version = "2.4.10"
agp = "9.3.0"
coroutines-extensions = "1.5.5"
decompose-version = "3.5.0"
image-loader = "1.10.0"
junit = "4.13.2"
lifecycleCoroutines = "2.5.0"
material = "1.11.3"
material = "1.11.4"
materialVersion = "1.14.0"
media3 = "1.10.1"
mockk = "1.14.11"
@@ -24,15 +24,15 @@ jsoup = "1.22.2"
gradle-plugin = "1.5.5"
kotlinx-coroutines-core = "1.11.0"
kotlinx-serialization-json = "1.11.0"
ktor-plugin = "3.5.0"
ktor-plugin = "3.5.1"
kxml2 = "2.4.1"
logback-classic = "1.5.32"
multiplatform-settings = "1.3.0"
postgresql = "42.7.11"
postgresql = "42.7.13"
runner = "1.7.0"
sqlite = "2.6.2"
sqlite = "2.7.0"
sqlite-driver = "2.3.2"
ui-tooling = "1.11.1"
ui-tooling = "1.11.4"
essenty = "1.3.0"
stately-common = "2.0.6"
proj-target = "37"
@@ -43,11 +43,11 @@ proj-server-java = "21"
datastore-preferences-core = "1.2.1"
ksp-version = "2.3.7"
koin-core = "4.2.2"
koin-compiler = "1.0.1"
koin-compiler = "1.0.2"
kotlin-dsl-version = "5.1.2"
coreKtx = "1.18.0"
coreKtx = "1.19.0"
workRuntimeKtx = "2.11.2"
krpc = "0.10.2"
krpc = "0.10.3"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+1 -1
View File
@@ -1,6 +1,6 @@
#Tue Jun 23 20:20:20 MSK 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-2
View File
@@ -34,8 +34,6 @@ include(":apps:vbox:client:shared")
include(":apps:vbox:client:web")
include(":apps:vbox:backend")
include(":apps:call-blocker")
include(":apps:experimentarium:androidApp")
include(":apps:experimentarium:shared")
include(":apps:experimentarium:web")