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.Notification
22 import android.app.Notification.CallStyle.CALL_TYPE_ONGOING
23 import android.app.PendingIntent
24 import android.app.UidObserver
25 import android.content.Context
26 import android.util.Log
27 import android.view.View
28 import androidx.annotation.VisibleForTesting
29 import com.android.internal.jank.InteractionJankMonitor
30 import com.android.systemui.Dumpable
31 import com.android.systemui.R
32 import com.android.systemui.animation.ActivityLaunchAnimator
33 import com.android.systemui.dagger.SysUISingleton
34 import com.android.systemui.dagger.qualifiers.Main
35 import com.android.systemui.dump.DumpManager
36 import com.android.systemui.plugins.ActivityStarter
37 import com.android.systemui.plugins.statusbar.StatusBarStateController
38 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
39 import com.android.systemui.statusbar.notification.collection.NotificationEntry
40 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
41 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
42 import com.android.systemui.statusbar.policy.CallbackController
43 import com.android.systemui.statusbar.window.StatusBarWindowController
44 import com.android.systemui.util.time.SystemClock
45 import java.io.PrintWriter
46 import java.util.Optional
47 import java.util.concurrent.Executor
48 import javax.inject.Inject
49
50 /**
51 * A controller to handle the ongoing call chip in the collapsed status bar.
52 */
53 @SysUISingleton
54 class OngoingCallController @Inject constructor(
55 private val context: Context,
56 private val notifCollection: CommonNotifCollection,
57 private val ongoingCallFlags: OngoingCallFlags,
58 private val systemClock: SystemClock,
59 private val activityStarter: ActivityStarter,
60 @Main private val mainExecutor: Executor,
61 private val iActivityManager: IActivityManager,
62 private val logger: OngoingCallLogger,
63 private val dumpManager: DumpManager,
64 private val statusBarWindowController: Optional<StatusBarWindowController>,
65 private val swipeStatusBarAwayGestureHandler: Optional<SwipeStatusBarAwayGestureHandler>,
66 private val statusBarStateController: StatusBarStateController
67 ) : CallbackController<OngoingCallListener>, Dumpable {
68 private var isFullscreen: Boolean = false
69 /** Non-null if there's an active call notification. */
70 private var callNotificationInfo: CallNotificationInfo? = null
71 private var chipView: View? = null
72
73 private val mListeners: MutableList<OngoingCallListener> = mutableListOf()
74 private val uidObserver = CallAppUidObserver()
75 private val notifListener = object : NotifCollectionListener {
76 // Temporary workaround for b/178406514 for testing purposes.
77 //
78 // b/178406514 means that posting an incoming call notif then updating it to an ongoing call
79 // notif does not work (SysUI never receives the update). This workaround allows us to
80 // trigger the ongoing call chip when an ongoing call notif is *added* rather than
81 // *updated*, allowing us to test the chip.
82 //
83 // TODO(b/183229367): Remove this function override when b/178406514 is fixed.
84 override fun onEntryAdded(entry: NotificationEntry) {
85 onEntryUpdated(entry, true)
86 }
87
88 override fun onEntryUpdated(entry: NotificationEntry) {
89 // We have a new call notification or our existing call notification has been updated.
90 // TODO(b/183229367): This likely won't work if you take a call from one app then
91 // switch to a call from another app.
92 if (callNotificationInfo == null && isCallNotification(entry) ||
93 (entry.sbn.key == callNotificationInfo?.key)) {
94 val newOngoingCallInfo = CallNotificationInfo(
95 entry.sbn.key,
96 entry.sbn.notification.`when`,
97 entry.sbn.notification.contentIntent,
98 entry.sbn.uid,
99 entry.sbn.notification.extras.getInt(
100 Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING,
101 statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false
102 )
103 if (newOngoingCallInfo == callNotificationInfo) {
104 return
105 }
106
107 callNotificationInfo = newOngoingCallInfo
108 if (newOngoingCallInfo.isOngoing) {
109 updateChip()
110 } else {
111 removeChip()
112 }
113 }
114 }
115
116 override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
117 if (entry.sbn.key == callNotificationInfo?.key) {
118 removeChip()
119 }
120 }
121 }
122
123 fun init() {
124 dumpManager.registerDumpable(this)
125 if (ongoingCallFlags.isStatusBarChipEnabled()) {
126 notifCollection.addCollectionListener(notifListener)
127 statusBarStateController.addCallback(statusBarStateListener)
128 }
129 }
130
131 /**
132 * Sets the chip view that will contain ongoing call information.
133 *
134 * Should only be called from [CollapsedStatusBarFragment].
135 */
136 fun setChipView(chipView: View) {
137 tearDownChipView()
138 this.chipView = chipView
139 val backgroundView: OngoingCallBackgroundContainer? =
140 chipView.findViewById(R.id.ongoing_call_chip_background)
141 backgroundView?.maxHeightFetcher = { statusBarWindowController.get().statusBarHeight }
142 if (hasOngoingCall()) {
143 updateChip()
144 }
145 }
146
147 /**
148 * Called when the chip's visibility may have changed.
149 *
150 * Should only be called from [CollapsedStatusBarFragment].
151 */
152 fun notifyChipVisibilityChanged(chipIsVisible: Boolean) {
153 logger.logChipVisibilityChanged(chipIsVisible)
154 }
155
156 /**
157 * Returns true if there's an active ongoing call that should be displayed in a status bar chip.
158 */
159 fun hasOngoingCall(): Boolean {
160 return callNotificationInfo?.isOngoing == true &&
161 // When the user is in the phone app, don't show the chip.
162 !uidObserver.isCallAppVisible
163 }
164
165 override fun addCallback(listener: OngoingCallListener) {
166 synchronized(mListeners) {
167 if (!mListeners.contains(listener)) {
168 mListeners.add(listener)
169 }
170 }
171 }
172
173 override fun removeCallback(listener: OngoingCallListener) {
174 synchronized(mListeners) {
175 mListeners.remove(listener)
176 }
177 }
178
179 private fun updateChip() {
180 val currentCallNotificationInfo = callNotificationInfo ?: return
181
182 val currentChipView = chipView
183 val timeView = currentChipView?.getTimeView()
184
185 if (currentChipView != null && timeView != null) {
186 if (currentCallNotificationInfo.hasValidStartTime()) {
187 timeView.setShouldHideText(false)
188 timeView.base = currentCallNotificationInfo.callStartTime -
189 systemClock.currentTimeMillis() +
190 systemClock.elapsedRealtime()
191 timeView.start()
192 } else {
193 timeView.setShouldHideText(true)
194 timeView.stop()
195 }
196 updateChipClickListener()
197
198 uidObserver.registerWithUid(currentCallNotificationInfo.uid)
199 if (!currentCallNotificationInfo.statusBarSwipedAway) {
200 statusBarWindowController.ifPresent {
201 it.setOngoingProcessRequiresStatusBarVisible(true)
202 }
203 }
204 updateGestureListening()
205 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
206 } else {
207 // If we failed to update the chip, don't store the call info. Then [hasOngoingCall]
208 // will return false and we fall back to typical notification handling.
209 callNotificationInfo = null
210
211 if (DEBUG) {
212 Log.w(TAG, "Ongoing call chip view could not be found; " +
213 "Not displaying chip in status bar")
214 }
215 }
216 }
217
218 private fun updateChipClickListener() {
219 if (callNotificationInfo == null) { return }
220 if (isFullscreen && !ongoingCallFlags.isInImmersiveChipTapEnabled()) {
221 chipView?.setOnClickListener(null)
222 } else {
223 val currentChipView = chipView
224 val backgroundView =
225 currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background)
226 val intent = callNotificationInfo?.intent
227 if (currentChipView != null && backgroundView != null && intent != null) {
228 currentChipView.setOnClickListener {
229 logger.logChipClicked()
230 activityStarter.postStartActivityDismissingKeyguard(
231 intent,
232 ActivityLaunchAnimator.Controller.fromView(
233 backgroundView,
234 InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP)
235 )
236 }
237 }
238 }
239 }
240
241 /** Returns true if the given [procState] represents a process that's visible to the user. */
242 private fun isProcessVisibleToUser(procState: Int): Boolean {
243 return procState <= ActivityManager.PROCESS_STATE_TOP
244 }
245
246 private fun updateGestureListening() {
247 if (callNotificationInfo == null ||
248 callNotificationInfo?.statusBarSwipedAway == true ||
249 !isFullscreen) {
250 swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) }
251 } else {
252 swipeStatusBarAwayGestureHandler.ifPresent {
253 it.addOnGestureDetectedCallback(TAG) { _ -> onSwipeAwayGestureDetected() }
254 }
255 }
256 }
257
258 private fun removeChip() {
259 callNotificationInfo = null
260 tearDownChipView()
261 statusBarWindowController.ifPresent { it.setOngoingProcessRequiresStatusBarVisible(false) }
262 swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) }
263 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
264 uidObserver.unregister()
265 }
266
267 /** Tear down anything related to the chip view to prevent leaks. */
268 @VisibleForTesting
269 fun tearDownChipView() = chipView?.getTimeView()?.stop()
270
271 private fun View.getTimeView(): OngoingCallChronometer? {
272 return this.findViewById(R.id.ongoing_call_chip_time)
273 }
274
275 /**
276 * If there's an active ongoing call, then we will force the status bar to always show, even if
277 * the user is in immersive mode. However, we also want to give users the ability to swipe away
278 * the status bar if they need to access the area under the status bar.
279 *
280 * This method updates the status bar window appropriately when the swipe away gesture is
281 * detected.
282 */
283 private fun onSwipeAwayGestureDetected() {
284 if (DEBUG) { Log.d(TAG, "Swipe away gesture detected") }
285 callNotificationInfo = callNotificationInfo?.copy(statusBarSwipedAway = true)
286 statusBarWindowController.ifPresent {
287 it.setOngoingProcessRequiresStatusBarVisible(false)
288 }
289 swipeStatusBarAwayGestureHandler.ifPresent {
290 it.removeOnGestureDetectedCallback(TAG)
291 }
292 }
293
294 private val statusBarStateListener = object : StatusBarStateController.StateListener {
295 override fun onFullscreenStateChanged(isFullscreen: Boolean) {
296 this@OngoingCallController.isFullscreen = isFullscreen
297 updateChipClickListener()
298 updateGestureListening()
299 }
300 }
301
302 private data class CallNotificationInfo(
303 val key: String,
304 val callStartTime: Long,
305 val intent: PendingIntent?,
306 val uid: Int,
307 /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
308 val isOngoing: Boolean,
309 /** True if the user has swiped away the status bar while in this phone call. */
310 val statusBarSwipedAway: Boolean
311 ) {
312 /**
313 * Returns true if the notification information has a valid call start time.
314 * See b/192379214.
315 */
316 fun hasValidStartTime(): Boolean = callStartTime > 0
317 }
318
319 override fun dump(pw: PrintWriter, args: Array<out String>) {
320 pw.println("Active call notification: $callNotificationInfo")
321 pw.println("Call app visible: ${uidObserver.isCallAppVisible}")
322 }
323
324 /** Our implementation of a [IUidObserver]. */
325 inner class CallAppUidObserver : UidObserver() {
326 /** True if the application managing the call is visible to the user. */
327 var isCallAppVisible: Boolean = false
328 private set
329
330 /** The UID of the application managing the call. Null if there is no active call. */
331 private var callAppUid: Int? = null
332
333 /**
334 * True if this observer is currently registered with the activity manager and false
335 * otherwise.
336 */
337 private var isRegistered = false
338
339 /** Register this observer with the activity manager and the given [uid]. */
340 fun registerWithUid(uid: Int) {
341 if (callAppUid == uid) {
342 return
343 }
344 callAppUid = uid
345
346 try {
347 isCallAppVisible = isProcessVisibleToUser(
348 iActivityManager.getUidProcessState(uid, context.opPackageName)
349 )
350 if (isRegistered) {
351 return
352 }
353 iActivityManager.registerUidObserver(
354 uidObserver,
355 ActivityManager.UID_OBSERVER_PROCSTATE,
356 ActivityManager.PROCESS_STATE_UNKNOWN,
357 context.opPackageName
358 )
359 isRegistered = true
360 } catch (se: SecurityException) {
361 Log.e(TAG, "Security exception when trying to set up uid observer: $se")
362 }
363 }
364
365 /** Unregister this observer with the activity manager. */
366 fun unregister() {
367 callAppUid = null
368 isRegistered = false
369 iActivityManager.unregisterUidObserver(uidObserver)
370 }
371
372 override fun onUidStateChanged(
373 uid: Int,
374 procState: Int,
375 procStateSeq: Long,
376 capability: Int
377 ) {
378 val currentCallAppUid = callAppUid ?: return
379 if (uid != currentCallAppUid) {
380 return
381 }
382
383 val oldIsCallAppVisible = isCallAppVisible
384 isCallAppVisible = isProcessVisibleToUser(procState)
385 if (oldIsCallAppVisible != isCallAppVisible) {
386 // Animations may be run as a result of the call's state change, so ensure
387 // the listener is notified on the main thread.
388 mainExecutor.execute {
389 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
390 }
391 }
392 }
393 }
394 }
395
isCallNotificationnull396 private fun isCallNotification(entry: NotificationEntry): Boolean {
397 return entry.sbn.notification.isStyle(Notification.CallStyle::class.java)
398 }
399
400 private const val TAG = "OngoingCallController"
401 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
402