From 6ce34cc9c535d0c3f40623e1705b2f0cdcada5ba Mon Sep 17 00:00:00 2001 From: aj3423 Date: Sun, 13 Oct 2024 08:24:39 +0800 Subject: [PATCH] v3.0 ready --- .github/workflows/latest.yml | 4 +- README.md | 2 +- app/build.gradle.kts | 4 +- .../java/spam/blocker/util/RecurringTest.kt | 84 ------------------ .../java/spam/blocker/util/ScheduleTest.kt | 87 +++++++++++++++++++ .../blocker/service/CallScreeningService.kt | 32 ++++--- .../java/spam/blocker/util/PermissionChain.kt | 46 +++++----- app/src/main/java/spam/blocker/util/Util.kt | 16 ++-- metadata/en-US/changelogs/300.txt | 17 ++++ 9 files changed, 163 insertions(+), 129 deletions(-) delete mode 100644 app/src/androidTest/java/spam/blocker/util/RecurringTest.kt create mode 100644 app/src/androidTest/java/spam/blocker/util/ScheduleTest.kt create mode 100644 metadata/en-US/changelogs/300.txt diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml index f101fb0a..bb94a5b7 100644 --- a/.github/workflows/latest.yml +++ b/.github/workflows/latest.yml @@ -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" @@ -38,4 +38,4 @@ title: "Latest Release" files: | ${{ steps.sign_app.outputs.signedFile }} - mapping.txt.tar.gz + debug.symbol.tar.gz diff --git a/README.md b/README.md index 66751bd4..daab6c42 100644 --- a/README.md +++ b/README.md @@ -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.
Use case:
  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:
- Any number: `.*` (the regex `.*` is equivalent to the wildcard `*` in other apps)
- Exact number: `12345`
- Starts with 400: `400.*`
- Ends with 123: `.*123`
- Shorter than 5: `.{0,4}`
- Longer than 10: `.{11,}`
- Unknown number (it's empty string): `.{0}` or `^$`
- Contains "verification": `.*verification.*`
- Contains any of the words: `.*(police\|hospital\|verification).*`
- Starts with 400, with leading country code 11 or not: `(?:11)?400.*`
- Extract verification code from SMS message: `code.*?(\d+)`

Ask AI to generate or explain a regex:
  "Show me regex for checking if a string starts with 400 or 200"
  Results in `(400\|200).*` | diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 67c4a2f4..00b4717f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" } diff --git a/app/src/androidTest/java/spam/blocker/util/RecurringTest.kt b/app/src/androidTest/java/spam/blocker/util/RecurringTest.kt deleted file mode 100644 index 97120ad2..00000000 --- a/app/src/androidTest/java/spam/blocker/util/RecurringTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -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 RecurringTest { - - @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 - - // now: <2024-10-1 Tuesday 0:0:0>, weekdays: [Tuesday], time: <1:0:0> -> dur: 1 hour - setNow(LocalDateTime.parse("2024-10-1T0:0:0")) - dur = Weekly(listOf(TUESDAY), Time(1,0)).nextOccurrence() - assertEquals("same day", 1*3600*1000, 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-1T0:0:0")) - dur = Weekly(listOf(TUESDAY, WEDNESDAY), Time(0,0)).nextOccurrence() - assertEquals("next day", 24*3600*1000, dur.toMillis()) - - // now: <2024-10-1 Tuesday 0:0:0>, weekdays: [Tuesday], time: <0:0:0> -> dur: 7 days - setNow(LocalDateTime.parse("2024-10-1T0:0:0")) - dur = Weekly(listOf(TUESDAY), Time(0,0)).nextOccurrence() - assertEquals("next week", 7*24*3600*1000, dur.toMillis()) - - // now: <2024-10-31 Thursday 0:0:0>, weekdays: [Thursday], time: <0:0:0> -> dur: 7 days - setNow(LocalDateTime.parse("2024-10-31T0:0:0")) - dur = Weekly(listOf(THURSDAY), Time(0,0)).nextOccurrence() - assertEquals("cross month", 24*3600*1000, dur.toMillis()) - - // now: <2024-12-31 Tuesday 0:0:0>, weekdays: [Tuesday], time: <0:0:0> -> dur: 7 days - setNow(LocalDateTime.parse("2024-12-31T0:0:0")) - dur = Weekly(listOf(TUESDAY), Time(0,0)).nextOccurrence() - assertEquals("cross year", 24*3600*1000, dur.toMillis()) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/spam/blocker/util/ScheduleTest.kt b/app/src/androidTest/java/spam/blocker/util/ScheduleTest.kt new file mode 100644 index 00000000..dd028902 --- /dev/null +++ b/app/src/androidTest/java/spam/blocker/util/ScheduleTest.kt @@ -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()) + } +} \ No newline at end of file diff --git a/app/src/main/java/spam/blocker/service/CallScreeningService.kt b/app/src/main/java/spam/blocker/service/CallScreeningService.kt index ddd1b049..51d6ae59 100644 --- a/app/src/main/java/spam/blocker/service/CallScreeningService.kt +++ b/app/src/main/java/spam/blocker/service/CallScreeningService.kt @@ -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(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS) + if (uri == null) { + uri = intentExtras.getParcelable(TelephonyManager.EXTRA_INCOMING_NUMBER); + } + if (uri != null) { + rawNumber = uri.schemeSpecificPart + } + } + return rawNumber +} class CallScreeningService : CallScreeningService() { @@ -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(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS) - if (uri == null) { - uri = details.intentExtras.getParcelable(TelephonyManager.EXTRA_INCOMING_NUMBER); - } - if (uri != null) { - rawNumber = uri.schemeSpecificPart - } - } + val rawNumber = details.getRawNumber() val r = processCall(this, rawNumber, details) diff --git a/app/src/main/java/spam/blocker/util/PermissionChain.kt b/app/src/main/java/spam/blocker/util/PermissionChain.kt index d90a9ea8..cfa94e7c 100644 --- a/app/src/main/java/spam/blocker/util/PermissionChain.kt +++ b/app/src/main/java/spam/blocker/util/PermissionChain.kt @@ -25,17 +25,19 @@ 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) } @@ -43,34 +45,36 @@ open class NormalPermission( // 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, @@ -93,8 +97,8 @@ class PermissionChain( // final callback private lateinit var onResult: (Boolean) -> Unit - private lateinit var currList: MutableList - private lateinit var curr: NormalPermission + private lateinit var currList: MutableList + private lateinit var curr: IPermission private lateinit var popupTrigger: MutableState @@ -148,7 +152,7 @@ class PermissionChain( fun ask( ctx: Context, - permissions: List, + permissions: List, onResult: (Boolean) -> Unit ) { this.onResult = onResult diff --git a/app/src/main/java/spam/blocker/util/Util.kt b/app/src/main/java/spam/blocker/util/Util.kt index dadd09f9..44025d18 100644 --- a/app/src/main/java/spam/blocker/util/Util.kt +++ b/app/src/main/java/spam/blocker/util/Util.kt @@ -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 { diff --git a/metadata/en-US/changelogs/300.txt b/metadata/en-US/changelogs/300.txt new file mode 100644 index 00000000..1e704b79 --- /dev/null +++ b/metadata/en-US/changelogs/300.txt @@ -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