Skip to content

Commit

Permalink
Complete migration from GreenDAO to SQLDelight (#25)
Browse files Browse the repository at this point in the history
- [x] Update dependencies and targetSDK to 35
- [x] Revert the refactoring of the denylist (only ~10% was done and
everything else was deleted and not working). I suggest we keep the old
(but working!) files, so we can get a release without GreenDAO asap, and
refactor denylist later
- [x] Rename "blacklist" to "denylist" everywhere, as you started doing
(+ in French)
- [x] Use [ReQuery SQLite](https://github.com/requery/sqlite-android)
(on top of SQLDelight) to provide a consistent SQLite version across OS
versions and devices
- [x] Fix a few bugs and crashes

Tested:
- [x] Denylist import
- [x] Denylist export
- [x] Denylist add
- [x] Denylist edit
- [x] Denylist delete
- [x] Denylist enable/disable
- [x] Call blocked when matching a denylist item rule (and denylist
enabled)
- [x] Pattern matching in the call log (correctly match and display the
denylist item name)
  • Loading branch information
papjul authored Jan 25, 2025
1 parent 35b54ad commit 1b809cf
Show file tree
Hide file tree
Showing 87 changed files with 2,300 additions and 1,077 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ Cheers

### Changed

- Changed blocking behavior in Direct Boot mode: blacklisted numbers are not blocked by default.
- Changed blocking behavior in Direct Boot mode: denylisted numbers are not blocked by default.
See #22 for details.
- \[Internal\] Settings refactoring.
- Updated Croatian translation thanks to Milo Ivir (@milotype).
Expand Down
8 changes: 4 additions & 4 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Currently, there's no whitelist feature, but there are plans to eventually imple

## Can I block all numbers not present in Contacts?

There's no dedicated option [yet](https://gitlab.com/xynngh/YetAnotherCallBlocker/-/issues/31), but there is a way to achieve the effect: enable "Use contacts" option and create a blacklist pattern matching any number (`*`). The app [never blocks contacts](FAQ.md#how-do-blocking-options-work-exactly), but all unfamiliar numbers will be blocked by this pattern. You will also need to enable "Block hidden numbers" option to have hidden numbers blocked.
There's no dedicated option [yet](https://gitlab.com/xynngh/YetAnotherCallBlocker/-/issues/31), but there is a way to achieve the effect: enable "Use contacts" option and create a denylist pattern matching any number (`*`). The app [never blocks contacts](FAQ.md#how-do-blocking-options-work-exactly), but all unfamiliar numbers will be blocked by this pattern. You will also need to enable "Block hidden numbers" option to have hidden numbers blocked.

Additionally, modern Android versions have "Do not disturb" mode which can be customized to block unfamiliar numbers.

Expand All @@ -68,7 +68,7 @@ I'm not sure to be honest. But I believe most of the world is covered.
You can install the app and look up some recent unwanted calls (if you had any) to see whether the app would have blocked them for you.


## How do wildcards in the blacklist work?
## How do wildcards in the denylist work?

`*` matches zero or more digits, `#` matches exactly one digit.
So a pattern `+123*` will match any number starting with `+123`.
Expand All @@ -87,7 +87,7 @@ The number format *must* match the format that Android uses, that's why the lead
Theoretically, a failure to detect number may result in a call from a contact to be blocked, but I haven't heard about it ever happening.
1. If "Block based on rating" is enabled and the number has a *negative rating*, the call is **blocked**.
Currently, "negative rating" means the number has more negative reviews than a sum of neutral and positive reviews.
1. If "Block blacklisted numbers" is enabled and the number matches any valid blacklist pattern, the call is **blocked**.
1. If "Block denylisted numbers" is enabled and the number matches any valid denylist pattern, the call is **blocked**.



Expand Down Expand Up @@ -137,5 +137,5 @@ If you redact personal data (which you should do), please *replace* numbers (wit

You don't have to. If you're happy with some other app - good for you.
This project was started because I needed to help my non-techie relatives fight phone spam. Giving calls and contacts permissions to some proprietary app is just not an option for me.
There's only a few FOSS (free and open source) apps that provide call blocking and none of them has any kind of a crowdsourced blacklist. So I created Tranquille to solve this.
There's only a few FOSS (free and open source) apps that provide call blocking and none of them has any kind of a crowdsourced denylist. So I created Tranquille to solve this.
After a while the app got new features, some of which are unique on the FOSS scene (for example, I believe that the "advanced call blocking mode" is not present in any other FOSS spam blocker).
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ A free and open source application that can block unwanted calls or warn about p

* Uses offline database.
* Blocks calls with negative rating automatically (option).
* Local blacklist with wildcard support.
* Local denylist with wildcard support.
* Displays a notification with phone number summary (rating, reviews count, category) during incoming calls (option).
* Automatic incremental/delta database updates (option).
* You can view online reviews for caller's number (provided by 3rd party service).
Expand Down Expand Up @@ -51,10 +51,10 @@ See [frequently asked questions](FAQ.md) to learn more.

At this point most of the essential features (one would expect from a call blocking app) are implemented. Here's an overview:

* Automatic blocking of calls with negative rating, calls from hidden or locally blacklisted numbers. *Works offline.*
* Automatic blocking of calls with negative rating, calls from hidden or locally denylisted numbers. *Works offline.*
Call blocking should work on most Android versions (including Android 10).
Some phones (like some Xiaomi) [require](https://gitlab.com/xynngh/YetAnotherCallBlocker/-/issues/12) to enable "Advanced call blocking mode" or a monitoring service.
* Local blacklist with wildcard support.
* Local denylist with wildcard support.
Import of NoPhoneSpam backup is supported.
* Showing a notification with some caller info (positive/negative rating, category, number of negative reviews and such) when the phone's ringing. *Works offline.*
* Loading and displaying a list of reviews for a number (accessed from the notification or the info view). *Requires internet.*
Expand Down
18 changes: 10 additions & 8 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
plugins {
alias(libs.plugins.androidApplication)
// alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.sqlite)
alias(libs.plugins.sqldelight)
kotlin("android")
}

android {
namespace = "fr.vinetos.tranquille"
compileSdk = 34
compileSdk = 35

defaultConfig {
applicationId = "fr.vinetos.tranquille"
minSdk = 21
targetSdk = 34
targetSdk = 35
versionCode = 1
versionName = "0.0.1"
multiDexEnabled = true
Expand Down Expand Up @@ -58,6 +58,7 @@ sqldelight {
databases {
create("Database") {
packageName.set("fr.vinetos.tranquille.data")
dialect(libs.sqldelight.dialects.sql)
}
}
}
Expand All @@ -77,14 +78,15 @@ dependencies {
implementation(libs.okhttp)
implementation(libs.lib.phone.number.info)
implementation(libs.commons.csv)
implementation(libs.sqlite.driver)
implementation(libs.sqlite.coroutines)
implementation(libs.sqldelight.driver)
implementation(libs.sqldelight.coroutines)
implementation(libs.bundles.sqlite)
implementation(libs.eventbus)

// todo add kotlin coriutines dependency
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)

annotationProcessor(libs.eventbus.annotation.processor)

Expand Down
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
</activity>
<activity
android:name=".presentation.denylist.DenylistActivity"
android:label="@string/title_blacklist_activity"
android:label="@string/title_denylist_activity"
android:launchMode="singleTop"
android:parentActivityName=".MainActivity">
<meta-data
Expand All @@ -74,7 +74,7 @@
</activity>
<activity
android:name=".presentation.denylist.EditDenylistItemActivity"
android:label="@string/title_add_blacklist_item_activity"
android:label="@string/title_add_denylist_item_activity"
android:parentActivityName=".presentation.denylist.DenylistActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ private String getLabel(Context context, CallLogItem item) {

if (numberInfo.name != null) return numberInfo.name;

if (numberInfo.blacklistItem != null
&& !TextUtils.isEmpty(numberInfo.blacklistItem.getName())) {
return numberInfo.blacklistItem.getName();
if (numberInfo.denylistItem != null
&& !TextUtils.isEmpty(numberInfo.denylistItem.getName())) {
return numberInfo.denylistItem.getName();
}

return item.number;
Expand Down
122 changes: 122 additions & 0 deletions app/src/main/java/fr/vinetos/tranquille/DenylistDataSource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package fr.vinetos.tranquille

import androidx.paging.DataSource
import androidx.paging.PositionalDataSource
import fr.vinetos.tranquille.data.DenylistItem
import fr.vinetos.tranquille.data.datasource.DenylistDao
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import kotlin.concurrent.Volatile

class DenylistDataSource(
private val dao: DenylistDao
) : PositionalDataSource<DenylistItem>() {

class Factory(
private var denylistDao: DenylistDao
) : DataSource.Factory<Int, DenylistItem>() {

@Volatile
private var ds: DenylistDataSource? = null

val currentDataSource: DenylistDataSource?
get() = ds

fun invalidate() {
LOG.debug("invalidate()")

val ds: DenylistDataSource? = this.ds
ds?.invalidate()
}

override fun create(): DataSource<Int, DenylistItem> {
return DenylistDataSource(denylistDao).also { ds = it }
}
}

/**
* The iterable must be iterated through
* or the underlying cursor won't be closed.
*
* @return an iterable containing ids of all items this DS would load
*/
fun getAllIds(): Iterable<Long> {
// TODO: Avoid loading everything into memory
val query = dao.getAll()
val iterator: Iterator<DenylistItem> = query.listIterator()

return Iterable {
object : Iterator<Long> {
override fun hasNext(): Boolean {
return iterator.hasNext()
}

override fun next(): Long {
return iterator.next().id
}
}
}
}

override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<DenylistItem>
) {
LOG.debug("loadInitial({}, {})", params.requestedStartPosition, params.requestedLoadSize)

var offset = params.requestedStartPosition

var items: List<DenylistItem> = loadItems(offset, params.requestedLoadSize)

var totalCount: Int? = null

if (items.isEmpty()) {
totalCount = countAll()
if (totalCount > 0) {
LOG.debug(
"loadInitial() initial range is empty: totalCount={}, offset={}",
totalCount, offset
)

offset = ((totalCount - params.requestedLoadSize)
/ params.pageSize * params.pageSize) // align to pageSize using integer math

if (offset < 0) offset = 0

LOG.debug("loadInitial() reloading with offset={}", offset)
items = loadItems(offset, params.requestedLoadSize)
} else {
offset = 0
}
}

if (params.placeholdersEnabled) {
if (totalCount == null) totalCount = countAll()

callback.onResult(items, offset, totalCount)
} else {
callback.onResult(items, offset)
}
}

override fun loadRange(
params: LoadRangeParams,
callback: LoadRangeCallback<DenylistItem>
) {
LOG.debug("loadRange({}, {})", params.startPosition, params.loadSize)

callback.onResult(loadItems(params.startPosition, params.loadSize))
}

private fun loadItems(offset: Int, limit: Int): List<DenylistItem> {
return dao.getAll(offset, limit)
}

private fun countAll(): Int {
return dao.countAll()
}

companion object {
private val LOG: Logger = LoggerFactory.getLogger(DenylistDataSource::class.java)
}
}
Loading

0 comments on commit 1b809cf

Please sign in to comment.