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.NetworkCapabilities
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.provider.ContactsContract
|
||||
import android.telecom.CallRedirectionService
|
||||
import android.telecom.PhoneAccountHandle
|
||||
|
@ -60,11 +61,14 @@ class CallRedirectionService : CallRedirectionService() {
|
|||
initialPhoneAccount: PhoneAccountHandle,
|
||||
allowInteractiveResponse: Boolean,
|
||||
) {
|
||||
Log.d("Redirection", "onPlaceCall triggered: uri=$handle, 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
|
||||
|
@ -72,25 +76,47 @@ class CallRedirectionService : CallRedirectionService() {
|
|||
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()
|
||||
return
|
||||
}
|
||||
|
||||
if (prefs.redirectIfRoaming && !isOutsideHomeCountry()) {
|
||||
Log.d("Redirection", "Aborting: redirect only while roaming, but we're inside home country")
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
||||
val phoneNumber = handle.schemeSpecificPart
|
||||
Log.d("Redirection", "Resolved phone number: $phoneNumber")
|
||||
|
||||
// Check if we only redirect international numbers
|
||||
if (prefs.redirectInternationalOnly && !isInternationalNumber(phoneNumber)) {
|
||||
Log.d("Redirection", "Aborting: number is not international and pref requires it")
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
||||
if (prefs.isBlacklistEnabled && !prefs.isContactWhitelisted(phoneNumber)) {
|
||||
Log.d("Redirection", "Aborting: number is not in whitelist while blacklist is enabled")
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
@ -98,22 +124,28 @@ class CallRedirectionService : CallRedirectionService() {
|
|||
val records: Array<Record>
|
||||
try {
|
||||
records = getRecordsFromPhoneNumber(phoneNumber)
|
||||
Log.d("Redirection", "Found ${records.size} raw records for contact")
|
||||
} catch (exc: SecurityException) {
|
||||
Log.w("Redirection", "SecurityException during record fetch", exc)
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
||||
// Filter to enabled services only
|
||||
val enabledRecords = records
|
||||
.filter { prefs.isServiceEnabled(it.mimetype) }
|
||||
.sortedBy { prefs.getServicePriority(it.mimetype) }
|
||||
|
||||
Log.d("Redirection", "Filtered to ${enabledRecords.size} enabled records")
|
||||
|
||||
val record = enabledRecords.firstOrNull()
|
||||
if (record == null) {
|
||||
Log.d("Redirection", "Aborting: no suitable record found for redirection")
|
||||
placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
|
||||
Log.d("Redirection", "Redirecting call to: ${record.mimetype} → ${record.uri}")
|
||||
|
||||
if (prefs.popupEnabled) {
|
||||
window.show(record.uri, MIMETYPE_TO_DST_NAME[record.mimetype] ?: return)
|
||||
} else {
|
||||
|
|
|
@ -15,6 +15,9 @@ import android.view.WindowManager
|
|||
import android.view.ContextThemeWrapper
|
||||
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
|
||||
|
@ -54,6 +57,7 @@ class PopupWindow(
|
|||
}
|
||||
private var currentEffect: PopupEffect = PopupEffect.NONE
|
||||
private var matrixOverlay: View? = null
|
||||
private var gamerAnimator: ValueAnimator? = null
|
||||
private var timer: Timer? = null
|
||||
|
||||
init {
|
||||
|
@ -88,35 +92,38 @@ class PopupWindow(
|
|||
}
|
||||
|
||||
fun show(uri: Uri, destinationId: Int) {
|
||||
val service = service?.get() ?: return
|
||||
if (!remove()) {
|
||||
service.placeCallUnmodified()
|
||||
return
|
||||
}
|
||||
val svc = service?.get() ?: return
|
||||
|
||||
timer?.cancel()
|
||||
timer = Timer()
|
||||
timer?.schedule(timerTask {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (!remove()) {
|
||||
service.placeCallUnmodified()
|
||||
return@timerTask
|
||||
svc.placeCallUnmodified()
|
||||
return@post
|
||||
}
|
||||
if (audioManager?.mode != AudioManager.MODE_IN_CALL) {
|
||||
service.placeCallUnmodified()
|
||||
return@timerTask
|
||||
svc.placeCallUnmodified()
|
||||
return@post
|
||||
}
|
||||
try {
|
||||
call(uri)
|
||||
} catch (exc: SecurityException) {
|
||||
service.placeCallUnmodified()
|
||||
return@timerTask
|
||||
svc.placeCallUnmodified()
|
||||
return@post
|
||||
}
|
||||
svc.cancelCall()
|
||||
}
|
||||
service.cancelCall()
|
||||
}, prefs.redirectionDelay)
|
||||
|
||||
layoutParams.y = prefs.popupPosition
|
||||
setDescription(destinationId)
|
||||
startProgressAnimation(prefs.redirectionDelay)
|
||||
|
||||
if (!add()) {
|
||||
Log.w("PopupWindow", "add() failed – popup not shown, calling directly.")
|
||||
timer?.cancel()
|
||||
service.placeCallUnmodified()
|
||||
svc.placeCallUnmodified()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,10 +154,24 @@ class PopupWindow(
|
|||
|
||||
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
|
||||
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) {
|
||||
PopupEffect.RANDOM -> PopupEffect.values().filter { it != PopupEffect.RANDOM && it != PopupEffect.NONE }.random()
|
||||
|
@ -164,10 +185,15 @@ class PopupWindow(
|
|||
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.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
|
||||
|
@ -227,18 +253,102 @@ class PopupWindow(
|
|||
} 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 -> 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.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(onEnd).start()
|
||||
view.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(150)
|
||||
.withEndAction(end)
|
||||
.start()
|
||||
|
||||
matrixOverlay?.let { overlay ->
|
||||
overlay.animate().cancel()
|
||||
overlay.animate().alpha(0f).setDuration(150).withEndAction {
|
||||
|
@ -249,7 +359,16 @@ class PopupWindow(
|
|||
}.start()
|
||||
}
|
||||
}
|
||||
else -> onEnd()
|
||||
|
||||
PopupEffect.SLIDE_SNAP -> view.animate()
|
||||
.translationY(200f)
|
||||
.alpha(0f)
|
||||
.setDuration(200)
|
||||
.setInterpolator(DecelerateInterpolator(2f))
|
||||
.withEndAction(end)
|
||||
.start()
|
||||
|
||||
else -> end.run()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,26 +388,33 @@ class PopupWindow(
|
|||
return true
|
||||
}
|
||||
|
||||
private fun remove(): Boolean {
|
||||
try {
|
||||
private fun remove(onRemoved: (() -> Unit)? = null): Boolean {
|
||||
return try {
|
||||
animateDisappear {
|
||||
try {
|
||||
windowManager?.removeView(view)
|
||||
matrixOverlay?.let {
|
||||
try {
|
||||
windowManager?.removeViewImmediate(it)
|
||||
} catch (_: Exception) {}
|
||||
} catch (e: Exception) {
|
||||
Log.e("PopupWindow", "Failed to remove matrix overlay", e)
|
||||
}
|
||||
matrixOverlay = null
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
} catch (e: Exception) {
|
||||
Log.e("PopupWindow", "Failed to remove popup view", e)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
return false
|
||||
onRemoved?.invoke()
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e("PopupWindow", "Exception during remove()", e)
|
||||
false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
Log.d("PopupWindow", "Cancel called")
|
||||
timer?.cancel()
|
||||
remove()
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ class Preferences(private val context: Context) {
|
|||
hasCallRedirectionRole(context)
|
||||
|
||||
enum class PopupEffect {
|
||||
NONE, FADE, SCALE, BOUNCE, FLOP, MATRIX, RANDOM
|
||||
NONE, FADE, SCALE, BOUNCE, FLOP, MATRIX, SLIDE_SNAP, GAMER_MODE, RANDOM
|
||||
}
|
||||
|
||||
var popupEffect: PopupEffect
|
||||
|
|
|
@ -37,10 +37,12 @@
|
|||
<string-array name="popup_effects">
|
||||
<item>None</item>
|
||||
<item>Fade</item>
|
||||
<item>Scale</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>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue