mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Improved account avatar
This commit is contained in:
parent
157c233ab1
commit
b93f75aade
6 changed files with 239 additions and 80 deletions
|
|
@ -27,13 +27,17 @@ import org.linphone.LinphoneApplication.Companion.coreContext
|
|||
import org.linphone.core.Account
|
||||
import org.linphone.core.AccountListenerStub
|
||||
import org.linphone.core.RegistrationState
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class AccountModel @WorkerThread constructor(
|
||||
private val account: Account,
|
||||
private val onMenuClicked: ((view: View, account: Account) -> Unit)? = null
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "[Account Model]"
|
||||
}
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val avatar = MutableLiveData<String>()
|
||||
|
|
@ -57,6 +61,9 @@ class AccountModel @WorkerThread constructor(
|
|||
state: RegistrationState?,
|
||||
message: String
|
||||
) {
|
||||
Log.i(
|
||||
"$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] registration state changed: [$state]($message)"
|
||||
)
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
|
@ -64,7 +71,6 @@ class AccountModel @WorkerThread constructor(
|
|||
init {
|
||||
account.addListener(accountListener)
|
||||
|
||||
avatar.postValue(account.getPicturePath())
|
||||
showTrust.postValue(account.isInSecureMode())
|
||||
|
||||
update()
|
||||
|
|
@ -97,10 +103,19 @@ class AccountModel @WorkerThread constructor(
|
|||
|
||||
@WorkerThread
|
||||
private fun update() {
|
||||
Log.i(
|
||||
"$TAG Refreshing info for account [${account.params.identityAddress?.asStringUriOnly()}]"
|
||||
)
|
||||
|
||||
val name = LinphoneUtils.getDisplayName(account.params.identityAddress)
|
||||
displayName.postValue(name)
|
||||
|
||||
initials.postValue(LinphoneUtils.getInitials(name))
|
||||
|
||||
val pictureUri = account.params.pictureUri.orEmpty()
|
||||
avatar.postValue(pictureUri)
|
||||
Log.i("$TAG Account picture URI is [$pictureUri]")
|
||||
|
||||
isDefault.postValue(coreContext.core.defaultAccount == account)
|
||||
|
||||
val state = when (account.state) {
|
||||
|
|
@ -118,15 +133,6 @@ class AccountModel @WorkerThread constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun Account.getPicturePath(): String {
|
||||
// TODO FIXME: get image path from account
|
||||
return FileUtils.getFileStoragePath(
|
||||
"john.jpg",
|
||||
isImage = true,
|
||||
overrideExisting = true
|
||||
).absolutePath
|
||||
}
|
||||
|
||||
fun Account.isInSecureMode(): Boolean {
|
||||
// TODO FIXME
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AccountProfileFragmentBinding
|
||||
|
|
@ -38,23 +36,15 @@ class AccountProfileFragment : GenericFragment() {
|
|||
|
||||
private val pickMedia = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
|
||||
if (uri != null) {
|
||||
val identity = "john" // TODO FIXME: use account identity
|
||||
val localFileName = FileUtils.getFileStoragePath(
|
||||
"$identity.jpg", // TODO FIXME: use correct file extension
|
||||
isImage = true,
|
||||
overrideExisting = true
|
||||
)
|
||||
Log.i("$TAG Picture picked [$uri], will be stored as [${localFileName.absolutePath}]")
|
||||
|
||||
Log.i("$TAG Picture picked [$uri]")
|
||||
lifecycleScope.launch {
|
||||
if (FileUtils.copyFile(uri, localFileName)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
viewModel.setImage(localFileName)
|
||||
}
|
||||
val localFileName = FileUtils.getFilePath(requireContext(), uri, true)
|
||||
if (localFileName != null) {
|
||||
Log.i("$TAG Picture will be locally stored as [$localFileName]")
|
||||
val path = FileUtils.getProperFilePath(localFileName)
|
||||
viewModel.picturePath.postValue(path)
|
||||
} else {
|
||||
Log.e(
|
||||
"$TAG Failed to copy file from [$uri] to [${localFileName.absolutePath}]"
|
||||
)
|
||||
Log.e("$TAG Failed to copy [$uri] to local storage")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,10 @@ package org.linphone.ui.main.settings.viewmodel
|
|||
import androidx.annotation.UiThread
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import java.io.File
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Account
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.ui.main.model.getPicturePath
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class AccountProfileViewModel @UiThread constructor() : ViewModel() {
|
||||
companion object {
|
||||
|
|
@ -35,11 +32,11 @@ class AccountProfileViewModel @UiThread constructor() : ViewModel() {
|
|||
it.params.identityAddress?.asStringUriOnly() == identity
|
||||
}
|
||||
if (found != null) {
|
||||
Log.i("$TAG Found matching local friend [$found]")
|
||||
Log.i("$TAG Found matching account [$found]")
|
||||
account = found
|
||||
sipAddress.postValue(account.params.identityAddress?.asStringUriOnly())
|
||||
displayName.postValue(account.params.identityAddress?.displayName)
|
||||
picturePath.postValue(account.getPicturePath())
|
||||
picturePath.postValue(account.params.pictureUri)
|
||||
internationalPrefix.postValue(account.params.internationalPrefix)
|
||||
|
||||
accountFoundEvent.postValue(Event(true))
|
||||
|
|
@ -58,6 +55,12 @@ class AccountProfileViewModel @UiThread constructor() : ViewModel() {
|
|||
|
||||
copy.internationalPrefix = internationalPrefix.value.orEmpty()
|
||||
|
||||
val newPictureUri = picturePath.value
|
||||
if (!newPictureUri.isNullOrEmpty() && newPictureUri != params.pictureUri) {
|
||||
Log.i("$TAG New account profile picture [$newPictureUri]")
|
||||
copy.pictureUri = newPictureUri
|
||||
}
|
||||
|
||||
val address = params.identityAddress?.clone()
|
||||
if (address != null) {
|
||||
val newValue = displayName.value.orEmpty().trim()
|
||||
|
|
@ -65,23 +68,13 @@ class AccountProfileViewModel @UiThread constructor() : ViewModel() {
|
|||
copy.identityAddress = address
|
||||
// This will trigger a REGISTER, so account display name will be updated by
|
||||
// CoreListener.onAccountRegistrationStateChanged everywhere in the app
|
||||
account.params = copy
|
||||
Log.i(
|
||||
"$TAG Updated account [$account] identity address display name [$newValue]"
|
||||
"$TAG Updated account [${params.identityAddress?.asStringUriOnly()}] identity address display name [$newValue]"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun setImage(file: File) {
|
||||
val path = FileUtils.getProperFilePath(file.absolutePath)
|
||||
picturePath.value = path
|
||||
|
||||
coreContext.postOnCoreThread {
|
||||
if (::account.isInitialized) {
|
||||
// TODO: save image path in account
|
||||
account.params = copy
|
||||
account.refreshRegister()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ class DrawerMenuViewModel @UiThread constructor() : ViewModel() {
|
|||
coreContext.postOnCoreThread {
|
||||
computeAccountsList()
|
||||
}
|
||||
|
||||
// TODO FIXME: account avatar not refreshed...
|
||||
}
|
||||
|
||||
@UiThread
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import androidx.databinding.BindingAdapter
|
|||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import io.getstream.avatarview.AvatarView
|
||||
|
|
@ -170,6 +171,7 @@ fun AppCompatTextView.setDrawableTint(@ColorInt color: Int) {
|
|||
@UiThread
|
||||
@BindingAdapter("coil")
|
||||
fun loadPictureWithCoil(imageView: ImageView, file: String?) {
|
||||
Log.i("[Data Binding Utils] Loading file [$file] with coil")
|
||||
if (file != null) {
|
||||
imageView.load(file) {
|
||||
transformations(CircleCropTransformation())
|
||||
|
|
@ -180,6 +182,7 @@ fun loadPictureWithCoil(imageView: ImageView, file: String?) {
|
|||
@UiThread
|
||||
@BindingAdapter("coilContact")
|
||||
fun loadContactPictureWithCoil2(imageView: ImageView, contact: ContactData?) {
|
||||
Log.i("[Data Binding Utils] Loading contact picture [${contact?.avatar}] with coil")
|
||||
if (contact == null) {
|
||||
imageView.load(R.drawable.contact_avatar)
|
||||
} else {
|
||||
|
|
@ -205,34 +208,61 @@ fun ImageView.setPresenceIcon(presence: ConsolidatedPresence?) {
|
|||
@UiThread
|
||||
@BindingAdapter("avatarInitials")
|
||||
fun AvatarView.loadInitials(initials: String?) {
|
||||
Log.i("[Data Binding Utils] Displaying initials [$initials] on AvatarView")
|
||||
avatarInitials = initials.orEmpty()
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@BindingAdapter("accountAvatar")
|
||||
fun AvatarView.loadAccountAvatar(account: AccountModel?) {
|
||||
Log.i("[Data Binding Utils] Loading account picture [${account?.avatar?.value}] with coil")
|
||||
if (account == null) {
|
||||
loadImage(R.drawable.contact_avatar)
|
||||
} else {
|
||||
val uri = account.avatar.value
|
||||
loadImage(
|
||||
data = uri,
|
||||
onStart = {
|
||||
// Use initials as placeholder
|
||||
avatarInitials = account.initials.value.orEmpty()
|
||||
val lifecycleOwner = findViewTreeLifecycleOwner()
|
||||
if (lifecycleOwner != null) {
|
||||
account.avatar.observe(lifecycleOwner) { uri ->
|
||||
loadImage(
|
||||
data = uri,
|
||||
onStart = {
|
||||
// Use initials as placeholder
|
||||
avatarInitials = account.initials.value.orEmpty()
|
||||
|
||||
if (account.showTrust.value == true) {
|
||||
avatarBorderColor = resources.getColor(R.color.trusted_blue, context.theme)
|
||||
avatarBorderWidth = AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt()
|
||||
} else {
|
||||
avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt()
|
||||
}
|
||||
},
|
||||
onSuccess = { _, _ ->
|
||||
// If loading is successful, remove initials otherwise image won't be visible
|
||||
avatarInitials = ""
|
||||
if (account.showTrust.value == true) {
|
||||
avatarBorderColor =
|
||||
resources.getColor(R.color.trusted_blue, context.theme)
|
||||
avatarBorderWidth =
|
||||
AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt()
|
||||
} else {
|
||||
avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt()
|
||||
}
|
||||
},
|
||||
onSuccess = { _, _ ->
|
||||
// If loading is successful, remove initials otherwise image won't be visible
|
||||
avatarInitials = ""
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
loadImage(
|
||||
data = account.avatar.value,
|
||||
onStart = {
|
||||
// Use initials as placeholder
|
||||
avatarInitials = account.initials.value.orEmpty()
|
||||
|
||||
if (account.showTrust.value == true) {
|
||||
avatarBorderColor = resources.getColor(R.color.trusted_blue, context.theme)
|
||||
avatarBorderWidth = AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt()
|
||||
} else {
|
||||
avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt()
|
||||
}
|
||||
},
|
||||
onSuccess = { _, _ ->
|
||||
// If loading is successful, remove initials otherwise image won't be visible
|
||||
avatarInitials = ""
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -242,25 +272,53 @@ fun AvatarView.loadContactAvatar(contact: ContactAvatarModel?) {
|
|||
if (contact == null) {
|
||||
loadImage(R.drawable.contact_avatar)
|
||||
} else {
|
||||
val uri = contact.avatar.value
|
||||
loadImage(
|
||||
data = uri,
|
||||
onStart = {
|
||||
// Use initials as placeholder
|
||||
avatarInitials = contact.initials
|
||||
val lifecycleOwner = findViewTreeLifecycleOwner()
|
||||
if (lifecycleOwner != null) {
|
||||
contact.avatar.observe(lifecycleOwner) { uri ->
|
||||
loadImage(
|
||||
data = uri,
|
||||
onStart = {
|
||||
// Use initials as placeholder
|
||||
avatarInitials = contact.initials
|
||||
|
||||
if (contact.showTrust.value == true) {
|
||||
avatarBorderColor = resources.getColor(R.color.trusted_blue, context.theme)
|
||||
avatarBorderWidth = AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt()
|
||||
} else {
|
||||
avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt()
|
||||
}
|
||||
},
|
||||
onSuccess = { _, _ ->
|
||||
// If loading is successful, remove initials otherwise image won't be visible
|
||||
avatarInitials = ""
|
||||
if (contact.showTrust.value == true) {
|
||||
avatarBorderColor =
|
||||
resources.getColor(R.color.trusted_blue, context.theme)
|
||||
avatarBorderWidth =
|
||||
AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt()
|
||||
} else {
|
||||
avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt()
|
||||
}
|
||||
},
|
||||
onSuccess = { _, _ ->
|
||||
// If loading is successful, remove initials otherwise image won't be visible
|
||||
avatarInitials = ""
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
val uri = contact.avatar.value
|
||||
loadImage(
|
||||
data = uri,
|
||||
onStart = {
|
||||
// Use initials as placeholder
|
||||
avatarInitials = contact.initials
|
||||
|
||||
if (contact.showTrust.value == true) {
|
||||
avatarBorderColor =
|
||||
resources.getColor(R.color.trusted_blue, context.theme)
|
||||
avatarBorderWidth =
|
||||
AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt()
|
||||
} else {
|
||||
avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt()
|
||||
}
|
||||
},
|
||||
onSuccess = { _, _ ->
|
||||
// If loading is successful, remove initials otherwise image won't be visible
|
||||
avatarInitials = ""
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,19 +19,36 @@
|
|||
*/
|
||||
package org.linphone.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.database.CursorIndexOutOfBoundsException
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.Process
|
||||
import android.provider.OpenableColumns
|
||||
import android.system.Os
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.AnyThread
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class FileUtils {
|
||||
enum class MimeType {
|
||||
PlainText,
|
||||
Pdf,
|
||||
Image,
|
||||
Video,
|
||||
Audio,
|
||||
Unknown
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "[File Utils]"
|
||||
|
||||
|
|
@ -82,6 +99,38 @@ class FileUtils {
|
|||
return file
|
||||
}
|
||||
|
||||
suspend fun getFilePath(context: Context, uri: Uri, overrideExisting: Boolean): String? {
|
||||
val name: String = getNameFromUri(uri, context)
|
||||
try {
|
||||
if (Os.fstat(
|
||||
ParcelFileDescriptor.open(
|
||||
File(uri.path),
|
||||
ParcelFileDescriptor.MODE_READ_ONLY
|
||||
).fileDescriptor
|
||||
).st_uid != Process.myUid()
|
||||
) {
|
||||
Log.e("$TAG File descriptor UID different from our, denying copy!")
|
||||
return null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("$TAG Can't check file ownership: ", e)
|
||||
}
|
||||
|
||||
val extension = getExtensionFromFileName(name)
|
||||
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
val isImage = getMimeType(type) == MimeType.Image
|
||||
|
||||
try {
|
||||
val localFile: File = getFileStoragePath(name, isImage, overrideExisting)
|
||||
copyFile(uri, localFile)
|
||||
return localFile.absolutePath
|
||||
} catch (e: Exception) {
|
||||
Log.e("$TAG Can't copy file in local storage: ", e)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
suspend fun copyFile(from: Uri, to: File): Boolean {
|
||||
try {
|
||||
|
|
@ -146,5 +195,66 @@ class FileUtils {
|
|||
|
||||
return returnPath
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
private fun getNameFromUri(uri: Uri, context: Context): String {
|
||||
var name = ""
|
||||
if (uri.scheme == "content") {
|
||||
val returnCursor =
|
||||
context.contentResolver.query(uri, null, null, null, null)
|
||||
if (returnCursor != null) {
|
||||
returnCursor.moveToFirst()
|
||||
val nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex != -1) {
|
||||
try {
|
||||
val displayName = returnCursor.getString(nameIndex)
|
||||
if (displayName != null) {
|
||||
name = displayName
|
||||
} else {
|
||||
Log.e(
|
||||
"$TAG Failed to get the display name for URI $uri, returned value is null"
|
||||
)
|
||||
}
|
||||
} catch (e: CursorIndexOutOfBoundsException) {
|
||||
Log.e(
|
||||
"$TAG Failed to get the display name for URI $uri, exception is $e"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Log.e("$TAG Couldn't get DISPLAY_NAME column index for URI: $uri")
|
||||
}
|
||||
returnCursor.close()
|
||||
}
|
||||
} else if (uri.scheme == "file") {
|
||||
name = uri.lastPathSegment ?: ""
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
private fun getExtensionFromFileName(fileName: String): String {
|
||||
var extension = MimeTypeMap.getFileExtensionFromUrl(fileName)
|
||||
if (extension.isNullOrEmpty()) {
|
||||
val i = fileName.lastIndexOf('.')
|
||||
if (i > 0) {
|
||||
extension = fileName.substring(i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
return extension.lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
private fun getMimeType(type: String?): MimeType {
|
||||
if (type.isNullOrEmpty()) return MimeType.Unknown
|
||||
return when {
|
||||
type.startsWith("image/") -> MimeType.Image
|
||||
type.startsWith("text/plain") -> MimeType.PlainText
|
||||
type.startsWith("video/") -> MimeType.Video
|
||||
type.startsWith("audio/") -> MimeType.Audio
|
||||
type.startsWith("application/pdf") -> MimeType.Pdf
|
||||
else -> MimeType.Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue