More animations
This commit is contained in:
parent
b2b839cf72
commit
693607de7c
4 changed files with 204 additions and 44 deletions
|
@ -4,6 +4,7 @@ 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
|
||||||
|
@ -60,11 +61,14 @@ class CallRedirectionService : CallRedirectionService() {
|
||||||
initialPhoneAccount: PhoneAccountHandle,
|
initialPhoneAccount: PhoneAccountHandle,
|
||||||
allowInteractiveResponse: Boolean,
|
allowInteractiveResponse: Boolean,
|
||||||
) {
|
) {
|
||||||
|
Log.d("Redirection", "onPlaceCall triggered: uri=$handle, interactive=$allowInteractiveResponse")
|
||||||
|
|
||||||
val capabilities = connectivityManager
|
val capabilities = connectivityManager
|
||||||
?.getNetworkCapabilities(connectivityManager?.activeNetwork)
|
?.getNetworkCapabilities(connectivityManager?.activeNetwork)
|
||||||
|
|
||||||
val isWifi = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
val isWifi = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||||
val isCellular = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
|
val isCellular = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
|
||||||
|
Log.d("Redirection", "isWifi=$isWifi, isCellular=$isCellular")
|
||||||
|
|
||||||
val shouldRedirect = when {
|
val shouldRedirect = when {
|
||||||
isWifi && !prefs.redirectOnWifi -> false
|
isWifi && !prefs.redirectOnWifi -> false
|
||||||
|
@ -72,25 +76,47 @@ class CallRedirectionService : CallRedirectionService() {
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prefs.isEnabled || !shouldRedirect || !hasInternet() || !allowInteractiveResponse) {
|
if (!prefs.isEnabled) {
|
||||||
|
Log.d("Redirection", "Aborting: redirection disabled in prefs")
|
||||||
|
placeCallUnmodified()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldRedirect) {
|
||||||
|
Log.d("Redirection", "Aborting: redirection blocked by current network preference")
|
||||||
|
placeCallUnmodified()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasInternet()) {
|
||||||
|
Log.d("Redirection", "Aborting: no internet connection detected")
|
||||||
|
placeCallUnmodified()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowInteractiveResponse) {
|
||||||
|
Log.d("Redirection", "Aborting: interactive response not allowed by system")
|
||||||
placeCallUnmodified()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefs.redirectIfRoaming && !isOutsideHomeCountry()) {
|
if (prefs.redirectIfRoaming && !isOutsideHomeCountry()) {
|
||||||
|
Log.d("Redirection", "Aborting: redirect only while roaming, but we're inside home country")
|
||||||
placeCallUnmodified()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val phoneNumber = handle.schemeSpecificPart
|
val phoneNumber = handle.schemeSpecificPart
|
||||||
|
Log.d("Redirection", "Resolved phone number: $phoneNumber")
|
||||||
|
|
||||||
// Check if we only redirect international numbers
|
|
||||||
if (prefs.redirectInternationalOnly && !isInternationalNumber(phoneNumber)) {
|
if (prefs.redirectInternationalOnly && !isInternationalNumber(phoneNumber)) {
|
||||||
|
Log.d("Redirection", "Aborting: number is not international and pref requires it")
|
||||||
placeCallUnmodified()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefs.isBlacklistEnabled && !prefs.isContactWhitelisted(phoneNumber)) {
|
if (prefs.isBlacklistEnabled && !prefs.isContactWhitelisted(phoneNumber)) {
|
||||||
|
Log.d("Redirection", "Aborting: number is not in whitelist while blacklist is enabled")
|
||||||
placeCallUnmodified()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -98,22 +124,28 @@ class CallRedirectionService : CallRedirectionService() {
|
||||||
val records: Array<Record>
|
val records: Array<Record>
|
||||||
try {
|
try {
|
||||||
records = getRecordsFromPhoneNumber(phoneNumber)
|
records = getRecordsFromPhoneNumber(phoneNumber)
|
||||||
|
Log.d("Redirection", "Found ${records.size} raw records for contact")
|
||||||
} 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 records")
|
||||||
|
|
||||||
val record = enabledRecords.firstOrNull()
|
val record = enabledRecords.firstOrNull()
|
||||||
if (record == null) {
|
if (record == null) {
|
||||||
|
Log.d("Redirection", "Aborting: no suitable record found for redirection")
|
||||||
placeCallUnmodified()
|
placeCallUnmodified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d("Redirection", "Redirecting call to: ${record.mimetype} → ${record.uri}")
|
||||||
|
|
||||||
if (prefs.popupEnabled) {
|
if (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 {
|
} else {
|
||||||
|
|
|
@ -15,6 +15,9 @@ import android.view.WindowManager
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.view.animation.OvershootInterpolator
|
import android.view.animation.OvershootInterpolator
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
@ -54,6 +57,7 @@ class PopupWindow(
|
||||||
}
|
}
|
||||||
private var currentEffect: PopupEffect = PopupEffect.NONE
|
private var currentEffect: PopupEffect = PopupEffect.NONE
|
||||||
private var matrixOverlay: View? = null
|
private var matrixOverlay: View? = null
|
||||||
|
private var gamerAnimator: ValueAnimator? = null
|
||||||
private var timer: Timer? = null
|
private var timer: Timer? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -88,35 +92,38 @@ class PopupWindow(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show(uri: Uri, destinationId: Int) {
|
fun show(uri: Uri, destinationId: Int) {
|
||||||
val service = service?.get() ?: return
|
val svc = service?.get() ?: return
|
||||||
if (!remove()) {
|
|
||||||
service.placeCallUnmodified()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
timer?.cancel()
|
timer?.cancel()
|
||||||
timer = Timer()
|
timer = Timer()
|
||||||
timer?.schedule(timerTask {
|
timer?.schedule(timerTask {
|
||||||
if (!remove()) {
|
Handler(Looper.getMainLooper()).post {
|
||||||
service.placeCallUnmodified()
|
if (!remove()) {
|
||||||
return@timerTask
|
svc.placeCallUnmodified()
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
if (audioManager?.mode != AudioManager.MODE_IN_CALL) {
|
||||||
|
svc.placeCallUnmodified()
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
call(uri)
|
||||||
|
} catch (exc: SecurityException) {
|
||||||
|
svc.placeCallUnmodified()
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
svc.cancelCall()
|
||||||
}
|
}
|
||||||
if (audioManager?.mode != AudioManager.MODE_IN_CALL) {
|
|
||||||
service.placeCallUnmodified()
|
|
||||||
return@timerTask
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
call(uri)
|
|
||||||
} catch (exc: SecurityException) {
|
|
||||||
service.placeCallUnmodified()
|
|
||||||
return@timerTask
|
|
||||||
}
|
|
||||||
service.cancelCall()
|
|
||||||
}, prefs.redirectionDelay)
|
}, prefs.redirectionDelay)
|
||||||
|
|
||||||
|
layoutParams.y = prefs.popupPosition
|
||||||
setDescription(destinationId)
|
setDescription(destinationId)
|
||||||
startProgressAnimation(prefs.redirectionDelay)
|
startProgressAnimation(prefs.redirectionDelay)
|
||||||
|
|
||||||
if (!add()) {
|
if (!add()) {
|
||||||
|
Log.w("PopupWindow", "add() failed – popup not shown, calling directly.")
|
||||||
timer?.cancel()
|
timer?.cancel()
|
||||||
service.placeCallUnmodified()
|
svc.placeCallUnmodified()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,10 +154,24 @@ class PopupWindow(
|
||||||
|
|
||||||
private fun animateAppear() {
|
private fun animateAppear() {
|
||||||
view.animate().cancel()
|
view.animate().cancel()
|
||||||
|
|
||||||
|
// Always reset all transforms before animation
|
||||||
view.rotationX = 0f
|
view.rotationX = 0f
|
||||||
|
view.alpha = 1f
|
||||||
|
view.translationX = 0f
|
||||||
|
view.translationY = 0f
|
||||||
view.scaleX = 1f
|
view.scaleX = 1f
|
||||||
view.scaleY = 1f
|
view.scaleY = 1f
|
||||||
view.alpha = 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) {
|
val effect = when (prefs.popupEffect) {
|
||||||
PopupEffect.RANDOM -> PopupEffect.values().filter { it != PopupEffect.RANDOM && it != PopupEffect.NONE }.random()
|
PopupEffect.RANDOM -> PopupEffect.values().filter { it != PopupEffect.RANDOM && it != PopupEffect.NONE }.random()
|
||||||
|
@ -164,10 +185,15 @@ class PopupWindow(
|
||||||
view.alpha = 0f
|
view.alpha = 0f
|
||||||
view.animate().alpha(1f).setDuration(300).start()
|
view.animate().alpha(1f).setDuration(300).start()
|
||||||
}
|
}
|
||||||
PopupEffect.SCALE -> {
|
PopupEffect.SCALE -> { //
|
||||||
view.scaleX = 0f
|
view.translationX = view.width.toFloat()
|
||||||
view.scaleY = 0f
|
view.alpha = 0f
|
||||||
view.animate().scaleX(1f).scaleY(1f).setDuration(300).start()
|
view.animate()
|
||||||
|
.translationX(0f)
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(350)
|
||||||
|
.setInterpolator(DecelerateInterpolator(2f))
|
||||||
|
.start()
|
||||||
}
|
}
|
||||||
PopupEffect.BOUNCE -> {
|
PopupEffect.BOUNCE -> {
|
||||||
view.scaleX = 0.7f
|
view.scaleX = 0.7f
|
||||||
|
@ -227,18 +253,102 @@ class PopupWindow(
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}.start()
|
}.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 -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun animateDisappear(onEnd: () -> Unit) {
|
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) {
|
when (currentEffect) {
|
||||||
PopupEffect.NONE -> onEnd()
|
|
||||||
PopupEffect.FADE -> view.animate().alpha(0f).setDuration(200).withEndAction(onEnd).start()
|
// PopupEffect.NONE -> falls to else
|
||||||
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.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 -> {
|
PopupEffect.MATRIX -> {
|
||||||
view.animate().alpha(0f).setDuration(150).withEndAction(onEnd).start()
|
view.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(150)
|
||||||
|
.withEndAction(end)
|
||||||
|
.start()
|
||||||
|
|
||||||
matrixOverlay?.let { overlay ->
|
matrixOverlay?.let { overlay ->
|
||||||
overlay.animate().cancel()
|
overlay.animate().cancel()
|
||||||
overlay.animate().alpha(0f).setDuration(150).withEndAction {
|
overlay.animate().alpha(0f).setDuration(150).withEndAction {
|
||||||
|
@ -249,7 +359,16 @@ class PopupWindow(
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> onEnd()
|
|
||||||
|
PopupEffect.SLIDE_SNAP -> view.animate()
|
||||||
|
.translationY(200f)
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(200)
|
||||||
|
.setInterpolator(DecelerateInterpolator(2f))
|
||||||
|
.withEndAction(end)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
else -> end.run()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,29 +388,36 @@ class PopupWindow(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun remove(): Boolean {
|
private fun remove(onRemoved: (() -> Unit)? = null): Boolean {
|
||||||
try {
|
return try {
|
||||||
animateDisappear {
|
animateDisappear {
|
||||||
try {
|
try {
|
||||||
windowManager?.removeView(view)
|
windowManager?.removeView(view)
|
||||||
matrixOverlay?.let {
|
matrixOverlay?.let {
|
||||||
try {
|
try {
|
||||||
windowManager?.removeViewImmediate(it)
|
windowManager?.removeViewImmediate(it)
|
||||||
} catch (_: Exception) {}
|
} catch (e: Exception) {
|
||||||
|
Log.e("PopupWindow", "Failed to remove matrix overlay", e)
|
||||||
|
}
|
||||||
matrixOverlay = null
|
matrixOverlay = null
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {}
|
} catch (e: Exception) {
|
||||||
|
Log.e("PopupWindow", "Failed to remove popup view", e)
|
||||||
|
}
|
||||||
|
onRemoved?.invoke()
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
true
|
||||||
return false
|
} catch (e: Exception) {
|
||||||
|
Log.e("PopupWindow", "Exception during remove()", e)
|
||||||
|
false
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel() {
|
fun cancel() {
|
||||||
|
Log.d("PopupWindow", "Cancel called")
|
||||||
timer?.cancel()
|
timer?.cancel()
|
||||||
remove()
|
remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyResolvedColors(view: android.view.View) {
|
private fun applyResolvedColors(view: android.view.View) {
|
||||||
val attrSurface = com.google.android.material.R.attr.colorSurface
|
val attrSurface = com.google.android.material.R.attr.colorSurface
|
||||||
|
|
|
@ -42,7 +42,7 @@ class Preferences(private val context: Context) {
|
||||||
hasCallRedirectionRole(context)
|
hasCallRedirectionRole(context)
|
||||||
|
|
||||||
enum class PopupEffect {
|
enum class PopupEffect {
|
||||||
NONE, FADE, SCALE, BOUNCE, FLOP, MATRIX, RANDOM
|
NONE, FADE, SCALE, BOUNCE, FLOP, MATRIX, SLIDE_SNAP, GAMER_MODE, RANDOM
|
||||||
}
|
}
|
||||||
|
|
||||||
var popupEffect: PopupEffect
|
var popupEffect: PopupEffect
|
||||||
|
|
|
@ -37,10 +37,12 @@
|
||||||
<string-array name="popup_effects">
|
<string-array name="popup_effects">
|
||||||
<item>None</item>
|
<item>None</item>
|
||||||
<item>Fade</item>
|
<item>Fade</item>
|
||||||
<item>Scale</item>
|
<item>Slide</item>
|
||||||
<item>Bounce</item>
|
<item>Bounce</item>
|
||||||
<item>Flop</item>
|
<item>Flop</item>
|
||||||
<item>Matrix</item>
|
<item>Matrix</item>
|
||||||
|
<item>Slide Snap</item>
|
||||||
|
<item>Gamer Mode</item>
|
||||||
<item>Random</item>
|
<item>Random</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue