1 /*
2  * 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 
396 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