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