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