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.content.Intent.FLAG_GRANT_READ_URI_PERMISSION 21 import android.net.Uri 22 import android.os.Build 23 import android.util.Log 24 import androidx.annotation.RequiresApi 25 import androidx.core.telecom.internal.CallIconStateListenerRemote 26 import androidx.core.telecom.internal.CapabilityExchangeRepository 27 import androidx.core.telecom.util.ExperimentalAppActions 28 import kotlin.collections.mutableSetOf 29 import kotlin.coroutines.CoroutineContext 30 import kotlinx.coroutines.CoroutineScope 31 import kotlinx.coroutines.flow.* 32 import kotlinx.coroutines.flow.MutableStateFlow 33 import kotlinx.coroutines.flow.launchIn 34 import kotlinx.coroutines.flow.onEach 35 36 /** 37 * Implementation of the [CallIconExtension] interface for managing and sharing call icon URIs with 38 * remote listeners. 39 * 40 * This class handles granting URI permissions to remote packages, syncing the current icon URI with 41 * new listeners, and managing the flow of URI updates. 42 * 43 * @param mContext The Android context. 44 * @param mCoroutineContext The coroutine context to use for asynchronous operations. 45 * @param mInitialCallIcon The initial call icon URI. 46 */ 47 @RequiresApi(Build.VERSION_CODES.O) 48 @OptIn(ExperimentalAppActions::class) 49 internal class CallIconExtensionImpl( 50 val mContext: Context, 51 val mCoroutineContext: CoroutineContext, 52 val mInitialCallIcon: Uri 53 ) : CallIconExtension { 54 55 companion object { 56 /** The current version of the Call Icon Extension. */ 57 internal const val VERSION = 1 58 59 /** The tag used for logging. */ 60 val TAG: String = CallIconExtensionImpl::class.java.simpleName 61 } 62 63 /** A state flow holding the current call icon URI. Emits [Uri.EMPTY] initially. */ 64 internal val mUriFlow = MutableStateFlow<Uri>(Uri.EMPTY) 65 66 /** 67 * Called when a capability exchange is started. 68 * 69 * @param callbacks The [CapabilityExchangeRepository] to register callbacks. 70 * @return A [Capability] object representing the Call Icon Extension. 71 */ 72 internal fun onExchangeStarted(callbacks: CapabilityExchangeRepository): Capability { 73 callbacks.onCreateCallIconExtension = ::onCreateCallIconExtension 74 return Capability().apply { 75 featureId = Extensions.CALL_ICON 76 featureVersion = VERSION 77 supportedActions = IntArray(0) 78 } 79 } 80 81 /** 82 * Updates the call icon URI. 83 * 84 * This method updates the current icon URI and notifies remote listeners of the change. 85 * 86 * @param iconUri The new call icon URI. 87 */ 88 override suspend fun updateCallIconUri(iconUri: Uri) { 89 Log.d(TAG, "updateCallIconUri: updatedUri=$iconUri") 90 if (mUriFlow.value == iconUri) { 91 // some clients may keep the same URI but modify the bitmap, in this case, 92 // the remote listeners need to implement a content observer and will be updated 93 // via the content observer 94 mContext.contentResolver.notifyChange(iconUri, null) 95 } 96 mUriFlow.emit(iconUri) 97 } 98 99 /** 100 * Called when a new remote listener is created. 101 * 102 * This method adds the remote listener to the list, grants the listener permission to access 103 * the current icon URI, and starts a flow to propagate URI updates to the listener. 104 * 105 * @param coroutineScope The coroutine scope to launch the flow in. 106 * @param remoteActions The set of remote actions. 107 * @param remoteName The package name of the remote listener. 108 * @param binder The [CallIconStateListenerRemote] binder for communication. 109 */ 110 private fun onCreateCallIconExtension( 111 coroutineScope: CoroutineScope, 112 remoteActions: Set<Int>, 113 remoteName: String, 114 binder: CallIconStateListenerRemote 115 ) { 116 Log.d(TAG, "onCreateCallIconExtension: actions=$remoteActions") 117 val urisToRevoke = mutableSetOf<Uri>() 118 119 // Sync the new remote listener with the initial icon. 120 mContext.grantUriPermission(remoteName, mInitialCallIcon, FLAG_GRANT_READ_URI_PERMISSION) 121 binder.updateCallIconUri(uri = mInitialCallIcon) 122 123 mUriFlow 124 .filter { it != Uri.EMPTY } // Ignore initial empty URI 125 .onEach { newUri -> 126 mContext.grantUriPermission(remoteName, newUri, FLAG_GRANT_READ_URI_PERMISSION) 127 binder.updateCallIconUri(uri = newUri) 128 urisToRevoke.add(newUri) 129 } 130 .onCompletion { // When the flow completes (e.g., listener disconnects) 131 urisToRevoke.forEach { uriSent -> 132 mContext.revokeUriPermission( 133 remoteName, 134 uriSent, 135 FLAG_GRANT_READ_URI_PERMISSION 136 ) 137 } 138 } 139 .launchIn(coroutineScope) 140 binder.finishSync() 141 } 142 } 143