diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6cd18bb..a4b46a5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/CircularNavFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/CircularNavFragment.kt
index cb226f0..345efd5 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/CircularNavFragment.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/CircularNavFragment.kt
@@ -4,10 +4,13 @@ import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.os.Bundle
+import android.os.VibrationEffect
+import android.os.Vibrator
import android.util.AttributeSet
import android.util.TypedValue
import android.view.*
import android.view.animation.DecelerateInterpolator
+import android.view.animation.LinearInterpolator
import android.animation.AnimatorListenerAdapter
import android.animation.Animator
import android.widget.FrameLayout
@@ -70,8 +73,9 @@ class CircularNavFragment : Fragment() {
accentColor = colorPrimary
surfaceColor = colorSurface
labelColor = colorOnSurface
- onItemClick = { navId -> (activity as? HomeActivity)?.navigateTo(navId) }
- onCenterClick = { (activity as? HomeActivity)?.lockApp() }
+ onItemClick = { navId -> (activity as? HomeActivity)?.navigateTo(navId) }
+ onCenterClick = { /* unused: tap on unlocked center locks the wheel */ }
+ onWheelCenterLockedTap = { (activity as? HomeActivity)?.notifyWheelLockTap() }
}
wheelContainer.addView(wheelView)
@@ -133,6 +137,10 @@ class CircularNavFragment : Fragment() {
toolbar.findViewWithTag("wheel_title")?.let { toolbar.removeView(it) }
requireActivity().invalidateOptionsMenu()
}
+
+ fun unlockWheelLock() {
+ wheelView?.unlockWheel()
+ }
}
// ---------------------------------------------------------------------------
@@ -169,8 +177,12 @@ class CircularWheelView @JvmOverloads constructor(
var labelColor: Int = Color.DKGRAY
set(value) { field = value; invalidate() }
+ var isWheelLocked = false
+ set(value) { field = value; invalidate() }
+
var onItemClick: ((Int) -> Unit)? = null
var onCenterClick: (() -> Unit)? = null
+ var onWheelCenterLockedTap: (() -> Unit)? = null
// ---- geometry ---------------------------------------------------------
@@ -195,6 +207,10 @@ class CircularWheelView @JvmOverloads constructor(
private var iconBitmaps: Array = emptyArray()
private var centerBitmap: Bitmap? = null
+ private var centerUnlockedBitmap: Bitmap? = null
+ private val grayFilter = ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(0f) })
+ private var lockShakeAngle = 0f
+ private var shakeAnimator: ValueAnimator? = null
// ---- touch & fling ----------------------------------------------------
@@ -244,7 +260,8 @@ class CircularWheelView @JvmOverloads constructor(
iconBitmaps[i] = tintedBitmap(item.iconRes, accentColor, iconPx)
}
val centerPx = (innerRadius * 0.64f).toInt().coerceAtLeast(1)
- centerBitmap = tintedBitmap(R.drawable.ic_lock, accentColor, centerPx)
+ centerBitmap = tintedBitmap(R.drawable.ic_lock, accentColor, centerPx)
+ centerUnlockedBitmap = tintedBitmap(R.drawable.ic_lock_open, accentColor, centerPx)
}
private fun tintedBitmap(@DrawableRes resId: Int, tint: Int, sizePx: Int): Bitmap? {
@@ -304,8 +321,13 @@ class CircularWheelView @JvmOverloads constructor(
canvas.drawCircle(cx, cy, innerRadius + dp(3f), centerRingPaint)
centerFillPaint.color = surfaceColor
canvas.drawCircle(cx, cy, innerRadius, centerFillPaint)
- centerBitmap?.let {
+ val activeCenterBitmap = if (isWheelLocked) centerBitmap else centerUnlockedBitmap
+ activeCenterBitmap?.let {
+ canvas.save()
+ // Shake pivots around the bottom-centre of the icon
+ if (lockShakeAngle != 0f) canvas.rotate(lockShakeAngle, cx, cy + it.height / 2f)
canvas.drawBitmap(it, cx - it.width / 2f, cy - it.height / 2f, bitmapPaint)
+ canvas.restore()
}
}
@@ -322,7 +344,15 @@ class CircularWheelView @JvmOverloads constructor(
canvas.save()
canvas.translate(iconX, iconY)
canvas.rotate(midDeg - 90f)
+ if (isWheelLocked) {
+ bitmapPaint.colorFilter = grayFilter
+ bitmapPaint.alpha = 100
+ }
canvas.drawBitmap(bmp, -bmp.width / 2f, -bmp.height / 2f, bitmapPaint)
+ if (isWheelLocked) {
+ bitmapPaint.colorFilter = null
+ bitmapPaint.alpha = 255
+ }
canvas.restore()
}
@@ -334,7 +364,7 @@ class CircularWheelView @JvmOverloads constructor(
val textX = cx + cosA * labelRadius
val textY = cy + sinA * labelRadius
val label = items[index].label
- textPaint.color = labelColor
+ textPaint.color = if (isWheelLocked) (labelColor and 0x00FFFFFF) or (80 shl 24) else labelColor
textPaint.textAlign = Paint.Align.LEFT
val halfAngleDeg = Math.toDegrees((textPaint.measureText(label) / 2.0) / labelRadius).toFloat()
val localArcRect = RectF(-labelRadius, -2f * labelRadius, labelRadius, 0f)
@@ -377,18 +407,38 @@ class CircularWheelView @JvmOverloads constructor(
invalidate()
}
}
- MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ MotionEvent.ACTION_UP -> {
if (!isDragging) {
val dist = hypot(event.x - cx, event.y - cy)
when {
- dist <= innerRadius -> onCenterClick?.invoke()
+ dist <= innerRadius -> {
+ if (isWheelLocked) {
+ onWheelCenterLockedTap?.invoke()
+ } else {
+ isWheelLocked = true
+ }
+ }
dist <= outerRadius -> {
- val idx = segmentAt(event.x, event.y)
- if (idx in items.indices) animateToSixOClock(idx) { onItemClick?.invoke(items[idx].navId) }
+ if (isWheelLocked) {
+ val idx = segmentAt(event.x, event.y)
+ if (idx in items.indices) animateToSixOClock(idx) {
+ vibrateDevice()
+ shakeLock()
+ }
+ } else {
+ val idx = segmentAt(event.x, event.y)
+ if (idx in items.indices) animateToSixOClock(idx) { onItemClick?.invoke(items[idx].navId) }
+ }
}
}
} else {
- val vel = computeVelocity() // degrees per millisecond
+ val vel = computeVelocity()
+ if (abs(vel) > 0.05f) fling(vel) else snapToNearest()
+ }
+ }
+ MotionEvent.ACTION_CANCEL -> {
+ if (isDragging) {
+ val vel = computeVelocity()
if (abs(vel) > 0.05f) fling(vel) else snapToNearest()
}
}
@@ -483,4 +533,29 @@ class CircularWheelView @JvmOverloads constructor(
start()
}
}
+
+ private fun vibrateDevice() {
+ val v = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+ v.vibrate(VibrationEffect.createOneShot(60, VibrationEffect.DEFAULT_AMPLITUDE))
+ }
+
+ fun shakeLock() {
+ shakeAnimator?.cancel()
+ shakeAnimator = ValueAnimator.ofFloat(0f, -18f, 18f, -12f, 12f, -6f, 6f, 0f).apply {
+ duration = 500
+ interpolator = LinearInterpolator()
+ addUpdateListener { lockShakeAngle = it.animatedValue as Float; invalidate() }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(a: Animator) { lockShakeAngle = 0f; invalidate() }
+ })
+ start()
+ }
+ }
+
+ fun unlockWheel() {
+ isWheelLocked = false
+ lockShakeAngle = 0f
+ shakeAnimator?.cancel()
+ invalidate()
+ }
}
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt
index 4038669..e7db253 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt
@@ -90,6 +90,7 @@ class HomeActivity : AppCompatActivity() {
private val warningRunnable = Runnable { showAutolockWarning() }
private var isLocked = false
+ private var pendingWheelUnlock = false
private val autolockRunnable = Runnable {
countdownTimer?.cancel(); countdownTimer = null
@@ -101,6 +102,19 @@ class HomeActivity : AppCompatActivity() {
fun lockApp() = lock()
+ fun notifyWheelLockTap() {
+ val securitySet = getSharedPreferences("prefs", MODE_PRIVATE)
+ .getString("security_method", null) != null
+ if (securitySet) {
+ pendingWheelUnlock = true
+ lock()
+ } else {
+ // No security configured — unlock the wheel immediately
+ (supportFragmentManager.findFragmentById(R.id.contentFrame) as? CircularNavFragment)
+ ?.unlockWheelLock()
+ }
+ }
+
private fun lock() {
isLocked = true
startActivity(
@@ -520,6 +534,11 @@ fun applyNavLabelVisibility() {
pauseTime = 0L
resetAutolockTimer()
autoRefresh(CredentialStore(this))
+ if (pendingWheelUnlock) {
+ pendingWheelUnlock = false
+ (supportFragmentManager.findFragmentById(R.id.contentFrame) as? CircularNavFragment)
+ ?.unlockWheelLock()
+ }
return
}
// If we were away long enough to have hit the autolock timeout (e.g. while
diff --git a/app/src/main/res/drawable/ic_lock_open.xml b/app/src/main/res/drawable/ic_lock_open.xml
new file mode 100644
index 0000000..0e119c5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_lock_open.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+