General fixes, UI cleanup and popup animations

This commit is contained in:
partisan 2025-05-17 20:43:06 +02:00
parent e810208a14
commit 6d9024a580
20 changed files with 457 additions and 77 deletions

View file

@ -25,6 +25,13 @@ class AboutFragment : Fragment() {
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)

View file

@ -63,9 +63,16 @@ class CallRedirectionService : CallRedirectionService() {
return
}
val phoneNumber = handle.schemeSpecificPart
if (prefs.isBlacklistEnabled && !prefs.isContactWhitelisted(phoneNumber)) {
placeCallUnmodified()
return
}
val records: Array<Record>
try {
records = getRecordsFromPhoneNumber(handle.schemeSpecificPart)
records = getRecordsFromPhoneNumber(phoneNumber)
} catch (exc: SecurityException) {
placeCallUnmodified()
return

View file

@ -26,6 +26,27 @@ class ContactsFragment : Fragment() {
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)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View file

@ -2,6 +2,9 @@ package partisan.weforge.xyz.pulse
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import partisan.weforge.xyz.pulse.databinding.ActivityMainBinding
import androidx.appcompat.app.ActionBarDrawerToggle
@ -9,17 +12,24 @@ 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() {
private lateinit var binding: ActivityMainBinding
private lateinit var prefs: Preferences
private var popupSwitch: SwitchMaterial? = null
private var popupMenuItem: MenuItem? = null
val popupToggle: SwitchMaterial
get() = findViewById(R.id.globalPopupToggle)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
prefs = Preferences(this)
setSupportActionBar(binding.topAppBar)
val drawerToggle = ActionBarDrawerToggle(
@ -27,7 +37,7 @@ class MainActivity : AppCompatActivity() {
binding.drawerLayout,
binding.topAppBar,
R.string.navigation_drawer_open,
R.string.navigation_drawer_close
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
)
binding.drawerLayout.addDrawerListener(drawerToggle)
drawerToggle.syncState()
@ -36,6 +46,8 @@ class MainActivity : AppCompatActivity() {
.replace(R.id.fragmentContainer, MainFragment())
.commit()
setupPopupToggle(false)
binding.navigationView.setNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.action_popup_settings -> {
@ -73,6 +85,33 @@ class MainActivity : AppCompatActivity() {
}
}
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)
}
}
}
fun setAppBarTitle(vararg parts: String) {
binding.topAppBar.title = parts.joinToString(" > ")
}
private fun hasPermissions(): Boolean {
return hasGeneralPermissions(this) &&
hasDrawOverlays(this) &&

View file

@ -18,6 +18,11 @@ 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?,

View file

@ -0,0 +1,48 @@
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)
}
}

View file

@ -7,6 +7,10 @@ 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 androidx.fragment.app.Fragment
import androidx.core.content.getSystemService
import partisan.weforge.xyz.pulse.databinding.FragmentPopupSettingsBinding
@ -28,20 +32,35 @@ class PopupSettingsFragment : Fragment() {
return binding.root
}
override fun onResume() {
super.onResume()
(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.popupEnabledCheckbox.isChecked = prefs.popupEnabled
binding.popupEnabledCheckbox.setOnCheckedChangeListener { _, isChecked ->
prefs.popupEnabled = isChecked
updateControls(isChecked)
}
binding.popupPreview.setOnClickListener {
window.preview()
window.preview(false)
}
binding.popupPreview.setOnLongClickListener {
window.preview(true)
true
}
binding.redirectionDelay.value = (prefs.redirectionDelay / 1000).toFloat()
@ -52,6 +71,22 @@ class PopupSettingsFragment : Fragment() {
prefs.redirectionDelay = (value * 1000).toLong()
}
val effectNames = resources.getStringArray(R.array.popup_effects)
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, effectNames)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.popupEffectSpinner.adapter = adapter
// Select current setting
binding.popupEffectSpinner.setSelection(prefs.popupEffect.ordinal)
binding.popupEffectSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
prefs.popupEffect = Preferences.PopupEffect.values()[position]
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
val screenHeight = getScreenHeightPx()
binding.popupHeightSlider.valueFrom = 0f
binding.popupHeightSlider.valueTo = screenHeight.toFloat()
@ -67,6 +102,8 @@ class PopupSettingsFragment : Fragment() {
binding.redirectionDelay.isEnabled = enabled
binding.popupHeightSlider.isEnabled = enabled
binding.popupPreview.isEnabled = enabled
binding.popupEffectSpinner.isEnabled = enabled
binding.popupEffectLabel.isEnabled = enabled
}
private fun getScreenHeightPx(): Int {

View file

@ -5,15 +5,19 @@ import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.PixelFormat
import android.graphics.Rect
import android.media.AudioManager
import android.net.Uri
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.view.ContextThemeWrapper
import android.widget.TextView
import android.animation.ObjectAnimator
import android.widget.ProgressBar
import android.view.animation.OvershootInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.annotation.RequiresPermission
import androidx.core.content.res.use
import com.google.android.material.card.MaterialCardView
@ -24,6 +28,8 @@ import android.util.Log
import android.content.res.ColorStateList
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.MaterialColors
import partisan.weforge.xyz.pulse.Preferences.PopupEffect
import partisan.weforge.xyz.pulse.MatrixRainView
class PopupWindow(
ctx: Context,
@ -46,7 +52,9 @@ class PopupWindow(
height = WindowManager.LayoutParams.WRAP_CONTENT
y = prefs.popupPosition
}
private var timer: Timer? = null
private var currentEffect: PopupEffect = PopupEffect.NONE
private var matrixOverlay: View? = null
private var timer: Timer? = null
init {
view.setOnClickListener {
@ -58,16 +66,25 @@ class PopupWindow(
applyResolvedColors(view)
}
fun preview() {
fun preview(isLongPress: Boolean = false) {
remove()
layoutParams.y = prefs.popupPosition
val destinations = listOf(
R.string.destination_signal,
R.string.destination_telegram,
R.string.destination_threema,
// Whatsapp smells
)
setDescription(destinations.random())
add()
val duration = if (isLongPress) prefs.redirectionDelay * 5 else prefs.redirectionDelay
timer?.cancel()
timer = Timer()
timer?.schedule(timerTask {
remove()
}, duration)
}
fun show(uri: Uri, destinationId: Int) {
@ -128,10 +145,125 @@ class PopupWindow(
}
}
private fun animateAppear() {
view.animate().cancel()
view.rotationX = 0f
view.scaleX = 1f
view.scaleY = 1f
view.alpha = 1f
val effect = when (prefs.popupEffect) {
PopupEffect.RANDOM -> PopupEffect.values().filter { it != PopupEffect.RANDOM && it != PopupEffect.NONE }.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.scaleX = 0f
view.scaleY = 0f
view.animate().scaleX(1f).scaleY(1f).setDuration(300).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()
}
else -> {}
}
}
private fun animateDisappear(onEnd: () -> Unit) {
when (currentEffect) {
PopupEffect.NONE -> onEnd()
PopupEffect.FADE -> view.animate().alpha(0f).setDuration(200).withEndAction(onEnd).start()
PopupEffect.SCALE, PopupEffect.BOUNCE -> view.animate().scaleX(0f).scaleY(0f).setDuration(200).withEndAction(onEnd).start()
PopupEffect.FLOP -> view.animate().rotationX(90f).alpha(0f).setDuration(200).withEndAction(onEnd).start()
PopupEffect.MATRIX -> {
view.animate().alpha(0f).setDuration(150).withEndAction(onEnd).start()
matrixOverlay?.let { overlay ->
overlay.animate().cancel()
overlay.animate().alpha(0f).setDuration(150).withEndAction {
try {
windowManager?.removeViewImmediate(overlay)
} catch (_: Exception) {}
matrixOverlay = null
}.start()
}
}
else -> onEnd()
}
}
private fun add(): Boolean {
try {
// If already attached, force remove and re-add
if (view.parent != null) {
windowManager?.removeViewImmediate(view)
}
view.animate().cancel()
windowManager?.addView(view, layoutParams)
} catch (exc: WindowManager.BadTokenException) {
animateAppear()
} catch (exc: Exception) {
Log.e("PopupWindow", "Failed to add popup view", exc)
return false
}
return true
@ -139,9 +271,18 @@ class PopupWindow(
private fun remove(): Boolean {
try {
windowManager?.removeView(view)
} catch (_: IllegalArgumentException) {
} catch (_: WindowManager.BadTokenException) {
animateDisappear {
try {
windowManager?.removeView(view)
matrixOverlay?.let {
try {
windowManager?.removeViewImmediate(it)
} catch (_: Exception) {}
matrixOverlay = null
}
} catch (_: Exception) {}
}
} catch (_: Exception) {
return false
}
return true

View file

@ -10,7 +10,9 @@ class Preferences(private val context: Context) {
private const val REDIRECTION_DELAY = "redirection_delay"
private const val POPUP_POSITION = "popup_position_y"
private const val POPUP_ENABLED = "popup_enabled"
private val POPUP_EFFECT = "popup_effect"
private const val BLACKLISTED_CONTACTS = "blacklisted_contacts"
private const val BLACKLIST_ENABLED = "blacklist_enabled"
private val SERVICE_ORDER_KEY = "service_order"
private const val DEFAULT_REDIRECTION_DELAY = 2000L
@ -32,6 +34,29 @@ class Preferences(private val context: Context) {
hasDrawOverlays(context) &&
hasCallRedirectionRole(context)
enum class PopupEffect {
NONE, FADE, SCALE, BOUNCE, FLOP, MATRIX, 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 popupEnabled: Boolean
get() = prefs.getBoolean(POPUP_ENABLED, true)
set(value) = prefs.edit { putBoolean(POPUP_ENABLED, value) }

View file

@ -23,6 +23,14 @@ class ServiceSettingsFragment : Fragment() {
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)

View file

@ -19,9 +19,11 @@
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_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintEnd_toEndOf="parent">
</com.google.android.material.appbar.MaterialToolbar>
<FrameLayout
android:id="@+id/fragmentContainer"

View file

@ -5,44 +5,24 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp"
tools:context=".PopupSettingsFragment">
android:padding="32dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Delay label -->
<TextView
android:id="@+id/description"
android:id="@+id/delayDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/popup_settings_description"
android:textSize="16sp"
android:text="@string/redirection_delay_description"
android:textSize="12sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<CheckBox
android:id="@+id/popupEnabledCheckbox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/popup_enabled"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/popupPreview"
android:layout_marginTop="16dp" />
<Button
android:id="@+id/popupPreview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/test"
app:layout_constraintTop_toTopOf="@id/popupEnabledCheckbox"
app:layout_constraintBottom_toBottomOf="@id/popupEnabledCheckbox"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="8dp" />
<!-- Delay slider -->
<com.google.android.material.slider.Slider
android:id="@+id/redirectionDelay"
android:layout_width="0dp"
@ -51,22 +31,24 @@
android:valueFrom="2"
android:valueTo="4"
android:contentDescription="@string/redirection_delay_description"
app:layout_constraintTop_toBottomOf="@id/popupEnabledCheckbox"
app:layout_constraintTop_toBottomOf="@id/delayDescription"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp" />
android:layout_marginTop="8dp" />
<!-- Position label -->
<TextView
android:id="@+id/delayDescription"
android:id="@+id/heightDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/redirection_delay_description"
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="4dp" />
android:layout_marginTop="16dp" />
<!-- Position slider -->
<com.google.android.material.slider.Slider
android:id="@+id/popupHeightSlider"
android:layout_width="0dp"
@ -75,21 +57,43 @@
android:valueTo="100"
android:stepSize="1"
android:contentDescription="@string/popup_position"
app:layout_constraintTop_toBottomOf="@id/delayDescription"
app:layout_constraintTop_toBottomOf="@id/heightDescription"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp" />
android:layout_marginTop="8dp" />
<!-- Animation label -->
<TextView
android:id="@+id/heightDescription"
android:id="@+id/popupEffectLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/popup_position"
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="4dp" />
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>

View file

@ -6,23 +6,25 @@
android:layout_height="match_parent"
android:padding="16dp">
<TextView
android:id="@+id/serviceHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/service_settings_title"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginBottom="8dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/serviceRecycler"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbars="vertical"
app:layout_constraintTop_toBottomOf="@id/serviceHeader"
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" />

View file

@ -5,8 +5,11 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
app:cardCornerRadius="24dp"
app:cardElevation="4dp"
android:padding="24dp"
app:cardBackgroundColor="?attr/colorSurface">
<LinearLayout

View file

@ -0,0 +1,7 @@
<?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" />

View file

@ -4,32 +4,32 @@
<!-- Settings section -->
<item
android:id="@+id/section_settings"
android:title="Settings"
android:title="@string/settings_name"
android:enabled="false" />
<item
android:id="@+id/action_contacts"
android:title="Contacts"
android:title="@string/whitelist_name"
android:icon="@drawable/group_24px"
app:showAsAction="never" />
<item
android:id="@+id/action_popup_settings"
android:title="Popup"
android:title="@string/popup_name"
android:icon="@drawable/tooltip_24px"
app:showAsAction="never" />
<item
android:id="@+id/action_services"
android:title="Services"
android:title="@string/services_name"
android:icon="@drawable/services_24"
app:showAsAction="never" />
<!-- About section -->
<item
android:id="@+id/section_about"
android:title="About"
android:title="@string/about_name"
android:enabled="false" />
<item
android:id="@+id/action_about"
android:title="About"
android:title="@string/about_name"
android:icon="@drawable/info_24px"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,7 @@
<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>

View file

@ -9,5 +9,4 @@
<string name="destination_whatsapp">WhatsApp</string>
<string name="redirection_delay_description">Задержка до того, как звонок будет перенаправлен.</string>
<string name="popup_position">Позиция всплывающего окна</string>
<string name="fallback">Обратная совместимость</string>
</resources>

View file

@ -3,21 +3,36 @@
<string name="app_name">Pulse</string>
<string name="description">Redirects outgoing calls to E2EE apps if available.</string>
<string name="popup">Redirecting to %1$s</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="tools_name">Tools</string>
<string name="about_name">About</string>
<string name="donate_name">Donate</string>
<string name="destination_signal">Signal</string>
<string name="destination_telegram">Telegram</string>
<string name="destination_threema">Threema</string>
<string name="destination_whatsapp">WhatsApp</string>
<string name="redirection_delay_description">The delay before a call will be redirected.</string>
<string name="services_desc">Here you can enable or disable redirection to individual services and change their priority by dragging them. Redirection will be handled in order from top to bottom.</string>
<string name="popup_position">Popup position</string>
<string name="fallback">Fallback</string>
<string name="activate_description">To start, grant the required permissions by tapping the Activate button.</string>
<string name="service_settings_title">Service Preferences</string>
<string name="activate">Activate</string>
<string name="navigation_drawer_open">Open menu</string>
<string name="navigation_drawer_close">Close menu</string>
<string name="popup_settings_description">Configure popup behavior, position, and delay.</string>
<string name="popup_enabled">Popup enabled</string>
<string name="test">Test</string>
<string name="source_code">Source Code</string>
<string name="license">License</string>
<string name="popup_effect_label">Popup Animation</string>
<string-array name="popup_effects">
<item>None</item>
<item>Fade</item>
<item>Scale</item>
<item>Bounce</item>
<item>Flop</item>
<item>Matrix</item>
<item>Random</item>
</string-array>
</resources>

View file

@ -4,4 +4,7 @@
<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>