Compare commits

..

3 commits
main ... work

Author SHA1 Message Date
partisan
079bbb20f5 Store anonymized call numbers in shared preferences 2025-06-05 09:36:17 +02:00
partisan
fc3f6c58ce Anonymized phone number logging 2025-06-05 09:11:05 +02:00
partisan
09de3785b9 Pulse no longer shows popup if disallowed by system 2025-06-04 22:52:37 +02:00
5 changed files with 54 additions and 30 deletions

View file

@ -83,24 +83,24 @@ In the “Add App” screen:
1. Add the following URL: https://weforge.xyz/partisan/Pulse
2. In **Override Source**, select **Forgejo (Codeberg)**
3. Tap the “Add” button at the very top, and youre done!
3. Tap the “Add” button at the very top, and you're done!
## Install directly
Go to the [Releases page](https://weforge.xyz/partisan/Pulse/releases) and download the latest file with the following format: `app-release.apk`.
Install it, and youre done!
Install it, and you're done!
_Please note that when installing directly, the app will not receive automatic updates._
# Permissions
- `ACCESS_NETWORK_STATE` check connectivity
- `CALL_PHONE` make a call via messenger
- `READ_CONTACTS` check if contact has a messenger
- `READ_PHONE_NUMBERS` detect outgoing call
- `SYSTEM_ALERT_WINDOW` show redirecting popup and launch from background
- `INTERNET` check connectivity and verify donates
- `ACCESS_NETWORK_STATE` - check connectivity
- `CALL_PHONE` - make a call via messenger
- `READ_CONTACTS` - check if contact has a messenger
- `READ_PHONE_NUMBERS` - detect outgoing call
- `SYSTEM_ALERT_WINDOW` - show redirecting popup and launch from background
- `INTERNET` - check connectivity and verify donates
Currently all of the permissions are required.

View file

@ -11,8 +11,8 @@ android {
applicationId = "partisan.weforge.xyz.pulse"
minSdk = 29
targetSdk = 34
versionCode = 16
versionName = "2.0.2"
versionCode = 17
versionName = "2.0.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View file

@ -61,7 +61,9 @@ class CallRedirectionService : CallRedirectionService() {
initialPhoneAccount: PhoneAccountHandle,
allowInteractiveResponse: Boolean,
) {
Log.d("Redirection", "onPlaceCall triggered: uri=$handle, interactive=$allowInteractiveResponse")
val phoneNumber = handle.schemeSpecificPart
val numberAlias = getAnonymizedAlias(phoneNumber)
Log.d("Redirection", "onPlaceCall triggered: alias=$numberAlias, interactive=$allowInteractiveResponse")
val capabilities = connectivityManager
?.getNetworkCapabilities(connectivityManager?.activeNetwork)
@ -94,37 +96,30 @@ class CallRedirectionService : CallRedirectionService() {
return
}
if (!allowInteractiveResponse) {
Log.d("Redirection", "Aborting: interactive response not allowed by system")
placeCallUnmodified()
return
}
if (prefs.redirectIfRoaming && !isOutsideHomeCountry()) {
Log.d("Redirection", "Aborting: redirect only while roaming, but we're inside home country")
placeCallUnmodified()
return
}
val phoneNumber = handle.schemeSpecificPart
Log.d("Redirection", "Resolved phone number: $phoneNumber")
if (prefs.redirectInternationalOnly && !isInternationalNumber(phoneNumber)) {
Log.d("Redirection", "Aborting: number is not international and pref requires it")
Log.d("Redirection", "Aborting: number $numberAlias is not international and pref requires it")
placeCallUnmodified()
return
}
if (prefs.isBlacklistEnabled && !prefs.isContactWhitelisted(phoneNumber)) {
Log.d("Redirection", "Aborting: number is not in whitelist while blacklist is enabled")
Log.d("Redirection", "Aborting: number $numberAlias is not in whitelist while blacklist is enabled")
placeCallUnmodified()
return
}
Log.d("Redirection", "Number $numberAlias is not in filters, processing redirection...")
val records: Array<Record>
try {
records = getRecordsFromPhoneNumber(phoneNumber)
Log.d("Redirection", "Found ${records.size} raw records for contact")
Log.d("Redirection", "Found ${records.size} raw redirect apps for number $numberAlias")
} catch (exc: SecurityException) {
Log.w("Redirection", "SecurityException during record fetch", exc)
placeCallUnmodified()
@ -135,18 +130,20 @@ class CallRedirectionService : CallRedirectionService() {
.filter { prefs.isServiceEnabled(it.mimetype) }
.sortedBy { prefs.getServicePriority(it.mimetype) }
Log.d("Redirection", "Filtered to ${enabledRecords.size} enabled records")
Log.d("Redirection", "Filtered to ${enabledRecords.size} enabled redirect apps")
val record = enabledRecords.firstOrNull()
if (record == null) {
Log.d("Redirection", "Aborting: no suitable record found for redirection")
Log.d("Redirection", "Aborting: no suitable redirect apps found for number $numberAlias")
placeCallUnmodified()
return
}
Log.d("Redirection", "Redirecting call to: ${record.mimetype}${record.uri}")
if (prefs.popupEnabled) {
Log.d("Redirection", "Popup ${if (allowInteractiveResponse) "allowed" else "not allowed"} by system; ${if (prefs.popupEnabled) "enabled" else "disabled"} in prefs")
if (allowInteractiveResponse && prefs.popupEnabled) {
window.show(record.uri, MIMETYPE_TO_DST_NAME[record.mimetype] ?: return)
} else {
window.call(record.uri)
@ -203,6 +200,33 @@ class CallRedirectionService : CallRedirectionService() {
return results.toTypedArray()
}
private fun getAnonymizedAlias(number: String): String {
val prefs = getSharedPreferences("anonymized_numbers", MODE_PRIVATE)
// Return existing alias if already mapped
val existing = prefs.getString(number, null)
if (existing != null) return existing
// Start from current counter
var counter = prefs.getInt("counter", 1)
var alias: String
// Find the first unused alias (safety check)
while (true) {
alias = "#$counter"
if (!prefs.all.containsValue(alias)) break
counter++
}
// Store new alias and increment counter
prefs.edit()
.putString(number, alias)
.putInt("counter", counter + 1)
.apply()
return alias
}
private fun isInternationalNumber(phoneNumber: String): Boolean {
val telephony = getSystemService(TelephonyManager::class.java) ?: return true
val simCountryIso = telephony.simCountryIso?.lowercase() ?: return true

View file

@ -51,8 +51,8 @@ class MainFragment : Fragment() {
emitter =
Emitter(duration = 100, TimeUnit.MILLISECONDS)
.perSecond(100),
speed = 30f,
maxSpeed = 40f,
speed = 25f,
maxSpeed = 30f,
damping = 0.85f,
spread = 360,
position = Position.Relative(0.5, 0.5)

View file

@ -38,8 +38,8 @@ class Preferences(private val context: Context) {
val isEnabled: Boolean
get() = isServiceEnabledByUser &&
hasGeneralPermissions(context) &&
hasDrawOverlays(context) &&
hasCallRedirectionRole(context)
hasCallRedirectionRole(context) &&
(popupEnabled.not() || hasDrawOverlays(context))
enum class PopupEffect {
NONE, FADE, SCALE, BOUNCE, FLOP, MATRIX, SLIDE_SNAP, GAMER_MODE, RANDOM