Compare commits
No commits in common. "main" and "v1.3.0" have entirely different histories.
|
@ -1,6 +1,9 @@
|
|||
name: Android Release Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
|
|
26
.github/workflows/android.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
name: Android CI
|
||||
|
||||
# on:
|
||||
# push:
|
||||
# branches: [ main ]
|
||||
# pull_request:
|
||||
# branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
10
.github/workflows/gradle-wrapper-validation.yml
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
name: "Validate Gradle Wrapper"
|
||||
# on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
validation:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gradle/wrapper-validation-action@v1
|
29
.github/workflows/super-linter.yml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
# This workflow executes several linters on changed files based on languages used in your code base whenever
|
||||
# you push a code or open a pull request.
|
||||
#
|
||||
# You can adjust the behavior by modifying this file.
|
||||
# For more information, see:
|
||||
# https://github.com/github/super-linter
|
||||
name: Lint Code Base
|
||||
|
||||
# on:
|
||||
# push:
|
||||
# branches: [ main ]
|
||||
# pull_request:
|
||||
# branches: [ main ]
|
||||
jobs:
|
||||
run-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# Full git history is needed to get a proper list of changed files within `super-linter`
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Lint Code Base
|
||||
uses: github/super-linter@v4
|
||||
env:
|
||||
VALIDATE_ALL_CODEBASE: false
|
||||
DEFAULT_BRANCH: main
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
3
.gitignore
vendored
|
@ -13,6 +13,3 @@
|
|||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
release-key.jks
|
||||
check.py
|
||||
round.sh
|
111
README.md
|
@ -1,5 +1,5 @@
|
|||
<p align="center">
|
||||
<img src="https://weforge.xyz/partisan/Pulse/raw/branch/main/data/icon.svg" alt="Pulse Icon" width="64" height="64">
|
||||
<img src="https://weforge.xyz/partisan/Pulse/raw/branch/main/data/icon.svg" alt="Logo" width="64" height="64">
|
||||
</p>
|
||||
|
||||
<p align="center" style="font-size: 32px;">
|
||||
|
@ -7,105 +7,22 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
Redirecting outgoing calls to E2EE apps.
|
||||
Tiny app to redirect outgoing calls to Signal/Telegram/Threema/Whatsapp.
|
||||
</p>
|
||||
|
||||
---
|
||||
You can cancel redirection by clicking on `Redirecting to..` popup.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/partisan.weforge.xyz.pulse">
|
||||
<img src="https://weforge.xyz/partisan/Pulse/raw/branch/main/data/IzzyOnDroidButton.svg" alt="Download on IzzyOnDroid" width="255" height="75">
|
||||
</a>
|
||||
|
||||
<a href="https://weforge.xyz/partisan/Pulse/src/branch/main/README.md#using-obtainium">
|
||||
<img src="https://weforge.xyz/partisan/Pulse/raw/branch/main/data/OptainiumButton.png" alt="Download using Optainium" width="255" height="75">
|
||||
</a>
|
||||
</p>
|
||||
## Permissions
|
||||
|
||||
---
|
||||
* ACCESS_NETWORK_STATE - check internet is available
|
||||
* CALL_PHONE - make a call via messenger
|
||||
* READ_CONTACTS - check contact has a messenger record
|
||||
* SYSTEM_ALERT_WINDOW - show redirecting popup and launch an activity from background
|
||||
* CALL_REDIRECTION - process outgoing call
|
||||
|
||||
<p align="center">
|
||||
<strong>Screenshots</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<span>
|
||||
<img src="https://weforge.xyz/partisan/Pulse/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" alt="Main screen" height="500" style="border-radius: 8px;">
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<img src="https://weforge.xyz/partisan/Pulse/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" alt="Redirecting popup" height="500" style="border-radius: 8px;">
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<img src="https://weforge.xyz/partisan/Pulse/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" alt="Redirecting popup" height="500" style="border-radius: 8px;">
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<img src="https://weforge.xyz/partisan/Pulse/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" alt="Redirecting popup" height="500" style="border-radius: 8px;">
|
||||
</span>
|
||||
</p>
|
||||
|
||||
# Features
|
||||
|
||||
- Material You design
|
||||
- Popup with cancel option
|
||||
- Extensive settings panel:
|
||||
- Toggle per-service support
|
||||
- Redirection only on Wi-Fi/Data
|
||||
- Allowlist specific contacts
|
||||
- Change per-service priority
|
||||
- Customize popup position, animation, and duration
|
||||
- ...
|
||||
|
||||
# Supports
|
||||
|
||||
- Signal
|
||||
- Telegram
|
||||
- Threema
|
||||
- WhatsApp
|
||||
|
||||
# How to Install
|
||||
|
||||
## Using Droid-ify (or other F-Droid client)
|
||||
|
||||
[Install Droid-ify from their page](https://droidify.eu.org/)
|
||||
|
||||
In the app, search for "Pulse" and install it.
|
||||
|
||||
_Pulse uses the IzzyOnDroid repo. Some F-Droid clients, such as F-Droid itself, do not include it by default. Please add the IzzyOnDroid repo: https://apt.izzysoft.de/fdroid/repo_
|
||||
|
||||
## Using Obtainium
|
||||
|
||||
[Install Obtainium](https://github.com/ImranR98/Obtainium/blob/main/README.md)
|
||||
|
||||
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 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 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
|
||||
|
||||
Currently all of the permissions are required.
|
||||
|
||||
# License
|
||||
All permissions are mandatory.
|
||||
|
||||
## License
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
This application is Free Software: You can use, study share and improve it at your will.
|
||||
|
@ -113,11 +30,11 @@ Specifically you can redistribute and/or modify it under the terms of the
|
|||
[GNU General Public License v3](https://www.gnu.org/licenses/gpl.html) as published by the Free
|
||||
Software Foundation.
|
||||
|
||||
## Icon Credit
|
||||
### Icon Credit
|
||||
|
||||
Icon based on "Pulse 53" from the [Flare Dashed Icons](https://www.svgrepo.com/svg/450484/pulse) collection by [Taras Shypka](https://www.svgrepo.com/author/Taras%20Shypka/).
|
||||
Licensed under the [Public Domain](https://www.svgrepo.com/page/licensing/#PD).
|
||||
|
||||
## Original Author
|
||||
### Original Author
|
||||
|
||||
[This software](https://github.com/x13a/Red) was originally developed by [x13a](https://github.com/x13a), but it has been archived by the owner on Jun 22, 2022.
|
||||
This software was originally developed by https://github.com/x13a, but it has been archived by the owner on Jun 22, 2022.
|
|
@ -11,8 +11,8 @@ android {
|
|||
applicationId = "partisan.weforge.xyz.pulse"
|
||||
minSdk = 29
|
||||
targetSdk = 34
|
||||
versionCode = 17
|
||||
versionName = "2.0.3"
|
||||
versionCode = 9
|
||||
versionName = "1.3.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ android {
|
|||
buildTypes {
|
||||
release {
|
||||
minifyEnabled = false
|
||||
signingConfig = signingConfigs.release
|
||||
signingConfig signingConfigs.release
|
||||
proguardFiles(getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro')
|
||||
}
|
||||
}
|
||||
|
@ -50,27 +50,16 @@ android {
|
|||
lint {
|
||||
disable += 'MissingTranslation'
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid)
|
||||
includeInApk = false
|
||||
// Disables dependency metadata when building Android App Bundles (for Google Play)
|
||||
includeInBundle = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.13.0-alpha13'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
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
|
||||
}
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<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.INTERNET" />
|
||||
<uses-feature android:name="android.hardware.telephony" android:required="true" />
|
||||
|
||||
<application
|
||||
|
@ -18,14 +16,13 @@
|
|||
android:theme="@style/Theme.Pulse">
|
||||
|
||||
<activity
|
||||
android:name=".WelcomeActivity"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".MainActivity" />
|
||||
|
||||
<service
|
||||
android:name=".CallRedirectionService"
|
||||
|
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 12 KiB |
|
@ -1,71 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import partisan.weforge.xyz.pulse.databinding.FragmentAboutBinding
|
||||
|
||||
class AboutFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentAboutBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var tapCount = 0
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentAboutBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(requireActivity() as? MainActivity)?.setAppBarTitle(
|
||||
getString(R.string.about_name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.sourceButton.setOnClickListener {
|
||||
openUrl("https://weforge.xyz/partisan/Pulse")
|
||||
}
|
||||
|
||||
binding.licenseButton.setOnClickListener {
|
||||
openUrl("https://www.gnu.org/licenses/gpl-3.0.en.html")
|
||||
}
|
||||
|
||||
binding.appIcon.setOnClickListener {
|
||||
tapCount++
|
||||
if (tapCount >= 5) {
|
||||
binding.secretButton.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
binding.secretButton.setOnClickListener {
|
||||
requireActivity().supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, SecretFragment())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openUrl(url: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
|
@ -4,13 +4,10 @@ import android.Manifest
|
|||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
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() {
|
||||
|
@ -61,94 +58,31 @@ class CallRedirectionService : CallRedirectionService() {
|
|||
initialPhoneAccount: PhoneAccountHandle,
|
||||
allowInteractiveResponse: Boolean,
|
||||
) {
|
||||
val phoneNumber = handle.schemeSpecificPart
|
||||
val numberAlias = getAnonymizedAlias(phoneNumber)
|
||||
Log.d("Redirection", "onPlaceCall triggered: alias=$numberAlias, interactive=$allowInteractiveResponse")
|
||||
|
||||
val capabilities = connectivityManager
|
||||
?.getNetworkCapabilities(connectivityManager?.activeNetwork)
|
||||
|
||||
val isWifi = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
val isCellular = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
|
||||
Log.d("Redirection", "isWifi=$isWifi, isCellular=$isCellular")
|
||||
|
||||
val shouldRedirect = when {
|
||||
isWifi && !prefs.redirectOnWifi -> false
|
||||
isCellular && !prefs.redirectOnData -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
if (!prefs.isEnabled) {
|
||||
Log.d("Redirection", "Aborting: redirection disabled in prefs")
|
||||
if (!prefs.isEnabled || !hasInternet() || !allowInteractiveResponse) {
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
||||
if (!shouldRedirect) {
|
||||
Log.d("Redirection", "Aborting: redirection blocked by current network preference")
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasInternet()) {
|
||||
Log.d("Redirection", "Aborting: no internet connection detected")
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
||||
if (prefs.redirectIfRoaming && !isOutsideHomeCountry()) {
|
||||
Log.d("Redirection", "Aborting: redirect only while roaming, but we're inside home country")
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
||||
if (prefs.redirectInternationalOnly && !isInternationalNumber(phoneNumber)) {
|
||||
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 $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 redirect apps for number $numberAlias")
|
||||
records = getRecordsFromPhoneNumber(handle.schemeSpecificPart)
|
||||
} catch (exc: SecurityException) {
|
||||
Log.w("Redirection", "SecurityException during record fetch", exc)
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
||||
// Filter to enabled services only
|
||||
val enabledRecords = records
|
||||
.filter { prefs.isServiceEnabled(it.mimetype) }
|
||||
.sortedBy { prefs.getServicePriority(it.mimetype) }
|
||||
|
||||
Log.d("Redirection", "Filtered to ${enabledRecords.size} enabled redirect apps")
|
||||
|
||||
val record = enabledRecords.firstOrNull()
|
||||
if (record == null) {
|
||||
Log.d("Redirection", "Aborting: no suitable redirect apps found for number $numberAlias")
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
||||
Log.d("Redirection", "Redirecting call to: ${record.mimetype} → ${record.uri}")
|
||||
|
||||
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)
|
||||
cancelCall()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.READ_CONTACTS)
|
||||
|
@ -200,63 +134,8 @@ 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
|
||||
|
||||
// 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 { // This "hasInternet" func is (kinda) re-defined in Donation Fragment
|
||||
private fun hasInternet(): Boolean {
|
||||
val capabilities = connectivityManager
|
||||
?.getNetworkCapabilities(connectivityManager?.activeNetwork) ?: return false
|
||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class ContactAdapter(
|
||||
private val prefs: Preferences,
|
||||
private val fullList: List<ContactEntry>
|
||||
) : RecyclerView.Adapter<ContactAdapter.ViewHolder>() {
|
||||
|
||||
private var filteredList = fullList.toMutableList()
|
||||
|
||||
inner class ViewHolder(inflater: LayoutInflater, parent: ViewGroup) :
|
||||
RecyclerView.ViewHolder(inflater.inflate(R.layout.item_contact, parent, false)) {
|
||||
val contactName: TextView = itemView.findViewById(R.id.contactName)
|
||||
val contactAllowed: CheckBox = itemView.findViewById(R.id.contactAllowed)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(LayoutInflater.from(parent.context), parent)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val contact = filteredList[position]
|
||||
holder.contactName.text = contact.name
|
||||
holder.contactAllowed.setOnCheckedChangeListener(null)
|
||||
holder.contactAllowed.isChecked = prefs.isContactWhitelisted(contact.phoneNumber)
|
||||
|
||||
holder.contactAllowed.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.setContactWhitelisted(contact.phoneNumber, isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = filteredList.size
|
||||
|
||||
fun filter(query: String) {
|
||||
filteredList = if (query.isBlank()) {
|
||||
fullList.toMutableList()
|
||||
} else {
|
||||
fullList.filter {
|
||||
it.name.contains(query, ignoreCase = true)
|
||||
}.toMutableList()
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
data class ContactEntry(
|
||||
val name: String,
|
||||
val phoneNumber: String
|
||||
)
|
|
@ -1,128 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.graphics.Color
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import partisan.weforge.xyz.pulse.databinding.FragmentContactsBinding
|
||||
|
||||
class ContactsFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentContactsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var prefs: Preferences
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentContactsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(requireActivity() as? MainActivity)?.apply {
|
||||
setAppBarTitle(getString(R.string.settings_name), getString(R.string.whitelist_name))
|
||||
setupPopupToggle(true, prefs.isBlacklistEnabled) { isChecked ->
|
||||
prefs.isBlacklistEnabled = isChecked
|
||||
binding.contactRecycler.isEnabled = isChecked
|
||||
binding.contactRecycler.alpha = if (isChecked) 1f else 0.4f
|
||||
}
|
||||
}
|
||||
|
||||
// Initial state
|
||||
binding.contactRecycler.isEnabled = prefs.isBlacklistEnabled
|
||||
binding.contactRecycler.alpha = if (prefs.isBlacklistEnabled) 1f else 0.4f
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
(requireActivity() as? MainActivity)?.setupPopupToggle(false)
|
||||
}
|
||||
|
||||
private lateinit var adapter: ContactAdapter
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
prefs = Preferences(requireContext())
|
||||
|
||||
val contacts = getContacts()
|
||||
adapter = ContactAdapter(prefs, contacts)
|
||||
|
||||
binding.contactRecycler.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.contactRecycler.adapter = adapter
|
||||
|
||||
binding.contactSearch.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
adapter.filter(newText ?: "")
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
val searchView = binding.contactSearch
|
||||
|
||||
searchView.setIconifiedByDefault(false)
|
||||
searchView.isIconified = false
|
||||
searchView.isSubmitButtonEnabled = false
|
||||
searchView.clearFocus()
|
||||
|
||||
val editText = searchView.findViewById<androidx.appcompat.widget.SearchView.SearchAutoComplete>(
|
||||
androidx.appcompat.R.id.search_src_text
|
||||
)
|
||||
editText.isFocusable = true
|
||||
editText.isFocusableInTouchMode = true
|
||||
editText.setTextColor(Color.WHITE)
|
||||
editText.setHintTextColor(Color.LTGRAY)
|
||||
|
||||
val searchPlate = searchView.findViewById<View>(androidx.appcompat.R.id.search_plate)
|
||||
searchPlate.setBackgroundColor(Color.TRANSPARENT)
|
||||
searchPlate.setPadding(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
private fun getContacts(): List<ContactEntry> {
|
||||
val results = mutableListOf<ContactEntry>()
|
||||
val resolver: ContentResolver = requireContext().contentResolver
|
||||
|
||||
val projection = arrayOf(
|
||||
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
|
||||
ContactsContract.CommonDataKinds.Phone.NUMBER
|
||||
)
|
||||
|
||||
val cursor = resolver.query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
"${ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME} ASC"
|
||||
)
|
||||
|
||||
cursor?.use {
|
||||
val nameIndex = it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
|
||||
val numberIndex = it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||
|
||||
while (it.moveToNext()) {
|
||||
val name = it.getString(nameIndex)
|
||||
val number = it.getString(numberIndex)
|
||||
if (!name.isNullOrBlank() && !number.isNullOrBlank()) {
|
||||
results.add(ContactEntry(name, number))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
|
@ -1,213 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.fragment.app.Fragment
|
||||
import okhttp3.*
|
||||
import partisan.weforge.xyz.pulse.databinding.FragmentDonateBinding
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class DonateFragment : Fragment() {
|
||||
|
||||
private val client = OkHttpClient()
|
||||
private val apiBase = "https://api.weforge.xyz/api"
|
||||
private var _binding: FragmentDonateBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var prefs: Preferences
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentDonateBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(requireActivity() as? MainActivity)?.setAppBarTitle(
|
||||
getString(R.string.about_name), getString(R.string.donate_name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
prefs = Preferences(requireContext())
|
||||
|
||||
// Pre-fill token field
|
||||
binding.tokenInput.setText(prefs.donationToken)
|
||||
|
||||
// Show toast and open Ko-fi
|
||||
binding.kofiButton.setOnClickListener {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
getString(R.string.donate_toast_reminder),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
val customTab = CustomTabsIntent.Builder().build()
|
||||
customTab.launchUrl(requireContext(), Uri.parse("https://ko-fi.com/internetaddict"))
|
||||
}
|
||||
|
||||
binding.tokenDisplay.text = "token:${prefs.donationToken}"
|
||||
binding.tokenDisplay.setOnClickListener {
|
||||
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, getString(R.string.donate_token_copied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
// If already donated, update UI to show activation message and hide token entry controls
|
||||
if (prefs.isDonationActivated) {
|
||||
binding.postDonatePrompt.text = getString(R.string.donate_token_activated)
|
||||
binding.openTokenSection.visibility = View.GONE
|
||||
binding.tokenSection.visibility = View.GONE
|
||||
} else {
|
||||
// Show token entry section button if not activated
|
||||
binding.openTokenSection.setOnClickListener {
|
||||
binding.tokenSection.visibility = View.VISIBLE
|
||||
binding.openTokenSection.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
binding.verifyButton.setOnClickListener {
|
||||
var token = binding.tokenInput.text.toString().trim()
|
||||
|
||||
// Strip optional "token:" prefix
|
||||
if (token.startsWith("token:")) {
|
||||
token = token.removePrefix("token:")
|
||||
}
|
||||
|
||||
// Validate token format
|
||||
if (token.length != 16) {
|
||||
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 = getString(R.string.donate_token_already_activated)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
// Step 0: Check INTERNET permission
|
||||
if (!hasInternetPermission(requireContext())) {
|
||||
binding.resultText.text = getString(R.string.donate_missing_permission)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
// Step 1: Try activation server first
|
||||
val aliveRequest = Request.Builder().url("$apiBase/alive").build()
|
||||
client.newCall(aliveRequest).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
// If server unreachable, fallback to internet check
|
||||
val internetCheck = Request.Builder()
|
||||
.url("https://deb.debian.org/")
|
||||
.build()
|
||||
|
||||
client.newCall(internetCheck).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
activity?.runOnUiThread {
|
||||
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 = getString(R.string.donate_no_internet)
|
||||
} else {
|
||||
binding.resultText.text = getString(R.string.donate_server_unreachable)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (response.body?.string()?.trim() != "true") {
|
||||
activity?.runOnUiThread {
|
||||
binding.resultText.text = getString(R.string.donate_server_not_responding)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2: Check token
|
||||
val checkRequest = Request.Builder()
|
||||
.url("$apiBase/check?token=$token")
|
||||
.build()
|
||||
|
||||
client.newCall(checkRequest).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
activity?.runOnUiThread {
|
||||
binding.resultText.text = getString(R.string.donate_token_check_failed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
val result = response.body?.string()?.trim()
|
||||
if (result == "0") {
|
||||
activity?.runOnUiThread {
|
||||
binding.resultText.text = getString(R.string.donate_token_invalid)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Step 3: Activate
|
||||
val activateRequest = Request.Builder()
|
||||
.url("$apiBase/activate?token=$token")
|
||||
.build()
|
||||
|
||||
client.newCall(activateRequest).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
activity?.runOnUiThread {
|
||||
binding.resultText.text = getString(R.string.donate_activation_failed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
val activateResult = response.body?.string()?.trim()
|
||||
if (activateResult == "success") {
|
||||
prefs.isDonationActivated = true
|
||||
activity?.runOnUiThread {
|
||||
val remaining = (result?.toIntOrNull() ?: 1) - 1
|
||||
binding.resultText.text =
|
||||
getString(R.string.donate_token_activated) + "\n" +
|
||||
getString(R.string.donate_token_left, remaining.toString())
|
||||
}
|
||||
} else {
|
||||
activity?.runOnUiThread {
|
||||
binding.resultText.text = getString(R.string.donate_activation_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private fun hasInternetPermission(context: Context): Boolean {
|
||||
return ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.INTERNET
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
|
@ -1,153 +1,193 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.Manifest
|
||||
import android.app.role.RoleManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import java.lang.NumberFormatException
|
||||
import android.text.InputType
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import partisan.weforge.xyz.pulse.getServicePriority
|
||||
import partisan.weforge.xyz.pulse.setServicePriority
|
||||
import partisan.weforge.xyz.pulse.isServiceEnabled
|
||||
import partisan.weforge.xyz.pulse.setServiceEnabled
|
||||
import partisan.weforge.xyz.pulse.databinding.ActivityMainBinding
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import partisan.weforge.xyz.pulse.REQUIRED_PERMISSIONS
|
||||
import partisan.weforge.xyz.pulse.hasCallRedirectionRole
|
||||
import partisan.weforge.xyz.pulse.hasDrawOverlays
|
||||
import partisan.weforge.xyz.pulse.hasGeneralPermissions
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private val PERMISSIONS = arrayOf(
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.CALL_PHONE,
|
||||
)
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var prefs: Preferences
|
||||
private lateinit var window: PopupWindow
|
||||
private var roleManager: RoleManager? = null
|
||||
|
||||
private var popupSwitch: SwitchMaterial? = null
|
||||
private var popupMenuItem: MenuItem? = null
|
||||
private val registerForCallRedirectionRole =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
|
||||
|
||||
val popupToggle: SwitchMaterial
|
||||
get() = findViewById(R.id.globalPopupToggle)
|
||||
private val registerForGeneralPermissions =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {}
|
||||
|
||||
private val registerForDrawOverlays =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
prefs = Preferences(this)
|
||||
updateDonationIcon()
|
||||
setContentView(binding.root)
|
||||
init()
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
window.cancel()
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
prefs = Preferences(this)
|
||||
setSupportActionBar(binding.topAppBar)
|
||||
window = PopupWindow(this, null)
|
||||
roleManager = getSystemService(RoleManager::class.java)
|
||||
binding.apply {
|
||||
redirectionDelay.value = (prefs.redirectionDelay / 1000).toFloat()
|
||||
popupPosition.editText?.setText(prefs.popupPosition.toString())
|
||||
toggle.isChecked = prefs.isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
val drawerToggle = ActionBarDrawerToggle(
|
||||
this,
|
||||
binding.drawerLayout,
|
||||
binding.topAppBar,
|
||||
R.string.navigation_drawer_open,
|
||||
R.string.navigation_drawer_open // The "close" string is never actually shown in the UI, so I reuse "navigation_drawer_open" as sort of a placeholder
|
||||
private fun setup() {
|
||||
binding.apply {
|
||||
redirectionDelay.setLabelFormatter {
|
||||
String.format("%.1f", it)
|
||||
}
|
||||
redirectionDelay.addOnChangeListener { _, value, _ ->
|
||||
prefs.redirectionDelay = (value * 1000).toLong()
|
||||
}
|
||||
popupPosition.setEndIconOnClickListener {
|
||||
window.preview()
|
||||
}
|
||||
popupPosition.editText?.doAfterTextChanged {
|
||||
try {
|
||||
prefs.popupPosition = it?.toString()?.toInt() ?: return@doAfterTextChanged
|
||||
} catch (exc: NumberFormatException) {}
|
||||
}
|
||||
toggle.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked && !hasPermissions()) {
|
||||
toggle.isChecked = false
|
||||
requestPermissions()
|
||||
return@setOnCheckedChangeListener
|
||||
}
|
||||
prefs.isEnabled = isChecked
|
||||
}
|
||||
val services = listOf(
|
||||
ServiceEntry("vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call", R.string.destination_signal, this@MainActivity.isServiceEnabled("vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call")),
|
||||
ServiceEntry("vnd.android.cursor.item/vnd.org.telegram.messenger.android.call", R.string.destination_telegram, this@MainActivity.isServiceEnabled("vnd.android.cursor.item/vnd.org.telegram.messenger.android.call")),
|
||||
ServiceEntry("vnd.android.cursor.item/vnd.ch.threema.app.call", R.string.destination_threema, this@MainActivity.isServiceEnabled("vnd.android.cursor.item/vnd.ch.threema.app.call")),
|
||||
ServiceEntry("vnd.android.cursor.item/vnd.com.whatsapp.voip.call", R.string.destination_whatsapp, this@MainActivity.isServiceEnabled("vnd.android.cursor.item/vnd.com.whatsapp.voip.call")),
|
||||
)
|
||||
binding.drawerLayout.addDrawerListener(drawerToggle)
|
||||
drawerToggle.syncState()
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, MainFragment())
|
||||
.commit()
|
||||
|
||||
setupPopupToggle(false)
|
||||
|
||||
binding.navigationView.setNavigationItemSelectedListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.action_home -> {
|
||||
supportFragmentManager.popBackStack(null, androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, MainFragment())
|
||||
.commit()
|
||||
true
|
||||
}
|
||||
R.id.action_popup_settings -> {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, PopupSettingsFragment())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
true
|
||||
}
|
||||
R.id.action_about -> {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, AboutFragment())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
true
|
||||
}
|
||||
R.id.action_services -> {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, ServiceSettingsFragment())
|
||||
.addToBackStack(null)
|
||||
.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())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
true
|
||||
}
|
||||
R.id.action_donate -> {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragmentContainer, DonateFragment())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}.also {
|
||||
binding.drawerLayout.closeDrawers()
|
||||
val adapter = ServiceAdapter(
|
||||
context = this@MainActivity,
|
||||
services = services.toMutableList(),
|
||||
onReordered = { updatedList ->
|
||||
updatedList.forEachIndexed { index, entry ->
|
||||
setServicePriority(entry.mimetype, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.topbar_toggle, menu)
|
||||
popupMenuItem = menu.findItem(R.id.globalPopupToggle)
|
||||
popupSwitch = popupMenuItem?.actionView?.findViewById(R.id.globalPopupToggle)
|
||||
popupMenuItem?.isVisible = false // hide by default
|
||||
return true
|
||||
}
|
||||
|
||||
fun setupPopupToggle(
|
||||
visible: Boolean,
|
||||
initialState: Boolean = false,
|
||||
onToggle: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
popupMenuItem?.isVisible = visible
|
||||
popupSwitch?.apply {
|
||||
setOnCheckedChangeListener(null)
|
||||
isChecked = initialState
|
||||
setOnCheckedChangeListener { _, isChecked ->
|
||||
onToggle?.invoke(isChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDonationIcon() {
|
||||
val donateItem = binding.navigationView.menu.findItem(R.id.action_donate)
|
||||
donateItem.setIcon(
|
||||
if (prefs.isDonationActivated)
|
||||
R.drawable.heart_filled_24
|
||||
else
|
||||
R.drawable.heart_24
|
||||
)
|
||||
binding.serviceRecycler.adapter = adapter
|
||||
binding.serviceRecycler.layoutManager = LinearLayoutManager(this@MainActivity)
|
||||
|
||||
val touchHelper = ItemTouchHelper(adapter.dragHelper)
|
||||
touchHelper.attachToRecyclerView(binding.serviceRecycler)
|
||||
|
||||
adapter.setDragStartListener { viewHolder ->
|
||||
touchHelper.startDrag(viewHolder)
|
||||
}
|
||||
|
||||
fun setAppBarTitle(vararg parts: String) {
|
||||
binding.topAppBar.title = parts.joinToString(" > ")
|
||||
// binding.serviceConfigList.removeAllViews()
|
||||
// for ((mimetype, labelRes) in mimetypes) {
|
||||
// val checkbox = CheckBox(this@MainActivity).apply {
|
||||
// text = getString(labelRes)
|
||||
// isChecked = this@MainActivity.isServiceEnabled(mimetype)
|
||||
// setOnCheckedChangeListener { _, checked ->
|
||||
// this@MainActivity.setServiceEnabled(mimetype, checked)
|
||||
// }
|
||||
// }
|
||||
|
||||
// val priorityInput = EditText(this@MainActivity).apply {
|
||||
// inputType = InputType.TYPE_CLASS_NUMBER
|
||||
// setEms(4)
|
||||
// hint = "Priority"
|
||||
// setText(this@MainActivity.getServicePriority(mimetype).toString())
|
||||
// setOnFocusChangeListener { _, hasFocus ->
|
||||
// if (!hasFocus) {
|
||||
// val value = text.toString().toIntOrNull()
|
||||
// if (value != null) this@MainActivity.setServicePriority(mimetype, value)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// val row = LinearLayout(this@MainActivity).apply {
|
||||
// orientation = LinearLayout.HORIZONTAL
|
||||
// setPadding(0, 16, 0, 16)
|
||||
// addView(checkbox, LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f))
|
||||
// addView(priorityInput, LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT))
|
||||
// }
|
||||
|
||||
// binding.serviceConfigList.addView(row)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
when {
|
||||
!hasGeneralPermissions() -> requestGeneralPermissions()
|
||||
!hasDrawOverlays() -> requestDrawOverlays()
|
||||
!hasCallRedirectionRole() -> requestCallRedirectionRole()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasPermissions(): Boolean {
|
||||
return hasGeneralPermissions(this) &&
|
||||
hasDrawOverlays(this) &&
|
||||
hasCallRedirectionRole(this)
|
||||
return hasGeneralPermissions() && hasDrawOverlays() && hasCallRedirectionRole()
|
||||
}
|
||||
|
||||
private fun requestDrawOverlays() {
|
||||
registerForDrawOverlays.launch(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION))
|
||||
}
|
||||
|
||||
private fun requestGeneralPermissions() {
|
||||
registerForGeneralPermissions.launch(PERMISSIONS)
|
||||
}
|
||||
|
||||
private fun hasGeneralPermissions(): Boolean {
|
||||
return !PERMISSIONS.any { checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED }
|
||||
}
|
||||
|
||||
private fun hasDrawOverlays(): Boolean {
|
||||
return Settings.canDrawOverlays(this)
|
||||
}
|
||||
|
||||
private fun requestCallRedirectionRole() {
|
||||
registerForCallRedirectionRole
|
||||
.launch(roleManager?.createRequestRoleIntent(RoleManager.ROLE_CALL_REDIRECTION))
|
||||
}
|
||||
|
||||
private fun hasCallRedirectionRole(): Boolean {
|
||||
return roleManager?.isRoleHeld(RoleManager.ROLE_CALL_REDIRECTION) ?: false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import java.util.concurrent.TimeUnit
|
||||
import nl.dionsegijn.konfetti.core.Party
|
||||
import nl.dionsegijn.konfetti.core.Position
|
||||
import nl.dionsegijn.konfetti.core.emitter.Emitter
|
||||
import nl.dionsegijn.konfetti.xml.KonfettiView
|
||||
|
||||
class MainFragment : Fragment() {
|
||||
|
||||
private lateinit var prefs: Preferences
|
||||
private var lastConfettiTime = 0L
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(requireActivity() as? MainActivity)?.setAppBarTitle(getString(R.string.app_name))
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val view = inflater.inflate(R.layout.fragment_main, container, false)
|
||||
prefs = Preferences(requireContext())
|
||||
|
||||
val toggle = view.findViewById<MaterialButton>(R.id.toggle)
|
||||
val konfetti = view.findViewById<KonfettiView>(R.id.confettiView)
|
||||
|
||||
toggle.isCheckable = true
|
||||
toggle.isChecked = prefs.isServiceEnabledByUser
|
||||
|
||||
toggle.setOnClickListener {
|
||||
// the button toggles itself internally since it's checkable
|
||||
val isNowChecked = toggle.isChecked
|
||||
prefs.isServiceEnabledByUser = isNowChecked
|
||||
|
||||
if (isNowChecked && SystemClock.elapsedRealtime() - lastConfettiTime > 500) {
|
||||
konfetti.start(
|
||||
Party(
|
||||
emitter =
|
||||
Emitter(duration = 100, TimeUnit.MILLISECONDS)
|
||||
.perSecond(100),
|
||||
speed = 25f,
|
||||
maxSpeed = 30f,
|
||||
damping = 0.85f,
|
||||
spread = 360,
|
||||
position = Position.Relative(0.5, 0.5)
|
||||
)
|
||||
)
|
||||
lastConfettiTime = SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
||||
toggle.post {
|
||||
toggle.jumpDrawablesToCurrentState()
|
||||
toggle.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
val warningText = view.findViewById<TextView>(R.id.warningText)
|
||||
val warnings = mutableListOf<String>()
|
||||
|
||||
// 1. Check if contacts are available
|
||||
val contactCursor =
|
||||
requireContext()
|
||||
.contentResolver
|
||||
.query(
|
||||
android.provider.ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
if (contactCursor != null) {
|
||||
contactCursor.use {
|
||||
if (!it.moveToFirst()) {
|
||||
warnings.add(getString(R.string.warn_no_contacts))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warnings.add(getString(R.string.warn_no_contacts))
|
||||
}
|
||||
|
||||
// 2. Check internet connectivity
|
||||
if (!hasInternet()) {
|
||||
warnings.add(getString(R.string.warn_no_internet))
|
||||
}
|
||||
|
||||
// Show warning if needed
|
||||
if (warnings.isNotEmpty()) {
|
||||
warningText.text = warnings.joinToString("\n")
|
||||
warningText.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun hasInternet(): Boolean {
|
||||
val cm = requireContext().getSystemService(ConnectivityManager::class.java)
|
||||
val capabilities = cm?.getNetworkCapabilities(cm.activeNetwork) ?: return false
|
||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.view.View
|
||||
import kotlin.random.Random
|
||||
|
||||
class MatrixRainView(context: Context) : View(context) {
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.GREEN
|
||||
textSize = 5f * resources.displayMetrics.density
|
||||
typeface = Typeface.MONOSPACE
|
||||
}
|
||||
|
||||
private val charset = "01アイウエオカキクケコ".toCharArray()
|
||||
private val random = Random
|
||||
private var columns = 0
|
||||
private lateinit var yOffsets: IntArray
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
columns = w / paint.textSize.toInt()
|
||||
yOffsets = IntArray(columns) { random.nextInt(h) }
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
// drawColor with transparent clear instead of black
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
||||
|
||||
for (i in 0 until columns) {
|
||||
val x = i * paint.textSize
|
||||
val y = yOffsets[i].toFloat()
|
||||
val char = charset[random.nextInt(charset.size)]
|
||||
|
||||
paint.alpha = 255
|
||||
canvas.drawText(char.toString(), x, y, paint)
|
||||
|
||||
paint.alpha = 100
|
||||
canvas.drawText(char.toString(), x, y - paint.textSize, paint)
|
||||
|
||||
yOffsets[i] += paint.textSize.toInt()
|
||||
if (yOffsets[i] > height) {
|
||||
yOffsets[i] = 0
|
||||
}
|
||||
}
|
||||
postInvalidateDelayed(50)
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.provider.Settings
|
||||
import android.app.role.RoleManager
|
||||
|
||||
val REQUIRED_PERMISSIONS = arrayOf(
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.CALL_PHONE,
|
||||
)
|
||||
|
||||
fun hasGeneralPermissions(context: Context): Boolean {
|
||||
return REQUIRED_PERMISSIONS.all {
|
||||
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
fun hasDrawOverlays(context: Context): Boolean {
|
||||
return Settings.canDrawOverlays(context)
|
||||
}
|
||||
|
||||
fun hasCallRedirectionRole(context: Context): Boolean {
|
||||
val roleManager = context.getSystemService(RoleManager::class.java)
|
||||
return roleManager?.isRoleHeld(RoleManager.ROLE_CALL_REDIRECTION) ?: false
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AdapterView
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.Spinner
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.core.content.getSystemService
|
||||
import partisan.weforge.xyz.pulse.databinding.FragmentPopupSettingsBinding
|
||||
|
||||
class PopupSettingsFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentPopupSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var prefs: Preferences
|
||||
private lateinit var window: PopupWindow
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentPopupSettingsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateSpinner()
|
||||
(requireActivity() as? MainActivity)?.apply {
|
||||
setAppBarTitle(getString(R.string.settings_name), getString(R.string.popup_name))
|
||||
setupPopupToggle(true, prefs.popupEnabled) { isChecked ->
|
||||
prefs.popupEnabled = isChecked
|
||||
updateControls(isChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
(requireActivity() as? MainActivity)?.setupPopupToggle(false)
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
prefs = Preferences(requireContext())
|
||||
window = PopupWindow(requireContext(), null)
|
||||
|
||||
binding.popupPreview.setOnClickListener {
|
||||
window.preview(false)
|
||||
}
|
||||
binding.popupPreview.setOnLongClickListener {
|
||||
window.preview(true)
|
||||
true
|
||||
}
|
||||
|
||||
binding.redirectionDelay.value = (prefs.redirectionDelay / 1000).toFloat()
|
||||
binding.redirectionDelay.setLabelFormatter {
|
||||
String.format("%.1f", it)
|
||||
}
|
||||
binding.redirectionDelay.addOnChangeListener { _, value, _ ->
|
||||
prefs.redirectionDelay = (value * 1000).toLong()
|
||||
}
|
||||
|
||||
updateSpinner()
|
||||
|
||||
val screenHeight = getScreenHeightPx()
|
||||
binding.popupHeightSlider.valueFrom = 0f
|
||||
binding.popupHeightSlider.valueTo = screenHeight.toFloat()
|
||||
binding.popupHeightSlider.value = prefs.popupPosition.toFloat()
|
||||
binding.popupHeightSlider.addOnChangeListener { _, value, _ ->
|
||||
prefs.popupPosition = value.toInt().coerceIn(0, screenHeight)
|
||||
}
|
||||
|
||||
updateControls(prefs.popupEnabled)
|
||||
}
|
||||
|
||||
private fun updateControls(enabled: Boolean) {
|
||||
binding.redirectionDelay.isEnabled = enabled
|
||||
binding.popupHeightSlider.isEnabled = enabled
|
||||
binding.popupPreview.isEnabled = enabled
|
||||
binding.popupEffectSpinner.isEnabled = enabled
|
||||
binding.popupEffectLabel.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun getScreenHeightPx(): Int {
|
||||
val wm = requireContext().getSystemService<android.view.WindowManager>()!!
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val bounds: Rect = wm.currentWindowMetrics.bounds
|
||||
bounds.height()
|
||||
} else {
|
||||
val metrics = DisplayMetrics()
|
||||
@Suppress("DEPRECATION")
|
||||
requireActivity().windowManager.defaultDisplay.getMetrics(metrics)
|
||||
metrics.heightPixels
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSpinner() {
|
||||
val allEffects = Preferences.PopupEffect.values()
|
||||
val effectLabels = resources.getStringArray(R.array.popup_effects)
|
||||
|
||||
val availableEffects = prefs.getAvailablePopupEffects() + listOf(
|
||||
Preferences.PopupEffect.NONE,
|
||||
Preferences.PopupEffect.RANDOM
|
||||
)
|
||||
|
||||
val displayNames = allEffects.mapIndexed { index, effect ->
|
||||
val baseName = effectLabels.getOrElse(index) { effect.name }
|
||||
if (!prefs.isDonationActivated && effect !in availableEffects)
|
||||
"$baseName 🔒"
|
||||
else
|
||||
baseName
|
||||
}
|
||||
|
||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, displayNames)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
binding.popupEffectSpinner.adapter = adapter
|
||||
|
||||
binding.popupEffectSpinner.setSelection(prefs.popupEffect.ordinal)
|
||||
|
||||
binding.popupEffectSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
||||
val selectedEffect = allEffects[position]
|
||||
if (!prefs.isDonationActivated &&
|
||||
selectedEffect !in prefs.getAvailablePopupEffects() &&
|
||||
selectedEffect != Preferences.PopupEffect.NONE &&
|
||||
selectedEffect != Preferences.PopupEffect.RANDOM
|
||||
) {
|
||||
Toast.makeText(requireContext(), getString(R.string.donate_lock), Toast.LENGTH_SHORT).show()
|
||||
binding.popupEffectSpinner.setSelection(prefs.popupEffect.ordinal)
|
||||
} else {
|
||||
prefs.popupEffect = selectedEffect
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>) {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
|
@ -3,49 +3,27 @@ package partisan.weforge.xyz.pulse
|
|||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.widget.TextView
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.widget.ProgressBar
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.core.content.res.use
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import kotlin.concurrent.timerTask
|
||||
import android.util.Log
|
||||
import android.content.res.ColorStateList
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import partisan.weforge.xyz.pulse.Preferences.PopupEffect
|
||||
import partisan.weforge.xyz.pulse.MatrixRainView
|
||||
|
||||
class PopupWindow(
|
||||
ctx: Context,
|
||||
private val ctx: Context,
|
||||
private val service: WeakReference<CallRedirectionService>?,
|
||||
) {
|
||||
private val themedCtx = DynamicColors.wrapContextIfAvailable(
|
||||
ContextThemeWrapper(ctx, R.style.Theme_Pulse)
|
||||
)
|
||||
private val prefs = Preferences(themedCtx)
|
||||
private val windowManager = themedCtx.getSystemService(WindowManager::class.java)
|
||||
private val audioManager = themedCtx.getSystemService(AudioManager::class.java)
|
||||
private val inflater = LayoutInflater.from(themedCtx)
|
||||
private val view = inflater.inflate(R.layout.popup, null)
|
||||
private val prefs = Preferences(ctx)
|
||||
private val windowManager = ctx.getSystemService(WindowManager::class.java)
|
||||
private val audioManager = ctx.getSystemService(AudioManager::class.java)
|
||||
@Suppress("InflateParams")
|
||||
private val view = LayoutInflater.from(ctx).inflate(R.layout.popup, null)
|
||||
private val layoutParams = WindowManager.LayoutParams().apply {
|
||||
format = PixelFormat.TRANSLUCENT
|
||||
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
|
@ -55,9 +33,6 @@ class PopupWindow(
|
|||
height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
y = prefs.popupPosition
|
||||
}
|
||||
private var currentEffect: PopupEffect = PopupEffect.NONE
|
||||
private var matrixOverlay: View? = null
|
||||
private var gamerAnimator: ValueAnimator? = null
|
||||
private var timer: Timer? = null
|
||||
|
||||
init {
|
||||
|
@ -65,385 +40,85 @@ class PopupWindow(
|
|||
cancel()
|
||||
service?.get()?.placeCallUnmodified()
|
||||
}
|
||||
|
||||
// This is utterly stupid, but it works
|
||||
applyResolvedColors(view)
|
||||
}
|
||||
|
||||
fun preview(isLongPress: Boolean = false) {
|
||||
fun preview() {
|
||||
remove()
|
||||
layoutParams.y = prefs.popupPosition
|
||||
|
||||
val destinations = listOf(
|
||||
val destinations = mutableListOf(
|
||||
R.string.destination_signal,
|
||||
R.string.destination_telegram,
|
||||
R.string.destination_threema,
|
||||
// Whatsapp smells
|
||||
)
|
||||
setDescription(destinations.random())
|
||||
add()
|
||||
|
||||
val duration = if (isLongPress) prefs.redirectionDelay * 5 else prefs.redirectionDelay
|
||||
timer?.cancel()
|
||||
timer = Timer()
|
||||
timer?.schedule(timerTask {
|
||||
remove()
|
||||
}, duration)
|
||||
}
|
||||
|
||||
fun show(uri: Uri, destinationId: Int) {
|
||||
val svc = service?.get() ?: return
|
||||
|
||||
val service = service?.get() ?: return
|
||||
if (!remove()) {
|
||||
service.placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
timer?.cancel()
|
||||
timer = Timer()
|
||||
timer?.schedule(timerTask {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (!remove()) {
|
||||
svc.placeCallUnmodified()
|
||||
return@post
|
||||
service.placeCallUnmodified()
|
||||
return@timerTask
|
||||
}
|
||||
if (audioManager?.mode != AudioManager.MODE_IN_CALL) {
|
||||
svc.placeCallUnmodified()
|
||||
return@post
|
||||
service.placeCallUnmodified()
|
||||
return@timerTask
|
||||
}
|
||||
try {
|
||||
call(uri)
|
||||
} catch (exc: SecurityException) {
|
||||
svc.placeCallUnmodified()
|
||||
return@post
|
||||
}
|
||||
svc.cancelCall()
|
||||
service.placeCallUnmodified()
|
||||
return@timerTask
|
||||
}
|
||||
service.cancelCall()
|
||||
}, prefs.redirectionDelay)
|
||||
|
||||
layoutParams.y = prefs.popupPosition
|
||||
setDescription(destinationId)
|
||||
startProgressAnimation(prefs.redirectionDelay)
|
||||
|
||||
if (!add()) {
|
||||
Log.w("PopupWindow", "add() failed – popup not shown, calling directly.")
|
||||
timer?.cancel()
|
||||
svc.placeCallUnmodified()
|
||||
service.placeCallUnmodified()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDescription(id: Int) {
|
||||
view.findViewById<TextView>(R.id.description).text = themedCtx.getString(
|
||||
view.findViewById<TextView>(R.id.description).text = ctx.getString(
|
||||
R.string.popup,
|
||||
themedCtx.getString(id),
|
||||
ctx.getString(id),
|
||||
)
|
||||
}
|
||||
|
||||
private fun startProgressAnimation(duration: Long) {
|
||||
val bar = view.findViewById<ProgressBar>(R.id.progress)
|
||||
bar.max = 100
|
||||
bar.progress = 0
|
||||
val animator = ObjectAnimator.ofInt(bar, "progress", 100)
|
||||
animator.duration = duration
|
||||
animator.start()
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.CALL_PHONE)
|
||||
fun call(data: Uri) {
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
this.data = data
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
themedCtx.startActivity(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateAppear() {
|
||||
view.animate().cancel()
|
||||
|
||||
// Always reset all transforms before animation
|
||||
view.rotationX = 0f
|
||||
view.alpha = 1f
|
||||
view.translationX = 0f
|
||||
view.translationY = 0f
|
||||
view.scaleX = 1f
|
||||
view.scaleY = 1f
|
||||
|
||||
// Reset gamer effect if it was active before
|
||||
gamerAnimator?.cancel()
|
||||
gamerAnimator = null
|
||||
|
||||
val card = view as MaterialCardView
|
||||
themedCtx.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorOutline)).use { ta ->
|
||||
val defaultStroke = ta.getColor(0, Color.DKGRAY)
|
||||
card.strokeColor = defaultStroke
|
||||
}
|
||||
|
||||
val effect = when (prefs.popupEffect) {
|
||||
PopupEffect.RANDOM -> prefs.getAvailablePopupEffects().random()
|
||||
else -> prefs.popupEffect
|
||||
}
|
||||
currentEffect = effect
|
||||
|
||||
when (effect) {
|
||||
PopupEffect.NONE -> {}
|
||||
PopupEffect.FADE -> {
|
||||
view.alpha = 0f
|
||||
view.animate().alpha(1f).setDuration(300).start()
|
||||
}
|
||||
PopupEffect.SCALE -> { //
|
||||
view.translationX = view.width.toFloat()
|
||||
view.alpha = 0f
|
||||
view.animate()
|
||||
.translationX(0f)
|
||||
.alpha(1f)
|
||||
.setDuration(350)
|
||||
.setInterpolator(DecelerateInterpolator(2f))
|
||||
.start()
|
||||
}
|
||||
PopupEffect.BOUNCE -> {
|
||||
view.scaleX = 0.7f
|
||||
view.scaleY = 0.7f
|
||||
view.animate().scaleX(1f).scaleY(1f)
|
||||
.setInterpolator(OvershootInterpolator())
|
||||
.setDuration(400).start()
|
||||
}
|
||||
PopupEffect.FLOP -> {
|
||||
view.rotationX = 90f
|
||||
view.alpha = 0f
|
||||
view.animate().rotationX(0f).alpha(1f)
|
||||
.setDuration(350)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.start()
|
||||
}
|
||||
PopupEffect.MATRIX -> {
|
||||
val rainView = MatrixRainView(themedCtx)
|
||||
matrixOverlay?.let {
|
||||
try {
|
||||
windowManager?.removeViewImmediate(it)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
matrixOverlay = rainView
|
||||
|
||||
val popupBounds = Rect()
|
||||
view.getGlobalVisibleRect(popupBounds)
|
||||
|
||||
val overlayParams = WindowManager.LayoutParams().apply {
|
||||
type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
format = PixelFormat.TRANSLUCENT
|
||||
flags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
width = view.width
|
||||
height = view.height
|
||||
gravity = Gravity.BOTTOM
|
||||
x = 0
|
||||
y = layoutParams.y
|
||||
}
|
||||
|
||||
try {
|
||||
windowManager?.addView(rainView, overlayParams)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MatrixRain", "Failed to add rainView", e)
|
||||
return
|
||||
}
|
||||
|
||||
// Fade-in popup over 500ms
|
||||
view.alpha = 0f
|
||||
view.animate().cancel()
|
||||
view.animate().alpha(1f).setDuration(500).start()
|
||||
|
||||
// Remove MatrixRainView in sync
|
||||
rainView.animate().alpha(0f).setDuration(500).withEndAction {
|
||||
try {
|
||||
windowManager?.removeView(rainView)
|
||||
matrixOverlay = null
|
||||
} catch (_: Exception) {}
|
||||
}.start()
|
||||
}
|
||||
PopupEffect.SLIDE_SNAP -> {
|
||||
view.translationY = 200f
|
||||
view.alpha = 0f
|
||||
view.animate()
|
||||
.translationY(0f)
|
||||
.alpha(1f)
|
||||
.setDuration(350)
|
||||
.setInterpolator(OvershootInterpolator(2f))
|
||||
.start()
|
||||
}
|
||||
PopupEffect.GAMER_MODE -> {
|
||||
val popupCard = view as MaterialCardView
|
||||
val hsv = floatArrayOf(0f, 1f, 1f)
|
||||
|
||||
gamerAnimator?.cancel() // Cancel any existing animator before starting new one
|
||||
|
||||
gamerAnimator = ValueAnimator.ofFloat(0f, 360f).apply {
|
||||
duration = 2000
|
||||
repeatCount = ValueAnimator.INFINITE
|
||||
addUpdateListener {
|
||||
hsv[0] = it.animatedValue as Float
|
||||
popupCard.strokeColor = Color.HSVToColor(hsv)
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
view.alpha = 0f
|
||||
view.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(400)
|
||||
.start()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateDisappear(onEnd: () -> Unit) {
|
||||
|
||||
// While the reset after animation can be a nice safety net, it's causing visual glitches and is already run at the start of every animation anyway
|
||||
// val resetAndFinish = {
|
||||
// view.animate().cancel()
|
||||
// view.translationX = 0f
|
||||
// view.translationY = 0f
|
||||
// view.scaleX = 1f
|
||||
// view.scaleY = 1f
|
||||
// view.rotationX = 0f
|
||||
// view.rotationY = 0f
|
||||
// view.alpha = 1f
|
||||
// Log.d("PopupWindow", "Reset and finish after disappear animation")
|
||||
// onEnd()
|
||||
// }
|
||||
|
||||
val end = Runnable {
|
||||
Log.d("PopupWindow", "Disappearance animation complete")
|
||||
view.post { onEnd() } // defer by one frame to ensure alpha=0 is rendered
|
||||
}
|
||||
|
||||
when (currentEffect) {
|
||||
|
||||
// PopupEffect.NONE -> falls to else
|
||||
|
||||
PopupEffect.FADE -> view.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(200)
|
||||
.withEndAction(end)
|
||||
.start()
|
||||
|
||||
PopupEffect.SCALE -> view.animate()
|
||||
.translationX(view.width.toFloat())
|
||||
.alpha(0f)
|
||||
.setDuration(200)
|
||||
.setInterpolator(DecelerateInterpolator(2f))
|
||||
.withEndAction(end)
|
||||
.start()
|
||||
|
||||
PopupEffect.BOUNCE -> view.animate()
|
||||
.scaleX(0f)
|
||||
.scaleY(0f)
|
||||
.setDuration(200)
|
||||
.withEndAction(end)
|
||||
.start()
|
||||
|
||||
PopupEffect.FLOP -> view.animate()
|
||||
.rotationX(90f)
|
||||
.alpha(0f)
|
||||
.setDuration(200)
|
||||
.withEndAction(end)
|
||||
.start()
|
||||
|
||||
PopupEffect.MATRIX -> {
|
||||
view.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(150)
|
||||
.withEndAction(end)
|
||||
.start()
|
||||
|
||||
matrixOverlay?.let { overlay ->
|
||||
overlay.animate().cancel()
|
||||
overlay.animate().alpha(0f).setDuration(150).withEndAction {
|
||||
try {
|
||||
windowManager?.removeViewImmediate(overlay)
|
||||
} catch (_: Exception) {}
|
||||
matrixOverlay = null
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
PopupEffect.SLIDE_SNAP -> view.animate()
|
||||
.translationY(200f)
|
||||
.alpha(0f)
|
||||
.setDuration(200)
|
||||
.setInterpolator(DecelerateInterpolator(2f))
|
||||
.withEndAction(end)
|
||||
.start()
|
||||
|
||||
else -> end.run()
|
||||
private fun call(data: Uri) {
|
||||
Intent(Intent.ACTION_VIEW).let {
|
||||
it.data = data
|
||||
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
ctx.startActivity(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun add(): Boolean {
|
||||
try {
|
||||
// If already attached, force remove and re-add
|
||||
if (view.parent != null) {
|
||||
windowManager?.removeViewImmediate(view)
|
||||
}
|
||||
view.animate().cancel()
|
||||
windowManager?.addView(view, layoutParams)
|
||||
animateAppear()
|
||||
} catch (exc: Exception) {
|
||||
Log.e("PopupWindow", "Failed to add popup view", exc)
|
||||
return false
|
||||
}
|
||||
} catch (exc: WindowManager.BadTokenException) { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
private fun remove(onRemoved: (() -> Unit)? = null): Boolean {
|
||||
return try {
|
||||
animateDisappear {
|
||||
private fun remove(): Boolean {
|
||||
try {
|
||||
windowManager?.removeView(view)
|
||||
matrixOverlay?.let {
|
||||
try {
|
||||
windowManager?.removeViewImmediate(it)
|
||||
} catch (e: Exception) {
|
||||
Log.e("PopupWindow", "Failed to remove matrix overlay", e)
|
||||
}
|
||||
matrixOverlay = null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("PopupWindow", "Failed to remove popup view", e)
|
||||
}
|
||||
onRemoved?.invoke()
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e("PopupWindow", "Exception during remove()", e)
|
||||
false
|
||||
}
|
||||
} catch (exc: IllegalArgumentException) {
|
||||
} catch (exc: WindowManager.BadTokenException) { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
Log.d("PopupWindow", "Cancel called")
|
||||
timer?.cancel()
|
||||
remove()
|
||||
}
|
||||
|
||||
private fun applyResolvedColors(view: android.view.View) {
|
||||
val attrSurface = com.google.android.material.R.attr.colorSurface
|
||||
val attrOnSurface = com.google.android.material.R.attr.colorOnSurface
|
||||
val attrPrimary = com.google.android.material.R.attr.colorPrimaryVariant
|
||||
|
||||
try {
|
||||
themedCtx.obtainStyledAttributes(intArrayOf(attrSurface, attrOnSurface, attrPrimary)).use { ta ->
|
||||
val surface = ta.getColor(0, Color.LTGRAY)
|
||||
val onSurface = ta.getColor(1, Color.DKGRAY)
|
||||
val primary = ta.getColor(2, Color.DKGRAY)
|
||||
|
||||
(view as? MaterialCardView)?.setCardBackgroundColor(surface)
|
||||
view.findViewById<TextView>(R.id.description)?.setTextColor(onSurface)
|
||||
|
||||
view.findViewById<ProgressBar>(R.id.progress)?.let { bar ->
|
||||
bar.progressTintList = ColorStateList.valueOf(primary)
|
||||
bar.progressBackgroundTintList = ColorStateList.valueOf(
|
||||
primary and 0x00FFFFFF or (0x40 shl 24)
|
||||
)
|
||||
}
|
||||
|
||||
view.invalidate()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("PopupTheme", "Color resolution error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,84 +4,24 @@ import android.content.Context
|
|||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
|
||||
class Preferences(private val context: Context) {
|
||||
|
||||
class Preferences(ctx: Context) {
|
||||
companion object {
|
||||
private const val ENABLED = "enabled"
|
||||
private const val REDIRECTION_DELAY = "redirection_delay"
|
||||
private const val POPUP_POSITION = "popup_position_y"
|
||||
private const val POPUP_ENABLED = "popup_enabled"
|
||||
private val POPUP_EFFECT = "popup_effect"
|
||||
private const val BLACKLISTED_CONTACTS = "blacklisted_contacts"
|
||||
private const val BLACKLIST_ENABLED = "blacklist_enabled"
|
||||
private val SERVICE_ORDER_KEY = "service_order"
|
||||
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
|
||||
|
||||
// migration
|
||||
private const val SERVICE_ENABLED = "service_enabled"
|
||||
}
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
// Whether user enabled/disabled the service manually by tiggle button
|
||||
var isServiceEnabledByUser: Boolean
|
||||
get() = prefs.getBoolean(SERVICE_ENABLED, true)
|
||||
set(value) = prefs.edit { putBoolean(SERVICE_ENABLED, value) }
|
||||
|
||||
// True only if all required permissions + toggle are satisfied
|
||||
val isEnabled: Boolean
|
||||
get() = isServiceEnabledByUser &&
|
||||
hasGeneralPermissions(context) &&
|
||||
hasCallRedirectionRole(context) &&
|
||||
(popupEnabled.not() || hasDrawOverlays(context))
|
||||
|
||||
enum class PopupEffect {
|
||||
NONE, FADE, SCALE, BOUNCE, FLOP, MATRIX, SLIDE_SNAP, GAMER_MODE, RANDOM
|
||||
}
|
||||
|
||||
var popupEffect: PopupEffect
|
||||
get() {
|
||||
val name = prefs.getString(POPUP_EFFECT, PopupEffect.FADE.name) ?: PopupEffect.FADE.name
|
||||
return try {
|
||||
PopupEffect.valueOf(name)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// If invalid, fallback and clear the broken value
|
||||
prefs.edit().remove(POPUP_EFFECT).apply()
|
||||
PopupEffect.BOUNCE
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
prefs.edit().putString(POPUP_EFFECT, value.name).apply()
|
||||
}
|
||||
|
||||
var isBlacklistEnabled: Boolean
|
||||
get() = prefs.getBoolean(BLACKLIST_ENABLED, false)
|
||||
set(value) = prefs.edit { putBoolean(BLACKLIST_ENABLED, value) }
|
||||
|
||||
var isDonationActivated: Boolean
|
||||
get() = prefs.getBoolean(DONATION_ACTIVATED, false)
|
||||
set(value) = prefs.edit { putBoolean(DONATION_ACTIVATED, value) }
|
||||
|
||||
var donationToken: String
|
||||
get() {
|
||||
val stored = prefs.getString(DONATION_TOKEN, null)
|
||||
return if (stored != null) {
|
||||
stored
|
||||
} else {
|
||||
generateAndStoreToken()
|
||||
}
|
||||
}
|
||||
set(value) = prefs.edit { putString(DONATION_TOKEN, value) }
|
||||
|
||||
var popupEnabled: Boolean
|
||||
get() = prefs.getBoolean(POPUP_ENABLED, true)
|
||||
set(value) = prefs.edit { putBoolean(POPUP_ENABLED, value) }
|
||||
var isEnabled: Boolean
|
||||
get() = prefs.getBoolean(ENABLED, prefs.getBoolean(SERVICE_ENABLED, false))
|
||||
set(value) = prefs.edit { putBoolean(ENABLED, value) }
|
||||
|
||||
var redirectionDelay: Long
|
||||
get() = prefs.getLong(REDIRECTION_DELAY, DEFAULT_REDIRECTION_DELAY)
|
||||
|
@ -91,87 +31,26 @@ 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"
|
||||
|
||||
fun getAvailablePopupEffects(): List<PopupEffect> {
|
||||
val locked = listOf(
|
||||
PopupEffect.FLOP,
|
||||
PopupEffect.MATRIX,
|
||||
PopupEffect.SLIDE_SNAP,
|
||||
PopupEffect.GAMER_MODE
|
||||
)
|
||||
return PopupEffect.values().filter {
|
||||
isDonationActivated || it !in locked
|
||||
}.filter { it != PopupEffect.RANDOM && it != PopupEffect.NONE }
|
||||
}
|
||||
|
||||
/** Whether this service is enabled */
|
||||
fun isServiceEnabled(mimetype: String): Boolean {
|
||||
return prefs.getBoolean(makeKeyEnabled(mimetype), true)
|
||||
}
|
||||
|
||||
/** Current priority for this service (lower = higher priority) */
|
||||
fun getServicePriority(mimetype: String): Int {
|
||||
val order = getServiceOrder()
|
||||
val index = order.indexOf(mimetype)
|
||||
return if (index != -1) index else Int.MAX_VALUE
|
||||
}
|
||||
|
||||
fun getServiceOrder(): List<String> {
|
||||
val stored = prefs.getString(SERVICE_ORDER_KEY, null)
|
||||
return stored?.split("|")?.filter { it.isNotBlank() } ?: emptyList()
|
||||
}
|
||||
|
||||
fun setServiceOrder(order: List<String>) {
|
||||
prefs.edit().putString(SERVICE_ORDER_KEY, order.joinToString("|")).apply()
|
||||
return prefs.getInt(makeKeyPriority(mimetype), Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
/** Enable or disable individual service */
|
||||
fun setServiceEnabled(mimetype: String, enabled: Boolean) {
|
||||
prefs.edit().putBoolean(makeKeyEnabled(mimetype), enabled).apply()
|
||||
}
|
||||
|
||||
/** Change priority for an individual service */
|
||||
fun setServicePriority(mimetype: String, priority: Int) {
|
||||
prefs.edit().putInt(makeKeyPriority(mimetype), priority).apply()
|
||||
}
|
||||
|
||||
var blacklistedContacts: Set<String>
|
||||
get() = prefs.getStringSet(BLACKLISTED_CONTACTS, emptySet()) ?: emptySet()
|
||||
set(value) = prefs.edit { putStringSet(BLACKLISTED_CONTACTS, value) }
|
||||
|
||||
fun isContactWhitelisted(phoneNumber: String): Boolean {
|
||||
return !blacklistedContacts.contains(phoneNumber)
|
||||
}
|
||||
|
||||
fun setContactWhitelisted(phoneNumber: String, allowed: Boolean) {
|
||||
val current = blacklistedContacts.toMutableSet()
|
||||
if (!allowed) {
|
||||
current.add(phoneNumber)
|
||||
} else {
|
||||
current.remove(phoneNumber)
|
||||
}
|
||||
blacklistedContacts = current
|
||||
}
|
||||
|
||||
private fun generateAndStoreToken(): String {
|
||||
val newToken = (1..16)
|
||||
.map { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".random() }
|
||||
.joinToString("")
|
||||
prefs.edit().putString(DONATION_TOKEN, newToken).apply()
|
||||
return newToken
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
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
|
||||
|
||||
class SecretFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return SecretView(requireContext())
|
||||
}
|
||||
}
|
|
@ -1,905 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.Choreographer
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import kotlin.math.*
|
||||
import kotlin.random.Random
|
||||
|
||||
class SecretView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : View(context, attrs), Choreographer.FrameCallback {
|
||||
|
||||
private val bulletPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
strokeWidth = 6f
|
||||
}
|
||||
private val starPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
alpha = 40
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val playerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 4f
|
||||
}
|
||||
private val enemyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 4f
|
||||
}
|
||||
private val rocketPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.RED
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 3f
|
||||
}
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = 64f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
private val retryRect = RectF()
|
||||
private val retryPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.DKGRAY
|
||||
}
|
||||
private val retryTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = 48f
|
||||
}
|
||||
private var colorSecondary: Int = Color.GREEN
|
||||
|
||||
private var playerX = 0f
|
||||
private var viewWidth = 0f
|
||||
private var viewHeight = 0f
|
||||
|
||||
private var enemyClearAggression = 0f
|
||||
private var adaptiveSpawnTimer = 0L
|
||||
private var lastEnemyCount = 0
|
||||
private var avgEnemiesPerSecond = 0f
|
||||
|
||||
private var isTouching = false
|
||||
private var shieldRechargeTimer = 0L
|
||||
private var shieldFlashAlpha = 0f
|
||||
private var lastMissileSide = -1
|
||||
private var missileCooldown = 0L
|
||||
private var bulletCooldownMs = 0L
|
||||
private var gameOver = false
|
||||
private var score = 0
|
||||
|
||||
// Player level
|
||||
private var multiFireLevel = 1
|
||||
private var piercingLevel = 1
|
||||
private var shieldLevel = 0
|
||||
private var missileLevel = 0
|
||||
private var rapidFireLevel = 1
|
||||
|
||||
private val bullets = mutableListOf<Bullet>()
|
||||
private val enemyBullets = mutableListOf<Bullet>()
|
||||
private val enemies = mutableListOf<Enemy>()
|
||||
private val rockets = mutableListOf<Rocket>()
|
||||
private val stars = mutableListOf<Star>()
|
||||
private val explosions = mutableListOf<Explosion>()
|
||||
private val rocketTrails = mutableListOf<Pair<Float, Float>>()
|
||||
private val pickups = mutableListOf<Pickup>()
|
||||
private val playerMissiles = mutableListOf<PlayerMissile>()
|
||||
|
||||
private data class Bullet(var x: Float, var y: Float, val dy: Float = -15f, var life: Int = 1)
|
||||
private data class Star(var x: Float, var y: Float, val radius: Float, val speed: Float)
|
||||
private data class Rocket(var x: Float, var y: Float, var angle: Float, val trail: MutableList<Pair<Float, Float>> = mutableListOf())
|
||||
private data class Explosion(var x: Float, var y: Float, var timer: Int = 12)
|
||||
private data class Pickup(
|
||||
val x: Float,
|
||||
var y: Float,
|
||||
val type: Int,
|
||||
var hue: Float = Random.nextFloat() * 360f,
|
||||
)
|
||||
private data class PlayerMissile(
|
||||
var x: Float,
|
||||
var y: Float,
|
||||
var angle: Float,
|
||||
var ttl: Long = 90000L,
|
||||
var target: Enemy? = null,
|
||||
var recheckCooldown: Long = 0L,
|
||||
var side: Int, // -1 = left, 1 = right
|
||||
val trail: MutableList<Pair<Float, Float>> = mutableListOf()
|
||||
)
|
||||
|
||||
private var lastLogicTime = 0L
|
||||
private val logicStepMs = 16L
|
||||
|
||||
private var waveTimer = 0L
|
||||
private var currentWave = 0
|
||||
private var enemiesLeftInWave = 0
|
||||
private var currentWaveType = ""
|
||||
|
||||
init {
|
||||
for (i in 0 until 50) {
|
||||
stars.add(Star(Random.nextFloat() * 1080f, Random.nextFloat() * 1920f, Random.nextFloat() * 2f + 1f, Random.nextFloat() * 2f + 0.5f))
|
||||
}
|
||||
|
||||
val colorAttrs = intArrayOf(
|
||||
com.google.android.material.R.attr.colorPrimaryVariant,
|
||||
com.google.android.material.R.attr.colorSecondary
|
||||
)
|
||||
val ta = context.obtainStyledAttributes(colorAttrs)
|
||||
try {
|
||||
playerPaint.color = ta.getColor(0, Color.CYAN)
|
||||
enemyPaint.color = ta.getColor(0, Color.CYAN)
|
||||
colorSecondary = ta.getColor(1, Color.GREEN)
|
||||
} finally {
|
||||
ta.recycle()
|
||||
}
|
||||
|
||||
Choreographer.getInstance().postFrameCallback(this)
|
||||
lastLogicTime = System.nanoTime()
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
viewWidth = w.toFloat()
|
||||
viewHeight = h.toFloat()
|
||||
// Kinda hacky way to center things, but I'd rather do this here than in update(), since it can't be in init() as the screen size isn't initialized at that point.
|
||||
playerX = viewWidth / 2f
|
||||
retryRect.set(viewWidth / 2f - 120f, viewHeight / 2f + 60f, viewWidth / 2f + 120f, viewHeight / 2f + 130f)
|
||||
}
|
||||
|
||||
override fun doFrame(frameTimeNanos: Long) {
|
||||
if (!gameOver) {
|
||||
val now = System.nanoTime()
|
||||
while ((now - lastLogicTime) / 1_000_000 >= logicStepMs) {
|
||||
update(logicStepMs)
|
||||
lastLogicTime += logicStepMs * 1_000_000
|
||||
}
|
||||
invalidate()
|
||||
Choreographer.getInstance().postFrameCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(deltaMs: Long) {
|
||||
stars.forEach {
|
||||
it.y += it.speed * deltaMs / 16f
|
||||
if (it.y > viewHeight) {
|
||||
it.y = 0f
|
||||
it.x = Random.nextFloat() * viewWidth
|
||||
}
|
||||
}
|
||||
|
||||
// Update shield recharge timer and flash animation
|
||||
if (shieldRechargeTimer > 0) {
|
||||
shieldRechargeTimer -= deltaMs
|
||||
if (shieldRechargeTimer <= 0) {
|
||||
shieldRechargeTimer = 0
|
||||
shieldFlashAlpha = 1f // trigger visual flash on recharge
|
||||
}
|
||||
}
|
||||
|
||||
if (shieldFlashAlpha > 0f) {
|
||||
shieldFlashAlpha -= deltaMs / 300f
|
||||
if (shieldFlashAlpha < 0f) shieldFlashAlpha = 0f
|
||||
}
|
||||
|
||||
rockets.forEach { rocket ->
|
||||
rocket.trail.add(0, rocket.x to rocket.y)
|
||||
if (rocket.trail.size > 20) {
|
||||
rocket.trail.removeLast()
|
||||
}
|
||||
}
|
||||
|
||||
explosions.forEach { it.timer-- }
|
||||
explosions.removeIf { it.timer <= 0 }
|
||||
|
||||
bullets.forEach { it.y += it.dy * deltaMs / 16f }
|
||||
bullets.removeIf { it.y < 0 || it.life <= 0 }
|
||||
|
||||
bulletCooldownMs -= deltaMs
|
||||
if (isTouching && bulletCooldownMs <= 0) {
|
||||
val baseCooldown = 450f
|
||||
var multiplier = 1f
|
||||
for (level in 2..rapidFireLevel) {
|
||||
val reduction = ((36 - level * 1.2f).coerceAtLeast(4f)) / 100f
|
||||
multiplier *= (1f - reduction)
|
||||
}
|
||||
bulletCooldownMs = max(50L, (baseCooldown * multiplier).toLong())
|
||||
|
||||
val baseY = viewHeight - 100f
|
||||
val spacing = 10f
|
||||
val count = multiFireLevel
|
||||
|
||||
for (i in 0 until count) {
|
||||
val offset = (i - (count - 1) / 2f) * spacing
|
||||
bullets.add(Bullet(playerX + offset, baseY))
|
||||
}
|
||||
}
|
||||
|
||||
enemies.forEach {
|
||||
it.update(deltaMs)
|
||||
it.x = max(20f, min(it.x, viewWidth - 20f))
|
||||
if (it.y > viewHeight + 100f) it.y = -40f // respawn at top
|
||||
}
|
||||
|
||||
pickups.forEach {
|
||||
it.y += 2.5f * deltaMs / 16f
|
||||
it.hue = (it.hue + deltaMs * 0.01f) % 360f
|
||||
}
|
||||
pickups.removeIf { it.y > viewHeight - 40f }
|
||||
|
||||
rockets.forEach {
|
||||
val targetAngle = atan2(viewHeight - 100f - it.y, playerX - it.x)
|
||||
it.angle += ((targetAngle - it.angle + PI).mod(2 * PI) - PI).toFloat() * 0.1f
|
||||
it.x += cos(it.angle) * 6f
|
||||
it.y += sin(it.angle) * 6f
|
||||
}
|
||||
rockets.removeIf { it.x < 0 || it.x > viewWidth || it.y < 0 || it.y > viewHeight }
|
||||
|
||||
enemyBullets.forEach { it.y += it.dy * deltaMs / 16f }
|
||||
enemyBullets.removeIf { it.y > viewHeight }
|
||||
|
||||
updatePlayerMissiles(deltaMs)
|
||||
|
||||
checkCollisions()
|
||||
spawnEnemies(deltaMs)
|
||||
}
|
||||
|
||||
private fun checkCollisions() {
|
||||
val pickupIter = pickups.iterator()
|
||||
while (pickupIter.hasNext()) {
|
||||
val p = pickupIter.next()
|
||||
if (hypot(playerX - p.x, viewHeight - 100f - p.y) < 40f) {
|
||||
when (p.type) {
|
||||
0 -> { // Multi-fire
|
||||
if (multiFireLevel < 4) {
|
||||
multiFireLevel++
|
||||
} else {
|
||||
applyRandomPowerupExcept(0)
|
||||
}
|
||||
}
|
||||
1 -> shieldLevel++
|
||||
2 -> if (missileLevel < 8) missileLevel++
|
||||
3 -> if (rapidFireLevel < 30) rapidFireLevel++
|
||||
4 -> if (piercingLevel < 15) piercingLevel++
|
||||
}
|
||||
pickupIter.remove()
|
||||
}
|
||||
}
|
||||
|
||||
val enemyIter = enemies.iterator()
|
||||
while (enemyIter.hasNext()) {
|
||||
val enemy = enemyIter.next()
|
||||
if (hypot(playerX - enemy.x, viewHeight - 100f - enemy.y) < 40f) {
|
||||
if (shieldLevel > 0 && shieldRechargeTimer <= 0) {
|
||||
shieldRechargeTimer = calculateShieldRechargeTime()
|
||||
enemyIter.remove()
|
||||
explosions.add(Explosion(enemy.x, enemy.y))
|
||||
continue
|
||||
} else {
|
||||
explosions.add(Explosion(playerX, viewHeight - 100f))
|
||||
gameOver = true
|
||||
return
|
||||
}
|
||||
}
|
||||
for (b in bullets) {
|
||||
if (hypot(b.x - enemy.x, b.y - enemy.y) < 30f) {
|
||||
explosions.add(Explosion(enemy.x, enemy.y))
|
||||
enemyIter.remove()
|
||||
score += 10
|
||||
b.life--
|
||||
|
||||
if (Random.nextFloat() < 0.03f) {
|
||||
val type = Random.nextInt(5) // 5 pickup types
|
||||
val hue = Random.nextFloat() * 360f
|
||||
pickups.add(Pickup(enemy.x, enemy.y, type, hue))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val bulletIter = enemyBullets.iterator()
|
||||
while (bulletIter.hasNext()) {
|
||||
val b = bulletIter.next()
|
||||
val dy = b.y - (viewHeight - 100f)
|
||||
val dx = b.x - playerX
|
||||
val dist = hypot(dx, dy)
|
||||
val hitRadius = if (shieldLevel > 0 && shieldRechargeTimer <= 0) 60f else 20f
|
||||
|
||||
if (dist < hitRadius) {
|
||||
if (shieldLevel > 0 && shieldRechargeTimer <= 0) {
|
||||
shieldRechargeTimer = calculateShieldRechargeTime()
|
||||
bulletIter.remove()
|
||||
explosions.add(Explosion(b.x, b.y))
|
||||
} else {
|
||||
explosions.add(Explosion(playerX, viewHeight - 100f))
|
||||
gameOver = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val rocketIter = rockets.iterator()
|
||||
while (rocketIter.hasNext()) {
|
||||
val rocket = rocketIter.next()
|
||||
val dy = viewHeight - 100f - rocket.y
|
||||
val dx = playerX - rocket.x
|
||||
val dist = hypot(dx, dy)
|
||||
val hitRadius = if (shieldLevel > 0 && shieldRechargeTimer <= 0) 60f else 30f
|
||||
|
||||
if (dist < hitRadius) {
|
||||
if (shieldLevel > 0 && shieldRechargeTimer <= 0) {
|
||||
shieldRechargeTimer = calculateShieldRechargeTime()
|
||||
rocketIter.remove()
|
||||
explosions.add(Explosion(rocket.x, rocket.y))
|
||||
continue
|
||||
} else {
|
||||
explosions.add(Explosion(playerX, viewHeight - 100f))
|
||||
gameOver = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for (b in bullets) {
|
||||
if (hypot(b.x - rocket.x, b.y - rocket.y) < 20f) {
|
||||
explosions.add(Explosion(rocket.x, rocket.y))
|
||||
rocketIter.remove()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val missileIter = playerMissiles.iterator()
|
||||
while (missileIter.hasNext()) {
|
||||
val missile = missileIter.next()
|
||||
val enemyHit = enemies.firstOrNull { enemy ->
|
||||
hypot(missile.x - enemy.x, missile.y - enemy.y) < 30f
|
||||
}
|
||||
|
||||
if (enemyHit != null) {
|
||||
explosions.add(Explosion(enemyHit.x, enemyHit.y))
|
||||
enemies.remove(enemyHit)
|
||||
missileIter.remove()
|
||||
score += 10
|
||||
|
||||
if (Random.nextFloat() < 0.5f) {
|
||||
val type = Random.nextInt(5)
|
||||
val hue = Random.nextFloat() * 360f
|
||||
pickups.add(Pickup(enemyHit.x, enemyHit.y, type, hue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlayerMissiles(deltaMs: Long) {
|
||||
// Missile cooldown and launch
|
||||
missileCooldown -= deltaMs
|
||||
if (missileLevel > 0 && missileCooldown <= 0) {
|
||||
val cooldown = when (missileLevel) {
|
||||
1 -> 20000L
|
||||
2 -> 15000L
|
||||
3 -> 12000L
|
||||
4 -> 20000L
|
||||
5 -> 18000L
|
||||
6 -> 17000L
|
||||
7 -> 16000L
|
||||
else -> 15000L
|
||||
}
|
||||
|
||||
val baseY = viewHeight - 100f
|
||||
if (missileLevel >= 4) {
|
||||
// fire both sides
|
||||
playerMissiles.add(PlayerMissile(playerX - 20f, baseY, -PI.toFloat() / 2f, side = -1))
|
||||
playerMissiles.add(PlayerMissile(playerX + 20f, baseY, -PI.toFloat() / 2f, side = 1))
|
||||
} else {
|
||||
lastMissileSide *= -1
|
||||
val offsetX = 20f * lastMissileSide
|
||||
playerMissiles.add(PlayerMissile(playerX + offsetX, baseY, -PI.toFloat() / 2f, side = lastMissileSide))
|
||||
}
|
||||
|
||||
missileCooldown = cooldown
|
||||
}
|
||||
|
||||
// Update missiles
|
||||
val lockedEnemies = playerMissiles.mapNotNull { it.target }.toSet()
|
||||
val availableTargets = enemies.filter { it !in lockedEnemies }
|
||||
|
||||
playerMissiles.forEach { missile ->
|
||||
missile.ttl -= deltaMs
|
||||
if (missile.ttl <= 0) return@forEach
|
||||
|
||||
// Add current position to trail
|
||||
missile.trail.add(0, missile.x to missile.y)
|
||||
if (missile.trail.size > 20) {
|
||||
missile.trail.removeLast()
|
||||
}
|
||||
|
||||
if (missile.target == null || !enemies.contains(missile.target)) {
|
||||
missile.recheckCooldown -= deltaMs
|
||||
if (missile.recheckCooldown <= 0) {
|
||||
val newTarget = availableTargets.minByOrNull {
|
||||
hypot((it.x - missile.x).toDouble(), (it.y - missile.y).toDouble())
|
||||
}
|
||||
if (newTarget != null) {
|
||||
missile.target = newTarget
|
||||
} else {
|
||||
missile.recheckCooldown = 1000L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val target = missile.target
|
||||
val angleTo = if (target != null) {
|
||||
atan2(target.y - missile.y, target.x - missile.x)
|
||||
} else missile.angle
|
||||
|
||||
// steer towards target slowly
|
||||
missile.angle += ((angleTo - missile.angle + PI).mod(2 * PI) - PI).toFloat() * 0.1f
|
||||
missile.x += cos(missile.angle) * 6f
|
||||
missile.y += sin(missile.angle) * 6f
|
||||
}
|
||||
|
||||
playerMissiles.removeIf { it.ttl <= 0 || it.x < 0 || it.x > viewWidth || it.y < 0 || it.y > viewHeight }
|
||||
}
|
||||
|
||||
private fun applyRandomPowerupExcept(excludedType: Int) {
|
||||
val types = (0..4).filter { it != excludedType }
|
||||
val type = types.random()
|
||||
when (type) {
|
||||
1 -> shieldLevel++
|
||||
2 -> if (missileLevel < 8) missileLevel++
|
||||
3 -> if (rapidFireLevel < 30) rapidFireLevel++
|
||||
4 -> if (piercingLevel < 15) piercingLevel++
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCubeIconPath(): Path {
|
||||
return Path().apply {
|
||||
addRect(-10f, -10f, 10f, 10f, Path.Direction.CW)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateShieldRechargeTime(): Long {
|
||||
val base = 60000L // 60 seconds
|
||||
val min = 10000L // 10 seconds
|
||||
val reduction = (1.0 - exp(-shieldLevel / 40.0)).toFloat()
|
||||
return (base - (base - min) * reduction).toLong()
|
||||
}
|
||||
|
||||
private fun spawnEnemies(deltaMs: Long) {
|
||||
waveTimer -= deltaMs
|
||||
adaptiveSpawnTimer += deltaMs
|
||||
|
||||
// Passive asteroids
|
||||
if (Random.nextFloat() < 0.002f) {
|
||||
enemies.add(EnemyAsteroid(Random.nextFloat() * viewWidth, -40f, viewWidth))
|
||||
}
|
||||
|
||||
// Setup new wave
|
||||
if (enemiesLeftInWave <= 0 && waveTimer <= 0) {
|
||||
currentWave++
|
||||
currentWaveType = when (currentWave % 3) {
|
||||
0 -> "easy"
|
||||
1 -> "medium"
|
||||
else -> "hard"
|
||||
}
|
||||
|
||||
val difficultyBoost = when (currentWaveType) {
|
||||
"easy" -> (currentWave * 1.2f).roundToInt()
|
||||
"medium" -> (currentWave * 1.5f).roundToInt()
|
||||
"hard" -> (currentWave * 1.8f).roundToInt()
|
||||
else -> currentWave
|
||||
}
|
||||
|
||||
enemiesLeftInWave = 3 + difficultyBoost
|
||||
waveTimer = 3000L
|
||||
adaptiveSpawnTimer = 0L
|
||||
lastEnemyCount = enemies.size
|
||||
avgEnemiesPerSecond = 0f
|
||||
enemyClearAggression = 0f
|
||||
}
|
||||
|
||||
// Adjust aggression every 1s
|
||||
if (adaptiveSpawnTimer >= 1000L) {
|
||||
val cleared = (lastEnemyCount - enemies.size).coerceAtLeast(0)
|
||||
avgEnemiesPerSecond = avgEnemiesPerSecond * 0.7f + cleared * 0.3f
|
||||
lastEnemyCount = enemies.size
|
||||
adaptiveSpawnTimer = 0L
|
||||
|
||||
// Normalize to 0–1 range (assuming 0 to 6 cleared per second)
|
||||
enemyClearAggression = (avgEnemiesPerSecond / 6f).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
// Spawn enemies in group
|
||||
if (enemiesLeftInWave > 0 && enemies.count { it !is EnemyAsteroid } < 10) {
|
||||
val baseX = Random.nextFloat() * (viewWidth - 100f) + 50f
|
||||
val baseY = -40f
|
||||
val spacing = 35f
|
||||
val sharedOffset = Random.nextFloat() * 1000f
|
||||
val sharedFireTime = Random.nextLong(2000L, 4000L)
|
||||
|
||||
// Dynamically adjust formation size
|
||||
val formationBase = when (currentWaveType) {
|
||||
"easy" -> 5
|
||||
"medium" -> 3
|
||||
"hard" -> 1
|
||||
else -> 3
|
||||
}
|
||||
|
||||
// Scale formation by aggression (max +2)
|
||||
val dynamicBonus = (enemyClearAggression * 2).roundToInt()
|
||||
val formationSize = (formationBase + dynamicBonus).coerceAtMost(enemiesLeftInWave)
|
||||
|
||||
for (i in 0 until formationSize) {
|
||||
val offsetX = (i - (formationSize - 1) / 2f) * spacing
|
||||
val x = baseX + offsetX
|
||||
|
||||
val enemy = when (currentWaveType) {
|
||||
"easy" -> EnemyEasy(x, baseY)
|
||||
"medium" -> EnemyMedium(x, baseY, sharedOffset, sharedFireTime) { enemyBullets.add(it) }
|
||||
"hard" -> {
|
||||
val hardEnemiesSoFar = enemies.count { it is EnemyHard }
|
||||
if (hardEnemiesSoFar >= 2) {
|
||||
EnemyMedium(x, baseY, sharedOffset, sharedFireTime) { enemyBullets.add(it) }
|
||||
} else {
|
||||
EnemyHard(x, baseY, { rockets.add(it) }, sharedOffset, sharedFireTime)
|
||||
}
|
||||
}
|
||||
else -> EnemyEasy(x, baseY)
|
||||
}
|
||||
|
||||
enemies.add(enemy)
|
||||
enemiesLeftInWave--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetGame() {
|
||||
bullets.clear()
|
||||
enemyBullets.clear()
|
||||
enemies.clear()
|
||||
rockets.clear()
|
||||
explosions.clear()
|
||||
rocketTrails.clear()
|
||||
playerMissiles.clear()
|
||||
pickups.clear()
|
||||
|
||||
// Reset upgrades
|
||||
multiFireLevel = 1
|
||||
piercingLevel = 1
|
||||
shieldLevel = 0
|
||||
missileLevel = 0
|
||||
rapidFireLevel = 1
|
||||
|
||||
// Reset gameplay state
|
||||
score = 0
|
||||
currentWave = 0
|
||||
enemiesLeftInWave = 0
|
||||
gameOver = false
|
||||
shieldRechargeTimer = 0L
|
||||
shieldFlashAlpha = 0f
|
||||
lastMissileSide = -1
|
||||
missileCooldown = 0L
|
||||
bulletCooldownMs = 0L
|
||||
enemyClearAggression = 0f
|
||||
adaptiveSpawnTimer = 0L
|
||||
lastEnemyCount = 0
|
||||
avgEnemiesPerSecond = 0f
|
||||
|
||||
playerX = viewWidth / 2f
|
||||
lastLogicTime = System.nanoTime()
|
||||
Choreographer.getInstance().postFrameCallback(this)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
viewWidth = width.toFloat()
|
||||
viewHeight = height.toFloat()
|
||||
|
||||
canvas.drawColor(Color.parseColor("#121212"))
|
||||
stars.forEach { canvas.drawCircle(it.x, it.y, it.radius, starPaint) }
|
||||
|
||||
val enemyBulletPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.RED
|
||||
strokeWidth = 6f // same as player bullets
|
||||
}
|
||||
enemyBullets.forEach { canvas.drawLine(it.x, it.y, it.x, it.y + 20f, enemyBulletPaint) }
|
||||
|
||||
// Enemy rockets
|
||||
rockets.forEach { rocket ->
|
||||
rocket.trail.forEachIndexed { index, (x, y) ->
|
||||
val alpha = ((1f - index / 20f.toFloat()) * 255).toInt()
|
||||
rocketPaint.alpha = alpha
|
||||
canvas.drawCircle(x, y, 2f, rocketPaint)
|
||||
}
|
||||
}
|
||||
rocketPaint.alpha = 255
|
||||
|
||||
// Player rockets
|
||||
val missilePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = colorSecondary
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 3f
|
||||
}
|
||||
|
||||
playerMissiles.forEach { missile ->
|
||||
missile.trail.forEachIndexed { index, (x, y) ->
|
||||
val alpha = ((1f - index / 20f.toFloat()) * 255).toInt()
|
||||
missilePaint.alpha = alpha
|
||||
canvas.drawCircle(x, y, 2f, missilePaint)
|
||||
}
|
||||
}
|
||||
|
||||
missilePaint.alpha = 255
|
||||
playerMissiles.forEach { missile ->
|
||||
canvas.drawCircle(missile.x, missile.y, 10f, missilePaint)
|
||||
}
|
||||
|
||||
explosions.forEach {
|
||||
val radius = 40f * (1f - it.timer / 12f.toFloat())
|
||||
val alpha = (255 * (it.timer / 12f.toFloat())).toInt()
|
||||
val paint = Paint().apply {
|
||||
color = Color.YELLOW
|
||||
this.alpha = alpha
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 3f
|
||||
}
|
||||
canvas.drawCircle(it.x, it.y, radius, paint)
|
||||
}
|
||||
|
||||
bullets.forEach { canvas.drawLine(it.x, it.y, it.x, it.y - 20f, bulletPaint) }
|
||||
enemies.forEach { it.draw(canvas, enemyPaint) }
|
||||
rockets.forEach { canvas.drawCircle(it.x, it.y, 10f, rocketPaint) }
|
||||
|
||||
// Draw pickups
|
||||
pickups.forEach { pickup ->
|
||||
// Draw cube (filled rect with outline)
|
||||
val size = 30f
|
||||
val left = pickup.x - size / 2f
|
||||
val top = pickup.y - size / 2f
|
||||
val right = pickup.x + size / 2f
|
||||
val bottom = pickup.y + size / 2f
|
||||
|
||||
val hsv = floatArrayOf(pickup.hue, 1f, 1f)
|
||||
val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.HSVToColor(hsv)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
val outlinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 2f
|
||||
}
|
||||
|
||||
canvas.drawRect(left, top, right, bottom, fillPaint)
|
||||
canvas.drawRect(left, top, right, bottom, outlinePaint)
|
||||
|
||||
// Draw icon
|
||||
val iconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
strokeWidth = 3f
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
canvas.save()
|
||||
canvas.translate(pickup.x, pickup.y)
|
||||
canvas.rotate(-45f)
|
||||
|
||||
when (pickup.type) {
|
||||
0 -> { // Multi-fire: two parallel lines
|
||||
canvas.drawLine(-8f, -10f, -8f, 10f, iconPaint)
|
||||
canvas.drawLine(8f, -10f, 8f, 10f, iconPaint)
|
||||
}
|
||||
1 -> { // Shield: quarter circle
|
||||
val path = Path()
|
||||
path.addArc(RectF(-10f, -10f, 10f, 10f), -90f, 90f)
|
||||
canvas.drawPath(path, iconPaint)
|
||||
}
|
||||
2 -> { // Missiles: circle + trail
|
||||
canvas.drawCircle(0f, 0f, 4f, iconPaint)
|
||||
canvas.drawLine(-6f, 6f, -2f, 2f, iconPaint)
|
||||
}
|
||||
3 -> { // Rapid fire: two lines in succession
|
||||
canvas.drawLine(-5f, -10f, -5f, 10f, iconPaint)
|
||||
canvas.drawLine(5f, -10f, 5f, 10f, iconPaint)
|
||||
}
|
||||
4 -> { // Piercing bullets: arrow head
|
||||
val path = Path()
|
||||
path.moveTo(-8f, 10f)
|
||||
path.lineTo(0f, -10f)
|
||||
path.lineTo(8f, 10f)
|
||||
canvas.drawPath(path, iconPaint)
|
||||
}
|
||||
}
|
||||
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
if (!gameOver) {
|
||||
// Draw player
|
||||
val baseY = viewHeight - 100f
|
||||
val safeX = max(30f, min(playerX, viewWidth - 30f))
|
||||
playerX = safeX
|
||||
val path = Path().apply {
|
||||
moveTo(safeX, baseY - 30f)
|
||||
lineTo(safeX - 30f, baseY + 30f)
|
||||
lineTo(safeX + 30f, baseY + 30f)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, playerPaint)
|
||||
|
||||
// Draw shield
|
||||
if (shieldLevel > 0 && shieldRechargeTimer <= 0) {
|
||||
val baseColor = colorSecondary
|
||||
val shieldPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = baseColor
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 6f
|
||||
alpha = (180 + shieldFlashAlpha * 75f).toInt().coerceAtMost(255)
|
||||
}
|
||||
canvas.drawCircle(playerX, viewHeight - 100f, 60f, shieldPaint)
|
||||
}
|
||||
} else {
|
||||
canvas.drawText("Game Over", viewWidth / 2f, viewHeight / 2f - 60f, textPaint)
|
||||
canvas.drawText("Score: $score", viewWidth / 2f, viewHeight / 2f + 10f, textPaint)
|
||||
canvas.drawText("Tap to restart", retryRect.centerX(), retryRect.centerY() + 16f, retryTextPaint)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (gameOver && event.action == MotionEvent.ACTION_DOWN) {
|
||||
resetGame()
|
||||
return true
|
||||
}
|
||||
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
||||
playerX = max(30f, min(event.x, width - 30f))
|
||||
isTouching = true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> isTouching = false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private interface Enemy {
|
||||
var x: Float
|
||||
var y: Float
|
||||
fun update(deltaMs: Long)
|
||||
fun draw(canvas: Canvas, paint: Paint)
|
||||
}
|
||||
|
||||
private class EnemyAsteroid(
|
||||
override var x: Float,
|
||||
override var y: Float,
|
||||
private val screenWidth: Float
|
||||
) : Enemy {
|
||||
private var vx = Random.nextFloat() * 2f - 1f
|
||||
private val vy = Random.nextFloat() * 2f + 2f
|
||||
private var angle = Random.nextFloat() * 360f
|
||||
private val rotationSpeed = Random.nextFloat() * 2f * if (Random.nextBoolean()) 1 else -1
|
||||
|
||||
override fun update(deltaMs: Long) {
|
||||
x += vx * deltaMs / 16f
|
||||
y += vy * deltaMs / 16f
|
||||
angle = (angle + rotationSpeed * deltaMs / 16f) % 360f
|
||||
|
||||
if (x < 20f || x > screenWidth - 20f) {
|
||||
vx = -vx
|
||||
x = max(20f, min(x, screenWidth - 20f))
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
canvas.save()
|
||||
canvas.rotate(angle, x, y)
|
||||
val path = Path().apply {
|
||||
moveTo(x - 20f, y)
|
||||
lineTo(x - 10f, y - 15f)
|
||||
lineTo(x + 10f, y - 15f)
|
||||
lineTo(x + 20f, y)
|
||||
lineTo(x + 10f, y + 15f)
|
||||
lineTo(x - 10f, y + 15f)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
|
||||
private class EnemyEasy(override var x: Float, override var y: Float) : Enemy {
|
||||
private val offset = Random.nextFloat() * 1000f
|
||||
override fun update(deltaMs: Long) {
|
||||
y += 2f * deltaMs / 16f
|
||||
x += sin((y + offset) / 50f) * 2f
|
||||
}
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
val path = Path().apply {
|
||||
moveTo(x, y - 20f)
|
||||
lineTo(x + 20f, y)
|
||||
lineTo(x, y + 20f)
|
||||
lineTo(x - 20f, y)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
private class EnemyMedium(
|
||||
override var x: Float,
|
||||
override var y: Float,
|
||||
private val offset: Float,
|
||||
private var fireTimer: Long,
|
||||
val fireEnemyBullet: (Bullet) -> Unit
|
||||
) : Enemy {
|
||||
override fun update(deltaMs: Long) {
|
||||
y += 2.5f * deltaMs / 16f
|
||||
x += sin((y + offset) / 40f) * 3f
|
||||
|
||||
fireTimer -= deltaMs
|
||||
if (fireTimer <= 0) {
|
||||
fireEnemyBullet(Bullet(x, y + 30f, dy = 10f))
|
||||
fireTimer = Random.nextLong(3000L, 6000L)
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
val path = Path().apply {
|
||||
moveTo(x, y - 25f)
|
||||
lineTo(x + 15f, y)
|
||||
lineTo(x, y + 25f)
|
||||
lineTo(x - 15f, y)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
private class EnemyHard(
|
||||
override var x: Float,
|
||||
override var y: Float,
|
||||
val fireRocket: (Rocket) -> Unit,
|
||||
private val offset: Float = Random.nextFloat() * 1000f,
|
||||
private var fireTimer: Long = Random.nextLong(2000L, 5000L)
|
||||
) : Enemy {
|
||||
private var cooldown = 0L
|
||||
private var firing = false
|
||||
|
||||
override fun update(deltaMs: Long) {
|
||||
if (firing) {
|
||||
cooldown += deltaMs
|
||||
if (cooldown > 500 && cooldown < 1300) {
|
||||
fireRocket(Rocket(x, y - 30f, -PI.toFloat() / 2))
|
||||
cooldown = 1300
|
||||
} else if (cooldown > 2000) {
|
||||
cooldown = 0L
|
||||
fireTimer = Random.nextLong(15000L, 25000L)
|
||||
firing = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
y += 3f * deltaMs / 16f
|
||||
x += sin((y + offset) / 25f) * 4f
|
||||
fireTimer -= deltaMs
|
||||
if (fireTimer <= 0) {
|
||||
firing = true
|
||||
cooldown = 0L
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
val path = Path().apply {
|
||||
moveTo(x, y - 25f)
|
||||
lineTo(x + 10f, y - 10f)
|
||||
lineTo(x + 20f, y + 10f)
|
||||
lineTo(x, y + 25f)
|
||||
lineTo(x - 20f, y + 10f)
|
||||
lineTo(x - 10f, y - 10f)
|
||||
close()
|
||||
}
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,8 +66,8 @@ class ServiceAdapter(
|
|||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val from = viewHolder.bindingAdapterPosition
|
||||
val to = target.bindingAdapterPosition
|
||||
val from = viewHolder.adapterPosition
|
||||
val to = target.adapterPosition
|
||||
services.add(to, services.removeAt(from))
|
||||
notifyItemMoved(from, to)
|
||||
return true
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
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 androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import partisan.weforge.xyz.pulse.databinding.FragmentServiceSettingsBinding
|
||||
|
||||
class ServiceSettingsFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentServiceSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentServiceSettingsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
(requireActivity() as? MainActivity)?.setAppBarTitle(
|
||||
getString(R.string.settings_name),
|
||||
getString(R.string.services_name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val available = listOf(
|
||||
ServiceEntry(
|
||||
"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call",
|
||||
R.string.destination_signal,
|
||||
requireContext().isServiceEnabled("vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call")
|
||||
),
|
||||
ServiceEntry(
|
||||
"vnd.android.cursor.item/vnd.org.telegram.messenger.android.call",
|
||||
R.string.destination_telegram,
|
||||
requireContext().isServiceEnabled("vnd.android.cursor.item/vnd.org.telegram.messenger.android.call")
|
||||
),
|
||||
ServiceEntry(
|
||||
"vnd.android.cursor.item/vnd.ch.threema.app.call",
|
||||
R.string.destination_threema,
|
||||
requireContext().isServiceEnabled("vnd.android.cursor.item/vnd.ch.threema.app.call")
|
||||
),
|
||||
ServiceEntry(
|
||||
"vnd.android.cursor.item/vnd.com.whatsapp.voip.call",
|
||||
R.string.destination_whatsapp,
|
||||
requireContext().isServiceEnabled("vnd.android.cursor.item/vnd.com.whatsapp.voip.call")
|
||||
),
|
||||
)
|
||||
|
||||
val storedOrder = Preferences(requireContext()).getServiceOrder()
|
||||
|
||||
val ordered = storedOrder.mapNotNull { mime ->
|
||||
available.find { it.mimetype == mime }
|
||||
}.toMutableList()
|
||||
|
||||
// Add any missing services that weren't stored (e.g., first run)
|
||||
val missing = available.filterNot { s -> ordered.any { it.mimetype == s.mimetype } }
|
||||
ordered += missing
|
||||
|
||||
val prefs = Preferences(requireContext())
|
||||
|
||||
val adapter = ServiceAdapter(
|
||||
context = requireContext(),
|
||||
services = ordered,
|
||||
onReordered = { updatedList ->
|
||||
val newOrder = updatedList.map { it.mimetype }
|
||||
prefs.setServiceOrder(newOrder)
|
||||
}
|
||||
)
|
||||
|
||||
binding.serviceRecycler.adapter = adapter
|
||||
binding.serviceRecycler.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
val touchHelper = ItemTouchHelper(adapter.dragHelper)
|
||||
touchHelper.attachToRecyclerView(binding.serviceRecycler)
|
||||
|
||||
adapter.setDragStartListener { viewHolder ->
|
||||
touchHelper.startDrag(viewHolder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import android.app.role.RoleManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import nl.dionsegijn.konfetti.core.Party
|
||||
import nl.dionsegijn.konfetti.core.Position
|
||||
import nl.dionsegijn.konfetti.core.emitter.Emitter
|
||||
import nl.dionsegijn.konfetti.xml.KonfettiView
|
||||
import partisan.weforge.xyz.pulse.databinding.ActivityWelcomeBinding
|
||||
import java.util.concurrent.TimeUnit
|
||||
import partisan.weforge.xyz.pulse.hasGeneralPermissions
|
||||
import partisan.weforge.xyz.pulse.hasDrawOverlays
|
||||
import partisan.weforge.xyz.pulse.hasCallRedirectionRole
|
||||
import partisan.weforge.xyz.pulse.REQUIRED_PERMISSIONS
|
||||
|
||||
class WelcomeActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityWelcomeBinding
|
||||
private lateinit var prefs: Preferences
|
||||
private var roleManager: RoleManager? = null
|
||||
|
||||
private val requestPermissionsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) {}
|
||||
|
||||
private val requestOverlayLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {}
|
||||
|
||||
private val requestRoleLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (hasGeneralPermissions(this) && hasDrawOverlays(this) && hasCallRedirectionRole(this)) {
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
binding = ActivityWelcomeBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
prefs = Preferences(this)
|
||||
roleManager = getSystemService(RoleManager::class.java)
|
||||
|
||||
binding.activateButton.setOnClickListener {
|
||||
when {
|
||||
!hasGeneralPermissions(this) -> {
|
||||
requestPermissionsLauncher.launch(REQUIRED_PERMISSIONS)
|
||||
}
|
||||
!hasDrawOverlays(this) -> {
|
||||
requestOverlayLauncher.launch(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION))
|
||||
}
|
||||
!hasCallRedirectionRole(this) -> {
|
||||
requestRoleLauncher.launch(roleManager?.createRequestRoleIntent(RoleManager.ROLE_CALL_REDIRECTION))
|
||||
}
|
||||
else -> {
|
||||
showConfettiAndContinue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showConfettiAndContinue() {
|
||||
binding.appIcon.post {
|
||||
val iconLocation = IntArray(2)
|
||||
binding.appIcon.getLocationOnScreen(iconLocation)
|
||||
|
||||
val iconCenterX = iconLocation[0] + binding.appIcon.width / 2f
|
||||
val iconCenterY = iconLocation[1] + binding.appIcon.height / 2f
|
||||
|
||||
val rootWidth = binding.root.width.toFloat()
|
||||
val rootHeight = binding.root.height.toFloat()
|
||||
|
||||
val relativeX = (iconCenterX / rootWidth).toDouble()
|
||||
val relativeY = (iconCenterY / rootHeight).toDouble()
|
||||
|
||||
binding.konfettiView.visibility = View.VISIBLE
|
||||
binding.konfettiView.start(
|
||||
Party(
|
||||
speed = 25f,
|
||||
maxSpeed = 50f,
|
||||
damping = 0.9f,
|
||||
spread = 360,
|
||||
colors = listOf(0xfce18a, 0xff726d, 0xf4306d, 0xb48def),
|
||||
position = Position.Relative(relativeX, relativeY),
|
||||
emitter = Emitter(duration = 1, TimeUnit.SECONDS).perSecond(80)
|
||||
)
|
||||
)
|
||||
|
||||
binding.konfettiView.postDelayed({
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:color="?attr/colorPrimary"/>
|
||||
<item android:color="?attr/colorSurfaceContainer"/>
|
||||
</selector>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:color="?attr/colorOnPrimaryContainer"/>
|
||||
<item android:color="?attr/colorOnSurfaceVariant"/>
|
||||
</selector>
|
|
@ -1,11 +0,0 @@
|
|||
<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>
|
|
@ -1,10 +0,0 @@
|
|||
<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">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M40,800L40,688Q40,654 57.5,625.5Q75,597 104,582Q166,551 230,535.5Q294,520 360,520Q426,520 490,535.5Q554,551 616,582Q645,597 662.5,625.5Q680,654 680,688L680,800L40,800ZM760,800L760,680Q760,636 735.5,595.5Q711,555 666,526Q717,532 762,546.5Q807,561 846,582Q882,602 901,626.5Q920,651 920,680L920,800L760,800ZM360,480Q294,480 247,433Q200,386 200,320Q200,254 247,207Q294,160 360,160Q426,160 473,207Q520,254 520,320Q520,386 473,433Q426,480 360,480ZM760,320Q760,386 713,433Q666,480 600,480Q589,480 572,477.5Q555,475 544,472Q571,440 585.5,401Q600,362 600,320Q600,278 585.5,239Q571,200 544,168Q558,163 572,161.5Q586,160 600,160Q666,160 713,207Q760,254 760,320ZM120,720L600,720L600,688Q600,677 594.5,668Q589,659 580,654Q526,627 471,613.5Q416,600 360,600Q304,600 249,613.5Q194,627 140,654Q131,659 125.5,668Q120,677 120,688L120,720ZM360,400Q393,400 416.5,376.5Q440,353 440,320Q440,287 416.5,263.5Q393,240 360,240Q327,240 303.5,263.5Q280,287 280,320Q280,353 303.5,376.5Q327,400 360,400ZM360,720L360,720L360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720ZM360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Z"/>
|
||||
</vector>
|
|
@ -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="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z"/>
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<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">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,840L422,788Q321,697 255,631Q189,565 150,512.5Q111,460 95.5,416Q80,372 80,326Q80,232 143,169Q206,106 300,106Q352,106 399,128Q446,150 480,190Q514,150 561,128Q608,106 660,106Q754,106 817,169Q880,232 880,326Q880,372 864.5,416Q849,460 810,512.5Q771,565 705,631Q639,697 538,788L480,840Z"/>
|
||||
</vector>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M13,3h-2v10h2L13,3zM17.83,5.17l-1.42,1.42C17.99,7.86 19,9.81 19,12c0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-2.19 1.01,-4.14 2.58,-5.42L6.17,5.17C4.23,6.82 3,9.26 3,12c0,4.97 4.03,9 9,9s9,-4.03 9,-9c0,-2.74 -1.23,-5.18 -3.17,-6.83z"/>
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<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">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M440,680L520,680L520,440L440,440L440,680ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
</vector>
|
|
@ -1,17 +0,0 @@
|
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#22000000" />
|
||||
<corners android:radius="999dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?attr/colorPrimary" />
|
||||
<corners android:radius="999dp" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -1,5 +0,0 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?attr/colorSurfaceVariant" />
|
||||
<corners android:radius="50dp" />
|
||||
</shape>
|
|
@ -1,13 +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="M14.17,13.71l1.4,-2.42c0.09,-0.15 0.05,-0.34 -0.08,-0.45l-1.48,-1.16c0.03,-0.22 0.05,-0.45 0.05,-0.68s-0.02,-0.46 -0.05,-0.69l1.48,-1.16c0.13,-0.11 0.17,-0.3 0.08,-0.45l-1.4,-2.42c-0.09,-0.15 -0.27,-0.21 -0.43,-0.15L12,4.83c-0.36,-0.28 -0.75,-0.51 -1.18,-0.69l-0.26,-1.85C10.53,2.13 10.38,2 10.21,2h-2.8C7.24,2 7.09,2.13 7.06,2.3L6.8,4.15C6.38,4.33 5.98,4.56 5.62,4.84l-1.74,-0.7c-0.16,-0.06 -0.34,0 -0.43,0.15l-1.4,2.42C1.96,6.86 2,7.05 2.13,7.16l1.48,1.16C3.58,8.54 3.56,8.77 3.56,9s0.02,0.46 0.05,0.69l-1.48,1.16C2,10.96 1.96,11.15 2.05,11.3l1.4,2.42c0.09,0.15 0.27,0.21 0.43,0.15l1.74,-0.7c0.36,0.28 0.75,0.51 1.18,0.69l0.26,1.85C7.09,15.87 7.24,16 7.41,16h2.8c0.17,0 0.32,-0.13 0.35,-0.3l0.26,-1.85c0.42,-0.18 0.82,-0.41 1.18,-0.69l1.74,0.7C13.9,13.92 14.08,13.86 14.17,13.71zM8.81,11c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C10.81,10.1 9.91,11 8.81,11z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21.92,18.67l-0.96,-0.74c0.02,-0.14 0.04,-0.29 0.04,-0.44c0,-0.15 -0.01,-0.3 -0.04,-0.44l0.95,-0.74c0.08,-0.07 0.11,-0.19 0.05,-0.29l-0.9,-1.55c-0.05,-0.1 -0.17,-0.13 -0.28,-0.1l-1.11,0.45c-0.23,-0.18 -0.48,-0.33 -0.76,-0.44l-0.17,-1.18C18.73,13.08 18.63,13 18.53,13h-1.79c-0.11,0 -0.21,0.08 -0.22,0.19l-0.17,1.18c-0.27,0.12 -0.53,0.26 -0.76,0.44l-1.11,-0.45c-0.1,-0.04 -0.22,0 -0.28,0.1l-0.9,1.55c-0.05,0.1 -0.04,0.22 0.05,0.29l0.95,0.74c-0.02,0.14 -0.03,0.29 -0.03,0.44c0,0.15 0.01,0.3 0.03,0.44l-0.95,0.74c-0.08,0.07 -0.11,0.19 -0.05,0.29l0.9,1.55c0.05,0.1 0.17,0.13 0.28,0.1l1.11,-0.45c0.23,0.18 0.48,0.33 0.76,0.44l0.17,1.18c0.02,0.11 0.11,0.19 0.22,0.19h1.79c0.11,0 0.21,-0.08 0.22,-0.19l0.17,-1.18c0.27,-0.12 0.53,-0.26 0.75,-0.44l1.12,0.45c0.1,0.04 0.22,0 0.28,-0.1l0.9,-1.55C22.03,18.86 22,18.74 21.92,18.67zM17.63,18.83c-0.74,0 -1.35,-0.6 -1.35,-1.35s0.6,-1.35 1.35,-1.35s1.35,0.6 1.35,1.35S18.37,18.83 17.63,18.83z"/>
|
||||
</vector>
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true">
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="?attr/colorPrimary" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="?attr/colorSurfaceContainerLowest" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="?attr/colorSurfaceContainerLowest" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="?attr/colorOutline"/>
|
||||
</shape>
|
|
@ -1,10 +0,0 @@
|
|||
<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">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,880L373,720L160,720Q127,720 103.5,696.5Q80,673 80,640L80,160Q80,127 103.5,103.5Q127,80 160,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L587,720L480,880ZM480,736L544,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L160,160Q160,160 160,160Q160,160 160,160L160,640Q160,640 160,640Q160,640 160,640L416,640L480,736ZM480,400L480,400L480,400Q480,400 480,400Q480,400 480,400L480,400Q480,400 480,400Q480,400 480,400L480,400Q480,400 480,400Q480,400 480,400L480,400Q480,400 480,400Q480,400 480,400L480,400Z"/>
|
||||
</vector>
|
|
@ -1,45 +1,100 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawerLayout"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:padding="32dp"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<!-- Main content -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/topAppBar"
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorSurface"
|
||||
app:titleTextColor="?attr/colorOnSurface"
|
||||
app:navigationIconTint="?attr/colorOnSurface"
|
||||
app:title="@string/app_name"
|
||||
app:titleTextAppearance="@style/Toolbar.Title.Small"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/description"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragmentContainer"
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/topAppBar"
|
||||
android:padding="16dp"
|
||||
android:layout_marginVertical="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/description"
|
||||
app:layout_constraintBottom_toTopOf="@id/toggle"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/redirectionDelay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stepSize="0.5"
|
||||
android:valueFrom="2"
|
||||
android:valueTo="4"
|
||||
android:contentDescription="@string/redirection_delay_description" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/redirection_delay_description"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/popupPosition"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:endIconMode="custom"
|
||||
app:endIconDrawable="@drawable/ic_baseline_check_circle_24"
|
||||
android:hint="@string/popup_position">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
android:background="?android:attr/listDivider" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/serviceRecycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:scrollbars="vertical" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/toggle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="12dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- Sliding menu -->
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/navigationView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
app:menu="@menu/main_menu" />
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/rootLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="32dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contentWrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/appIcon"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/description"
|
||||
android:textAlignment="center"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/activateButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/activate"
|
||||
android:layout_marginTop="32dp"
|
||||
app:cornerRadius="24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activateDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/activate_description"
|
||||
android:textAlignment="center"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginTop="16dp" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<nl.dionsegijn.konfetti.xml.KonfettiView
|
||||
android:id="@+id/konfettiView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
|
@ -1,77 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/rootLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="32dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contentWrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/appIcon"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/description"
|
||||
android:textAlignment="center"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/sourceButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/source_code"
|
||||
android:layout_marginTop="24dp"
|
||||
app:cornerRadius="24dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/licenseButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/license"
|
||||
android:layout_marginTop="8dp"
|
||||
app:cornerRadius="24dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/secretButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Secret"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
app:cornerRadius="24dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
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="16dp">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/contactSearch"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/search_background"
|
||||
android:iconifiedByDefault="false"
|
||||
android:queryHint="@string/contact_search"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/contactRecycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/contactSearch"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="8dp" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,114 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/donate_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<!-- Intro section -->
|
||||
<TextView
|
||||
android:id="@+id/titleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate_title"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/descText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate_description"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tokenDisplay"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reminderText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate_toast_reminder"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/kofiButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate_button"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/postDonatePrompt"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate_post_prompt"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/openTokenSection"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate_have_token"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Token activation section (initially hidden) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/tokenSection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/instruction"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate_token_instruction"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/tokenInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/donate_token_hint"
|
||||
android:inputType="text"
|
||||
android:maxLength="32"
|
||||
android:layout_marginBottom="12dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/verifyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate_token_activate" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/resultText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:layout_marginTop="16dp"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
|
@ -1,50 +0,0 @@
|
|||
<FrameLayout 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:gravity="center">
|
||||
|
||||
<nl.dionsegijn.konfetti.xml.KonfettiView
|
||||
android:id="@+id/confettiView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<View
|
||||
android:layout_width="104dp"
|
||||
android:layout_height="104dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/toggle_button_bg_outline" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/toggle"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="center"
|
||||
android:checkable="true"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:insetLeft="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:text=""
|
||||
app:icon="@drawable/ic_power_settings_new_24"
|
||||
app:iconTint="@color/toggle_button_icon"
|
||||
app:backgroundTint="@color/toggle_button_bg"
|
||||
app:iconSize="48dp"
|
||||
app:iconGravity="textTop"
|
||||
app:iconPadding="0dp"
|
||||
app:cornerRadius="48dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/warningText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:padding="16dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:background="#AAFF5252"
|
||||
android:textSize="14sp"
|
||||
android:text=""
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
|
@ -1,99 +0,0 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
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">
|
||||
|
||||
<!-- Delay label -->
|
||||
<TextView
|
||||
android:id="@+id/delayDescription"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/redirection_delay_description"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<!-- Delay slider -->
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/redirectionDelay"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:stepSize="0.5"
|
||||
android:valueFrom="2"
|
||||
android:valueTo="4"
|
||||
android:contentDescription="@string/redirection_delay_description"
|
||||
app:layout_constraintTop_toBottomOf="@id/delayDescription"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<!-- Position label -->
|
||||
<TextView
|
||||
android:id="@+id/heightDescription"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/popup_position"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintTop_toBottomOf="@id/redirectionDelay"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<!-- Position slider -->
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/popupHeightSlider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="100"
|
||||
android:stepSize="1"
|
||||
android:contentDescription="@string/popup_position"
|
||||
app:layout_constraintTop_toBottomOf="@id/heightDescription"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<!-- Animation label -->
|
||||
<TextView
|
||||
android:id="@+id/popupEffectLabel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/popup_effect_label"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintTop_toBottomOf="@id/popupHeightSlider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<!-- Animation dropdown -->
|
||||
<Spinner
|
||||
android:id="@+id/popupEffectSpinner"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/popupEffectLabel"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<!-- Test button -->
|
||||
<Button
|
||||
android:id="@+id/popupPreview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/test"
|
||||
app:layout_constraintTop_toBottomOf="@id/popupEffectSpinner"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
|
@ -1,57 +0,0 @@
|
|||
<?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>
|
|
@ -1,32 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
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="16dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/serviceRecycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/serviceHeader"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/serviceHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/services_desc"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/serviceRecycler"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contactName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Name" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/contactAllowed"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
/>
|
||||
</LinearLayout>
|
|
@ -1,41 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:cardElevation="4dp"
|
||||
android:padding="24dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
card_view:cardBackgroundColor="@android:color/transparent"
|
||||
card_view:cardCornerRadius="32dp"
|
||||
card_view:cardElevation="0dp"
|
||||
card_view:contentPadding="0dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/popup"
|
||||
android:padding="16dp"
|
||||
android:textSize="16sp"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textAlignment="center"
|
||||
tools:text="Popup text" />
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="3dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:indeterminate="false"
|
||||
android:max="100"
|
||||
style="@android:style/Widget.ProgressBar.Horizontal"/>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</androidx.cardview.widget.CardView>
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/secretRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#CC000000">
|
||||
|
||||
<partisan.weforge.xyz.pulse.SecretView
|
||||
android:id="@+id/secretView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/globalPopupToggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:checked="true" />
|
|
@ -1,58 +0,0 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<!-- Home (main screen toggle) -->
|
||||
<group android:checkableBehavior="none">
|
||||
<item
|
||||
android:id="@+id/action_home"
|
||||
android:title="@string/home_name"
|
||||
android:icon="@drawable/ic_power_settings_new_24"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
|
||||
<!-- Settings section -->
|
||||
<!-- <item
|
||||
android:id="@+id/section_settings"
|
||||
android:title="@string/settings_name"
|
||||
android:enabled="false" /> -->
|
||||
<group android:checkableBehavior="none">
|
||||
<item
|
||||
android:id="@+id/action_contacts"
|
||||
android:title="@string/whitelist_name"
|
||||
android:icon="@drawable/group_24px"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_popup_settings"
|
||||
android:title="@string/popup_name"
|
||||
android:icon="@drawable/tooltip_24px"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_services"
|
||||
android:title="@string/services_name"
|
||||
android:icon="@drawable/services_24"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_redirect_settings"
|
||||
android:title="@string/redirect_name"
|
||||
android:icon="@drawable/call_split_24px"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
|
||||
<!-- About section -->
|
||||
<!-- <item
|
||||
android:id="@+id/section_about"
|
||||
android:title="@string/about_name"
|
||||
android:enabled="false" /> -->
|
||||
<group android:checkableBehavior="none">
|
||||
<item
|
||||
android:id="@+id/action_donate"
|
||||
android:title="Donate"
|
||||
android:icon="@drawable/heart_24"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_about"
|
||||
android:title="@string/about_name"
|
||||
android:icon="@drawable/info_24px"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
</menu>
|
|
@ -1,7 +0,0 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/globalPopupToggle"
|
||||
app:actionLayout="@layout/switch_item"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
|
@ -9,4 +9,5 @@
|
|||
<string name="destination_whatsapp">WhatsApp</string>
|
||||
<string name="redirection_delay_description">Задержка до того, как звонок будет перенаправлен.</string>
|
||||
<string name="popup_position">Позиция всплывающего окна</string>
|
||||
<string name="fallback">Обратная совместимость</string>
|
||||
</resources>
|
|
@ -14,6 +14,4 @@
|
|||
<color name="colorOnSecondary">#000000</color>
|
||||
|
||||
<color name="launcher_background">@color/colorPrimary</color>
|
||||
|
||||
<color name="colorSurfaceVariant">#2B3542</color>
|
||||
</resources>
|
||||
|
|
|
@ -1,79 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Pulse</string>
|
||||
<string name="description">Redirecting outgoing calls to E2EE apps.</string>
|
||||
<string name="description">The app will try to redirect outgoing calls to Signal/Telegram/Threema/WhatsApp if available. For work it requires many permissions. Click on the toggle and grant permissions until it turns ON.</string>
|
||||
<string name="popup">Redirecting to %1$s</string>
|
||||
<string name="home_name">Home</string>
|
||||
<string name="settings_name">Settings</string>
|
||||
<string name="popup_name">Popup</string>
|
||||
<string name="services_name">Services</string>
|
||||
<string name="whitelist_name">Allowlist</string>
|
||||
<string name="redirect_name">Redirect</string>
|
||||
<string name="tools_name">Tools</string>
|
||||
<string name="about_name">About</string>
|
||||
<string name="donate_name">Donate</string>
|
||||
<string name="destination_signal">Signal</string>
|
||||
<string name="destination_telegram">Telegram</string>
|
||||
<string name="destination_threema">Threema</string>
|
||||
<string name="destination_whatsapp">WhatsApp</string>
|
||||
<string name="redirection_delay_description">The delay before a call will be redirected.</string>
|
||||
<string name="services_desc">Here you can enable or disable redirection to individual services and change their priority by dragging them. Redirection will be handled in order from top to bottom.</string>
|
||||
<string name="contact_search">Filter contacts</string>
|
||||
<string name="popup_position">Popup position</string>
|
||||
<string name="activate_description">To start, grant the required permissions by tapping the Activate button.</string>
|
||||
<string name="activate">Activate</string>
|
||||
<string name="navigation_drawer_open">Open menu</string>
|
||||
<string name="test">Test</string>
|
||||
<string name="source_code">Source Code</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-array name="popup_effects">
|
||||
<item>None</item>
|
||||
<item>Fade</item>
|
||||
<item>Slide</item>
|
||||
<item>Bounce</item>
|
||||
<item>Flop</item>
|
||||
<item>Matrix</item>
|
||||
<item>Slide Snap</item>
|
||||
<item>Gamer Mode</item>
|
||||
<item>Random</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Missing perms -->
|
||||
<string name="warn_no_contacts">Unable to access contacts.</string>
|
||||
<string name="warn_no_internet">No internet connection.</string>
|
||||
|
||||
<!-- 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>
|
||||
<string name="donate_lock">Donate to unlock this effect</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!</string>
|
||||
<string name="donate_token_left">You have %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>
|
||||
<string name="fallback">Fallback</string>
|
||||
</resources>
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="RoundIconShape" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">100%</item>
|
||||
</style>
|
||||
<style name="Toolbar.Title.Small" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
|
||||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -7,11 +7,6 @@
|
|||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
|
||||
<item name="colorSurface">@color/white</item>
|
||||
<item name="colorOnSurface">@color/black</item>
|
||||
<item name="textAppearanceBodyMedium">@style/TextAppearance.Material3.BodyMedium</item>
|
||||
<item name="colorSurfaceContainerLowest">?attr/colorSurfaceContainerLowest</item>
|
||||
<item name="colorSurfaceVariant">@color/colorSurfaceVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
17
app/src/test/java/me/lucky/red/ExampleUnitTest.kt
Normal file
|
@ -0,0 +1,17 @@
|
|||
package partisan.weforge.xyz.pulse
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
data/icon.png
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 56 KiB |
|
@ -1,197 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
fill="#000000"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
width="845.71301"
|
||||
height="842.34912"
|
||||
viewBox="0 0 76.114171 75.811421"
|
||||
enable-background="new 0 0 72 72"
|
||||
sodipodi:docname="icon_appstore.svg"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
inkscape:export-filename="icon_appstore.png"
|
||||
inkscape:export-xdpi="58.351101"
|
||||
inkscape:export-ydpi="58.351101"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:bx="https://boxy-svg.com">
|
||||
<sodipodi:namedview
|
||||
id="namedview44"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.49741846"
|
||||
inkscape:cx="-82.42557"
|
||||
inkscape:cy="414.13823"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1008"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" />
|
||||
<defs
|
||||
id="defs5">
|
||||
<radialGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="254.89999"
|
||||
cy="255.633"
|
||||
r="238.439"
|
||||
id="gradient-1"
|
||||
gradientTransform="matrix(0.25708576,0,0,0.25708576,-27.372645,-27.645846)">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color: rgb(1, 5, 11);"
|
||||
id="stop2" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color: rgb(11, 22, 36);"
|
||||
id="stop4" />
|
||||
</radialGradient>
|
||||
<bx:export>
|
||||
<bx:file
|
||||
format="png" />
|
||||
</bx:export>
|
||||
</defs>
|
||||
<ellipse
|
||||
style="fill:url(#gradient-1);fill-rule:nonzero;stroke:#000000;stroke-width:2.305;paint-order:fill"
|
||||
cx="38.160999"
|
||||
cy="38.068001"
|
||||
rx="61.297596"
|
||||
ry="62.212971"
|
||||
id="ellipse9" />
|
||||
<path
|
||||
d="m 70.268677,25.717168 c -0.01907,0.04599 -0.118909,0.486854 -0.224357,0.998387 -0.111056,0.499193 -0.206408,0.91874 -0.216504,0.928836 -0.01683,0.01346 -0.44647,0.111057 -0.96249,0.222113 -0.51602,0.09984 -0.96249,0.210895 -0.983804,0.232209 -0.02131,0.02468 0.404964,0.145832 0.956881,0.256889 l 0.999509,0.208651 0.08413,0.366823 c 0.04375,0.210896 0.135735,0.630442 0.199677,0.951272 0.06843,0.321952 0.141345,0.611372 0.162659,0.632686 0.02131,0.02244 0.13237,-0.410573 0.251279,-0.943419 0.116666,-0.550796 0.208652,-0.9939 0.216504,-0.9939 0.0056,-0.01346 0.448713,-0.09984 0.983804,-0.222113 0.535091,-0.111057 0.986047,-0.210896 0.997265,-0.220992 0.03253,-0.0359 -0.08974,-0.06843 -1.048867,-0.268106 -0.500315,-0.121152 -0.916497,-0.208652 -0.926593,-0.222113 -0.01346,-0.0101 -0.116665,-0.464418 -0.241183,-1.018579 -0.115544,-0.540699 -0.2266,-0.951272 -0.247914,-0.908644 z"
|
||||
style="fill:#ffffff;fill-opacity:0.79;stroke-width:3.35081"
|
||||
id="path47">
|
||||
<title
|
||||
id="title45">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 6.8446282,33.335388 c -0.010769,0.04326 -0.1108098,0.508144 -0.2216375,1.005478 -0.1108142,0.51085 -0.2108276,0.943315 -0.2216382,0.954125 -0.013443,0.01077 -0.4540879,0.11081 -0.9757484,0.221638 -0.5189574,0.110809 -0.9757484,0.221639 -0.9973715,0.23245 -0.021618,0.02162 0.4108416,0.143258 0.9757484,0.264884 l 1.0081821,0.210827 0.089195,0.378407 c 0.043254,0.197312 0.1324497,0.629776 0.2000169,0.951421 0.064873,0.332457 0.1432585,0.610857 0.1648752,0.643292 0.021617,0.02162 0.1324501,-0.410842 0.2432609,-0.964937 0.1216226,-0.540581 0.2216397,-1.008183 0.2216397,-1.008183 0.010769,0 0.4648983,-0.09729 0.9973718,-0.210827 C 8.8718044,35.903154 9.3258921,35.795028 9.336705,35.781515 9.369136,35.749075 9.247512,35.716645 8.2825732,35.516629 7.7744271,35.395007 7.3527746,35.305804 7.3419617,35.294991 7.3311926,35.284222 7.2203402,34.81928 7.0987008,34.265187 6.9770614,33.71109 6.8662505,33.289438 6.8446282,33.335388 Z"
|
||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
||||
id="path59">
|
||||
<title
|
||||
id="title57">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 46.305146,4.9237957 c -0.03253,0.075159 -0.2008,0.8503116 -0.387016,1.7051103 -0.189581,0.8626512 -0.345509,1.5839578 -0.364579,1.6052717 -0.0359,0.021314 -0.765056,0.1895813 -1.651265,0.3892587 -0.88733,0.17612 -1.652386,0.3645795 -1.695014,0.3971112 -0.03478,0.032532 0.697749,0.2322092 1.637803,0.4330083 l 1.727546,0.3645795 0.145832,0.6326856 c 0.06731,0.343266 0.220991,1.072425 0.343266,1.626586 0.111056,0.554161 0.232209,1.043258 0.275958,1.087008 0.03478,0.03253 0.220992,-0.69775 0.420669,-1.629951 0.200799,-0.929959 0.367945,-1.7051107 0.375797,-1.7051107 0.01346,-0.011218 0.776274,-0.1649021 1.687162,-0.3757974 C 49.740046,9.2639746 50.516319,9.0878546 50.537633,9.0642972 50.591479,9.0104516 50.383949,8.9554842 48.74278,8.609975 47.877886,8.423759 47.17004,8.2554916 47.147605,8.2341777 47.126291,8.2128638 46.947927,7.4365899 46.737032,6.4853178 46.537355,5.553116 46.337677,4.8452709 46.305146,4.9237957 Z"
|
||||
style="fill:#ffffff;stroke-width:3.33357"
|
||||
id="path103">
|
||||
<title
|
||||
id="title101">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 27.927379,55.523595 c -0.02131,0.07292 -0.172754,0.745986 -0.337656,1.502068 -0.162659,0.757203 -0.306247,1.399985 -0.327561,1.411203 -0.01907,0.01907 -0.675313,0.172754 -1.451587,0.337657 -0.775153,0.162658 -1.451588,0.324195 -1.484119,0.356727 -0.04039,0.03029 0.613615,0.205286 1.443734,0.379162 l 1.513286,0.326439 0.122274,0.551917 c 0.07292,0.297273 0.205287,0.951273 0.307369,1.432518 0.10096,0.489097 0.206408,0.907522 0.243427,0.947906 0.03253,0.03253 0.19519,-0.61025 0.370188,-1.429151 0.183973,-0.818902 0.327561,-1.50319 0.338779,-1.50319 0,0 0.67307,-0.143588 1.480753,-0.326439 0.807684,-0.162658 1.480754,-0.32756 1.50319,-0.338778 0.05048,-0.05048 -0.132371,-0.102082 -1.576106,-0.407207 -0.753838,-0.164903 -1.389889,-0.308491 -1.407837,-0.327561 -0.01122,-0.01122 -0.17612,-0.705602 -0.360093,-1.532356 -0.172754,-0.830119 -0.348874,-1.443735 -0.378041,-1.380915 z"
|
||||
style="fill:#ffffff;stroke-width:3.85334"
|
||||
id="path103-3">
|
||||
<title
|
||||
id="title101-5">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 29.870808,39.99153 c -0.0101,0.03477 -0.07852,0.356727 -0.15705,0.710088 -0.07516,0.354484 -0.142467,0.655122 -0.153684,0.662974 -0.01122,0.01346 -0.321952,0.07852 -0.686532,0.15705 -0.364579,0.0774 -0.675313,0.153684 -0.697749,0.167145 -0.0101,0.0079 0.287177,0.0976 0.686531,0.17612 l 0.708967,0.155928 0.05609,0.254645 c 0.03253,0.142466 0.09984,0.454322 0.143588,0.675313 0.04375,0.23221 0.09984,0.433009 0.111056,0.443105 0.02131,0.02244 0.09984,-0.28942 0.178364,-0.664096 0.08638,-0.389259 0.153684,-0.708967 0.153684,-0.708967 0.01122,0 0.321952,-0.06731 0.697749,-0.155928 0.375798,-0.07852 0.696628,-0.154806 0.707845,-0.154806 0.02468,-0.02131 -0.06506,-0.04599 -0.740376,-0.188459 -0.354484,-0.08974 -0.654,-0.143589 -0.654,-0.15705 -0.01122,-0.0079 -0.08974,-0.329804 -0.178364,-0.719063 -0.07628,-0.387015 -0.164902,-0.686531 -0.176119,-0.653999 z"
|
||||
style="fill:#ffffff;fill-opacity:0.6;stroke-width:3.67879"
|
||||
id="path115">
|
||||
<title
|
||||
id="title113">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 55.038991,53.910536 c -0.02131,0.07516 -0.189582,0.818902 -0.36458,1.647899 -0.189581,0.833485 -0.343265,1.54133 -0.364579,1.551426 -0.02468,0.02468 -0.732525,0.189582 -1.59742,0.367945 -0.854798,0.17612 -1.595175,0.36458 -1.627707,0.397111 -0.04375,0.03253 0.675314,0.222113 1.583958,0.421791 l 1.662483,0.343266 0.142466,0.608006 c 0.06843,0.33317 0.222113,1.054476 0.321952,1.587323 0.111056,0.529482 0.232209,0.9939 0.275958,1.039893 0.03478,0.03253 0.218748,-0.675313 0.413938,-1.57274 0.191825,-0.897426 0.347753,-1.651265 0.358971,-1.651265 0.0079,-0.01122 0.745986,-0.168267 1.632194,-0.354483 0.887331,-0.18846 1.633317,-0.36458 1.652387,-0.378041 0.05385,-0.05385 -0.149197,-0.108813 -1.735399,-0.440861 -0.830119,-0.189581 -1.522259,-0.343265 -1.54133,-0.367945 -0.01907,-0.0101 -0.194068,-0.772908 -0.397111,-1.683796 -0.191825,-0.907523 -0.38365,-1.583958 -0.416181,-1.515529 z"
|
||||
style="fill:#ffffff;fill-opacity:0.85;stroke-width:7.65905"
|
||||
id="path135">
|
||||
<title
|
||||
id="title133">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 24.092191,5.6795911 c -0.03253,0.1032041 -0.262497,1.133001 -0.502559,2.276098 -0.262497,1.1464623 -0.473392,2.1224139 -0.505924,2.1437279 -0.02917,0.02917 -1.018579,0.251279 -2.205426,0.513777 -1.183481,0.240061 -2.205425,0.494706 -2.257027,0.537334 -0.0516,0.0516 0.935567,0.314099 2.195329,0.586692 l 2.299656,0.481245 0.189581,0.843581 c 0.105448,0.462175 0.302881,1.437004 0.462175,2.180746 0.145831,0.738134 0.312977,1.378672 0.366823,1.44037 0.0516,0.0516 0.306246,-0.934445 0.57884,-2.175137 0.262497,-1.248545 0.481245,-2.276098 0.492463,-2.276098 0.0101,-0.01346 1.029796,-0.23221 2.255905,-0.494707 1.219379,-0.262497 2.257028,-0.502559 2.279464,-0.523873 0.08638,-0.08413 -0.197434,-0.160415 -2.392764,-0.611371 -1.14534,-0.262498 -2.099978,-0.473393 -2.132509,-0.502559 -0.02131,-0.02131 -0.261376,-1.0701815 -0.545187,-2.3333093 -0.262497,-1.2485446 -0.524995,-2.1919642 -0.57884,-2.0865166 z"
|
||||
style="fill:#ffffff;stroke-width:4.9325"
|
||||
id="path143">
|
||||
<title
|
||||
id="title141">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 62.536413,34.870169 c -0.02917,0.07852 -0.210896,0.897426 -0.404964,1.806071 -0.200799,0.908644 -0.373554,1.672578 -0.392624,1.695014 -0.02917,0.02131 -0.805441,0.199678 -1.745495,0.399355 -0.937811,0.200799 -1.746616,0.397111 -1.787,0.433008 -0.04039,0.03253 0.738133,0.243427 1.735398,0.450957 l 1.816167,0.389259 0.153684,0.665217 c 0.07852,0.36458 0.241184,1.140854 0.362336,1.72979 0.122275,0.583327 0.25128,1.083642 0.292786,1.140853 0.04038,0.04263 0.242305,-0.743742 0.4532,-1.719693 0.214261,-0.983804 0.38365,-1.804949 0.394868,-1.804949 0.0079,-0.01122 0.816658,-0.186216 1.787,-0.400477 0.966977,-0.197434 1.785879,-0.385893 1.804949,-0.408329 0.06282,-0.05609 -0.161537,-0.121153 -1.896935,-0.489097 -0.908644,-0.197434 -1.664726,-0.36458 -1.687162,-0.385894 -0.01907,-0.02244 -0.210895,-0.851433 -0.431886,-1.848698 -0.214261,-0.98717 -0.413938,-1.741008 -0.454322,-1.652387 z"
|
||||
style="fill:#ffffff;fill-opacity:0.85;stroke-width:3.78785"
|
||||
id="path163">
|
||||
<title
|
||||
id="title161">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 14.351587,22.573585 c -0.01122,0.04263 -0.111056,0.508168 -0.222113,1.007361 -0.111056,0.508168 -0.210895,0.941176 -0.220991,0.952394 -0.01122,0.0101 -0.444226,0.109935 -0.973708,0.220991 -0.521629,0.111057 -0.975951,0.222113 -0.997265,0.232209 -0.01346,0.02244 0.419547,0.145832 0.972586,0.268106 l 1.008483,0.210896 0.08974,0.364579 c 0.04263,0.208652 0.132371,0.640538 0.199678,0.96249 0.06506,0.332048 0.143588,0.61025 0.164902,0.642782 0.02468,0.02244 0.134614,-0.408329 0.256888,-0.964734 0.111057,-0.540699 0.210896,-0.997265 0.218748,-0.997265 0,-0.0079 0.456566,-0.107691 0.987169,-0.218748 0.542943,-0.1133 0.997265,-0.222113 1.007361,-0.232209 0.03253,-0.0359 -0.08862,-0.06843 -1.05111,-0.268106 -0.510412,-0.121152 -0.929959,-0.210895 -0.94342,-0.220991 -0.01122,-0.01122 -0.121153,-0.476758 -0.243427,-1.029797 -0.111056,-0.554161 -0.220991,-0.973708 -0.253523,-0.929958 z"
|
||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
||||
id="path-1">
|
||||
<title
|
||||
id="bx-title-1">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 42.539166,23.607638 c -0.01077,0.03244 -0.07839,0.354082 -0.154067,0.708161 -0.07839,0.35408 -0.143258,0.654103 -0.156774,0.654103 -0.01077,0.01077 -0.30813,0.08919 -0.686535,0.164875 -0.364893,0.07839 -0.675728,0.156774 -0.697351,0.167583 -0.01077,0.01077 0.286509,0.100007 0.686538,0.178392 l 0.710864,0.154067 0.05406,0.254072 c 0.03243,0.143259 0.100007,0.443276 0.143258,0.675727 0.04594,0.232448 0.100007,0.421651 0.110809,0.443274 0.02432,0.02162 0.100007,-0.28921 0.178392,-0.664914 0.08919,-0.386514 0.154067,-0.708159 0.154067,-0.708159 0.01077,0 0.321645,-0.06758 0.697348,-0.154067 0.378408,-0.07839 0.700053,-0.156774 0.710864,-0.167583 0.02162,-0.02162 -0.06758,-0.04326 -0.743298,-0.189204 -0.354082,-0.07567 -0.654102,-0.143258 -0.654102,-0.154068 -0.01077,0 -0.08649,-0.321645 -0.175685,-0.718971 C 42.639162,23.861708 42.54997,23.5752 42.53916,23.607634 Z"
|
||||
style="fill:#ffffff;fill-opacity:0.6;stroke-width:3.67879"
|
||||
id="path-2">
|
||||
<title
|
||||
id="bx-title-2">Star</title>
|
||||
</path>
|
||||
<g
|
||||
transform="matrix(0.71937225,0,0,0.71937225,11.595061,9.3401941)"
|
||||
id="g32">
|
||||
<path
|
||||
d="m 60.999,9.506 c -5.233,0 -9.493,4.26 -9.493,9.495 0,3.646 2.07,6.815 5.095,8.404 l -4.95,14.254 C 51.114,41.564 50.564,41.506 50,41.506 c -1.456,0 -2.831,0.34 -4.065,0.928 L 34.689,30.814 c 0.515,-1.168 0.806,-2.457 0.806,-3.813 0,-5.235 -4.259,-9.495 -9.495,-9.495 -5.234,0 -9.493,4.26 -9.493,9.495 0,2.931 1.337,5.555 3.431,7.298 L 15.172,44.485 C 13.911,43.865 12.498,43.506 11,43.506 c -5.234,0 -9.493,4.26 -9.493,9.495 0,5.233 4.259,9.493 9.493,9.493 5.236,0 9.495,-4.26 9.495,-9.493 0,-2.253 -0.792,-4.321 -2.107,-5.951 L 23.49,36.146 c 0.801,0.221 1.641,0.348 2.511,0.348 2.416,0 4.616,-0.914 6.293,-2.404 l 10.452,10.801 c -1.394,1.653 -2.238,3.784 -2.238,6.11 0,5.233 4.26,9.493 9.493,9.493 5.235,0 9.495,-4.26 9.495,-9.493 0,-3.248 -1.641,-6.117 -4.136,-7.83 l 5.108,-14.704 c 0.177,0.01 0.353,0.027 0.532,0.027 5.235,0 9.495,-4.259 9.495,-9.493 -0.001,-5.235 -4.261,-9.495 -9.496,-9.495 z m -50,48.988 c -3.029,0 -5.493,-2.465 -5.493,-5.493 0,-3.03 2.464,-5.495 5.493,-5.495 3.03,0 5.495,2.465 5.495,5.495 0,3.028 -2.465,5.493 -5.495,5.493 z m 15,-26 c -3.029,0 -5.493,-2.464 -5.493,-5.493 0,-3.03 2.464,-5.495 5.493,-5.495 3.03,0 5.495,2.465 5.495,5.495 0,3.029 -2.465,5.493 -5.495,5.493 z m 24,24 c -3.028,0 -5.493,-2.465 -5.493,-5.493 0,-3.03 2.465,-5.495 5.493,-5.495 3.03,0 5.495,2.465 5.495,5.495 0,3.028 -2.465,5.493 -5.495,5.493 z m 11,-32 c -3.028,0 -5.493,-2.464 -5.493,-5.493 0,-3.03 2.465,-5.495 5.493,-5.495 3.03,0 5.495,2.465 5.495,5.495 0,3.029 -2.465,5.493 -5.495,5.493 z"
|
||||
style="fill:#979797"
|
||||
id="path30" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.71937225,0,0,0.71937225,10.767746,8.0147726)"
|
||||
id="g36">
|
||||
<path
|
||||
d="m 60.999,9.506 c -5.233,0 -9.493,4.26 -9.493,9.495 0,3.646 2.07,6.815 5.095,8.404 l -4.95,14.254 C 51.114,41.564 50.564,41.506 50,41.506 c -1.456,0 -2.831,0.34 -4.065,0.928 L 34.689,30.814 c 0.515,-1.168 0.806,-2.457 0.806,-3.813 0,-5.235 -4.259,-9.495 -9.495,-9.495 -5.234,0 -9.493,4.26 -9.493,9.495 0,2.931 1.337,5.555 3.431,7.298 L 15.172,44.485 C 13.911,43.865 12.498,43.506 11,43.506 c -5.234,0 -9.493,4.26 -9.493,9.495 0,5.233 4.259,9.493 9.493,9.493 5.236,0 9.495,-4.26 9.495,-9.493 0,-2.253 -0.792,-4.321 -2.107,-5.951 L 23.49,36.146 c 0.801,0.221 1.641,0.348 2.511,0.348 2.416,0 4.616,-0.914 6.293,-2.404 l 10.452,10.801 c -1.394,1.653 -2.238,3.784 -2.238,6.11 0,5.233 4.26,9.493 9.493,9.493 5.235,0 9.495,-4.26 9.495,-9.493 0,-3.248 -1.641,-6.117 -4.136,-7.83 l 5.108,-14.704 c 0.177,0.01 0.353,0.027 0.532,0.027 5.235,0 9.495,-4.259 9.495,-9.493 -0.001,-5.235 -4.261,-9.495 -9.496,-9.495 z m -50,48.988 c -3.029,0 -5.493,-2.465 -5.493,-5.493 0,-3.03 2.464,-5.495 5.493,-5.495 3.03,0 5.495,2.465 5.495,5.495 0,3.028 -2.465,5.493 -5.495,5.493 z m 15,-26 c -3.029,0 -5.493,-2.464 -5.493,-5.493 0,-3.03 2.464,-5.495 5.493,-5.495 3.03,0 5.495,2.465 5.495,5.495 0,3.029 -2.465,5.493 -5.495,5.493 z m 24,24 c -3.028,0 -5.493,-2.465 -5.493,-5.493 0,-3.03 2.465,-5.495 5.493,-5.495 3.03,0 5.495,2.465 5.495,5.495 0,3.028 -2.465,5.493 -5.495,5.493 z m 11,-32 c -3.028,0 -5.493,-2.464 -5.493,-5.493 0,-3.03 2.465,-5.495 5.493,-5.495 3.03,0 5.495,2.465 5.495,5.495 0,3.029 -2.465,5.493 -5.495,5.493 z"
|
||||
style="fill:#ffffff"
|
||||
id="path34" />
|
||||
</g>
|
||||
<path
|
||||
d="m 8.756373,13.330864 c -0.01122,0.04263 -0.111056,0.508168 -0.222113,1.007361 -0.111056,0.508168 -0.210895,0.941176 -0.220991,0.952394 -0.01122,0.0101 -0.444226,0.109935 -0.973708,0.220991 -0.5216288,0.111057 -0.9759509,0.222113 -0.9972648,0.232209 -0.013461,0.02244 0.4195469,0.145832 0.9725858,0.268106 l 1.008483,0.210896 0.08974,0.364579 c 0.04263,0.208652 0.13237,0.640538 0.199677,0.96249 0.06506,0.332048 0.143588,0.61025 0.164902,0.642782 0.02468,0.02244 0.134614,-0.408329 0.256889,-0.964734 0.111056,-0.540699 0.210895,-0.997265 0.218747,-0.997265 0,-0.0079 0.456566,-0.107691 0.98717,-0.218748 0.542943,-0.1133 0.997265,-0.222113 1.007361,-0.232209 0.03253,-0.0359 -0.08862,-0.06843 -1.051111,-0.268106 C 9.686329,15.390458 9.266782,15.300715 9.25332,15.290619 9.2421,15.279399 9.132168,14.813861 9.009893,14.260822 8.898837,13.706661 8.788902,13.287114 8.75637,13.330864 Z"
|
||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
||||
id="path-3">
|
||||
<title
|
||||
id="bx:title-1">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 5.7189969,55.62054 c -0.010096,0.03702 -0.096473,0.441983 -0.1929467,0.876113 -0.097595,0.441983 -0.1839724,0.818902 -0.1929467,0.828998 -0.00897,0.0079 -0.3858934,0.09535 -0.8469462,0.191825 -0.4532004,0.09647 -0.8480681,0.192946 -0.8671384,0.201921 -0.011218,0.01907 0.3657013,0.126761 0.8458245,0.233331 l 0.8772344,0.18285 0.078525,0.317465 c 0.037019,0.181729 0.1144219,0.556404 0.1727546,0.83685 0.057211,0.288298 0.1256397,0.530603 0.1435883,0.558648 0.021314,0.02019 0.1177872,-0.354483 0.2232348,-0.839094 0.096473,-0.470027 0.1839724,-0.867138 0.1907031,-0.867138 0,-0.0067 0.3971113,-0.09311 0.8581641,-0.189581 0.4722707,-0.09872 0.8671384,-0.192947 0.8761127,-0.201921 0.028044,-0.03141 -0.077403,-0.05945 -0.9142535,-0.233331 C 6.5278026,57.412028 6.1621013,57.333503 6.1508834,57.325651 6.1407874,57.315555 6.0454358,56.910591 5.9388664,56.429346 5.8423931,55.948101 5.7470415,55.583522 5.7189969,55.62054 Z"
|
||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
||||
id="path-4">
|
||||
<title
|
||||
id="bx:title-2">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 69.390321,11.116039 c -0.0079,0.02804 -0.07516,0.342143 -0.149198,0.678678 -0.07628,0.342144 -0.142466,0.63493 -0.150319,0.642782 -0.0067,0.0056 -0.298394,0.07404 -0.656243,0.148075 -0.351118,0.07516 -0.657365,0.150319 -0.671948,0.15705 -0.009,0.01458 0.283811,0.0976 0.655122,0.180607 l 0.680922,0.141345 0.06058,0.246792 c 0.02805,0.140223 0.08862,0.430765 0.133492,0.648391 0.04487,0.223235 0.0976,0.411694 0.111057,0.433008 0.01683,0.01571 0.09199,-0.274837 0.173876,-0.650634 0.07404,-0.36458 0.142467,-0.671948 0.146954,-0.671948 0,-0.0056 0.30849,-0.07179 0.665217,-0.146954 0.366823,-0.07628 0.671948,-0.149197 0.679801,-0.155928 0.02131,-0.02468 -0.06058,-0.04711 -0.708967,-0.181729 -0.343266,-0.08189 -0.627077,-0.142466 -0.636051,-0.148075 -0.0079,-0.0079 -0.08077,-0.321952 -0.16378,-0.694384 -0.07516,-0.373553 -0.149198,-0.656243 -0.170511,-0.627076 z"
|
||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
||||
id="path-5">
|
||||
<title
|
||||
id="bx:title-3">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 68.04615,54.60911 c -0.01122,0.04263 -0.111057,0.508168 -0.222113,1.007361 -0.111057,0.508168 -0.210895,0.941176 -0.220992,0.952394 -0.01122,0.0101 -0.444226,0.109935 -0.973707,0.220992 -0.521629,0.111056 -0.975952,0.222113 -0.997265,0.232209 -0.01346,0.02244 0.419546,0.145831 0.972586,0.268106 l 1.008483,0.210895 0.08974,0.36458 c 0.04263,0.208651 0.132371,0.640538 0.199678,0.962489 0.06506,0.332048 0.143588,0.610251 0.164902,0.642782 0.02468,0.02244 0.134614,-0.408329 0.256888,-0.964733 0.111057,-0.5407 0.210895,-0.997265 0.218748,-0.997265 0,-0.0079 0.456565,-0.107692 0.987169,-0.218748 0.542943,-0.1133 0.997265,-0.222113 1.007361,-0.232209 0.03253,-0.0359 -0.08862,-0.06843 -1.051111,-0.268106 -0.510411,-0.121153 -0.929958,-0.210896 -0.943419,-0.220992 -0.01122,-0.01122 -0.121153,-0.476758 -0.243427,-1.029797 -0.111057,-0.554161 -0.220991,-0.973707 -0.253523,-0.929958 z"
|
||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
||||
id="path-6">
|
||||
<title
|
||||
id="bx:title-4">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 41.510644,63.994652 c -0.0101,0.04263 -0.109934,0.508167 -0.220991,1.005117 -0.111056,0.511533 -0.210895,0.94342 -0.222113,0.954638 -0.01346,0.0101 -0.4532,0.111056 -0.975951,0.220991 -0.518264,0.111056 -0.97483,0.222113 -0.997265,0.233331 -0.02131,0.02131 0.411694,0.142466 0.975951,0.264741 l 1.008483,0.210895 0.08862,0.378041 c 0.04375,0.197434 0.133492,0.62932 0.200799,0.951272 0.06506,0.332048 0.142466,0.611372 0.164902,0.642782 0.02131,0.02244 0.132371,-0.410573 0.243427,-0.964734 0.121153,-0.540699 0.220991,-1.007361 0.220991,-1.007361 0.01122,0 0.46554,-0.09759 0.997266,-0.210895 0.544064,-0.111057 0.997265,-0.21987 1.008483,-0.233331 0.03253,-0.03141 -0.08974,-0.06394 -1.054477,-0.264741 -0.508167,-0.121153 -0.929958,-0.210895 -0.940054,-0.220991 -0.01122,-0.01122 -0.122274,-0.475636 -0.243427,-1.029797 -0.121152,-0.554161 -0.232209,-0.975952 -0.254644,-0.929958 z"
|
||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
||||
id="path-7">
|
||||
<title
|
||||
id="bx:title-5">Star</title>
|
||||
</path>
|
||||
<path
|
||||
d="m 15.624023,64.277622 c -0.0101,0.03253 -0.07852,0.354483 -0.153684,0.707845 -0.07852,0.354484 -0.143589,0.655121 -0.15705,0.655121 -0.0101,0.0101 -0.307369,0.08862 -0.686531,0.164903 -0.36458,0.0774 -0.675314,0.155927 -0.696628,0.167145 -0.01122,0.01122 0.286055,0.09984 0.686532,0.178364 l 0.710088,0.153684 0.05497,0.254645 c 0.03141,0.143588 0.09984,0.443104 0.142466,0.675313 0.04599,0.232209 0.09984,0.421791 0.111057,0.443105 0.02468,0.02244 0.09984,-0.288299 0.178363,-0.664096 0.08974,-0.387015 0.153685,-0.708967 0.153685,-0.708967 0.01122,0 0.321951,-0.06731 0.697749,-0.153684 0.378041,-0.07852 0.699992,-0.15705 0.71121,-0.167146 0.02131,-0.02244 -0.06843,-0.04375 -0.743742,-0.189581 -0.354484,-0.07628 -0.654,-0.143589 -0.654,-0.153685 -0.01122,0 -0.08638,-0.321951 -0.17612,-0.719063 -0.07852,-0.389258 -0.167145,-0.676435 -0.178363,-0.643903 z"
|
||||
style="fill:#ffffff;fill-opacity:0.6;stroke-width:3.67879"
|
||||
id="path-8">
|
||||
<title
|
||||
id="bx:title-6">Star</title>
|
||||
</path>
|
||||
</svg>
|
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 163 KiB |
|
@ -1,5 +0,0 @@
|
|||
v1.3.0
|
||||
- Forked from Red and renamed to Pulse.
|
||||
- Changed Icons and graphic.
|
||||
- Added material you icon.
|
||||
- Added options to toggle and change priority to individual redirect services.
|
|
@ -1,2 +0,0 @@
|
|||
v1.3.1
|
||||
- Updated metadata and removed some background Google BLOB to improve compliance with IzzyOnDroid repo.
|
|
@ -1,2 +0,0 @@
|
|||
v1.3.2
|
||||
- Fixed crash related to redirect popup
|
|
@ -1,3 +0,0 @@
|
|||
v1.4.0
|
||||
- Added progress bar to popup, to better indicate loading
|
||||
- Updated appstore icon
|
|
@ -1,2 +0,0 @@
|
|||
v1.4.1
|
||||
- Dependency update
|
|
@ -1,22 +0,0 @@
|
|||
v2.0.0
|
||||
- Reworked the entire UI
|
||||
- Added welcome screen to check for required permissions
|
||||
- New landing page with a single toggle, moved settings to separate menus
|
||||
- Added side menu with:
|
||||
- Allowlist
|
||||
- Redirection: toggle on Wi-Fi, etc.
|
||||
- Services: toggle individual redirect services and set their priority
|
||||
- Popup: change position, animation, and duration
|
||||
- Added About section with:
|
||||
- Donate
|
||||
- About the app
|
||||
|
||||
Fixes:
|
||||
- Popup now uses system Material colors
|
||||
- Fixed issue where service priority changes didn't work
|
||||
|
||||
Misc:
|
||||
- Updated Gradle to v8.14.1
|
||||
- Updated screenshots to reflect the new look of the app
|
||||
- Updated description
|
||||
- Temporarily added "v2.0" to the store icon to indicate the new version
|
|
@ -1,2 +0,0 @@
|
|||
v2.0.1
|
||||
- Fixed lock warning on NONE and RANDOM popup effects
|
|
@ -1,3 +0,0 @@
|
|||
v2.0.2
|
||||
- Added warning text in case the app does not have sufficient permissions
|
||||
- Fixed a bug related to tapping the Pulse logo in the About section, specific to MIUI
|
|
@ -1,3 +0,0 @@
|
|||
v2.0.3
|
||||
- Anonymized phone numbers in logs
|
||||
- Pulse no longer shows popup if disallowed by system
|
|
@ -1,39 +1,15 @@
|
|||
Redirect calls to Signal, Telegram, Threema, or WhatsApp.
|
||||
Tiny app to redirect outgoing calls to Signal/Telegram/Threema if available.
|
||||
|
||||
---
|
||||
You can cancel redirection by clicking on "Redirecting to.." popup.
|
||||
|
||||
**Features:**
|
||||
Permissions:
|
||||
* ACCESS_NETWORK_STATE - check internet is available
|
||||
* CALL_PHONE - make a call via messenger
|
||||
* READ_CONTACTS - check contact has a messenger record
|
||||
* SYSTEM_ALERT_WINDOW - show redirecting popup and launch an activity from background
|
||||
* CALL_REDIRECTION - process outgoing call
|
||||
|
||||
- Material You design
|
||||
- Popup with cancel option
|
||||
- Extensive settings panel:
|
||||
- Toggle per-service support
|
||||
- Redirection only on Wi-Fi/Data
|
||||
- Allowlist specific contacts
|
||||
- Change per-service priority
|
||||
- Customize popup position, animation, and duration
|
||||
- etc
|
||||
All permissions are mandatory.
|
||||
|
||||
**Supports:**
|
||||
|
||||
- Signal
|
||||
- Telegram
|
||||
- Threema
|
||||
- WhatsApp
|
||||
|
||||
**Permissions required:**
|
||||
|
||||
- `CALL_PHONE` - initiate calls via messenger
|
||||
- `READ_CONTACTS` - check contact compatibility
|
||||
- `READ_PHONE_NUMBERS` - detect outgoing call
|
||||
- `SYSTEM_ALERT_WINDOW` - show popup overlay
|
||||
- `ACCESS_NETWORK_STATE` - check connectivity
|
||||
- `INTERNET` - check connectivity and verify donates
|
||||
|
||||
Currently all of the permissions are required.
|
||||
|
||||
---
|
||||
|
||||
**License:** GPL-3.0
|
||||
|
||||
Free and open source
|
||||
It is Free Open Source Software.
|
||||
License: GPL-3
|
||||
|
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 95 KiB |
Before Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 121 KiB |
|
@ -1 +1 @@
|
|||
Redirecting outgoing calls to E2EE apps
|
||||
Redirect outgoing calls to Signal/Telegram/Threema
|
||||
|
|
|
@ -1 +1 @@
|
|||
Pulse
|
||||
Red
|
||||
|
|
17
fastlane/metadata/android/fr-FR/full_description.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
Petite application redirigereant les appels sortants vers Signal/Telegram/Threema si ils sont
|
||||
disponibles.
|
||||
|
||||
Vous pouvez annuler la redirection en cliquant sur la fenêtre contextuelle "Redirection vers...".
|
||||
|
||||
Autorisations:
|
||||
* ACCESS_NETWORK_STATE - Vérifié la disponibilité d\'accès à internet
|
||||
* CALL_PHONE - Passer un appel via messenger
|
||||
* READ_CONTACTS - Vérifier que le contact a un enregistreur de message
|
||||
* SYSTEM_ALERT_WINDOW - Afficher une fenêtre contextuelle de redirection et lancer une activité en
|
||||
arrière-plan
|
||||
* CALL_REDIRECTION - Traiter les appels sortants
|
||||
|
||||
Toutes les autorisations sont obligatoires.
|
||||
|
||||
C'est un logiciel libre et gratuit.
|
||||
Licence : GPL-3
|
1
fastlane/metadata/android/fr-FR/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Rediriger les appels sortants vers Signal/Telegram/Threema
|
1
fastlane/metadata/android/fr-FR/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Red
|
16
fastlane/metadata/android/ru-RU/full_description.txt
Normal file
|
@ -0,0 +1,16 @@
|
|||
Мини приложение для перенаправления исходящих вызовов в Signal/Telegram/Threema.
|
||||
|
||||
Вы можете отменить перенаправление, кликнув на всплывающее сообщение "Перенаправление в..".
|
||||
|
||||
Разрешения:
|
||||
* ACCESS_NETWORK_STATE - проверить наличие интернета
|
||||
* CALL_PHONE - позвонить через мессенджер
|
||||
* READ_CONTACTS - проверить контакт на наличие записи из мессенджера
|
||||
* SYSTEM_ALERT_WINDOW - показать всплывающее сообщение о перенаправлении и запустить активити из
|
||||
фона
|
||||
* CALL_REDIRECTION - обработать исходящий вызов
|
||||
|
||||
Все разрешения обязательны для работы приложения.
|
||||
|
||||
Это свободное программное обеспечение с открытым исходным кодом.
|
||||
Лицензия: GPL-3
|
1
fastlane/metadata/android/ru-RU/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Перенаправление исходящих вызовов в Signal/Telegram/Threema
|
1
fastlane/metadata/android/ru-RU/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Red
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,7 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
49
gradlew
vendored
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -15,8 +15,6 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
|
@ -57,7 +55,7 @@
|
|||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
@ -82,11 +80,13 @@ do
|
|||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
@ -114,7 +114,7 @@ case "$( uname )" in #(
|
|||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
|
@ -133,29 +133,22 @@ location of your Java installation."
|
|||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
|
@ -200,28 +193,18 @@ if "$cygwin" || "$msys" ; then
|
|||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
|
|
37
gradlew.bat
vendored
|
@ -13,8 +13,6 @@
|
|||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
|
@ -28,7 +26,6 @@ if "%OS%"=="Windows_NT" setlocal
|
|||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
@ -43,13 +40,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
|
@ -59,34 +56,32 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
|