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.findNavController
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
@ -107,20 +104,17 @@ class MainActivity : AppCompatActivity() {
viewModel.changeSystemTopBarColorEvent.observe(this) {
it.consume { mode ->
val color = when (mode) {
MainViewModel.IN_CALL -> AppUtils.getColor(R.color.green_success_500)
MainViewModel.ACCOUNT_REGISTRATION_FAILURE -> AppUtils.getColor(
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
}
window.statusBarColor = when (mode) {
MainViewModel.SINGLE_CALL, MainViewModel.MULTIPLE_CALLS -> {
AppUtils.getColor(R.color.green_success_500)
}
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.lifecycle.MutableLiveData
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.R
import org.linphone.core.Account
@ -40,16 +47,21 @@ class MainViewModel @UiThread constructor() : ViewModel() {
private const val TAG = "[Main ViewModel]"
const val NONE = 0
const val ACCOUNT_REGISTRATION_FAILURE = 1
const val IN_CALL = 2
const val NON_DEFAULT_ACCOUNT_NOTIFICATIONS = 5
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 callLabel = MutableLiveData<String>()
val callsStatus = MutableLiveData<String>()
val defaultAccountRegistrationErrorEvent: MutableLiveData<Event<Boolean>> by lazy {
@ -68,13 +80,24 @@ class MainViewModel @UiThread constructor() : ViewModel() {
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() {
@WorkerThread
override fun onLastCallEnded(core: Core) {
Log.i("$TAG Last call ended, asking fragment to change back status bar color")
changeSystemTopBarColorEvent.postValue(Event(NONE))
Log.i("$TAG Last call ended, removing in-call 'alert'")
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
@ -84,12 +107,28 @@ class MainViewModel @UiThread constructor() : ViewModel() {
state: Call.State?,
message: String
) {
if (core.callsNb > 0) {
updateCurrentCallInfo()
if (
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
@ -105,17 +144,12 @@ class MainViewModel @UiThread constructor() : ViewModel() {
Log.e("$TAG Default account registration failed!")
defaultAccountRegistrationFailed = true
defaultAccountRegistrationErrorEvent.postValue(Event(true))
} else {
} else if (core.isNetworkReachable) {
Log.e("$TAG Non-default account registration failed!")
// Do not show connection error top bar if there is a call
if (atLeastOneCall.value == false) {
changeSystemTopBarColorEvent.postValue(
Event(
ACCOUNT_REGISTRATION_FAILURE
)
)
showTopBar.postValue(true)
}
val label = AppUtils.getString(
R.string.connection_error_for_non_default_account
)
addAlert(SINGLE_CALL, label)
}
}
RegistrationState.Ok -> {
@ -129,11 +163,7 @@ class MainViewModel @UiThread constructor() : ViewModel() {
it.state == RegistrationState.Failed
}
if (found == null) {
Log.i("$TAG No account in Failed state anymore")
if (atLeastOneCall.value == false) {
changeSystemTopBarColorEvent.postValue(Event(NONE))
showTopBar.postValue(false)
}
removeAlert(NON_DEFAULT_ACCOUNT_NOT_CONNECTED)
}
}
}
@ -144,18 +174,21 @@ class MainViewModel @UiThread constructor() : ViewModel() {
init {
defaultAccountRegistrationFailed = false
showTopBar.value = false
showAlert.value = false
coreContext.postOnCoreThread { core ->
core.addListener(coreListener)
if (core.callsNb > 0) {
updateCurrentCallInfo()
if (!core.isNetworkReachable) {
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
showTopBar.postValue(calls)
atLeastOneCall.postValue(calls)
if (core.callsNb > 0) {
updateCallAlert()
}
atLeastOneCall.postValue(core.callsNb > 0)
}
}
@ -170,7 +203,7 @@ class MainViewModel @UiThread constructor() : ViewModel() {
@UiThread
fun closeTopBar() {
showTopBar.value = false
showAlert.value = false
}
@UiThread
@ -183,38 +216,121 @@ class MainViewModel @UiThread constructor() : ViewModel() {
}
@WorkerThread
private fun updateCurrentCallInfo() {
private fun updateCallAlert() {
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()
if (currentCall != null) {
val contact = coreContext.contactsManager.findContactByAddress(
currentCall.remoteAddress
)
if (contact != null) {
callLabel.postValue(
contact.name ?: LinphoneUtils.getDisplayName(currentCall.remoteAddress)
)
val label = if (contact != null) {
contact.name ?: LinphoneUtils.getDisplayName(currentCall.remoteAddress)
} else {
val conferenceInfo = coreContext.core.findConferenceInformationFromUri(
currentCall.remoteAddress
)
callLabel.postValue(
conferenceInfo?.subject ?: LinphoneUtils.getDisplayName(
currentCall.remoteAddress
)
conferenceInfo?.subject ?: LinphoneUtils.getDisplayName(
currentCall.remoteAddress
)
}
addAlert(SINGLE_CALL, label)
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")
changeSystemTopBarColorEvent.postValue(Event(IN_CALL))
} else {
callLabel.postValue(
AppUtils.getFormattedString(R.string.calls_count_label, core.callsNb)
addAlert(
MULTIPLE_CALLS,
AppUtils.getFormattedString(R.string.calls_count_label, callsNb)
)
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
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_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:layout_constraintTop_toTopOf="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: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
style="@style/default_text_style_300"
android:id="@+id/error_label"
android:id="@+id/label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
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:textSize="16sp"
android:maxLines="1"
android:ellipsize="end"
android:drawableStart="@drawable/bell_simple"
android:drawablePadding="10dp"
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_constraintEnd_toStartOf="@id/end_barrier"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/call_display_name"
android:layout_width="0dp"
<androidx.constraintlayout.widget.Barrier
android:id="@+id/end_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
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"/>
app:barrierDirection="start"
app:constraint_referenced_ids="call_time, close_notif" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
@ -76,7 +68,7 @@
android:textSize="14sp"
android:visibility="@{viewModel.atLeastOneCall ? View.VISIBLE : View.GONE, default=gone}"
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_constraintBottom_toBottomOf="parent"/>

View file

@ -457,6 +457,7 @@
<string name="conference_participants_list_title">%s participants</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! -->
<string name="welcome_carousel_skip"><u>Skip</u></string>