init projects
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.shadowsparky.callblocker.domain
|
||||
|
||||
interface ContactReader {
|
||||
|
||||
suspend fun read(): Set<ContactEntry>
|
||||
}
|
||||
|
||||
data class ContactEntry(
|
||||
val name: String,
|
||||
val phoneNumber: String
|
||||
)
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package ru.shadowsparky.callblocker.domain
|
||||
|
||||
interface RejectCallDelegate {
|
||||
suspend fun reject()
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
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.BaseScreen
|
||||
|
||||
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 {
|
||||
BaseScreen(
|
||||
appBar = {
|
||||
TopAppBar(title = { Text(stringResource(R.string.app_name)) })
|
||||
}
|
||||
) {
|
||||
CompositionLocalProvider(LocalPermissionRequest provides permissionRequest) {
|
||||
Box(modifier = Modifier.padding(it)) {
|
||||
PolicyContent(rejectPolicyManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
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)
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user