I'm making a note app that trigger notificaitions when you enter a geofence if I make more than 1 note with the same location when I enter it only on geofence triggers and only shows 1 notification.
Here is the code:
Broadcast Receiver
class GeofenceBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
//if (intent.action == ACTION_GEOFENCE_EVENT) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
val errorMessage = errorMessage(context, geofencingEvent.errorCode)
Log.e(TAG, errorMessage)
return
}
if (geofencingEvent.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
Log.v(TAG, "context.getString(R.string.geofence_entered)")
val fenceId = when {
geofencingEvent.triggeringGeofences.isNotEmpty() ->
geofencingEvent.triggeringGeofences
else -> {
Log.e(TAG, "No Geofence Trigger Found! Abort mission!")
return
}
}
/*val foundIndex = GeofencingConstants.LANDMARK_DATA.indexOfFirst {
it.id == fenceId
}*/
// Unknown Geofences aren't helpful to us
/*if ( -1 == foundIndex ) {
Log.e(TAG, "Unknown Geofence: Abort Mission")
return
}*/
for (geofence in fenceId){
sendGeofenceEnteredNotification(
context, geofence.requestId, intent.getStringExtra("note")!!
)
}
}
}
// }
}
-------------- Methods for adding geofencing ---------------------------
private fun createPendingIntent(context: Context, note: Note) : PendingIntent{
val intent = Intent(context, GeofenceBroadcastReceiver::class.java)
intent.action = "MainActivity.treasureHunt.action.ACTION_GEOFENCE_EVENT"
intent.putExtra("note", note.note)
return PendingIntent.getBroadcast(context, note.date.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun addGeofenceForNote(note: Note, context: Context) {
val geofencingClient = LocationServices.getGeofencingClient(context)
val geofence = Geofence.Builder()
.setRequestId(note.id)
.setCircularRegion(note.latitude, note.longitude, GeofencingConstants.GEOFENCE_RADIUS_IN_METERS)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.build()
// Build the geofence request
val geofencingRequest = GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofence(geofence)
.build()
geofencingClient.addGeofences(geofencingRequest, createPendingIntent(context, note))?.run {
addOnSuccessListener {
Log.e("Add Geofence", geofence.requestId)
}
addOnFailureListener {
if ((it.message != null)) {
Log.w("TAG", it.message!!)
}
}
}
}
I made 3 notes with the same location and the 2nd one was the one that trigger the broadcastreceiver.
P.S. even if the geofence are in totally different locations I still can only get 1 notification. As far as I can see all pending intents and notification get different IDs.
Ty in advance
the problem wasa the phone for some reason couldnt create all the notifications.
the code that fixed it was the .apply{} on the notificationManager
fun NotificationManager.handleNotification(context: Context, geofences: List<Geofence>){
val list = ArrayList<Notification>()
val set = mutableSetOf<Int>()
val notificationIdBase = (Date().time / 1000L % Int.MAX_VALUE).toInt()
apply {
for (i in geofences.indices){
val notificationId = notificationIdBase + i
val notification = createGroupNotification(context, notificationId, geofences[i].requestId)
list.add(notification)
notify(notificationId, notification)
set.add(notificationId)
Log.d("TAG", notificationId.toString())
}
Log.d("TAG", geofences.size.toString() + " " + list.size + " " + set.size)
}
}
Related
I am using this package with customisation home_widget and trying to build a home widget with number buttons like this image.
I am trying to achieve it by the following code
My WidgetProvider.kt
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) {
appWidgetIds.forEach { widgetId ->
val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
// Open App on Widget Click
val pendingIntent = HomeWidgetLaunchIntent.getActivity(context,
MainActivity::class.java)
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
val counter = widgetData.getString("_number", "")
Log.i("onMethodCall", "onMethodCall: $counter")
print("onMethodCall: $counter")
var counterText = "$counter"
setTextViewText(R.id.tv_counter, counterText)
val pendingIntent2 = HomeWidgetLaunchIntent.getActivity(context,
MainActivity::class.java)
setOnClickPendingIntent(R.id.bt_update, pendingIntent2)
val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("myAppWidget://onNumberBtnClick"))
setOnClickPendingIntent(R.id.bt_one, backgroundIntent)
}
appWidgetManager.updateAppWidget(widgetId, views)
}
}
I am passing intent as "onNumberBtnClick" and this is my broadcast function
fun getBroadcast(context: Context, uri: Uri? = null): PendingIntent {
val intent = Intent(context, HomeWidgetBackgroundReceiver::class.java)
intent.data = uri
intent.action = HOME_WIDGET_BACKGROUND_ACTION
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= 23) {
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
return PendingIntent.getBroadcast(context, 0, intent, flags)
}
in my "HomeWidgetPlugin" class I have set that value in shared preference.
"onNumberBtnClick" -> {
if (call.hasArgument("id") && call.hasArgument("data")) {
Log.i("onMethodCallHere", "onMethodCallHere: ")
print("onMethodCallHere")
val id = call.argument<String>("id")
val data = call.argument<Any>("data")
val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit()
if(data != null) {
when (data) {
is Boolean -> prefs.putBoolean(id, data)
is Float -> prefs.putFloat(id, data)
is String -> prefs.putString(id, data)
is Double -> prefs.putLong(id, java.lang.Double.doubleToRawLongBits(data))
is Int -> prefs.putInt(id, data)
else -> result.error("-10", "Invalid Type ${data!!::class.java.simpleName}. Supported types are Boolean, Float, String, Double, Long", IllegalArgumentException())
}
} else {
prefs.remove(id);
}
result.success(prefs.commit())
}else {
result.error("-1", "InvalidArguments saveWidgetData must be called with id and data", IllegalArgumentException())
}
}
In main.dart I handled callback with workmanager plugin as in the example of the home widget plugin
Future<void> backgroundCallback(Uri? uri) async {
if (uri?.host == 'onNumberBtnClick') {
await HomeWidget.getWidgetData<String>('_number', defaultValue: "1").then((value) {
print("num:$value");
});
await HomeWidget.saveWidgetData<String>('_number', "13");
await HomeWidget.updateWidget(name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider');
}
}
Can someone please help me with what I am missing or how can I achieve this?
I'm trying to implement geofencing as mentioned here: https://developer.android.com/training/location/geofencing
I get my BroadcastReceiver's onReceive called, but then a weird thing happens, geofencingEvent.geofenceTransition is always -1, instead of Geofence.GEOFENCE_TRANSITION_ENTER.
In any given time i have only 1 geofence. Tested on several devices and emulators. On real devices used Lockito app to simulate movement, on emulators used it's own control panel to change location.
CODE:
RECEIVER:
class GeofenceBroadcastReceiver : BroadcastReceiver(),KoinComponent {
val navInManager:NavInManager = get()
override fun onReceive(context: Context, intent: Intent) {
Timber.d("***** GeofenceBroadcastReceiver onReceive")
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
val errorMessage = GeofenceStatusCodes
.getStatusCodeString(geofencingEvent.errorCode)
Timber.e("***** error $errorMessage")
return
}
val geofenceTransition = geofencingEvent.geofenceTransition
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
val triggeringGeofences = geofencingEvent.triggeringGeofences
Timber.i("***** ENTER triggered id ${triggeringGeofences[0].requestId}")
} else if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
val triggeringGeofences = geofencingEvent.triggeringGeofences
Timber.i("***** EXIT triggered id ${triggeringGeofences[0].requestId}")
} else {
Timber.e("***** invalid_type $geofenceTransition")
}
}
}
CREATING THE GEOFENCE:
private val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(App.instance, GeofenceBroadcastReceiver::class.java)
PendingIntent.getBroadcast(App.instance, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
private fun getGeofencingRequest(geofence: Geofence): GeofencingRequest {
return GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofence(geofence)
}.build()
}
fun setGeofenceForPoint(stopPoint: StopPoint){
val sessionId = "DELIVERY"
Timber.d("***** before Geofence $sessionId")
val geofence =
Geofence.Builder()
.setRequestId(sessionId)
.setCircularRegion(
stopPoint.location.latitude,
stopPoint.location.longitude,
1000F
)
.setExpirationDuration(1800000)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.build()
if (ActivityCompat.checkSelfPermission(
GetPackageCourierApp.instance,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
geofencingClient?.addGeofences(getGeofencingRequest(geofence), geofencePendingIntent)?.run {
addOnSuccessListener {
Timber.d("***** Geofence(s) added")
}
addOnFailureListener {
Timber.e("***** Failed to add geofence(s)")
}
}
}
}
I'm trying to make an application for wearable in which I get various metrics from health services such as the heart rate for different sports. However I get the following error:: lateinit property healthServicesManager has not been initialized.
I have declared the lateinit as:
#Inject
lateinit var healthServicesManager: HealthServicesManagerTaekwondo
The complete code of the class:
#AndroidEntryPoint
class ExerciseServiceTaekwondo : LifecycleService() {
#Inject
lateinit var healthServicesManager: HealthServicesManagerTaekwondo
private val localBinder = LocalBinder()
private var isBound = false
private var isStarted = false
private var isForeground = false
private val _exerciseState = MutableStateFlow(ExerciseState.USER_ENDED)
val exerciseState: StateFlow<ExerciseState> = _exerciseState
private val _exerciseMetrics = MutableStateFlow(emptyMap<DataType, List<DataPoint>>())
val exerciseMetrics: StateFlow<Map<DataType, List<DataPoint>>> = _exerciseMetrics
private val _aggregateMetrics = MutableStateFlow(emptyMap<DataType, AggregateDataPoint>())
val aggregateMetrics: StateFlow<Map<DataType, AggregateDataPoint>> = _aggregateMetrics
private val _exerciseLaps = MutableStateFlow(0)
val exerciseLaps: StateFlow<Int> = _exerciseLaps
private val _exerciseDurationUpdate = MutableStateFlow(ActiveDurationUpdate())
val exerciseDurationUpdate: StateFlow<ActiveDurationUpdate> = _exerciseDurationUpdate
private val _locationAvailabilityState = MutableStateFlow(LocationAvailability.UNKNOWN)
val locationAvailabilityState: StateFlow<LocationAvailability> = _locationAvailabilityState
fun prepareExercise() {
lifecycleScope.launch {
healthServicesManager.prepareExercise()
}
}
fun startExercise() {
lifecycleScope.launch {
healthServicesManager.startExercise()
}
postOngoingActivityNotification()
}
fun pauseExercise() {
lifecycleScope.launch {
healthServicesManager.pauseExercise()
}
}
fun resumeExercise() {
lifecycleScope.launch {
healthServicesManager.resumeExercise()
}
}
/**
* End exercise in this service's coroutine context.
*/
fun endExercise() {
lifecycleScope.launch {
healthServicesManager.endExercise()
}
removeOngoingActivityNotification()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Log.d(TAG, "onStartCommand")
if (!isStarted) {
isStarted = true
if (!isBound) {
// We may have been restarted by the system. Manage our lifetime accordingly.
stopSelfIfNotRunning()
}
// Start collecting exercise information. We might stop shortly (see above), in which
// case launchWhenStarted takes care of canceling this coroutine.
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
healthServicesManager.exerciseUpdateFlow.collect {
when (it) {
is ExerciseMessage.ExerciseUpdateMessage ->
processExerciseUpdate(it.exerciseUpdate)
is ExerciseMessage.LapSummaryMessage ->
_exerciseLaps.value = it.lapSummary.lapCount
is ExerciseMessage.LocationAvailabilityMessage ->
_locationAvailabilityState.value = it.locationAvailability
}
}
}
}
}
}
// If our process is stopped, we might have an active exercise. We want the system to
// recreate our service so that we can present the ongoing notification in that case.
return Service.START_STICKY
}
private fun stopSelfIfNotRunning() {
lifecycleScope.launch {
// We may have been restarted by the system. Check for an ongoing exercise.
if (!healthServicesManager.isExerciseInProgress()) {
// Need to cancel [prepareExercise()] to prevent battery drain.
if (_exerciseState.value == ExerciseState.PREPARING) {
lifecycleScope.launch {
healthServicesManager.endExercise()
}
}
// We have nothing to do, so we can stop.
stopSelf()
}
}
}
private fun processExerciseUpdate(exerciseUpdate: ExerciseUpdate) {
val oldState = _exerciseState.value
if (!oldState.isEnded && exerciseUpdate.state.isEnded) {
// Our exercise ended. Gracefully handle this termination be doing the following:
// TODO Save partial workout state, show workout summary, and let the user know why the exercise was ended.
// Dismiss any ongoing activity notification.
removeOngoingActivityNotification()
// Custom flow for the possible states captured by the isEnded boolean
when (exerciseUpdate.state) {
ExerciseState.TERMINATED -> {
// TODO Send the user a notification (another app ended their workout)
Log.i(
TAG,
"Your exercise was terminated because another app started tracking an exercise"
)
}
ExerciseState.AUTO_ENDED -> {
// TODO Send the user a notification
Log.i(
TAG,
"Your exercise was auto ended because there were no registered listeners"
)
}
ExerciseState.AUTO_ENDED_PERMISSION_LOST -> {
// TODO Send the user a notification
Log.w(
TAG,
"Your exercise was auto ended because it lost the required permissions"
)
}
else -> {
}
}
} else if (oldState.isEnded && exerciseUpdate.state == ExerciseState.ACTIVE) {
// Reset laps.
_exerciseLaps.value = 0
}
_exerciseState.value = exerciseUpdate.state
_exerciseMetrics.value = exerciseUpdate.latestMetrics
_aggregateMetrics.value = exerciseUpdate.latestAggregateMetrics
_exerciseDurationUpdate.value =
ActiveDurationUpdate(exerciseUpdate.activeDuration, Instant.now())
}
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
handleBind()
return localBinder
}
override fun onRebind(intent: Intent?) {
super.onRebind(intent)
handleBind()
}
private fun handleBind() {
if (!isBound) {
isBound = true
// Start ourself. This will begin collecting exercise state if we aren't already.
startService(Intent(this, this::class.java))
}
}
override fun onUnbind(intent: Intent?): Boolean {
isBound = false
lifecycleScope.launch {
// Client can unbind because it went through a configuration change, in which case it
// will be recreated and bind again shortly. Wait a few seconds, and if still not bound,
// manage our lifetime accordingly.
delay(UNBIND_DELAY_MILLIS)
if (!isBound) {
stopSelfIfNotRunning()
}
}
// Allow clients to re-bind. We will be informed of this in onRebind().
return true
}
private fun removeOngoingActivityNotification() {
if (isForeground) {
Log.d(TAG, "Removing ongoing activity notification")
isForeground = false
stopForeground(true)
}
}
private fun postOngoingActivityNotification() {
if (!isForeground) {
isForeground = true
Log.d(TAG, "Posting ongoing activity notification")
createNotificationChannel()
startForeground(NOTIFICATION_ID, buildNotification())
}
}
private fun createNotificationChannel() {
val notificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL,
NOTIFICATION_CHANNEL_DISPLAY,
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(notificationChannel)
}
private fun buildNotification(): Notification {
// Make an intent that will take the user straight to the exercise UI.
val pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.exerciseFragment)
.createPendingIntent()
// Build the notification.
val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL)
.setContentTitle(NOTIFICATION_TITLE)
.setContentText(NOTIFICATION_TEXT)
.setSmallIcon(R.drawable.ic_run)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_WORKOUT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
// Ongoing Activity allows an ongoing Notification to appear on additional surfaces in the
// Wear OS user interface, so that users can stay more engaged with long running tasks.
val lastUpdate = exerciseDurationUpdate.value
val duration = lastUpdate.duration + Duration.between(lastUpdate.timestamp, Instant.now())
val startMillis = SystemClock.elapsedRealtime() - duration.toMillis()
val ongoingActivityStatus = Status.Builder()
.addTemplate(ONGOING_STATUS_TEMPLATE)
.addPart("duration", Status.StopwatchPart(startMillis))
.build()
val ongoingActivity =
OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder)
.setAnimatedIcon(R.drawable.ic_run)
.setStaticIcon(R.drawable.ic_run)
.setTouchIntent(pendingIntent)
.setStatus(ongoingActivityStatus)
.build()
ongoingActivity.apply(applicationContext)
return notificationBuilder.build()
}
/** Local clients will use this to access the service. */
inner class LocalBinder : Binder() {
fun getService() = this#ExerciseServiceTaekwondo
}
companion object {
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_CHANNEL = "com.example.exercise.ONGOING_EXERCISE"
private const val NOTIFICATION_CHANNEL_DISPLAY = "Ongoing Exercise"
private const val NOTIFICATION_TITLE = "Exercise Sample"
private const val NOTIFICATION_TEXT = "Ongoing Exercise"
private const val ONGOING_STATUS_TEMPLATE = "Ongoing Exercise #duration#"
private const val UNBIND_DELAY_MILLIS = 3_000L
fun bindService(context: Context, serviceConnection: ServiceConnection) {
val serviceIntent = Intent(context, ExerciseServiceTaekwondo::class.java)
context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE)
}
fun unbindService(context: Context, serviceConnection: ServiceConnection) {
context.unbindService(serviceConnection)
}
}
}
I am playing around with Wear OS on a Fossil Falster 3 (it is running API 28). Also please know that I am just starting with Kotlin, so the code below is not good to say the least.
I want to create a Geofence using broadcast receiver.
In my MainActivity.kt I have the following:
override fun onStart() {
super.onStart()
requestForegroundAndBackgroundLocationPermissions()
val locations = arrayOf(Location("tgh", 58.798233, 11.123959))
val geofences = locations.map {
Geofence.Builder()
.setRequestId(it.name)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setCircularRegion(it.latitude, it.longitude, 100F)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL)
.setLoiteringDelay(10)
.build()
}
val geofencingRequest = GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofences(geofences)
}.build()
val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(this, GeofenceReceiver::class.java)
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
val geofencingClient = LocationServices.getGeofencingClient(this)
geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent).run {
addOnFailureListener {
// display error
exception -> Log.d(TAG, "addOnFailureListener Exception: $exception")
}
addOnSuccessListener {
// move on
Log.d(TAG, "addOnSuccessListener, move on")
}
}
}
The GeofenceReceiver is in the same file:
class GeofenceReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
Log.d(TAG, "geofencingEvent.hasError()")
} else {
geofencingEvent.triggeringGeofences.forEach {
val geofence = it.requestId
Log.d(TAG, "location at: " + geofence)
}
}
}
}
The manifest has the following snippet:
<receiver android:name=".GeofenceReceiver"
android:enabled="true"
android:exported="true"/>
That is pretty much it. When I run the code, addOnFailureListener is triggered and the error message is printed and it is resulting in a 1000 exception.
Exception 1000 according to google documentation means GEOFENCE_NOT_AVAILABLE. I have enabled location on both the watch and the phone to its highest level on phone (Improve Location Accuracy is ON). On the watch I have set to use location from both watch and phone (highest level). Still I keep getting the 1000 error code.
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!