@@ -14,11 +14,12 @@ import androidx.core.app.NotificationCompat
1414import androidx.core.app.NotificationManagerCompat
1515import com.addev.listaspam.R
1616import 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
1921import okhttp3.OkHttpClient
2022import okhttp3.Request
21- import okhttp3.Response
2223import org.jsoup.Jsoup
2324import 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