diff --git a/app/build.gradle b/app/build.gradle
index 2c9acdf..b8cd5e9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -71,5 +71,6 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation "androidx.browser:browser:1.7.0"
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.13.32'
implementation 'nl.dionsegijn:konfetti-xml:2.0.2' // This library holds the fabric of reality together please dont remove it at any costs >:3
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d5e9390..486c998 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt b/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt
index 5f7a9b3..62720e5 100644
--- a/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt
+++ b/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt
@@ -7,7 +7,9 @@ import android.net.Uri
import android.provider.ContactsContract
import android.telecom.CallRedirectionService
import android.telecom.PhoneAccountHandle
+import android.telephony.TelephonyManager
import androidx.annotation.RequiresPermission
+import com.google.i18n.phonenumbers.PhoneNumberUtil
import java.lang.ref.WeakReference
class CallRedirectionService : CallRedirectionService() {
@@ -58,13 +60,36 @@ class CallRedirectionService : CallRedirectionService() {
initialPhoneAccount: PhoneAccountHandle,
allowInteractiveResponse: Boolean,
) {
- if (!prefs.isEnabled || !hasInternet() || !allowInteractiveResponse) {
+ val capabilities = connectivityManager
+ ?.getNetworkCapabilities(connectivityManager?.activeNetwork)
+
+ val isWifi = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
+ val isCellular = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
+
+ val shouldRedirect = when {
+ isWifi && !prefs.redirectOnWifi -> false
+ isCellular && !prefs.redirectOnData -> false
+ else -> true
+ }
+
+ if (!prefs.isEnabled || !shouldRedirect || !hasInternet() || !allowInteractiveResponse) {
+ placeCallUnmodified()
+ return
+ }
+
+ if (prefs.redirectIfRoaming && !isOutsideHomeCountry()) {
placeCallUnmodified()
return
}
val phoneNumber = handle.schemeSpecificPart
+ // Check if we only redirect international numbers
+ if (prefs.redirectInternationalOnly && !isInternationalNumber(phoneNumber)) {
+ placeCallUnmodified()
+ return
+ }
+
if (prefs.isBlacklistEnabled && !prefs.isContactWhitelisted(phoneNumber)) {
placeCallUnmodified()
return
@@ -146,8 +171,36 @@ class CallRedirectionService : CallRedirectionService() {
return results.toTypedArray()
}
+ private fun isInternationalNumber(phoneNumber: String): Boolean {
+ val telephony = getSystemService(TelephonyManager::class.java) ?: return true
+ val simCountryIso = telephony.simCountryIso?.lowercase() ?: return true
+
+ // Use libphonenumber to parse the number and get region
+ val util = PhoneNumberUtil.getInstance()
+ return try {
+ val numberProto = util.parse(phoneNumber, simCountryIso.uppercase())
+ val numberRegion = util.getRegionCodeForNumber(numberProto)?.lowercase()
+ numberRegion != simCountryIso
+ } catch (e: Exception) {
+ true // treat as international if parsing fails
+ }
+ }
+
+ fun isOutsideHomeCountry(): Boolean {
+ val telephony = getSystemService(TelephonyManager::class.java) ?: return true
+
+ val simCountry = telephony.simCountryIso?.lowercase()
+ val networkCountry = telephony.networkCountryIso?.lowercase()
+
+ // If SIM or network country can't be determined, assume we're abroad
+ if (simCountry.isNullOrBlank() || networkCountry.isNullOrBlank()) return true
+
+ // If they don't match, you're abroad
+ return simCountry != networkCountry
+ }
+
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
- private fun hasInternet(): Boolean {
+ private fun hasInternet(): Boolean { // This "hasInternet" func is (kinda) re-defined in Donation Fragment
val capabilities = connectivityManager
?.getNetworkCapabilities(connectivityManager?.activeNetwork) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/DonateFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/DonateFragment.kt
index e8899f4..b00b00b 100644
--- a/app/src/main/java/partisan/weforge/xyz/pulse/DonateFragment.kt
+++ b/app/src/main/java/partisan/weforge/xyz/pulse/DonateFragment.kt
@@ -37,7 +37,7 @@ class DonateFragment : Fragment() {
override fun onResume() {
super.onResume()
(requireActivity() as? MainActivity)?.setAppBarTitle(
- getString(R.string.about_name, R.string.donate_name)
+ getString(R.string.about_name), getString(R.string.donate_name)
)
}
@@ -51,7 +51,7 @@ class DonateFragment : Fragment() {
binding.kofiButton.setOnClickListener {
Toast.makeText(
requireContext(),
- "Make sure to include your token in the donation message to get rewarded 😊",
+ getString(R.string.donate_toast_reminder),
Toast.LENGTH_LONG
).show()
val customTab = CustomTabsIntent.Builder().build()
@@ -63,7 +63,7 @@ class DonateFragment : Fragment() {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
val clip = android.content.ClipData.newPlainText("Ko-fi token", "token:${prefs.donationToken}")
clipboard.setPrimaryClip(clip)
- Toast.makeText(context, "Token copied to clipboard", Toast.LENGTH_SHORT).show()
+ Toast.makeText(context, getString(R.string.donate_token_copied), Toast.LENGTH_SHORT).show()
}
// Show token entry section
@@ -82,20 +82,20 @@ class DonateFragment : Fragment() {
// Validate token format
if (token.length != 16) {
- Toast.makeText(context, "Invalid token format", Toast.LENGTH_SHORT).show()
+ Toast.makeText(context, getString(R.string.donate_token_invalid_format), Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
prefs.donationToken = token
if (prefs.isDonationActivated) {
- binding.resultText.text = "✅ Already activated"
+ binding.resultText.text = getString(R.string.donate_token_already_activated)
return@setOnClickListener
}
// Step 0: Check INTERNET permission
if (!hasInternetPermission(requireContext())) {
- binding.resultText.text = "❌ Missing INTERNET permission"
+ binding.resultText.text = getString(R.string.donate_missing_permission)
return@setOnClickListener
}
@@ -111,16 +111,16 @@ class DonateFragment : Fragment() {
client.newCall(internetCheck).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
activity?.runOnUiThread {
- binding.resultText.text = "❌ No internet access"
+ binding.resultText.text = getString(R.string.donate_no_internet)
}
}
override fun onResponse(call: Call, response: Response) {
activity?.runOnUiThread {
if (!response.isSuccessful || response.body?.string().isNullOrBlank()) {
- binding.resultText.text = "❌ No internet access"
+ binding.resultText.text = getString(R.string.donate_no_internet)
} else {
- binding.resultText.text = "❌ Activation server is unreachable"
+ binding.resultText.text = getString(R.string.donate_server_unreachable)
}
}
}
@@ -130,7 +130,7 @@ class DonateFragment : Fragment() {
override fun onResponse(call: Call, response: Response) {
if (response.body?.string()?.trim() != "true") {
activity?.runOnUiThread {
- binding.resultText.text = "❌ Server not responding"
+ binding.resultText.text = getString(R.string.donate_server_not_responding)
}
return
}
@@ -143,7 +143,7 @@ class DonateFragment : Fragment() {
client.newCall(checkRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
activity?.runOnUiThread {
- binding.resultText.text = "❌ Could not check token"
+ binding.resultText.text = getString(R.string.donate_token_check_failed)
}
}
@@ -151,7 +151,7 @@ class DonateFragment : Fragment() {
val result = response.body?.string()?.trim()
if (result == "0") {
activity?.runOnUiThread {
- binding.resultText.text = "❌ Invalid or expired token"
+ binding.resultText.text = getString(R.string.donate_token_invalid)
}
return
}
@@ -164,7 +164,7 @@ class DonateFragment : Fragment() {
client.newCall(activateRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
activity?.runOnUiThread {
- binding.resultText.text = "❌ Activation failed"
+ binding.resultText.text = getString(R.string.donate_activation_failed)
}
}
@@ -174,11 +174,11 @@ class DonateFragment : Fragment() {
prefs.isDonationActivated = true
activity?.runOnUiThread {
binding.resultText.text =
- "✅ Token activated! You had $result activations left."
+ getString(R.string.donate_token_activated, result)
}
} else {
activity?.runOnUiThread {
- binding.resultText.text = "❌ Activation failed"
+ binding.resultText.text = getString(R.string.donate_activation_failed)
}
}
}
diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt b/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt
index 3a189ef..ecb7e92 100644
--- a/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt
+++ b/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt
@@ -71,6 +71,13 @@ class MainActivity : AppCompatActivity() {
.commit()
true
}
+ R.id.action_redirect_settings -> {
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.fragmentContainer, RedirectSettingsFragment())
+ .addToBackStack(null)
+ .commit()
+ true
+ }
R.id.action_contacts -> {
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, ContactsFragment())
diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt b/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt
index bc7266c..fdf6c82 100644
--- a/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt
+++ b/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt
@@ -17,6 +17,11 @@ class Preferences(private val context: Context) {
private const val DONATION_ACTIVATED = "donation_activated"
private const val DONATION_TOKEN = "donation_token"
+ private const val REDIRECT_WIFI = "redirect_wifi"
+ private const val REDIRECT_DATA = "redirect_data"
+ private const val REDIRECT_INTERNATIONAL = "redirect_international"
+ private const val REDIRECT_ROAMING = "redirect_roaming"
+
private const val DEFAULT_REDIRECTION_DELAY = 2000L
private const val DEFAULT_POPUP_POSITION = 333
private const val SERVICE_ENABLED = "service_enabled"
@@ -86,6 +91,22 @@ class Preferences(private val context: Context) {
get() = prefs.getInt(POPUP_POSITION, DEFAULT_POPUP_POSITION)
set(value) = prefs.edit { putInt(POPUP_POSITION, value) }
+ var redirectOnWifi: Boolean
+ get() = prefs.getBoolean(REDIRECT_WIFI, true)
+ set(value) = prefs.edit { putBoolean(REDIRECT_WIFI, value) }
+
+ var redirectOnData: Boolean
+ get() = prefs.getBoolean(REDIRECT_DATA, true)
+ set(value) = prefs.edit { putBoolean(REDIRECT_DATA, value) }
+
+ var redirectInternationalOnly: Boolean
+ get() = prefs.getBoolean(REDIRECT_INTERNATIONAL, false)
+ set(value) = prefs.edit { putBoolean(REDIRECT_INTERNATIONAL, value) }
+
+ var redirectIfRoaming: Boolean
+ get() = prefs.getBoolean(REDIRECT_ROAMING, false)
+ set(value) = prefs.edit { putBoolean(REDIRECT_ROAMING, value) }
+
private fun makeKeyEnabled(mimetype: String) = "enabled_$mimetype"
private fun makeKeyPriority(mimetype: String) = "priority_$mimetype"
diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/RedirectSettingsFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/RedirectSettingsFragment.kt
new file mode 100644
index 0000000..8bf0f55
--- /dev/null
+++ b/app/src/main/java/partisan/weforge/xyz/pulse/RedirectSettingsFragment.kt
@@ -0,0 +1,56 @@
+package partisan.weforge.xyz.pulse
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.google.android.material.materialswitch.MaterialSwitch
+
+class RedirectSettingsFragment : Fragment() {
+
+ private lateinit var prefs: Preferences
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ prefs = Preferences(requireContext())
+ return inflater.inflate(R.layout.fragment_redirect_settings, container, false)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ (requireActivity() as? MainActivity)?.setAppBarTitle(
+ getString(R.string.settings_name), getString(R.string.redirect_name)
+ )
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val switchWifi = view.findViewById(R.id.switchRedirectWifi)
+ val switchData = view.findViewById(R.id.switchRedirectData)
+ val switchInternational = view.findViewById(R.id.switchRedirectInternational)
+ val switchRoaming = view.findViewById(R.id.switchRedirectRoaming)
+
+ // Load saved state
+ switchWifi.isChecked = prefs.redirectOnWifi
+ switchData.isChecked = prefs.redirectOnData
+ switchInternational.isChecked = prefs.redirectInternationalOnly
+ switchRoaming.isChecked = prefs.redirectIfRoaming
+
+ // Save on toggle
+ switchWifi.setOnCheckedChangeListener { _, isChecked ->
+ prefs.redirectOnWifi = isChecked
+ }
+ switchData.setOnCheckedChangeListener { _, isChecked ->
+ prefs.redirectOnData = isChecked
+ }
+ switchInternational.setOnCheckedChangeListener { _, isChecked ->
+ prefs.redirectInternationalOnly = isChecked
+ }
+ switchRoaming.setOnCheckedChangeListener { _, isChecked ->
+ prefs.redirectIfRoaming = isChecked
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/call_split_24px.xml b/app/src/main/res/drawable/call_split_24px.xml
new file mode 100644
index 0000000..0056d80
--- /dev/null
+++ b/app/src/main/res/drawable/call_split_24px.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/settings_24.xml b/app/src/main/res/drawable/settings_24.xml
deleted file mode 100644
index 41a82ed..0000000
--- a/app/src/main/res/drawable/settings_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/layout/fragment_donate.xml b/app/src/main/res/layout/fragment_donate.xml
index ab45953..f739c47 100644
--- a/app/src/main/res/layout/fragment_donate.xml
+++ b/app/src/main/res/layout/fragment_donate.xml
@@ -15,7 +15,7 @@
android:id="@+id/titleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="Support Pulse Development 💖"
+ android:text="@string/donate_title"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
@@ -24,7 +24,7 @@
android:id="@+id/descText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="Pulse is free and open-source. You can support future development through Ko-fi. As a thank-you, donors get special animation effects!"
+ android:text="@string/donate_description"
android:textSize="16sp"
android:layout_marginBottom="12dp" />
@@ -43,14 +43,14 @@
android:id="@+id/kofiButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Donate via Ko-fi 💙"
+ android:text="@string/donate_button"
android:layout_marginBottom="12dp" />
@@ -59,7 +59,7 @@
android:id="@+id/openTokenSection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="I have a token"
+ android:text="@string/donate_have_token"
android:layout_marginBottom="16dp" />
@@ -74,7 +74,7 @@
android:id="@+id/instruction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="Enter your Ko-fi token:"
+ android:text="@string/donate_token_instruction"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
@@ -82,7 +82,7 @@
android:id="@+id/tokenInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:hint="token:abcd1234efgh5678"
+ android:hint="@string/donate_token_hint"
android:inputType="text"
android:maxLength="32"
android:layout_marginBottom="12dp"/>
@@ -91,7 +91,7 @@
android:id="@+id/verifyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Activate Token" />
+ android:text="@string/donate_token_activate" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml
index bd36f92..2aef278 100644
--- a/app/src/main/res/menu/main_menu.xml
+++ b/app/src/main/res/menu/main_menu.xml
@@ -21,6 +21,11 @@
android:title="@string/services_name"
android:icon="@drawable/services_24"
app:showAsAction="never" />
+
- Popup
Services
Allowlist
+ Redirect
Tools
About
Donate
@@ -25,6 +26,13 @@
Source Code
License
+
+ Redirect while using Wi-Fi
+ Redirect while on mobile data
+ Redirect only international numbers
+ Redirect only if roaming
+
+
Popup Animation
- None
@@ -36,4 +44,27 @@
- Random
+
+ Support Pulse Development 💖
+ Pulse is free and open-source. You can support future development through Ko-fi. As a thank-you, donors get special popup animation effects!
+ Donate via Ko-fi 💙
+ Already donated? Tap below to activate your token.
+ I have a token
+ Enter your Ko-fi token:
+ token: abcd1234efgh5678
+ Activate Token
+
+
+ Token copied to clipboard
+ Invalid token format
+ ❌ Missing INTERNET permission
+ ❌ No internet access
+ ❌ Activation server is unreachable
+ ❌ Server not responding
+ ❌ Could not check token
+ ❌ Invalid or expired token
+ ❌ Activation failed
+ ✅ Token activated! You had %1$s activations left.
+ ✅ Already activated
+ Make sure to include your token in the donation message to get rewarded 😊
\ No newline at end of file