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