Skip to content

Commit

Permalink
v3.0 ready
Browse files Browse the repository at this point in the history
  • Loading branch information
aj3423 committed Oct 13, 2024
1 parent 79f912b commit 6ce34cc
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 129 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
buildToolsVersion: 34.0.0

- name: Compress mapping.txt
run: tar zcvf mapping.txt.tar.gz app/build/outputs/mapping/release/mapping.txt
run: tar zcvf debug.symbol.tar.gz app/build/outputs/mapping/release/mapping.txt

- name: Publish Latest Release
uses: "marvinpinto/action-automatic-releases@latest"
Expand All @@ -38,4 +38,4 @@
title: "Latest Release"
files: |
${{ steps.sign_app.outputs.signedFile }}
mapping.txt.tar.gz
debug.symbol.tar.gz
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ It works without replacing your default Call/SMS app.
| Dialed | Whether you have dialed the number |
| Recent Apps | If some specific apps have been used recently, all calls are allowed.<br>Use case:<br>&emsp; You ordered Pizza online and soon they call you to refund. |
| Off Time | A time period that always permits calls, usually no spams at night. |
| Spam Database | If it matches any spam number in the database, any public downloadable spam databases can be integrated, such as the [DNC](https://www.ftc.gov/policy-notices/open-government/data-sets/do-not-call-data) |
| Spam Database | If it matches any spam number in the database. Any public downloadable spam databases can be integrated, such as the [DNC](https://www.ftc.gov/policy-notices/open-government/data-sets/do-not-call-data). |
| Regex Pattern | Some typical patterns:<br> - Any number: `.*` (the regex `.*` is equivalent to the wildcard `*` in other apps) <br> - Exact number: `12345` <br> - Starts with 400: `400.*` <br> - Ends with 123: `.*123` <br> - Shorter than 5: `.{0,4}` <br> - Longer than 10: `.{11,}` <br> - Unknown number (it's empty string): `.{0}` or `^$`<br> - Contains "verification": `.*verification.*` <br> - Contains any of the words: `.*(police\|hospital\|verification).*` <br> - Starts with 400, with leading country code 11 or not: `(?:11)?400.*` <br>- Extract verification code from SMS message: `code.*?(\d+)`<br><br> Ask AI to generate or explain a regex: <br>&emsp; "Show me regex for checking if a string starts with 400 or 200"<br> &emsp; Results in `(400\|200).*` |


Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ android {
applicationId = "spam.blocker"
minSdk = 29
targetSdk = 35
versionCode = 202
versionName = "2.2"
versionCode = 300
versionName = "3.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
84 changes: 0 additions & 84 deletions app/src/androidTest/java/spam/blocker/util/RecurringTest.kt

This file was deleted.

87 changes: 87 additions & 0 deletions app/src/androidTest/java/spam/blocker/util/ScheduleTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package spam.blocker.util

import io.mockk.every
import io.mockk.mockkObject
import org.junit.Assert.assertEquals
import org.junit.Test
import spam.blocker.service.bot.Daily
import spam.blocker.service.bot.Time
import spam.blocker.service.bot.Weekly
import java.time.DayOfWeek.THURSDAY
import java.time.DayOfWeek.TUESDAY
import java.time.DayOfWeek.WEDNESDAY
import java.time.Duration
import java.time.LocalDateTime

class ScheduleTest {

@Test
fun timeIsValid() {
assertEquals(Time(0, 0).isValid(), true)
assertEquals(Time(23, 59).isValid(), true)
assertEquals(Time(-1, 0).isValid(), false)
assertEquals(Time(24, 0).isValid(), false)
assertEquals(Time(0, 60).isValid(), false)
assertEquals(Time(0, 60).isValid(), false)
}

private fun setNow(now: LocalDateTime) {
every { LocalDateTimeMockk.now() } returns now
}

@Test
fun daily() {
mockkObject(LocalDateTimeMockk)

var dur: Duration

// now: <2000-1-1 0:0>, time: <1:0> -> dur: 1 hour
setNow(LocalDateTime.of(2000, 1, 1, 0, 0, 0))
dur = Daily(Time(1, 0)).nextOccurrence()
assertEquals("same day", 1 * 3600 * 1000, dur.toMillis())

// now: <2000-1-1 0:0>, time: <0:0> -> dur: 24 hours
setNow(LocalDateTime.of(2000, 1, 1, 0, 0, 0))
dur = Daily(Time(0, 0)).nextOccurrence()
assertEquals("next day", 24 * 3600 * 1000, dur.toMillis())

// now: <2023-12-31 10:0>, time: <0:0> -> dur: 14 hours
setNow(LocalDateTime.of(2023, 12, 31, 10, 0, 0))
dur = Daily(Time(0, 0)).nextOccurrence()
assertEquals("cross year", 14 * 3600 * 1000, dur.toMillis())
}

@Test
fun weekly() {
mockkObject(LocalDateTimeMockk)

var dur: Duration
val hour = (1 * 3600 * 1000).toLong()
val day = (24 * hour).toLong()

// now: <2024-10-1 Tuesday 0:0:0>, weekdays: [Tuesday], time: <1:0:0> -> dur: 1 hour
setNow(LocalDateTime.parse("2024-10-01T00:00:00"))
dur = Weekly(listOf(TUESDAY), Time(1, 0)).nextOccurrence()
assertEquals("same day", 1 * hour, dur.toMillis())

// now: <2024-10-1 Tuesday 0:0:0>, weekdays: [Tuesday, Wednesday], time: <0:0:0> -> dur: 1 day
setNow(LocalDateTime.parse("2024-10-01T00:00:00"))
dur = Weekly(listOf(TUESDAY, WEDNESDAY), Time(0, 0)).nextOccurrence()
assertEquals("next day", 1 * day, dur.toMillis())

// now: <2024-10-1 Tuesday 0:0:0>, weekdays: [Tuesday], time: <0:0:0> -> dur: 7 days
setNow(LocalDateTime.parse("2024-10-01T00:00:00"))
dur = Weekly(listOf(TUESDAY), Time(0, 0)).nextOccurrence()
assertEquals("next week", 7 * day, dur.toMillis())

// now: <2024-10-31 Thursday 0:0:0>, weekdays: [Thursday], time: <0:0:0> -> dur: 7 days
setNow(LocalDateTime.parse("2024-10-31T00:00:00"))
dur = Weekly(listOf(THURSDAY), Time(0, 0)).nextOccurrence()
assertEquals("cross month", 7 * day, dur.toMillis())

// now: <2024-12-31 Tuesday 0:0:0>, weekdays: [Tuesday], time: <0:0:0> -> dur: 7 days
setNow(LocalDateTime.parse("2024-12-31T00:00:00"))
dur = Weekly(listOf(TUESDAY), Time(0, 0)).nextOccurrence()
assertEquals("cross year", 7 * day, dur.toMillis())
}
}
32 changes: 18 additions & 14 deletions app/src/main/java/spam/blocker/service/CallScreeningService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ import spam.blocker.util.Util
import spam.blocker.util.logd
import android.telecom.Call as TelecomCall

fun TelecomCall.Details.getRawNumber():String {
var rawNumber = ""
if (handle != null) {
rawNumber = handle.schemeSpecificPart
} else if (gatewayInfo?.originalAddress != null){
rawNumber = gatewayInfo?.originalAddress?.schemeSpecificPart!!
} else if (intentExtras != null) {
var uri = intentExtras.getParcelable<Uri>(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS)
if (uri == null) {
uri = intentExtras.getParcelable<Uri>(TelephonyManager.EXTRA_INCOMING_NUMBER);
}
if (uri != null) {
rawNumber = uri.schemeSpecificPart
}
}
return rawNumber
}

class CallScreeningService : CallScreeningService() {

Expand Down Expand Up @@ -73,20 +90,7 @@ class CallScreeningService : CallScreeningService() {
return
}

var rawNumber = ""
if (details.handle != null) {
rawNumber = details.handle.schemeSpecificPart
} else if (details.gatewayInfo?.originalAddress != null){
rawNumber = details.gatewayInfo?.originalAddress?.schemeSpecificPart!!
} else if (details.intentExtras != null) {
var uri = details.intentExtras.getParcelable<Uri>(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS)
if (uri == null) {
uri = details.intentExtras.getParcelable<Uri>(TelephonyManager.EXTRA_INCOMING_NUMBER);
}
if (uri != null) {
rawNumber = uri.schemeSpecificPart
}
}
val rawNumber = details.getRawNumber()

val r = processCall(this, rawNumber, details)

Expand Down
46 changes: 25 additions & 21 deletions app/src/main/java/spam/blocker/util/PermissionChain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,52 +25,56 @@ import spam.blocker.util.Util.doOnce


abstract class IPermission {
abstract fun name(): String
abstract fun isGranted(ctx: Context): Boolean
abstract val name: String
abstract val isOptional: Boolean
abstract fun isGranted(ctx: Context): Boolean

// Show a prompt dialog before asking for permission, explaining for why it's required
abstract val prompt: String?
}

open class NormalPermission(
open val name: String,
override val name: String,
override val isOptional: Boolean = false,
val prompt: String? = null // show a prompt dialog before asking for permission
override val prompt: String? = null
) : IPermission() {
override fun name(): String{ return name }
override fun isGranted(ctx: Context): Boolean {
return Permissions.isPermissionGranted(ctx, name)
}
}

// For those permissions that will launch an Intent Activity, such as UsageStats and AllFileAccess
open class IntentPermission(
isOptional: Boolean = false,
prompt: String? = null,
override val isOptional: Boolean = false,
override val prompt: String? = null,

// intent for opening system setting page
val intent: Intent,
val checkGranted: (ctx: Context) -> Boolean
) : NormalPermission(intent.action!!, isOptional, prompt) {
val isGrantedChecker: (ctx: Context) -> Boolean,
) : IPermission() {

override val name: String
get() = intent.action!!

override fun name(): String{ return intent.action!! }
override fun isGranted(ctx: Context): Boolean {
return checkGranted(ctx)
return isGrantedChecker(ctx)
}
}

// For UsageStats
// For permissions like UsageStats
class AppOpsPermission(
override val name: String,
intent: Intent,
isOptional: Boolean = false,
prompt: String? = null,
intent: Intent,
) : IntentPermission(
isOptional, prompt, intent, { ctx ->
isOptional = isOptional,
prompt = prompt,
intent = intent,
isGrantedChecker = { ctx ->
Permissions.isAppOpsPermissionGranted(ctx, name)
}
) {

override fun name(): String{ return name }
}
)

/*
Convenient class for asking for multiple permissions,
Expand All @@ -93,8 +97,8 @@ class PermissionChain(
// final callback
private lateinit var onResult: (Boolean) -> Unit

private lateinit var currList: MutableList<NormalPermission>
private lateinit var curr: NormalPermission
private lateinit var currList: MutableList<IPermission>
private lateinit var curr: IPermission

private lateinit var popupTrigger: MutableState<Boolean>

Expand Down Expand Up @@ -148,7 +152,7 @@ class PermissionChain(

fun ask(
ctx: Context,
permissions: List<NormalPermission>,
permissions: List<IPermission>,
onResult: (Boolean) -> Unit
) {
this.onResult = onResult
Expand Down
16 changes: 11 additions & 5 deletions app/src/main/java/spam/blocker/util/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ fun String.resolveTimeTags(): String {
val now = LocalDateTime.now()
return this
.replace("{year}", now.year.toString())
.replace("{month}", now.monthValue.toString())
.replace("{day}", now.dayOfMonth.toString())
.replace("{hour}", now.hour.toString())
.replace("{minute}", now.minute.toString())
.replace("{second}", now.second.toString())
.replace("{month}", now.monthValue.toString().padStart(2, '0'))
.replace("{day}", now.dayOfMonth.toString().padStart(2, '0'))
.replace("{hour}", now.hour.toString().padStart(2, '0'))
.replace("{minute}", now.minute.toString().padStart(2, '0'))
.replace("{second}", now.second.toString().padStart(2, '0'))

.replace("{month_index}", now.monthValue.toString())
.replace("{day_index}", now.dayOfMonth.toString())
.replace("{hour_index}", now.hour.toString())
.replace("{minute_index}", now.minute.toString())
.replace("{second_index}", now.second.toString())
}

fun String.resolvePathTags(): String {
Expand Down
17 changes: 17 additions & 0 deletions metadata/en-US/changelogs/300.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Breaking:
- no longer support "export backup as json format"
- New permissions:
- INTERNET: for downloading spam database, you can [disable it](https://github.com/aj3423/SpamBlocker/issues/147) if you don't use this feature.
- MANAGE_EXTERNAL_STORAGE(Android 11+) or READ/WRITE_EXTERNAL_STORAGE(Android 10), for automated file operations, such as "backup" or "import .csv".

New:
- Spam Database. It's compatible with any public data sources, such as the [FTC - DNC](ftc.gov/policy-notices/open-government/data-sets/do-not-call-data)
- Automated workflow. Use cases:
- Download spam numbers from public database everyday.
- Clean up expired numbers in database.
- Auto backup, auto switch configuration, auto import csv/xml, etc.
- Regex contact mode. The regex will match the contact name instead of the phone number
- language support: pt-rBR, ja

Fix:
- The history cleanup task is not applied after backup-import

0 comments on commit 6ce34cc

Please sign in to comment.