Prototype to try InCallService APIs

This commit is contained in:
Sylvain Berfini 2025-08-07 12:04:26 +02:00
parent fac6e42c22
commit feda5bb5f7
3 changed files with 164 additions and 0 deletions

View file

@ -121,6 +121,11 @@
<data android:scheme="linphone-config" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
@ -208,6 +213,16 @@
</intent-filter>
</service>-->
<service android:name=".telecom.TelecomInCallService"
android:exported="true"
android:permission="android.permission.BIND_INCALL_SERVICE">
<meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" />
<meta-data android:name="android.telecom.IN_CALL_SERVICE_RINGING" android:value="true" />
<intent-filter>
<action android:name="android.telecom.InCallService" />
</intent-filter>
</service>
<!-- Receivers -->
<receiver android:name=".core.CorePushReceiver"

View file

@ -0,0 +1,115 @@
/*
* Copyright (c) 2010-2025 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.telecom
import android.os.Build
import android.telecom.Call
import android.telecom.InCallService
import android.telecom.VideoProfile
import androidx.annotation.RequiresApi
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.tools.Log
class TelecomInCallService : InCallService() {
companion object {
private const val TAG = "[Telecom InCall Service]"
}
private val callsList = arrayListOf<Call>()
private val callListener = object : Call.Callback() {
override fun onStateChanged(call: Call, state: Int) {
Log.i("$TAG Call state changed [${callStateToString(state)}]")
}
}
override fun onBringToForeground(showDialpad: Boolean) {
Log.i("$TAG onBringToForeground, showDialpad: $showDialpad")
}
@RequiresApi(Build.VERSION_CODES.Q)
override fun onCallAdded(call: Call) {
super.onCallAdded(call)
val uri = call.details.handle
val isIncoming = call.details.callDirection == Call.Details.DIRECTION_INCOMING
val state = call.details.state
Log.i("$TAG Added call direction is [${if (isIncoming) "incoming" else "outgoing"}], URI is [$uri], state [${callStateToString(state)}], call [$call]")
call.registerCallback(callListener)
callsList.add(call)
if (uri.scheme == "tel") {
val number = uri.schemeSpecificPart
Log.i("$TAG Extract number [$number] from tel: URI")
if (!isIncoming) {
coreContext.postOnCoreThread { core ->
val address = core.interpretUrl(number, true)
if (address != null) {
Log.i("$TAG Calling SIP URI [${address.asStringUriOnly()}]")
if (call.details.videoState == VideoProfile.STATE_AUDIO_ONLY) {
coreContext.startAudioCall(address)
} else {
coreContext.startVideoCall(address)
}
}
}
}
}
}
override fun onCallRemoved(call: Call) {
super.onCallRemoved(call)
Log.i("$TAG onCallRemoved with call [$call]")
call.unregisterCallback(callListener)
callsList.remove(call)
}
override fun onCanAddCallChanged(canAddCall: Boolean) {
Log.i("$TAG onCanAddCallChanged: $canAddCall")
}
override fun onMuteStateChanged(isMuted: Boolean) {
Log.i("$TAG onMuteStateChanged: $isMuted")
}
override fun onSilenceRinger() {
Log.i("$TAG onSilenceRinger")
}
private fun callStateToString(state: Int): String {
return when (state) {
Call.STATE_NEW -> "NEW"
Call.STATE_DIALING -> "DIALING"
Call.STATE_RINGING -> "RINGING"
Call.STATE_HOLDING -> "HOLDING"
Call.STATE_ACTIVE -> "ACTIVE"
Call.STATE_DISCONNECTED -> "DISCONNECTED"
Call.STATE_SELECT_PHONE_ACCOUNT -> "SELECT_PHONE_ACCOUNT"
Call.STATE_CONNECTING -> "CONNECTING"
Call.STATE_DISCONNECTING -> "DISCONNECTING"
Call.STATE_PULLING_CALL -> "PULLING_CALL"
Call.STATE_AUDIO_PROCESSING -> "AUDIO_PROCESSING"
Call.STATE_SIMULATED_RINGING -> "SIMULATED_RINGING"
else -> "UNKNOWN [$state]"
}
}
}

View file

@ -21,7 +21,10 @@ package org.linphone.ui.main
import android.Manifest
import android.annotation.SuppressLint
import android.app.ComponentCaller
import android.app.Dialog
import android.app.role.RoleManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
@ -89,6 +92,8 @@ class MainActivity : GenericActivity() {
private const val CHAT_FRAGMENT_ID = 3
private const val MEETINGS_FRAGMENT_ID = 4
private const val DIALER_ROLE_REQUEST_CODE = 1
const val ARGUMENTS_CHAT = "Chat"
const val ARGUMENTS_CONVERSATION_ID = "ConversationId"
}
@ -391,6 +396,8 @@ class MainActivity : GenericActivity() {
Log.d("$TAG savedInstanceState is null but intent isn't, handling it")
handleIntent(intent)
}
requestDialerRole()
}
override fun onPause() {
@ -438,6 +445,21 @@ class MainActivity : GenericActivity() {
handleIntent(intent)
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?,
caller: ComponentCaller
) {
if (requestCode == DIALER_ROLE_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
Log.i("$TAG Dialer role was granted!")
} else {
Log.w("$TAG Dialer role was denied!")
}
}
}
@SuppressLint("RtlHardcoded")
fun toggleDrawerMenu() {
if (binding.drawerMenu.isDrawerOpen(Gravity.LEFT)) {
@ -849,4 +871,16 @@ class MainActivity : GenericActivity() {
}
}
}
private fun requestDialerRole() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
val roleManager: RoleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager
val shouldRequestRole = roleManager.isRoleAvailable(RoleManager.ROLE_DIALER) &&
!roleManager.isRoleHeld(RoleManager.ROLE_DIALER)
if (shouldRequestRole) {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)
startActivityForResult(intent, DIALER_ROLE_REQUEST_CODE)
}
}
}
}