diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 12a2735b1..a78dfea18 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -107,6 +107,10 @@
+
+
diff --git a/app/src/main/java/com/chiller3/bcr/Contact.kt b/app/src/main/java/com/chiller3/bcr/Contact.kt
index 0f310ec57..6ea0d9905 100644
--- a/app/src/main/java/com/chiller3/bcr/Contact.kt
+++ b/app/src/main/java/com/chiller3/bcr/Contact.kt
@@ -3,20 +3,48 @@ package com.chiller3.bcr
import android.Manifest
import android.content.Context
import android.net.Uri
+import android.os.Parcelable
import android.provider.ContactsContract
import androidx.annotation.RequiresPermission
+import androidx.core.database.getLongOrNull
+import androidx.core.database.getStringOrNull
import com.chiller3.bcr.output.PhoneNumber
+import kotlinx.parcelize.Parcelize
private val PROJECTION = arrayOf(
ContactsContract.PhoneLookup.LOOKUP_KEY,
ContactsContract.PhoneLookup.DISPLAY_NAME,
)
+private val PROJECTION_GROUP_MEMBERSHIP = arrayOf(
+ ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID,
+ ContactsContract.CommonDataKinds.GroupMembership.GROUP_SOURCE_ID,
+)
+
+private val CONTACT_GROUP_PROJECTION = arrayOf(
+ ContactsContract.Groups._ID,
+ ContactsContract.Groups.SOURCE_ID,
+ ContactsContract.Groups.TITLE,
+)
+
data class ContactInfo(
val lookupKey: String,
val displayName: String,
)
+sealed interface GroupLookup {
+ data class RowId(val id: Long): GroupLookup
+
+ data class SourceId(val id: String): GroupLookup
+}
+
+@Parcelize
+data class ContactGroupInfo(
+ val rowId: Long,
+ val sourceId: String,
+ val title: String,
+) : Parcelable
+
@RequiresPermission(Manifest.permission.READ_CONTACTS)
fun findContactsByPhoneNumber(context: Context, number: PhoneNumber): Iterator {
val rawNumber = number.toString()
@@ -35,7 +63,7 @@ fun findContactsByPhoneNumber(context: Context, number: PhoneNumber): Iterator
+ val indexRowId = cursor.getColumnIndexOrThrow(
+ ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID)
+ val indexSourceId = cursor.getColumnIndexOrThrow(
+ ContactsContract.CommonDataKinds.GroupMembership.GROUP_SOURCE_ID)
+
+ if (cursor.moveToFirst()) {
+ cursor.getLongOrNull(indexRowId)?.let {
+ yield(GroupLookup.RowId(it))
+ }
+ cursor.getStringOrNull(indexSourceId)?.let {
+ yield(GroupLookup.SourceId(it))
+ }
+
+ while (cursor.moveToNext()) {
+ cursor.getLongOrNull(indexRowId)?.let {
+ yield(GroupLookup.RowId(it))
+ }
+ cursor.getStringOrNull(indexSourceId)?.let {
+ yield(GroupLookup.SourceId(it))
+ }
+ }
+ }
+ }
+}
+
+@RequiresPermission(Manifest.permission.READ_CONTACTS)
+fun getContactGroupById(context: Context, id: GroupLookup): ContactGroupInfo? {
+ var selectionArgs: Array? = null
+ val selection = buildString {
+ when (id) {
+ is GroupLookup.RowId -> {
+ append(ContactsContract.Groups._ID)
+ append(" = ")
+ append(id.id)
+ }
+ is GroupLookup.SourceId -> {
+ append(ContactsContract.Groups.SOURCE_ID)
+ append(" = ?")
+
+ selectionArgs = arrayOf(id.id)
+ }
+ }
+ }
+
+ return findContactGroups(context, selection, selectionArgs).asSequence().firstOrNull()
+}
+
+fun findContactGroups(
+ context: Context,
+ selection: String? = null,
+ selectionArgs: Array? = null,
+) = iterator {
+ context.contentResolver.query(
+ ContactsContract.Groups.CONTENT_URI,
+ CONTACT_GROUP_PROJECTION,
+ selection,
+ selectionArgs,
+ null,
+ )?.use { cursor ->
+ val indexRowId = cursor.getColumnIndexOrThrow(ContactsContract.Groups._ID)
+ val indexSourceId = cursor.getColumnIndexOrThrow(ContactsContract.Groups.SOURCE_ID)
+ val indexTitle = cursor.getColumnIndexOrThrow(ContactsContract.Groups.TITLE)
+
+ if (cursor.moveToFirst()) {
+ yield(ContactGroupInfo(
+ cursor.getLong(indexRowId),
+ cursor.getString(indexSourceId),
+ cursor.getString(indexTitle),
+ ))
+
+ while (cursor.moveToNext()) {
+ yield(ContactGroupInfo(
+ cursor.getLong(indexRowId),
+ cursor.getString(indexSourceId),
+ cursor.getString(indexTitle),
+ ))
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/chiller3/bcr/PreferenceBaseActivity.kt b/app/src/main/java/com/chiller3/bcr/PreferenceBaseActivity.kt
new file mode 100644
index 000000000..ba1724e6d
--- /dev/null
+++ b/app/src/main/java/com/chiller3/bcr/PreferenceBaseActivity.kt
@@ -0,0 +1,65 @@
+package com.chiller3.bcr
+
+import android.os.Bundle
+import android.view.MenuItem
+import android.view.ViewGroup
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
+import androidx.fragment.app.Fragment
+import com.chiller3.bcr.databinding.SettingsActivityBinding
+
+abstract class PreferenceBaseActivity : AppCompatActivity() {
+ protected abstract val titleResId: Int
+
+ protected abstract val showUpButton: Boolean
+
+ protected abstract fun createFragment(): Fragment
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ val binding = SettingsActivityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ if (savedInstanceState == null) {
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.settings, createFragment())
+ .commit()
+ }
+
+ ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { v, windowInsets ->
+ val insets = windowInsets.getInsets(
+ WindowInsetsCompat.Type.systemBars()
+ or WindowInsetsCompat.Type.displayCutout()
+ )
+
+ v.updateLayoutParams {
+ leftMargin = insets.left
+ topMargin = insets.top
+ rightMargin = insets.right
+ }
+
+ WindowInsetsCompat.CONSUMED
+ }
+
+ setSupportActionBar(binding.toolbar)
+ supportActionBar!!.setDisplayHomeAsUpEnabled(showUpButton)
+
+ setTitle(titleResId)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressedDispatcher.onBackPressed()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+}
diff --git a/app/src/main/java/com/chiller3/bcr/PreferenceBaseFragment.kt b/app/src/main/java/com/chiller3/bcr/PreferenceBaseFragment.kt
new file mode 100644
index 000000000..0d003c163
--- /dev/null
+++ b/app/src/main/java/com/chiller3/bcr/PreferenceBaseFragment.kt
@@ -0,0 +1,43 @@
+package com.chiller3.bcr
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.preference.PreferenceFragmentCompat
+import androidx.recyclerview.widget.RecyclerView
+
+abstract class PreferenceBaseFragment : PreferenceFragmentCompat() {
+ override fun onCreateRecyclerView(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+ savedInstanceState: Bundle?
+ ): RecyclerView {
+ val view = super.onCreateRecyclerView(inflater, parent, savedInstanceState)
+
+ view.clipToPadding = false
+
+ ViewCompat.setOnApplyWindowInsetsListener(view) { v, windowInsets ->
+ val insets = windowInsets.getInsets(
+ WindowInsetsCompat.Type.systemBars()
+ or WindowInsetsCompat.Type.displayCutout()
+ )
+
+ // This is a little bit ugly in landscape mode because the divider lines for categories
+ // extend into the inset area. However, it's worth applying the left/right padding here
+ // anyway because it allows the inset area to be used for scrolling instead of just
+ // being a useless dead zone.
+ v.updatePadding(
+ bottom = insets.bottom,
+ left = insets.left,
+ right = insets.right,
+ )
+
+ WindowInsetsCompat.CONSUMED
+ }
+
+ return view
+ }
+}
diff --git a/app/src/main/java/com/chiller3/bcr/Preferences.kt b/app/src/main/java/com/chiller3/bcr/Preferences.kt
index 0ad801c72..f7b25b5ec 100644
--- a/app/src/main/java/com/chiller3/bcr/Preferences.kt
+++ b/app/src/main/java/com/chiller3/bcr/Preferences.kt
@@ -38,8 +38,8 @@ class Preferences(initialContext: Context) {
private const val PREF_FORCE_DIRECT_BOOT = "force_direct_boot"
const val PREF_MIGRATE_DIRECT_BOOT = "migrate_direct_boot"
- const val PREF_ADD_RULE = "add_rule"
- const val PREF_RULE_PREFIX = "rule_"
+ const val PREF_ADD_CONTACT_RULE = "add_contact_rule"
+ const val PREF_ADD_CONTACT_GROUP_RULE = "add_contact_group_rule"
// Not associated with a UI preference
private const val PREF_DEBUG_MODE = "debug_mode"
diff --git a/app/src/main/java/com/chiller3/bcr/rule/PickContactGroup.kt b/app/src/main/java/com/chiller3/bcr/rule/PickContactGroup.kt
new file mode 100644
index 000000000..d6475a324
--- /dev/null
+++ b/app/src/main/java/com/chiller3/bcr/rule/PickContactGroup.kt
@@ -0,0 +1,25 @@
+package com.chiller3.bcr.rule
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.core.content.IntentCompat
+import com.chiller3.bcr.ContactGroupInfo
+
+/** Launch our own picker for contact groups. There is no standard Android component for this. */
+class PickContactGroup : ActivityResultContract() {
+ override fun createIntent(context: Context, input: Void?): Intent {
+ return Intent(context, PickContactGroupActivity::class.java)
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): ContactGroupInfo? {
+ return intent.takeIf { resultCode == Activity.RESULT_OK }?.let {
+ IntentCompat.getParcelableExtra(
+ it,
+ PickContactGroupActivity.RESULT_CONTACT_GROUP,
+ ContactGroupInfo::class.java,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/chiller3/bcr/rule/PickContactGroupActivity.kt b/app/src/main/java/com/chiller3/bcr/rule/PickContactGroupActivity.kt
new file mode 100644
index 000000000..379ac3e71
--- /dev/null
+++ b/app/src/main/java/com/chiller3/bcr/rule/PickContactGroupActivity.kt
@@ -0,0 +1,17 @@
+package com.chiller3.bcr.rule
+
+import androidx.fragment.app.Fragment
+import com.chiller3.bcr.PreferenceBaseActivity
+import com.chiller3.bcr.R
+
+class PickContactGroupActivity : PreferenceBaseActivity() {
+ override val titleResId: Int = R.string.pick_contact_group_title
+
+ override val showUpButton: Boolean = true
+
+ override fun createFragment(): Fragment = PickContactGroupFragment()
+
+ companion object {
+ const val RESULT_CONTACT_GROUP = "contact_group"
+ }
+}
diff --git a/app/src/main/java/com/chiller3/bcr/rule/PickContactGroupFragment.kt b/app/src/main/java/com/chiller3/bcr/rule/PickContactGroupFragment.kt
new file mode 100644
index 000000000..8e97c7c73
--- /dev/null
+++ b/app/src/main/java/com/chiller3/bcr/rule/PickContactGroupFragment.kt
@@ -0,0 +1,75 @@
+package com.chiller3.bcr.rule
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.preference.Preference
+import androidx.preference.get
+import androidx.preference.size
+import com.chiller3.bcr.ContactGroupInfo
+import com.chiller3.bcr.PreferenceBaseFragment
+import com.chiller3.bcr.R
+import kotlinx.coroutines.launch
+
+class PickContactGroupFragment : PreferenceBaseFragment(), Preference.OnPreferenceClickListener {
+ private val viewModel: PickContactGroupViewModel by viewModels()
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.record_rules_preferences, rootKey)
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.groups.collect {
+ updateGroups(it)
+ }
+ }
+ }
+ }
+
+ private fun updateGroups(newGroups: List) {
+ val context = requireContext()
+
+ for (i in (0 until preferenceScreen.size).reversed()) {
+ val p = preferenceScreen[i]
+ preferenceScreen.removePreference(p)
+ }
+
+ for ((i, group) in newGroups.withIndex()) {
+ val p = Preference(context).apply {
+ key = PREF_GROUP_PREFIX + i
+ isPersistent = false
+ title = group.title
+ isIconSpaceReserved = false
+ onPreferenceClickListener = this@PickContactGroupFragment
+ }
+ preferenceScreen.addPreference(p)
+ }
+ }
+
+ override fun onPreferenceClick(preference: Preference): Boolean {
+ when {
+ preference.key.startsWith(PREF_GROUP_PREFIX) -> {
+ val index = preference.key.substring(PREF_GROUP_PREFIX.length).toInt()
+ val activity = requireActivity()
+
+ activity.setResult(Activity.RESULT_OK, Intent().putExtra(
+ PickContactGroupActivity.RESULT_CONTACT_GROUP,
+ viewModel.groups.value[index],
+ ))
+ activity.finish()
+
+ return true
+ }
+ }
+
+ return false
+ }
+
+ companion object {
+ private const val PREF_GROUP_PREFIX = "group_"
+ }
+}
diff --git a/app/src/main/java/com/chiller3/bcr/rule/PickContactGroupViewModel.kt b/app/src/main/java/com/chiller3/bcr/rule/PickContactGroupViewModel.kt
new file mode 100644
index 000000000..a1cfe9160
--- /dev/null
+++ b/app/src/main/java/com/chiller3/bcr/rule/PickContactGroupViewModel.kt
@@ -0,0 +1,53 @@
+package com.chiller3.bcr.rule
+
+import android.app.Application
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.chiller3.bcr.ContactGroupInfo
+import com.chiller3.bcr.findContactGroups
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class PickContactGroupViewModel(application: Application) : AndroidViewModel(application) {
+ private val _groups = MutableStateFlow>(emptyList())
+ val groups: StateFlow> = _groups
+
+ init {
+ refreshGroups()
+ }
+
+ private fun refreshGroups() {
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ val groups = try {
+ findContactGroups(getApplication())
+ .asSequence()
+ .sortedWith { o1, o2 ->
+ compareValuesBy(
+ o1,
+ o2,
+ { it.title },
+ { it.rowId },
+ { it.sourceId },
+ )
+ }
+ .toList()
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to list all contact groups", e)
+ return@withContext
+ }
+
+ _groups.update { groups }
+ }
+ }
+ }
+
+ companion object {
+ private val TAG = PickContactGroupViewModel::class.java.simpleName
+ }
+}
diff --git a/app/src/main/java/com/chiller3/bcr/rule/RecordRule.kt b/app/src/main/java/com/chiller3/bcr/rule/RecordRule.kt
index e9332e47b..c200683c5 100644
--- a/app/src/main/java/com/chiller3/bcr/rule/RecordRule.kt
+++ b/app/src/main/java/com/chiller3/bcr/rule/RecordRule.kt
@@ -5,7 +5,9 @@ import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.util.Log
+import com.chiller3.bcr.GroupLookup
import com.chiller3.bcr.findContactsByPhoneNumber
+import com.chiller3.bcr.getContactGroupMemberships
import com.chiller3.bcr.output.PhoneNumber
sealed class RecordRule {
@@ -14,10 +16,13 @@ sealed class RecordRule {
/**
* Check if the rule matches the set of contacts in [contactLookupKeys].
*
- * @param contactLookupKeys The set of contacts, if any, involved in the call. If null, then
- * [Manifest.permission.READ_CONTACTS] was not granted.
+ * @param contactLookupKeys The set of contacts, if any, involved in the call.
+ * @param contactGroupIds The list of contact groups associated with [contactLookupKeys].
*/
- abstract fun matches(contactLookupKeys: Collection?): Boolean
+ abstract fun matches(
+ contactLookupKeys: Collection,
+ contactGroupIds: Collection,
+ ): Boolean
open fun toRawPreferences(editor: SharedPreferences.Editor, prefix: String) {
editor.putString(prefix + PREF_SUFFIX_TYPE, javaClass.simpleName)
@@ -25,7 +30,10 @@ sealed class RecordRule {
}
data class AllCalls(override val record: Boolean) : RecordRule() {
- override fun matches(contactLookupKeys: Collection?): Boolean = true
+ override fun matches(
+ contactLookupKeys: Collection,
+ contactGroupIds: Collection,
+ ): Boolean = true
companion object {
fun fromRawPreferences(prefs: SharedPreferences, prefix: String): AllCalls {
@@ -37,8 +45,10 @@ sealed class RecordRule {
}
data class UnknownCalls(override val record: Boolean) : RecordRule() {
- override fun matches(contactLookupKeys: Collection?): Boolean =
- contactLookupKeys?.isEmpty() ?: false
+ override fun matches(
+ contactLookupKeys: Collection,
+ contactGroupIds: Collection,
+ ): Boolean = contactLookupKeys.isEmpty()
companion object {
fun fromRawPreferences(prefs: SharedPreferences, prefix: String): UnknownCalls {
@@ -50,8 +60,10 @@ sealed class RecordRule {
}
data class Contact(val lookupKey: String, override val record: Boolean) : RecordRule() {
- override fun matches(contactLookupKeys: Collection?): Boolean =
- contactLookupKeys != null && lookupKey in contactLookupKeys
+ override fun matches(
+ contactLookupKeys: Collection,
+ contactGroupIds: Collection,
+ ): Boolean = lookupKey in contactLookupKeys
override fun toRawPreferences(editor: SharedPreferences.Editor, prefix: String) {
super.toRawPreferences(editor, prefix)
@@ -72,6 +84,49 @@ sealed class RecordRule {
}
}
+ data class ContactGroup(
+ val rowId: Long,
+ val sourceId: String,
+ override val record: Boolean,
+ ) : RecordRule() {
+ override fun matches(
+ contactLookupKeys: Collection,
+ contactGroupIds: Collection,
+ ): Boolean = contactGroupIds.any {
+ when (it) {
+ is GroupLookup.RowId -> it.id == rowId
+ is GroupLookup.SourceId -> it.id == sourceId
+ }
+ }
+
+ override fun toRawPreferences(editor: SharedPreferences.Editor, prefix: String) {
+ super.toRawPreferences(editor, prefix)
+ editor.putLong(prefix + PREF_SUFFIX_CONTACT_GROUP_ROW_ID, rowId)
+ editor.putString(prefix + PREF_SUFFIX_CONTACT_GROUP_SOURCE_ID, sourceId)
+ }
+
+ companion object {
+ private const val PREF_SUFFIX_CONTACT_GROUP_ROW_ID = "contact_group_row_id"
+ private const val PREF_SUFFIX_CONTACT_GROUP_SOURCE_ID = "contact_group_source_id"
+
+ fun fromRawPreferences(prefs: SharedPreferences, prefix: String): ContactGroup {
+ val prefRowId = prefix + PREF_SUFFIX_CONTACT_GROUP_ROW_ID
+ val rowId = prefs.getLong(prefRowId, -1)
+ if (rowId == -1L) {
+ throw IllegalStateException("Missing $prefRowId")
+ }
+
+ val prefSourceId = prefix + PREF_SUFFIX_CONTACT_GROUP_SOURCE_ID
+ val sourceId = prefs.getString(prefSourceId, null)
+ ?: throw IllegalStateException("Missing $prefSourceId")
+
+ val record = prefs.getBoolean(prefix + PREF_SUFFIX_RECORD, false)
+
+ return ContactGroup(rowId, sourceId, record)
+ }
+ }
+ }
+
companion object {
private val TAG = RecordRule::class.java.simpleName
@@ -86,6 +141,8 @@ sealed class RecordRule {
UnknownCalls.fromRawPreferences(prefs, prefix)
Contact::class.java.simpleName ->
Contact.fromRawPreferences(prefs, prefix)
+ ContactGroup::class.java.simpleName ->
+ ContactGroup.fromRawPreferences(prefs, prefix)
null -> null
else -> {
Log.w(TAG, "Unknown record rule type: $type")
@@ -109,24 +166,36 @@ sealed class RecordRule {
numbers: Collection): Boolean {
val contactsAllowed = context.checkSelfPermission(Manifest.permission.READ_CONTACTS) ==
PackageManager.PERMISSION_GRANTED
- val contactLookupKeys = if (contactsAllowed) {
- val keys = hashSetOf()
-
- for (number in numbers) {
- findContactsByPhoneNumber(context, number)
- .asSequence()
- .map { it.lookupKey }
- .toCollection(keys)
+ var contactLookupKeys = emptySet()
+ var contactGroupIds = emptySet()
+
+ if (contactsAllowed) {
+ contactLookupKeys = hashSetOf().apply {
+ for (number in numbers) {
+ findContactsByPhoneNumber(context, number)
+ .asSequence()
+ .map { it.lookupKey }
+ .toCollection(this)
+ }
}
- keys
+ // Avoid doing group membership lookups if we don't need to.
+ if (rules.any { it is ContactGroup }) {
+ contactGroupIds = hashSetOf().apply {
+ for (lookupKey in contactLookupKeys) {
+ getContactGroupMemberships(context, lookupKey)
+ .asSequence()
+ .toCollection(this)
+ }
+ }
+ }
} else {
Log.i(TAG, "Contacts permission not granted")
- null
}
for (rule in rules) {
- if (rule.matches(contactLookupKeys)) {
+ if (rule.matches(contactLookupKeys, contactGroupIds)) {
+ Log.i(TAG, "Matched rule: $rule")
return rule.record
}
}
diff --git a/app/src/main/java/com/chiller3/bcr/rule/RecordRulesActivity.kt b/app/src/main/java/com/chiller3/bcr/rule/RecordRulesActivity.kt
index 7444b0f6e..7272d2e63 100644
--- a/app/src/main/java/com/chiller3/bcr/rule/RecordRulesActivity.kt
+++ b/app/src/main/java/com/chiller3/bcr/rule/RecordRulesActivity.kt
@@ -1,60 +1,13 @@
package com.chiller3.bcr.rule
-import android.os.Bundle
-import android.view.MenuItem
-import android.view.ViewGroup
-import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.updateLayoutParams
+import androidx.fragment.app.Fragment
+import com.chiller3.bcr.PreferenceBaseActivity
import com.chiller3.bcr.R
-import com.chiller3.bcr.databinding.SettingsActivityBinding
-class RecordRulesActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- enableEdgeToEdge()
- super.onCreate(savedInstanceState)
+class RecordRulesActivity : PreferenceBaseActivity() {
+ override val titleResId: Int = R.string.pref_record_rules_name
- val binding = SettingsActivityBinding.inflate(layoutInflater)
- setContentView(binding.root)
+ override val showUpButton: Boolean = true
- if (savedInstanceState == null) {
- supportFragmentManager
- .beginTransaction()
- .replace(R.id.settings, RecordRulesFragment())
- .commit()
- }
-
- ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { v, windowInsets ->
- val insets = windowInsets.getInsets(
- WindowInsetsCompat.Type.systemBars()
- or WindowInsetsCompat.Type.displayCutout()
- )
-
- v.updateLayoutParams {
- leftMargin = insets.left
- topMargin = insets.top
- rightMargin = insets.right
- }
-
- WindowInsetsCompat.CONSUMED
- }
-
- setSupportActionBar(binding.toolbar)
- supportActionBar!!.setDisplayHomeAsUpEnabled(true)
-
- setTitle(R.string.pref_record_rules_name)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- android.R.id.home -> {
- onBackPressedDispatcher.onBackPressed()
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
- }
-
-}
\ No newline at end of file
+ override fun createFragment(): Fragment = RecordRulesFragment()
+}
diff --git a/app/src/main/java/com/chiller3/bcr/rule/RecordRulesFragment.kt b/app/src/main/java/com/chiller3/bcr/rule/RecordRulesFragment.kt
index 9fbed986b..b820030b4 100644
--- a/app/src/main/java/com/chiller3/bcr/rule/RecordRulesFragment.kt
+++ b/app/src/main/java/com/chiller3/bcr/rule/RecordRulesFragment.kt
@@ -3,27 +3,21 @@ package com.chiller3.bcr.rule
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
-import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
-import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.MenuProvider
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
-import androidx.preference.PreferenceFragmentCompat
import androidx.preference.get
import androidx.preference.size
-import androidx.recyclerview.widget.RecyclerView
+import com.chiller3.bcr.PreferenceBaseFragment
import com.chiller3.bcr.Preferences
import com.chiller3.bcr.R
import com.chiller3.bcr.view.LongClickableSwitchPreference
@@ -32,53 +26,27 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import kotlin.properties.Delegates
-class RecordRulesFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener,
+class RecordRulesFragment : PreferenceBaseFragment(), Preference.OnPreferenceClickListener,
Preference.OnPreferenceChangeListener, OnPreferenceLongClickListener {
private val viewModel: RecordRulesViewModel by viewModels()
private lateinit var categoryRules: PreferenceCategory
- private lateinit var prefAddRule: Preference
+ private lateinit var prefAddContactRule: Preference
+ private lateinit var prefAddContactGroupRule: Preference
private var ruleOffset by Delegates.notNull()
+ // We don't bother using persisted URI permissions because we need the full READ_CONTACTS
+ // permission for this feature to work at all (eg. to perform lookups by number).
private val requestContact =
registerForActivityResult(ActivityResultContracts.PickContact()) { uri ->
- // We don't bother using persisted URI permissions for the contact because we need the
- // full READ_CONTACTS permission for this feature to work at all (lookups by number).
uri?.let { viewModel.addContactRule(it) }
}
-
- override fun onCreateRecyclerView(
- inflater: LayoutInflater,
- parent: ViewGroup,
- savedInstanceState: Bundle?
- ): RecyclerView {
- val view = super.onCreateRecyclerView(inflater, parent, savedInstanceState)
-
- view.clipToPadding = false
-
- ViewCompat.setOnApplyWindowInsetsListener(view) { v, windowInsets ->
- val insets = windowInsets.getInsets(
- WindowInsetsCompat.Type.systemBars()
- or WindowInsetsCompat.Type.displayCutout()
- )
-
- // This is a little bit ugly in landscape mode because the divider lines for categories
- // extend into the inset area. However, it's worth applying the left/right padding here
- // anyway because it allows the inset area to be used for scrolling instead of just
- // being a useless dead zone.
- v.updatePadding(
- bottom = insets.bottom,
- left = insets.left,
- right = insets.right,
- )
-
- WindowInsetsCompat.CONSUMED
+ private val requestContactGroup =
+ registerForActivityResult(PickContactGroup()) { group ->
+ group?.let { viewModel.addContactGroupRule(it) }
}
- return view
- }
-
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.record_rules_preferences, rootKey)
@@ -86,8 +54,11 @@ class RecordRulesFragment : PreferenceFragmentCompat(), Preference.OnPreferenceC
ruleOffset = categoryRules.preferenceCount
- prefAddRule = findPreference(Preferences.PREF_ADD_RULE)!!
- prefAddRule.onPreferenceClickListener = this
+ prefAddContactRule = findPreference(Preferences.PREF_ADD_CONTACT_RULE)!!
+ prefAddContactRule.onPreferenceClickListener = this
+
+ prefAddContactGroupRule = findPreference(Preferences.PREF_ADD_CONTACT_GROUP_RULE)!!
+ prefAddContactGroupRule.onPreferenceClickListener = this
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -148,7 +119,8 @@ class RecordRulesFragment : PreferenceFragmentCompat(), Preference.OnPreferenceC
val contactsGranted = context.checkSelfPermission(Manifest.permission.READ_CONTACTS) ==
PackageManager.PERMISSION_GRANTED
- prefAddRule.isEnabled = contactsGranted
+ prefAddContactRule.isEnabled = contactsGranted
+ prefAddContactGroupRule.isEnabled = contactsGranted
for (i in (ruleOffset until categoryRules.size).reversed()) {
val p = categoryRules[i]
@@ -157,7 +129,7 @@ class RecordRulesFragment : PreferenceFragmentCompat(), Preference.OnPreferenceC
for ((i, rule) in newRules.withIndex()) {
val p = LongClickableSwitchPreference(context).apply {
- key = Preferences.PREF_RULE_PREFIX + i
+ key = PREF_RULE_PREFIX + i
isPersistent = false
when (rule) {
is DisplayedRecordRule.AllCalls -> {
@@ -180,7 +152,16 @@ class RecordRulesFragment : PreferenceFragmentCompat(), Preference.OnPreferenceC
R.string.record_rule_type_contact_name,
rule.displayName ?: rule.lookupKey,
)
- summary = getString(R.string.record_rule_type_contact_desc)
+ summary = getString(R.string.record_rule_removable_desc)
+ isEnabled = contactsGranted
+ onPreferenceLongClickListener = this@RecordRulesFragment
+ }
+ is DisplayedRecordRule.ContactGroup -> {
+ title = getString(
+ R.string.record_rule_type_contact_group_name,
+ rule.title ?: rule.sourceId,
+ )
+ summary = getString(R.string.record_rule_removable_desc)
isEnabled = contactsGranted
onPreferenceLongClickListener = this@RecordRulesFragment
}
@@ -195,10 +176,14 @@ class RecordRulesFragment : PreferenceFragmentCompat(), Preference.OnPreferenceC
override fun onPreferenceClick(preference: Preference): Boolean {
when (preference) {
- prefAddRule -> {
+ prefAddContactRule -> {
requestContact.launch(null)
return true
}
+ prefAddContactGroupRule -> {
+ requestContactGroup.launch(null)
+ return true
+ }
}
return false
@@ -206,8 +191,8 @@ class RecordRulesFragment : PreferenceFragmentCompat(), Preference.OnPreferenceC
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
when {
- preference.key.startsWith(Preferences.PREF_RULE_PREFIX) -> {
- val index = preference.key.substring(Preferences.PREF_RULE_PREFIX.length).toInt()
+ preference.key.startsWith(PREF_RULE_PREFIX) -> {
+ val index = preference.key.substring(PREF_RULE_PREFIX.length).toInt()
viewModel.setRuleRecord(index, newValue as Boolean)
return true
}
@@ -218,8 +203,8 @@ class RecordRulesFragment : PreferenceFragmentCompat(), Preference.OnPreferenceC
override fun onPreferenceLongClick(preference: Preference): Boolean {
when {
- preference.key.startsWith(Preferences.PREF_RULE_PREFIX) -> {
- val index = preference.key.substring(Preferences.PREF_RULE_PREFIX.length).toInt()
+ preference.key.startsWith(PREF_RULE_PREFIX) -> {
+ val index = preference.key.substring(PREF_RULE_PREFIX.length).toInt()
viewModel.deleteRule(index)
return true
}
@@ -237,4 +222,8 @@ class RecordRulesFragment : PreferenceFragmentCompat(), Preference.OnPreferenceC
})
.show()
}
+
+ companion object {
+ private const val PREF_RULE_PREFIX = "rule_"
+ }
}
diff --git a/app/src/main/java/com/chiller3/bcr/rule/RecordRulesViewModel.kt b/app/src/main/java/com/chiller3/bcr/rule/RecordRulesViewModel.kt
index 04d4c7280..7fb70d17a 100644
--- a/app/src/main/java/com/chiller3/bcr/rule/RecordRulesViewModel.kt
+++ b/app/src/main/java/com/chiller3/bcr/rule/RecordRulesViewModel.kt
@@ -7,9 +7,12 @@ import android.net.Uri
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
+import com.chiller3.bcr.ContactGroupInfo
+import com.chiller3.bcr.GroupLookup
import com.chiller3.bcr.Preferences
-import com.chiller3.bcr.findContactByLookupKey
import com.chiller3.bcr.findContactsByUri
+import com.chiller3.bcr.getContactByLookupKey
+import com.chiller3.bcr.getContactGroupById
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -22,48 +25,68 @@ import kotlinx.coroutines.withContext
sealed class DisplayedRecordRule : Comparable {
abstract var record: Boolean
+ protected abstract val sortCategory: Int
+
/**
- * [Contact] comes first, sorted by display name, followed by [UnknownCalls] and [AllCalls].
+ * Rule types are sorted by [sortCategory]. Rules within the same category are sorted by the
+ * display name if possible.
*/
override fun compareTo(other: DisplayedRecordRule): Int {
when (this) {
- is AllCalls -> {
- return when (other) {
- is AllCalls -> record.compareTo(other.record)
- else -> 1
- }
+ is AllCalls -> if (other is AllCalls) {
+ return record.compareTo(other.record)
}
- is UnknownCalls -> {
- return when (other) {
- is UnknownCalls -> record.compareTo(other.record)
- is AllCalls -> -1
- is Contact -> 1
- }
+ is UnknownCalls -> if (other is UnknownCalls) {
+ return record.compareTo(other.record)
}
- is Contact -> {
- return when (other) {
- is Contact -> compareValuesBy(
- this,
- other,
- { it.displayName },
- { it.lookupKey },
- { it.record },
- )
- else -> -1
- }
+ is Contact -> if (other is Contact) {
+ return compareValuesBy(
+ this,
+ other,
+ { it.displayName },
+ { it.lookupKey },
+ { it.record },
+ )
+ }
+ is ContactGroup -> if (other is ContactGroup) {
+ return compareValuesBy(
+ this,
+ other,
+ { it.title },
+ { it.rowId },
+ { it.sourceId },
+ { it.record },
+ )
}
}
+
+ return sortCategory.compareTo(other.sortCategory)
}
- data class AllCalls(override var record: Boolean) : DisplayedRecordRule()
+ data class AllCalls(override var record: Boolean) : DisplayedRecordRule() {
+ override val sortCategory: Int = 4
+ }
- data class UnknownCalls(override var record: Boolean) : DisplayedRecordRule()
+ data class UnknownCalls(override var record: Boolean) : DisplayedRecordRule() {
+ override val sortCategory: Int = 3
+ }
data class Contact(
- val lookupKey: String,
val displayName: String?,
+ val lookupKey: String,
override var record: Boolean,
- ) : DisplayedRecordRule()
+ ) : DisplayedRecordRule() {
+ override val sortCategory: Int = 1
+ }
+
+ data class ContactGroup(
+ val title: String?,
+ val rowId: Long,
+ val sourceId: String,
+ override var record: Boolean,
+ ) : DisplayedRecordRule() {
+ override val sortCategory: Int = 2
+ }
}
sealed class Message {
@@ -101,8 +124,14 @@ class RecordRulesViewModel(application: Application) : AndroidViewModel(applicat
is RecordRule.AllCalls -> DisplayedRecordRule.AllCalls(rule.record)
is RecordRule.UnknownCalls -> DisplayedRecordRule.UnknownCalls(rule.record)
is RecordRule.Contact -> DisplayedRecordRule.Contact(
- rule.lookupKey,
getContactDisplayName(rule.lookupKey),
+ rule.lookupKey,
+ rule.record,
+ )
+ is RecordRule.ContactGroup -> DisplayedRecordRule.ContactGroup(
+ getContactGroupTitle(rule.sourceId),
+ rule.rowId,
+ rule.sourceId,
rule.record,
)
}
@@ -123,12 +152,17 @@ class RecordRulesViewModel(application: Application) : AndroidViewModel(applicat
val rawRules = sortedRules.map { displayedRule ->
when (displayedRule) {
- is DisplayedRecordRule.AllCalls ->
- RecordRule.AllCalls(displayedRule.record)
- is DisplayedRecordRule.UnknownCalls ->
- RecordRule.UnknownCalls(displayedRule.record)
- is DisplayedRecordRule.Contact ->
- RecordRule.Contact(displayedRule.lookupKey, displayedRule.record)
+ is DisplayedRecordRule.AllCalls -> RecordRule.AllCalls(displayedRule.record)
+ is DisplayedRecordRule.UnknownCalls -> RecordRule.UnknownCalls(displayedRule.record)
+ is DisplayedRecordRule.Contact -> RecordRule.Contact(
+ displayedRule.lookupKey,
+ displayedRule.record,
+ )
+ is DisplayedRecordRule.ContactGroup -> RecordRule.ContactGroup(
+ displayedRule.rowId,
+ displayedRule.sourceId,
+ displayedRule.record,
+ )
}
}
@@ -147,13 +181,27 @@ class RecordRulesViewModel(application: Application) : AndroidViewModel(applicat
}
return try {
- findContactByLookupKey(getApplication(), lookupKey)?.displayName
+ getContactByLookupKey(getApplication(), lookupKey)?.displayName
} catch (e: Exception) {
Log.w(TAG, "Failed to look up contact", e)
null
}
}
+ private fun getContactGroupTitle(sourceId: String): String? {
+ if (getApplication().checkSelfPermission(Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ return null
+ }
+
+ return try {
+ getContactGroupById(getApplication(), GroupLookup.SourceId(sourceId))?.title
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to look up contact group", e)
+ null
+ }
+ }
+
fun addContactRule(uri: Uri) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
@@ -184,9 +232,45 @@ class RecordRulesViewModel(application: Application) : AndroidViewModel(applicat
val newRules = ArrayList(oldRules)
newRules.add(
DisplayedRecordRule.Contact(
- contact.lookupKey,
contact.displayName,
- true
+ contact.lookupKey,
+ true,
+ )
+ )
+
+ saveRulesLocked(newRules)
+
+ _messages.update { it + Message.RuleAdded }
+ }
+ }
+ }
+ }
+ }
+
+ fun addContactGroupRule(group: ContactGroupInfo) {
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ rulesMutex.withLock {
+ val oldRules = rules.value
+ val existingRule = oldRules.find {
+ it is DisplayedRecordRule.ContactGroup &&
+ (it.rowId == group.rowId || it.sourceId == group.sourceId)
+ }
+
+ if (existingRule != null) {
+ Log.d(TAG, "Rule already exists for ${group.rowId}, ${group.sourceId}")
+
+ _messages.update { it + Message.RuleExists }
+ } else {
+ Log.d(TAG, "Adding new rule for ${group.rowId}, ${group.sourceId}")
+
+ val newRules = ArrayList(oldRules)
+ newRules.add(
+ DisplayedRecordRule.ContactGroup(
+ group.title,
+ group.rowId,
+ group.sourceId,
+ true,
)
)
@@ -209,6 +293,7 @@ class RecordRulesViewModel(application: Application) : AndroidViewModel(applicat
is DisplayedRecordRule.AllCalls -> displayedRule.copy(record = record)
is DisplayedRecordRule.UnknownCalls -> displayedRule.copy(record = record)
is DisplayedRecordRule.Contact -> displayedRule.copy(record = record)
+ is DisplayedRecordRule.ContactGroup -> displayedRule.copy(record = record)
}
} else {
displayedRule
diff --git a/app/src/main/java/com/chiller3/bcr/settings/SettingsActivity.kt b/app/src/main/java/com/chiller3/bcr/settings/SettingsActivity.kt
index f27f9e6b1..e4a664ada 100644
--- a/app/src/main/java/com/chiller3/bcr/settings/SettingsActivity.kt
+++ b/app/src/main/java/com/chiller3/bcr/settings/SettingsActivity.kt
@@ -1,47 +1,13 @@
package com.chiller3.bcr.settings
-import android.os.Bundle
-import android.view.ViewGroup
-import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.updateLayoutParams
+import androidx.fragment.app.Fragment
+import com.chiller3.bcr.PreferenceBaseActivity
import com.chiller3.bcr.R
-import com.chiller3.bcr.databinding.SettingsActivityBinding
-class SettingsActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- enableEdgeToEdge()
- super.onCreate(savedInstanceState)
+class SettingsActivity : PreferenceBaseActivity() {
+ override val titleResId: Int = R.string.app_name_full
- val binding = SettingsActivityBinding.inflate(layoutInflater)
- setContentView(binding.root)
+ override val showUpButton: Boolean = false
- if (savedInstanceState == null) {
- supportFragmentManager
- .beginTransaction()
- .replace(R.id.settings, SettingsFragment())
- .commit()
- }
-
- ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { v, windowInsets ->
- val insets = windowInsets.getInsets(
- WindowInsetsCompat.Type.systemBars()
- or WindowInsetsCompat.Type.displayCutout()
- )
-
- v.updateLayoutParams {
- leftMargin = insets.left
- topMargin = insets.top
- rightMargin = insets.right
- }
-
- WindowInsetsCompat.CONSUMED
- }
-
- setSupportActionBar(binding.toolbar)
-
- setTitle(R.string.app_name_full)
- }
+ override fun createFragment(): Fragment = SettingsFragment()
}
diff --git a/app/src/main/java/com/chiller3/bcr/settings/SettingsFragment.kt b/app/src/main/java/com/chiller3/bcr/settings/SettingsFragment.kt
index f2932f26f..a09c59b0b 100644
--- a/app/src/main/java/com/chiller3/bcr/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/chiller3/bcr/settings/SettingsFragment.kt
@@ -5,20 +5,14 @@ import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.updatePadding
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
-import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
-import androidx.recyclerview.widget.RecyclerView
import com.chiller3.bcr.BuildConfig
import com.chiller3.bcr.DirectBootMigrationService
import com.chiller3.bcr.Permissions
+import com.chiller3.bcr.PreferenceBaseFragment
import com.chiller3.bcr.Preferences
import com.chiller3.bcr.R
import com.chiller3.bcr.dialog.MinDurationDialogFragment
@@ -32,7 +26,7 @@ import com.chiller3.bcr.view.LongClickablePreference
import com.chiller3.bcr.view.OnPreferenceLongClickListener
import com.google.android.material.snackbar.Snackbar
-class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener,
+class SettingsFragment : PreferenceBaseFragment(), Preference.OnPreferenceChangeListener,
Preference.OnPreferenceClickListener, OnPreferenceLongClickListener,
SharedPreferences.OnSharedPreferenceChangeListener {
private lateinit var prefs: Preferences
@@ -60,37 +54,6 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChan
refreshInhibitBatteryOptState()
}
- override fun onCreateRecyclerView(
- inflater: LayoutInflater,
- parent: ViewGroup,
- savedInstanceState: Bundle?
- ): RecyclerView {
- val view = super.onCreateRecyclerView(inflater, parent, savedInstanceState)
-
- view.clipToPadding = false
-
- ViewCompat.setOnApplyWindowInsetsListener(view) { v, windowInsets ->
- val insets = windowInsets.getInsets(
- WindowInsetsCompat.Type.systemBars()
- or WindowInsetsCompat.Type.displayCutout()
- )
-
- // This is a little bit ugly in landscape mode because the divider lines for categories
- // extend into the inset area. However, it's worth applying the left/right padding here
- // anyway because it allows the inset area to be used for scrolling instead of just
- // being a useless dead zone.
- v.updatePadding(
- bottom = insets.bottom,
- left = insets.left,
- right = insets.right,
- )
-
- WindowInsetsCompat.CONSUMED
- }
-
- return view
- }
-
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = requireContext()
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index dc19119e8..7ba9484bc 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -13,8 +13,7 @@
تعطيل تحسين البطارية
الاصدار
الارقام المراد تسجيل مكالماتها
- اضافه ارقام
- اضافه الرقم لقائمه التسجيل
+ اضافه الرقم لقائمه التسجيل
اضافه الرقم للتسجيل ، اضغط مطولا للحذف
الرقم موجود سابقا
عند تشغيل هذه الميزة، يتم تسجيل المكالمة تلقائيًا بشكل افتراضي ويمكن حذفها يدويًا من الإشعار. عند إيقاف تشغيل هذه الخاصية ، سيتم حذف التسجيل في نهاية المكالمة ما لم يتم الاحتفاظ به باستخدام زر الاستعادة في الإشعار.
@@ -22,7 +21,7 @@
جميع المكالمات الاخرئ
ارقام غير معروفه
الاسم: %s
- ضغطه متواصله لحذف القيد
+ ضغطه متواصله لحذف القيد
تغيير المجلد
اسم القالب
تعديل القالب
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 4f697c9eb..63729739b 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -24,9 +24,8 @@
Version
-
- Füge eine Regel hinzu
- Füge eine automatische Aufnahmeregel für einen Kontakt hinzu.
+
+ Füge eine automatische Aufnahmeregel für einen Kontakt hinzu.
Neue Regel hinzugefügt. Lange gedrückt halten, um sie zu löschen.
Regel existiert bereits.
Alle anderen Anrufe
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 3b719c11c..e818116c6 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -41,10 +41,9 @@
Déplacer les enregistrements du démarrage direct
Déplacer les enregistrements réalisés avant le premier déverrouillage. Cela devrait se produire normalement après le premier déverrouillage.
-
+
Règles
- Ajouter une règle
- Ajouter une règle d\auto-enregistrement pour un contact.
+ Ajouter une règle d\auto-enregistrement pour un contact.
Règle ajoutée. Maintenir appuyé pour la supprimer.
Règle déjà existante.
Quand une règle est active, l\'appel est automatiquement enregistré et peut être supprimé manuellement via la notification. Quand une règle est inactive, l\'enregistrement sera supprimé automatiquement à la fin de l\'appel et peut être restauré manuellement via la notification.
@@ -55,7 +54,7 @@
Appels inconnus
Appels ne correspondant à aucun contact.
Contact: %s
- Maintenir appuyé pour supprimer la règle.
+ Maintenir appuyé pour supprimer la règle.
Changer le dossier
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 4036b4087..b64985dd7 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -27,10 +27,9 @@
वर्जन:
-
+
नियम
- नियम जोड़ें
- किसी संपर्क के लिए ऑटो-रिकॉर्ड नियम जोड़ें।
+ किसी संपर्क के लिए ऑटो-रिकॉर्ड नियम जोड़ें।
नया नियम जोड़ा गया। नियम को हटाने के लिए उसे देर तक दबाएँ।
नियम पहले से मौजूद है।
जब कोई नियम चालू होता है, तो कॉल डिफ़ॉल्ट रूप से स्वचालित रूप से रिकॉर्ड हो जाती है और अधिसूचना से मैन्युअल रूप से हटाया जा सकता है। जब कोई नियम बंद कर दिया जाता है, तो कॉल के अंत में रिकॉर्डिंग हटा दी जाएगी जब तक कि इसे अधिसूचना में पुनर्स्थापना बटन का उपयोग करके संरक्षित नहीं किया जाता है।
@@ -41,7 +40,7 @@
अज्ञात कॉल
ऐसी कॉलें जो किसी भी संपर्क से मेल नहीं खातीं।
संपर्क: %s
- नियम हटाने के लिए देर तक दबाएँ।
+ नियम हटाने के लिए देर तक दबाएँ।
निर्देशिका बदलें
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index efbc244b5..96b9df25b 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -27,10 +27,9 @@
Versione
-
+
Regole
- Aggiungi regola
- Aggiungi una regola automatica per un contatto.
+ Aggiungi una regola automatica per un contatto.
Nuova regola aggiunta. Tieni premuto per cancellarla.
La regola già esiste.
Quando una regola è attiva, la chiamata verrà registrata automaticamente e potrà essere cancellata dalla notifica. Quando una regola non è attiva, la registrazione verrà cancellata alla fine della chiamata a meno che non premi Ripristina nella notifica.
@@ -41,7 +40,7 @@
Sconosciuto
Chiamate che non rientrano in nessun contatto.
Contatto: %s
- Tieni premuto per rimuovere la regola.
+ Tieni premuto per rimuovere la regola.
Cambia cartella
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index b08508d1f..8e019b263 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -27,10 +27,9 @@
מספר גרסה
-
+
כללים
- הוסף כלל
- הוסף כלל הקלטה אוטומטית עבור איש קשר.
+ הוסף כלל הקלטה אוטומטית עבור איש קשר.
כלל חדש נוסף. לחץ לחיצה ארוכה על הכלל כדי למחוק אותו.
הכלל כבר קיים.
כאשר כלל מופעל, השיחה מוקלטת באופן אוטומטי כברירת מחדל וניתן למחוק אותה באופן ידני מההודעה. כאשר כלל מבוטל, ההקלטה תימחק בסוף שיחה, אלא אם כן היא נשמרת באמצעות לחצן שחזר בהודעה.
@@ -41,7 +40,7 @@
שיחות לא מזוהות
שיחות שאינן מאנשי הקשר.
איש קשר: %s
- לחץ לחיצה ארוכה כדי להסיר כלל.
+ לחץ לחיצה ארוכה כדי להסיר כלל.
שינוי תיקיה
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 605c2e25a..22c55d46c 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -57,8 +57,7 @@
Android の通信システムと統合されているサードパーティ製アプリからの通話を録音します。 これらの通話の録音は、無音となってしまう可能性があります。
バージョン
ルール
- ルールの追加
- 連絡先による自動録音ルールを追加します。
+ 連絡先による自動録音ルールを追加します。
新しいルールを追加しました。 ルールを長押しして削除します。
ルールはすでに存在します。
ルールがオンになっている場合、通話はデフォルトで自動的に録音され、通知から手動で削除できます。 ルールがオフになっている場合、通知の [復元] ボタンを使用して録音を保存しない限り、通話の終了時に録音は削除されます。
@@ -69,7 +68,7 @@
不明な通話
どの連絡先とも一致しない通話。
連絡先: %s
- 長押ししてルールを削除します。
+ 長押ししてルールを削除します。
ディレクトリを変更する
ファイル名のテンプレート
テンプレートの編集
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 96b207fc3..060f5eac8 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -30,10 +30,9 @@
Wersja
-
+
Reguły
- Dodaj regułę
- Dodaj regułę automatycznego rejestrowania dla kontaktu.
+ Dodaj regułę automatycznego rejestrowania dla kontaktu.
Dodano nową regułę. Naciśnij i przytrzymaj regułę, aby ją usunąć.
Reguła już istnieje.
Gdy reguła jest włączona, połączenie jest domyślnie automatycznie nagrywane i można je ręcznie usunąć z powiadomienia. Gdy reguła jest wyłączona, nagranie zostanie usunięte po zakończeniu połączenia, chyba że zostanie zachowane za pomocą przycisku Przywróć w powiadomieniu.
@@ -44,7 +43,7 @@
Nieznane połączenia
Połączenia, które nie pasują do żadnego kontaktu.
Kontakt: %s
- Naciśnij i przytrzymaj, aby usunąć regułę.
+ Naciśnij i przytrzymaj, aby usunąć regułę.
Zmień katalog
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index 9f74a4e4f..96f77d39f 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -27,10 +27,9 @@
Versão
-
+
Regras
- Adicionar Regra
- Adicione uma regra de auto-gravação para um contacto.
+ Adicione uma regra de auto-gravação para um contacto.
Adicionou nova regra. Pressione longamente para eliminar.
Regra já existe.
Quando uma regra está ativada, a chamada é gravada automaticamente por padrão e pode ser eliminada manualmente a partir da notificação. Quando uma regra está desativada, a gravação será eliminada no final da chamada, a menos que seja preservada usando o botão Restaurar na notificação.
@@ -41,7 +40,7 @@
Chamadas Desconhecidas
Chamadas que não correspondem a nenhum contacto.
Contacto: %s
- Pressione longamente para remover a regra.
+ Pressione longamente para remover a regra.
Alterar Diretório
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 857bb2a1c..43e576c20 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -33,10 +33,9 @@
Версия
-
+
Правила
- Добавить правило
- Добавить правило автоматической записи для контакта.
+ Добавить правило автоматической записи для контакта.
Добавлено новое правило. Нажмите и удерживайте правило, чтобы удалить его.
Правило уже существует.
Если правило включено, то по умолчанию запись разговора ведется автоматически и может быть удалена вручную из уведомления. Если правило выключено, запись будет удалена по окончании разговора, если она не была сохранена с помощью кнопки "Восстановить" в уведомлении.
@@ -47,7 +46,7 @@
Звонки от неизвестных
Звонки, которые не соответствуют ни одному контакту.
Контакт: %s
- Нажмите и удерживайте для удаления правила.
+ Нажмите и удерживайте для удаления правила.
Изменить папку
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 132617f34..bc2f0ed2c 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -27,10 +27,9 @@
Verzia
-
+
Pravidlá
- Pridať pravidlo
- Pravidlo automatického nahrávania pre kontakt.
+ Pravidlo automatického nahrávania pre kontakt.
Pravidlo pridané. Vymažete dlhým stlačením.
Pravidlo už existuje.
Keď je pravidlo aktívne, hovory sa budú nahrávať automaticky a nahrávku práve skončeného hovoru je možné vymazať z upozornenia. Keď pravidlo nie je aktívne, nahrávky budú vždy po ukončení hovoru vymazané, jedine, že si ich ponecháte klepnutím na tlačidlo Obnoviť v upozornení.
@@ -41,7 +40,7 @@
Neznáme hovory
Hovory, ktorých číslo nemáte v kontaktoch.
Kontakt: %s
- Pravidlo odstránite dlhým stlačením.
+ Pravidlo odstránite dlhým stlačením.
Zmeniť priečinok
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 7ce661a7b..077b23be6 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -30,10 +30,9 @@
Sürüm
-
+
Kurallar
- Kural ekle
- Bir kişi için otomatik-kayıt kuralı ekleyin.
+ Bir kişi için otomatik-kayıt kuralı ekleyin.
Yeni kural eklendi. Silmek için kurala uzun basın.
Kural zaten var.
Bir kural etkinleştirildiğinde, arama varsayılan olarak otomatik kaydedilir ve bildirimden manuel olarak silinebilir. Bir kural kapatıldığında, bildirimdeki Geri yükle butonu kullanılarak kaydedilmediği sürece kayıt, aramanın sonunda silinecektir.
@@ -44,7 +43,7 @@
Bilinmeyen aramalar
Herhangi bir kişiyle eşleşmeyen aramalar.
Kişi: %s
- Kuralı kaldırmak için uzun basın.
+ Kuralı kaldırmak için uzun basın.
Çıkış klasörünü değiştir
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index f7d6bf83f..88d4e6e27 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -24,9 +24,8 @@
Версія
-
- Додати правило
- Додайте правило автоматичного запису для контакту.
+
+ Додайте правило автоматичного запису для контакту.
Додано нове правило. Натисніть і утримуйте правило, щоб видалити його.
Правило вже існує.
Усі інші дзвінки
diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml
index 094f83bb0..32f3acbcd 100644
--- a/app/src/main/res/values-ur/strings.xml
+++ b/app/src/main/res/values-ur/strings.xml
@@ -17,8 +17,7 @@
کال کے بارے میں تفصیلات شامل کرنے والی ایک JSON فائل بنائیں اور اسے آڈیو فائل کے بجائے رکھیں۔
ورژن
قواعد
- قاعدہ شامل کریں
- ایک اتو ریکارڈ قاعدہ شخص کے لئے شامل کریں۔
+ ایک اتو ریکارڈ قاعدہ شخص کے لئے شامل کریں۔
نیا قاعدہ شامل کر دیا گیا۔ اسے حذف کرنے کے لئے لانگ پریس کریں۔
قاعدہ پہلے سے موجود ہے۔
جب ایک قاعدہ چالو ہوتا ہے، کال خود بخود بطور افتراضی ریکارڈ ہوتی ہے اور اسے اطلاع کی زریعے سے دستیاب ریکارڈ کیسے بھی حذف کیا جا سکتا ہے۔ جب ایک قاعدہ بند ہوتا ہے، کال کے آخر میں ریکارڈ حذف کر دیا جائے گا مگر اگر آپ اطلاع کے ریاست بٹن کا استعمال کرکے اسے بحفظ کریں تو۔
@@ -29,7 +28,7 @@
نامعلوم کالوں
کسی بھی رابطے سے مطابقت نہ کرنے والی کالیں
رابطہ: %s
- قاعدہ ہٹانے کے لئے لانگ پریس کریں۔
+ قاعدہ ہٹانے کے لئے لانگ پریس کریں۔
ڈائریکٹری تبدیل کریں
فائل کا نام ٹیمپلیٹ
ٹیمپلیٹ میں ترمیم کریں
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 5ab30f5c8..5f26d159d 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -30,10 +30,9 @@
Phiên bản
-
+
Quy tắc
- Thêm quy tắc
- Thêm quy tắc tự động ghi âm cho một liên hệ.
+ Thêm quy tắc tự động ghi âm cho một liên hệ.
Đã thêm quy tắc mới. Nhấn giữ quy tắc để xóa.
Quy tắc đã tồn tại.
Khi một quy tắc được bật, cuộc gọi sẽ được tự động ghi âm theo mặc định và có thể xóa thủ công từ thông báo. Khi một quy tắc bị tắt, bản ghi âm sẽ bị xóa khi cuộc gọi kết thúc trừ khi nó được giữ lại bằng cách sử dụng nút Khôi phục trong thông báo.
@@ -44,7 +43,7 @@
Cuộc gọi không xác định
Các cuộc gọi không khớp với bất kỳ liên hệ nào.
Liên hệ: %s
- Nhấn giữ để xóa quy tắc.
+ Nhấn giữ để xóa quy tắc.
Thay đổi thư mục
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index d907e9d66..967c95c99 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -27,10 +27,9 @@
软件版本
-
+
规则
- 新增规则
- 从通讯录里面选择联系人添加到规则里面。
+ 从通讯录里面选择联系人添加到规则里面。
规则已添加。删除请长按规则。
规则已存在。
开启规则后,默认情况下会自动记录通话,并可以在通知中手动删除。当规则被禁用时,在通话结束时将删除记录,除非使用通知中的“还原”按钮保留记录。
@@ -41,7 +40,7 @@
未知号码
未匹配任何联系人的通话。
联系人: %s
- 长按以移除规则
+ 长按以移除规则
修改
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index a6d912c7e..ac94ef342 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -30,10 +30,9 @@
版本
-
+
規則
- 加入規則
- 加入聯絡人的自動錄音規則。
+ 加入聯絡人的自動錄音規則。
已加入新規則。長按規則以將其刪除。
規則已存在。
若啟用規則,預設會自動錄製通話,並可以在通知中手動刪除。若停用規則,通話結束時會刪除錄音檔,可按下通知中的還原按鈕保留錄音檔。
@@ -44,7 +43,7 @@
未知通話
不符合任何聯絡人的通話。
聯絡人:%s
- 長按以移除規則。
+ 長按以移除規則。
變更目錄
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ea0799c35..52d0ade97 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -48,10 +48,15 @@
Migrate direct boot recordings
Migrate recordings made before the first unlock. This normally happens automatically after the first unlock.
-
+
+ Pick contact group
+
+
Rules
- Add rule
- Add an auto-record rule for a contact.
+ Add contact rule
+ Add an auto-record rule for a contact.
+ Add contact group rule
+ Add an auto-record rule for a contact group.
Added new rule. Long press the rule to delete it.
Rule already exists.
When a rule is turned on, the call is automatically recorded by default and can be manually deleted from the notification. When a rule is turned off, the recording will be deleted at the end of a call unless it is preserved using the Restore button in the notification.
@@ -62,7 +67,8 @@
Unknown calls
Calls that don\'t match any contact.
Contact: %s
- Long press to remove rule.
+ Contact group: %s
+ Long press to remove rule.
Change directory
diff --git a/app/src/main/res/xml/record_rules_preferences.xml b/app/src/main/res/xml/record_rules_preferences.xml
index 87ef49438..ebd1d2b1a 100644
--- a/app/src/main/res/xml/record_rules_preferences.xml
+++ b/app/src/main/res/xml/record_rules_preferences.xml
@@ -12,10 +12,17 @@
app:iconSpaceReserved="false">
+
+