Reworked top bar alert mechanism, added network not reachable alert

This commit is contained in:
Sylvain Berfini 2023-11-17 15:58:28 +01:00
parent 6b95cc6a5c
commit c7f86311aa
6 changed files with 212 additions and 100 deletions

View file

@ -42,12 +42,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.findNavController import androidx.navigation.findNavController
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
@ -107,20 +104,17 @@ class MainActivity : AppCompatActivity() {
viewModel.changeSystemTopBarColorEvent.observe(this) { viewModel.changeSystemTopBarColorEvent.observe(this) {
it.consume { mode -> it.consume { mode ->
val color = when (mode) { window.statusBarColor = when (mode) {
MainViewModel.IN_CALL -> AppUtils.getColor(R.color.green_success_500) MainViewModel.SINGLE_CALL, MainViewModel.MULTIPLE_CALLS -> {
MainViewModel.ACCOUNT_REGISTRATION_FAILURE -> AppUtils.getColor( AppUtils.getColor(R.color.green_success_500)
R.color.red_danger_500
)
else -> AppUtils.getColor(R.color.orange_main_500)
}
lifecycleScope.launch {
withContext(Dispatchers.IO) {
delay(if (mode == MainViewModel.IN_CALL) 1000 else 0)
withContext(Dispatchers.Main) {
window.statusBarColor = color
}
} }
MainViewModel.NETWORK_NOT_REACHABLE, MainViewModel.NON_DEFAULT_ACCOUNT_NOT_CONNECTED -> {
AppUtils.getColor(R.color.red_danger_500)
}
MainViewModel.NON_DEFAULT_ACCOUNT_NOTIFICATIONS -> {
AppUtils.getColor(R.color.gray_main2_500)
}
else -> AppUtils.getColor(R.color.orange_main_500)
} }
} }
} }

View file

@ -23,6 +23,13 @@ import androidx.annotation.UiThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R import org.linphone.R
import org.linphone.core.Account import org.linphone.core.Account
@ -40,16 +47,21 @@ class MainViewModel @UiThread constructor() : ViewModel() {
private const val TAG = "[Main ViewModel]" private const val TAG = "[Main ViewModel]"
const val NONE = 0 const val NONE = 0
const val ACCOUNT_REGISTRATION_FAILURE = 1 const val NON_DEFAULT_ACCOUNT_NOTIFICATIONS = 5
const val IN_CALL = 2 const val NON_DEFAULT_ACCOUNT_NOT_CONNECTED = 10
const val NETWORK_NOT_REACHABLE = 19
const val SINGLE_CALL = 20
const val MULTIPLE_CALLS = 21
} }
val showTopBar = MutableLiveData<Boolean>() val showAlert = MutableLiveData<Boolean>()
val alertLabel = MutableLiveData<String>()
val alertIcon = MutableLiveData<Int>()
val atLeastOneCall = MutableLiveData<Boolean>() val atLeastOneCall = MutableLiveData<Boolean>()
val callLabel = MutableLiveData<String>()
val callsStatus = MutableLiveData<String>() val callsStatus = MutableLiveData<String>()
val defaultAccountRegistrationErrorEvent: MutableLiveData<Event<Boolean>> by lazy { val defaultAccountRegistrationErrorEvent: MutableLiveData<Event<Boolean>> by lazy {
@ -68,13 +80,24 @@ class MainViewModel @UiThread constructor() : ViewModel() {
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
var defaultAccountRegistrationFailed = false private var defaultAccountRegistrationFailed = false
private val alertsList = arrayListOf<Pair<Int, String>>()
private var alertJob: Job? = null
private val coreListener = object : CoreListenerStub() { private val coreListener = object : CoreListenerStub() {
@WorkerThread @WorkerThread
override fun onLastCallEnded(core: Core) { override fun onLastCallEnded(core: Core) {
Log.i("$TAG Last call ended, asking fragment to change back status bar color") Log.i("$TAG Last call ended, removing in-call 'alert'")
changeSystemTopBarColorEvent.postValue(Event(NONE)) removeAlert(SINGLE_CALL)
atLeastOneCall.postValue(false)
}
override fun onFirstCallStarted(core: Core) {
Log.i("$TAG First call started, adding in-call 'alert'")
updateCallAlert()
atLeastOneCall.postValue(true)
} }
@WorkerThread @WorkerThread
@ -84,12 +107,28 @@ class MainViewModel @UiThread constructor() : ViewModel() {
state: Call.State?, state: Call.State?,
message: String message: String
) { ) {
if (core.callsNb > 0) { if (
updateCurrentCallInfo() core.callsNb > 1 && (
LinphoneUtils.isCallEnding(call.state) ||
LinphoneUtils.isCallIncoming(call.state) ||
LinphoneUtils.isCallOutgoing(call.state)
)
) {
updateCallAlert()
} else if (core.callsNb == 1) {
callsStatus.postValue(LinphoneUtils.callStateToString(call.state))
}
}
@WorkerThread
override fun onNetworkReachable(core: Core, reachable: Boolean) {
Log.i("$TAG Network is ${if (reachable) "reachable" else "not reachable"}")
if (!reachable) {
val label = AppUtils.getString(R.string.network_not_reachable)
addAlert(NETWORK_NOT_REACHABLE, label)
} else {
removeAlert(NETWORK_NOT_REACHABLE)
} }
val calls = core.callsNb > 0
showTopBar.postValue(calls)
atLeastOneCall.postValue(calls)
} }
@WorkerThread @WorkerThread
@ -105,17 +144,12 @@ class MainViewModel @UiThread constructor() : ViewModel() {
Log.e("$TAG Default account registration failed!") Log.e("$TAG Default account registration failed!")
defaultAccountRegistrationFailed = true defaultAccountRegistrationFailed = true
defaultAccountRegistrationErrorEvent.postValue(Event(true)) defaultAccountRegistrationErrorEvent.postValue(Event(true))
} else { } else if (core.isNetworkReachable) {
Log.e("$TAG Non-default account registration failed!") Log.e("$TAG Non-default account registration failed!")
// Do not show connection error top bar if there is a call val label = AppUtils.getString(
if (atLeastOneCall.value == false) { R.string.connection_error_for_non_default_account
changeSystemTopBarColorEvent.postValue( )
Event( addAlert(SINGLE_CALL, label)
ACCOUNT_REGISTRATION_FAILURE
)
)
showTopBar.postValue(true)
}
} }
} }
RegistrationState.Ok -> { RegistrationState.Ok -> {
@ -129,11 +163,7 @@ class MainViewModel @UiThread constructor() : ViewModel() {
it.state == RegistrationState.Failed it.state == RegistrationState.Failed
} }
if (found == null) { if (found == null) {
Log.i("$TAG No account in Failed state anymore") removeAlert(NON_DEFAULT_ACCOUNT_NOT_CONNECTED)
if (atLeastOneCall.value == false) {
changeSystemTopBarColorEvent.postValue(Event(NONE))
showTopBar.postValue(false)
}
} }
} }
} }
@ -144,18 +174,21 @@ class MainViewModel @UiThread constructor() : ViewModel() {
init { init {
defaultAccountRegistrationFailed = false defaultAccountRegistrationFailed = false
showTopBar.value = false showAlert.value = false
coreContext.postOnCoreThread { core -> coreContext.postOnCoreThread { core ->
core.addListener(coreListener) core.addListener(coreListener)
if (core.callsNb > 0) { if (!core.isNetworkReachable) {
updateCurrentCallInfo() Log.w("$TAG Network is not reachable!")
val label = AppUtils.getString(R.string.network_not_reachable)
addAlert(NETWORK_NOT_REACHABLE, label)
} }
val calls = core.callsNb > 0 if (core.callsNb > 0) {
showTopBar.postValue(calls) updateCallAlert()
atLeastOneCall.postValue(calls) }
atLeastOneCall.postValue(core.callsNb > 0)
} }
} }
@ -170,7 +203,7 @@ class MainViewModel @UiThread constructor() : ViewModel() {
@UiThread @UiThread
fun closeTopBar() { fun closeTopBar() {
showTopBar.value = false showAlert.value = false
} }
@UiThread @UiThread
@ -183,38 +216,121 @@ class MainViewModel @UiThread constructor() : ViewModel() {
} }
@WorkerThread @WorkerThread
private fun updateCurrentCallInfo() { private fun updateCallAlert() {
val core = coreContext.core val core = coreContext.core
if (core.callsNb == 1) { val callsNb = core.callsNb
if (callsNb == 1) {
removeAlert(MULTIPLE_CALLS)
val currentCall = core.currentCall ?: core.calls.firstOrNull() val currentCall = core.currentCall ?: core.calls.firstOrNull()
if (currentCall != null) { if (currentCall != null) {
val contact = coreContext.contactsManager.findContactByAddress( val contact = coreContext.contactsManager.findContactByAddress(
currentCall.remoteAddress currentCall.remoteAddress
) )
if (contact != null) { val label = if (contact != null) {
callLabel.postValue( contact.name ?: LinphoneUtils.getDisplayName(currentCall.remoteAddress)
contact.name ?: LinphoneUtils.getDisplayName(currentCall.remoteAddress)
)
} else { } else {
val conferenceInfo = coreContext.core.findConferenceInformationFromUri( val conferenceInfo = coreContext.core.findConferenceInformationFromUri(
currentCall.remoteAddress currentCall.remoteAddress
) )
callLabel.postValue( conferenceInfo?.subject ?: LinphoneUtils.getDisplayName(
conferenceInfo?.subject ?: LinphoneUtils.getDisplayName( currentCall.remoteAddress
currentCall.remoteAddress
)
) )
} }
addAlert(SINGLE_CALL, label)
callsStatus.postValue(LinphoneUtils.callStateToString(currentCall.state)) callsStatus.postValue(LinphoneUtils.callStateToString(currentCall.state))
} }
} else if (callsNb > 1) {
removeAlert(SINGLE_CALL)
Log.i("$TAG At least a call, asking fragment to change status bar color") addAlert(
changeSystemTopBarColorEvent.postValue(Event(IN_CALL)) MULTIPLE_CALLS,
} else { AppUtils.getFormattedString(R.string.calls_count_label, callsNb)
callLabel.postValue(
AppUtils.getFormattedString(R.string.calls_count_label, core.callsNb)
) )
callsStatus.postValue("") // TODO: improve ? callsStatus.postValue("") // TODO: improve ?
} }
} }
@WorkerThread
private fun addAlert(type: Int, label: String) {
val found = alertsList.find {
it.first == type
}
if (found == null) {
cancelAlertJob()
val alert = Pair(type, label)
alertsList.add(alert)
updateDisplayedAlert()
} else {
Log.w("$TAG There is already an alert with type [$type], skipping...")
}
}
@WorkerThread
private fun removeAlert(type: Int) {
val found = alertsList.find {
it.first == type
}
if (found != null) {
cancelAlertJob()
alertsList.remove(found)
updateDisplayedAlert()
} else {
Log.w("$TAG Failed to remove alert with type [$type], not found in current alerts list")
}
}
@WorkerThread
private fun cancelAlertJob() {
viewModelScope.launch {
withContext(Dispatchers.Main) {
alertJob?.cancelAndJoin()
alertJob = null
}
}
}
@WorkerThread
private fun updateDisplayedAlert() {
// Sort alerts by priority
alertsList.sortByDescending {
it.first
}
val maxedPriorityAlert = alertsList.firstOrNull()
if (maxedPriorityAlert == null) {
Log.i("$TAG No alert to display")
showAlert.postValue(false)
changeSystemTopBarColorEvent.postValue(Event(NONE))
} else {
val type = maxedPriorityAlert.first
val label = maxedPriorityAlert.second
Log.i("$TAG Max priority alert right now is [$type]")
when (type) {
NON_DEFAULT_ACCOUNT_NOTIFICATIONS, NON_DEFAULT_ACCOUNT_NOT_CONNECTED -> {
alertIcon.postValue(R.drawable.bell_simple)
}
NETWORK_NOT_REACHABLE -> {
alertIcon.postValue(R.drawable.wifi_slash)
}
SINGLE_CALL, MULTIPLE_CALLS -> {
alertIcon.postValue(R.drawable.phone)
}
}
alertLabel.postValue(label)
coreContext.postOnMainThread {
val delayMs = if (type == SINGLE_CALL) 1000L else 0L
alertJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
delay(delayMs)
withContext(Dispatchers.Main) {
showAlert.value = true
changeSystemTopBarColorEvent.value = Event(type)
}
}
}
}
}
}
} }

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M213.92,210.62a8,8 0,1 1,-11.84 10.76l-52,-57.15a60,60 0,0 0,-57.41 7.24,8 8,0 1,1 -9.42,-12.93A75.43,75.43 0,0 1,128 144c1.28,0 2.55,0 3.82,0.1L104.9,114.49A108,108 0,0 0,61 135.31,8 8,0 0,1 49.73,134 8,8 0,0 1,51 122.77a124.27,124.27 0,0 1,41.71 -21.66L69.37,75.4a155.43,155.43 0,0 0,-40.29 24A8,8 0,0 1,18.92 87,171.87 171.87,0 0,1 58,62.86L42.08,45.38A8,8 0,1 1,53.92 34.62ZM128,192a12,12 0,1 0,12 12A12,12 0,0 0,128 192ZM237.08,87A172.3,172.3 0,0 0,106 49.4a8,8 0,1 0,2 15.87A158.33,158.33 0,0 1,128 64a156.25,156.25 0,0 1,98.92 35.37A8,8 0,0 0,237.08 87ZM195,135.31a8,8 0,0 0,11.24 -1.3,8 8,0 0,0 -1.3,-11.24 124.25,124.25 0,0 0,-51.73 -24.2A8,8 0,1 0,150 114.24,108.12 108.12,0 0,1 195,135.31Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -24,10 +24,10 @@
<include <include
android:id="@+id/in_call_top_bar" android:id="@+id/in_call_top_bar"
layout="@layout/main_activity_in_call_top_bar" layout="@layout/main_activity_notification_top_bar"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="@{viewModel.showTopBar ? View.VISIBLE : View.GONE, default=gone}" android:visibility="@{viewModel.showAlert ? View.VISIBLE : View.GONE, default=gone}"
app:viewModel="@{viewModel}" app:viewModel="@{viewModel}"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -16,53 +16,45 @@
android:background="@{viewModel.atLeastOneCall ? @color/green_success_500 : @color/red_danger_500, default=@color/red_danger_500}" android:background="@{viewModel.atLeastOneCall ? @color/green_success_500 : @color/red_danger_500, default=@color/red_danger_500}"
android:onClick="@{() -> viewModel.onTopBarClicked()}"> android:onClick="@{() -> viewModel.onTopBarClicked()}">
<ImageView
android:id="@+id/icon"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginStart="16dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:src="@{viewModel.alertIcon, default=@drawable/bell_simple}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:tint="@color/white" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300" style="@style/default_text_style_300"
android:id="@+id/error_label" android:id="@+id/label"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="5dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="5dp" android:layout_marginBottom="5dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:text="@string/connection_error_for_non_default_account" android:text="@{viewModel.alertLabel, default=@string/connection_error_for_non_default_account}"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" android:textSize="16sp"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" android:ellipsize="end"
android:drawableStart="@drawable/bell_simple" app:layout_constraintEnd_toStartOf="@id/end_barrier"
android:drawablePadding="10dp" app:layout_constraintStart_toEndOf="@id/icon"
android:drawableTint="@color/white"
android:visibility="@{viewModel.atLeastOneCall ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/close_notif"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/> app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView <androidx.constraintlayout.widget.Barrier
style="@style/default_text_style_800" android:id="@+id/end_barrier"
android:id="@+id/call_display_name" android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" app:barrierDirection="start"
android:layout_marginEnd="10dp" app:constraint_referenced_ids="call_time, close_notif" />
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:gravity="center_vertical"
android:text="@{viewModel.callLabel, default=`John Doe`}"
android:textColor="@color/white"
android:textSize="16sp"
android:maxLines="1"
android:ellipsize="end"
android:drawableStart="@drawable/phone"
android:drawablePadding="10dp"
android:drawableTint="@color/white"
android:visibility="@{viewModel.atLeastOneCall ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintEnd_toStartOf="@id/call_time"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style" style="@style/default_text_style"
@ -76,7 +68,7 @@
android:textSize="14sp" android:textSize="14sp"
android:visibility="@{viewModel.atLeastOneCall ? View.VISIBLE : View.GONE, default=gone}" android:visibility="@{viewModel.atLeastOneCall ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/call_display_name" app:layout_constraintStart_toEndOf="@id/label"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/> app:layout_constraintBottom_toBottomOf="parent"/>

View file

@ -457,6 +457,7 @@
<string name="conference_participants_list_title">%s participants</string> <string name="conference_participants_list_title">%s participants</string>
<string name="connection_error_for_non_default_account">Account connection error</string> <string name="connection_error_for_non_default_account">Account connection error</string>
<string name="network_not_reachable">You aren\'t connected to internet</string>
<!-- Keep <u></u> in following strings translations! --> <!-- Keep <u></u> in following strings translations! -->
<string name="welcome_carousel_skip"><u>Skip</u></string> <string name="welcome_carousel_skip"><u>Skip</u></string>