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