diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/AboutFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/AboutFragment.kt index 48ec0a1..e312052 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/AboutFragment.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/AboutFragment.kt @@ -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) diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt b/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt index 105157b..5f7a9b3 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt @@ -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 diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/ContactsFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/ContactsFragment.kt index d7d9822..2244501 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/ContactsFragment.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/ContactsFragment.kt @@ -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) diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt b/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt index 8a281be..a291b4e 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt @@ -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) && diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt index 90b53ca..9e8c3b4 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt @@ -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?, diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/MatrixRainView.kt b/app/src/main/java/partisan/weforge/xyz/pulse/MatrixRainView.kt new file mode 100644 index 0000000..01b61c4 --- /dev/null +++ b/app/src/main/java/partisan/weforge/xyz/pulse/MatrixRainView.kt @@ -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) + } +} diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt index 6bd8d97..29eda70 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt @@ -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 { diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/PopupWindow.kt b/app/src/main/java/partisan/weforge/xyz/pulse/PopupWindow.kt index fa81ca6..d3059ec 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/PopupWindow.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/PopupWindow.kt @@ -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 diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt b/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt index d9b87a8..bad91c5 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt @@ -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) } diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/ServicesFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/ServicesFragment.kt index 2d82657..add151a 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/ServicesFragment.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/ServicesFragment.kt @@ -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) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 36fed6b..c0ed222 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -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" diff --git a/app/src/main/res/layout/fragment_popup_settings.xml b/app/src/main/res/layout/fragment_popup_settings.xml index e11fade..d9fb419 100644 --- a/app/src/main/res/layout/fragment_popup_settings.xml +++ b/app/src/main/res/layout/fragment_popup_settings.xml @@ -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> diff --git a/app/src/main/res/layout/fragment_service_settings.xml b/app/src/main/res/layout/fragment_service_settings.xml index a5dce49..03da59f 100644 --- a/app/src/main/res/layout/fragment_service_settings.xml +++ b/app/src/main/res/layout/fragment_service_settings.xml @@ -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" /> diff --git a/app/src/main/res/layout/popup.xml b/app/src/main/res/layout/popup.xml index 1031757..1d401d5 100644 --- a/app/src/main/res/layout/popup.xml +++ b/app/src/main/res/layout/popup.xml @@ -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 diff --git a/app/src/main/res/layout/switch_item.xml b/app/src/main/res/layout/switch_item.xml new file mode 100644 index 0000000..ece0273 --- /dev/null +++ b/app/src/main/res/layout/switch_item.xml @@ -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" /> diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml index 8cd0dd4..eb960d5 100644 --- a/app/src/main/res/menu/main_menu.xml +++ b/app/src/main/res/menu/main_menu.xml @@ -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> diff --git a/app/src/main/res/menu/topbar_toggle.xml b/app/src/main/res/menu/topbar_toggle.xml new file mode 100644 index 0000000..157a5d9 --- /dev/null +++ b/app/src/main/res/menu/topbar_toggle.xml @@ -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> diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4caa5d3..ba94153 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -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> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2fdb72e..e5e8e6c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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> \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index eabcdff..1464804 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -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>