Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
|
079bbb20f5 | ||
|
fc3f6c58ce | ||
|
09de3785b9 |
5 changed files with 54 additions and 30 deletions
16
README.md
16
README.md
|
@ -83,24 +83,24 @@ In the “Add App” screen:
|
||||||
|
|
||||||
1. Add the following URL: https://weforge.xyz/partisan/Pulse
|
1. Add the following URL: https://weforge.xyz/partisan/Pulse
|
||||||
2. In **Override Source**, select **Forgejo (Codeberg)**
|
2. In **Override Source**, select **Forgejo (Codeberg)**
|
||||||
3. Tap the “Add” button at the very top, and you’re done!
|
3. Tap the “Add” button at the very top, and you're done!
|
||||||
|
|
||||||
## Install directly
|
## 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`.
|
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 you’re done!
|
Install it, and you're done!
|
||||||
|
|
||||||
_Please note that when installing directly, the app will not receive automatic updates._
|
_Please note that when installing directly, the app will not receive automatic updates._
|
||||||
|
|
||||||
# Permissions
|
# Permissions
|
||||||
|
|
||||||
- `ACCESS_NETWORK_STATE` – check connectivity
|
- `ACCESS_NETWORK_STATE` - check connectivity
|
||||||
- `CALL_PHONE` – make a call via messenger
|
- `CALL_PHONE` - make a call via messenger
|
||||||
- `READ_CONTACTS` – check if contact has a messenger
|
- `READ_CONTACTS` - check if contact has a messenger
|
||||||
- `READ_PHONE_NUMBERS` – detect outgoing call
|
- `READ_PHONE_NUMBERS` - detect outgoing call
|
||||||
- `SYSTEM_ALERT_WINDOW` – show redirecting popup and launch from background
|
- `SYSTEM_ALERT_WINDOW` - show redirecting popup and launch from background
|
||||||
- `INTERNET` – check connectivity and verify donates
|
- `INTERNET` - check connectivity and verify donates
|
||||||
|
|
||||||
Currently all of the permissions are required.
|
Currently all of the permissions are required.
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ android {
|
||||||
applicationId = "partisan.weforge.xyz.pulse"
|
applicationId = "partisan.weforge.xyz.pulse"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 16
|
versionCode = 17
|
||||||
versionName = "2.0.2"
|
versionName = "2.0.3"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,9 @@ class CallRedirectionService : CallRedirectionService() {
|
||||||
initialPhoneAccount: PhoneAccountHandle,
|
initialPhoneAccount: PhoneAccountHandle,
|
||||||
allowInteractiveResponse: Boolean,
|
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
|
val capabilities = connectivityManager
|
||||||
?.getNetworkCapabilities(connectivityManager?.activeNetwork)
|
?.getNetworkCapabilities(connectivityManager?.activeNetwork)
|
||||||
|
@ -94,37 +96,30 @@ class CallRedirectionService : CallRedirectionService() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allowInteractiveResponse) {
|
|
||||||
Log.d("Redirection", "Aborting: interactive response not allowed by system")
|
|
||||||
placeCallUnmodified()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prefs.redirectIfRoaming && !isOutsideHomeCountry()) {
|
if (prefs.redirectIfRoaming && !isOutsideHomeCountry()) {
|
||||||
Log.d("Redirection", "Aborting: redirect only while roaming, but we're inside home country")
|
Log.d("Redirection", "Aborting: redirect only while roaming, but we're inside home country")
|
||||||
placeCallUnmodified()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val phoneNumber = handle.schemeSpecificPart
|
|
||||||
Log.d("Redirection", "Resolved phone number: $phoneNumber")
|
|
||||||
|
|
||||||
if (prefs.redirectInternationalOnly && !isInternationalNumber(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()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefs.isBlacklistEnabled && !prefs.isContactWhitelisted(phoneNumber)) {
|
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()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d("Redirection", "Number $numberAlias is not in filters, processing redirection...")
|
||||||
|
|
||||||
val records: Array<Record>
|
val records: Array<Record>
|
||||||
try {
|
try {
|
||||||
records = getRecordsFromPhoneNumber(phoneNumber)
|
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) {
|
} catch (exc: SecurityException) {
|
||||||
Log.w("Redirection", "SecurityException during record fetch", exc)
|
Log.w("Redirection", "SecurityException during record fetch", exc)
|
||||||
placeCallUnmodified()
|
placeCallUnmodified()
|
||||||
|
@ -135,18 +130,20 @@ class CallRedirectionService : CallRedirectionService() {
|
||||||
.filter { prefs.isServiceEnabled(it.mimetype) }
|
.filter { prefs.isServiceEnabled(it.mimetype) }
|
||||||
.sortedBy { prefs.getServicePriority(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()
|
val record = enabledRecords.firstOrNull()
|
||||||
if (record == null) {
|
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()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("Redirection", "Redirecting call to: ${record.mimetype} → ${record.uri}")
|
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)
|
window.show(record.uri, MIMETYPE_TO_DST_NAME[record.mimetype] ?: return)
|
||||||
} else {
|
} else {
|
||||||
window.call(record.uri)
|
window.call(record.uri)
|
||||||
|
@ -203,6 +200,33 @@ class CallRedirectionService : CallRedirectionService() {
|
||||||
return results.toTypedArray()
|
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 {
|
private fun isInternationalNumber(phoneNumber: String): Boolean {
|
||||||
val telephony = getSystemService(TelephonyManager::class.java) ?: return true
|
val telephony = getSystemService(TelephonyManager::class.java) ?: return true
|
||||||
val simCountryIso = telephony.simCountryIso?.lowercase() ?: return true
|
val simCountryIso = telephony.simCountryIso?.lowercase() ?: return true
|
||||||
|
|
|
@ -51,8 +51,8 @@ class MainFragment : Fragment() {
|
||||||
emitter =
|
emitter =
|
||||||
Emitter(duration = 100, TimeUnit.MILLISECONDS)
|
Emitter(duration = 100, TimeUnit.MILLISECONDS)
|
||||||
.perSecond(100),
|
.perSecond(100),
|
||||||
speed = 30f,
|
speed = 25f,
|
||||||
maxSpeed = 40f,
|
maxSpeed = 30f,
|
||||||
damping = 0.85f,
|
damping = 0.85f,
|
||||||
spread = 360,
|
spread = 360,
|
||||||
position = Position.Relative(0.5, 0.5)
|
position = Position.Relative(0.5, 0.5)
|
||||||
|
|
|
@ -38,8 +38,8 @@ class Preferences(private val context: Context) {
|
||||||
val isEnabled: Boolean
|
val isEnabled: Boolean
|
||||||
get() = isServiceEnabledByUser &&
|
get() = isServiceEnabledByUser &&
|
||||||
hasGeneralPermissions(context) &&
|
hasGeneralPermissions(context) &&
|
||||||
hasDrawOverlays(context) &&
|
hasCallRedirectionRole(context) &&
|
||||||
hasCallRedirectionRole(context)
|
(popupEnabled.not() || hasDrawOverlays(context))
|
||||||
|
|
||||||
enum class PopupEffect {
|
enum class PopupEffect {
|
||||||
NONE, FADE, SCALE, BOUNCE, FLOP, MATRIX, SLIDE_SNAP, GAMER_MODE, RANDOM
|
NONE, FADE, SCALE, BOUNCE, FLOP, MATRIX, SLIDE_SNAP, GAMER_MODE, RANDOM
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue