Compare commits
No commits in common. "main" and "v1.3.1" have entirely different histories.
|
@ -1,6 +1,9 @@
|
||||||
name: Android Release Build
|
name: Android Release Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
2
.gitignore
vendored
|
@ -14,5 +14,3 @@
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
release-key.jks
|
release-key.jks
|
||||||
check.py
|
|
||||||
round.sh
|
|
97
README.md
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center" style="font-size: 32px;">
|
<p align="center" style="font-size: 32px;">
|
||||||
|
@ -7,19 +7,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Redirecting outgoing calls to E2EE apps.
|
Tiny app to redirect outgoing calls to Signal/Telegram/Threema/Whatsapp.
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<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>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -30,82 +18,27 @@ Redirecting outgoing calls to E2EE apps.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<span>
|
<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;">
|
<img src="https://weforge.xyz/partisan/Pulse/raw/branch/main/data/screenshot.png" alt="Main screen" height="500">
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<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;">
|
<img src="https://weforge.xyz/partisan/Pulse/raw/branch/main/data/screenshot-redirecting.png" alt="Redirecting popup" height="500">
|
||||||
</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>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Features
|
You can cancel redirection by clicking on `Redirecting to..` popup.
|
||||||
|
|
||||||
- Material You design
|
## Permissions
|
||||||
- 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
|
* 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
|
||||||
|
|
||||||
- Signal
|
All permissions are mandatory.
|
||||||
- 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
|
|
||||||
|
|
||||||
|
## License
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](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.
|
This application is Free Software: You can use, study share and improve it at your will.
|
||||||
|
@ -113,11 +46,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
|
[GNU General Public License v3](https://www.gnu.org/licenses/gpl.html) as published by the Free
|
||||||
Software Foundation.
|
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/).
|
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).
|
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](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.
|
|
@ -11,8 +11,8 @@ android {
|
||||||
applicationId = "partisan.weforge.xyz.pulse"
|
applicationId = "partisan.weforge.xyz.pulse"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 17
|
versionCode = 10
|
||||||
versionName = "2.0.3"
|
versionName = "1.3.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ android {
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled = false
|
minifyEnabled = false
|
||||||
signingConfig = signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
proguardFiles(getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro')
|
proguardFiles(getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,15 +62,11 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'com.google.android.material:material:1.13.0-alpha13'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
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.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-feature android:name="android.hardware.telephony" android:required="true" />
|
<uses-feature android:name="android.hardware.telephony" android:required="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
@ -18,14 +16,13 @@
|
||||||
android:theme="@style/Theme.Pulse">
|
android:theme="@style/Theme.Pulse">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".WelcomeActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".MainActivity" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".CallRedirectionService"
|
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.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import android.telecom.CallRedirectionService
|
import android.telecom.CallRedirectionService
|
||||||
import android.telecom.PhoneAccountHandle
|
import android.telecom.PhoneAccountHandle
|
||||||
import android.telephony.TelephonyManager
|
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
class CallRedirectionService : CallRedirectionService() {
|
class CallRedirectionService : CallRedirectionService() {
|
||||||
|
@ -61,94 +58,31 @@ class CallRedirectionService : CallRedirectionService() {
|
||||||
initialPhoneAccount: PhoneAccountHandle,
|
initialPhoneAccount: PhoneAccountHandle,
|
||||||
allowInteractiveResponse: Boolean,
|
allowInteractiveResponse: Boolean,
|
||||||
) {
|
) {
|
||||||
val phoneNumber = handle.schemeSpecificPart
|
if (!prefs.isEnabled || !hasInternet() || !allowInteractiveResponse) {
|
||||||
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")
|
|
||||||
placeCallUnmodified()
|
placeCallUnmodified()
|
||||||
return
|
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>
|
val records: Array<Record>
|
||||||
try {
|
try {
|
||||||
records = getRecordsFromPhoneNumber(phoneNumber)
|
records = getRecordsFromPhoneNumber(handle.schemeSpecificPart)
|
||||||
Log.d("Redirection", "Found ${records.size} raw redirect apps for number $numberAlias")
|
|
||||||
} catch (exc: SecurityException) {
|
} catch (exc: SecurityException) {
|
||||||
Log.w("Redirection", "SecurityException during record fetch", exc)
|
|
||||||
placeCallUnmodified()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter to enabled services only
|
||||||
val enabledRecords = records
|
val enabledRecords = records
|
||||||
.filter { prefs.isServiceEnabled(it.mimetype) }
|
.filter { prefs.isServiceEnabled(it.mimetype) }
|
||||||
.sortedBy { prefs.getServicePriority(it.mimetype) }
|
.sortedBy { prefs.getServicePriority(it.mimetype) }
|
||||||
|
|
||||||
Log.d("Redirection", "Filtered to ${enabledRecords.size} enabled redirect apps")
|
|
||||||
|
|
||||||
val record = enabledRecords.firstOrNull()
|
val record = enabledRecords.firstOrNull()
|
||||||
if (record == null) {
|
if (record == null) {
|
||||||
Log.d("Redirection", "Aborting: no suitable redirect apps found for number $numberAlias")
|
|
||||||
placeCallUnmodified()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("Redirection", "Redirecting call to: ${record.mimetype} → ${record.uri}")
|
|
||||||
|
|
||||||
Log.d("Redirection", "Popup ${if (allowInteractiveResponse) "allowed" else "not allowed"} by system; ${if (prefs.popupEnabled) "enabled" else "disabled"} in prefs")
|
|
||||||
|
|
||||||
if (allowInteractiveResponse && prefs.popupEnabled) {
|
|
||||||
window.show(record.uri, MIMETYPE_TO_DST_NAME[record.mimetype] ?: return)
|
window.show(record.uri, MIMETYPE_TO_DST_NAME[record.mimetype] ?: return)
|
||||||
} else {
|
|
||||||
window.call(record.uri)
|
|
||||||
cancelCall()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresPermission(Manifest.permission.READ_CONTACTS)
|
@RequiresPermission(Manifest.permission.READ_CONTACTS)
|
||||||
|
@ -200,63 +134,8 @@ class CallRedirectionService : CallRedirectionService() {
|
||||||
return results.toTypedArray()
|
return results.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAnonymizedAlias(number: String): String {
|
|
||||||
val prefs = getSharedPreferences("anonymized_numbers", MODE_PRIVATE)
|
|
||||||
|
|
||||||
// Return existing alias if already mapped
|
|
||||||
val existing = prefs.getString(number, null)
|
|
||||||
if (existing != null) return existing
|
|
||||||
|
|
||||||
// Start from current counter
|
|
||||||
var counter = prefs.getInt("counter", 1)
|
|
||||||
var alias: String
|
|
||||||
|
|
||||||
// Find the first unused alias (safety check)
|
|
||||||
while (true) {
|
|
||||||
alias = "#$counter"
|
|
||||||
if (!prefs.all.containsValue(alias)) break
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store new alias and increment counter
|
|
||||||
prefs.edit()
|
|
||||||
.putString(number, alias)
|
|
||||||
.putInt("counter", counter + 1)
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
return alias
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isInternationalNumber(phoneNumber: String): Boolean {
|
|
||||||
val telephony = getSystemService(TelephonyManager::class.java) ?: return true
|
|
||||||
val simCountryIso = telephony.simCountryIso?.lowercase() ?: return true
|
|
||||||
|
|
||||||
// Use libphonenumber to parse the number and get region
|
|
||||||
val util = PhoneNumberUtil.getInstance()
|
|
||||||
return try {
|
|
||||||
val numberProto = util.parse(phoneNumber, simCountryIso.uppercase())
|
|
||||||
val numberRegion = util.getRegionCodeForNumber(numberProto)?.lowercase()
|
|
||||||
numberRegion != simCountryIso
|
|
||||||
} catch (e: Exception) {
|
|
||||||
true // treat as international if parsing fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isOutsideHomeCountry(): Boolean {
|
|
||||||
val telephony = getSystemService(TelephonyManager::class.java) ?: return true
|
|
||||||
|
|
||||||
val simCountry = telephony.simCountryIso?.lowercase()
|
|
||||||
val networkCountry = telephony.networkCountryIso?.lowercase()
|
|
||||||
|
|
||||||
// If SIM or network country can't be determined, assume we're abroad
|
|
||||||
if (simCountry.isNullOrBlank() || networkCountry.isNullOrBlank()) return true
|
|
||||||
|
|
||||||
// If they don't match, you're abroad
|
|
||||||
return simCountry != networkCountry
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
|
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
|
||||||
private fun hasInternet(): Boolean { // This "hasInternet" func is (kinda) re-defined in Donation Fragment
|
private fun hasInternet(): Boolean {
|
||||||
val capabilities = connectivityManager
|
val capabilities = connectivityManager
|
||||||
?.getNetworkCapabilities(connectivityManager?.activeNetwork) ?: return false
|
?.getNetworkCapabilities(connectivityManager?.activeNetwork) ?: return false
|
||||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||||
|
|
|
@ -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
|
package partisan.weforge.xyz.pulse
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.role.RoleManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.provider.Settings
|
||||||
import android.view.Menu
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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 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() {
|
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 binding: ActivityMainBinding
|
||||||
private lateinit var prefs: Preferences
|
private lateinit var prefs: Preferences
|
||||||
|
private lateinit var window: PopupWindow
|
||||||
|
private var roleManager: RoleManager? = null
|
||||||
|
|
||||||
private var popupSwitch: SwitchMaterial? = null
|
private val registerForCallRedirectionRole =
|
||||||
private var popupMenuItem: MenuItem? = null
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
|
||||||
|
|
||||||
val popupToggle: SwitchMaterial
|
private val registerForGeneralPermissions =
|
||||||
get() = findViewById(R.id.globalPopupToggle)
|
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {}
|
||||||
|
|
||||||
|
private val registerForDrawOverlays =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
prefs = Preferences(this)
|
|
||||||
updateDonationIcon()
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
init()
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
window.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init() {
|
||||||
prefs = Preferences(this)
|
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(
|
private fun setup() {
|
||||||
this,
|
binding.apply {
|
||||||
binding.drawerLayout,
|
redirectionDelay.setLabelFormatter {
|
||||||
binding.topAppBar,
|
String.format("%.1f", it)
|
||||||
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
|
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()
|
val adapter = ServiceAdapter(
|
||||||
.replace(R.id.fragmentContainer, MainFragment())
|
context = this@MainActivity,
|
||||||
.commit()
|
services = services.toMutableList(),
|
||||||
|
onReordered = { updatedList ->
|
||||||
setupPopupToggle(false)
|
updatedList.forEachIndexed { index, entry ->
|
||||||
|
setServicePriority(entry.mimetype, index)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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.serviceConfigList.removeAllViews()
|
||||||
binding.topAppBar.title = parts.joinToString(" > ")
|
// 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 {
|
private fun hasPermissions(): Boolean {
|
||||||
return hasGeneralPermissions(this) &&
|
return hasGeneralPermissions() && hasDrawOverlays() && hasCallRedirectionRole()
|
||||||
hasDrawOverlays(this) &&
|
}
|
||||||
hasCallRedirectionRole(this)
|
|
||||||
|
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.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.PixelFormat
|
import android.graphics.PixelFormat
|
||||||
import android.graphics.Rect
|
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.ContextThemeWrapper
|
|
||||||
import android.widget.TextView
|
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.annotation.RequiresPermission
|
||||||
import androidx.core.content.res.use
|
|
||||||
import com.google.android.material.card.MaterialCardView
|
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.timerTask
|
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(
|
class PopupWindow(
|
||||||
ctx: Context,
|
private val ctx: Context,
|
||||||
private val service: WeakReference<CallRedirectionService>?,
|
private val service: WeakReference<CallRedirectionService>?,
|
||||||
) {
|
) {
|
||||||
private val themedCtx = DynamicColors.wrapContextIfAvailable(
|
private val prefs = Preferences(ctx)
|
||||||
ContextThemeWrapper(ctx, R.style.Theme_Pulse)
|
private val windowManager = ctx.getSystemService(WindowManager::class.java)
|
||||||
)
|
private val audioManager = ctx.getSystemService(AudioManager::class.java)
|
||||||
private val prefs = Preferences(themedCtx)
|
@Suppress("InflateParams")
|
||||||
private val windowManager = themedCtx.getSystemService(WindowManager::class.java)
|
private val view = LayoutInflater.from(ctx).inflate(R.layout.popup, null)
|
||||||
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 layoutParams = WindowManager.LayoutParams().apply {
|
private val layoutParams = WindowManager.LayoutParams().apply {
|
||||||
format = PixelFormat.TRANSLUCENT
|
format = PixelFormat.TRANSLUCENT
|
||||||
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||||
|
@ -55,9 +33,6 @@ class PopupWindow(
|
||||||
height = WindowManager.LayoutParams.WRAP_CONTENT
|
height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
y = prefs.popupPosition
|
y = prefs.popupPosition
|
||||||
}
|
}
|
||||||
private var currentEffect: PopupEffect = PopupEffect.NONE
|
|
||||||
private var matrixOverlay: View? = null
|
|
||||||
private var gamerAnimator: ValueAnimator? = null
|
|
||||||
private var timer: Timer? = null
|
private var timer: Timer? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -65,385 +40,85 @@ class PopupWindow(
|
||||||
cancel()
|
cancel()
|
||||||
service?.get()?.placeCallUnmodified()
|
service?.get()?.placeCallUnmodified()
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is utterly stupid, but it works
|
|
||||||
applyResolvedColors(view)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun preview(isLongPress: Boolean = false) {
|
fun preview() {
|
||||||
remove()
|
remove()
|
||||||
layoutParams.y = prefs.popupPosition
|
layoutParams.y = prefs.popupPosition
|
||||||
|
val destinations = mutableListOf(
|
||||||
val destinations = listOf(
|
|
||||||
R.string.destination_signal,
|
R.string.destination_signal,
|
||||||
R.string.destination_telegram,
|
R.string.destination_telegram,
|
||||||
R.string.destination_threema,
|
R.string.destination_threema,
|
||||||
// Whatsapp smells
|
|
||||||
)
|
)
|
||||||
setDescription(destinations.random())
|
setDescription(destinations.random())
|
||||||
add()
|
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) {
|
fun show(uri: Uri, destinationId: Int) {
|
||||||
val svc = service?.get() ?: return
|
val service = service?.get() ?: return
|
||||||
|
if (!remove()) {
|
||||||
|
service.placeCallUnmodified()
|
||||||
|
return
|
||||||
|
}
|
||||||
timer?.cancel()
|
timer?.cancel()
|
||||||
timer = Timer()
|
timer = Timer()
|
||||||
timer?.schedule(timerTask {
|
timer?.schedule(timerTask {
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
if (!remove()) {
|
if (!remove()) {
|
||||||
svc.placeCallUnmodified()
|
service.placeCallUnmodified()
|
||||||
return@post
|
return@timerTask
|
||||||
}
|
}
|
||||||
if (audioManager?.mode != AudioManager.MODE_IN_CALL) {
|
if (audioManager?.mode != AudioManager.MODE_IN_CALL) {
|
||||||
svc.placeCallUnmodified()
|
service.placeCallUnmodified()
|
||||||
return@post
|
return@timerTask
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
call(uri)
|
call(uri)
|
||||||
} catch (exc: SecurityException) {
|
} catch (exc: SecurityException) {
|
||||||
svc.placeCallUnmodified()
|
service.placeCallUnmodified()
|
||||||
return@post
|
return@timerTask
|
||||||
}
|
|
||||||
svc.cancelCall()
|
|
||||||
}
|
}
|
||||||
|
service.cancelCall()
|
||||||
}, prefs.redirectionDelay)
|
}, prefs.redirectionDelay)
|
||||||
|
|
||||||
layoutParams.y = prefs.popupPosition
|
|
||||||
setDescription(destinationId)
|
setDescription(destinationId)
|
||||||
startProgressAnimation(prefs.redirectionDelay)
|
|
||||||
|
|
||||||
if (!add()) {
|
if (!add()) {
|
||||||
Log.w("PopupWindow", "add() failed – popup not shown, calling directly.")
|
|
||||||
timer?.cancel()
|
timer?.cancel()
|
||||||
svc.placeCallUnmodified()
|
service.placeCallUnmodified()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setDescription(id: Int) {
|
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,
|
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)
|
@RequiresPermission(Manifest.permission.CALL_PHONE)
|
||||||
fun call(data: Uri) {
|
private fun call(data: Uri) {
|
||||||
Intent(Intent.ACTION_VIEW).apply {
|
Intent(Intent.ACTION_VIEW).let {
|
||||||
this.data = data
|
it.data = data
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
themedCtx.startActivity(this)
|
ctx.startActivity(it)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 add(): Boolean {
|
private fun add(): Boolean {
|
||||||
try {
|
try {
|
||||||
// If already attached, force remove and re-add
|
|
||||||
if (view.parent != null) {
|
|
||||||
windowManager?.removeViewImmediate(view)
|
|
||||||
}
|
|
||||||
view.animate().cancel()
|
|
||||||
windowManager?.addView(view, layoutParams)
|
windowManager?.addView(view, layoutParams)
|
||||||
animateAppear()
|
} catch (exc: WindowManager.BadTokenException) { return false }
|
||||||
} catch (exc: Exception) {
|
|
||||||
Log.e("PopupWindow", "Failed to add popup view", exc)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun remove(onRemoved: (() -> Unit)? = null): Boolean {
|
private fun remove(): Boolean {
|
||||||
return try {
|
|
||||||
animateDisappear {
|
|
||||||
try {
|
try {
|
||||||
windowManager?.removeView(view)
|
windowManager?.removeView(view)
|
||||||
matrixOverlay?.let {
|
} catch (exc: IllegalArgumentException) {
|
||||||
try {
|
} catch (exc: WindowManager.BadTokenException) { return false }
|
||||||
windowManager?.removeViewImmediate(it)
|
return true
|
||||||
} 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel() {
|
fun cancel() {
|
||||||
Log.d("PopupWindow", "Cancel called")
|
|
||||||
timer?.cancel()
|
timer?.cancel()
|
||||||
remove()
|
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.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
|
||||||
class Preferences(private val context: Context) {
|
class Preferences(ctx: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val ENABLED = "enabled"
|
||||||
private const val REDIRECTION_DELAY = "redirection_delay"
|
private const val REDIRECTION_DELAY = "redirection_delay"
|
||||||
private const val POPUP_POSITION = "popup_position_y"
|
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_REDIRECTION_DELAY = 2000L
|
||||||
private const val DEFAULT_POPUP_POSITION = 333
|
private const val DEFAULT_POPUP_POSITION = 333
|
||||||
|
|
||||||
|
// migration
|
||||||
private const val SERVICE_ENABLED = "service_enabled"
|
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 isEnabled: Boolean
|
||||||
var isServiceEnabledByUser: Boolean
|
get() = prefs.getBoolean(ENABLED, prefs.getBoolean(SERVICE_ENABLED, false))
|
||||||
get() = prefs.getBoolean(SERVICE_ENABLED, true)
|
set(value) = prefs.edit { putBoolean(ENABLED, value) }
|
||||||
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 redirectionDelay: Long
|
var redirectionDelay: Long
|
||||||
get() = prefs.getLong(REDIRECTION_DELAY, DEFAULT_REDIRECTION_DELAY)
|
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)
|
get() = prefs.getInt(POPUP_POSITION, DEFAULT_POPUP_POSITION)
|
||||||
set(value) = prefs.edit { putInt(POPUP_POSITION, value) }
|
set(value) = prefs.edit { putInt(POPUP_POSITION, value) }
|
||||||
|
|
||||||
var redirectOnWifi: Boolean
|
|
||||||
get() = prefs.getBoolean(REDIRECT_WIFI, true)
|
|
||||||
set(value) = prefs.edit { putBoolean(REDIRECT_WIFI, value) }
|
|
||||||
|
|
||||||
var redirectOnData: Boolean
|
|
||||||
get() = prefs.getBoolean(REDIRECT_DATA, true)
|
|
||||||
set(value) = prefs.edit { putBoolean(REDIRECT_DATA, value) }
|
|
||||||
|
|
||||||
var redirectInternationalOnly: Boolean
|
|
||||||
get() = prefs.getBoolean(REDIRECT_INTERNATIONAL, false)
|
|
||||||
set(value) = prefs.edit { putBoolean(REDIRECT_INTERNATIONAL, value) }
|
|
||||||
|
|
||||||
var redirectIfRoaming: Boolean
|
|
||||||
get() = prefs.getBoolean(REDIRECT_ROAMING, false)
|
|
||||||
set(value) = prefs.edit { putBoolean(REDIRECT_ROAMING, value) }
|
|
||||||
|
|
||||||
private fun makeKeyEnabled(mimetype: String) = "enabled_$mimetype"
|
private fun makeKeyEnabled(mimetype: String) = "enabled_$mimetype"
|
||||||
private fun makeKeyPriority(mimetype: String) = "priority_$mimetype"
|
private fun makeKeyPriority(mimetype: String) = "priority_$mimetype"
|
||||||
|
|
||||||
fun getAvailablePopupEffects(): List<PopupEffect> {
|
/** Whether this service is enabled */
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isServiceEnabled(mimetype: String): Boolean {
|
fun isServiceEnabled(mimetype: String): Boolean {
|
||||||
return prefs.getBoolean(makeKeyEnabled(mimetype), true)
|
return prefs.getBoolean(makeKeyEnabled(mimetype), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Current priority for this service (lower = higher priority) */
|
||||||
fun getServicePriority(mimetype: String): Int {
|
fun getServicePriority(mimetype: String): Int {
|
||||||
val order = getServiceOrder()
|
return prefs.getInt(makeKeyPriority(mimetype), Int.MAX_VALUE)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Enable or disable individual service */
|
||||||
fun setServiceEnabled(mimetype: String, enabled: Boolean) {
|
fun setServiceEnabled(mimetype: String, enabled: Boolean) {
|
||||||
prefs.edit().putBoolean(makeKeyEnabled(mimetype), enabled).apply()
|
prefs.edit().putBoolean(makeKeyEnabled(mimetype), enabled).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Change priority for an individual service */
|
||||||
fun setServicePriority(mimetype: String, priority: Int) {
|
fun setServicePriority(mimetype: String, priority: Int) {
|
||||||
prefs.edit().putInt(makeKeyPriority(mimetype), priority).apply()
|
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,
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
target: RecyclerView.ViewHolder
|
target: RecyclerView.ViewHolder
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val from = viewHolder.bindingAdapterPosition
|
val from = viewHolder.adapterPosition
|
||||||
val to = target.bindingAdapterPosition
|
val to = target.adapterPosition
|
||||||
services.add(to, services.removeAt(from))
|
services.add(to, services.removeAt(from))
|
||||||
notifyItemMoved(from, to)
|
notifyItemMoved(from, to)
|
||||||
return true
|
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"?>
|
<?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:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
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_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:padding="32dp"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<!-- Main content -->
|
<TextView
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
android:id="@+id/description"
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@+id/topAppBar"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/colorSurface"
|
android:text="@string/description"
|
||||||
app:titleTextColor="?attr/colorOnSurface"
|
|
||||||
app:navigationIconTint="?attr/colorOnSurface"
|
|
||||||
app:title="@string/app_name"
|
|
||||||
app:titleTextAppearance="@style/Toolbar.Title.Small"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
</com.google.android.material.appbar.MaterialToolbar>
|
|
||||||
|
|
||||||
<FrameLayout
|
<ScrollView
|
||||||
android:id="@+id/fragmentContainer"
|
android:id="@+id/scrollView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="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_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</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,19 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
app:cardCornerRadius="24dp"
|
app:cardCornerRadius="24dp"
|
||||||
app:cardElevation="4dp"
|
app:cardElevation="4dp"
|
||||||
android:padding="24dp"
|
|
||||||
app:cardBackgroundColor="?attr/colorSurface">
|
app:cardBackgroundColor="?attr/colorSurface">
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/description"
|
android:id="@+id/description"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center" />
|
||||||
tools:text="Popup text" />
|
|
||||||
|
|
||||||
<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>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
|
@ -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="destination_whatsapp">WhatsApp</string>
|
||||||
<string name="redirection_delay_description">Задержка до того, как звонок будет перенаправлен.</string>
|
<string name="redirection_delay_description">Задержка до того, как звонок будет перенаправлен.</string>
|
||||||
<string name="popup_position">Позиция всплывающего окна</string>
|
<string name="popup_position">Позиция всплывающего окна</string>
|
||||||
|
<string name="fallback">Обратная совместимость</string>
|
||||||
</resources>
|
</resources>
|
|
@ -14,6 +14,4 @@
|
||||||
<color name="colorOnSecondary">#000000</color>
|
<color name="colorOnSecondary">#000000</color>
|
||||||
|
|
||||||
<color name="launcher_background">@color/colorPrimary</color>
|
<color name="launcher_background">@color/colorPrimary</color>
|
||||||
|
|
||||||
<color name="colorSurfaceVariant">#2B3542</color>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,79 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Pulse</string>
|
<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="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_signal">Signal</string>
|
||||||
<string name="destination_telegram">Telegram</string>
|
<string name="destination_telegram">Telegram</string>
|
||||||
<string name="destination_threema">Threema</string>
|
<string name="destination_threema">Threema</string>
|
||||||
<string name="destination_whatsapp">WhatsApp</string>
|
<string name="destination_whatsapp">WhatsApp</string>
|
||||||
<string name="redirection_delay_description">The delay before a call will be redirected.</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="popup_position">Popup position</string>
|
||||||
<string name="activate_description">To start, grant the required permissions by tapping the Activate button.</string>
|
<string name="fallback">Fallback</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>
|
|
||||||
</resources>
|
</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. -->
|
<!-- Secondary brand color. -->
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
<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>
|
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</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 |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
@ -1,197 +1,58 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<svg
|
<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" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
|
||||||
fill="#000000"
|
<defs id="defs5">
|
||||||
version="1.1"
|
<radialGradient gradientUnits="userSpaceOnUse" cx="254.89999" cy="255.633" r="238.439" id="gradient-1" gradientTransform="matrix(0.229176, 0, 0, 0.229176, -20.258176, -20.511814)">
|
||||||
id="Layer_1"
|
<stop offset="0" style="stop-color: rgb(1, 5, 11);" id="stop2"/>
|
||||||
width="845.71301"
|
<stop offset="1" style="stop-color: rgb(11, 22, 36);" id="stop4"/>
|
||||||
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>
|
</radialGradient>
|
||||||
<bx:export>
|
<bx:export>
|
||||||
<bx:file
|
<bx:file format="png"/>
|
||||||
format="png" />
|
|
||||||
</bx:export>
|
</bx:export>
|
||||||
</defs>
|
</defs>
|
||||||
<ellipse
|
<ellipse style="fill: url("#gradient-1"); fill-rule: nonzero; stroke: rgb(0, 0, 0); stroke-width: 2.05476; paint-order: fill;" cx="38.161" cy="38.068" rx="54.643" ry="55.459" id="ellipse9"/>
|
||||||
style="fill:url(#gradient-1);fill-rule:nonzero;stroke:#000000;stroke-width:2.305;paint-order:fill"
|
<path d="M 65.078 28.536 C 65.061 28.577 64.972 28.97 64.878 29.426 C 64.779 29.871 64.694 30.245 64.685 30.254 C 64.67 30.266 64.287 30.353 63.827 30.452 C 63.367 30.541 62.969 30.64 62.95 30.659 C 62.931 30.681 63.311 30.789 63.803 30.888 L 64.694 31.074 L 64.769 31.401 C 64.808 31.589 64.89 31.963 64.947 32.249 C 65.008 32.536 65.073 32.794 65.092 32.813 C 65.111 32.833 65.21 32.447 65.316 31.972 C 65.42 31.481 65.502 31.086 65.509 31.086 C 65.514 31.074 65.909 30.997 66.386 30.888 C 66.863 30.789 67.265 30.7 67.275 30.691 C 67.304 30.659 67.195 30.63 66.34 30.452 C 65.894 30.344 65.523 30.266 65.514 30.254 C 65.502 30.245 65.41 29.84 65.299 29.346 C 65.196 28.864 65.097 28.498 65.078 28.536 L 65.078 28.536 Z" style="fill:#ffffff;fill-opacity:0.79;stroke-width:2.98704" id="path47">
|
||||||
cx="38.160999"
|
<title id="title45">Star</title>
|
||||||
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>
|
||||||
<path
|
<path d="m 10.244399,33.84917 c -0.0096,0.03856 -0.09878,0.452979 -0.197576,0.896322 -0.098784,0.455391 -0.1879397,0.840906 -0.1975767,0.850543 -0.011984,0.0096 -0.4047912,0.09878 -0.8698191,0.197576 -0.4626183,0.09878 -0.8698191,0.197578 -0.8890948,0.207215 -0.019271,0.01927 0.3662398,0.127706 0.8698191,0.236128 l 0.8987318,0.187939 0.079512,0.337326 c 0.038558,0.175892 0.1180707,0.561406 0.1783027,0.848133 0.05783,0.296365 0.127706,0.544541 0.146976,0.573455 0.01927,0.01927 0.118071,-0.36624 0.216852,-0.860182 0.108419,-0.481894 0.197578,-0.898732 0.197578,-0.898732 0.0096,0 0.414428,-0.08673 0.889095,-0.187939 0.484302,-0.09878 0.889093,-0.195167 0.898732,-0.207213 0.02891,-0.02892 -0.07951,-0.05783 -0.939693,-0.23613 -0.452981,-0.108418 -0.828858,-0.187937 -0.838497,-0.197576 -0.0096,-0.0096 -0.108418,-0.424067 -0.216852,-0.918007 -0.108434,-0.493943 -0.207215,-0.869819 -0.22649,-0.828858 z" style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.06584" id="path59">
|
||||||
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"
|
<title id="title57">Star</title>
|
||||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
|
||||||
id="path59">
|
|
||||||
<title
|
|
||||||
id="title57">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path d="M 44.625 9.659 C 44.596 9.726 44.446 10.417 44.28 11.179 C 44.111 11.948 43.972 12.591 43.955 12.61 C 43.923 12.629 43.273 12.779 42.483 12.957 C 41.692 13.114 41.01 13.282 40.972 13.311 C 40.941 13.34 41.594 13.518 42.432 13.697 L 43.972 14.022 L 44.102 14.586 C 44.162 14.892 44.299 15.542 44.408 16.036 C 44.507 16.53 44.615 16.966 44.654 17.005 C 44.685 17.034 44.851 16.383 45.029 15.552 C 45.208 14.723 45.357 14.032 45.364 14.032 C 45.376 14.022 46.056 13.885 46.868 13.697 C 47.687 13.528 48.379 13.371 48.398 13.35 C 48.446 13.302 48.261 13.253 46.798 12.945 C 46.027 12.779 45.396 12.629 45.376 12.61 C 45.357 12.591 45.198 11.899 45.01 11.051 C 44.832 10.22 44.654 9.589 44.625 9.659 Z" style="fill:#ffffff;stroke-width:2.97167" id="path103">
|
||||||
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"
|
<title id="title101">Star</title>
|
||||||
style="fill:#ffffff;stroke-width:3.33357"
|
|
||||||
id="path103">
|
|
||||||
<title
|
|
||||||
id="title101">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path d="M 27.098 54.888 C 27.079 54.953 26.944 55.553 26.797 56.227 C 26.652 56.902 26.524 57.475 26.505 57.485 C 26.488 57.502 25.903 57.639 25.211 57.786 C 24.52 57.931 23.917 58.075 23.888 58.104 C 23.852 58.131 24.435 58.287 25.175 58.442 L 26.524 58.733 L 26.633 59.225 C 26.698 59.49 26.816 60.073 26.907 60.502 C 26.997 60.938 27.091 61.311 27.124 61.347 C 27.153 61.376 27.298 60.803 27.454 60.073 C 27.618 59.343 27.746 58.733 27.756 58.733 C 27.756 58.733 28.356 58.605 29.076 58.442 C 29.796 58.297 30.396 58.15 30.416 58.14 C 30.461 58.095 30.298 58.049 29.011 57.777 C 28.339 57.63 27.772 57.502 27.756 57.485 C 27.746 57.475 27.599 56.856 27.435 56.119 C 27.281 55.379 27.124 54.832 27.098 54.888 L 27.098 54.888 Z" style="fill:#ffffff;stroke-width:3.43501" id="path103-3">
|
||||||
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"
|
<title id="title101-5">Star</title>
|
||||||
style="fill:#ffffff;stroke-width:3.85334"
|
|
||||||
id="path103-3">
|
|
||||||
<title
|
|
||||||
id="title101-5">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path d="M 34.627 47.061 C 34.618 47.092 34.557 47.379 34.487 47.694 C 34.42 48.01 34.36 48.278 34.35 48.285 C 34.34 48.297 34.063 48.355 33.738 48.425 C 33.413 48.494 33.136 48.562 33.116 48.574 C 33.107 48.581 33.372 48.661 33.728 48.731 L 34.36 48.87 L 34.41 49.097 C 34.439 49.224 34.499 49.502 34.538 49.699 C 34.577 49.906 34.627 50.085 34.637 50.094 C 34.656 50.114 34.726 49.836 34.796 49.502 C 34.873 49.155 34.933 48.87 34.933 48.87 C 34.943 48.87 35.22 48.81 35.555 48.731 C 35.89 48.661 36.176 48.593 36.186 48.593 C 36.208 48.574 36.128 48.552 35.526 48.425 C 35.21 48.345 34.943 48.297 34.943 48.285 C 34.933 48.278 34.863 47.991 34.784 47.644 C 34.716 47.299 34.637 47.032 34.627 47.061 L 34.627 47.061 Z" style="fill:#ffffff;fill-opacity:0.6;stroke-width:3.27941" id="path115">
|
||||||
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"
|
<title id="title113">Star</title>
|
||||||
style="fill:#ffffff;fill-opacity:0.6;stroke-width:3.67879"
|
|
||||||
id="path115">
|
|
||||||
<title
|
|
||||||
id="title113">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path d="M 54.953 53.341 C 54.934 53.408 54.784 54.071 54.628 54.81 C 54.459 55.553 54.322 56.184 54.303 56.193 C 54.281 56.215 53.65 56.362 52.879 56.521 C 52.117 56.678 51.457 56.846 51.428 56.875 C 51.389 56.904 52.03 57.073 52.84 57.251 L 54.322 57.557 L 54.449 58.099 C 54.51 58.396 54.647 59.039 54.736 59.514 C 54.835 59.986 54.943 60.4 54.982 60.441 C 55.013 60.47 55.177 59.839 55.351 59.039 C 55.522 58.239 55.661 57.567 55.671 57.567 C 55.678 57.557 56.336 57.417 57.126 57.251 C 57.917 57.083 58.582 56.926 58.599 56.914 C 58.647 56.866 58.466 56.817 57.052 56.521 C 56.312 56.352 55.695 56.215 55.678 56.193 C 55.661 56.184 55.505 55.504 55.324 54.692 C 55.153 53.883 54.982 53.28 54.953 53.341 L 54.953 53.341 Z" style="fill:#ffffff;fill-opacity:0.85;stroke-width:6.82757" id="path135">
|
||||||
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"
|
<title id="title133">Star</title>
|
||||||
style="fill:#ffffff;fill-opacity:0.85;stroke-width:7.65905"
|
|
||||||
id="path135">
|
|
||||||
<title
|
|
||||||
id="title133">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path d="M 26.728 8.816 C 26.699 8.908 26.494 9.826 26.28 10.845 C 26.046 11.867 25.858 12.737 25.829 12.756 C 25.803 12.782 24.921 12.98 23.863 13.214 C 22.808 13.428 21.897 13.655 21.851 13.693 C 21.805 13.739 22.685 13.973 23.808 14.216 L 25.858 14.645 L 26.027 15.397 C 26.121 15.809 26.297 16.678 26.439 17.341 C 26.569 17.999 26.718 18.57 26.766 18.625 C 26.812 18.671 27.039 17.792 27.282 16.686 C 27.516 15.573 27.711 14.657 27.721 14.657 C 27.73 14.645 28.639 14.45 29.732 14.216 C 30.819 13.982 31.744 13.768 31.764 13.749 C 31.841 13.674 31.588 13.606 29.631 13.204 C 28.61 12.97 27.759 12.782 27.73 12.756 C 27.711 12.737 27.497 11.802 27.244 10.676 C 27.01 9.563 26.776 8.722 26.728 8.816 Z" style="fill:#ffffff;stroke-width:4.39702" id="path143">
|
||||||
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"
|
<title id="title141">Star</title>
|
||||||
style="fill:#ffffff;stroke-width:4.9325"
|
|
||||||
id="path143">
|
|
||||||
<title
|
|
||||||
id="title141">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path d="M 60.382 36.087 C 60.356 36.157 60.194 36.887 60.021 37.697 C 59.842 38.507 59.688 39.188 59.671 39.208 C 59.645 39.227 58.953 39.386 58.115 39.564 C 57.279 39.743 56.558 39.918 56.522 39.95 C 56.486 39.979 57.18 40.167 58.069 40.352 L 59.688 40.699 L 59.825 41.292 C 59.895 41.617 60.04 42.309 60.148 42.834 C 60.257 43.354 60.372 43.8 60.409 43.851 C 60.445 43.889 60.625 43.188 60.813 42.318 C 61.004 41.441 61.155 40.709 61.165 40.709 C 61.172 40.699 61.893 40.543 62.758 40.352 C 63.62 40.176 64.35 40.008 64.367 39.988 C 64.423 39.938 64.223 39.88 62.676 39.552 C 61.866 39.376 61.192 39.227 61.172 39.208 C 61.155 39.188 60.984 38.449 60.787 37.56 C 60.596 36.68 60.418 36.008 60.382 36.087 L 60.382 36.087 Z" style="fill:#ffffff;fill-opacity:0.85;stroke-width:3.37663" id="path163">
|
||||||
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"
|
<title id="title161">Star</title>
|
||||||
style="fill:#ffffff;fill-opacity:0.85;stroke-width:3.78785"
|
|
||||||
id="path163">
|
|
||||||
<title
|
|
||||||
id="title161">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path d="M 18.185 28.173 C 18.175 28.211 18.086 28.626 17.987 29.071 C 17.888 29.524 17.799 29.91 17.79 29.92 C 17.78 29.929 17.394 30.018 16.922 30.117 C 16.457 30.216 16.052 30.315 16.033 30.324 C 16.021 30.344 16.407 30.454 16.9 30.563 L 17.799 30.751 L 17.879 31.076 C 17.917 31.262 17.997 31.647 18.057 31.934 C 18.115 32.23 18.185 32.478 18.204 32.507 C 18.226 32.527 18.324 32.143 18.433 31.647 C 18.532 31.165 18.621 30.758 18.628 30.758 C 18.628 30.751 19.035 30.662 19.508 30.563 C 19.992 30.462 20.397 30.365 20.406 30.356 C 20.435 30.324 20.327 30.295 19.469 30.117 C 19.014 30.009 18.64 29.929 18.628 29.92 C 18.618 29.91 18.52 29.495 18.411 29.002 C 18.312 28.508 18.214 28.134 18.185 28.173 Z" style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.06584" id="path-1">
|
||||||
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"
|
<title id="bx-title-1">Star</title>
|
||||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
|
||||||
id="path-1">
|
|
||||||
<title
|
|
||||||
id="bx-title-1">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path d="m 42.063863,25.177485 c -0.0096,0.02892 -0.06988,0.315642 -0.137341,0.631282 -0.06988,0.31564 -0.127706,0.583092 -0.139754,0.583092 -0.0096,0.0096 -0.274679,0.07951 -0.612004,0.146976 -0.325279,0.06988 -0.602369,0.139754 -0.621645,0.14939 -0.0096,0.0096 0.255405,0.08915 0.612006,0.159025 l 0.633691,0.137341 0.04819,0.22649 c 0.02891,0.127706 0.08915,0.395153 0.127705,0.602368 0.04095,0.207213 0.08915,0.375876 0.09878,0.395152 0.02168,0.01927 0.08915,-0.257813 0.159025,-0.59273 0.07951,-0.344553 0.137341,-0.63128 0.137341,-0.63128 0.0096,0 0.286727,-0.06024 0.621643,-0.137341 0.337327,-0.06988 0.624053,-0.139754 0.633691,-0.14939 0.01927,-0.01927 -0.06024,-0.03856 -0.662604,-0.168663 -0.315642,-0.06746 -0.583092,-0.127706 -0.583092,-0.137342 -0.0096,0 -0.0771,-0.286727 -0.156612,-0.640918 -0.06988,-0.346964 -0.149389,-0.602368 -0.159025,-0.573455 z" style="fill:#ffffff;fill-opacity:0.6;stroke-width:3.27941" id="path-2">
|
||||||
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"
|
<title id="bx-title-2">Star</title>
|
||||||
style="fill:#ffffff;fill-opacity:0.6;stroke-width:3.67879"
|
|
||||||
id="path-2">
|
|
||||||
<title
|
|
||||||
id="bx-title-2">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<g
|
<g transform="matrix(0.64127569,0,0,0.64127569,14.479115,12.458945)" id="g32">
|
||||||
transform="matrix(0.71937225,0,0,0.71937225,11.595061,9.3401941)"
|
<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"/>
|
||||||
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>
|
||||||
<g
|
<g transform="matrix(0.64127569,0,0,0.64127569,13.741615,11.277414)" id="g36">
|
||||||
transform="matrix(0.71937225,0,0,0.71937225,10.767746,8.0147726)"
|
<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"/>
|
||||||
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>
|
</g>
|
||||||
<path
|
<path d="M 15.187 18.329 C 15.177 18.367 15.088 18.782 14.989 19.227 C 14.89 19.68 14.801 20.066 14.792 20.076 C 14.782 20.085 14.396 20.174 13.924 20.273 C 13.459 20.372 13.054 20.471 13.035 20.48 C 13.023 20.5 13.409 20.61 13.902 20.719 L 14.801 20.907 L 14.881 21.232 C 14.919 21.418 14.999 21.803 15.059 22.09 C 15.117 22.386 15.187 22.634 15.206 22.663 C 15.228 22.683 15.326 22.299 15.435 21.803 C 15.534 21.321 15.623 20.914 15.63 20.914 C 15.63 20.907 16.037 20.818 16.51 20.719 C 16.994 20.618 17.399 20.521 17.408 20.512 C 17.437 20.48 17.329 20.451 16.471 20.273 C 16.016 20.165 15.642 20.085 15.63 20.076 C 15.62 20.066 15.522 19.651 15.413 19.158 C 15.314 18.664 15.216 18.29 15.187 18.329 Z" style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.06584" id="path-3">
|
||||||
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"
|
<title id="bx:title-1">Star</title>
|
||||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
|
||||||
id="path-3">
|
|
||||||
<title
|
|
||||||
id="bx:title-1">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path d="M 8.77 53.715 C 8.761 53.748 8.684 54.109 8.598 54.496 C 8.511 54.89 8.434 55.226 8.426 55.235 C 8.418 55.242 8.082 55.32 7.671 55.406 C 7.267 55.492 6.915 55.578 6.898 55.586 C 6.888 55.603 7.224 55.699 7.652 55.794 L 8.434 55.957 L 8.504 56.24 C 8.537 56.402 8.606 56.736 8.658 56.986 C 8.709 57.243 8.77 57.459 8.786 57.484 C 8.805 57.502 8.891 57.168 8.985 56.736 C 9.071 56.317 9.149 55.963 9.155 55.963 C 9.155 55.957 9.509 55.88 9.92 55.794 C 10.341 55.706 10.693 55.622 10.701 55.614 C 10.726 55.586 10.632 55.561 9.886 55.406 C 9.491 55.312 9.165 55.242 9.155 55.235 C 9.146 55.226 9.061 54.865 8.966 54.436 C 8.88 54.007 8.795 53.682 8.77 53.715 Z" style="fill: rgb(255, 255, 255); fill-opacity: 0.76; stroke-width: 6.06584;" id="path-4">
|
||||||
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"
|
<title id="bx:title-2">Star</title>
|
||||||
style="fill:#ffffff;fill-opacity:0.76;stroke-width:6.80456"
|
|
||||||
id="path-4">
|
|
||||||
<title
|
|
||||||
id="bx:title-2">Star</title>
|
|
||||||
</path>
|
</path>
|
||||||
<path
|
<path d="M 66 14.042 C 65.993 14.067 65.933 14.347 65.867 14.647 C 65.799 14.952 65.74 15.213 65.733 15.22 C 65.727 15.225 65.467 15.286 65.148 15.352 C 64.835 15.419 64.562 15.486 64.549 15.492 C 64.541 15.505 64.802 15.579 65.133 15.653 L 65.74 15.779 L 65.794 15.999 C 65.819 16.124 65.873 16.383 65.913 16.577 C 65.953 16.776 66 16.944 66.012 16.963 C 66.027 16.977 66.094 16.718 66.167 16.383 C 66.233 16.058 66.294 15.784 66.298 15.784 C 66.298 15.779 66.573 15.72 66.891 15.653 C 67.218 15.585 67.49 15.52 67.497 15.514 C 67.516 15.492 67.443 15.472 66.865 15.352 C 66.559 15.279 66.306 15.225 66.298 15.22 C 66.291 15.213 66.226 14.933 66.152 14.601 C 66.085 14.268 66.019 14.016 66 14.042 Z" style="fill: rgb(255, 255, 255); fill-opacity: 0.76; stroke-width: 6.06584;" id="path-5">
|
||||||
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"
|
<title id="bx:title-3">Star</title>
|
||||||
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>
|
</path>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 15 KiB |
BIN
data/screenshot-redirecting.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
data/screenshot.png
Normal file
After Width: | Height: | Size: 126 KiB |
|
@ -1,5 +1,2 @@
|
||||||
v1.3.0
|
Forked from Red and renamed to Pulse.
|
||||||
- Forked from Red and renamed to Pulse.
|
Changed Icons and graphic.
|
||||||
- Changed Icons and graphic.
|
|
||||||
- Added material you icon.
|
|
||||||
- Added options to toggle and change priority to individual redirect services.
|
|
|
@ -1,2 +1 @@
|
||||||
v1.3.1
|
Added material you icon.
|
||||||
- Updated metadata and removed some background Google BLOB to improve compliance with IzzyOnDroid repo.
|
|
|
@ -1,2 +1 @@
|
||||||
v1.3.2
|
Added options to toggle and change priority to individual redirect services.
|
||||||
- Fixed crash related to redirect popup
|
|
|
@ -1,3 +1 @@
|
||||||
v1.4.0
|
Updated metadata and removed some background Google BLOB to improve compliance with IzzyOnDroid repo.
|
||||||
- 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/Whatsapp 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
|
All permissions are mandatory.
|
||||||
- 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
|
|
||||||
|
|
||||||
**Supports:**
|
It is Free Open Source Software.
|
||||||
|
License: GPL-3
|
||||||
- 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
|
|
||||||
|
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 163 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 88 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/Whatsapp
|
||||||
|
|
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/Whatsapp 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/Whatsapp
|
1
fastlane/metadata/android/fr-FR/title.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Pulse
|
16
fastlane/metadata/android/ru-RU/full_description.txt
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
Мини приложение для перенаправления исходящих вызовов в Signal/Telegram/Threema/Whatsapp.
|
||||||
|
|
||||||
|
Вы можете отменить перенаправление, кликнув на всплывающее сообщение "Перенаправление в..".
|
||||||
|
|
||||||
|
Разрешения:
|
||||||
|
* `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/Whatsapp
|
1
fastlane/metadata/android/ru-RU/title.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Pulse
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,7 +1,5 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
49
gradlew
vendored
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015 the original authors.
|
# Copyright © 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with 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
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
|
@ -57,7 +55,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (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.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
@ -82,11 +80,13 @@ do
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# This is normally unused
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
# shellcheck disable=SC2034
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
APP_BASE_NAME=${0##*/}
|
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.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
@ -114,7 +114,7 @@ case "$( uname )" in #(
|
||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH="\\\"\\\""
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
|
@ -133,29 +133,22 @@ location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD=java
|
||||||
if ! command -v java >/dev/null 2>&1
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
then
|
|
||||||
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
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 ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
'' | soft) :;; #(
|
'' | 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" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
|
@ -200,28 +193,18 @@ if "$cygwin" || "$msys" ; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
# Collect all arguments for the java command:
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
# * 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.
|
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-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.
|
# Use "xargs" to parse quoted args.
|
||||||
#
|
#
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
|
41
gradlew.bat
vendored
|
@ -13,10 +13,8 @@
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%" == "" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
|
@ -27,8 +25,7 @@
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@ -43,13 +40,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -59,34 +56,32 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@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
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
exit /b 1
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|