Added Redirect settings page

This commit is contained in:
partisan 2025-05-20 14:31:24 +02:00
parent 4ce065425b
commit e8549c8841
13 changed files with 268 additions and 35 deletions

View file

@ -71,5 +71,6 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation "androidx.browser:browser:1.7.0" implementation "androidx.browser:browser:1.7.0"
implementation 'com.squareup.okhttp3:okhttp:4.12.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 implementation 'nl.dionsegijn:konfetti-xml:2.0.2' // This library holds the fabric of reality together please dont remove it at any costs >:3
} }

View file

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.telephony" android:required="true" /> <uses-feature android:name="android.hardware.telephony" android:required="true" />

View file

@ -7,7 +7,9 @@ import android.net.Uri
import android.provider.ContactsContract import android.provider.ContactsContract
import android.telecom.CallRedirectionService import android.telecom.CallRedirectionService
import android.telecom.PhoneAccountHandle import android.telecom.PhoneAccountHandle
import android.telephony.TelephonyManager
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import com.google.i18n.phonenumbers.PhoneNumberUtil
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class CallRedirectionService : CallRedirectionService() { class CallRedirectionService : CallRedirectionService() {
@ -58,13 +60,36 @@ class CallRedirectionService : CallRedirectionService() {
initialPhoneAccount: PhoneAccountHandle, initialPhoneAccount: PhoneAccountHandle,
allowInteractiveResponse: Boolean, 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() placeCallUnmodified()
return return
} }
val phoneNumber = handle.schemeSpecificPart val phoneNumber = handle.schemeSpecificPart
// Check if we only redirect international numbers
if (prefs.redirectInternationalOnly && !isInternationalNumber(phoneNumber)) {
placeCallUnmodified()
return
}
if (prefs.isBlacklistEnabled && !prefs.isContactWhitelisted(phoneNumber)) { if (prefs.isBlacklistEnabled && !prefs.isContactWhitelisted(phoneNumber)) {
placeCallUnmodified() placeCallUnmodified()
return return
@ -146,8 +171,36 @@ class CallRedirectionService : CallRedirectionService() {
return results.toTypedArray() 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) @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 val capabilities = connectivityManager
?.getNetworkCapabilities(connectivityManager?.activeNetwork) ?: return false ?.getNetworkCapabilities(connectivityManager?.activeNetwork) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&

View file

@ -37,7 +37,7 @@ class DonateFragment : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(requireActivity() as? MainActivity)?.setAppBarTitle( (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 { binding.kofiButton.setOnClickListener {
Toast.makeText( Toast.makeText(
requireContext(), requireContext(),
"Make sure to include your token in the donation message to get rewarded 😊", getString(R.string.donate_toast_reminder),
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
val customTab = CustomTabsIntent.Builder().build() val customTab = CustomTabsIntent.Builder().build()
@ -63,7 +63,7 @@ class DonateFragment : Fragment() {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
val clip = android.content.ClipData.newPlainText("Ko-fi token", "token:${prefs.donationToken}") val clip = android.content.ClipData.newPlainText("Ko-fi token", "token:${prefs.donationToken}")
clipboard.setPrimaryClip(clip) 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 // Show token entry section
@ -82,20 +82,20 @@ class DonateFragment : Fragment() {
// Validate token format // Validate token format
if (token.length != 16) { 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 return@setOnClickListener
} }
prefs.donationToken = token prefs.donationToken = token
if (prefs.isDonationActivated) { if (prefs.isDonationActivated) {
binding.resultText.text = "✅ Already activated" binding.resultText.text = getString(R.string.donate_token_already_activated)
return@setOnClickListener return@setOnClickListener
} }
// Step 0: Check INTERNET permission // Step 0: Check INTERNET permission
if (!hasInternetPermission(requireContext())) { if (!hasInternetPermission(requireContext())) {
binding.resultText.text = "❌ Missing INTERNET permission" binding.resultText.text = getString(R.string.donate_missing_permission)
return@setOnClickListener return@setOnClickListener
} }
@ -111,16 +111,16 @@ class DonateFragment : Fragment() {
client.newCall(internetCheck).enqueue(object : Callback { client.newCall(internetCheck).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
activity?.runOnUiThread { activity?.runOnUiThread {
binding.resultText.text = "❌ No internet access" binding.resultText.text = getString(R.string.donate_no_internet)
} }
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
activity?.runOnUiThread { activity?.runOnUiThread {
if (!response.isSuccessful || response.body?.string().isNullOrBlank()) { if (!response.isSuccessful || response.body?.string().isNullOrBlank()) {
binding.resultText.text = "❌ No internet access" binding.resultText.text = getString(R.string.donate_no_internet)
} else { } 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) { override fun onResponse(call: Call, response: Response) {
if (response.body?.string()?.trim() != "true") { if (response.body?.string()?.trim() != "true") {
activity?.runOnUiThread { activity?.runOnUiThread {
binding.resultText.text = "❌ Server not responding" binding.resultText.text = getString(R.string.donate_server_not_responding)
} }
return return
} }
@ -143,7 +143,7 @@ class DonateFragment : Fragment() {
client.newCall(checkRequest).enqueue(object : Callback { client.newCall(checkRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
activity?.runOnUiThread { 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() val result = response.body?.string()?.trim()
if (result == "0") { if (result == "0") {
activity?.runOnUiThread { activity?.runOnUiThread {
binding.resultText.text = "❌ Invalid or expired token" binding.resultText.text = getString(R.string.donate_token_invalid)
} }
return return
} }
@ -164,7 +164,7 @@ class DonateFragment : Fragment() {
client.newCall(activateRequest).enqueue(object : Callback { client.newCall(activateRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
activity?.runOnUiThread { 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 prefs.isDonationActivated = true
activity?.runOnUiThread { activity?.runOnUiThread {
binding.resultText.text = binding.resultText.text =
"✅ Token activated! You had $result activations left." getString(R.string.donate_token_activated, result)
} }
} else { } else {
activity?.runOnUiThread { activity?.runOnUiThread {
binding.resultText.text = "❌ Activation failed" binding.resultText.text = getString(R.string.donate_activation_failed)
} }
} }
} }

View file

@ -71,6 +71,13 @@ class MainActivity : AppCompatActivity() {
.commit() .commit()
true true
} }
R.id.action_redirect_settings -> {
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, RedirectSettingsFragment())
.addToBackStack(null)
.commit()
true
}
R.id.action_contacts -> { R.id.action_contacts -> {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, ContactsFragment()) .replace(R.id.fragmentContainer, ContactsFragment())

View file

@ -17,6 +17,11 @@ class Preferences(private val context: Context) {
private const val DONATION_ACTIVATED = "donation_activated" private const val DONATION_ACTIVATED = "donation_activated"
private const val DONATION_TOKEN = "donation_token" 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_REDIRECTION_DELAY = 2000L
private const val DEFAULT_POPUP_POSITION = 333 private const val DEFAULT_POPUP_POSITION = 333
private const val SERVICE_ENABLED = "service_enabled" 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) get() = prefs.getInt(POPUP_POSITION, DEFAULT_POPUP_POSITION)
set(value) = prefs.edit { putInt(POPUP_POSITION, value) } 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 makeKeyEnabled(mimetype: String) = "enabled_$mimetype"
private fun makeKeyPriority(mimetype: String) = "priority_$mimetype" private fun makeKeyPriority(mimetype: String) = "priority_$mimetype"

View file

@ -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<MaterialSwitch>(R.id.switchRedirectWifi)
val switchData = view.findViewById<MaterialSwitch>(R.id.switchRedirectData)
val switchInternational = view.findViewById<MaterialSwitch>(R.id.switchRedirectInternational)
val switchRoaming = view.findViewById<MaterialSwitch>(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
}
}
}

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M440,800L440,496L240,296L240,400L160,400L160,160L400,160L400,240L296,240L520,464L520,800L440,800ZM594,424L536,366L664,240L560,240L560,160L800,160L800,400L720,400L720,296L594,424Z"/>
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

View file

@ -15,7 +15,7 @@
android:id="@+id/titleText" android:id="@+id/titleText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Support Pulse Development 💖" android:text="@string/donate_title"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginBottom="8dp" /> android:layout_marginBottom="8dp" />
@ -24,7 +24,7 @@
android:id="@+id/descText" android:id="@+id/descText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" 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:textSize="16sp"
android:layout_marginBottom="12dp" /> android:layout_marginBottom="12dp" />
@ -43,14 +43,14 @@
android:id="@+id/kofiButton" android:id="@+id/kofiButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Donate via Ko-fi 💙" android:text="@string/donate_button"
android:layout_marginBottom="12dp" /> android:layout_marginBottom="12dp" />
<TextView <TextView
android:id="@+id/postDonatePrompt" android:id="@+id/postDonatePrompt"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Already donated? Tap below to activate your token." android:text="@string/donate_post_prompt"
android:textSize="16sp" android:textSize="16sp"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginBottom="8dp" /> android:layout_marginBottom="8dp" />
@ -59,7 +59,7 @@
android:id="@+id/openTokenSection" android:id="@+id/openTokenSection"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="I have a token" android:text="@string/donate_have_token"
android:layout_marginBottom="16dp" /> android:layout_marginBottom="16dp" />
<!-- Token activation section (initially hidden) --> <!-- Token activation section (initially hidden) -->
@ -74,7 +74,7 @@
android:id="@+id/instruction" android:id="@+id/instruction"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Enter your Ko-fi token:" android:text="@string/donate_token_instruction"
android:textSize="16sp" android:textSize="16sp"
android:layout_marginBottom="8dp" /> android:layout_marginBottom="8dp" />
@ -82,7 +82,7 @@
android:id="@+id/tokenInput" android:id="@+id/tokenInput"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="token:abcd1234efgh5678" android:hint="@string/donate_token_hint"
android:inputType="text" android:inputType="text"
android:maxLength="32" android:maxLength="32"
android:layout_marginBottom="12dp"/> android:layout_marginBottom="12dp"/>
@ -91,7 +91,7 @@
android:id="@+id/verifyButton" android:id="@+id/verifyButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Activate Token" /> android:text="@string/donate_token_activate" />
<TextView <TextView
android:id="@+id/resultText" android:id="@+id/resultText"

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Redirect on WiFi -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchRedirectWifi"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/redirect_wifi"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Redirect on Data -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchRedirectData"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/redirect_data"
app:layout_constraintTop_toBottomOf="@id/switchRedirectWifi"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp" />
<!-- Redirect only international numbers -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchRedirectInternational"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/redirect_international"
app:layout_constraintTop_toBottomOf="@id/switchRedirectData"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp" />
<!-- Redirect only on roaming -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchRedirectRoaming"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/redirect_roaming"
app:layout_constraintTop_toBottomOf="@id/switchRedirectInternational"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -21,6 +21,11 @@
android:title="@string/services_name" android:title="@string/services_name"
android:icon="@drawable/services_24" android:icon="@drawable/services_24"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_redirect_settings"
android:title="@string/redirect_name"
android:icon="@drawable/call_split_24px"
app:showAsAction="never" />
<!-- About section --> <!-- About section -->
<item <item

View file

@ -7,6 +7,7 @@
<string name="popup_name">Popup</string> <string name="popup_name">Popup</string>
<string name="services_name">Services</string> <string name="services_name">Services</string>
<string name="whitelist_name">Allowlist</string> <string name="whitelist_name">Allowlist</string>
<string name="redirect_name">Redirect</string>
<string name="tools_name">Tools</string> <string name="tools_name">Tools</string>
<string name="about_name">About</string> <string name="about_name">About</string>
<string name="donate_name">Donate</string> <string name="donate_name">Donate</string>
@ -25,6 +26,13 @@
<string name="source_code">Source Code</string> <string name="source_code">Source Code</string>
<string name="license">License</string> <string name="license">License</string>
<!-- Redirect Settings -->
<string name="redirect_wifi">Redirect while using Wi-Fi</string>
<string name="redirect_data">Redirect while on mobile data</string>
<string name="redirect_international">Redirect only international numbers</string>
<string name="redirect_roaming">Redirect only if roaming</string>
<!-- Popup Animations -->
<string name="popup_effect_label">Popup Animation</string> <string name="popup_effect_label">Popup Animation</string>
<string-array name="popup_effects"> <string-array name="popup_effects">
<item>None</item> <item>None</item>
@ -36,4 +44,27 @@
<item>Random</item> <item>Random</item>
</string-array> </string-array>
<!-- Donate screen -->
<string name="donate_title">Support Pulse Development 💖</string>
<string name="donate_description">Pulse is free and open-source. You can support future development through Ko-fi. As a thank-you, donors get special popup animation effects!</string>
<string name="donate_button">Donate via Ko-fi 💙</string>
<string name="donate_post_prompt">Already donated? Tap below to activate your token.</string>
<string name="donate_have_token">I have a token</string>
<string name="donate_token_instruction">Enter your Ko-fi token:</string>
<string name="donate_token_hint">token: abcd1234efgh5678</string>
<string name="donate_token_activate">Activate Token</string>
<!-- DonateFragment -->
<string name="donate_token_copied">Token copied to clipboard</string>
<string name="donate_token_invalid_format">Invalid token format</string>
<string name="donate_missing_permission">❌ Missing INTERNET permission</string>
<string name="donate_no_internet">❌ No internet access</string>
<string name="donate_server_unreachable">❌ Activation server is unreachable</string>
<string name="donate_server_not_responding">❌ Server not responding</string>
<string name="donate_token_check_failed">❌ Could not check token</string>
<string name="donate_token_invalid">❌ Invalid or expired token</string>
<string name="donate_activation_failed">❌ Activation failed</string>
<string name="donate_token_activated">✅ Token activated! You had %1$s activations left.</string>
<string name="donate_token_already_activated">✅ Already activated</string>
<string name="donate_toast_reminder">Make sure to include your token in the donation message to get rewarded 😊</string>
</resources> </resources>