- 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

View File

@ -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 {

View File

@ -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

View File

@ -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)
}
}

View File

@ -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<Contact>,
@ -72,6 +75,16 @@ class ContactListAdapter(private val cxt: Context, private val rows: ArrayList<C
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
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()

View File

@ -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))

View File

@ -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<String>, zipFileName: String): Boolean {
fun zip(fileNames: ArrayList<String>, 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
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/ContactView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -8,21 +8,40 @@
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="24dp"
android:orientation="vertical" >
android:orientation="vertical">
<TextView
android:id="@+id/Avatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center_horizontal"
android:scaleType="centerInside"
android:background="@drawable/circle"
android:layout_width="36dp"
android:layout_height="36dp"
android:textSize="24sp"
android:textSize="72sp"
android:textStyle="bold"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:gravity="center" >
</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
android:id="@+id/NameTitle"
android:layout_width="wrap_content"