Skip to content

Commit a2a42a2

Browse files
committed
SpamUtils.kt: refactor checkSpamNumber and add clever dialer database
1 parent 10f1ccc commit a2a42a2

File tree

1 file changed

+115
-128
lines changed

1 file changed

+115
-128
lines changed

app/src/main/java/com/addev/listaspam/utils/SpamUtils.kt

Lines changed: 115 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import androidx.core.app.NotificationCompat
1414
import androidx.core.app.NotificationManagerCompat
1515
import com.addev.listaspam.R
1616
import com.addev.listaspam.model.SpamData
17-
import okhttp3.Call
18-
import okhttp3.Callback
17+
import kotlinx.coroutines.CoroutineScope
18+
import kotlinx.coroutines.Dispatchers
19+
import kotlinx.coroutines.launch
20+
import kotlinx.coroutines.withContext
1921
import okhttp3.OkHttpClient
2022
import okhttp3.Request
21-
import okhttp3.Response
2223
import org.jsoup.Jsoup
2324
import java.io.IOException
2425

@@ -30,104 +31,128 @@ class SpamUtils {
3031
companion object {
3132
const val SPAM_PREFS = "SPAM_PREFS"
3233
const val BLOCK_NUMBERS_KEY = "BLOCK_NUMBERS"
33-
const val SPAM_URL_TEMPLATE = "https://www.listaspam.com/busca.php?Telefono=%s"
34+
private const val NOTIFICATION_CHANNEL_ID = "NOTIFICATION_CHANNEL"
35+
private const val NOTIFICATION_ID = 1
36+
37+
// URLs
3438
const val REPORT_URL_TEMPLATE = "https://www.listaspam.com/busca.php?Telefono=%s#denuncia"
39+
const val LISTA_SPAM_URL_TEMPLATE = "https://www.listaspam.com/busca.php?Telefono=%s"
3540
private const val RESPONDERONO_URL_TEMPLATE =
3641
"https://www.responderono.es/numero-de-telefono/%s"
37-
private const val NOTIFICATION_CHANNEL_ID = "NOTIFICATION_CHANNEL"
38-
private const val NOTIFICATION_ID = 1
39-
private const val SPAM_REPORT_THRESHOLD = 1
42+
private const val CLEVER_DIALER_URL_TEMPLATE = "https://www.cleverdialer.es/numero/%s"
4043
}
4144

45+
private val client = OkHttpClient()
46+
4247
/**
43-
* Checks if a given phone number is considered spam by querying spam databases.
44-
* @param context Context for accessing resources.
45-
* @param number Phone number to check.
46-
* @param callback Callback function to handle the result.
48+
* Checks if a given phone number is spam by checking local blocklist and online databases.
49+
*
50+
* @param context The application context.
51+
* @param number The phone number to check.
52+
* @param callback A function to be called with the result (true if spam, false otherwise).
4753
*/
4854
fun checkSpamNumber(context: Context, number: String, callback: (isSpam: Boolean) -> Unit) {
49-
val sharedPreferences = context.getSharedPreferences(SPAM_PREFS, Context.MODE_PRIVATE)
50-
val blockedNumbers = sharedPreferences.getStringSet(BLOCK_NUMBERS_KEY, null)
55+
CoroutineScope(Dispatchers.IO).launch {
56+
if (isNumberBlockedLocally(context, number)) {
57+
handleSpamNumber(context, number, callback)
58+
return@launch
59+
}
5160

52-
// End call if the number is already blocked
53-
if (blockedNumbers?.contains(number) == true) {
54-
sendNotification(context, number)
55-
return callback(true)
56-
}
61+
val spamCheckers = listOf(
62+
::checkListaSpam,
63+
::checkResponderono,
64+
::checkCleverDialer
65+
)
5766

58-
val url = SPAM_URL_TEMPLATE.format(number)
59-
val request = Request.Builder().url(url).build()
60-
61-
OkHttpClient().newCall(request).enqueue(object : Callback {
62-
override fun onFailure(call: Call, e: IOException) {
63-
handleNetworkFailure(
64-
context,
65-
"Failed to check number in www.listaspam.com",
66-
e,
67-
callback
68-
)
67+
val isSpam = spamCheckers.any { checker ->
68+
runCatching { checker(number) }.getOrDefault(false)
6969
}
7070

71-
override fun onResponse(call: Call, response: Response) {
72-
response.body?.string()?.let { body ->
73-
val spamData = parseHtmlForSpamReports(body)
74-
if (spamData.reports > SPAM_REPORT_THRESHOLD) {
75-
handleSpamNumber(context, number, callback)
76-
} else {
77-
checkResponderono(context, number) { isResponderONoNegative ->
78-
if (isResponderONoNegative) {
79-
handleSpamNumber(context, number, callback)
80-
} else {
81-
handleNonSpamNumber(context, number, callback)
82-
}
83-
}
84-
}
85-
} ?: handleNetworkFailure(
86-
context,
87-
"Empty response from www.listaspam.com",
88-
null,
89-
callback
90-
)
71+
if (isSpam) {
72+
handleSpamNumber(context, number, callback)
73+
} else {
74+
handleNonSpamNumber(context, number, callback)
9175
}
92-
})
76+
}
9377
}
9478

9579
/**
96-
* Checks if a given phone number is considered negative by the ResponderONo database.
97-
* @param context Context for accessing resources.
98-
* @param number Phone number to check.
99-
* @param callback Callback function to handle the result.
80+
* Checks if a number is blocked locally in shared preferences.
81+
*
82+
* @param context The application context.
83+
* @param number The phone number to check.
84+
* @return True if the number is blocked locally, false otherwise.
10085
*/
101-
private fun checkResponderono(
102-
context: Context,
103-
number: String,
104-
callback: (isNegative: Boolean) -> Unit
105-
) {
106-
val url = RESPONDERONO_URL_TEMPLATE.format(number)
107-
val request = Request.Builder().url(url).build()
86+
private fun isNumberBlockedLocally(context: Context, number: String): Boolean {
87+
val sharedPreferences = context.getSharedPreferences(SPAM_PREFS, Context.MODE_PRIVATE)
88+
val blockedNumbers = sharedPreferences.getStringSet(BLOCK_NUMBERS_KEY, emptySet())
89+
if (blockedNumbers != null) {
90+
return blockedNumbers.contains(number)
91+
}
92+
return false
93+
}
10894

109-
OkHttpClient().newCall(request).enqueue(object : Callback {
110-
override fun onFailure(call: Call, e: IOException) {
111-
handleNetworkFailure(
112-
context,
113-
"Failed to check number in www.responderono.es",
114-
e,
115-
callback
116-
)
117-
}
95+
/**
96+
* Checks if a number is marked as spam on CleverDialer.
97+
*
98+
* @param number The phone number to check.
99+
* @return True if the number is marked as spam, false otherwise.
100+
*/
101+
private suspend fun checkCleverDialer(number: String): Boolean {
102+
val url = CLEVER_DIALER_URL_TEMPLATE.format(number)
103+
return checkUrlForSpam(
104+
url,
105+
".front-stars.stars-1, .front-stars.stars-2, .front-stars.stars-3"
106+
)
107+
}
118108

119-
override fun onResponse(call: Call, response: Response) {
120-
response.body?.string()?.let { body ->
121-
val isResponderONoNegative = body.contains(".scoreContainer .score.negative")
122-
callback(isResponderONoNegative)
123-
} ?: handleNetworkFailure(
124-
context,
125-
"Empty response from www.responderono.es",
126-
null,
127-
callback
128-
)
109+
/**
110+
* Checks if a number is marked as spam on ListaSpam.
111+
*
112+
* @param number The phone number to check.
113+
* @return True if the number is marked as spam, false otherwise.
114+
*/
115+
private suspend fun checkListaSpam(number: String): Boolean {
116+
val url = LISTA_SPAM_URL_TEMPLATE.format(number)
117+
return checkUrlForSpam(
118+
url,
119+
".phone_rating.result-3, .phone_rating.result-2, .phone_rating.result-1"
120+
)
121+
}
122+
123+
/**
124+
* Checks if a number is marked as spam on Responderono.
125+
*
126+
* @param number The phone number to check.
127+
* @return True if the number is marked as spam, false otherwise.
128+
*/
129+
private suspend fun checkResponderono(number: String): Boolean {
130+
val url = RESPONDERONO_URL_TEMPLATE.format(number)
131+
return checkUrlForSpam(url, ".scoreContainer .score.negative")
132+
}
133+
134+
/**
135+
* Checks a URL for spam indicators using a CSS selector.
136+
*
137+
* @param url The URL to check.
138+
* @param cssSelector The CSS selector to use for finding spam indicators.
139+
* @return True if spam indicators are found, false otherwise.
140+
*/
141+
private suspend fun checkUrlForSpam(url: String, cssSelector: String): Boolean {
142+
val request = Request.Builder().url(url).build()
143+
return withContext(Dispatchers.IO) {
144+
try {
145+
val response = client.newCall(request).execute()
146+
val body = response.body?.string()
147+
body?.let {
148+
val doc = Jsoup.parse(it)
149+
doc.select(cssSelector).isNotEmpty()
150+
} ?: false
151+
} catch (e: IOException) {
152+
e.printStackTrace()
153+
false
129154
}
130-
})
155+
}
131156
}
132157

133158
/**
@@ -164,26 +189,6 @@ class SpamUtils {
164189
callback(false)
165190
}
166191

167-
/**
168-
* Handles network failures by showing a toast message and logging the error.
169-
* @param context Context for accessing resources.
170-
* @param message Error message to display.
171-
* @param e Exception that occurred (optional).
172-
* @param callback Callback function to handle the result.
173-
*/
174-
private fun handleNetworkFailure(
175-
context: Context,
176-
message: String,
177-
e: IOException?,
178-
callback: (Boolean) -> Unit
179-
) {
180-
Handler(Looper.getMainLooper()).post {
181-
showToast(context, message, Toast.LENGTH_LONG)
182-
}
183-
e?.printStackTrace()
184-
callback(false)
185-
}
186-
187192
/**
188193
* Saves a phone number as spam in SharedPreferences.
189194
* @param context Context for accessing resources.
@@ -222,7 +227,7 @@ class SpamUtils {
222227
* @param message Message to display.
223228
* @param duration Duration of the toast display.
224229
*/
225-
private fun showToast(context: Context, message: String, duration: Int = Toast.LENGTH_SHORT) {
230+
private fun showToast(context: Context, message: String, duration: Int = Toast.LENGTH_LONG) {
226231
Handler(Looper.getMainLooper()).post {
227232
Toast.makeText(context, message, duration).show()
228233
}
@@ -258,33 +263,15 @@ class SpamUtils {
258263
* @param context Context for creating the notification channel.
259264
*/
260265
private fun createNotificationChannel(context: Context) {
261-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
262-
val name = "Spam Blocker Channel"
263-
val descriptionText = "Notifications for blocked spam numbers"
264-
val importance = NotificationManager.IMPORTANCE_HIGH
265-
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance).apply {
266-
description = descriptionText
267-
}
268-
269-
val notificationManager: NotificationManager =
270-
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
271-
notificationManager.createNotificationChannel(channel)
266+
val name = "Spam Blocker Channel"
267+
val descriptionText = "Notifications for blocked spam numbers"
268+
val importance = NotificationManager.IMPORTANCE_HIGH
269+
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance).apply {
270+
description = descriptionText
272271
}
273-
}
274-
275-
/**
276-
* Parses HTML content to extract spam report data.
277-
* @param html HTML content to parse.
278-
* @return [SpamData] containing the number of reports and searches.
279-
*/
280-
private fun parseHtmlForSpamReports(html: String): SpamData {
281-
val document = Jsoup.parse(html)
282-
val elementReports = document.select(".n_reports .result").first()
283-
val elementSearches = document.select(".n_search .result").first()
284-
285-
val reports = elementReports?.text()?.toIntOrNull() ?: 0
286-
val searches = elementSearches?.text()?.toIntOrNull() ?: 0
287272

288-
return SpamData(reports, searches, false)
273+
val notificationManager: NotificationManager =
274+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
275+
notificationManager.createNotificationChannel(channel)
289276
}
290277
}

0 commit comments

Comments
 (0)