Added option to protect settings by account password
This commit is contained in:
parent
29ee69fc7b
commit
182d80a630
8 changed files with 155 additions and 9 deletions
|
@ -18,6 +18,7 @@ Group changes to describe their impact on the project, as follows:
|
||||||
- Attended transfer instead of blind transfer if there is more than 1 call
|
- Attended transfer instead of blind transfer if there is more than 1 call
|
||||||
- Added hidden setting to disable video completely
|
- Added hidden setting to disable video completely
|
||||||
- Added hidden setting to prevent adding / editing / removing native contacts
|
- Added hidden setting to prevent adding / editing / removing native contacts
|
||||||
|
- Added hidden setting to protect settings access using account password
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Account EXPIRES is now set to 1 month instead of 1 year for sip.linphone.org accounts
|
- Account EXPIRES is now set to 1 month instead of 1 year for sip.linphone.org accounts
|
||||||
|
|
|
@ -31,20 +31,18 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.activities.*
|
import org.linphone.activities.*
|
||||||
import org.linphone.activities.assistant.AssistantActivity
|
import org.linphone.activities.assistant.AssistantActivity
|
||||||
|
import org.linphone.activities.main.MainActivity
|
||||||
import org.linphone.activities.main.settings.SettingListenerStub
|
import org.linphone.activities.main.settings.SettingListenerStub
|
||||||
import org.linphone.activities.main.sidemenu.viewmodels.SideMenuViewModel
|
import org.linphone.activities.main.sidemenu.viewmodels.SideMenuViewModel
|
||||||
import org.linphone.activities.navigateToAbout
|
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||||
import org.linphone.activities.navigateToAccountSettings
|
import org.linphone.core.Factory
|
||||||
import org.linphone.activities.navigateToRecordings
|
|
||||||
import org.linphone.activities.navigateToSettings
|
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.databinding.SideMenuFragmentBinding
|
import org.linphone.databinding.SideMenuFragmentBinding
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.*
|
||||||
import org.linphone.utils.FileUtils
|
|
||||||
import org.linphone.utils.PermissionHelper
|
|
||||||
|
|
||||||
class SideMenuFragment : GenericFragment<SideMenuFragmentBinding>() {
|
class SideMenuFragment : GenericFragment<SideMenuFragmentBinding>() {
|
||||||
private lateinit var viewModel: SideMenuViewModel
|
private lateinit var viewModel: SideMenuViewModel
|
||||||
|
@ -87,9 +85,14 @@ class SideMenuFragment : GenericFragment<SideMenuFragmentBinding>() {
|
||||||
Log.i("[Side Menu] Navigation to settings for account with identity: $identity")
|
Log.i("[Side Menu] Navigation to settings for account with identity: $identity")
|
||||||
|
|
||||||
sharedViewModel.toggleDrawerEvent.value = Event(true)
|
sharedViewModel.toggleDrawerEvent.value = Event(true)
|
||||||
|
|
||||||
|
if (corePreferences.askForAccountPasswordToAccessSettings) {
|
||||||
|
showPasswordDialog(goToAccountSettings = true, accountIdentity = identity)
|
||||||
|
} else {
|
||||||
navigateToAccountSettings(identity)
|
navigateToAccountSettings(identity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.setSelfPictureClickListener {
|
binding.setSelfPictureClickListener {
|
||||||
pickFile()
|
pickFile()
|
||||||
|
@ -102,8 +105,13 @@ class SideMenuFragment : GenericFragment<SideMenuFragmentBinding>() {
|
||||||
|
|
||||||
binding.setSettingsClickListener {
|
binding.setSettingsClickListener {
|
||||||
sharedViewModel.toggleDrawerEvent.value = Event(true)
|
sharedViewModel.toggleDrawerEvent.value = Event(true)
|
||||||
|
|
||||||
|
if (corePreferences.askForAccountPasswordToAccessSettings) {
|
||||||
|
showPasswordDialog(goToSettings = true)
|
||||||
|
} else {
|
||||||
navigateToSettings()
|
navigateToSettings()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.setRecordingsClickListener {
|
binding.setRecordingsClickListener {
|
||||||
sharedViewModel.toggleDrawerEvent.value = Event(true)
|
sharedViewModel.toggleDrawerEvent.value = Event(true)
|
||||||
|
@ -172,4 +180,69 @@ class SideMenuFragment : GenericFragment<SideMenuFragmentBinding>() {
|
||||||
|
|
||||||
startActivityForResult(chooserIntent, 0)
|
startActivityForResult(chooserIntent, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showPasswordDialog(
|
||||||
|
goToSettings: Boolean = false,
|
||||||
|
goToAccountSettings: Boolean = false,
|
||||||
|
accountIdentity: String = ""
|
||||||
|
) {
|
||||||
|
val dialogViewModel = DialogViewModel(getString(R.string.settings_password_protection_dialog_title))
|
||||||
|
dialogViewModel.showIcon = true
|
||||||
|
dialogViewModel.iconResource = R.drawable.security_toggle_icon_green
|
||||||
|
dialogViewModel.showPassword = true
|
||||||
|
dialogViewModel.passwordTitle = getString(R.string.settings_password_protection_dialog_input_hint)
|
||||||
|
val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||||
|
|
||||||
|
dialogViewModel.showCancelButton {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogViewModel.showOkButton(
|
||||||
|
{
|
||||||
|
val defaultAccount = coreContext.core.defaultAccount ?: coreContext.core.accountList.firstOrNull()
|
||||||
|
if (defaultAccount == null) {
|
||||||
|
Log.e("[Side Menu] No account found, can't check password input!")
|
||||||
|
(requireActivity() as MainActivity).showSnackBar(R.string.error_unexpected)
|
||||||
|
} else {
|
||||||
|
val authInfo = defaultAccount.findAuthInfo()
|
||||||
|
if (authInfo == null) {
|
||||||
|
Log.e("[Side Menu] No auth info found for account [${defaultAccount.params.identityAddress?.asString()}], can't check password input!")
|
||||||
|
(requireActivity() as MainActivity).showSnackBar(R.string.error_unexpected)
|
||||||
|
} else {
|
||||||
|
val expectedHash = authInfo.ha1
|
||||||
|
if (expectedHash == null) {
|
||||||
|
Log.e("[Side Menu] No ha1 found in auth info, can't check password input!")
|
||||||
|
(requireActivity() as MainActivity).showSnackBar(R.string.error_unexpected)
|
||||||
|
} else {
|
||||||
|
val hashAlgorithm = authInfo.algorithm ?: "MD5"
|
||||||
|
val userId = (authInfo.userid ?: authInfo.username).orEmpty()
|
||||||
|
val realm = authInfo.realm.orEmpty()
|
||||||
|
val password = dialogViewModel.password
|
||||||
|
val computedHash = Factory.instance().computeHa1ForAlgorithm(
|
||||||
|
userId,
|
||||||
|
password,
|
||||||
|
realm,
|
||||||
|
hashAlgorithm
|
||||||
|
)
|
||||||
|
if (computedHash != expectedHash) {
|
||||||
|
Log.e("[Side Menu] Computed hash [$computedHash] using userId [$userId], realm [$realm] and algorithm [$hashAlgorithm] doesn't match expected hash!")
|
||||||
|
(requireActivity() as MainActivity).showSnackBar(R.string.settings_password_protection_dialog_invalid_input)
|
||||||
|
} else {
|
||||||
|
if (goToSettings) {
|
||||||
|
navigateToSettings()
|
||||||
|
} else if (goToAccountSettings) {
|
||||||
|
navigateToAccountSettings(accountIdentity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.dismiss()
|
||||||
|
},
|
||||||
|
getString(R.string.settings_password_protection_dialog_ok_label)
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,14 @@ class DialogViewModel(val message: String, val title: String = "") : ViewModel()
|
||||||
|
|
||||||
val dismissEvent = MutableLiveData<Event<Boolean>>()
|
val dismissEvent = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
var password: String = ""
|
||||||
|
|
||||||
|
var passwordTitle: String = ""
|
||||||
|
|
||||||
|
var passwordSubtitle: String = ""
|
||||||
|
|
||||||
|
var showPassword: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
doNotAskAgain.value = false
|
doNotAskAgain.value = false
|
||||||
showTitle = title.isNotEmpty()
|
showTitle = title.isNotEmpty()
|
||||||
|
|
|
@ -521,6 +521,9 @@ class CorePreferences constructor(private val context: Context) {
|
||||||
val autoRemoteProvisioningOnConfigUriHandler: Boolean
|
val autoRemoteProvisioningOnConfigUriHandler: Boolean
|
||||||
get() = config.getBool("app", "auto_apply_provisioning_config_uri_handler", false)
|
get() = config.getBool("app", "auto_apply_provisioning_config_uri_handler", false)
|
||||||
|
|
||||||
|
val askForAccountPasswordToAccessSettings: Boolean
|
||||||
|
get() = config.getBool("app", "require_password_to_access_settings", false)
|
||||||
|
|
||||||
/* Default values related */
|
/* Default values related */
|
||||||
|
|
||||||
val echoCancellerCalibration: Int
|
val echoCancellerCalibration: Int
|
||||||
|
|
|
@ -24,9 +24,12 @@ import android.content.Context
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkInfo
|
import android.net.NetworkInfo
|
||||||
import android.telephony.TelephonyManager.*
|
import android.telephony.TelephonyManager.*
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import okhttp3.internal.and
|
||||||
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
|
||||||
|
@ -257,5 +260,27 @@ class LinphoneUtils {
|
||||||
|
|
||||||
return true // Legacy behavior
|
return true // Legacy behavior
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hashPassword(
|
||||||
|
userId: String,
|
||||||
|
password: String,
|
||||||
|
realm: String,
|
||||||
|
algorithm: String = "MD5"
|
||||||
|
): String? {
|
||||||
|
val input = "$userId:$realm:$password"
|
||||||
|
try {
|
||||||
|
val digestEngine = MessageDigest.getInstance(algorithm)
|
||||||
|
val digest = digestEngine.digest(input.toByteArray())
|
||||||
|
val hexString = StringBuffer()
|
||||||
|
for (i in digest.indices) {
|
||||||
|
hexString.append(Integer.toHexString(digest[i].and(0xFF)))
|
||||||
|
}
|
||||||
|
return hexString.toString()
|
||||||
|
} catch (nsae: NoSuchAlgorithmException) {
|
||||||
|
Log.e("[Side Menu] Can't compute hash using [$algorithm] algorithm!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<import type="android.view.View"/>
|
<import type="android.view.View"/>
|
||||||
|
<import type="android.text.InputType"/>
|
||||||
<variable
|
<variable
|
||||||
name="viewModel"
|
name="viewModel"
|
||||||
type="org.linphone.activities.main.viewmodels.DialogViewModel" />
|
type="org.linphone.activities.main.viewmodels.DialogViewModel" />
|
||||||
|
@ -54,6 +55,31 @@
|
||||||
android:text="@string/assistant_forgotten_password_link"
|
android:text="@string/assistant_forgotten_password_link"
|
||||||
android:visibility="@{viewModel.showSubscribeLinphoneOrgLink ? View.VISIBLE : View.GONE}" />
|
android:visibility="@{viewModel.showSubscribeLinphoneOrgLink ? View.VISIBLE : View.GONE}" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/settings_input_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/settings_margin"
|
||||||
|
android:hint="@{viewModel.passwordTitle}"
|
||||||
|
android:textColorHint="@color/white_color"
|
||||||
|
android:visibility="@{viewModel.showPassword ? View.VISIBLE : View.GONE}"
|
||||||
|
app:helperTextTextColor="@color/white_color"
|
||||||
|
app:helperText="@{viewModel.passwordSubtitle}"
|
||||||
|
app:helperTextEnabled="@{viewModel.passwordSubtitle.length() > 0}">
|
||||||
|
|
||||||
|
<org.linphone.views.SettingTextInputEditText
|
||||||
|
android:id="@+id/settings_input"
|
||||||
|
android:text="@={viewModel.password}"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="@{InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD}"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:background="@color/transparent_color"
|
||||||
|
android:textColor="@color/white_color"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -774,4 +774,9 @@
|
||||||
<string name="chat_room_last_message_sender_format">%s :</string>
|
<string name="chat_room_last_message_sender_format">%s :</string>
|
||||||
<string name="dialog_apply_remote_provisioning_title">Voulez-vous télécharger et appliquer la configuration depuis cette adresse ?</string>
|
<string name="dialog_apply_remote_provisioning_title">Voulez-vous télécharger et appliquer la configuration depuis cette adresse ?</string>
|
||||||
<string name="dialog_apply_remote_provisioning_button">Appliquer</string>
|
<string name="dialog_apply_remote_provisioning_button">Appliquer</string>
|
||||||
|
<string name="error_unexpected">Erreur inattendue…</string>
|
||||||
|
<string name="settings_password_protection_dialog_title">Merci de saisir votre mot de passe ci-dessous pour accéder aux paramètres</string>
|
||||||
|
<string name="settings_password_protection_dialog_input_hint">Mot de passe</string>
|
||||||
|
<string name="settings_password_protection_dialog_ok_label">Valider</string>
|
||||||
|
<string name="settings_password_protection_dialog_invalid_input">Le mot de passe est invalide !</string>
|
||||||
</resources>
|
</resources>
|
|
@ -51,6 +51,7 @@
|
||||||
<string name="share_uploaded_logs_link">Share logs link using…</string>
|
<string name="share_uploaded_logs_link">Share logs link using…</string>
|
||||||
|
|
||||||
<!-- Errors -->
|
<!-- Errors -->
|
||||||
|
<string name="error_unexpected">Unexpected error…</string>
|
||||||
|
|
||||||
<!-- Date & Time -->
|
<!-- Date & Time -->
|
||||||
<string name="today">Today</string>
|
<string name="today">Today</string>
|
||||||
|
@ -475,6 +476,10 @@
|
||||||
<string name="settings_primary_account_title">Primary Account</string>
|
<string name="settings_primary_account_title">Primary Account</string>
|
||||||
<string name="settings_primary_account_display_name_title">Display Name</string>
|
<string name="settings_primary_account_display_name_title">Display Name</string>
|
||||||
<string name="settings_primary_account_username_title">Username</string>
|
<string name="settings_primary_account_username_title">Username</string>
|
||||||
|
<string name="settings_password_protection_dialog_title">Please input your password below to access the settings</string>
|
||||||
|
<string name="settings_password_protection_dialog_input_hint">Password</string>
|
||||||
|
<string name="settings_password_protection_dialog_ok_label">Validate</string>
|
||||||
|
<string name="settings_password_protection_dialog_invalid_input">Invalid password!</string>
|
||||||
|
|
||||||
<!-- Audio settings -->
|
<!-- Audio settings -->
|
||||||
<string name="audio_settings_echo_cancellation_title">Echo cancellation</string>
|
<string name="audio_settings_echo_cancellation_title">Echo cancellation</string>
|
||||||
|
|
Loading…
Reference in a new issue