Release IKAR 1.0.8: fix incoming calls, cancellation and call history
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Tests / test (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Tests / test (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
This commit is contained in:
@@ -71,6 +71,15 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="ru.ikar.app.IncomingCallActivity"
|
||||
android:taskAffinity=""
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTop"
|
||||
android:showWhenLocked="true"
|
||||
android:turnScreenOn="true"
|
||||
android:theme="@android:style/Theme.Material.NoActionBar" />
|
||||
<activity-alias
|
||||
android:name="ru.ikar.app.MinimalIcon"
|
||||
android:enabled="false"
|
||||
|
||||
@@ -36,11 +36,11 @@ object CallEvents {
|
||||
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
|
||||
fun emit(action: String) {
|
||||
fun emit(action: String, conversationId: String? = null) {
|
||||
val s = sink ?: return
|
||||
main.post {
|
||||
try {
|
||||
s.success(mapOf("action" to action))
|
||||
s.success(mapOf("action" to action, "conversationId" to conversationId))
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
@@ -72,14 +72,18 @@ object CallConst {
|
||||
|
||||
object CallNotifier {
|
||||
|
||||
fun showIncoming(ctx: Context, data: Map<String, String>) {
|
||||
if (AppState.resumed) {
|
||||
@Synchronized fun showIncoming(ctx: Context, data: Map<String, String>) {
|
||||
val locked = (ctx.getSystemService(Context.KEYGUARD_SERVICE) as android.app.KeyguardManager).isKeyguardLocked
|
||||
if (AppState.resumed && !locked) {
|
||||
Log.d("QlyraFcm", "call push suppressed (app foreground)")
|
||||
return
|
||||
}
|
||||
val name = data["userName"] ?: data["title"] ?: "Неизвестный"
|
||||
val callerId = data["suid"] ?: data["callerId"] ?: ""
|
||||
val conversationId = data["conversationId"] ?: data["vcId"] ?: ""
|
||||
val ringMillis = IncomingCallPolicy.remainingMillis(data["expiresAt"]?.toLongOrNull(), System.currentTimeMillis())
|
||||
if (ringMillis == 0L) return
|
||||
if (!IncomingCallRegistry.begin(ctx, conversationId, data["expiresAt"]?.toLongOrNull())) return
|
||||
val vcp = data["vcp"] ?: ""
|
||||
val account = data["c"] ?: ""
|
||||
val callJson = JSONObject(data as Map<*, *>).toString()
|
||||
@@ -88,7 +92,7 @@ object CallNotifier {
|
||||
|
||||
ensureChannel(ctx)
|
||||
|
||||
val fullScreen = launchIntent(ctx, callJson, CallConst.ACTION_RING, name, 1)
|
||||
val fullScreen = incomingIntent(ctx, callJson, conversationId)
|
||||
val answer = launchIntent(ctx, callJson, CallConst.ACTION_ANSWER, name, 2)
|
||||
val decline = declineIntent(ctx, vcp, conversationId, account)
|
||||
val video = isVideo(data)
|
||||
@@ -104,6 +108,7 @@ object CallNotifier {
|
||||
.setColor(CallConst.ACCENT)
|
||||
.setColorized(true)
|
||||
.setOngoing(true)
|
||||
.setTimeoutAfter(ringMillis)
|
||||
.setOnlyAlertOnce(update)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
@@ -119,7 +124,7 @@ object CallNotifier {
|
||||
|
||||
val manager = NotificationManagerCompat.from(ctx)
|
||||
manager.notify(CallConst.NOTIF_ID, notification(NotifAvatars.initials(name), update = false))
|
||||
CallRinger.start(ctx, conversationId)
|
||||
CallRinger.start(ctx, conversationId, ringMillis)
|
||||
|
||||
val photo = NotifAvatars.photo(ctx, callerId) ?: return
|
||||
if (!isIncomingShowing(ctx)) return
|
||||
@@ -134,11 +139,24 @@ object CallNotifier {
|
||||
false
|
||||
}
|
||||
|
||||
fun finishCall(ctx: Context, data: Map<String, String>) {
|
||||
@Synchronized fun finishCall(ctx: Context, data: Map<String, String>, emitEvent: Boolean = true) {
|
||||
Log.d("QlyraFcm", "finishCall keys=${data.keys}")
|
||||
CallRinger.stop()
|
||||
NotificationManagerCompat.from(ctx).cancel(CallConst.NOTIF_ID)
|
||||
CallEvents.emit("ended")
|
||||
val id = data["conversationId"] ?: data["vcId"] ?: return
|
||||
if (IncomingCallRegistry.finish(ctx, id)) {
|
||||
CallRinger.stop()
|
||||
NotificationManagerCompat.from(ctx).cancel(CallConst.NOTIF_ID)
|
||||
IncomingCallActivity.close(id)
|
||||
}
|
||||
if (emitEvent) CallEvents.emit("ended", id)
|
||||
}
|
||||
|
||||
private fun incomingIntent(ctx: Context, json: String, id: String): PendingIntent {
|
||||
val intent = Intent(ctx, IncomingCallActivity::class.java).apply {
|
||||
data = android.net.Uri.parse("ikar://incoming-call/$id")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
putExtra(CallConst.EXTRA_CALL, json)
|
||||
}
|
||||
return PendingIntent.getActivity(ctx, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
private fun isVideo(data: Map<String, String>): Boolean {
|
||||
@@ -156,6 +174,7 @@ object CallNotifier {
|
||||
requestCode: Int,
|
||||
): PendingIntent {
|
||||
val intent = LaunchIntents.app(ctx).apply {
|
||||
data = android.net.Uri.parse("ikar://answer-call/${JSONObject(callJson).optString("conversationId")}")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
putExtra(CallConst.EXTRA_CALL, callJson)
|
||||
putExtra(CallConst.EXTRA_ACTION, action)
|
||||
@@ -168,7 +187,7 @@ object CallNotifier {
|
||||
return PendingIntent.getActivity(ctx, requestCode, intent, flags)
|
||||
}
|
||||
|
||||
private fun declineIntent(
|
||||
fun declineIntent(
|
||||
ctx: Context,
|
||||
vcp: String,
|
||||
conversationId: String,
|
||||
@@ -180,6 +199,7 @@ object CallNotifier {
|
||||
.put("c", account.toLongOrNull() ?: account)
|
||||
.toString()
|
||||
val intent = Intent(ctx, CallActionReceiver::class.java).apply {
|
||||
data = android.net.Uri.parse("ikar://decline-call/$conversationId")
|
||||
action = CallConst.ACTION_DECLINE
|
||||
putExtra(CallConst.EXTRA_DECLINE_PAYLOAD, payload)
|
||||
putExtra(CallConst.EXTRA_NOTIF_ID, CallConst.NOTIF_ID)
|
||||
@@ -221,7 +241,7 @@ object CallRinger {
|
||||
var activeConversationId: String? = null
|
||||
private set
|
||||
|
||||
fun start(ctx: Context, conversationId: String) {
|
||||
@Synchronized fun start(ctx: Context, conversationId: String, ringMillis: Long = 45_000L) {
|
||||
stop()
|
||||
activeConversationId = conversationId
|
||||
|
||||
@@ -240,12 +260,12 @@ object CallRinger {
|
||||
vibrate(ctx)
|
||||
}
|
||||
|
||||
val t = Runnable { onTimeout(ctx) }
|
||||
val t = Runnable { onTimeout(ctx, conversationId) }
|
||||
timeout = t
|
||||
handler.postDelayed(t, 45_000L)
|
||||
handler.postDelayed(t, ringMillis)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
@Synchronized fun stop() {
|
||||
timeout?.let { handler.removeCallbacks(it) }
|
||||
timeout = null
|
||||
try {
|
||||
@@ -266,7 +286,13 @@ object CallRinger {
|
||||
activeConversationId = null
|
||||
}
|
||||
|
||||
private fun onTimeout(ctx: Context) {
|
||||
@Synchronized private fun onTimeout(ctx: Context, expectedId: String) {
|
||||
if (activeConversationId != expectedId) return
|
||||
activeConversationId?.let {
|
||||
IncomingCallRegistry.finish(ctx, it)
|
||||
IncomingCallActivity.close(it)
|
||||
CallEvents.emit("ended", it)
|
||||
}
|
||||
stop()
|
||||
NotificationManagerCompat.from(ctx).cancel(CallConst.NOTIF_ID)
|
||||
}
|
||||
@@ -318,11 +344,11 @@ class CallActionReceiver : BroadcastReceiver() {
|
||||
return
|
||||
}
|
||||
if (intent.action != CallConst.ACTION_DECLINE) return
|
||||
CallRinger.stop()
|
||||
val notifId = intent.getIntExtra(CallConst.EXTRA_NOTIF_ID, CallConst.NOTIF_ID)
|
||||
NotificationManagerCompat.from(ctx).cancel(notifId)
|
||||
|
||||
val payload = intent.getStringExtra(CallConst.EXTRA_DECLINE_PAYLOAD) ?: return
|
||||
val callId = try { JSONObject(payload).optString("conversationId") } catch (_: Exception) { return }
|
||||
if (!IncomingCallRegistry.isActive(ctx, callId)) return
|
||||
val notifId = intent.getIntExtra(CallConst.EXTRA_NOTIF_ID, CallConst.NOTIF_ID)
|
||||
CallNotifier.finishCall(ctx, mapOf("conversationId" to callId))
|
||||
val fln = Intent(CallConst.FLN_ACTION_TAPPED).apply {
|
||||
setClassName(ctx, CallConst.FLN_RECEIVER)
|
||||
putExtra("notificationId", notifId)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package ru.ikar.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import org.json.JSONObject
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/** A native incoming screen works before Flutter starts and keeps chats behind the keyguard. */
|
||||
class IncomingCallActivity : Activity() {
|
||||
companion object {
|
||||
private var shown: WeakReference<IncomingCallActivity>? = null
|
||||
fun close(id: String?) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
shown?.get()?.let { if (id == null || it.callId == id) it.finish() }
|
||||
}
|
||||
}
|
||||
}
|
||||
private var callId = ""
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
setShowWhenLocked(true)
|
||||
setTurnScreenOn(true)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
|
||||
}
|
||||
renderCall()
|
||||
}
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
renderCall()
|
||||
}
|
||||
private fun renderCall() {
|
||||
val raw = intent.getStringExtra(CallConst.EXTRA_CALL) ?: run { finish(); return }
|
||||
val call = try { JSONObject(raw) } catch (_: Exception) { finish(); return }
|
||||
callId = call.optString("conversationId")
|
||||
if (!IncomingCallRegistry.isActive(this, callId)) { finish(); return }
|
||||
shown = WeakReference(this)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
val name = call.optString("userName").ifEmpty { call.optString("senderName").ifEmpty { call.optString("title").ifEmpty { "Неизвестный" } } }
|
||||
val layout = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
val density = resources.displayMetrics.density
|
||||
setPadding((32 * density).toInt(), (48 * density).toInt(), (32 * density).toInt(), (48 * density).toInt())
|
||||
setBackgroundColor(Color.rgb(24, 27, 38))
|
||||
}
|
||||
layout.addView(TextView(this).apply { text = name; textSize = 30f; setTextColor(Color.WHITE); gravity = Gravity.CENTER })
|
||||
layout.addView(TextView(this).apply {
|
||||
text = if (call.optString("iv") == "true" || call.optString("type") == "VIDEO") "Входящий видеозвонок ИКАР" else "Входящий звонок ИКАР"
|
||||
textSize = 18f; setTextColor(Color.LTGRAY); gravity = Gravity.CENTER; setPadding(0, 24, 0, 64)
|
||||
})
|
||||
layout.addView(Button(this).apply {
|
||||
text = "Принять"
|
||||
setOnClickListener {
|
||||
if (IncomingCallRegistry.isActive(this@IncomingCallActivity, callId)) {
|
||||
CallRinger.stop()
|
||||
startActivity(LaunchIntents.app(this@IncomingCallActivity).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
putExtra(CallConst.EXTRA_CALL, raw)
|
||||
putExtra(CallConst.EXTRA_ACTION, CallConst.ACTION_ANSWER)
|
||||
putExtra(CallConst.EXTRA_CALLER, name)
|
||||
})
|
||||
}
|
||||
finish()
|
||||
}
|
||||
})
|
||||
layout.addView(Button(this).apply {
|
||||
text = "Отклонить"
|
||||
setOnClickListener {
|
||||
CallNotifier.declineIntent(this@IncomingCallActivity, call.optString("vcp"), callId, call.optString("c")).send()
|
||||
finish()
|
||||
}
|
||||
})
|
||||
setContentView(layout)
|
||||
}
|
||||
override fun onDestroy() {
|
||||
if (shown?.get() === this) shown = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.ikar.app
|
||||
|
||||
/** A delayed push may ring only for the remaining server invitation lifetime. */
|
||||
internal object IncomingCallPolicy {
|
||||
fun remainingMillis(expiresAt: Long?, now: Long): Long =
|
||||
((expiresAt ?: (now + 45_000L)) - now).coerceIn(0L, 45_000L)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package ru.ikar.app
|
||||
|
||||
import android.content.Context
|
||||
import org.json.JSONObject
|
||||
|
||||
object IncomingCallRegistry {
|
||||
private fun prefs(ctx: Context) = ctx.getSharedPreferences("ikar_incoming_calls", Context.MODE_PRIVATE)
|
||||
private fun ended(ctx: Context) = try { JSONObject(prefs(ctx).getString("ended", "{}") ?: "{}") } catch (_: Exception) { JSONObject() }
|
||||
@Synchronized fun begin(ctx: Context, id: String, expiresAt: Long?): Boolean {
|
||||
if (!isAvailable(ctx, id) || expiresAt?.let { it <= System.currentTimeMillis() } == true) return false
|
||||
val state = prefs(ctx)
|
||||
if (state.getString("active", null) == id) return false
|
||||
state.edit().putString("active", id).putLong("expires", expiresAt ?: System.currentTimeMillis() + 45_000).commit()
|
||||
return true
|
||||
}
|
||||
@Synchronized fun isAvailable(ctx: Context, id: String): Boolean {
|
||||
if (id.isEmpty()) return false
|
||||
val ended = ended(ctx)
|
||||
return !ended.has(id)
|
||||
}
|
||||
@Synchronized fun isActive(ctx: Context, id: String): Boolean =
|
||||
prefs(ctx).getString("active", null) == id && isAvailable(ctx, id) && prefs(ctx).getLong("expires", 0) > System.currentTimeMillis()
|
||||
@Synchronized fun finish(ctx: Context, id: String): Boolean {
|
||||
if (id.isEmpty()) return false
|
||||
val state = prefs(ctx)
|
||||
val ended = ended(ctx)
|
||||
val now = System.currentTimeMillis()
|
||||
ended.keys().asSequence().toList().filter { ended.optLong(it) < now - 600_000 }.forEach { ended.remove(it) }
|
||||
ended.put(id, now)
|
||||
ended.keys().asSequence().toList().sortedBy { ended.optLong(it) }.dropLast(128).forEach { ended.remove(it) }
|
||||
val matches = state.getString("active", null) == id
|
||||
val editor = state.edit().putString("ended", ended.toString())
|
||||
if (matches) editor.remove("active").remove("expires")
|
||||
editor.commit()
|
||||
return matches
|
||||
}
|
||||
fun activeId(ctx: Context): String? = prefs(ctx).getString("active", null)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package ru.ikar.app
|
||||
|
||||
import android.Manifest
|
||||
import android.app.KeyguardManager
|
||||
import org.json.JSONObject
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -362,13 +362,31 @@ class MainActivity : AudioServiceFragmentActivity() {
|
||||
"ru.ikar.app/calls",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"showIncoming" -> {
|
||||
val raw = call.arguments as? Map<*, *> ?: emptyMap<Any, Any>()
|
||||
CallNotifier.showIncoming(applicationContext, raw.entries.associate { it.key.toString() to it.value.toString() })
|
||||
result.success(null)
|
||||
}
|
||||
"cancelConversation" -> {
|
||||
val id = call.argument<String>("conversationId")
|
||||
if (id != null) {
|
||||
CallNotifier.finishCall(applicationContext, mapOf("conversationId" to id), emitEvent = false)
|
||||
if (pendingCall?.get("data")?.toString()?.let { JSONObject(it).optString("conversationId") } == id) pendingCall = null
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"consumeInitialCall" -> {
|
||||
val p = pendingCall
|
||||
pendingCall = null
|
||||
result.success(p)
|
||||
val id = p?.get("data")?.toString()?.let { JSONObject(it).optString("conversationId") }
|
||||
result.success(if (id != null && IncomingCallRegistry.isAvailable(applicationContext, id)) p else null)
|
||||
}
|
||||
"notifyAccepted" -> {
|
||||
val caller = call.argument<String>("caller") ?: "Звонок"
|
||||
call.argument<String>("conversationId")?.let {
|
||||
// Ignore an incoming push delivered after this call was accepted.
|
||||
CallNotifier.finishCall(applicationContext, mapOf("conversationId" to it), emitEvent = false)
|
||||
}
|
||||
CallRinger.stop()
|
||||
NotificationManagerCompat.from(this).cancel(CallConst.NOTIF_ID)
|
||||
CallForegroundService.start(applicationContext, caller)
|
||||
@@ -394,8 +412,14 @@ class MainActivity : AudioServiceFragmentActivity() {
|
||||
result.success(null)
|
||||
}
|
||||
"notifyEnded" -> {
|
||||
CallRinger.stop()
|
||||
NotificationManagerCompat.from(this).cancel(CallConst.NOTIF_ID)
|
||||
val id = call.argument<String>("conversationId")
|
||||
id?.let {
|
||||
CallNotifier.finishCall(applicationContext, mapOf("conversationId" to it), emitEvent = false)
|
||||
}
|
||||
if (id == null) {
|
||||
CallRinger.stop()
|
||||
NotificationManagerCompat.from(this).cancel(CallConst.NOTIF_ID)
|
||||
}
|
||||
CallForegroundService.stop(applicationContext)
|
||||
clearCallWindowFlags()
|
||||
result.success(null)
|
||||
@@ -403,6 +427,7 @@ class MainActivity : AudioServiceFragmentActivity() {
|
||||
"cancelIncoming" -> {
|
||||
CallRinger.stop()
|
||||
NotificationManagerCompat.from(this).cancel(CallConst.NOTIF_ID)
|
||||
IncomingCallActivity.close(null)
|
||||
result.success(null)
|
||||
}
|
||||
"canUseFullScreenIntent" -> {
|
||||
@@ -673,6 +698,8 @@ class MainActivity : AudioServiceFragmentActivity() {
|
||||
intent.removeExtra(CallConst.EXTRA_CALL)
|
||||
intent.removeExtra(CallConst.EXTRA_ACTION)
|
||||
intent.removeExtra(CallConst.EXTRA_CALLER)
|
||||
val callId = try { JSONObject(json).optString("conversationId") } catch (_: Exception) { clearCallWindowFlags(); return }
|
||||
if (!IncomingCallRegistry.isActive(applicationContext, callId)) { clearCallWindowFlags(); return }
|
||||
if (action == CallConst.ACTION_ANSWER) CallRinger.stop()
|
||||
val map = mapOf<String, Any?>("data" to json, "action" to action)
|
||||
val sink = CallEvents.sink
|
||||
@@ -694,10 +721,6 @@ class MainActivity : AudioServiceFragmentActivity() {
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
|
||||
)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val km = getSystemService(Context.KEYGUARD_SERVICE) as? KeyguardManager
|
||||
km?.requestDismissKeyguard(this, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCallWindowFlags() {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package ru.ikar.app
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class IncomingCallPolicyTest {
|
||||
@Test fun delayedInvitationRingsOnlyUntilServerExpiry() {
|
||||
assertEquals(15_000L, IncomingCallPolicy.remainingMillis(60_000L, 45_000L))
|
||||
}
|
||||
@Test fun alreadyExpiredInvitationDoesNotRing() {
|
||||
assertEquals(0L, IncomingCallPolicy.remainingMillis(30_000L, 45_000L))
|
||||
assertEquals(0L, IncomingCallPolicy.remainingMillis(45_000L, 45_000L))
|
||||
}
|
||||
@Test fun legacyPushAndFarFutureExpiryHaveFiniteRinging() {
|
||||
assertEquals(45_000L, IncomingCallPolicy.remainingMillis(null, 45_000L))
|
||||
assertEquals(45_000L, IncomingCallPolicy.remainingMillis(300_000L, 45_000L))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user