1 /* <lambda>null2 * Copyright 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.core.telecom.extensions 18 19 import android.content.Context 20 import android.net.Uri 21 import android.util.Log 22 import androidx.core.telecom.internal.CallIconStateListener 23 import androidx.core.telecom.internal.CapabilityExchangeListenerRemote 24 import androidx.core.telecom.util.ExperimentalAppActions 25 import kotlin.coroutines.resume 26 import kotlin.properties.Delegates 27 import kotlinx.coroutines.CoroutineScope 28 import kotlinx.coroutines.launch 29 import kotlinx.coroutines.suspendCancellableCoroutine 30 31 /** 32 * Remote implementation of the [CallIconExtensionRemote] interface. 33 * 34 * This class handles communication with the remote call icon extension, receiving updates to the 35 * call icon URI and providing them to the application through a flow. 36 * 37 * @param context The Android context. 38 * @param callScope The coroutine scope for launching flows. 39 */ 40 @ExperimentalAppActions 41 internal class CallIconExtensionRemoteImpl( 42 private val context: Context, 43 private val callScope: CoroutineScope, 44 private val onCallIconChanged: suspend (Uri) -> Unit 45 ) : CallIconExtensionRemote { 46 47 /** Indicates whether the remote call icon extension is supported. */ 48 override var isSupported by Delegates.notNull<Boolean>() 49 50 companion object { 51 /** The tag used for logging. */ 52 val TAG: String = CallIconExtensionRemoteImpl::class.java.simpleName 53 } 54 55 /** Returns an empty array of actions, as no actions are supported. */ 56 internal val actions 57 get() = IntArray(0) // No actions supported 58 59 /** 60 * Called when the capability exchange is complete. 61 * 62 * This method determines whether the remote extension is supported based on the negotiated 63 * capability and connects to the remote actions interface if supported. 64 * 65 * @param negotiatedCapability The negotiated capability. 66 * @param remote The remote capability exchange listener. 67 */ 68 internal suspend fun onExchangeComplete( 69 negotiatedCapability: Capability?, 70 remote: CapabilityExchangeListenerRemote? 71 ) { 72 if (negotiatedCapability == null || remote == null) { 73 Log.i(TAG, "onNegotiated: remote is not capable") 74 isSupported = false 75 return 76 } 77 78 isSupported = true 79 connectToRemote(negotiatedCapability, remote) 80 } 81 82 /** 83 * Connects to the remote call icon actions interface. 84 * 85 * This method establishes communication with the remote extension using the provided capability 86 * and listener. It sets up a listener to receive call icon URI updates and resumes the 87 * continuation with the remote actions interface. 88 * 89 * @param negotiatedCapability The negotiated capability. 90 * @param remote The remote capability exchange listener. 91 */ 92 private suspend fun connectToRemote( 93 negotiatedCapability: Capability, 94 remote: CapabilityExchangeListenerRemote 95 ): Unit = suspendCancellableCoroutine { continuation -> 96 val stateListener = 97 CallIconStateListener( 98 callIconUriUpdater = { 99 callScope.launch { 100 // Called when the remote extension updates the URI. 101 onCallIconChanged(it) 102 } 103 }, 104 finishSync = { callScope.launch { continuation.resume(Unit) } } 105 ) 106 remote.onCreateCallIconExtension( 107 negotiatedCapability.featureVersion, 108 negotiatedCapability.supportedActions, 109 context.packageName, 110 stateListener 111 ) 112 } 113 } 114