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.app.PendingIntent
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 if (hasOngoingCall()) {
140 updateChip()
141 }
142 }
143
144 /**
145 * Called when the chip's visibility may have changed.
146 *
147 * Should only be called from [CollapsedStatusBarFragment].
148 */
149 fun notifyChipVisibilityChanged(chipIsVisible: Boolean) {
150 logger.logChipVisibilityChanged(chipIsVisible)
151 }
152
153 /**
154 * Returns true if there's an active ongoing call that should be displayed in a status bar chip.
155 */
156 fun hasOngoingCall(): Boolean {
157 return callNotificationInfo?.isOngoing == true &&
158 // When the user is in the phone app, don't show the chip.
159 !uidObserver.isCallAppVisible
160 }
161
162 override fun addCallback(listener: OngoingCallListener) {
163 synchronized(mListeners) {
164 if (!mListeners.contains(listener)) {
165 mListeners.add(listener)
166 }
167 }
168 }
169
170 override fun removeCallback(listener: OngoingCallListener) {
171 synchronized(mListeners) {
172 mListeners.remove(listener)
173 }
174 }
175
176 private fun updateChip() {
177 val currentCallNotificationInfo = callNotificationInfo ?: return
178
179 val currentChipView = chipView
180 val timeView = currentChipView?.getTimeView()
181
182 if (currentChipView != null && timeView != null) {
183 if (currentCallNotificationInfo.hasValidStartTime()) {
184 timeView.setShouldHideText(false)
185 timeView.base = currentCallNotificationInfo.callStartTime -
186 systemClock.currentTimeMillis() +
187 systemClock.elapsedRealtime()
188 timeView.start()
189 } else {
190 timeView.setShouldHideText(true)
191 timeView.stop()
192 }
193 updateChipClickListener()
194
195 uidObserver.registerWithUid(currentCallNotificationInfo.uid)
196 if (!currentCallNotificationInfo.statusBarSwipedAway) {
197 statusBarWindowController.ifPresent {
198 it.setOngoingProcessRequiresStatusBarVisible(true)
199 }
200 }
201 updateGestureListening()
202 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
203 } else {
204 // If we failed to update the chip, don't store the call info. Then [hasOngoingCall]
205 // will return false and we fall back to typical notification handling.
206 callNotificationInfo = null
207
208 if (DEBUG) {
209 Log.w(TAG, "Ongoing call chip view could not be found; " +
210 "Not displaying chip in status bar")
211 }
212 }
213 }
214
215 private fun updateChipClickListener() {
216 if (callNotificationInfo == null) { return }
217 if (isFullscreen && !ongoingCallFlags.isInImmersiveChipTapEnabled()) {
218 chipView?.setOnClickListener(null)
219 } else {
220 val currentChipView = chipView
221 val backgroundView =
222 currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background)
223 val intent = callNotificationInfo?.intent
224 if (currentChipView != null && backgroundView != null && intent != null) {
225 currentChipView.setOnClickListener {
226 logger.logChipClicked()
227 activityStarter.postStartActivityDismissingKeyguard(
228 intent,
229 ActivityLaunchAnimator.Controller.fromView(
230 backgroundView,
231 InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP)
232 )
233 }
234 }
235 }
236 }
237
238 /** Returns true if the given [procState] represents a process that's visible to the user. */
239 private fun isProcessVisibleToUser(procState: Int): Boolean {
240 return procState <= ActivityManager.PROCESS_STATE_TOP
241 }
242
243 private fun updateGestureListening() {
244 if (callNotificationInfo == null ||
245 callNotificationInfo?.statusBarSwipedAway == true ||
246 !isFullscreen) {
247 swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) }
248 } else {
249 swipeStatusBarAwayGestureHandler.ifPresent {
250 it.addOnGestureDetectedCallback(TAG) { _ -> onSwipeAwayGestureDetected() }
251 }
252 }
253 }
254
255 private fun removeChip() {
256 callNotificationInfo = null
257 tearDownChipView()
258 statusBarWindowController.ifPresent { it.setOngoingProcessRequiresStatusBarVisible(false) }
259 swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) }
260 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
261 uidObserver.unregister()
262 }
263
264 /** Tear down anything related to the chip view to prevent leaks. */
265 @VisibleForTesting
266 fun tearDownChipView() = chipView?.getTimeView()?.stop()
267
268 private fun View.getTimeView(): OngoingCallChronometer? {
269 return this.findViewById(R.id.ongoing_call_chip_time)
270 }
271
272 /**
273 * If there's an active ongoing call, then we will force the status bar to always show, even if
274 * the user is in immersive mode. However, we also want to give users the ability to swipe away
275 * the status bar if they need to access the area under the status bar.
276 *
277 * This method updates the status bar window appropriately when the swipe away gesture is
278 * detected.
279 */
280 private fun onSwipeAwayGestureDetected() {
281 if (DEBUG) { Log.d(TAG, "Swipe away gesture detected") }
282 callNotificationInfo = callNotificationInfo?.copy(statusBarSwipedAway = true)
283 statusBarWindowController.ifPresent {
284 it.setOngoingProcessRequiresStatusBarVisible(false)
285 }
286 swipeStatusBarAwayGestureHandler.ifPresent {
287 it.removeOnGestureDetectedCallback(TAG)
288 }
289 }
290
291 private val statusBarStateListener = object : StatusBarStateController.StateListener {
292 override fun onFullscreenStateChanged(isFullscreen: Boolean) {
293 this@OngoingCallController.isFullscreen = isFullscreen
294 updateChipClickListener()
295 updateGestureListening()
296 }
297 }
298
299 private data class CallNotificationInfo(
300 val key: String,
301 val callStartTime: Long,
302 val intent: PendingIntent?,
303 val uid: Int,
304 /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
305 val isOngoing: Boolean,
306 /** True if the user has swiped away the status bar while in this phone call. */
307 val statusBarSwipedAway: Boolean
308 ) {
309 /**
310 * Returns true if the notification information has a valid call start time.
311 * See b/192379214.
312 */
313 fun hasValidStartTime(): Boolean = callStartTime > 0
314 }
315
316 override fun dump(pw: PrintWriter, args: Array<out String>) {
317 pw.println("Active call notification: $callNotificationInfo")
318 pw.println("Call app visible: ${uidObserver.isCallAppVisible}")
319 }
320
321 /** Our implementation of a [IUidObserver]. */
322 inner class CallAppUidObserver : IUidObserver.Stub() {
323 /** True if the application managing the call is visible to the user. */
324 var isCallAppVisible: Boolean = false
325 private set
326
327 /** The UID of the application managing the call. Null if there is no active call. */
328 private var callAppUid: Int? = null
329
330 /**
331 * True if this observer is currently registered with the activity manager and false
332 * otherwise.
333 */
334 private var isRegistered = false
335
336 /** Register this observer with the activity manager and the given [uid]. */
337 fun registerWithUid(uid: Int) {
338 if (callAppUid == uid) {
339 return
340 }
341 callAppUid = uid
342
343 try {
344 isCallAppVisible = isProcessVisibleToUser(
345 iActivityManager.getUidProcessState(uid, context.opPackageName)
346 )
347 if (isRegistered) {
348 return
349 }
350 iActivityManager.registerUidObserver(
351 uidObserver,
352 ActivityManager.UID_OBSERVER_PROCSTATE,
353 ActivityManager.PROCESS_STATE_UNKNOWN,
354 context.opPackageName
355 )
356 isRegistered = true
357 } catch (se: SecurityException) {
358 Log.e(TAG, "Security exception when trying to set up uid observer: $se")
359 }
360 }
361
362 /** Unregister this observer with the activity manager. */
363 fun unregister() {
364 callAppUid = null
365 isRegistered = false
366 iActivityManager.unregisterUidObserver(uidObserver)
367 }
368
369 override fun onUidStateChanged(
370 uid: Int,
371 procState: Int,
372 procStateSeq: Long,
373 capability: Int
374 ) {
375 val currentCallAppUid = callAppUid ?: return
376 if (uid != currentCallAppUid) {
377 return
378 }
379
380 val oldIsCallAppVisible = isCallAppVisible
381 isCallAppVisible = isProcessVisibleToUser(procState)
382 if (oldIsCallAppVisible != isCallAppVisible) {
383 // Animations may be run as a result of the call's state change, so ensure
384 // the listener is notified on the main thread.
385 mainExecutor.execute {
386 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
387 }
388 }
389 }
390
391 override fun onUidGone(uid: Int, disabled: Boolean) {}
392 override fun onUidActive(uid: Int) {}
393 override fun onUidIdle(uid: Int, disabled: Boolean) {}
394 override fun onUidCachedChanged(uid: Int, cached: Boolean) {}
395 override fun onUidProcAdjChanged(uid: Int) {}
396 }
397 }
398
isCallNotificationnull399 private fun isCallNotification(entry: NotificationEntry): Boolean {
400 return entry.sbn.notification.isStyle(Notification.CallStyle::class.java)
401 }
402
403 private const val TAG = "OngoingCallController"
404 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
405