mirror of
https://github.com/SimpleMobileTools/Simple-Dialer.git
synced 2025-06-05 21:49:23 +02:00
manage conference
This commit is contained in:
@ -82,6 +82,12 @@
|
||||
android:label="@string/speed_dial"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.ConferenceActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/conference"
|
||||
android:parentActivityName=".activities.CallActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:exported="true"
|
||||
|
@ -14,11 +14,15 @@ import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.telecom.Call
|
||||
import android.telecom.CallAudioState
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import com.simplemobiletools.commons.extensions.*
|
||||
import com.simplemobiletools.commons.helpers.*
|
||||
import com.simplemobiletools.commons.helpers.LOWER_ALPHA
|
||||
import com.simplemobiletools.commons.helpers.MINUTE_SECONDS
|
||||
import com.simplemobiletools.commons.helpers.isOreoMr1Plus
|
||||
import com.simplemobiletools.commons.helpers.isOreoPlus
|
||||
import com.simplemobiletools.dialer.R
|
||||
import com.simplemobiletools.dialer.extensions.*
|
||||
import com.simplemobiletools.dialer.helpers.*
|
||||
@ -26,7 +30,7 @@ import com.simplemobiletools.dialer.models.CallContact
|
||||
import kotlinx.android.synthetic.main.activity_call.*
|
||||
import kotlinx.android.synthetic.main.dialpad.*
|
||||
|
||||
const val TAG = "SimpleDialer:CallManager"
|
||||
const val TAG = "SimpleDialer:CallActivityTag"
|
||||
|
||||
class CallActivity : SimpleActivity() {
|
||||
companion object {
|
||||
@ -53,7 +57,7 @@ class CallActivity : SimpleActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_call)
|
||||
|
||||
if (CallManager.call == null) {
|
||||
if (CallManager.getPhoneState() == NoCall) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
@ -70,6 +74,11 @@ class CallActivity : SimpleActivity() {
|
||||
updateCallContactInfo(CallManager.getPrimaryCall())
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
updateState()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateState()
|
||||
@ -149,7 +158,7 @@ class CallActivity : SimpleActivity() {
|
||||
}
|
||||
|
||||
call_manage.setOnClickListener {
|
||||
// TODO open conference participants list
|
||||
startActivity(Intent(this, ConferenceActivity::class.java))
|
||||
}
|
||||
|
||||
call_end.setOnClickListener {
|
||||
@ -356,6 +365,8 @@ class CallActivity : SimpleActivity() {
|
||||
|
||||
if (avatar != null) {
|
||||
caller_avatar.setImageBitmap(avatar)
|
||||
} else {
|
||||
caller_avatar.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -373,7 +384,7 @@ class CallActivity : SimpleActivity() {
|
||||
val accounts = telecomManager.callCapablePhoneAccounts
|
||||
if (accounts.size > 1) {
|
||||
accounts.forEachIndexed { index, account ->
|
||||
if (account == CallManager.call?.details?.accountHandle) {
|
||||
if (account == CallManager.getPrimaryCall()?.details?.accountHandle) {
|
||||
call_sim_id.text = "${index + 1}"
|
||||
call_sim_id.beVisible()
|
||||
call_sim_image.beVisible()
|
||||
@ -395,8 +406,9 @@ class CallActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCallState(call: Call?) {
|
||||
val state = call?.getStateCompat()
|
||||
private fun updateCallState(call: Call) {
|
||||
val state = call.getStateCompat()
|
||||
Log.d(TAG, "updateCallState: $state")
|
||||
when (state) {
|
||||
Call.STATE_RINGING -> callRinging()
|
||||
Call.STATE_ACTIVE -> callStarted()
|
||||
@ -407,17 +419,22 @@ class CallActivity : SimpleActivity() {
|
||||
|
||||
val statusTextId = when (state) {
|
||||
Call.STATE_RINGING -> R.string.is_calling
|
||||
Call.STATE_DIALING -> R.string.dialing
|
||||
Call.STATE_CONNECTING, Call.STATE_DIALING -> R.string.dialing
|
||||
else -> 0
|
||||
}
|
||||
|
||||
if (statusTextId != 0) {
|
||||
call_status_label.text = getString(statusTextId)
|
||||
}
|
||||
|
||||
call_manage.beVisibleIf(call.hasCapability(Call.Details.CAPABILITY_MANAGE_CONFERENCE))
|
||||
setActionButtonEnabled(call_swap, state == Call.STATE_ACTIVE)
|
||||
setActionButtonEnabled(call_merge, state == Call.STATE_ACTIVE)
|
||||
}
|
||||
|
||||
private fun updateState() {
|
||||
val phoneState = CallManager.getPhoneState()
|
||||
Log.d(TAG, "updateState: $phoneState")
|
||||
if (phoneState is SingleCall) {
|
||||
updateCallState(phoneState.call)
|
||||
updateCallOnHoldState(null)
|
||||
@ -426,7 +443,6 @@ class CallActivity : SimpleActivity() {
|
||||
|| state == Call.STATE_DISCONNECTING || state == Call.STATE_HOLDING)
|
||||
setActionButtonEnabled(call_toggle_hold, isSingleCallActionsEnabled)
|
||||
setActionButtonEnabled(call_add, isSingleCallActionsEnabled)
|
||||
call_manage.beVisibleIf(phoneState.call.isConference())
|
||||
} else if (phoneState is TwoCalls) {
|
||||
updateCallState(phoneState.active)
|
||||
updateCallOnHoldState(phoneState.onHold)
|
||||
@ -436,7 +452,7 @@ class CallActivity : SimpleActivity() {
|
||||
private fun updateCallOnHoldState(call: Call?) {
|
||||
val hasCallOnHold = call != null
|
||||
if (hasCallOnHold) {
|
||||
CallManager.getCallContact(applicationContext, call) { contact ->
|
||||
getCallContact(applicationContext, call) { contact ->
|
||||
runOnUiThread {
|
||||
on_hold_caller_name.text = getContactNameOrNumber(contact)
|
||||
}
|
||||
@ -448,18 +464,15 @@ class CallActivity : SimpleActivity() {
|
||||
}
|
||||
|
||||
private fun updateCallContactInfo(call: Call?) {
|
||||
if (call.isConference()) {
|
||||
caller_avatar.setImageDrawable(null)
|
||||
caller_number.text = null
|
||||
caller_name_label.text = getString(R.string.conference)
|
||||
} else {
|
||||
CallManager.getCallContact(applicationContext, call) { contact ->
|
||||
callContact = contact
|
||||
val avatar = callContactAvatarHelper.getCallContactAvatar(contact)
|
||||
runOnUiThread {
|
||||
updateOtherPersonsInfo(avatar)
|
||||
checkCalledSIMCard()
|
||||
}
|
||||
getCallContact(applicationContext, call) { contact ->
|
||||
if (call != CallManager.getPrimaryCall()) {
|
||||
return@getCallContact
|
||||
}
|
||||
callContact = contact
|
||||
val avatar = if (!call.isConference()) callContactAvatarHelper.getCallContactAvatar(contact) else null
|
||||
runOnUiThread {
|
||||
updateOtherPersonsInfo(avatar)
|
||||
checkCalledSIMCard()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -489,7 +502,7 @@ class CallActivity : SimpleActivity() {
|
||||
private fun showPhoneAccountPicker() {
|
||||
if (callContact != null) {
|
||||
getHandleToUse(intent, callContact!!.number) { handle ->
|
||||
CallManager.call?.phoneAccountSelected(handle, false)
|
||||
CallManager.getPrimaryCall()?.phoneAccountSelected(handle, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -523,11 +536,13 @@ class CallActivity : SimpleActivity() {
|
||||
}
|
||||
|
||||
private val callCallback = object : CallManagerListener {
|
||||
override fun onStateChanged(call: Call, state: Int) {
|
||||
override fun onStateChanged() {
|
||||
updateState()
|
||||
}
|
||||
|
||||
override fun onPrimaryCallChanged(call: Call) {
|
||||
Log.d(TAG, "onPrimaryCallChanged: $call")
|
||||
callDurationHandler.removeCallbacks(updateCallDurationTask)
|
||||
updateCallContactInfo(call)
|
||||
updateState()
|
||||
}
|
||||
@ -535,7 +550,7 @@ class CallActivity : SimpleActivity() {
|
||||
|
||||
private val updateCallDurationTask = object : Runnable {
|
||||
override fun run() {
|
||||
callDuration = CallManager.getCallDuration()
|
||||
callDuration = CallManager.getPrimaryCall().getCallDuration()
|
||||
if (!isCallEnded) {
|
||||
call_status_label.text = callDuration.getFormattedDuration()
|
||||
callDurationHandler.postDelayed(this, 1000)
|
||||
|
@ -0,0 +1,18 @@
|
||||
package com.simplemobiletools.dialer.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import com.simplemobiletools.dialer.R
|
||||
import com.simplemobiletools.dialer.adapters.ConferenceCallsAdapter
|
||||
import com.simplemobiletools.dialer.helpers.CallManager
|
||||
import kotlinx.android.synthetic.main.activity_conference.*
|
||||
|
||||
class ConferenceActivity : SimpleActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_conference)
|
||||
supportActionBar
|
||||
|
||||
conference_calls_list.adapter = ConferenceCallsAdapter(this, conference_calls_list, ArrayList(CallManager.getConferenceCalls())) {}
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package com.simplemobiletools.dialer.adapters
|
||||
|
||||
import android.telecom.Call
|
||||
import android.view.Menu
|
||||
import android.view.ViewGroup
|
||||
import com.bumptech.glide.Glide
|
||||
import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter
|
||||
import com.simplemobiletools.commons.helpers.LOWER_ALPHA
|
||||
import com.simplemobiletools.commons.helpers.SimpleContactsHelper
|
||||
import com.simplemobiletools.commons.views.MyRecyclerView
|
||||
import com.simplemobiletools.dialer.R
|
||||
import com.simplemobiletools.dialer.activities.SimpleActivity
|
||||
import com.simplemobiletools.dialer.extensions.hasCapability
|
||||
import com.simplemobiletools.dialer.helpers.getCallContact
|
||||
import kotlinx.android.synthetic.main.item_conference_call.view.*
|
||||
|
||||
class ConferenceCallsAdapter(
|
||||
activity: SimpleActivity, recyclerView: MyRecyclerView, val data: ArrayList<Call>, itemClick: (Any) -> Unit
|
||||
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
|
||||
|
||||
override fun actionItemPressed(id: Int) {
|
||||
}
|
||||
|
||||
override fun getActionMenuId(): Int = 0
|
||||
|
||||
override fun getIsItemSelectable(position: Int): Boolean = false
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun getItemKeyPosition(key: Int): Int = -1
|
||||
|
||||
override fun getItemSelectionKey(position: Int): Int? = null
|
||||
|
||||
override fun getSelectableItemCount(): Int = data.size
|
||||
|
||||
override fun onActionModeCreated() {
|
||||
}
|
||||
|
||||
override fun onActionModeDestroyed() {
|
||||
}
|
||||
|
||||
override fun prepareActionMode(menu: Menu) {
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_conference_call, parent)
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val call = data[position]
|
||||
holder.bindView(call, allowSingleClick = false, allowLongClick = false) { itemView, _ ->
|
||||
getCallContact(itemView.context, call) { callContact ->
|
||||
itemView.post {
|
||||
itemView.item_conference_call_name.text = callContact.name.ifEmpty { itemView.context.getString(R.string.unknown_caller) }
|
||||
SimpleContactsHelper(activity).loadContactImage(
|
||||
callContact.photoUri,
|
||||
itemView.item_conference_call_image,
|
||||
callContact.name,
|
||||
activity.getDrawable(R.drawable.ic_person_vector)
|
||||
)
|
||||
}
|
||||
}
|
||||
val canSeparate = call.hasCapability(Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE)
|
||||
val canDisconnect = call.hasCapability(Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE)
|
||||
itemView.item_conference_call_split.isEnabled = canSeparate
|
||||
itemView.item_conference_call_split.alpha = if (canSeparate) 1.0f else LOWER_ALPHA
|
||||
itemView.item_conference_call_split.setOnClickListener {
|
||||
call.splitFromConference()
|
||||
data.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
if (data.size == 1) {
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
itemView.item_conference_call_end.isEnabled = canDisconnect
|
||||
itemView.item_conference_call_end.alpha = if (canDisconnect) 1.0f else LOWER_ALPHA
|
||||
itemView.item_conference_call_end.setOnClickListener {
|
||||
call.disconnect()
|
||||
data.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
if (data.size == 1) {
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
bindViewHolder(holder)
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
if (!activity.isDestroyed && !activity.isFinishing) {
|
||||
Glide.with(activity).clear(holder.itemView.item_conference_call_image)
|
||||
}
|
||||
}
|
||||
}
|
@ -19,12 +19,22 @@ fun Call?.getStateCompat(): Int {
|
||||
}
|
||||
}
|
||||
|
||||
fun Call?.getCallDuration(): Int {
|
||||
return if (this != null) {
|
||||
val connectTimeMillis = details.connectTimeMillis
|
||||
if (connectTimeMillis == 0L) {
|
||||
return 0
|
||||
}
|
||||
((System.currentTimeMillis() - connectTimeMillis) / 1000).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fun Call.isOutgoing(): Boolean {
|
||||
return OUTGOING_CALL_STATES.contains(getStateCompat())
|
||||
}
|
||||
|
||||
fun Call.hasCapability(capability: Int): Boolean = details.callCapabilities and capability != 0
|
||||
|
||||
fun Call.hasProperty(property: Int): Boolean = details.hasProperty(property)
|
||||
fun Call.hasCapability(capability: Int): Boolean = (details.callCapabilities and capability) != 0
|
||||
|
||||
fun Call?.isConference(): Boolean = this?.details?.hasProperty(Call.Details.PROPERTY_CONFERENCE) == true
|
||||
|
@ -0,0 +1,71 @@
|
||||
package com.simplemobiletools.dialer.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.telecom.Call
|
||||
import com.simplemobiletools.commons.extensions.getMyContactsCursor
|
||||
import com.simplemobiletools.commons.extensions.getPhoneNumberTypeText
|
||||
import com.simplemobiletools.commons.helpers.MyContactsContentProvider
|
||||
import com.simplemobiletools.commons.helpers.SimpleContactsHelper
|
||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||
import com.simplemobiletools.dialer.R
|
||||
import com.simplemobiletools.dialer.extensions.isConference
|
||||
import com.simplemobiletools.dialer.models.CallContact
|
||||
|
||||
fun getCallContact(context: Context, call: Call?, callback: (CallContact) -> Unit) {
|
||||
if (call.isConference()) {
|
||||
callback(CallContact(context.getString(R.string.conference), "", "", ""))
|
||||
return
|
||||
}
|
||||
val privateCursor = context.getMyContactsCursor(false, true)
|
||||
ensureBackgroundThread {
|
||||
val callContact = CallContact("", "", "", "")
|
||||
val handle = try {
|
||||
call?.details?.handle?.toString()
|
||||
} catch (e: NullPointerException) {
|
||||
null
|
||||
}
|
||||
|
||||
if (handle == null) {
|
||||
callback(callContact)
|
||||
return@ensureBackgroundThread
|
||||
}
|
||||
|
||||
val uri = Uri.decode(handle)
|
||||
if (uri.startsWith("tel:")) {
|
||||
val number = uri.substringAfter("tel:")
|
||||
SimpleContactsHelper(context).getAvailableContacts(false) { contacts ->
|
||||
val privateContacts = MyContactsContentProvider.getSimpleContacts(context, privateCursor)
|
||||
if (privateContacts.isNotEmpty()) {
|
||||
contacts.addAll(privateContacts)
|
||||
}
|
||||
|
||||
val contactsWithMultipleNumbers = contacts.filter { it.phoneNumbers.size > 1 }
|
||||
val numbersToContactIDMap = HashMap<String, Int>()
|
||||
contactsWithMultipleNumbers.forEach { contact ->
|
||||
contact.phoneNumbers.forEach { phoneNumber ->
|
||||
numbersToContactIDMap[phoneNumber.value] = contact.contactId
|
||||
numbersToContactIDMap[phoneNumber.normalizedNumber] = contact.contactId
|
||||
}
|
||||
}
|
||||
|
||||
callContact.number = number
|
||||
val contact = contacts.firstOrNull { it.doesHavePhoneNumber(number) }
|
||||
if (contact != null) {
|
||||
callContact.name = contact.name
|
||||
callContact.photoUri = contact.photoUri
|
||||
|
||||
if (contact.phoneNumbers.size > 1) {
|
||||
val specificPhoneNumber = contact.phoneNumbers.firstOrNull { it.value == number }
|
||||
if (specificPhoneNumber != null) {
|
||||
callContact.numberLabel = context.getPhoneNumberTypeText(specificPhoneNumber.type, specificPhoneNumber.label)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
callContact.name = number
|
||||
}
|
||||
callback(callContact)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +1,31 @@
|
||||
package com.simplemobiletools.dialer.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.annotation.SuppressLint
|
||||
import android.telecom.Call
|
||||
import android.telecom.InCallService
|
||||
import android.telecom.VideoProfile
|
||||
import android.util.Log
|
||||
import com.simplemobiletools.commons.extensions.getMyContactsCursor
|
||||
import com.simplemobiletools.commons.extensions.getPhoneNumberTypeText
|
||||
import com.simplemobiletools.commons.helpers.MyContactsContentProvider
|
||||
import com.simplemobiletools.commons.helpers.SimpleContactsHelper
|
||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||
import com.simplemobiletools.dialer.extensions.getStateCompat
|
||||
import com.simplemobiletools.dialer.extensions.hasCapability
|
||||
import com.simplemobiletools.dialer.extensions.isConference
|
||||
import com.simplemobiletools.dialer.models.CallContact
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
const val TAG = "SimpleDialer:CallManager"
|
||||
const val TAG2 = TAG /*"SimpleDialer:CallState"*/
|
||||
|
||||
// inspired by https://github.com/Chooloo/call_manage
|
||||
class CallManager {
|
||||
companion object {
|
||||
var call: Call? = null
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
var inCallService: InCallService? = null
|
||||
val calls = mutableListOf<Call>()
|
||||
private var call: Call? = null
|
||||
private val calls = mutableListOf<Call>()
|
||||
private val listeners = CopyOnWriteArraySet<CallManagerListener>()
|
||||
|
||||
fun onCallAdded(call: Call) {
|
||||
this.call = call
|
||||
calls.add(call)
|
||||
Log.d(TAG, "onCallAdded (${calls.size}): $call")
|
||||
for (listener in listeners) {
|
||||
listener.onPrimaryCallChanged(call)
|
||||
}
|
||||
@ -37,13 +33,15 @@ class CallManager {
|
||||
override fun onStateChanged(call: Call, state: Int) {
|
||||
Log.d(TAG, "onStateChanged: $call")
|
||||
updateState()
|
||||
for (listener in listeners) {
|
||||
listener.onStateChanged(call, state)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetailsChanged(call: Call, details: Call.Details) {
|
||||
Log.d(TAG, "onDetailsChanged")
|
||||
updateState()
|
||||
}
|
||||
|
||||
override fun onConferenceableCallsChanged(call: Call, conferenceableCalls: MutableList<Call>) {
|
||||
Log.d(TAG, "onConferenceableCallsChanged: $call, conferenceableCalls size=${conferenceableCalls.size}")
|
||||
Log.d(TAG, "onConferenceableCallsChanged (${conferenceableCalls.size}): $call")
|
||||
updateState()
|
||||
}
|
||||
})
|
||||
@ -51,34 +49,59 @@ class CallManager {
|
||||
|
||||
fun onCallRemoved(call: Call) {
|
||||
calls.remove(call)
|
||||
Log.d(TAG, "onCallRemoved (${calls.size}): $call")
|
||||
updateState()
|
||||
}
|
||||
|
||||
fun getPhoneState(): PhoneState {
|
||||
return when (calls.size) {
|
||||
0 -> {
|
||||
Log.d(TAG2, "No call")
|
||||
NoCall
|
||||
}
|
||||
1 -> {
|
||||
Log.d(TAG2, "Single call")
|
||||
SingleCall(calls.first())
|
||||
}
|
||||
2 -> {
|
||||
Log.d(TAG2, "Two calls")
|
||||
val active = calls.find { it.getStateCompat() == Call.STATE_ACTIVE }
|
||||
val newCall = calls.find { it.getStateCompat() == Call.STATE_CONNECTING || it.getStateCompat() == Call.STATE_DIALING }
|
||||
val onHold = calls.find { it.getStateCompat() == Call.STATE_HOLDING }
|
||||
if (active != null && newCall != null) {
|
||||
TwoCalls(newCall, active)
|
||||
} else if(newCall != null && onHold != null) {
|
||||
} else if (newCall != null && onHold != null) {
|
||||
TwoCalls(newCall, onHold)
|
||||
} else if(active != null && onHold != null) {
|
||||
} else if (active != null && onHold != null) {
|
||||
TwoCalls(active, onHold)
|
||||
} else {
|
||||
TwoCalls(calls[0], calls[1])
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
SingleCall(calls.find { it.isConference() }!!)
|
||||
// TODO handle the call on hold (outside the conference)
|
||||
val conference = calls.find { it.isConference() }!!
|
||||
val secondCall = if (conference.children.size + 1 != calls.size) {
|
||||
calls.filter { !it.isConference() }
|
||||
.subtract(conference.children.toSet())
|
||||
.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Log.d(TAG2, "Conference call (${conference.children.size} children)")
|
||||
Log.d(TAG2, "secondCall: $secondCall")
|
||||
if (secondCall == null) {
|
||||
Log.d(TAG2, "Conference call (single)")
|
||||
SingleCall(conference)
|
||||
} else {
|
||||
val newCallState = secondCall.getStateCompat()
|
||||
if (newCallState == Call.STATE_ACTIVE || newCallState == Call.STATE_CONNECTING || newCallState == Call.STATE_DIALING) {
|
||||
Log.d(TAG2, "Conference call and regular call (conference on hold)")
|
||||
TwoCalls(secondCall, conference)
|
||||
} else {
|
||||
Log.d(TAG2, "Conference call and regular call (regular call on hold)")
|
||||
TwoCalls(conference, secondCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,6 +112,7 @@ class CallManager {
|
||||
is SingleCall -> phoneState.call
|
||||
is TwoCalls -> phoneState.active
|
||||
}
|
||||
var notify = true
|
||||
if (primaryCall == null) {
|
||||
call = null
|
||||
} else if (primaryCall != call) {
|
||||
@ -96,6 +120,12 @@ class CallManager {
|
||||
for (listener in listeners) {
|
||||
listener.onPrimaryCallChanged(primaryCall)
|
||||
}
|
||||
notify = false
|
||||
}
|
||||
if (notify) {
|
||||
for (listener in listeners) {
|
||||
listener.onStateChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,11 +133,8 @@ class CallManager {
|
||||
return call
|
||||
}
|
||||
|
||||
fun getSecondaryCall(): Call? {
|
||||
if (calls.size == 1) {
|
||||
return null
|
||||
}
|
||||
return calls.find { it.getStateCompat() == Call.STATE_HOLDING }
|
||||
fun getConferenceCalls(): List<Call> {
|
||||
return calls.find { it.isConference() }?.children ?: emptyList()
|
||||
}
|
||||
|
||||
fun accept() {
|
||||
@ -134,11 +161,10 @@ class CallManager {
|
||||
return !isOnHold
|
||||
}
|
||||
|
||||
val isConference: Boolean
|
||||
get() = call?.details?.hasProperty(Call.Details.PROPERTY_CONFERENCE) ?: false
|
||||
|
||||
fun swap() {
|
||||
getSecondaryCall()?.unhold()
|
||||
if (calls.size > 1) {
|
||||
calls.find { it.getStateCompat() == Call.STATE_HOLDING }?.unhold()
|
||||
}
|
||||
}
|
||||
|
||||
fun merge() {
|
||||
@ -166,85 +192,15 @@ class CallManager {
|
||||
call?.playDtmfTone(c)
|
||||
call?.stopDtmfTone()
|
||||
}
|
||||
|
||||
fun getCallContact(context: Context, callback: (CallContact?) -> Unit) {
|
||||
return getCallContact(context, call, callback)
|
||||
}
|
||||
|
||||
fun getCallContact(context: Context, call: Call?, callback: (CallContact) -> Unit) {
|
||||
val privateCursor = context.getMyContactsCursor(false, true)
|
||||
ensureBackgroundThread {
|
||||
val callContact = CallContact("", "", "", "")
|
||||
val handle = try {
|
||||
call?.details?.handle?.toString()
|
||||
} catch (e: NullPointerException) {
|
||||
null
|
||||
}
|
||||
|
||||
if (handle == null) {
|
||||
callback(callContact)
|
||||
return@ensureBackgroundThread
|
||||
}
|
||||
|
||||
val uri = Uri.decode(handle)
|
||||
if (uri.startsWith("tel:")) {
|
||||
val number = uri.substringAfter("tel:")
|
||||
SimpleContactsHelper(context).getAvailableContacts(false) { contacts ->
|
||||
val privateContacts = MyContactsContentProvider.getSimpleContacts(context, privateCursor)
|
||||
if (privateContacts.isNotEmpty()) {
|
||||
contacts.addAll(privateContacts)
|
||||
}
|
||||
|
||||
val contactsWithMultipleNumbers = contacts.filter { it.phoneNumbers.size > 1 }
|
||||
val numbersToContactIDMap = HashMap<String, Int>()
|
||||
contactsWithMultipleNumbers.forEach { contact ->
|
||||
contact.phoneNumbers.forEach { phoneNumber ->
|
||||
numbersToContactIDMap[phoneNumber.value] = contact.contactId
|
||||
numbersToContactIDMap[phoneNumber.normalizedNumber] = contact.contactId
|
||||
}
|
||||
}
|
||||
|
||||
callContact.number = number
|
||||
val contact = contacts.firstOrNull { it.doesHavePhoneNumber(number) }
|
||||
if (contact != null) {
|
||||
callContact.name = contact.name
|
||||
callContact.photoUri = contact.photoUri
|
||||
|
||||
if (contact.phoneNumbers.size > 1) {
|
||||
val specificPhoneNumber = contact.phoneNumbers.firstOrNull { it.value == number }
|
||||
if (specificPhoneNumber != null) {
|
||||
callContact.numberLabel = context.getPhoneNumberTypeText(specificPhoneNumber.type, specificPhoneNumber.label)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
callContact.name = number
|
||||
}
|
||||
callback(callContact)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCallDuration(): Int {
|
||||
return if (call != null) {
|
||||
val connectTimeMillis = call!!.details.connectTimeMillis
|
||||
if (connectTimeMillis == 0L) {
|
||||
return 0
|
||||
}
|
||||
((System.currentTimeMillis() - connectTimeMillis) / 1000).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CallManagerListener {
|
||||
fun onStateChanged(call: Call, state: Int)
|
||||
fun onStateChanged()
|
||||
fun onPrimaryCallChanged(call: Call)
|
||||
}
|
||||
|
||||
sealed class PhoneState
|
||||
object NoCall : PhoneState()
|
||||
class SingleCall(val call: Call?) : PhoneState()
|
||||
class SingleCall(val call: Call) : PhoneState()
|
||||
class TwoCalls(val active: Call, val onHold: Call) : PhoneState()
|
||||
|
@ -28,7 +28,7 @@ class CallNotificationManager(private val context: Context) {
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
fun setupNotification() {
|
||||
CallManager.getCallContact(context.applicationContext) { callContact ->
|
||||
getCallContact(context.applicationContext, CallManager.getPrimaryCall()) { callContact ->
|
||||
val callContactAvatar = callContactAvatarHelper.getCallContactAvatar(callContact)
|
||||
val callState = CallManager.getState()
|
||||
val isHighPriority = context.powerManager.isInteractive && callState == Call.STATE_RINGING
|
||||
|
@ -4,15 +4,13 @@ import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.telecom.Call
|
||||
import android.telecom.InCallService
|
||||
import android.util.Log
|
||||
import com.simplemobiletools.dialer.activities.CallActivity
|
||||
import com.simplemobiletools.dialer.extensions.getStateCompat
|
||||
import com.simplemobiletools.dialer.extensions.isOutgoing
|
||||
import com.simplemobiletools.dialer.extensions.powerManager
|
||||
import com.simplemobiletools.dialer.helpers.CallManager
|
||||
import com.simplemobiletools.dialer.helpers.CallNotificationManager
|
||||
|
||||
const val TAG = "SimpleDialer:CallService"
|
||||
import com.simplemobiletools.dialer.helpers.NoCall
|
||||
|
||||
class CallService : InCallService() {
|
||||
private val callNotificationManager by lazy { CallNotificationManager(this) }
|
||||
@ -45,18 +43,18 @@ class CallService : InCallService() {
|
||||
|
||||
override fun onCallRemoved(call: Call) {
|
||||
super.onCallRemoved(call)
|
||||
Log.d(TAG, "onCallRemoved: $call")
|
||||
call.unregisterCallback(callListener)
|
||||
val wasPrimaryCall = call == CallManager.getPrimaryCall()
|
||||
CallManager.onCallRemoved(call)
|
||||
if (CallManager.calls.isEmpty()) {
|
||||
CallManager.call = null
|
||||
if (CallManager.getPhoneState() == NoCall) {
|
||||
CallManager.inCallService = null
|
||||
callNotificationManager.cancelNotification()
|
||||
} else {
|
||||
callNotificationManager.setupNotification()
|
||||
startActivity(CallActivity.getStartIntent(this))
|
||||
if (wasPrimaryCall) {
|
||||
startActivity(CallActivity.getStartIntent(this))
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "onCallRemoved: calls=${CallManager.calls.size}")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
5
app/src/main/res/drawable/ic_call_split_vector.xml
Normal file
5
app/src/main/res/drawable/ic_call_split_vector.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="#FFFFFF" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M14,4l2.29,2.29 -2.88,2.88 1.42,1.42 2.88,-2.88L20,10L20,4zM10,4L4,4v6l2.29,-2.29 4.71,4.7L11,20h2v-8.41l-5.29,-5.3z"/>
|
||||
</vector>
|
@ -207,9 +207,9 @@
|
||||
android:contentDescription="@string/hold_call"
|
||||
android:padding="@dimen/medium_margin"
|
||||
android:src="@drawable/ic_pause_vector"
|
||||
app:layout_constraintEnd_toStartOf="@id/call_add"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/call_toggle_speaker" />
|
||||
app:layout_constraintEnd_toEndOf="@+id/call_toggle_microphone"
|
||||
app:layout_constraintStart_toStartOf="@+id/call_toggle_microphone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/call_toggle_microphone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/call_add"
|
||||
@ -220,8 +220,8 @@
|
||||
android:contentDescription="@string/call_add"
|
||||
android:padding="@dimen/medium_margin"
|
||||
android:src="@drawable/ic_add_call_vector"
|
||||
app:layout_constraintEnd_toEndOf="@id/call_manage"
|
||||
app:layout_constraintStart_toEndOf="@+id/call_toggle_hold"
|
||||
app:layout_constraintEnd_toEndOf="@+id/call_toggle_speaker"
|
||||
app:layout_constraintStart_toStartOf="@+id/call_toggle_speaker"
|
||||
app:layout_constraintTop_toBottomOf="@+id/call_toggle_speaker" />
|
||||
|
||||
<ImageView
|
||||
@ -234,9 +234,10 @@
|
||||
android:padding="@dimen/medium_margin"
|
||||
android:src="@drawable/ic_people_vector"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/call_add"
|
||||
app:layout_constraintTop_toBottomOf="@+id/call_toggle_speaker" />
|
||||
app:layout_constraintEnd_toEndOf="@+id/call_dialpad"
|
||||
app:layout_constraintStart_toStartOf="@+id/call_dialpad"
|
||||
app:layout_constraintTop_toBottomOf="@+id/call_dialpad"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/call_swap"
|
||||
@ -247,11 +248,10 @@
|
||||
android:contentDescription="@string/call_swap"
|
||||
android:padding="@dimen/medium_margin"
|
||||
android:src="@drawable/ic_call_swap_vector"
|
||||
app:layout_constraintEnd_toStartOf="@+id/call_merge"
|
||||
app:layout_constraintEnd_toEndOf="@+id/call_toggle_microphone"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/call_add"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/call_toggle_speaker" />
|
||||
app:layout_constraintStart_toStartOf="@+id/call_toggle_microphone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/call_toggle_microphone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/call_merge"
|
||||
@ -262,9 +262,9 @@
|
||||
android:contentDescription="@string/call_merge"
|
||||
android:padding="@dimen/medium_margin"
|
||||
android:src="@drawable/ic_call_merge_vector"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/call_toggle_speaker"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/call_swap"
|
||||
app:layout_constraintStart_toStartOf="@+id/call_toggle_speaker"
|
||||
app:layout_constraintTop_toBottomOf="@+id/call_toggle_speaker" />
|
||||
|
||||
<ImageView
|
||||
|
17
app/src/main/res/layout/activity_conference.xml
Normal file
17
app/src/main/res/layout/activity_conference.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.simplemobiletools.commons.views.MyRecyclerView
|
||||
android:id="@+id/conference_calls_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:scrollbars="none"
|
||||
app:layoutManager="com.simplemobiletools.commons.views.MyLinearLayoutManager"
|
||||
tools:listitem="@layout/item_conference_call" />
|
||||
|
||||
</FrameLayout>
|
60
app/src/main/res/layout/item_conference_call.xml
Normal file
60
app/src/main/res/layout/item_conference_call.xml
Normal file
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/item_conference_call_image"
|
||||
android:layout_width="@dimen/normal_icon_size"
|
||||
android:layout_height="@dimen/normal_icon_size"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_margin="@dimen/medium_margin"
|
||||
android:contentDescription="@null"
|
||||
android:padding="@dimen/small_margin"
|
||||
android:src="@drawable/ic_person_vector"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.simplemobiletools.commons.views.MyTextView
|
||||
android:id="@+id/item_conference_call_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/medium_margin"
|
||||
android:layout_toEndOf="@+id/item_conference_call_image"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="@dimen/bigger_text_size"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/item_conference_call_image"
|
||||
app:layout_constraintEnd_toStartOf="@+id/item_conference_call_split"
|
||||
app:layout_constraintStart_toEndOf="@+id/item_conference_call_image"
|
||||
app:layout_constraintTop_toTopOf="@+id/item_conference_call_image"
|
||||
tools:text="John Doe" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/item_conference_call_end"
|
||||
android:layout_width="@dimen/normal_icon_size"
|
||||
android:layout_height="@dimen/normal_icon_size"
|
||||
android:layout_margin="@dimen/medium_margin"
|
||||
android:background="?actionBarItemBackground"
|
||||
android:contentDescription="@string/end_call"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_phone_down_vector" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/item_conference_call_split"
|
||||
android:layout_width="@dimen/normal_icon_size"
|
||||
android:layout_height="@dimen/normal_icon_size"
|
||||
android:layout_margin="@dimen/medium_margin"
|
||||
android:background="?actionBarItemBackground"
|
||||
android:contentDescription="@string/call_split"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/item_conference_call_end"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_call_split_vector" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Reference in New Issue
Block a user