- 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.
This commit is contained in:
Juha Heinanen
2019-11-22 15:17:14 +02:00
parent 1876dd76a2
commit 3308d3e561
7 changed files with 253 additions and 50 deletions
+1
View File
@@ -48,6 +48,7 @@ dependencies {
implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:design:28.0.0' implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support:cardview-v7:28.0.0'
} }
repositories { repositories {
@@ -1,8 +1,14 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.io.File
import java.util.ArrayList 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 { companion object {
@@ -14,7 +20,8 @@ class Contact(var name: String, var uri: String, var color: Int) {
fun save(): Boolean { fun save(): Boolean {
var contents = "" 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", return Utils.putFileContents(BaresipService.filesPath + "/contacts",
contents.toByteArray()) contents.toByteArray())
} }
@@ -25,16 +32,44 @@ class Contact(var name: String, var uri: String, var color: Int) {
val contacts = String(content) val contacts = String(content)
Api.contacts_remove() Api.contacts_remove()
BaresipService.contacts.clear() BaresipService.contacts.clear()
var contactNo = 0
val baseId = System.currentTimeMillis()
contacts.lines().forEach { contacts.lines().forEach {
val parts = it.split("\"") val parts = it.split("\"")
if (parts.size == 3) { if (parts.size == 3) {
contactNo++
val name = parts[1] val name = parts[1]
var uri = parts[2].trim() val uriParams = parts[2].trim()
if (uri.startsWith("<")) val uri = uriParams.substringAfter("<").substringBefore(">")
uri = uri.substringAfter("<").substringBefore(">") val params = uriParams.substringAfter(">;")
// Currently no need to make baresip aware of the contact val colorValue = Utils.paramValue(params, "color" )
// Api.contact_add("\"$name\" $uri") var color = 0
BaresipService.contacts.add(Contact(name, uri, Utils.randomColor())) 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 return true
@@ -2,41 +2,56 @@ package com.tutpro.baresip
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Bundle import android.os.Bundle
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.* 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() { 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 nameView: EditText
lateinit var uriView: EditText lateinit var uriView: EditText
internal var new = false internal var newContact = false
internal var newAvatar = ""
private var index = 0 private var index = 0
private var color = 0 private var color = 0
private var id: Long = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contact) 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 nameView = findViewById(R.id.Name) as EditText
uriView = findViewById(R.id.Uri) as EditText uriView = findViewById(R.id.Uri) as EditText
new = intent.getBooleanExtra("new", false) newContact = intent.getBooleanExtra("new", false)
val uOrI: String val uOrI: String
if (new) { if (newContact) {
title = getString(R.string.new_contact) title = getString(R.string.new_contact)
color = Utils.randomColor() color = Utils.randomColor()
val background = avatarView.background as GradientDrawable id = System.currentTimeMillis()
background.setColor(color) showTextAvatar("?", color)
nameView.setText("") nameView.setText("")
nameView.hint = getString(R.string.contact_name) nameView.hint = getString(R.string.contact_name)
nameView.setSelection(nameView.text.length) nameView.setSelection(nameView.text.length)
@@ -50,23 +65,83 @@ class ContactActivity : AppCompatActivity() {
uOrI = uri uOrI = uri
} else { } else {
index = intent.getIntExtra("index", 0) index = intent.getIntExtra("index", 0)
val name = Contact.contacts()[index].name val contact = Contact.contacts()[index]
color = Contact.contacts()[index].color val name = contact.name
(avatarView.background as GradientDrawable).setColor(color) color = contact.color
if (name.length > 0) id = contact.id
avatarView.text = "${name[0]}" val avatarImage = contact.avatarImage
setTitle(name) if (avatarImage != null)
showImageAvatar(avatarImage)
else
showTextAvatar(name, color)
title = name
nameView.setText(name) nameView.setText(name)
uriView.setText(Contact.contacts()[index].uri) uriView.setText(contact.uri)
uOrI = index.toString() uOrI = index.toString()
} }
avatarView.setOnClickListener { view -> textAvatarView.setOnClickListener { _ ->
color = Utils.randomColor() 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 return false
} }
val alert: Boolean val alert: Boolean
if (new) if (newContact)
alert = ContactsActivity.nameExists(newName, true) alert = ContactsActivity.nameExists(newName, true)
else else
alert = (Contact.contacts()[index].name != newName) && alert = (Contact.contacts()[index].name != newName) &&
@@ -123,7 +198,8 @@ class ContactActivity : AppCompatActivity() {
return false return false
} }
if (new) { val contact: Contact
if (newContact) {
if (Contact.contacts().size >= Contact.CONTACTS_SIZE) { if (Contact.contacts().size >= Contact.CONTACTS_SIZE) {
Utils.alertView(this, getString(R.string.notice), Utils.alertView(this, getString(R.string.notice),
String.format(getString(R.string.contacts_exceeded), String.format(getString(R.string.contacts_exceeded),
@@ -131,15 +207,33 @@ class ContactActivity : AppCompatActivity() {
BaresipService.activities.removeAt(0) BaresipService.activities.removeAt(0)
return true return true
} else { } else {
Contact.contacts().add(Contact(newName, newUri, color)) contact = Contact(newName, newUri, color, id)
Contact.contacts().add(contact)
} }
} else { } else {
Contact.contacts()[index].uri = newUri contact = Contact.contacts()[index]
Contact.contacts()[index].name = newName contact.uri = newUri
Contact.contacts()[index].color = color 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.contacts().sortBy { Contact -> Contact.name }
Contact.save() Contact.save()
i.putExtra("name", newName) 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)
}
} }
@@ -14,6 +14,9 @@ import android.widget.ArrayAdapter
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import java.io.File
import java.io.IOException
import java.util.* import java.util.*
class ContactListAdapter(private val cxt: Context, private val rows: ArrayList<Contact>, class ContactListAdapter(private val cxt: Context, private val rows: ArrayList<Contact>,
@@ -72,6 +75,16 @@ class ContactListAdapter(private val cxt: Context, private val rows: ArrayList<C
val dialogClickListener = DialogInterface.OnClickListener { _, which -> val dialogClickListener = DialogInterface.OnClickListener { _, which ->
when (which) { when (which) {
DialogInterface.BUTTON_POSITIVE -> { 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.contacts().removeAt(position)
Contact.save() Contact.save()
this.notifyDataSetChanged() this.notifyDataSetChanged()
@@ -18,6 +18,7 @@ import android.text.TextWatcher
import android.widget.* import android.widget.*
import android.view.* import android.view.*
import java.io.File
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -966,8 +967,12 @@ class MainActivity : AppCompatActivity() {
} }
private fun backup(password: String) { 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") "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 backupFilePath = BaresipService.downloadsPath + "/baresip.bs"
val zipFilePath = BaresipService.filesPath + "/baresip.zip" val zipFilePath = BaresipService.filesPath + "/baresip.zip"
if (!Utils.zip(files, "baresip.zip")) { if (!Utils.zip(files, "baresip.zip")) {
@@ -984,7 +989,7 @@ class MainActivity : AppCompatActivity() {
return return
} }
Utils.alertView(this, getString(R.string.info), getString(R.string.backed_up)) Utils.alertView(this, getString(R.string.info), getString(R.string.backed_up))
Utils.deleteFile(zipFilePath) Utils.deleteFile(File(zipFilePath))
} }
private fun restore(password: String) { private fun restore(password: String) {
@@ -1005,7 +1010,7 @@ class MainActivity : AppCompatActivity() {
"Failed to unzip file 'baresip.zip'") "Failed to unzip file 'baresip.zip'")
return return
} }
Utils.deleteFile(zipFilePath) Utils.deleteFile(File(zipFilePath))
Log.d("Baresip", "Showing restart dialog") Log.d("Baresip", "Showing restart dialog")
val restartDialog = AlertDialog.Builder(this) val restartDialog = AlertDialog.Builder(this)
restartDialog.setMessage(getString(R.string.restored)) restartDialog.setMessage(getString(R.string.restored))
@@ -6,6 +6,8 @@ import android.content.Context
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Bitmap.createScaledBitmap
import android.graphics.Color import android.graphics.Color
import android.net.LinkAddress import android.net.LinkAddress
import android.os.Bundle import android.os.Bundle
@@ -104,7 +106,7 @@ object Utils {
checkPort(parts[1]) checkPort(parts[1])
} }
fun checkE164Number(no: String): Boolean { private fun checkE164Number(no: String): Boolean {
return Regex("^[+][1-9][0-9]{0,14}\$").matches(no) return Regex("^[+][1-9][0-9]{0,14}\$").matches(no)
} }
@@ -121,7 +123,7 @@ object Utils {
checkIpV6(bracketedIp.substring(1, bracketedIp.length - 2)) checkIpV6(bracketedIp.substring(1, bracketedIp.length - 2))
} }
fun checkIp(ip: String): Boolean { private fun checkIp(ip: String): Boolean {
return checkIpV4(ip) || checkIpV6(ip) return checkIpV4(ip) || checkIpV6(ip)
} }
@@ -220,7 +222,7 @@ object Utils {
fun checkName(name: String): Boolean { fun checkName(name: String): Boolean {
return name.isNotEmpty() && name == String(name.toByteArray(), Charsets.UTF_8) && 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 { fun checkIfName(name: String): Boolean {
@@ -312,7 +314,6 @@ object Utils {
} }
} }
fun checkPermission(ctx: Context, permission: String) : Boolean { fun checkPermission(ctx: Context, permission: String) : Boolean {
return ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED return ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED
} }
@@ -343,11 +344,13 @@ object Utils {
} }
} }
fun deleteFile(filePath: String) { fun deleteFile(file: File) {
val file = File(filePath)
if (file.exists()) { if (file.exists()) {
Log.d("Baresip", "Deleting file '$filePath'") try {
file.delete() file.delete()
} catch (e: IOException) {
Log.e("Baresip", "Could not delete file ${file.absolutePath}")
}
} }
} }
@@ -374,6 +377,22 @@ object Utils {
return true 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): class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray):
Serializable { Serializable {
val serialVersionUID = -29238082928391L val serialVersionUID = -29238082928391L
@@ -449,7 +468,7 @@ object Utils {
return plainData return plainData
} }
fun zip(fileNames: Array<String>, zipFileName: String): Boolean { fun zip(fileNames: ArrayList<String>, zipFileName: String): Boolean {
val zipFilePath = BaresipService.filesPath + "/" + zipFileName val zipFilePath = BaresipService.filesPath + "/" + zipFileName
try { try {
ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFilePath))).use { out -> ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFilePath))).use { out ->
@@ -498,7 +517,7 @@ object Utils {
return false return false
} }
(allFiles - zipFiles).iterator().forEach { (allFiles - zipFiles).iterator().forEach {
deleteFile(BaresipService.filesPath + "/$it") deleteFile(File(BaresipService.filesPath, "$it"))
} }
return true return true
} }
+25 -6
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/ContactView" android:id="@+id/ContactView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -8,21 +8,40 @@
android:paddingRight="16dp" android:paddingRight="16dp"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingBottom="24dp" android:paddingBottom="24dp"
android:orientation="vertical" > android:orientation="vertical">
<TextView <TextView
android:id="@+id/Avatar" android:id="@+id/Avatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center_horizontal"
android:scaleType="centerInside" android:scaleType="centerInside"
android:background="@drawable/circle" android:background="@drawable/circle"
android:layout_width="36dp" android:textSize="72sp"
android:layout_height="36dp"
android:textSize="24sp"
android:textStyle="bold" android:textStyle="bold"
android:textAllCaps="true" android:textAllCaps="true"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:gravity="center" > android:gravity="center" >
</TextView> </TextView>
<android.support.v7.widget.CardView
android:id="@+id/CardAvatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center_horizontal"
android:elevation="12dp"
app:cardCornerRadius="48dp" >
<ImageView
android:id="@+id/ImageAvatar"
android:layout_height="90dp"
android:layout_width="96dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop" >
</ImageView>
</android.support.v7.widget.CardView>
<TextView <TextView
android:id="@+id/NameTitle" android:id="@+id/NameTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"