diff --git a/app/src/main/kotlin/com/tutpro/baresip/AndroidContact.kt b/app/src/main/kotlin/com/tutpro/baresip/AndroidContact.kt new file mode 100644 index 00000000..d6c6ff96 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/AndroidContact.kt @@ -0,0 +1,9 @@ +package com.tutpro.baresip + +import java.util.ArrayList + +class AndroidContact(val id: Long, var name: String, var color: Int, val thumbnailUri: String?) { + + val uris = ArrayList() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/tutpro/baresip/AndroidContactActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/AndroidContactActivity.kt new file mode 100644 index 00000000..5faa0818 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/AndroidContactActivity.kt @@ -0,0 +1,162 @@ +package com.tutpro.baresip + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.widget.* +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.cardview.widget.CardView +import androidx.core.app.ActivityCompat +import androidx.core.net.toUri +import com.google.android.material.snackbar.Snackbar +import com.tutpro.baresip.Utils.showSnackBar +import com.tutpro.baresip.databinding.ActivityAndroidContactBinding + +class AndroidContactActivity : AppCompatActivity() { + + private lateinit var binding: ActivityAndroidContactBinding + private lateinit var layout: LinearLayout + private lateinit var textAvatarView: TextView + private lateinit var cardAvatarView: CardView + private lateinit var cardImageAvatarView: ImageView + private lateinit var nameView: TextView + private lateinit var ulAdapter: AndroidUriListAdapter + private lateinit var aor: String + private lateinit var requestPermissionsLauncher: ActivityResultLauncher> + + private var index = 0 + private var color = 0 + private var id: Long = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + binding = ActivityAndroidContactBinding.inflate(layoutInflater) + setContentView(binding.root) + layout = binding.ContactView + + aor = intent.getStringExtra("aor")!! + index = intent.getIntExtra("index", 0) + + textAvatarView = binding.TextAvatar + cardAvatarView = binding.CardAvatar + cardImageAvatarView = binding.ImageAvatar + nameView = binding.Name + + val contact = AndroidContactsActivity.androidContacts[index] + val name = contact.name + color = contact.color + id = contact.id + val thumbnailUri = contact.thumbnailUri + if (thumbnailUri != null) + showImageAvatar(thumbnailUri) + else + showTextAvatar(name, color) + title = name + nameView.text = name + + val listView = binding.uris + ulAdapter = AndroidUriListAdapter(this, contact.uris, aor) + listView.adapter = ulAdapter + + Utils.addActivity("android contact,$aor,$index") + + } + + override fun onStart() { + super.onStart() + requestPermissionsLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {} + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, + grandResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grandResults) + when (requestCode) { + CONTACT_PERMISSION_REQUEST_CODE -> { + var allowed = true + for (res in grandResults) + allowed = allowed && res == PackageManager.PERMISSION_GRANTED + if (!allowed) { + when { + ActivityCompat.shouldShowRequestPermissionRationale(this, + Manifest.permission.READ_CONTACTS) -> { + layout.showSnackBar( + binding.root, + getString(R.string.no_android_contacts), + Snackbar.LENGTH_INDEFINITE, + getString(R.string.ok) + ) { + requestPermissionsLauncher.launch(permissions) + } + } + ActivityCompat.shouldShowRequestPermissionRationale(this, + Manifest.permission.WRITE_CONTACTS) -> { + layout.showSnackBar( + binding.root, + getString(R.string.no_android_contacts), + Snackbar.LENGTH_INDEFINITE, + getString(R.string.ok) + ) { + requestPermissionsLauncher.launch(permissions) + } + } + else -> { + requestPermissionsLauncher.launch(permissions) + } + } + } + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + + if (BaresipService.activities.indexOf("android contact,$aor,$index") == -1) return true + + when (item.itemId) { + + android.R.id.home -> { + + BaresipService.activities.remove("android contact,$aor,$index") + setResult(Activity.RESULT_CANCELED, Intent(this, MainActivity::class.java)) + finish() + } + + } + + return true + + } + + override fun onBackPressed() { + + BaresipService.activities.remove("android contact,$index") + 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(thumbNailUri: String) { + textAvatarView.visibility = View.GONE + cardAvatarView.visibility = View.VISIBLE + cardImageAvatarView.setImageURI(thumbNailUri.toUri()) + } + +} + + diff --git a/app/src/main/kotlin/com/tutpro/baresip/AndroidContactListAdapter.kt b/app/src/main/kotlin/com/tutpro/baresip/AndroidContactListAdapter.kt new file mode 100644 index 00000000..0101dee8 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/AndroidContactListAdapter.kt @@ -0,0 +1,83 @@ +package com.tutpro.baresip + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.os.SystemClock +import androidx.appcompat.app.AlertDialog +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.core.content.ContextCompat.startActivity +import androidx.core.net.toUri + +import java.io.File +import java.io.IOException +import java.util.* + +class AndroidContactListAdapter(private val ctx: Context, + private val rows: ArrayList, + private val aor: String) : + ArrayAdapter(ctx, R.layout.android_contact_row, rows) { + + private val layoutInflater = LayoutInflater.from(context) + private var lastClick: Long = 0 + + private class ViewHolder(view: View?) { + val textAvatarView = view?.findViewById(R.id.TextAvatar) as TextView + val imageAvatarView = view?.findViewById(R.id.ImageAvatar) as ImageView + val nameView = view?.findViewById(R.id.contactName) as TextView + } + + override fun getView(position: Int, view: View?, parent: ViewGroup): View { + + val viewHolder: ViewHolder + val rowView: View + + if (view == null) { + rowView = layoutInflater.inflate(R.layout.android_contact_row, parent, false) + viewHolder = ViewHolder(rowView) + rowView.tag = viewHolder + } else { + rowView = view + viewHolder = rowView.tag as ViewHolder + } + + val contact = rows[position] + + val thumbNailUri = contact.thumbnailUri + if (thumbNailUri != null) { + viewHolder.imageAvatarView.setImageURI(thumbNailUri.toUri()) + } else { + contact.color = Utils.randomColor() + viewHolder.textAvatarView.background.setTint(contact.color) + if (contact.name.isNotEmpty()) + viewHolder.textAvatarView.text = "${contact.name[0]}" + else + viewHolder.textAvatarView.text = "" + viewHolder.imageAvatarView.setImageBitmap(Utils.bitmapFromView(viewHolder.textAvatarView)) + } + + viewHolder.nameView.text = contact.name + viewHolder.nameView.textSize = 20f + viewHolder.nameView.setPadding(6, 6, 0, 6) + + viewHolder.nameView.setOnClickListener { + if (SystemClock.elapsedRealtime() - lastClick > 1000) { + lastClick = SystemClock.elapsedRealtime() + val i = Intent(ctx, AndroidContactActivity::class.java) + val b = Bundle() + b.putString("aor", aor) + b.putInt("index", position) + i.putExtras(b) + startActivity(ctx, i, null) + } + } + + return rowView + } +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/AndroidContactsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/AndroidContactsActivity.kt new file mode 100644 index 00000000..c9efba80 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/AndroidContactsActivity.kt @@ -0,0 +1,121 @@ +package com.tutpro.baresip + +import android.app.Activity +import android.content.* +import android.database.Cursor +import android.os.Bundle +import android.os.SystemClock +import android.provider.ContactsContract +import android.view.Menu +import androidx.appcompat.app.AppCompatActivity +import android.view.MenuItem +import com.tutpro.baresip.databinding.ActivityAndroidContactsBinding + +class AndroidContactsActivity : AppCompatActivity() { + + private lateinit var binding: ActivityAndroidContactsBinding + private lateinit var clAdapter: AndroidContactListAdapter + private lateinit var aor: String + private var lastSwap: Long = 0 + + public override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + binding = ActivityAndroidContactsBinding.inflate(layoutInflater) + setContentView(binding.root) + + aor = intent.getStringExtra("aor")!! + + val listView = binding.androidContacts + + getAndroidContacts(this) + clAdapter = AndroidContactListAdapter(this, androidContacts, aor) + listView.adapter = clAdapter + + Utils.addActivity("android contacts,$aor") + + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.swap_contacts_icon, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + + when (item.itemId) { + + R.id.swapContactsIcon -> { + if (SystemClock.elapsedRealtime() - lastSwap > 1000) { + lastSwap = SystemClock.elapsedRealtime() + BaresipService.activities.remove("android contacts,$aor") + val intent = Intent(this, ContactsActivity::class.java) + intent.putExtra("aor", aor) + startActivity(intent) + finish() + return true + } + } + + android.R.id.home -> { + BaresipService.activities.remove("android contacts,$aor") + setResult(Activity.RESULT_OK, Intent()) + finish() + } + } + + return true + + } + + override fun onBackPressed() { + + BaresipService.activities.remove("android contacts,$aor") + setResult(Activity.RESULT_OK, Intent()) + finish() + + } + + private fun getAndroidContacts(ctx: Context) { + val projection = arrayOf(ContactsContract.Data.CONTACT_ID, ContactsContract.Data.DISPLAY_NAME, + ContactsContract.Data.MIMETYPE, ContactsContract.Data.DATA1, + ContactsContract.Data.PHOTO_THUMBNAIL_URI) + val selection = + ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + "' OR " + + ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'" + Log.d(TAG, "******* selection = /$selection/") + val cur: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, + selection, null, null) + val contacts = HashMap() + while (cur != null && cur.moveToNext()) { + val id = cur.getLong(0) + val name = cur.getString(1) // display name + val mime = cur.getString(2) // type of data + val data = cur.getString(3) // data + val thumb = cur.getString(4) // thumbnail + // Log.d(TAG, "****** $id - $name - $mime - $data - $thumb") + val contact = if (contacts.containsKey(id)) + contacts[id]!! + else + AndroidContact(id, name, 0, thumb) + if (mime == ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + contact.uris.add("tel:$data") + else if (mime == ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE) + contact.uris.add("sip:$data") + else + continue + if (!contacts.containsKey(id)) + contacts[id] = contact + } + cur?.close() + androidContacts.clear() + for ((_, value) in contacts) + androidContacts.add(value) + return + } + + companion object { + val androidContacts = ArrayList() + } + +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/AndroidUriListAdapter.kt b/app/src/main/kotlin/com/tutpro/baresip/AndroidUriListAdapter.kt new file mode 100644 index 00000000..9dfa84a6 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/AndroidUriListAdapter.kt @@ -0,0 +1,76 @@ +package com.tutpro.baresip + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ImageButton +import android.widget.TextView + +class AndroidUriListAdapter(private val ctx: Context, private val rows: ArrayList, val aor: String) : + ArrayAdapter(ctx, R.layout.android_uri_row, rows) { + + private val layoutInflater = LayoutInflater.from(context) + + private class ViewHolder(view: View?) { + val uriView = view?.findViewById(R.id.uri) as TextView + val messageView = view?.findViewById(R.id.message) as ImageButton + val callView = view?.findViewById(R.id.call) as ImageButton + } + + override fun getView(position: Int, view: View?, parent: ViewGroup): View { + + val viewHolder: ViewHolder + val rowView: View + + if (view == null) { + rowView = layoutInflater.inflate(R.layout.android_uri_row, parent, false) + viewHolder = ViewHolder(rowView) + rowView.tag = viewHolder + } else { + rowView = view + viewHolder = rowView.tag as ViewHolder + } + + val uri = rows[position] + + viewHolder.uriView.text = uri.substringAfter(":") + + viewHolder.messageView.setOnClickListener { + val i = Intent(ctx, MainActivity::class.java) + i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + i.putExtra("action", "message") + val ua = UserAgent.ofAor(aor) + if (ua == null) { + Log.w(TAG, "onClickListener did not find AoR $aor") + } else { + BaresipService.activities.clear() + i.putExtra("uap", ua.uap) + i.putExtra("peer", uri) + (ctx as Activity).startActivity(i) + } + } + + viewHolder.callView.setOnClickListener { + val i = Intent(ctx, MainActivity::class.java) + i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + i.putExtra("action", "call") + val ua = UserAgent.ofAor(aor) + if (ua == null) { + Log.w(TAG, "onClickListener did not find AoR $aor") + } else { + BaresipService.activities.clear() + i.putExtra("uap", ua.uap) + i.putExtra("peer", uri) + (ctx as Activity).startActivity(i) + } + } + + return rowView + } +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt index 93951c7f..6985d14e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ContactsActivity.kt @@ -77,6 +77,7 @@ class ContactsActivity : AppCompatActivity() { R.id.swapContactsIcon -> { if (SystemClock.elapsedRealtime() - lastClick > 1000) { lastClick = SystemClock.elapsedRealtime() + BaresipService.activities.remove("contacts,$aor") val intent = Intent(this, AndroidContactsActivity::class.java) intent.putExtra("aor", aor) startActivity(intent) diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index d14b47ca..7bcc1e21 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -1966,6 +1966,21 @@ class MainActivity : AppCompatActivity() { i.putExtras(b) startActivity(i) } + "android contacts" -> { + val i = Intent(this, AndroidContactsActivity::class.java) + val b = Bundle() + b.putString("aor", activity[1]) + i.putExtras(b) + startActivity(i) + } + "android contact" -> { + val i = Intent(this, AndroidContactActivity::class.java) + val b = Bundle() + b.putString("aor", activity[1]) + b.putInt("index", activity[2].toInt()) + i.putExtras(b) + startActivity(i) + } "chats" -> { val i = Intent(this, ChatsActivity::class.java) val b = Bundle() diff --git a/app/src/main/res/drawable/call.xml b/app/src/main/res/drawable/call.xml new file mode 100644 index 00000000..3da2ad45 --- /dev/null +++ b/app/src/main/res/drawable/call.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/message.xml b/app/src/main/res/drawable/message.xml new file mode 100644 index 00000000..a8e114eb --- /dev/null +++ b/app/src/main/res/drawable/message.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/swap_contacts.xml b/app/src/main/res/drawable/swap_contacts.xml new file mode 100644 index 00000000..69851328 --- /dev/null +++ b/app/src/main/res/drawable/swap_contacts.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_android_contact.xml b/app/src/main/res/layout/activity_android_contact.xml new file mode 100644 index 00000000..97ff7968 --- /dev/null +++ b/app/src/main/res/layout/activity_android_contact.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_android_contacts.xml b/app/src/main/res/layout/activity_android_contacts.xml new file mode 100644 index 00000000..652add17 --- /dev/null +++ b/app/src/main/res/layout/activity_android_contacts.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/layout/android_contact_row.xml b/app/src/main/res/layout/android_contact_row.xml new file mode 100644 index 00000000..d2fa025e --- /dev/null +++ b/app/src/main/res/layout/android_contact_row.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/android_uri_row.xml b/app/src/main/res/layout/android_uri_row.xml new file mode 100644 index 00000000..e0a5daa6 --- /dev/null +++ b/app/src/main/res/layout/android_uri_row.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/swap_contacts_icon.xml b/app/src/main/res/menu/swap_contacts_icon.xml new file mode 100644 index 00000000..6fb1b1f6 --- /dev/null +++ b/app/src/main/res/menu/swap_contacts_icon.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index d829d665..75dd8fa9 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -12,6 +12,7 @@ #0ca1fd #ffffff #000000 + @color/colorWhite #01df01 #df0101 @color/colorGrayLight diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d311a3c4..cffac98f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,6 +12,7 @@ #0ca1fd #ffffff #000000 + @color/colorBlack #01df01 #df0101 @color/colorWhite