498 lines
19 KiB
Kotlin
498 lines
19 KiB
Kotlin
package com.tutpro.baresip
|
|
|
|
import android.app.Activity
|
|
import android.content.ContentProviderOperation
|
|
import android.content.ContentValues
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.database.Cursor
|
|
import android.graphics.Bitmap
|
|
import android.graphics.BitmapFactory
|
|
import android.graphics.Matrix
|
|
import android.graphics.drawable.BitmapDrawable
|
|
import android.os.Bundle
|
|
import android.provider.ContactsContract
|
|
import android.provider.ContactsContract.CommonDataKinds
|
|
import android.provider.ContactsContract.Contacts.Data
|
|
import android.view.Menu
|
|
import android.view.MenuItem
|
|
import android.view.View
|
|
import android.widget.*
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.cardview.widget.CardView
|
|
import androidx.exifinterface.media.ExifInterface
|
|
import com.tutpro.baresip.databinding.ActivityContactBinding
|
|
import java.io.ByteArrayOutputStream
|
|
import java.io.File
|
|
|
|
class ContactActivity : AppCompatActivity() {
|
|
|
|
private lateinit var binding: ActivityContactBinding
|
|
private lateinit var layout: LinearLayout
|
|
private lateinit var textAvatarView: TextView
|
|
private lateinit var cardAvatarView: CardView
|
|
private lateinit var cardImageAvatarView: ImageView
|
|
private lateinit var nameView: EditText
|
|
private lateinit var uriView: EditText
|
|
private lateinit var androidCheck: CheckBox
|
|
private lateinit var menu: Menu
|
|
|
|
private var newContact = false
|
|
private var newAvatar = ""
|
|
private var uOrI = ""
|
|
|
|
private var index = 0
|
|
private var color = 0
|
|
private var id: Long = 0
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
|
binding = ActivityContactBinding.inflate(layoutInflater)
|
|
setContentView(binding.root)
|
|
layout = binding.ContactView
|
|
|
|
textAvatarView = binding.TextAvatar
|
|
cardAvatarView = binding.CardAvatar
|
|
cardImageAvatarView = binding.ImageAvatar
|
|
nameView = binding.Name
|
|
uriView = binding.Uri
|
|
androidCheck = binding.Android
|
|
|
|
newContact = intent.getBooleanExtra("new", false)
|
|
|
|
if (newContact) {
|
|
if (BaresipService.contactsMode == "baresip") {
|
|
binding.AndroidTitle.visibility = View.GONE
|
|
androidCheck.visibility = View.GONE
|
|
}
|
|
title = getString(R.string.new_contact)
|
|
color = Utils.randomColor()
|
|
id = System.currentTimeMillis()
|
|
showTextAvatar("?", color)
|
|
nameView.setText("")
|
|
nameView.hint = getString(R.string.contact_name)
|
|
nameView.setSelection(nameView.text.length)
|
|
val uri = intent.getStringExtra("uri")!!
|
|
if (uri == "")
|
|
uriView.setText("")
|
|
else
|
|
uriView.setText(uri)
|
|
uOrI = uri
|
|
if ((BaresipService.contactsMode == "Android")) {
|
|
androidCheck.isChecked = true
|
|
androidCheck.isClickable = false
|
|
} else {
|
|
androidCheck.isChecked = false
|
|
}
|
|
} else {
|
|
binding.AndroidTitle.visibility = View.GONE
|
|
androidCheck.visibility = View.GONE
|
|
index = intent.getIntExtra("index", 0)
|
|
val contact = Contact.contacts()[index] as Contact.BaresipContact
|
|
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.uri)
|
|
uOrI = index.toString()
|
|
}
|
|
|
|
val avatarRequest =
|
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if (it.resultCode == Activity.RESULT_OK) {
|
|
it.data?.data?.also { uri ->
|
|
try {
|
|
val inputStream = baseContext.contentResolver.openInputStream(uri)
|
|
val avatarBitmap = BitmapFactory.decodeStream(inputStream)
|
|
inputStream?.close()
|
|
val scaledBitmap = Bitmap.createScaledBitmap(avatarBitmap, 192, 192, true)
|
|
val exif = ExifInterface(baseContext.contentResolver.openInputStream(uri)!!)
|
|
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
|
|
ExifInterface.ORIENTATION_NORMAL)
|
|
val rotatedBitmap = rotateBitmap(scaledBitmap, orientation)
|
|
showImageAvatar(rotatedBitmap)
|
|
if (Utils.saveBitmap(rotatedBitmap, File(BaresipService.filesPath, "tmp.png")))
|
|
newAvatar = "image"
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Could not read avatar image: $e")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
textAvatarView.setOnClickListener {
|
|
|
|
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
type = "image/*"
|
|
}
|
|
avatarRequest.launch(intent)
|
|
|
|
}
|
|
|
|
textAvatarView.setOnLongClickListener {
|
|
|
|
color = Utils.randomColor()
|
|
showTextAvatar(textAvatarView.text.toString(), color)
|
|
newAvatar = "text"
|
|
true
|
|
|
|
}
|
|
|
|
cardAvatarView.setOnClickListener {
|
|
|
|
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
type = "image/*"
|
|
}
|
|
avatarRequest.launch(intent)
|
|
|
|
}
|
|
|
|
cardAvatarView.setOnLongClickListener {
|
|
|
|
color = Utils.randomColor()
|
|
showTextAvatar(nameView.text.toString(), color)
|
|
newAvatar = "text"
|
|
true
|
|
|
|
}
|
|
|
|
binding.AndroidTitle.setOnClickListener {
|
|
Utils.alertView(this, getString(R.string.android),
|
|
getString(R.string.android_contact_help))
|
|
}
|
|
|
|
// Log.d(TAG, "Android sip contacts ${logAndroidContacts(this, "sip")}")
|
|
// Log.d(TAG, "Android tel contacts ${logAndroidContacts(this, "tel")}")
|
|
|
|
Utils.addActivity("contact,$newContact,$uOrI")
|
|
|
|
}
|
|
|
|
override fun onCreateOptionsMenu(optionsMenu: Menu): Boolean {
|
|
|
|
super.onCreateOptionsMenu(optionsMenu)
|
|
|
|
val inflater = menuInflater
|
|
inflater.inflate(R.menu.check_icon, optionsMenu)
|
|
menu = optionsMenu
|
|
return true
|
|
|
|
}
|
|
|
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
|
|
if (BaresipService.activities.indexOf("contact,$newContact,$uOrI") == -1) return true
|
|
|
|
when (item.itemId) {
|
|
|
|
R.id.checkIcon -> {
|
|
|
|
var newName = nameView.text.toString().trim()
|
|
var newUri = uriView.text.toString().filterNot{setOf('-', ' ').contains(it)}
|
|
|
|
if (newName == "") newName = newUri
|
|
|
|
if (!Utils.checkName(newName)) {
|
|
Utils.alertView(this, getString(R.string.notice),
|
|
String.format(getString(R.string.invalid_contact), newName))
|
|
return false
|
|
}
|
|
|
|
val alert: Boolean = if (newContact)
|
|
Contact.nameExists(newName, true)
|
|
else {
|
|
val c = Contact.contacts()[index] as Contact.BaresipContact
|
|
(c.name != newName) && Contact.nameExists(newName, false)
|
|
}
|
|
if (alert) {
|
|
Utils.alertView(this, getString(R.string.notice),
|
|
String.format(getString(R.string.contact_already_exists), newName))
|
|
return false
|
|
}
|
|
|
|
if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:"))
|
|
newUri = if (Utils.isTelNumber(newUri))
|
|
"tel:$newUri"
|
|
else
|
|
"sip:$newUri"
|
|
if (!Utils.checkUri(newUri)) {
|
|
Utils.alertView(this, getString(R.string.notice),
|
|
String.format(getString(R.string.invalid_sip_or_tel_uri), newUri))
|
|
return false
|
|
}
|
|
|
|
val contact: Contact.BaresipContact
|
|
|
|
if (newContact) {
|
|
if (Contact.contacts().size >= Contact.CONTACTS_SIZE) {
|
|
Utils.alertView(this, getString(R.string.notice),
|
|
String.format(getString(R.string.contacts_exceeded),
|
|
Contact.CONTACTS_SIZE))
|
|
BaresipService.activities.removeAt(0)
|
|
return true
|
|
} else {
|
|
contact = Contact.BaresipContact(newName, newUri, color, id)
|
|
}
|
|
} else {
|
|
contact = Contact.contacts()[index] as Contact.BaresipContact
|
|
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 = (cardImageAvatarView.drawable as BitmapDrawable).bitmap
|
|
Utils.deleteFile(File(BaresipService.filesPath, "${contact.id}.png"))
|
|
File(BaresipService.filesPath, "tmp.png")
|
|
.renameTo(File(BaresipService.filesPath, "${contact.id}.png"))
|
|
}
|
|
}
|
|
|
|
if (androidCheck.isChecked) {
|
|
addOrUpdateAndroidContact(this, contact)
|
|
} else {
|
|
if (newContact)
|
|
Contact.contacts().add(contact)
|
|
Contact.saveBaresipContacts()
|
|
Contact.contactsUpdate()
|
|
}
|
|
|
|
BaresipService.activities.remove("contact,$newContact,$uOrI")
|
|
|
|
val i = Intent(this, MainActivity::class.java)
|
|
i.putExtra("name", newName)
|
|
setResult(Activity.RESULT_OK, i)
|
|
finish()
|
|
}
|
|
|
|
android.R.id.home -> {
|
|
|
|
BaresipService.activities.remove("contact,$newContact,$uOrI")
|
|
setResult(Activity.RESULT_CANCELED, Intent(this, MainActivity::class.java))
|
|
finish()
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
override fun onBackPressed() {
|
|
|
|
BaresipService.activities.remove("contact,$newContact,$uOrI")
|
|
setResult(Activity.RESULT_CANCELED, Intent(this, MainActivity::class.java))
|
|
finish()
|
|
super.onBackPressed()
|
|
|
|
}
|
|
|
|
private fun showTextAvatar(name: String, color: Int) {
|
|
textAvatarView.visibility = View.VISIBLE
|
|
cardAvatarView.visibility = View.GONE
|
|
textAvatarView.background.setTint(color)
|
|
textAvatarView.text = "${name[0]}"
|
|
}
|
|
|
|
private fun showImageAvatar(image: Bitmap) {
|
|
textAvatarView.visibility = View.GONE
|
|
cardAvatarView.visibility = View.VISIBLE
|
|
cardImageAvatarView.setImageBitmap(image)
|
|
}
|
|
|
|
private fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap {
|
|
val matrix = Matrix()
|
|
when (orientation) {
|
|
ExifInterface.ORIENTATION_NORMAL -> return bitmap
|
|
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
|
|
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
|
|
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
|
|
matrix.setRotate(180f)
|
|
matrix.postScale(-1f, 1f)
|
|
}
|
|
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
|
matrix.setRotate(90f)
|
|
matrix.postScale(-1f, 1f)
|
|
}
|
|
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
|
|
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
|
matrix.setRotate(-90f)
|
|
matrix.postScale(-1f, 1f)
|
|
}
|
|
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
|
|
else -> return bitmap
|
|
}
|
|
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
|
|
bitmap.width, bitmap.height, matrix, true)
|
|
bitmap.recycle()
|
|
return rotatedBitmap
|
|
}
|
|
|
|
private fun addOrUpdateAndroidContact(ctx: Context, contact: Contact.BaresipContact) {
|
|
val projection = arrayOf(ContactsContract.Data.RAW_CONTACT_ID)
|
|
val selection = ContactsContract.Data.MIMETYPE + "='" +
|
|
CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' AND " +
|
|
CommonDataKinds.StructuredName.DISPLAY_NAME + "='" + contact.name + "'"
|
|
val c: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection,
|
|
selection, null, null)
|
|
if (c != null && c.moveToFirst()) {
|
|
updateAndroidContact(c.getLong(0), contact)
|
|
} else {
|
|
addAndroidContact(ctx, contact)
|
|
}
|
|
c?.close()
|
|
}
|
|
|
|
private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Boolean {
|
|
val ops = ArrayList<ContentProviderOperation>()
|
|
ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
|
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
|
|
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null).build())
|
|
ops.add(ContentProviderOperation
|
|
.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
|
.withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
|
.withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name)
|
|
.build())
|
|
val mimeType = if (contact.uri.startsWith("sip:"))
|
|
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
|
else
|
|
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
|
ops.add(ContentProviderOperation
|
|
.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
|
.withValue(Data.MIMETYPE, mimeType)
|
|
.withValue(Data.DATA1, contact.uri.substringAfter(":"))
|
|
.build())
|
|
if (contact.avatarImage != null) {
|
|
val photoData: ByteArray? = bitmapToPNGByteArray(contact.avatarImage!!)
|
|
if (photoData != null) {
|
|
ops.add(ContentProviderOperation
|
|
.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
|
|
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
|
|
.withValue(CommonDataKinds.Photo.PHOTO, photoData)
|
|
.build())
|
|
}
|
|
}
|
|
try {
|
|
ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Adding of contact ${contact.name} failed")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private fun updateAndroidContact(rawContactId: Long, contact: Contact.BaresipContact) {
|
|
if (updateAndroidUri(rawContactId, contact.uri) == 0)
|
|
addAndroidUri(rawContactId, contact.uri)
|
|
if (updateAndroidPhoto(rawContactId, contact.avatarImage) == 0)
|
|
if (contact.avatarImage != null)
|
|
addAndroidPhoto(rawContactId, contact.avatarImage!!)
|
|
}
|
|
|
|
private fun addAndroidUri(rawContactId: Long, uri: String) {
|
|
val mimeType = if (uri.startsWith("sip:"))
|
|
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
|
else
|
|
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
|
val ops = ArrayList<ContentProviderOperation>()
|
|
ops.add(ContentProviderOperation
|
|
.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
.withValue(Data.RAW_CONTACT_ID, rawContactId)
|
|
.withValue(Data.MIMETYPE, mimeType)
|
|
.withValue(Data.DATA1, uri.substringAfter(":"))
|
|
.build())
|
|
try {
|
|
contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Adding of SIP URI $uri failed")
|
|
}
|
|
}
|
|
|
|
private fun updateAndroidUri(rawContactId: Long, uri: String): Int {
|
|
val mimeType = if (uri.startsWith("sip:"))
|
|
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
|
|
else
|
|
CommonDataKinds.Phone.CONTENT_ITEM_TYPE
|
|
val contentValues = ContentValues()
|
|
contentValues.put(ContactsContract.Data.DATA1, uri)
|
|
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
|
|
"${ContactsContract.Data.MIMETYPE}='$mimeType'"
|
|
return try {
|
|
contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Adding of SIP URI $uri failed")
|
|
0
|
|
}
|
|
}
|
|
|
|
private fun addAndroidPhoto(rawContactId: Long, photoBits: Bitmap) {
|
|
val photoBytes = bitmapToPNGByteArray(photoBits)
|
|
if (photoBytes != null) {
|
|
val ops = ArrayList<ContentProviderOperation>()
|
|
ops.add(ContentProviderOperation
|
|
.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
.withValue(Data.RAW_CONTACT_ID, rawContactId)
|
|
.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
|
|
.withValue(CommonDataKinds.Photo.PHOTO, photoBytes)
|
|
.build())
|
|
try {
|
|
contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Adding of Android photo failed")
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun updateAndroidPhoto(rawContactId: Long, photoBits: Bitmap?): Int {
|
|
val photoBytes = if (photoBits == null)
|
|
null
|
|
else
|
|
bitmapToPNGByteArray(photoBits)
|
|
val contentValues = ContentValues()
|
|
contentValues.put(CommonDataKinds.Photo.PHOTO, photoBytes)
|
|
val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " +
|
|
"${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'"
|
|
return try {
|
|
contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "updateAndroidPhoto failed")
|
|
0
|
|
}
|
|
}
|
|
|
|
private fun bitmapToPNGByteArray(bitmap: Bitmap): ByteArray? {
|
|
val size = bitmap.width * bitmap.height * 4
|
|
val out = ByteArrayOutputStream(size)
|
|
return try {
|
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
|
out.flush()
|
|
out.close()
|
|
out.toByteArray()
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, "Unable to serialize photo: $e")
|
|
null
|
|
}
|
|
}
|
|
|
|
}
|