I'm trying to make a custom implementation of a call using TelecomManager between two users who had installed my app on their devices
Following this guide I implemented connection service, subclass of Connection, added permissions, registered a PhoneAccount and so on...
The thing I'm struggling to understand for a third week already how to place a call between users of my app without using telephone number but user name or userId.
Below code starting to make a call from my device but this call never reaches end user device
telecomManager.placeCall(Uri.fromParts(/*tried also with PhoneAccount.SCHEME_SIP and PhoneAccount.SCHEME_TEL*/
TripmateConnectionService.SCHEME_AG, "userId", null), extras)
Need to mention, that in my BroadcastReceiver implementation I can detect incoming calls from other apps, so it's seems that I handling call detection correct and the call from above code is never really send to device of the user it was intended to.
Now is the question. I feel like I missing something vital. How exactly do devices with same app can communicate to each other without phone number? Does it really enough just to pass a user name to a telecomManager.placeCall and it should somehow manage to find right device with installed app and make a call to it? How can telecomManager distinguish where to make a call?
Sorry for unclear question, it's my first time doing something related to calls and I feel I luck understanding of the subject and it hard to make a question more concrete because I don't exactly know what am I missing.
I will put below some code I'm using now
Start an outgoing call
private fun placeSystemCall(myUid: String, peerUid: String, channel: String, role: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val extras = Bundle()
extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_BIDIRECTIONAL)
val extraBundle = Bundle()
extraBundle.putString(Constants.CS_KEY_UID, myUid)
extraBundle.putString(Constants.CS_KEY_SUBSCRIBER, peerUid)
extraBundle.putString(Constants.CS_KEY_CHANNEL, channel)
extraBundle.putInt(Constants.CS_KEY_ROLE, Constants.CALL_ID_OUT)
extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extraBundle)
try {
val telecomManager = applicationContext.getSystemService(TELECOM_SERVICE) as TelecomManager
val pa: PhoneAccount = telecomManager.getPhoneAccount(
config().phoneAccountOut?.accountHandle)
extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, true);
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, pa.accountHandle)
telecomManager.placeCall(Uri.fromParts(
TripmateConnectionService.SCHEME_AG, peerUid, null), extras)
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}
In ConnectionService
override fun onCreateOutgoingConnection(phoneAccount: PhoneAccountHandle?, request: ConnectionRequest): Connection {
Log.i(TAG, "onCreateOutgoingConnection: called. $phoneAccount $request")
val extras = request.extras
val uid = extras.getString(Constants.CS_KEY_UID) ?: "0"
val channel = extras.getString(Constants.CS_KEY_CHANNEL) ?: "0"
val subscriber = extras.getString(Constants.CS_KEY_SUBSCRIBER) ?: "0"
val role = extras.getInt(Constants.CS_KEY_ROLE)
val videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE)
val connection = TripmateConnection(applicationContext, uid, channel, subscriber, role)
connection.setVideoState(videoState)
connection.setAddress(Uri.fromParts(SCHEME_AG, subscriber, null), TelecomManager.PRESENTATION_ALLOWED)
connection.setCallerDisplayName(subscriber, TelecomManager.PRESENTATION_ALLOWED)
connection.setRinging()
TMApplication.getInstance().config().setConnection(connection)
return connection
}
creating PhoneAccounts
private fun registerPhoneAccount(context: Context) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager
?: throw RuntimeException("cannot obtain telecom system service")
val accountHandleIn = PhoneAccountHandle(
ComponentName(context, TripmateConnectionService::class.java), Constants.PA_LABEL_CALL_IN)
val accountHandleOut = PhoneAccountHandle(
ComponentName(context, TripmateConnectionService::class.java), Constants.PA_LABEL_CALL_OUT)
try {
var paBuilder: PhoneAccount.Builder = PhoneAccount.builder(accountHandleIn, Constants.PA_LABEL_CALL_IN)
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
val phoneIn = paBuilder.build()
paBuilder = PhoneAccount.builder(accountHandleOut, Constants.PA_LABEL_CALL_OUT)
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
val extra = Bundle()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
extra.putBoolean(PhoneAccount.EXTRA_LOG_SELF_MANAGED_CALLS, true)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
paBuilder.setExtras(extra)
}
val phoneOut = paBuilder.build()
telecomManager.registerPhoneAccount(phoneIn)
telecomManager.registerPhoneAccount(phoneOut)
if (telecomManager.getPhoneAccount(phoneIn.accountHandle) == null || telecomManager.getPhoneAccount(phoneOut.accountHandle) == null) {
throw RuntimeException("cannot create account");
}
mCallSession = TripmateCallSession()
mCallSession?.phoneAccountIn = phoneIn
mCallSession?.phoneAccountOut = phoneOut
} catch (e: SecurityException) {
throw RuntimeException("cannot create account", e);
}
}
Thank you for your time! Any suggestions and links that could help me to understand more will be highly appreciated!
Related
I implemented the Activity Transitions API with a PendingIntent and a BroadcastReceiver as seen below. The code works perfectly fine on a Pixel 3a. However, on a Samsung A32 and Samsung S22 Pro, the Broadcast receiver is never reached, eventhough the ActivityRecognition.getClient(mainActivity).requestActivityTransitionUpdates() succeeds and enters the onSuccessListener().
After a lot of time spent reading through the internet, I wasn't able to find any further information. Neither concerning the Activity Transitions API, nor concerning such problems on Samsung devices (e.g. not raching BroadcastReceiver). Some people hint to disabling battery saving features from Samsung, but the App runs currently only in foreground in the MainActivty thread, therefore I don't think my problem is related to that. Other point out that, for example Huawai devices, need a diffferent permission than the in the android docs specified one for the Activity Transition API. So currently I'm specifing those three permissions (and check them run-time with ContextCompat.checkSelfPermission()):
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="com.huawei.hms.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
Code
Main class with
initalization function initController() called in the form of
mActivityTransitionController = ActivityTransitionController().also { it.initController(this#MainActivity) }
and the entrypoint onClickEnableOrDisableActivityRecognition():
class ActivityTransitionController() {
companion object {
internal val TRANSITION_RECEIVER_ACTION: String =
"MyMachineLearningStalkingProtection.TRANSITIONS_RECEIVER_ACTION"
}
private var activityTrackingOn: Boolean = false
private lateinit var activityTransitionList: List<ActivityTransition>
private lateinit var mActivityTransitionPendingIntent: PendingIntent
internal fun initController(mainActivity: MainActivity) {
activityTrackingOn = false
activityTransitionList = buildTransitionList()
val intent = Intent(TRANSITION_RECEIVER_ACTION)
mActivityTransitionPendingIntent =
PendingIntent.getBroadcast(mainActivity, 0, intent, PendingIntent.FLAG_MUTABLE)
Utils.makeSnackBar("Activity Recognition initialized!", mainActivity)
}
internal fun onClickEnableOrDisableActivityRecognition(mainActivity: MainActivity) {
if (activityTrackingOn) {
disableActivityTransitions(mainActivity)
} else {
enableActivityTransitions(mainActivity)
}
}
private fun buildTransitionList(): ArrayList<ActivityTransition> {
val list = ArrayList<ActivityTransition>()
list.add(
ActivityTransition.Builder()
.setActivityType(DetectedActivity.WALKING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()
)
list.add(
ActivityTransition.Builder()
.setActivityType(DetectedActivity.WALKING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()
)
list.add(
ActivityTransition.Builder()
.setActivityType(DetectedActivity.STILL)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()
)
list.add(
ActivityTransition.Builder()
.setActivityType(DetectedActivity.STILL)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()
)
return list
}
#SuppressLint("MissingPermission")
internal fun disableActivityTransitions(mainActivity: MainActivity) {
Log.d(Utils.MY_LOG_TAG, "disableActivityTransitions()")
ActivityRecognition.getClient(mainActivity)
.removeActivityTransitionUpdates(mActivityTransitionPendingIntent)
.addOnSuccessListener {
activityTrackingOn = false
Utils.makeSnackBar("Transitions successfully unregistered.", mainActivity)
}.addOnFailureListener {
Utils.makeSnackBar("Transitions could NOT be unregistered.", mainActivity)
Log.e(Utils.MY_LOG_TAG, "Transitions could not be unregistered $it")
}
}
#SuppressLint("MissingPermission")
internal fun enableActivityTransitions(mainActivity: MainActivity) {
Log.d(Utils.MY_LOG_TAG, "enableActivityTransitions()")
val request = ActivityTransitionRequest(activityTransitionList)
ActivityRecognition.getClient(mainActivity)
.requestActivityTransitionUpdates(request, mActivityTransitionPendingIntent)
.addOnSuccessListener {
Utils.makeSnackBar("Transitions Api was successfully registered", mainActivity)
activityTrackingOn = true
}
.addOnFailureListener {
Utils.makeSnackBar("Transitions Api could NOT be registered", mainActivity)
Log.e(Utils.MY_LOG_TAG, "Transitions Api could NOT be registered. $it")
}
}
}
Boradcast receiver
registerd in MainActivity's onStart()
unregisterd in MainActivity's onStop()
class ActivityTransitionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val mainActivity = context as MainActivity
val findViewById = mainActivity.findViewById<TextView>(R.id.txt_activity)
val now = Calendar.getInstance().time.toString()
val currentText = findViewById.text
if (currentText.isEmpty()) {
findViewById.text = "1##$now"
} else {
val split = currentText.split("##")
val num = split[0].toInt() + 1
findViewById.text = "$num##$now"
}
if (ActivityRecognitionResult.hasResult(intent)) {
Log.d(Utils.MY_LOG_TAG, "RECOGNITION called")
}
if (ActivityTransitionResult.hasResult(intent)) {
Log.d(Utils.MY_LOG_TAG, "TRANSITION called")
val result = ActivityTransitionResult.extractResult(intent!!)
for (event in result!!.transitionEvents) {
val activityType = event.activityType
val transitionType = event.transitionType
val elapsedRealTimeNanos = event.elapsedRealTimeNanos
val findViewById1 = mainActivity.findViewById<TextView>(R.id.txt_confidence)
findViewById1.text ="${findViewById1.text} + $activityType + $transitionType"
}
}
}
}
With this code, the Pixel 3a is able to detect my activities as soon as I call the onClickEnableOrDisableActivityRecognition() entrypoint. On the Samsung devices however, nothing happens, the requestActivityTransitionUpdates() succeeds, though the broadcast receiver ActivityTransitionReceiver is never reached. Do you guys have any idea why I expereience this behaviour? Maybe you experienced similar behaviour with a BroadcastReceiver and were able to fix it?
On a short side note: I also tested if the ActivityRecognition API is available on the Samsung devices using code which is equivalent as described in the docs https://developers.google.com/android/guides/api-client#check-api-availability which succeeded.
If something is unclear, do not hesitate to ask for clarification. Thanks in advance!
In my driving-companion app, I have a need to detect the state of Android Auto. For several years now, I've been using UiModeManager to get the current state at startup and a BroadcastReceiver to detect state changes while the app is running. This has always worked perfectly, until Android 12. With Android 12, UiModeManager always reports UI_MODE_TYPE_NORMAL, even when Android Auto is connected and active, and my BroadcastReceiver is never called after connecting or disconnecting.
This is my code for detecting state at startup:
inCarMode = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
and this is my BroadcastReceiver setup:
IntentFilter carModeFilter = new IntentFilter();
carModeFilter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE);
carModeFilter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE);
registerReceiver(carModeReceiver, carModeFilter);
Again, this has always worked perfectly with Android 5 through Android 11. Is this a bug in Android 12, or is there some new way to detect Android Auto state in Android 12?
You need to use the CarConnection API documented here
Configuration.UI_MODE_TYPE_CAR is not working on Anroid 12. As #Pierre-Olivier Dybman said, you can use CarConnection API in the androidx.car.app:app library. But that is too heavy to import entire library only for car connections if you don't need other features.
So I write a piece of code base on the CarConnection to detect Android Auto connection, as below:
class AutoConnectionDetector(val context: Context) {
companion object {
const val TAG = "AutoConnectionDetector"
// columnName for provider to query on connection status
const val CAR_CONNECTION_STATE = "CarConnectionState"
// auto app on your phone will send broadcast with this action when connection state changes
const val ACTION_CAR_CONNECTION_UPDATED = "androidx.car.app.connection.action.CAR_CONNECTION_UPDATED"
// phone is not connected to car
const val CONNECTION_TYPE_NOT_CONNECTED = 0
// phone is connected to Automotive OS
const val CONNECTION_TYPE_NATIVE = 1
// phone is connected to Android Auto
const val CONNECTION_TYPE_PROJECTION = 2
private const val QUERY_TOKEN = 42
private const val CAR_CONNECTION_AUTHORITY = "androidx.car.app.connection"
private val PROJECTION_HOST_URI = Uri.Builder().scheme("content").authority(CAR_CONNECTION_AUTHORITY).build()
}
private val carConnectionReceiver = CarConnectionBroadcastReceiver()
private val carConnectionQueryHandler = CarConnectionQueryHandler(context.contentResolver)
fun registerCarConnectionReceiver() {
context.registerReceiver(carConnectionReceiver, IntentFilter(ACTION_CAR_CONNECTION_UPDATED))
queryForState()
}
fun unRegisterCarConnectionReceiver() {
context.unregisterReceiver(carConnectionReceiver)
}
private fun queryForState() {
carConnectionQueryHandler.startQuery(
QUERY_TOKEN,
null,
PROJECTION_HOST_URI,
arrayOf(CAR_CONNECTION_STATE),
null,
null,
null
)
}
inner class CarConnectionBroadcastReceiver : BroadcastReceiver() {
// query for connection state every time the receiver receives the broadcast
override fun onReceive(context: Context?, intent: Intent?) {
queryForState()
}
}
internal class CarConnectionQueryHandler(resolver: ContentResolver?) : AsyncQueryHandler(resolver) {
// notify new queryed connection status when query complete
override fun onQueryComplete(token: Int, cookie: Any?, response: Cursor?) {
if (response == null) {
Log.w(TAG, "Null response from content provider when checking connection to the car, treating as disconnected")
notifyCarDisconnected()
return
}
val carConnectionTypeColumn = response.getColumnIndex(CAR_CONNECTION_STATE)
if (carConnectionTypeColumn < 0) {
Log.w(TAG, "Connection to car response is missing the connection type, treating as disconnected")
notifyCarDisconnected()
return
}
if (!response.moveToNext()) {
Log.w(TAG, "Connection to car response is empty, treating as disconnected")
notifyCarDisconnected()
return
}
val connectionState = response.getInt(carConnectionTypeColumn)
if (connectionState == CONNECTION_TYPE_NOT_CONNECTED) {
Log.i(TAG, "Android Auto disconnected")
notifyCarDisconnected()
} else {
Log.i(TAG, "Android Auto connected")
notifyCarConnected()
}
}
}
}
This solution works on android 6~12. If you need to detect car connection status on android 5, use the Configuration.UI_MODE_TYPE_CAR solution.
Background
I'm working on an app that implements InCallService, so it can handle phone calls
The problem
On devices with multi-SIM, I need to show information of which SIM and associated phone number is used (of the current device).
Thing is, I can't find where to get this information.
What I've found
Given that I reach a function like onCallAdded, I get an instance of Call class, so I need to associate something I get from there, to a sim slot and phone number.
Using call.getDetails().getHandle() , I can get a Uri consisting only the phone number of the other person that called (who called the current user, or who the current user has called).
I can iterate over all SIM cards, but can't find what I can use to map between them and the current call:
final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
final SubscriptionManager subscriptionManager = SubscriptionManager.from(context);
for (final SubscriptionInfo subscriptionInfo : subscriptionManager.getActiveSubscriptionInfoList()) {
final TelephonyManager subscriptionId = telephonyManager.createForSubscriptionId(subscriptionInfo.getSubscriptionId());
}
There was an old code that doesn't work anymore that uses call.getDetails().getAccountHandle().getId() and SubscriptionManager.from(context)getActiveSubscriptionInfoList().getActiveSubscriptionInfoList() .
I think I can use telephonyManager.getSubscriptionId(callDetails.getAccountHandle()) , but it requires API 30, which is quite new...
The questions
Given a phone call that I get from this callback (and probably others), how can I get the associated SIM slot and phone number of it?
I prefer to know how to do it for as wide range of Android versions as possible, because InCallService is from API 23... It should be possible before API 30, right?
Use call.getDetails().getAccountHandle() to get PhoneAccountHandle.
Then use TelecomManager.getPhoneAccount() to get PhoneAccount instance.
Permission Manifest.permission.READ_PHONE_NUMBERS is needed for applications targeting API level 31+.
Disclaimer: I am no Android expert, so please do validate my thoughts.
EDIT: So solution for both before API 30 and from API 30 could be as such:
#RequiresApi(Build.VERSION_CODES.M)
fun handleCall(context: Context, call: Call) {
var foundAndSetSimDetails = false
val callDetailsAccountHandle = callDetails.accountHandle
val subscriptionManager = context
.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager
val telephonyManager =
context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val telecomManager =
context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val hasReadPhoneStatePermission =
ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == android.content.pm.PackageManager.PERMISSION_GRANTED
val phoneAccount = telecomManager.getPhoneAccount(callDetailsAccountHandle)
//TODO when targeting API 31, we might need to check for a new permission here, of READ_PHONE_NUMBERS
//find SIM by phone account
if (!foundAndSetSimDetails && phoneAccount != null && hasReadPhoneStatePermission) {
val callCapablePhoneAccounts = telecomManager.callCapablePhoneAccounts
run {
callCapablePhoneAccounts?.forEachIndexed { index, phoneAccountHandle ->
if (phoneAccountHandle != callDetailsAccountHandle)
return#forEachIndexed
if (!phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION))
return#run
//found the sim card index
simName = phoneAccount.label?.toString().orEmpty()
simIndex = index + 1
foundAndSetSimDetails = true
return#run
}
}
}
//find SIM by subscription ID
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && hasReadPhoneStatePermission) {
try {
val callSubscriptionId: Int =
telephonyManager.getSubscriptionId(callDetailsAccountHandle!!)
for (subscriptionInfo: SubscriptionInfo in subscriptionManager.activeSubscriptionInfoList) {
val activeSubscriptionId: Int = subscriptionInfo.subscriptionId
if (activeSubscriptionId == callSubscriptionId) {
setSimDetails(telephonyManager, subscriptionInfo)
foundAndSetSimDetails = true
break
}
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
//find SIM by phone number
if (!foundAndSetSimDetails && hasReadPhoneStatePermission) {
try {
val simPhoneNumber: String? = phoneAccount?.address?.schemeSpecificPart
if (!simPhoneNumber.isNullOrBlank()) {
for (subscriptionInfo in subscriptionManager.activeSubscriptionInfoList) {
if (simPhoneNumber == subscriptionInfo.number) {
setSimDetails(telephonyManager, subscriptionInfo)
foundAndSetSimDetails = true
break
}
}
if (!foundAndSetSimDetails)
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
private fun setSimDetails(telephonyManager: TelephonyManager, subscriptionInfo: SubscriptionInfo) {
var foundSimName: String? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val telephonyManagerForSubscriptionId =
telephonyManager.createForSubscriptionId(subscriptionInfo.subscriptionId)
foundSimName = telephonyManagerForSubscriptionId.simOperatorName
}
if (foundSimName.isNullOrBlank())
foundSimName = subscriptionInfo.carrierName?.toString()
simName = if (foundSimName.isNullOrBlank())
""
else foundSimName
simIndex = subscriptionInfo.simSlotIndex + 1
}
I am sending SMS using SmsManager and I made use of WorkManager to be able to do it in background. Since the number of messages is dynamic, I chained WorkerRequest's for this. This is the snippet:
private fun startSmsWork(sms: Array<SmsEntity>) {
val gson = Gson()
val smsWorkerRequestBuilder = OneTimeWorkRequestBuilder<SmsWorker>()
smsWorkerRequestBuilder.setInputData(workDataOf(SMS_WORKER_INPUT_KEY to gson.toJson(sms[0])))
var continuation = WorkManager.getInstance()
.beginWith(smsWorkerRequestBuilder.build())
for (i in 1 until sms.size) {
smsWorkerRequestBuilder.setInputData(workDataOf(SMS_WORKER_INPUT_KEY to gson.toJson(sms[i])))
continuation = continuation.then(smsWorkerRequestBuilder.build())
}
continuation.enqueue()
}
And this is my worker class:
class SmsWorker(ctx: Context, workerParams: WorkerParameters) : Worker(ctx, workerParams) {
override fun doWork(): Result {
return try {
val data = inputData.getString(SMS_WORKER_INPUT_KEY)
val messages = Gson().fromJson(data, SmsEntity::class.java)
sendSms(messages)
Result.success()
} catch (e: Exception) {
Log.e("Sms", e.localizedMessage)
Result.failure()
}
}
private fun sendSms(msg: SmsEntity) {
try {
Log.d("SmsWorker", "Sending message ${msg.id}....")
val sentIntent = Intent(SENT_INTENT_ACTION)
sentIntent.putExtra(SENT_INTENT_EXTRA, msg.id)
val sentPI = PendingIntent.getBroadcast(applicationContext, 0, sentIntent, 0)
val deliveredIntent = Intent(DELIVERED_INTENT_ACTION)
deliveredIntent.putExtra(DELIVERED_INTENT_EXTRA, msg.id)
val deliveredPI = PendingIntent.getBroadcast(applicationContext, 0, sentIntent, 0)
val smsManager = SmsManager.getDefault()
smsManager.sendTextMessage(msg.num, null, msg.message, sentPI, deliveredPI)
} catch (e: Exception) {
Log.e("Sms", e.localizedMessage)
}
}
}
It works okay, BUT I want to be able to monitor each message sent. So I added a BroadcastReceiver for this but the intent that the BroadcastReceiver receives, would only be the latest one (in case of multiple messages). I found out that I have to wait for a message to be sent before sending one.
My question is how would I be able to implement this inside my worker class that it should wait for the result in BroadcastReceiver before returning the Result object.
Any input is much appreciated
If you want to wait asynchronously for the result of an operation during a worker, use a ListenableWorker.
Also, some general advice: since you're dealing with SMS, I'm pretty sure you will need a network constraint for your work. And you may also want to look at unique work and appending if that makes sense for your use case.
I'm trying to implement iOS callkit behavior on Android. I'm receiving a push notification from firebase and I want to show "incoming call" screen to the user. To do it I use ConnectionService from android.telecom package and other classes.
Here is my call manager class:
class CallManager(context: Context) {
val telecomManager: TelecomManager
var phoneAccountHandle:PhoneAccountHandle
var context:Context
val number = "3924823202"
init {
telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
this.context = context
val componentName = ComponentName(this.context, CallConnectionService::class.java)
phoneAccountHandle = PhoneAccountHandle(componentName, "Admin")
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "Admin").setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED).build()
telecomManager.registerPhoneAccount(phoneAccount)
val intent = Intent()
intent.component = ComponentName("com.android.server.telecom", "com.android.server.telecom.settings.EnableAccountPreferenceActivity")
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
}
#TargetApi(Build.VERSION_CODES.M)
fun startOutgoingCall() {
val extras = Bundle()
extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, true)
val manager = context.getSystemService(TELECOM_SERVICE) as TelecomManager
val phoneAccountHandle = PhoneAccountHandle(ComponentName(context.packageName, CallConnectionService::class.java!!.getName()), "estosConnectionServiceId")
val test = Bundle()
test.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
test.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_BIDIRECTIONAL)
test.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras)
try {
manager.placeCall(Uri.parse("tel:$number"), test)
} catch (e:SecurityException){
e.printStackTrace()
}
}
#TargetApi(Build.VERSION_CODES.M)
fun startIncomingCall(){
if (this.context.checkSelfPermission(Manifest.permission.MANAGE_OWN_CALLS) == PackageManager.PERMISSION_GRANTED) {
val extras = Bundle()
val uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null)
extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri)
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, true)
val isCallPermitted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
telecomManager.isIncomingCallPermitted(phoneAccountHandle)
} else {
true
}
Log.i("CallManager", "is incoming call permited = $isCallPermitted")
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
}
}
}
And my custom ConnectionService implementation:
class CallConnectionService : ConnectionService() {
override fun onCreateOutgoingConnection(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?): Connection {
Log.i("CallConnectionService", "onCreateOutgoingConnection")
val conn = CallConnection(applicationContext)
conn.setAddress(request!!.address, PRESENTATION_ALLOWED)
conn.setInitializing()
conn.videoProvider = MyVideoProvider()
conn.setActive()
return conn
}
override fun onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?) {
super.onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount, request)
Log.i("CallConnectionService", "create outgoing call failed")
}
override fun onCreateIncomingConnection(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?): Connection {
Log.i("CallConnectionService", "onCreateIncomingConnection")
val conn = CallConnection(applicationContext)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
conn.connectionProperties = Connection.PROPERTY_SELF_MANAGED
}
conn.setCallerDisplayName("test call", TelecomManager.PRESENTATION_ALLOWED)
conn.setAddress(request!!.address, PRESENTATION_ALLOWED)
conn.setInitializing()
conn.videoProvider = MyVideoProvider()
conn.setActive()
return conn
}
override fun onCreateIncomingConnectionFailed(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?) {
super.onCreateIncomingConnectionFailed(connectionManagerPhoneAccount, request)
Log.i("CallConnectionService", "create outgoing call failed ")
}
}
And my Connection implementation is like that:
class CallConnection(ctx:Context) : Connection() {
var ctx:Context = ctx
val TAG = "CallConnection"
override fun onShowIncomingCallUi() {
// super.onShowIncomingCallUi()
Log.i(TAG, "onShowIncomingCallUi")
val intent = Intent(Intent.ACTION_MAIN, null)
intent.flags = Intent.FLAG_ACTIVITY_NO_USER_ACTION or Intent.FLAG_ACTIVITY_NEW_TASK
intent.setClass(ctx, IncomingCallActivity::class.java!!)
val pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0)
val builder = Notification.Builder(ctx)
builder.setOngoing(true)
builder.setPriority(Notification.PRIORITY_HIGH)
// Set notification content intent to take user to fullscreen UI if user taps on the
// notification body.
builder.setContentIntent(pendingIntent)
// Set full screen intent to trigger display of the fullscreen UI when the notification
// manager deems it appropriate.
builder.setFullScreenIntent(pendingIntent, true)
// Setup notification content.
builder.setSmallIcon(R.mipmap.ic_launcher)
builder.setContentTitle("Your notification title")
builder.setContentText("Your notification content.")
// Use builder.addAction(..) to add buttons to answer or reject the call.
val notificationManager = ctx.getSystemService(
NotificationManager::class.java)
notificationManager.notify("Call Notification", 37, builder.build())
}
override fun onCallAudioStateChanged(state: CallAudioState?) {
Log.i(TAG, "onCallAudioStateChanged")
}
override fun onAnswer() {
Log.i(TAG, "onAnswer")
}
override fun onDisconnect() {
Log.i(TAG, "onDisconnect")
}
override fun onHold() {
Log.i(TAG, "onHold")
}
override fun onUnhold() {
Log.i(TAG, "onUnhold")
}
override fun onReject() {
Log.i(TAG, "onReject")
}
}
According to the document to show user incoming calcustomon UI - I should do some actions in onShowIncomingCallUi() method. But it just does not called by the system.
How can I fix it?
I was able to get it to work using a test app and Android Pie running on a Pixel 2 XL.
From my testing the important parts are to ensure:
That Connection.PROPERTY_SELF_MANAGED is set on the connection. Requires a minimum of API 26.
You have to register your phone account.
You have to set PhoneAccount.CAPABILITY_SELF_MANAGED in your capabilities when registering the phone account. That is the only capability that I set. Setting other capabilities caused it to throw an exception.
Finally, you need to ensure that you have this permission set in AndroidManifest.xml. android.permission.MANAGE_OWN_CALLS
So, I would check your manifest to ensure you have the permissions and also ensure the capabilities are set correctly. It looks like everything else was set correctly in your code above.
Hope that helps!