switch between two calls

This commit is contained in:
Mysochenko Yuriy 2022-05-15 09:12:23 +03:00
parent a2bff29d59
commit 445e13389e
6 changed files with 297 additions and 59 deletions

View File

@ -99,6 +99,7 @@
android:excludeFromRecents="true"
android:exported="false"
android:label="@string/ongoing_call"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:showOnLockScreen="true" />

View File

@ -22,21 +22,21 @@ 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.addCharacter
import com.simplemobiletools.dialer.extensions.audioManager
import com.simplemobiletools.dialer.extensions.config
import com.simplemobiletools.dialer.extensions.getHandleToUse
import com.simplemobiletools.dialer.extensions.*
import com.simplemobiletools.dialer.helpers.CallContactAvatarHelper
import com.simplemobiletools.dialer.helpers.CallManager
import com.simplemobiletools.dialer.helpers.CallManagerListener
import com.simplemobiletools.dialer.models.CallContact
import kotlinx.android.synthetic.main.activity_call.*
import kotlinx.android.synthetic.main.dialpad.*
const val TAG = "SimpleDialer:CallManager"
class CallActivity : SimpleActivity() {
companion object {
fun getStartIntent(context: Context): Intent {
val openAppIntent = Intent(context, CallActivity::class.java)
openAppIntent.flags = Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT or Intent.FLAG_ACTIVITY_NEW_TASK
openAppIntent.flags = Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
return openAppIntent
}
}
@ -62,24 +62,21 @@ class CallActivity : SimpleActivity() {
audioManager.mode = AudioManager.MODE_IN_CALL
CallManager.getCallContact(applicationContext) { contact ->
callContact = contact
val avatar = callContactAvatarHelper.getCallContactAvatar(contact)
runOnUiThread {
updateOtherPersonsInfo(avatar)
checkCalledSIMCard()
}
}
addLockScreenFlags()
CallManager.registerCallback(callCallback)
updateCallState(CallManager.getState())
CallManager.addListener(callCallback)
}
override fun onResume() {
super.onResume()
updateCallState(CallManager.getPrimaryCall())
updateCallOnHoldState(CallManager.getSecondaryCall())
updateCallContactInfo(CallManager.getPrimaryCall())
}
override fun onDestroy() {
super.onDestroy()
CallManager.unregisterCallback(callCallback)
CallManager.removeListener(callCallback)
disableProximitySensor()
}
@ -135,12 +132,23 @@ class CallActivity : SimpleActivity() {
toggleHold()
}
call_conference.setOnClickListener {
/*if (is conference) {
// show manage conference screen
} else {
// show dialpad and contacts
}*/
call_add.setOnClickListener {
Intent(applicationContext, DialpadActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
startActivity(this)
}
}
call_swap.setOnClickListener {
CallManager.swap()
}
call_merge.setOnClickListener {
CallManager.merge()
}
call_manage.setOnClickListener {
// TODO open conference participants list
}
call_end.setOnClickListener {
@ -347,6 +355,14 @@ class CallActivity : SimpleActivity() {
}
}
private fun getContactNameOrNumber(contact: CallContact): String {
return contact.name.ifEmpty {
contact.number.ifEmpty {
getString(R.string.unknown_caller)
}
}
}
@SuppressLint("MissingPermission")
private fun checkCalledSIMCard() {
try {
@ -375,7 +391,8 @@ class CallActivity : SimpleActivity() {
}
}
private fun updateCallState(state: Int) {
private fun updateCallState(call: Call?) {
val state = call?.getStateCompat()
when (state) {
Call.STATE_RINGING -> callRinging()
Call.STATE_ACTIVE -> callStarted()
@ -394,9 +411,35 @@ class CallActivity : SimpleActivity() {
call_status_label.text = getString(statusTextId)
}
val isActiveCall = state == Call.STATE_ACTIVE || state == Call.STATE_HOLDING
call_toggle_hold.isEnabled = isActiveCall
call_toggle_hold.alpha = if (isActiveCall) 1.0f else 0.5f
val isSingleCallActionsEnabled = (state == Call.STATE_ACTIVE || state == Call.STATE_DISCONNECTED
|| state == Call.STATE_DISCONNECTING || state == Call.STATE_HOLDING)
setActionButtonEnabled(call_toggle_hold, isSingleCallActionsEnabled)
setActionButtonEnabled(call_add, isSingleCallActionsEnabled)
}
private fun updateCallOnHoldState(call: Call?) {
val hasCallOnHold = call != null
if (hasCallOnHold) {
CallManager.getCallContact(applicationContext, call) { contact ->
runOnUiThread {
on_hold_caller_name.text = getContactNameOrNumber(contact)
}
}
}
on_hold_status_holder.beVisibleIf(hasCallOnHold)
controls_single_call.beVisibleIf(!hasCallOnHold) // TODO and not conference
controls_two_calls.beVisibleIf(hasCallOnHold)
}
private fun updateCallContactInfo(call: Call?) {
CallManager.getCallContact(applicationContext, call) { contact ->
callContact = contact
val avatar = callContactAvatarHelper.getCallContactAvatar(contact)
runOnUiThread {
updateOtherPersonsInfo(avatar)
checkCalledSIMCard()
}
}
}
private fun acceptCall() {
@ -417,6 +460,7 @@ class CallActivity : SimpleActivity() {
enableProximitySensor()
incoming_call_holder.beGone()
ongoing_call_holder.beVisible()
callDurationHandler.removeCallbacks(updateCallDurationTask)
callDurationHandler.post(updateCallDurationTask)
}
@ -456,10 +500,19 @@ class CallActivity : SimpleActivity() {
}
}
private val callCallback = object : Call.Callback() {
private val callCallback = object : CallManagerListener {
override fun onStateChanged(call: Call, state: Int) {
super.onStateChanged(call, state)
updateCallState(state)
updateCallState(call)
}
override fun onCallPutOnHold(call: Call?) {
updateCallOnHoldState(call)
}
override fun onCallsChanged(active: Call, onHold: Call?) {
updateCallState(active)
updateCallOnHoldState(onHold)
updateCallContactInfo(active)
}
}
@ -507,4 +560,9 @@ class CallActivity : SimpleActivity() {
proximityWakeLock!!.release()
}
}
private fun setActionButtonEnabled(button: ImageView, isEnabled: Boolean) {
button.isEnabled = isEnabled
button.alpha = if (isEnabled) 1.0f else 0.4f
}
}

View File

@ -9,8 +9,10 @@ import com.simplemobiletools.commons.helpers.isSPlus
private val OUTGOING_CALL_STATES = arrayOf(STATE_CONNECTING, STATE_DIALING, STATE_SELECT_PHONE_ACCOUNT)
@Suppress("DEPRECATION")
fun Call.getStateCompat(): Int {
return if (isSPlus()) {
fun Call?.getStateCompat(): Int {
return if (this == null) {
Call.STATE_DISCONNECTED
} else if (isSPlus()) {
details.state
} else {
state
@ -20,3 +22,9 @@ fun Call.getStateCompat(): Int {
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?.isConference(): Boolean = this?.details?.hasProperty(Call.Details.PROPERTY_CONFERENCE) == true

View File

@ -5,6 +5,7 @@ import android.net.Uri
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
@ -12,12 +13,64 @@ import com.simplemobiletools.commons.helpers.SimpleContactsHelper
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.dialer.extensions.getStateCompat
import com.simplemobiletools.dialer.models.CallContact
import java.util.concurrent.CopyOnWriteArraySet
const val TAG = "SimpleDialer:CallManager"
// inspired by https://github.com/Chooloo/call_manage
class CallManager {
companion object {
var call: Call? = null
var inCallService: InCallService? = null
val calls = mutableListOf<Call>()
private val listeners = CopyOnWriteArraySet<CallManagerListener>()
fun onCallAdded(call: Call) {
this.call = call
calls.add(call)
call.registerCallback(object : Call.Callback() {
override fun onStateChanged(call: Call, state: Int) {
Log.d(TAG, "onStateChanged: $call")
for (listener in listeners) {
listener.onStateChanged(call, state)
}
if (state == Call.STATE_HOLDING && calls.size > 1) {
for (listener in listeners) {
listener.onCallPutOnHold(call)
}
}
if (state == Call.STATE_ACTIVE && calls.size == 1) {
for (listener in listeners) {
listener.onCallPutOnHold(null)
}
}
if ((state == Call.STATE_CONNECTING || state == Call.STATE_DIALING || state == Call.STATE_ACTIVE) && calls.size > 1) {
if (CallManager.call != call) {
CallManager.call = call
Log.d(TAG, "onCallsChanged")
for (listener in listeners) {
listener.onCallsChanged(call, getSecondaryCall())
}
}
}
}
})
}
fun onCallRemoved(call: Call) {
calls.remove(call)
}
fun getPrimaryCall(): Call? {
return call
}
fun getSecondaryCall(): Call? {
if (calls.size == 1) {
return null
}
return calls.find { it.getStateCompat() == Call.STATE_HOLDING }
}
fun accept() {
call?.answer(VideoProfile.STATE_AUDIO_ONLY)
@ -43,27 +96,33 @@ class CallManager {
return !isOnHold
}
fun swap() {
val isConference: Boolean
get() = call?.details?.hasProperty(Call.Details.PROPERTY_CONFERENCE) ?: false
fun swap() {
getSecondaryCall()?.unhold()
}
fun merge() {
// val conferenceableCalls = call!!.conferenceableCalls
// if (conferenceableCalls.isNotEmpty()) {
// call!!.conference(conferenceableCalls.first())
// } else {
// if (call!!.hasCapability(Call.Details.CAPABILITY_MERGE_CONFERENCE)) {
// call!!.mergeConference()
// }
// }
}
fun registerCallback(callback: Call.Callback) {
call?.registerCallback(callback)
fun addListener(listener: CallManagerListener) {
listeners.add(listener)
}
fun unregisterCallback(callback: Call.Callback) {
call?.unregisterCallback(callback)
fun removeListener(listener: CallManagerListener) {
listeners.remove(listener)
}
fun getState() = if (call == null) {
Call.STATE_DISCONNECTED
} else {
call!!.getStateCompat()
}
fun getState() = getPrimaryCall()?.getStateCompat()
fun keypad(c: Char) {
call?.playDtmfTone(c)
@ -71,6 +130,10 @@ class CallManager {
}
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("", "", "", "")
@ -126,10 +189,20 @@ class CallManager {
fun getCallDuration(): Int {
return if (call != null) {
((System.currentTimeMillis() - call!!.details.connectTimeMillis) / 1000).toInt()
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 onCallPutOnHold(call: Call?)
fun onCallsChanged(active: Call, onHold: Call?)
}

View File

@ -17,7 +17,6 @@ class CallService : InCallService() {
private val callListener = object : Call.Callback() {
override fun onStateChanged(call: Call, state: Int) {
super.onStateChanged(call, state)
Log.d(TAG, "onStateChanged: $call")
if (state != Call.STATE_DISCONNECTED) {
callNotificationManager.setupNotification()
}
@ -27,26 +26,36 @@ class CallService : InCallService() {
override fun onCallAdded(call: Call) {
super.onCallAdded(call)
Log.d(TAG, "onCallAdded: $call")
if (!powerManager.isInteractive || call.isOutgoing()) {
CallManager.onCallAdded(call)
if ((!powerManager.isInteractive || call.isOutgoing())) {
startActivity(CallActivity.getStartIntent(this))
}
CallManager.call = call
call.registerCallback(callListener)
CallManager.inCallService = this
CallManager.registerCallback(callListener)
callNotificationManager.setupNotification()
Log.d(TAG, "onCallAdded: calls=${CallManager.calls.size}")
}
override fun onCallRemoved(call: Call) {
super.onCallRemoved(call)
Log.d(TAG, "onCallRemoved: $call")
CallManager.call = null
CallManager.inCallService = null
callNotificationManager.cancelNotification()
call.unregisterCallback(callListener)
CallManager.onCallRemoved(call)
if (CallManager.calls.isEmpty()) {
CallManager.call = null
CallManager.inCallService = null
callNotificationManager.cancelNotification()
} else {
// TODO if left more than 1
CallManager.call = CallManager.calls.first()
callNotificationManager.setupNotification()
startActivity(CallActivity.getStartIntent(this))
}
Log.d(TAG, "onCallRemoved: calls=${CallManager.calls.size}")
}
override fun onDestroy() {
super.onDestroy()
CallManager.unregisterCallback(callListener)
callNotificationManager.cancelNotification()
}
}

View File

@ -102,6 +102,54 @@
app:layout_constraintTop_toTopOf="@+id/call_sim_image"
tools:text="1" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/on_hold_status_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/cardview_shadow_start_color"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/normal_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_phone_vector" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/on_hold_caller_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/normal_margin"
android:ellipsize="end"
android:maxLines="1"
android:textSize="@dimen/call_status_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/on_hold_label"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="0912 345 678" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/on_hold_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/normal_margin"
android:text="@string/call_on_hold"
android:textSize="@dimen/call_status_text_size"
app:layout_constraintBottom_toBottomOf="@+id/imageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/imageView" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/ongoing_call_holder"
android:layout_width="match_parent"
@ -157,24 +205,24 @@
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_pause_inset"
app:layout_constraintEnd_toStartOf="@id/call_conference"
app:layout_constraintEnd_toStartOf="@id/call_add"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/call_toggle_speaker" />
<ImageView
android:id="@+id/call_conference"
android:id="@+id/call_add"
android:layout_width="@dimen/dialpad_button_size"
android:layout_height="@dimen/dialpad_button_size"
android:layout_marginTop="@dimen/bigger_margin"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_add_call_vector"
app:layout_constraintEnd_toStartOf="@id/manage_conference"
app:layout_constraintEnd_toEndOf="@id/call_manage"
app:layout_constraintStart_toEndOf="@+id/call_toggle_hold"
app:layout_constraintTop_toBottomOf="@+id/call_toggle_speaker" />
<ImageView
android:id="@+id/manage_conference"
android:id="@+id/call_manage"
android:layout_width="@dimen/dialpad_button_size"
android:layout_height="@dimen/dialpad_button_size"
android:layout_marginTop="@dimen/bigger_margin"
@ -183,9 +231,35 @@
android:src="@drawable/ic_people_vector"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/call_conference"
app:layout_constraintTop_toBottomOf="@+id/call_toggle_speaker"
tools:visibility="visible" />
app:layout_constraintStart_toEndOf="@+id/call_add"
app:layout_constraintTop_toBottomOf="@+id/call_toggle_speaker" />
<ImageView
android:id="@+id/call_swap"
android:layout_width="@dimen/dialpad_button_size"
android:layout_height="@dimen/dialpad_button_size"
android:layout_marginTop="@dimen/bigger_margin"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_call_swap_vector"
app:layout_constraintEnd_toStartOf="@+id/call_merge"
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" />
<ImageView
android:id="@+id/call_merge"
android:layout_width="@dimen/dialpad_button_size"
android:layout_height="@dimen/dialpad_button_size"
android:layout_marginTop="@dimen/bigger_margin"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_call_merge_vector"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/call_swap"
app:layout_constraintTop_toBottomOf="@+id/call_toggle_speaker" />
<ImageView
android:id="@+id/call_end"
@ -199,6 +273,21 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.9" />
<androidx.constraintlayout.widget.Group
android:id="@+id/controls_single_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="call_toggle_hold,call_add"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
android:id="@+id/controls_two_calls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="call_swap,call_merge" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout