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