From 3308d3e5610efd751fe4be749024b18accbf3abc Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Fri, 22 Nov 2019 15:17:14 +0200 Subject: [PATCH] - Started work on image avatars (only Contact Activity done so far). - Do not allow " character in contact's name, since it can be used as SIP diaplay name. --- app/build.gradle | 1 + .../main/kotlin/com/tutpro/baresip/Contact.kt | 51 +++++- .../com/tutpro/baresip/ContactActivity.kt | 157 +++++++++++++++--- .../com/tutpro/baresip/ContactListAdapter.kt | 13 ++ .../kotlin/com/tutpro/baresip/MainActivity.kt | 11 +- .../main/kotlin/com/tutpro/baresip/Utils.kt | 39 +++-- app/src/main/res/layout/activity_contact.xml | 31 +++- 7 files changed, 253 insertions(+), 50 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 15b14a97..34fc0aba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,6 +48,7 @@ dependencies { implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:design:28.0.0' + implementation 'com.android.support:cardview-v7:28.0.0' } repositories { diff --git a/app/src/main/kotlin/com/tutpro/baresip/Contact.kt b/app/src/main/kotlin/com/tutpro/baresip/Contact.kt index d1c3d9ed..a3502a45 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Contact.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Contact.kt @@ -1,8 +1,14 @@ package com.tutpro.baresip +import android.graphics.Bitmap +import android.graphics.BitmapFactory + +import java.io.File import java.util.ArrayList -class Contact(var name: String, var uri: String, var color: Int) { +class Contact(var name: String, var uri: String, var color: Int, val id: Long) { + + var avatarImage: Bitmap? = null companion object { @@ -14,7 +20,8 @@ class Contact(var name: String, var uri: String, var color: Int) { fun save(): Boolean { var contents = "" - for (c in BaresipService.contacts) contents += "\"${c.name}\" <${c.uri}>\n" + for (c in BaresipService.contacts) contents += + "\"${c.name}\" <${c.uri}>;id=${c.id};color=${c.color}\n" return Utils.putFileContents(BaresipService.filesPath + "/contacts", contents.toByteArray()) } @@ -25,16 +32,44 @@ class Contact(var name: String, var uri: String, var color: Int) { val contacts = String(content) Api.contacts_remove() BaresipService.contacts.clear() + var contactNo = 0 + val baseId = System.currentTimeMillis() contacts.lines().forEach { val parts = it.split("\"") if (parts.size == 3) { + contactNo++ val name = parts[1] - var uri = parts[2].trim() - if (uri.startsWith("<")) - uri = uri.substringAfter("<").substringBefore(">") - // Currently no need to make baresip aware of the contact - // Api.contact_add("\"$name\" $uri") - BaresipService.contacts.add(Contact(name, uri, Utils.randomColor())) + val uriParams = parts[2].trim() + val uri = uriParams.substringAfter("<").substringBefore(">") + val params = uriParams.substringAfter(">;") + val colorValue = Utils.paramValue(params, "color" ) + var color = 0 + if (colorValue != "") + color = colorValue.toInt() + else + color = Utils.randomColor() + val idValue = Utils.paramValue(params, "id" ) + val id: Long + if (idValue != "") + id = idValue.toLong() + else + id = baseId + contactNo + Log.d("Baresip", "Restoring contact $name, $uri, $color, $id") + val contact = Contact(name, uri, color, id) + val avatarFilePath = BaresipService.filesPath + "/$id.png" + if (File(avatarFilePath).exists()) { + try { + contact.avatarImage = BitmapFactory.decodeFile(avatarFilePath) + Log.d("Baresip", "Set avatarImage") + if (contact.avatarImage == null) + Log.d("Baresip", "Contact $id avatarImage is null") + } catch (e: Exception) { + Log.e("Baresip", "Could not read avatar image from '$id.img") + } + } else { + Log.d("Baresip", "Contact $id does not have avatarImage") + } + BaresipService.contacts.add(contact) } } return true diff --git a/app/src/main/kotlin/com/tutpro/baresip/ContactActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/ContactActivity.kt index ad05cc06..a5447a8a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ContactActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ContactActivity.kt @@ -2,41 +2,56 @@ package com.tutpro.baresip import android.app.Activity import android.content.Intent +import android.graphics.Bitmap import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.view.Menu import android.view.MenuItem import android.widget.* +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.support.v7.widget.CardView +import android.view.View + +import java.io.File + +private const val READ_REQUEST_CODE = 42 class ContactActivity : AppCompatActivity() { - lateinit var avatarView: TextView + lateinit var textAvatarView: TextView + lateinit var cardAvatarView: CardView + lateinit var imageAvatarView: ImageView lateinit var nameView: EditText lateinit var uriView: EditText - internal var new = false + internal var newContact = false + internal var newAvatar = "" private var index = 0 private var color = 0 + private var id: Long = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_contact) - avatarView = findViewById(R.id.Avatar) as TextView + textAvatarView = findViewById(R.id.Avatar) as TextView + cardAvatarView = findViewById(R.id.CardAvatar) as CardView + imageAvatarView = findViewById(R.id.ImageAvatar) as ImageView nameView = findViewById(R.id.Name) as EditText uriView = findViewById(R.id.Uri) as EditText - new = intent.getBooleanExtra("new", false) + newContact = intent.getBooleanExtra("new", false) val uOrI: String - if (new) { + if (newContact) { title = getString(R.string.new_contact) color = Utils.randomColor() - val background = avatarView.background as GradientDrawable - background.setColor(color) + id = System.currentTimeMillis() + showTextAvatar("?", color) nameView.setText("") nameView.hint = getString(R.string.contact_name) nameView.setSelection(nameView.text.length) @@ -50,23 +65,83 @@ class ContactActivity : AppCompatActivity() { uOrI = uri } else { index = intent.getIntExtra("index", 0) - val name = Contact.contacts()[index].name - color = Contact.contacts()[index].color - (avatarView.background as GradientDrawable).setColor(color) - if (name.length > 0) - avatarView.text = "${name[0]}" - setTitle(name) + val contact = Contact.contacts()[index] + val name = contact.name + color = contact.color + id = contact.id + val avatarImage = contact.avatarImage + if (avatarImage != null) + showImageAvatar(avatarImage) + else + showTextAvatar(name, color) + title = name nameView.setText(name) - uriView.setText(Contact.contacts()[index].uri) + uriView.setText(contact.uri) uOrI = index.toString() } - avatarView.setOnClickListener { view -> + textAvatarView.setOnClickListener { _ -> + color = Utils.randomColor() - (avatarView.background as GradientDrawable).setColor(color) + showTextAvatar(textAvatarView.text.toString(), color) + newAvatar = "text" + } - BaresipService.activities.add(0, "contact,$new,$uOrI") + textAvatarView.setOnLongClickListener { _ -> + + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "image/*" + } + + startActivityForResult(intent, READ_REQUEST_CODE) + + true + + } + + cardAvatarView.setOnClickListener { _ -> + + color = Utils.randomColor() + showTextAvatar(nameView.text.toString(), color) + newAvatar = "text" + + } + + cardAvatarView.setOnLongClickListener { _ -> + + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "image/*" + } + + startActivityForResult(intent, READ_REQUEST_CODE) + + true + + } + + BaresipService.activities.add(0, "contact,$newContact,$uOrI") + + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { + + if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + resultData?.data?.also { uri -> + Log.d("Baresip", "Uri: $uri") + try { + val inputStream = baseContext.contentResolver.openInputStream(uri) + val avatarImage = BitmapFactory.decodeStream(inputStream) + showImageAvatar(avatarImage) + if (Utils.saveBitmap(avatarImage, File(BaresipService.filesPath, "tmp.png"))) + newAvatar = "image" + } catch (e: Exception) { + Log.e("Baresip", "Could not read avatar image") + } + } + } } @@ -99,7 +174,7 @@ class ContactActivity : AppCompatActivity() { return false } val alert: Boolean - if (new) + if (newContact) alert = ContactsActivity.nameExists(newName, true) else alert = (Contact.contacts()[index].name != newName) && @@ -123,7 +198,8 @@ class ContactActivity : AppCompatActivity() { return false } - if (new) { + val contact: Contact + if (newContact) { if (Contact.contacts().size >= Contact.CONTACTS_SIZE) { Utils.alertView(this, getString(R.string.notice), String.format(getString(R.string.contacts_exceeded), @@ -131,15 +207,33 @@ class ContactActivity : AppCompatActivity() { BaresipService.activities.removeAt(0) return true } else { - Contact.contacts().add(Contact(newName, newUri, color)) + contact = Contact(newName, newUri, color, id) + Contact.contacts().add(contact) } } else { - Contact.contacts()[index].uri = newUri - Contact.contacts()[index].name = newName - Contact.contacts()[index].color = color + contact = Contact.contacts()[index] + contact.uri = newUri + contact.name = newName + contact.color = color + } + + when (newAvatar) { + "text" -> { + if (contact.avatarImage != null) { + contact.avatarImage = null + Utils.deleteFile(File(BaresipService.filesPath, "${contact.id}.png")) + } + } + "image" -> { + contact.avatarImage = (imageAvatarView.drawable as BitmapDrawable).bitmap + Utils.deleteFile(File(BaresipService.filesPath, "${contact.id}.png")) + File(BaresipService.filesPath, "tmp.png") + .renameTo(File(BaresipService.filesPath, "${contact.id}.png")) + } } Contact.contacts().sortBy { Contact -> Contact.name } + Contact.save() i.putExtra("name", newName) @@ -167,4 +261,21 @@ class ContactActivity : AppCompatActivity() { } + private fun showTextAvatar(name: String, color: Int) { + textAvatarView.visibility = View.VISIBLE + cardAvatarView.visibility = View.GONE + imageAvatarView.visibility = View.GONE + (textAvatarView.background as GradientDrawable).setColor(color) + if (name.isNotEmpty()) + textAvatarView.text = "${name[0]}" + else + textAvatarView.text = "" + } + + private fun showImageAvatar(image: Bitmap) { + textAvatarView.visibility = View.GONE + cardAvatarView.visibility = View.VISIBLE + imageAvatarView.visibility = View.VISIBLE + imageAvatarView.setImageBitmap(image) + } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/ContactListAdapter.kt b/app/src/main/kotlin/com/tutpro/baresip/ContactListAdapter.kt index 680176b1..dd7e5ae1 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ContactListAdapter.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ContactListAdapter.kt @@ -14,6 +14,9 @@ import android.widget.ArrayAdapter import android.widget.ImageButton import android.widget.TextView +import java.io.File +import java.io.IOException + import java.util.* class ContactListAdapter(private val cxt: Context, private val rows: ArrayList, @@ -72,6 +75,16 @@ class ContactListAdapter(private val cxt: Context, private val rows: ArrayList when (which) { DialogInterface.BUTTON_POSITIVE -> { + val contact = Contact.contacts()[position] + val id = contact.id + val avatarFile = File(BaresipService.filesPath, "$id.img") + if (avatarFile.exists()) { + try { + avatarFile.delete() + } catch (e: IOException) { + Log.e("Baresip", "Could not delete file '$id.img") + } + } Contact.contacts().removeAt(position) Contact.save() this.notifyDataSetChanged() diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index 7a2ea5d6..5bc771f3 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -18,6 +18,7 @@ import android.text.TextWatcher import android.widget.* import android.view.* +import java.io.File import kotlin.collections.ArrayList class MainActivity : AppCompatActivity() { @@ -966,8 +967,12 @@ class MainActivity : AppCompatActivity() { } private fun backup(password: String) { - val files = arrayOf("accounts", "calls", "config", "contacts", "messages", "uuid", + val files = arrayListOf("accounts", "calls", "config", "contacts", "messages", "uuid", "zrtp_cache.dat", "zrtp_zid", "cert.pem", "ca_cert", "ca_certs.crt") + File(BaresipService.filesPath).walk().forEach { + if (it.name.endsWith(".png")) files.add(it.name) + } + Log.d("Baresip", "Backing up files $files") val backupFilePath = BaresipService.downloadsPath + "/baresip.bs" val zipFilePath = BaresipService.filesPath + "/baresip.zip" if (!Utils.zip(files, "baresip.zip")) { @@ -984,7 +989,7 @@ class MainActivity : AppCompatActivity() { return } Utils.alertView(this, getString(R.string.info), getString(R.string.backed_up)) - Utils.deleteFile(zipFilePath) + Utils.deleteFile(File(zipFilePath)) } private fun restore(password: String) { @@ -1005,7 +1010,7 @@ class MainActivity : AppCompatActivity() { "Failed to unzip file 'baresip.zip'") return } - Utils.deleteFile(zipFilePath) + Utils.deleteFile(File(zipFilePath)) Log.d("Baresip", "Showing restart dialog") val restartDialog = AlertDialog.Builder(this) restartDialog.setMessage(getString(R.string.restored)) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index b6f5dcd0..603ba7ef 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -6,6 +6,8 @@ import android.content.Context import android.support.v7.app.AlertDialog import android.content.Intent import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Bitmap.createScaledBitmap import android.graphics.Color import android.net.LinkAddress import android.os.Bundle @@ -104,7 +106,7 @@ object Utils { checkPort(parts[1]) } - fun checkE164Number(no: String): Boolean { + private fun checkE164Number(no: String): Boolean { return Regex("^[+][1-9][0-9]{0,14}\$").matches(no) } @@ -121,7 +123,7 @@ object Utils { checkIpV6(bracketedIp.substring(1, bracketedIp.length - 2)) } - fun checkIp(ip: String): Boolean { + private fun checkIp(ip: String): Boolean { return checkIpV4(ip) || checkIpV6(ip) } @@ -220,7 +222,7 @@ object Utils { fun checkName(name: String): Boolean { return name.isNotEmpty() && name == String(name.toByteArray(), Charsets.UTF_8) && - name.lines().size == 1 + name.lines().size == 1 && !name.contains('"') } fun checkIfName(name: String): Boolean { @@ -312,7 +314,6 @@ object Utils { } } - fun checkPermission(ctx: Context, permission: String) : Boolean { return ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED } @@ -343,11 +344,13 @@ object Utils { } } - fun deleteFile(filePath: String) { - val file = File(filePath) + fun deleteFile(file: File) { if (file.exists()) { - Log.d("Baresip", "Deleting file '$filePath'") - file.delete() + try { + file.delete() + } catch (e: IOException) { + Log.e("Baresip", "Could not delete file ${file.absolutePath}") + } } } @@ -374,6 +377,22 @@ object Utils { return true } + fun saveBitmap(bitmap: Bitmap, file: File): Boolean { + if (file.exists()) file.delete() + try { + val out = FileOutputStream(file) + val scaledBitmap = createScaledBitmap (bitmap, 96, 96, true) + scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, out) + out.flush() + out.close() + Log.e("Baresip", "Saved bitmap to ${file.absolutePath} of length ${file.length()}") + } catch (e: Exception) { + Log.e("Baresip", "Failed to save bitmap to ${file.absolutePath}") + return false + } + return true + } + class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray): Serializable { val serialVersionUID = -29238082928391L @@ -449,7 +468,7 @@ object Utils { return plainData } - fun zip(fileNames: Array, zipFileName: String): Boolean { + fun zip(fileNames: ArrayList, zipFileName: String): Boolean { val zipFilePath = BaresipService.filesPath + "/" + zipFileName try { ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFilePath))).use { out -> @@ -498,7 +517,7 @@ object Utils { return false } (allFiles - zipFiles).iterator().forEach { - deleteFile(BaresipService.filesPath + "/$it") + deleteFile(File(BaresipService.filesPath, "$it")) } return true } diff --git a/app/src/main/res/layout/activity_contact.xml b/app/src/main/res/layout/activity_contact.xml index 0e0ce374..6b2d7095 100644 --- a/app/src/main/res/layout/activity_contact.xml +++ b/app/src/main/res/layout/activity_contact.xml @@ -1,6 +1,6 @@ - + android:orientation="vertical"> + + + + + + +