up versions, remove legacy callblocker app
@@ -1,2 +0,0 @@
|
||||
/build
|
||||
/bin
|
||||
@@ -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>
|
||||
|
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>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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")
|
||||
|
||||