Skip to content

Commit 46356cf

Browse files
committed
Add features: manually block/unlock, whitelist/unwhitelist and search in Google
1 parent 0462f1b commit 46356cf

File tree

23 files changed

+497
-256
lines changed

23 files changed

+497
-256
lines changed

.idea/misc.xml

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
android:icon="@mipmap/ic_launcher"
2020
android:label="@string/app_name"
2121
android:supportsRtl="true"
22-
android:theme="@style/Theme.ListaSpam"
23-
tools:targetApi="28">
22+
android:theme="@style/Theme.ListaSpam">
2423
<activity
2524
android:name=".SettingsActivity"
2625
android:exported="false" />
@@ -35,7 +34,7 @@
3534
</activity>
3635

3736
<service
38-
android:name=".services.MyCallScreeningService"
37+
android:name=".service.MyCallScreeningService"
3938
android:exported="true"
4039
android:permission="android.permission.BIND_SCREENING_SERVICE">
4140
<intent-filter>
@@ -44,7 +43,7 @@
4443
</service>
4544

4645
<receiver
47-
android:name=".services.MyCallReceiver"
46+
android:name=".service.MyCallReceiver"
4847
android:exported="true">
4948
<intent-filter>
5049
<action android:name="android.intent.action.PHONE_STATE" />

app/src/main/java/com/addev/listaspam/MainActivity.kt

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,35 @@ import androidx.activity.result.ActivityResultLauncher
1919
import androidx.activity.result.contract.ActivityResultContracts
2020
import androidx.annotation.RequiresApi
2121
import androidx.appcompat.app.AppCompatActivity
22-
import androidx.core.app.ActivityCompat
2322
import androidx.core.content.ContextCompat
2423
import androidx.core.view.ViewCompat
2524
import androidx.core.view.WindowInsetsCompat
2625
import androidx.recyclerview.widget.LinearLayoutManager
2726
import androidx.recyclerview.widget.RecyclerView
28-
import com.addev.listaspam.calllog.CallLogAdapter
29-
import com.addev.listaspam.calllog.getCallLogs
30-
import com.addev.listaspam.utils.SpamUtils
27+
import com.addev.listaspam.adapter.CallLogAdapter
28+
import com.addev.listaspam.util.BLOCK_NUMBERS_KEY
29+
import com.addev.listaspam.util.SPAM_PREFS
30+
import com.addev.listaspam.util.getCallLogs
31+
import com.addev.listaspam.util.SpamUtils
32+
import com.addev.listaspam.util.WHITELIST_NUMBERS_KEY
33+
import com.addev.listaspam.util.getBlockedNumbers
34+
import com.addev.listaspam.util.getWhitelistNumbers
3135

32-
class MainActivity : AppCompatActivity() {
36+
class MainActivity : AppCompatActivity(), CallLogAdapter.OnItemChangedListener {
3337

3438
private lateinit var intentLauncher: ActivityResultLauncher<Intent>
3539
private var permissionDeniedDialog: AlertDialog? = null
36-
40+
private var callLogAdapter: CallLogAdapter? =null
41+
private var recyclerView: RecyclerView? = null
3742

3843
override fun onCreate(savedInstanceState: Bundle?) {
3944
super.onCreate(savedInstanceState)
4045
setContentView(R.layout.activity_main)
4146
setupWindowInsets()
4247
setupIntentLauncher()
48+
49+
recyclerView = findViewById(R.id.recyclerView)
50+
recyclerView?.layoutManager = LinearLayoutManager(this)
4351
}
4452

4553
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@@ -58,6 +66,16 @@ class MainActivity : AppCompatActivity() {
5866
}
5967
}
6068

69+
override fun onItemChanged(number: String) {
70+
val positions = mutableListOf<Int>()
71+
callLogAdapter?.callLogs?.forEachIndexed { index, callLog ->
72+
if (callLog.number == number) {
73+
positions.add(index)
74+
}
75+
}
76+
refreshCallLogs(positions)
77+
}
78+
6179
private fun init() {
6280
checkPermissionsAndRequest()
6381

@@ -77,18 +95,29 @@ class MainActivity : AppCompatActivity() {
7795
init()
7896
}
7997

80-
private fun refreshCallLogs() {
81-
val blockedNumbers = getBlockedNumbers()
98+
private fun refreshCallLogs(positions: List<Int> = listOf()) {
99+
val blockedNumbers = getBlockedNumbers(this)
100+
val whitelistNumbers = getWhitelistNumbers(this)
101+
82102
val callLogs = getCallLogs(this)
83103

84-
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
85-
recyclerView.layoutManager = LinearLayoutManager(this)
86-
recyclerView.adapter = CallLogAdapter(this, callLogs, blockedNumbers)
87-
}
104+
if (callLogAdapter == null) {
105+
callLogAdapter = CallLogAdapter(this, callLogs, blockedNumbers, whitelistNumbers)
106+
recyclerView?.adapter = callLogAdapter
107+
callLogAdapter?.setOnItemChangedListener(this)
108+
} else {
109+
callLogAdapter?.callLogs = callLogs
110+
callLogAdapter?.blockedNumbers = blockedNumbers
111+
callLogAdapter?.whitelistNumbers = whitelistNumbers
112+
}
88113

89-
private fun getBlockedNumbers(): Set<String> {
90-
val sharedPreferences = getSharedPreferences(SpamUtils.SPAM_PREFS, Context.MODE_PRIVATE)
91-
return sharedPreferences.getStringSet(SpamUtils.BLOCK_NUMBERS_KEY, emptySet()) ?: emptySet()
114+
if (positions.isNotEmpty()) {
115+
positions.forEach { position ->
116+
callLogAdapter?.notifyItemChanged(position)
117+
}
118+
} else {
119+
callLogAdapter?.notifyDataSetChanged()
120+
}
92121
}
93122

94123
private fun setupWindowInsets() {

app/src/main/java/com/addev/listaspam/SettingsActivity.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package com.addev.listaspam
22

33
import android.os.Bundle
4-
import androidx.activity.enableEdgeToEdge
54
import androidx.appcompat.app.AppCompatActivity
6-
import androidx.core.view.ViewCompat
7-
import androidx.core.view.WindowInsetsCompat
85
import androidx.preference.PreferenceFragmentCompat
96

107
class SettingsActivity : AppCompatActivity() {
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package com.addev.listaspam.adapter
2+
3+
import android.content.ClipData
4+
import android.content.ClipboardManager
5+
import android.content.Context
6+
import android.content.Intent
7+
import android.net.Uri
8+
import android.view.Gravity
9+
import android.view.LayoutInflater
10+
import android.view.View
11+
import android.view.ViewGroup
12+
import android.widget.ImageButton
13+
import android.widget.PopupMenu
14+
import android.widget.TextView
15+
import android.widget.Toast
16+
import androidx.core.content.ContextCompat
17+
import androidx.recyclerview.widget.RecyclerView
18+
import com.addev.listaspam.R
19+
import com.addev.listaspam.util.CallLogEntry
20+
import com.addev.listaspam.util.SpamUtils
21+
import com.addev.listaspam.util.addNumberToWhitelist
22+
import com.addev.listaspam.util.removeSpamNumber
23+
import com.addev.listaspam.util.removeWhitelistNumber
24+
import com.addev.listaspam.util.saveSpamNumber
25+
import java.text.SimpleDateFormat
26+
27+
class CallLogAdapter(
28+
private val context: Context,
29+
var callLogs: List<CallLogEntry>,
30+
var blockedNumbers: Set<String>,
31+
var whitelistNumbers: Set<String>
32+
) : RecyclerView.Adapter<CallLogAdapter.CallLogViewHolder>() {
33+
34+
interface OnItemChangedListener {
35+
fun onItemChanged(number: String)
36+
}
37+
38+
companion object {
39+
// URLs
40+
const val GOOGLE_URL_TEMPLATE = "https://www.google.com/search?q=%s"
41+
const val REPORT_URL_TEMPLATE = "https://www.listaspam.com/busca.php?Telefono=%s#denuncia"
42+
}
43+
44+
private val formatter = SimpleDateFormat("dd/MM/yyyy HH:ss")
45+
private var onItemChangedListener: OnItemChangedListener? = null
46+
47+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CallLogViewHolder {
48+
val view = LayoutInflater.from(context).inflate(R.layout.item_call_log, parent, false)
49+
return CallLogViewHolder(view)
50+
}
51+
52+
override fun onBindViewHolder(holder: CallLogViewHolder, position: Int) {
53+
val callLog = callLogs[position]
54+
holder.bind(callLog, blockedNumbers.contains(callLog.number), whitelistNumbers.contains(callLog.number))
55+
}
56+
57+
override fun getItemCount(): Int = callLogs.size
58+
59+
inner class CallLogViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
60+
private val numberTextView: TextView = itemView.findViewById(R.id.numberTextView)
61+
private val dateTextView: TextView = itemView.findViewById(R.id.dateTextView)
62+
private val durationTextView: TextView = itemView.findViewById(R.id.durationTextView)
63+
private val overflowMenuButton = itemView.findViewById<ImageButton>(R.id.overflowMenuButton)
64+
65+
fun bind(callLog: CallLogEntry, isBlocked: Boolean, isWhitelisted: Boolean = false) {
66+
val number = callLog.number ?: ""
67+
val textToShow = if (isBlocked) {
68+
context.getString(R.string.blocked_text_format, number)
69+
} else if (isWhitelisted) {
70+
context.getString(R.string.whitelisted_text_format, number)
71+
} else {
72+
number
73+
}
74+
numberTextView.text = textToShow
75+
dateTextView.text = formatter.format(callLog.date)
76+
durationTextView.text = context.getString(R.string.duration_label, callLog.duration)
77+
78+
if (isBlocked) {
79+
numberTextView.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_light))
80+
} else if (isWhitelisted) {
81+
numberTextView.setTextColor(ContextCompat.getColor(context, android.R.color.holo_blue_dark))
82+
} else {
83+
numberTextView.setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
84+
}
85+
86+
overflowMenuButton.setOnClickListener {
87+
val popupMenu = PopupMenu(itemView.context, overflowMenuButton, Gravity.NO_GRAVITY, android.R.attr.popupMenuStyle, R.style.PopupMenuStyle)
88+
popupMenu.inflate(R.menu.item_actions)
89+
90+
setDynamicTitles(popupMenu, isBlocked, isWhitelisted)
91+
92+
popupMenu.setOnMenuItemClickListener { menuItem ->
93+
when (menuItem.itemId) {
94+
R.id.search_action -> {
95+
searchAction(number)
96+
true
97+
}
98+
R.id.report_action -> {
99+
reportAction(number)
100+
true
101+
}
102+
R.id.whitelist_action -> {
103+
if (isWhitelisted) {
104+
removeWhitelistNumber(context, number)
105+
} else {
106+
addNumberToWhitelist(context, number)
107+
}
108+
onItemChangedListener?.onItemChanged(number)
109+
true
110+
}
111+
R.id.block_action -> {
112+
if (isBlocked) {
113+
removeSpamNumber(context, number)
114+
} else {
115+
saveSpamNumber(context, number)
116+
}
117+
onItemChangedListener?.onItemChanged(number)
118+
true
119+
}
120+
else -> false
121+
}
122+
}
123+
popupMenu.show()
124+
}
125+
126+
// Copy number to clipboard
127+
itemView.setOnLongClickListener {
128+
clipboardAction(number)
129+
true
130+
}
131+
}
132+
133+
private fun setDynamicTitles(
134+
popupMenu: PopupMenu,
135+
isBlocked: Boolean,
136+
isWhitelisted: Boolean
137+
) {
138+
val blockMenuItem = popupMenu.menu.findItem(R.id.block_action)
139+
val whitelistMenuItem = popupMenu.menu.findItem(R.id.whitelist_action)
140+
if (isBlocked) {
141+
blockMenuItem.setTitle(R.string.unblock)
142+
} else {
143+
blockMenuItem.setTitle(R.string.block)
144+
}
145+
146+
if (isWhitelisted) {
147+
whitelistMenuItem.setTitle(R.string.remove_from_whitelist)
148+
} else {
149+
whitelistMenuItem.setTitle(R.string.add_to_whitelist)
150+
}
151+
}
152+
}
153+
154+
private fun clipboardAction(number: String) {
155+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
156+
val clip = ClipData.newPlainText("phone number", number)
157+
clipboard.setPrimaryClip(clip)
158+
Toast.makeText(
159+
context,
160+
context.getString(R.string.number_copied_to_clipboard),
161+
Toast.LENGTH_SHORT
162+
).show()
163+
}
164+
165+
private fun reportAction(number: String) {
166+
val url = String.format(REPORT_URL_TEMPLATE, number)
167+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
168+
context.startActivity(intent)
169+
}
170+
171+
private fun searchAction(number: String) {
172+
val url = String.format(GOOGLE_URL_TEMPLATE, number)
173+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
174+
context.startActivity(intent)
175+
}
176+
177+
fun setOnItemChangedListener(listener: OnItemChangedListener) {
178+
this.onItemChangedListener = listener
179+
}
180+
}

0 commit comments

Comments
 (0)