Added account settings

This commit is contained in:
Sylvain Berfini 2023-10-03 14:19:06 +02:00
parent e1c4be005f
commit de37ae245d
5 changed files with 466 additions and 9 deletions

View file

@ -24,6 +24,7 @@ import androidx.annotation.WorkerThread
import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import java.util.Locale
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
@ -42,10 +43,6 @@ import org.linphone.utils.Event
class ThirdPartySipAccountLoginViewModel @UiThread constructor() : ViewModel() { class ThirdPartySipAccountLoginViewModel @UiThread constructor() : ViewModel() {
companion object { companion object {
private const val TAG = "[Third Party SIP Account Login ViewModel]" private const val TAG = "[Third Party SIP Account Login ViewModel]"
private const val UDP = "UDP"
private const val TCP = "TCP"
private const val TLS = "TLS"
} }
val username = MutableLiveData<String>() val username = MutableLiveData<String>()
@ -134,9 +131,9 @@ class ThirdPartySipAccountLoginViewModel @UiThread constructor() : ViewModel() {
// TODO: handle formatting errors ? // TODO: handle formatting errors ?
availableTransports.add(UDP) availableTransports.add(TransportType.Udp.name.uppercase(Locale.getDefault()))
availableTransports.add(TCP) availableTransports.add(TransportType.Tcp.name.uppercase(Locale.getDefault()))
availableTransports.add(TLS) availableTransports.add(TransportType.Tls.name.uppercase(Locale.getDefault()))
} }
@UiThread @UiThread
@ -167,8 +164,8 @@ class ThirdPartySipAccountLoginViewModel @UiThread constructor() : ViewModel() {
val serverAddress = Factory.instance().createAddress("sip:$domainValue") val serverAddress = Factory.instance().createAddress("sip:$domainValue")
serverAddress?.transport = when (transport.value.orEmpty().trim()) { serverAddress?.transport = when (transport.value.orEmpty().trim()) {
TCP -> TransportType.Tcp TransportType.Tcp.name.uppercase(Locale.getDefault()) -> TransportType.Tcp
TLS -> TransportType.Tls TransportType.Tls.name.uppercase(Locale.getDefault()) -> TransportType.Tls
else -> TransportType.Udp else -> TransportType.Udp
} }
accountParams.serverAddress = serverAddress accountParams.serverAddress = serverAddress

View file

@ -4,11 +4,20 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import java.util.Locale
import org.linphone.R
import org.linphone.core.TransportType
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.AccountSettingsFragmentBinding import org.linphone.databinding.AccountSettingsFragmentBinding
import org.linphone.ui.main.fragment.GenericFragment import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.ui.main.settings.viewmodel.AccountSettingsViewModel
@UiThread @UiThread
class AccountSettingsFragment : GenericFragment() { class AccountSettingsFragment : GenericFragment() {
@ -20,6 +29,29 @@ class AccountSettingsFragment : GenericFragment() {
private val args: AccountSettingsFragmentArgs by navArgs() private val args: AccountSettingsFragmentArgs by navArgs()
private lateinit var viewModel: AccountSettingsViewModel
private val dropdownListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val transport = viewModel.availableTransports[position]
val transportType = when {
transport == TransportType.Tcp.name.uppercase(Locale.getDefault()) -> TransportType.Tcp
transport == TransportType.Tls.name.uppercase(Locale.getDefault()) -> TransportType.Tls
else -> TransportType.Udp
}
Log.i("$TAG Selected transport updated [$transport] -> [${transportType.name}]")
viewModel.selectedTransport.value = transportType
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
override fun goBack(): Boolean {
findNavController().popBackStack()
return true
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -31,14 +63,58 @@ class AccountSettingsFragment : GenericFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
viewModel = requireActivity().run {
ViewModelProvider(this)[AccountSettingsViewModel::class.java]
}
binding.viewModel = viewModel
val identity = args.accountIdentity val identity = args.accountIdentity
Log.i("$TAG Looking up for account with identity address [$identity]") Log.i("$TAG Looking up for account with identity address [$identity]")
viewModel.findAccountMatchingIdentity(identity)
binding.setBackClickListener { binding.setBackClickListener {
goBack() goBack()
} }
viewModel.accountFoundEvent.observe(viewLifecycleOwner) {
it.consume { found ->
if (found) {
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
val adapter = ArrayAdapter(
requireContext(),
R.layout.drop_down_item,
viewModel.availableTransports
)
adapter.setDropDownViewResource(R.layout.generic_dropdown_cell)
val currentTransport = viewModel.selectedTransport.value?.name?.uppercase(
Locale.getDefault()
)
binding.transportSpinner.adapter = adapter
binding.transportSpinner.setSelection(
viewModel.availableTransports.indexOf(currentTransport)
)
binding.transportSpinner.onItemSelectedListener = dropdownListener
}
} else {
Log.e(
"$TAG Failed to find an account matching this identity address [$identity]"
)
// TODO: show error
goBack()
}
}
}
}
override fun onPause() {
super.onPause()
viewModel.saveChanges()
} }
} }

View file

@ -0,0 +1,142 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.settings.viewmodel
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.Locale
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.AVPFMode
import org.linphone.core.Account
import org.linphone.core.Factory
import org.linphone.core.NatPolicy
import org.linphone.core.TransportType
import org.linphone.core.tools.Log
import org.linphone.utils.Event
class AccountSettingsViewModel @UiThread constructor() : ViewModel() {
companion object {
private const val TAG = "[Account Settings ViewModel]"
}
val availableTransports = arrayListOf<String>()
val selectedTransport = MutableLiveData<TransportType>()
val pushNotificationsEnabled = MutableLiveData<Boolean>()
val sipProxyServer = MutableLiveData<String>()
val outboundProxyEnabled = MutableLiveData<Boolean>()
val stunServer = MutableLiveData<String>()
val iceEnabled = MutableLiveData<Boolean>()
val avpfEnabled = MutableLiveData<Boolean>()
val expire = MutableLiveData<String>()
val accountFoundEvent = MutableLiveData<Event<Boolean>>()
private lateinit var account: Account
private lateinit var natPolicy: NatPolicy
init {
availableTransports.add(TransportType.Udp.name.uppercase(Locale.getDefault()))
availableTransports.add(TransportType.Tcp.name.uppercase(Locale.getDefault()))
availableTransports.add(TransportType.Tls.name.uppercase(Locale.getDefault()))
}
@UiThread
fun findAccountMatchingIdentity(identity: String) {
coreContext.postOnCoreThread { core ->
val found = core.accountList.find {
it.params.identityAddress?.asStringUriOnly() == identity
}
if (found != null) {
Log.i("$TAG Found matching account [$found]")
account = found
val params = account.params
pushNotificationsEnabled.postValue(params.pushNotificationAllowed)
val transportType = params.serverAddress?.transport ?: TransportType.Tls
selectedTransport.postValue(transportType)
sipProxyServer.postValue(params.serverAddress?.asStringUriOnly())
outboundProxyEnabled.postValue(params.isOutboundProxyEnabled)
natPolicy = params.natPolicy ?: core.createNatPolicy()
stunServer.postValue(natPolicy.stunServer)
iceEnabled.postValue(natPolicy.isIceEnabled)
avpfEnabled.postValue(account.isAvpfEnabled)
expire.postValue(params.expires.toString())
accountFoundEvent.postValue(Event(true))
} else {
Log.e("$TAG Failed to find account matching identity [$identity]")
accountFoundEvent.postValue(Event(false))
}
}
}
@UiThread
fun saveChanges() {
coreContext.postOnCoreThread { core ->
Log.i("$TAG Saving changes...")
if (::account.isInitialized) {
val newParams = account.params.clone()
newParams.pushNotificationAllowed = pushNotificationsEnabled.value == true
val server = sipProxyServer.value.orEmpty()
if (server.isNotEmpty()) {
val serverAddress = Factory.instance().createAddress(server)
if (serverAddress != null) {
serverAddress.transport = selectedTransport.value
newParams.serverAddress = serverAddress
}
}
newParams.isOutboundProxyEnabled = outboundProxyEnabled.value == true
if (::natPolicy.isInitialized) {
Log.i("$TAG Also applying changes to NAT policy")
natPolicy.stunServer = stunServer.value
natPolicy.isStunEnabled = stunServer.value.orEmpty().isNotEmpty()
natPolicy.isIceEnabled = iceEnabled.value == true
newParams.natPolicy = natPolicy
}
newParams.avpfMode = if (avpfEnabled.value == true) AVPFMode.Enabled else AVPFMode.Disabled
newParams.expires = expire.value?.toInt() ?: 31536000
account.params = newParams
Log.i("$TAG Changes have been saved")
}
}
}
}

View file

@ -8,6 +8,9 @@
<variable <variable
name="backClickListener" name="backClickListener"
type="View.OnClickListener" /> type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.settings.viewmodel.AccountSettingsViewModel" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -57,7 +60,238 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.materialswitch.MaterialSwitch
style="@style/material_switch_style"
android:id="@+id/push_notifications_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:checked="@={viewModel.pushNotificationsEnabled}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:id="@+id/push_notifications_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/account_settings_push_notification_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toTopOf="@id/push_notifications_switch"
app:layout_constraintBottom_toBottomOf="@id/push_notifications_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/push_notifications_switch"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:id="@+id/transport_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/assistant_sip_account_transport_protocol"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/push_notifications_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/transport_spinner"
android:layout_width="0dp"
android:layout_height="50dp"
android:background="@drawable/edit_text_background"
android:paddingStart="20dp"
android:paddingEnd="20dp"
app:layout_constraintTop_toBottomOf="@id/transport_title"
app:layout_constraintStart_toStartOf="@id/transport_title"
app:layout_constraintEnd_toEndOf="@id/transport_title" />
<ImageView
android:id="@+id/transport_spinner_caret"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:src="@drawable/caret_down"
app:layout_constraintTop_toTopOf="@id/transport_spinner"
app:layout_constraintBottom_toBottomOf="@id/transport_spinner"
app:layout_constraintEnd_toEndOf="@id/transport_spinner"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:id="@+id/sip_proxy_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/assistant_sip_account_sip_proxy_url_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/transport_spinner"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/sip_proxy"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:background="@drawable/edit_text_background"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@={viewModel.sipProxyServer}"
android:inputType="text|textUri"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/sip_proxy_title"/>
<com.google.android.material.materialswitch.MaterialSwitch
style="@style/material_switch_style"
android:id="@+id/outbound_proxy_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:checked="@={viewModel.outboundProxyEnabled}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/sip_proxy" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:id="@+id/outbound_proxy_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/account_settings_outbound_proxy_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toTopOf="@id/outbound_proxy_switch"
app:layout_constraintBottom_toBottomOf="@id/outbound_proxy_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/outbound_proxy_switch"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:id="@+id/stun_server_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/assistant_sip_account_stun_server_url_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/outbound_proxy_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/stun_server"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:background="@drawable/edit_text_background"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@={viewModel.stunServer}"
android:inputType="text|textUri"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/stun_server_title"/>
<com.google.android.material.materialswitch.MaterialSwitch
style="@style/material_switch_style"
android:id="@+id/ice_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:checked="@={viewModel.iceEnabled}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/stun_server" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:id="@+id/ice_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/assistant_sip_account_enable_ice_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toTopOf="@id/ice_switch"
app:layout_constraintBottom_toBottomOf="@id/ice_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/ice_switch"/>
<com.google.android.material.materialswitch.MaterialSwitch
style="@style/material_switch_style"
android:id="@+id/avpf_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:checked="@={viewModel.avpfEnabled}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/ice_switch" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:id="@+id/avpf_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/account_settings_avpf_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toTopOf="@id/avpf_switch"
app:layout_constraintBottom_toBottomOf="@id/avpf_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/avpf_switch"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:id="@+id/expire_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/assistant_sip_account_expire_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/avpf_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/expire_server"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:background="@drawable/edit_text_background"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@={viewModel.expire}"
android:inputType="number"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/expire_title"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -245,6 +245,14 @@
<string name="manage_account_device_remove">Remove</string> <string name="manage_account_device_remove">Remove</string>
<string name="manage_account_device_last_connection">Last connection:</string> <string name="manage_account_device_last_connection">Last connection:</string>
<string name="account_settings_push_notification_title">Allow push notifications</string>
<string name="assistant_sip_account_sip_proxy_url_title">SIP proxy server URL</string>
<string name="account_settings_outbound_proxy_title">Outbound proxy</string>
<string name="assistant_sip_account_stun_server_url_title">STUN server server URL</string>
<string name="assistant_sip_account_enable_ice_title">Enable ICE</string>
<string name="account_settings_avpf_title">AVPF</string>
<string name="assistant_sip_account_expire_title">Expire</string>
<string name="friend_presence_status_online">Online</string> <string name="friend_presence_status_online">Online</string>
<string name="friend_presence_status_was_online_on">Online on %s</string> <string name="friend_presence_status_was_online_on">Online on %s</string>
<string name="friend_presence_status_was_online_today_at">Online today at %s</string> <string name="friend_presence_status_was_online_today_at">Online today at %s</string>