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