1 /*
<lambda>null2 * Copyright (C) 2021 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 com.android.systemui.statusbar.phone.ongoingcall
18
19 import android.app.ActivityManager
20 import android.app.IActivityManager
21 import android.app.IUidObserver
22 import android.app.Notification
23 import android.app.Notification.CallStyle.CALL_TYPE_ONGOING
24 import android.content.Intent
25 import android.util.Log
26 import android.view.View
27 import android.widget.Chronometer
28 import androidx.annotation.VisibleForTesting
29 import com.android.internal.jank.InteractionJankMonitor
30 import com.android.systemui.R
31 import com.android.systemui.animation.ActivityLaunchAnimator
32 import com.android.systemui.dagger.SysUISingleton
33 import com.android.systemui.dagger.qualifiers.Main
34 import com.android.systemui.plugins.ActivityStarter
35 import com.android.systemui.statusbar.FeatureFlags
36 import com.android.systemui.statusbar.notification.collection.NotificationEntry
37 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
38 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
39 import com.android.systemui.statusbar.policy.CallbackController
40 import com.android.systemui.util.time.SystemClock
41 import java.util.concurrent.Executor
42 import javax.inject.Inject
43
44 /**
45 * A controller to handle the ongoing call chip in the collapsed status bar.
46 */
47 @SysUISingleton
48 class OngoingCallController @Inject constructor(
49 private val notifCollection: CommonNotifCollection,
50 private val featureFlags: FeatureFlags,
51 private val systemClock: SystemClock,
52 private val activityStarter: ActivityStarter,
53 @Main private val mainExecutor: Executor,
54 private val iActivityManager: IActivityManager,
55 private val logger: OngoingCallLogger
56 ) : CallbackController<OngoingCallListener> {
57
58 /** Non-null if there's an active call notification. */
59 private var callNotificationInfo: CallNotificationInfo? = null
60 /** True if the application managing the call is visible to the user. */
61 private var isCallAppVisible: Boolean = true
62 private var chipView: View? = null
63 private var uidObserver: IUidObserver.Stub? = null
64
65 private val mListeners: MutableList<OngoingCallListener> = mutableListOf()
66
67 private val notifListener = object : NotifCollectionListener {
68 // Temporary workaround for b/178406514 for testing purposes.
69 //
70 // b/178406514 means that posting an incoming call notif then updating it to an ongoing call
71 // notif does not work (SysUI never receives the update). This workaround allows us to
72 // trigger the ongoing call chip when an ongoing call notif is *added* rather than
73 // *updated*, allowing us to test the chip.
74 //
75 // TODO(b/183229367): Remove this function override when b/178406514 is fixed.
76 override fun onEntryAdded(entry: NotificationEntry) {
77 onEntryUpdated(entry)
78 }
79
80 override fun onEntryUpdated(entry: NotificationEntry) {
81 // We have a new call notification or our existing call notification has been updated.
82 // TODO(b/183229367): This likely won't work if you take a call from one app then
83 // switch to a call from another app.
84 if (callNotificationInfo == null && isCallNotification(entry) ||
85 (entry.sbn.key == callNotificationInfo?.key)) {
86 val newOngoingCallInfo = CallNotificationInfo(
87 entry.sbn.key,
88 entry.sbn.notification.`when`,
89 entry.sbn.notification.contentIntent?.intent,
90 entry.sbn.uid,
91 entry.sbn.notification.extras.getInt(
92 Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING
93 )
94 if (newOngoingCallInfo == callNotificationInfo) {
95 return
96 }
97
98 callNotificationInfo = newOngoingCallInfo
99 if (newOngoingCallInfo.isOngoing) {
100 updateChip()
101 } else {
102 removeChip()
103 }
104 }
105 }
106
107 override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
108 if (entry.sbn.key == callNotificationInfo?.key) {
109 removeChip()
110 }
111 }
112 }
113
114 fun init() {
115 if (featureFlags.isOngoingCallStatusBarChipEnabled) {
116 notifCollection.addCollectionListener(notifListener)
117 }
118 }
119
120 /**
121 * Sets the chip view that will contain ongoing call information.
122 *
123 * Should only be called from [CollapsedStatusBarFragment].
124 */
125 fun setChipView(chipView: View) {
126 tearDownChipView()
127 this.chipView = chipView
128 if (hasOngoingCall()) {
129 updateChip()
130 }
131 }
132
133
134 /**
135 * Called when the chip's visibility may have changed.
136 *
137 * Should only be called from [CollapsedStatusBarFragment].
138 */
139 fun notifyChipVisibilityChanged(chipIsVisible: Boolean) {
140 logger.logChipVisibilityChanged(chipIsVisible)
141 }
142
143 /**
144 * Returns true if there's an active ongoing call that should be displayed in a status bar chip.
145 */
146 fun hasOngoingCall(): Boolean {
147 return callNotificationInfo?.isOngoing == true &&
148 // When the user is in the phone app, don't show the chip.
149 !isCallAppVisible
150 }
151
152 override fun addCallback(listener: OngoingCallListener) {
153 synchronized(mListeners) {
154 if (!mListeners.contains(listener)) {
155 mListeners.add(listener)
156 }
157 }
158 }
159
160 override fun removeCallback(listener: OngoingCallListener) {
161 synchronized(mListeners) {
162 mListeners.remove(listener)
163 }
164 }
165
166 private fun updateChip() {
167 val currentCallNotificationInfo = callNotificationInfo ?: return
168
169 val currentChipView = chipView
170 val timeView = currentChipView?.getTimeView()
171 val backgroundView =
172 currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background)
173
174 if (currentChipView != null && timeView != null && backgroundView != null) {
175 if (currentCallNotificationInfo.hasValidStartTime()) {
176 timeView.setShouldHideText(false)
177 timeView.base = currentCallNotificationInfo.callStartTime -
178 systemClock.currentTimeMillis() +
179 systemClock.elapsedRealtime()
180 timeView.start()
181 } else {
182 timeView.setShouldHideText(true)
183 timeView.stop()
184 }
185
186 currentCallNotificationInfo.intent?.let { intent ->
187 currentChipView.setOnClickListener {
188 logger.logChipClicked()
189 activityStarter.postStartActivityDismissingKeyguard(
190 intent,
191 0,
192 ActivityLaunchAnimator.Controller.fromView(
193 backgroundView,
194 InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP)
195 )
196 }
197 }
198
199 setUpUidObserver(currentCallNotificationInfo)
200
201 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
202 } else {
203 // If we failed to update the chip, don't store the call info. Then [hasOngoingCall]
204 // will return false and we fall back to typical notification handling.
205 callNotificationInfo = null
206
207 if (DEBUG) {
208 Log.w(TAG, "Ongoing call chip view could not be found; " +
209 "Not displaying chip in status bar")
210 }
211 }
212 }
213
214 /**
215 * Sets up an [IUidObserver] to monitor the status of the application managing the ongoing call.
216 */
217 private fun setUpUidObserver(currentCallNotificationInfo: CallNotificationInfo) {
218 isCallAppVisible = isProcessVisibleToUser(
219 iActivityManager.getUidProcessState(currentCallNotificationInfo.uid, null))
220
221 if (uidObserver != null) {
222 iActivityManager.unregisterUidObserver(uidObserver)
223 }
224
225 uidObserver = object : IUidObserver.Stub() {
226 override fun onUidStateChanged(
227 uid: Int, procState: Int, procStateSeq: Long, capability: Int) {
228 if (uid == currentCallNotificationInfo.uid) {
229 val oldIsCallAppVisible = isCallAppVisible
230 isCallAppVisible = isProcessVisibleToUser(procState)
231 if (oldIsCallAppVisible != isCallAppVisible) {
232 // Animations may be run as a result of the call's state change, so ensure
233 // the listener is notified on the main thread.
234 mainExecutor.execute {
235 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
236 }
237 }
238 }
239 }
240
241 override fun onUidGone(uid: Int, disabled: Boolean) {}
242 override fun onUidActive(uid: Int) {}
243 override fun onUidIdle(uid: Int, disabled: Boolean) {}
244 override fun onUidCachedChanged(uid: Int, cached: Boolean) {}
245 }
246
247 iActivityManager.registerUidObserver(
248 uidObserver,
249 ActivityManager.UID_OBSERVER_PROCSTATE,
250 ActivityManager.PROCESS_STATE_UNKNOWN,
251 null
252 )
253 }
254
255 /** Returns true if the given [procState] represents a process that's visible to the user. */
256 private fun isProcessVisibleToUser(procState: Int): Boolean {
257 return procState <= ActivityManager.PROCESS_STATE_TOP
258 }
259
260 private fun removeChip() {
261 callNotificationInfo = null
262 tearDownChipView()
263 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
264 if (uidObserver != null) {
265 iActivityManager.unregisterUidObserver(uidObserver)
266 }
267 }
268
269 /** Tear down anything related to the chip view to prevent leaks. */
270 @VisibleForTesting
271 fun tearDownChipView() = chipView?.getTimeView()?.stop()
272
273 private fun View.getTimeView(): OngoingCallChronometer? {
274 return this.findViewById(R.id.ongoing_call_chip_time)
275 }
276
277 private data class CallNotificationInfo(
278 val key: String,
279 val callStartTime: Long,
280 val intent: Intent?,
281 val uid: Int,
282 /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
283 val isOngoing: Boolean
284 ) {
285 /**
286 * Returns true if the notification information has a valid call start time.
287 * See b/192379214.
288 */
289 fun hasValidStartTime(): Boolean = callStartTime > 0
290 }
291 }
292
isCallNotificationnull293 private fun isCallNotification(entry: NotificationEntry): Boolean {
294 return entry.sbn.notification.isStyle(Notification.CallStyle::class.java)
295 }
296
297 private const val TAG = "OngoingCallController"
298 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
299