Contact Whitelist added

This commit is contained in:
partisan 2025-05-13 08:31:02 +02:00
parent 663463cd38
commit 1691891a4d
13 changed files with 227 additions and 31 deletions

View file

@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity
class AboutActivity : AppCompatActivity() { class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// placeholder until real UI is added // placeholder
setContentView(androidx.appcompat.R.layout.abc_action_bar_title_item) setContentView(androidx.appcompat.R.layout.abc_action_bar_title_item)
} }
} }

View file

@ -0,0 +1,35 @@
package partisan.weforge.xyz.pulse
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class ContactAdapter(
private val prefs: Preferences,
private val contacts: List<ContactEntry>
) : RecyclerView.Adapter<ContactAdapter.ViewHolder>() {
inner class ViewHolder(inflater: LayoutInflater, parent: ViewGroup) :
RecyclerView.ViewHolder(inflater.inflate(R.layout.item_contact, parent, false)) {
val contactName: TextView = itemView.findViewById(R.id.contactName)
val contactAllowed: CheckBox = itemView.findViewById(R.id.contactAllowed)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context), parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val contact = contacts[position]
holder.contactName.text = contact.name
holder.contactAllowed.isChecked = prefs.isContactWhitelisted(contact.phoneNumber)
holder.contactAllowed.setOnCheckedChangeListener { _, isChecked ->
prefs.setContactWhitelisted(contact.phoneNumber, isChecked)
}
}
override fun getItemCount(): Int = contacts.size
}

View file

@ -0,0 +1,6 @@
package partisan.weforge.xyz.pulse
data class ContactEntry(
val name: String,
val phoneNumber: String
)

View file

@ -0,0 +1,75 @@
package partisan.weforge.xyz.pulse
import android.content.ContentResolver
import android.os.Bundle
import android.provider.ContactsContract
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import partisan.weforge.xyz.pulse.databinding.FragmentContactsBinding
class ContactsFragment : Fragment() {
private var _binding: FragmentContactsBinding? = null
private val binding get() = _binding!!
private lateinit var prefs: Preferences
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentContactsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
prefs = Preferences(requireContext())
val contacts = getContacts()
val adapter = ContactAdapter(prefs, contacts)
binding.contactRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.contactRecycler.adapter = adapter
}
private fun getContacts(): List<ContactEntry> {
val resolver: ContentResolver = requireContext().contentResolver
val projection = arrayOf(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
)
val cursor = resolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
projection,
null,
null,
"${ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME} ASC"
)
val results = mutableListOf<ContactEntry>()
cursor?.use {
val nameIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
val numberIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
while (it.moveToNext()) {
val name = it.getString(nameIndex) ?: continue
val number = it.getString(numberIndex) ?: continue
results.add(ContactEntry(name, number))
}
}
return results
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -33,6 +33,7 @@ class MainActivity : AppCompatActivity() {
) )
binding.drawerLayout.addDrawerListener(drawerToggle) binding.drawerLayout.addDrawerListener(drawerToggle)
drawerToggle.syncState() drawerToggle.syncState()
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, MainFragment()) .replace(R.id.fragmentContainer, MainFragment())
.commit() .commit()
@ -42,17 +43,21 @@ class MainActivity : AppCompatActivity() {
R.id.action_popup_settings -> { R.id.action_popup_settings -> {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, PopupSettingsFragment()) .replace(R.id.fragmentContainer, PopupSettingsFragment())
.addToBackStack(null)
.commit() .commit()
true true
} }
R.id.action_settings -> {
startActivity(Intent(this, SettingsActivity::class.java))
true
}
R.id.action_about -> { R.id.action_about -> {
startActivity(Intent(this, AboutActivity::class.java)) startActivity(Intent(this, AboutActivity::class.java))
true true
} }
R.id.action_contacts -> {
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, ContactsFragment())
.addToBackStack(null)
.commit()
true
}
else -> false else -> false
}.also { }.also {
binding.drawerLayout.closeDrawers() binding.drawerLayout.closeDrawers()

View file

@ -1,4 +1,3 @@
package partisan.weforge.xyz.pulse package partisan.weforge.xyz.pulse
import android.Manifest import android.Manifest
@ -25,4 +24,4 @@ fun hasDrawOverlays(context: Context): Boolean {
fun hasCallRedirectionRole(context: Context): Boolean { fun hasCallRedirectionRole(context: Context): Boolean {
val roleManager = context.getSystemService(RoleManager::class.java) val roleManager = context.getSystemService(RoleManager::class.java)
return roleManager?.isRoleHeld(RoleManager.ROLE_CALL_REDIRECTION) ?: false return roleManager?.isRoleHeld(RoleManager.ROLE_CALL_REDIRECTION) ?: false
} }

View file

@ -9,13 +9,12 @@ class Preferences(ctx: Context) {
private const val ENABLED = "enabled" private const val ENABLED = "enabled"
private const val REDIRECTION_DELAY = "redirection_delay" private const val REDIRECTION_DELAY = "redirection_delay"
private const val POPUP_POSITION = "popup_position_y" private const val POPUP_POSITION = "popup_position_y"
private const val POPUP_ENABLED = "popup_enabled"
private const val BLACKLISTED_CONTACTS = "blacklisted_contacts"
private const val DEFAULT_REDIRECTION_DELAY = 2000L private const val DEFAULT_REDIRECTION_DELAY = 2000L
private const val DEFAULT_POPUP_POSITION = 333 private const val DEFAULT_POPUP_POSITION = 333
private const val SERVICE_ENABLED = "service_enabled" private const val SERVICE_ENABLED = "service_enabled"
private const val POPUP_ENABLED = "popup_enabled"
} }
private val prefs = PreferenceManager.getDefaultSharedPreferences(ctx) private val prefs = PreferenceManager.getDefaultSharedPreferences(ctx)
@ -39,23 +38,37 @@ class Preferences(ctx: Context) {
private fun makeKeyEnabled(mimetype: String) = "enabled_$mimetype" private fun makeKeyEnabled(mimetype: String) = "enabled_$mimetype"
private fun makeKeyPriority(mimetype: String) = "priority_$mimetype" private fun makeKeyPriority(mimetype: String) = "priority_$mimetype"
/** Whether this service is enabled */
fun isServiceEnabled(mimetype: String): Boolean { fun isServiceEnabled(mimetype: String): Boolean {
return prefs.getBoolean(makeKeyEnabled(mimetype), true) return prefs.getBoolean(makeKeyEnabled(mimetype), true)
} }
/** Current priority for this service (lower = higher priority) */
fun getServicePriority(mimetype: String): Int { fun getServicePriority(mimetype: String): Int {
return prefs.getInt(makeKeyPriority(mimetype), Int.MAX_VALUE) return prefs.getInt(makeKeyPriority(mimetype), Int.MAX_VALUE)
} }
/** Enable or disable individual service */
fun setServiceEnabled(mimetype: String, enabled: Boolean) { fun setServiceEnabled(mimetype: String, enabled: Boolean) {
prefs.edit().putBoolean(makeKeyEnabled(mimetype), enabled).apply() prefs.edit().putBoolean(makeKeyEnabled(mimetype), enabled).apply()
} }
/** Change priority for an individual service */
fun setServicePriority(mimetype: String, priority: Int) { fun setServicePriority(mimetype: String, priority: Int) {
prefs.edit().putInt(makeKeyPriority(mimetype), priority).apply() prefs.edit().putInt(makeKeyPriority(mimetype), priority).apply()
} }
var blacklistedContacts: Set<String>
get() = prefs.getStringSet(BLACKLISTED_CONTACTS, emptySet()) ?: emptySet()
set(value) = prefs.edit { putStringSet(BLACKLISTED_CONTACTS, value) }
fun isContactWhitelisted(phoneNumber: String): Boolean {
return !blacklistedContacts.contains(phoneNumber)
}
fun setContactWhitelisted(phoneNumber: String, allowed: Boolean) {
val current = blacklistedContacts.toMutableSet()
if (!allowed) {
current.add(phoneNumber)
} else {
current.remove(phoneNumber)
}
blacklistedContacts = current
}
} }

View file

@ -1,12 +0,0 @@
package partisan.weforge.xyz.pulse
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// placeholder until real UI is added
setContentView(androidx.appcompat.R.layout.abc_action_bar_title_item)
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M40,800L40,688Q40,654 57.5,625.5Q75,597 104,582Q166,551 230,535.5Q294,520 360,520Q426,520 490,535.5Q554,551 616,582Q645,597 662.5,625.5Q680,654 680,688L680,800L40,800ZM760,800L760,680Q760,636 735.5,595.5Q711,555 666,526Q717,532 762,546.5Q807,561 846,582Q882,602 901,626.5Q920,651 920,680L920,800L760,800ZM360,480Q294,480 247,433Q200,386 200,320Q200,254 247,207Q294,160 360,160Q426,160 473,207Q520,254 520,320Q520,386 473,433Q426,480 360,480ZM760,320Q760,386 713,433Q666,480 600,480Q589,480 572,477.5Q555,475 544,472Q571,440 585.5,401Q600,362 600,320Q600,278 585.5,239Q571,200 544,168Q558,163 572,161.5Q586,160 600,160Q666,160 713,207Q760,254 760,320ZM120,720L600,720L600,688Q600,677 594.5,668Q589,659 580,654Q526,627 471,613.5Q416,600 360,600Q304,600 249,613.5Q194,627 140,654Q131,659 125.5,668Q120,677 120,688L120,720ZM360,400Q393,400 416.5,376.5Q440,353 440,320Q440,287 416.5,263.5Q393,240 360,240Q327,240 303.5,263.5Q280,287 280,320Q280,353 303.5,376.5Q327,400 360,400ZM360,720L360,720L360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720ZM360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M440,680L520,680L520,440L440,440L440,680ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contactRecycler"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:clipToPadding="false"
android:paddingBottom="8dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:id="@+id/contactName"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="Name" />
<CheckBox
android:id="@+id/contactAllowed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>

View file

@ -1,14 +1,30 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Settings section -->
<item <item
android:id="@+id/action_settings" android:id="@+id/section_settings"
android:title="Settings" /> android:title="Settings"
<item android:enabled="false" />
android:id="@+id/action_about"
android:title="About" />
<item <item
android:id="@+id/action_popup_settings" android:id="@+id/action_popup_settings"
android:title="Popup Settings" android:title="Popup Settings"
android:icon="@drawable/tooltip_24px" android:icon="@drawable/tooltip_24px"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_contacts"
android:title="Contacts"
android:icon="@drawable/group_24px"
app:showAsAction="never" />
<!-- About section -->
<item
android:id="@+id/section_about"
android:title="About"
android:enabled="false" />
<item
android:id="@+id/action_about"
android:title="About"
android:icon="@drawable/info_24px"
app:showAsAction="never" />
</menu> </menu>