Added sharing/removal of contact

This commit is contained in:
Sylvain Berfini 2023-08-18 13:50:53 +02:00
parent 715e6dc8be
commit ab15d05ffd
11 changed files with 204 additions and 41 deletions

View file

@ -41,6 +41,8 @@ class AssistantViewModel : ViewModel() {
val password = MutableLiveData<String>()
val showPassword = MutableLiveData<Boolean>()
val loginEnabled = MediatorLiveData<Boolean>()
val registrationInProgress = MutableLiveData<Boolean>()
@ -84,6 +86,7 @@ class AssistantViewModel : ViewModel() {
}
init {
showPassword.value = false
registrationInProgress.value = false
loginEnabled.addSource(username) {
@ -125,6 +128,11 @@ class AssistantViewModel : ViewModel() {
}
}
fun toggleShowPassword() {
// UI thread
showPassword.value = showPassword.value == false
}
private fun isLoginButtonEnabled(): Boolean {
return username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
}

View file

@ -29,11 +29,13 @@ import android.provider.ContactsContract
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.FileProvider
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import java.io.File
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.ContactFragmentBinding
@ -45,6 +47,10 @@ import org.linphone.utils.DialogUtils
import org.linphone.utils.Event
class ContactFragment : GenericFragment() {
companion object {
const val TAG = "[Contact Fragment]"
}
private lateinit var binding: ContactFragmentBinding
private lateinit var viewModel: ContactViewModel
@ -82,6 +88,16 @@ class ContactFragment : GenericFragment() {
goBack()
}
binding.setShareClickListener {
viewModel.exportContactAsVCard()
}
binding.setDeleteClickListener {
viewModel.deleteContact()
goBack()
// TODO: show toast ?
}
sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) { slideable ->
viewModel.showBackButton.value = slideable
}
@ -143,6 +159,13 @@ class ContactFragment : GenericFragment() {
findNavController().navigate(action)
}
}
viewModel.vCardTerminatedEvent.observe(viewLifecycleOwner) {
it.consume { file ->
Log.i("$TAG Friend was exported as vCard file [${file.absolutePath}]")
shareContact(file)
}
}
}
private fun copyNumberOrAddressToClipboard(value: String, isSip: Boolean) {
@ -155,4 +178,23 @@ class ContactFragment : GenericFragment() {
R.drawable.check
)
}
private fun shareContact(file: File) {
val publicUri = FileProvider.getUriForFile(
requireContext(),
requireContext().getString(R.string.file_provider),
file
)
Log.i("$TAG Public URI for vCard file is [$publicUri], starting intent chooser")
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, publicUri)
putExtra(Intent.EXTRA_SUBJECT, "John Doe")
type = ContactsContract.Contacts.CONTENT_VCARD_TYPE
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
}
}

View file

@ -21,16 +21,26 @@ package org.linphone.ui.main.contacts.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.io.File
import java.util.Locale
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Address
import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.contacts.model.ContactDeviceModel
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
class ContactViewModel : ViewModel() {
companion object {
const val TAG = "[Contact ViewModel]"
}
val contact = MutableLiveData<ContactAvatarModel>()
val sipAddressesAndPhoneNumbers = MutableLiveData<ArrayList<ContactNumberOrAddressModel>>()
@ -67,6 +77,10 @@ class ContactViewModel : ViewModel() {
MutableLiveData<Event<String>>()
}
val vCardTerminatedEvent: MutableLiveData<Event<File>> by lazy {
MutableLiveData<Event<File>>()
}
private val listener = object : ContactNumberOrAddressClickListener {
override fun onClicked(address: Address?) {
// UI thread
@ -181,12 +195,52 @@ class ContactViewModel : ViewModel() {
fun editContact() {
// UI thread
val uri = contact.value?.friend?.nativeUri
if (uri != null) {
openLinphoneContactEditor.value = Event(contact.value?.id.orEmpty())
// TODO FIXME : openNativeContactEditor.value = Event(uri)
} else {
openLinphoneContactEditor.value = Event(contact.value?.id.orEmpty())
coreContext.postOnCoreThread {
if (::friend.isInitialized) {
val uri = friend.nativeUri
if (uri != null) {
openNativeContactEditor.postValue(Event(uri))
} else {
openLinphoneContactEditor.postValue(Event(contact.value?.id.orEmpty()))
}
}
}
}
fun exportContactAsVCard() {
// UI thread
coreContext.postOnCoreThread {
if (::friend.isInitialized) {
val vCard = friend.vcard?.asVcard4String()
if (!vCard.isNullOrEmpty()) {
Log.i("$TAG Friend has been successfully dumped as vCard string")
val fileName = friend.name.orEmpty().replace(" ", "_").toLowerCase(
Locale.getDefault()
)
val file = FileUtils.getFileStorageCacheDir("$fileName.vcf")
viewModelScope.launch {
if (FileUtils.dumpStringToFile(vCard, file)) {
Log.i("$TAG vCard string saved as file in cache folder")
vCardTerminatedEvent.postValue(Event(file))
} else {
Log.e("$TAG Failed to save vCard string as file in cache folder")
}
}
} else {
Log.e("$TAG Failed to dump contact as vCard string")
}
}
}
}
fun deleteContact() {
// UI thread
coreContext.postOnCoreThread { core ->
if (::friend.isInitialized) {
Log.i("$TAG Deleting friend [$friend]")
friend.remove()
coreContext.contactsManager.notifyContactsListChanged()
}
}
}

View file

@ -47,6 +47,20 @@ class FileUtils {
return file
}
fun getFileStorageCacheDir(fileName: String): File {
val path = coreContext.context.cacheDir
Log.i("$TAG Cache directory is: $path")
var file = File(path, fileName)
var prefix = 1
while (file.exists()) {
file = File(path, prefix.toString() + "_" + fileName)
Log.w("$TAG File with that name already exists, renamed to ${file.name}")
prefix += 1
}
return file
}
suspend fun copyFile(from: Uri, to: File): Boolean {
try {
withContext(Dispatchers.IO) {
@ -68,6 +82,25 @@ class FileUtils {
return false
}
suspend fun dumpStringToFile(data: String, to: File): Boolean {
try {
withContext(Dispatchers.IO) {
FileOutputStream(to).use { outputStream ->
val inputStream = data.byteInputStream()
val buffer = ByteArray(4096)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } >= 0) {
outputStream.write(buffer, 0, bytesRead)
}
}
}
return true
} catch (e: IOException) {
Log.e("$TAG dumpStringToFile [$data] to [$to] exception: $e")
}
return false
}
private fun getFileStorageDir(isPicture: Boolean = false): File {
var path: File? = null
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_1"
android:pathData="M 12 21.175 L 10.55 19.855 C 5.4 15.185 2 12.105 2 8.325 C 2 5.245 4.42 2.825 7.5 2.825 C 9.24 2.825 10.91 3.635 12 4.915 C 13.09 3.635 14.76 2.825 16.5 2.825 C 19.58 2.825 22 5.245 22 8.325 C 22 12.105 18.6 15.185 13.45 19.865 L 12 21.175 Z"
android:fillColor="#000000"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:pathData="M 12 6.05 C 15.79 6.05 19.17 8.18 20.82 11.55 C 20.23 12.77 19.4 13.82 18.41 14.67 L 19.82 16.08 C 21.21 14.85 22.31 13.31 23 11.55 C 21.27 7.16 17 4.05 12 4.05 C 10.73 4.05 9.51 4.25 8.36 4.62 L 10.01 6.27 C 10.66 6.14 11.32 6.05 12 6.05 Z M 10.93 7.19 L 13 9.26 C 13.57 9.51 14.03 9.97 14.28 10.54 L 16.35 12.61 C 16.43 12.27 16.49 11.91 16.49 11.54 C 16.5 9.06 14.48 7.05 12 7.05 C 11.63 7.05 11.28 7.1 10.93 7.19 Z M 2.01 3.92 L 4.69 6.6 C 3.06 7.88 1.77 9.58 1 11.55 C 2.73 15.94 7 19.05 12 19.05 C 13.52 19.05 14.98 18.76 16.32 18.23 L 19.74 21.65 L 21.15 20.24 L 3.42 2.5 L 2.01 3.92 Z M 9.51 11.42 L 12.12 14.03 C 12.08 14.04 12.04 14.05 12 14.05 C 10.62 14.05 9.5 12.93 9.5 11.55 C 9.5 11.5 9.51 11.47 9.51 11.42 Z M 6.11 8.02 L 7.86 9.77 C 7.63 10.32 7.5 10.92 7.5 11.55 C 7.5 14.03 9.52 16.05 12 16.05 C 12.63 16.05 13.23 15.92 13.77 15.69 L 14.75 16.67 C 13.87 16.91 12.95 17.05 12 17.05 C 8.21 17.05 4.83 14.92 3.18 11.55 C 3.88 10.12 4.9 8.94 6.11 8.02 Z"
android:fillColor="#000000"
android:strokeWidth="1"/>
</vector>

View file

@ -5,6 +5,7 @@
<data>
<import type="android.view.View" />
<import type="android.text.InputType" />
<variable
name="backClickListener"
type="View.OnClickListener" />
@ -108,13 +109,23 @@
android:textSize="14sp"
android:textColor="@color/gray_9"
android:background="@drawable/shape_edit_text_background"
android:inputType="textPassword"
android:drawableEnd="@drawable/show_password"
android:drawableTint="@color/gray_1"
android:inputType="@{viewModel.showPassword ? InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : InputType.TYPE_TEXT_VARIATION_PASSWORD, default=textPassword}"
app:layout_constraintTop_toBottomOf="@id/password_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:onClick="@{() -> viewModel.toggleShowPassword()}"
android:id="@+id/show_password"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="20dp"
android:src="@{viewModel.showPassword ? @drawable/hide_password : @drawable/show_password, default=@drawable/show_password}"
app:tint="@color/gray_1"
app:layout_constraintEnd_toEndOf="@id/password"
app:layout_constraintTop_toTopOf="@id/password"
app:layout_constraintBottom_toBottomOf="@id/password" />
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.login()}"
android:enabled="@{viewModel.loginEnabled &amp;&amp; !viewModel.registrationInProgress, default=false}"

View file

@ -9,6 +9,12 @@
<variable
name="backClickListener"
type="View.OnClickListener" />
<variable
name="shareClickListener"
type="View.OnClickListener" />
<variable
name="deleteClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.contacts.viewmodel.ContactViewModel" />
@ -491,9 +497,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="17dp"
android:layout_marginEnd="17dp"
android:text="@{viewModel.isFavourite() ? `Remove from favourites` : `Add to favourites`, default=`Add to favourites`}"
android:drawableStart="@drawable/favorite"
android:drawableTint="@{viewModel.isFavourite() ? @color/red_danger : @color/gray_1, default=@color/gray_1}"
android:text="@{viewModel.isFavourite ? `Remove from favourites` : `Add to favourites`, default=`Add to favourites`}"
android:drawableStart="@{viewModel.isFavourite ? @drawable/favourite : @drawable/not_favourite, default=@drawable/favourite}"
android:drawableTint="@{viewModel.isFavourite ? @color/red_danger : @color/gray_1, default=@color/gray_1}"
app:layout_constraintTop_toBottomOf="@id/action_edit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
@ -509,6 +515,7 @@
app:layout_constraintTop_toBottomOf="@+id/action_favorite"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{shareClickListener}"
style="@style/context_menu_action_label_style"
android:id="@+id/action_share"
android:layout_width="0dp"
@ -532,29 +539,7 @@
app:layout_constraintTop_toBottomOf="@+id/action_share"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/context_menu_action_label_style"
android:id="@+id/action_invite"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="17dp"
android:layout_marginEnd="17dp"
android:text="Invite"
android:drawableStart="@drawable/invite"
app:layout_constraintTop_toBottomOf="@id/action_share"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="17dp"
android:layout_marginEnd="17dp"
android:background="@color/blue_outgoing_message"
app:layout_constraintStart_toStartOf="@id/action_invite"
app:layout_constraintEnd_toEndOf="@id/action_invite"
app:layout_constraintTop_toBottomOf="@+id/action_invite"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{deleteClickListener}"
style="@style/context_menu_danger_action_label_style"
android:id="@+id/action_delete"
android:layout_width="0dp"
@ -563,7 +548,7 @@
android:layout_marginEnd="17dp"
android:text="Delete"
android:drawableStart="@drawable/delete"
app:layout_constraintTop_toBottomOf="@id/action_invite"
app:layout_constraintTop_toBottomOf="@id/action_share"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>

View file

@ -46,7 +46,7 @@
android:textColor="@color/gray_9"
android:background="@drawable/shape_edit_text_background"
android:maxLines="1"
android:inputType="@{model.isSip ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS : InputType.TYPE_CLASS_PHONE}"
android:inputType="@{model.isSip ? InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS : InputType.TYPE_CLASS_PHONE}"
app:layout_constraintTop_toBottomOf="@id/label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/remove"/>

View file

@ -16,6 +16,9 @@
<variable
name="deleteClickListener"
type="View.OnClickListener" />
<variable
name="isFavourite"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -24,15 +27,16 @@
android:background="@color/separator">
<androidx.appcompat.widget.AppCompatTextView
style="@style/context_menu_action_label_style"
android:id="@+id/favorite"
android:onClick="@{favoriteClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Mettre en favoris"
style="@style/context_menu_action_label_style"
android:background="@color/gray_2"
android:layout_marginBottom="1dp"
android:drawableStart="@drawable/favorite"
android:background="@color/gray_2"
android:text="@{isFavourite ? `Remove from favourites` : `Add to favourites`, default=`Add to favourites`}"
android:drawableStart="@{isFavourite ? @drawable/favourite : @drawable/not_favourite, default=@drawable/favourite}"
android:drawableTint="@{isFavourite ? @color/red_danger : @color/gray_1, default=@color/gray_1}"
app:layout_constraintBottom_toTopOf="@id/share"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>