• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.PendingIntent
22 import android.app.UidObserver
23 import android.content.Context
24 import android.view.View
25 import androidx.annotation.VisibleForTesting
26 import com.android.app.tracing.coroutines.launchTraced as launch
27 import com.android.internal.logging.InstanceId
28 import com.android.systemui.CoreStartable
29 import com.android.systemui.dagger.SysUISingleton
30 import com.android.systemui.dagger.qualifiers.Application
31 import com.android.systemui.dagger.qualifiers.Main
32 import com.android.systemui.dump.DumpManager
33 import com.android.systemui.log.LogBuffer
34 import com.android.systemui.log.core.LogLevel
35 import com.android.systemui.res.R
36 import com.android.systemui.statusbar.StatusBarIconView
37 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
38 import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
39 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
40 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
41 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
42 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
43 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
44 import com.android.systemui.statusbar.notification.shared.CallType
45 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
46 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
47 import com.android.systemui.statusbar.policy.CallbackController
48 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
49 import java.io.PrintWriter
50 import java.util.concurrent.Executor
51 import javax.inject.Inject
52 import kotlinx.coroutines.CoroutineScope
53 
54 /**
55  * A controller to handle the ongoing call chip in the collapsed status bar.
56  *
57  * @deprecated Use [OngoingCallInteractor] instead, which follows recommended architecture patterns
58  */
59 @Deprecated("Use OngoingCallInteractor instead")
60 @SysUISingleton
61 class OngoingCallController
62 @Inject
63 constructor(
64     @Application private val scope: CoroutineScope,
65     private val context: Context,
66     private val ongoingCallRepository: OngoingCallRepository,
67     private val activeNotificationsInteractor: ActiveNotificationsInteractor,
68     @Main private val mainExecutor: Executor,
69     private val iActivityManager: IActivityManager,
70     private val dumpManager: DumpManager,
71     private val statusBarWindowControllerStore: StatusBarWindowControllerStore,
72     private val swipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler,
73     private val statusBarModeRepository: StatusBarModeRepositoryStore,
74     @OngoingCallLog private val logger: LogBuffer,
75 ) : CallbackController<OngoingCallListener>, CoreStartable {
76     private var isFullscreen: Boolean = false
77     /** Non-null if there's an active call notification. */
78     private var callNotificationInfo: CallNotificationInfo? = null
79     private var chipView: View? = null
80 
81     private val mListeners: MutableList<OngoingCallListener> = mutableListOf()
82     private val uidObserver = CallAppUidObserver()
83 
84     override fun start() {
85         if (StatusBarChipsModernization.isEnabled) return
86 
87         dumpManager.registerDumpable(this)
88 
89         scope.launch {
90             // Listening to [ActiveNotificationsInteractor] instead of using
91             // [NotifCollectionListener#onEntryUpdated] is better for two reasons:
92             // 1. ActiveNotificationsInteractor automatically filters the notification list to
93             // just notifications for the current user, which ensures we don't show a call chip
94             // for User 1's call while User 2 is active (see b/328584859).
95             // 2. ActiveNotificationsInteractor only emits notifications that are currently
96             // present in the shade, which means we know we've already inflated the icon that we
97             // might use for the call chip (see b/354930838).
98             activeNotificationsInteractor.ongoingCallNotification.collect {
99                 updateInfoFromNotifModel(it)
100             }
101         }
102 
103         scope.launch {
104             statusBarModeRepository.defaultDisplay.isInFullscreenMode.collect {
105                 isFullscreen = it
106                 updateGestureListening()
107             }
108         }
109     }
110 
111     /**
112      * Sets the chip view that will contain ongoing call information.
113      *
114      * Should only be called from
115      * [com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment].
116      */
117     fun setChipView(chipView: View) {
118         StatusBarChipsModernization.assertInLegacyMode()
119 
120         tearDownChipView()
121         this.chipView = chipView
122         val backgroundView: ChipBackgroundContainer? =
123             chipView.findViewById(R.id.ongoing_activity_chip_background)
124         backgroundView?.maxHeightFetcher = {
125             statusBarWindowControllerStore.defaultDisplay.statusBarHeight
126         }
127         if (hasOngoingCall()) {
128             updateChip()
129         }
130     }
131 
132     /**
133      * Returns true if there's an active ongoing call that should be displayed in a status bar chip.
134      */
135     fun hasOngoingCall(): Boolean {
136         StatusBarChipsModernization.assertInLegacyMode()
137 
138         return callNotificationInfo?.isOngoing == true &&
139             // When the user is in the phone app, don't show the chip.
140             !uidObserver.isCallAppVisible
141     }
142 
143     /** Creates the right [OngoingCallModel] based on the call state. */
144     private fun getOngoingCallModel(): OngoingCallModel {
145         StatusBarChipsModernization.assertInLegacyMode()
146 
147         if (hasOngoingCall()) {
148             val currentInfo =
149                 callNotificationInfo
150                     // This shouldn't happen, but protect against it in case
151                     ?: return OngoingCallModel.NoCall
152             logger.log(
153                 TAG,
154                 LogLevel.DEBUG,
155                 { bool1 = currentInfo.notificationIconView != null },
156                 { "Creating OngoingCallModel.InCall. hasIcon=$bool1" },
157             )
158             return OngoingCallModel.InCall(
159                 startTimeMs = currentInfo.callStartTime,
160                 notificationIconView = currentInfo.notificationIconView,
161                 intent = currentInfo.intent,
162                 notificationKey = currentInfo.key,
163                 appName = currentInfo.appName,
164                 promotedContent = currentInfo.promotedContent,
165                 // [hasOngoingCall()] filters out the case in which the call is ongoing but the app
166                 // is visible (we issue [OngoingCallModel.NoCall] below in that case), so this can
167                 // be safely made false.
168                 isAppVisible = false,
169                 notificationInstanceId = currentInfo.instanceId,
170             )
171         } else {
172             return OngoingCallModel.NoCall
173         }
174     }
175 
176     override fun addCallback(listener: OngoingCallListener) {
177         StatusBarChipsModernization.assertInLegacyMode()
178 
179         synchronized(mListeners) {
180             if (!mListeners.contains(listener)) {
181                 mListeners.add(listener)
182             }
183         }
184     }
185 
186     override fun removeCallback(listener: OngoingCallListener) {
187         StatusBarChipsModernization.assertInLegacyMode()
188 
189         synchronized(mListeners) { mListeners.remove(listener) }
190     }
191 
192     private fun updateInfoFromNotifModel(notifModel: ActiveNotificationModel?) {
193         StatusBarChipsModernization.assertInLegacyMode()
194 
195         if (notifModel == null) {
196             logger.log(TAG, LogLevel.DEBUG, {}, { "NotifInteractorCallModel: null" })
197             removeChipInfo()
198         } else if (notifModel.callType != CallType.Ongoing) {
199             logger.log(
200                 TAG,
201                 LogLevel.ERROR,
202                 { str1 = notifModel.callType.name },
203                 { "Notification Interactor sent ActiveNotificationModel with callType=$str1" },
204             )
205             removeChipInfo()
206         } else {
207             logger.log(
208                 TAG,
209                 LogLevel.DEBUG,
210                 {
211                     str1 = notifModel.key
212                     long1 = notifModel.whenTime
213                     str1 = notifModel.callType.name
214                     bool1 = notifModel.statusBarChipIconView != null
215                 },
216                 { "NotifInteractorCallModel: key=$str1 when=$long1 callType=$str2 hasIcon=$bool1" },
217             )
218 
219             val newOngoingCallInfo =
220                 CallNotificationInfo(
221                     notifModel.key,
222                     notifModel.whenTime,
223                     notifModel.statusBarChipIconView,
224                     notifModel.contentIntent,
225                     notifModel.uid,
226                     notifModel.appName,
227                     notifModel.instanceId,
228                     notifModel.promotedContent,
229                     isOngoing = true,
230                     statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false,
231                 )
232             if (newOngoingCallInfo == callNotificationInfo) {
233                 return
234             }
235             callNotificationInfo = newOngoingCallInfo
236             updateChip()
237         }
238     }
239 
240     private fun updateChip() {
241         StatusBarChipsModernization.assertInLegacyMode()
242 
243         val currentCallNotificationInfo = callNotificationInfo ?: return
244 
245         val currentChipView = chipView
246         val timeView = currentChipView?.getTimeView()
247 
248         if (currentChipView != null && timeView != null) {
249             // Current behavior: Displaying the call chip is handled by HomeStatusBarViewBinder, but
250             // this class is still responsible for the non-display logic.
251             // Future behavior: if StatusBarChipsModernization flag is enabled, this class is
252             // completely deprecated and does nothing.
253             uidObserver.registerWithUid(currentCallNotificationInfo.uid)
254             if (!currentCallNotificationInfo.statusBarSwipedAway) {
255                 statusBarWindowControllerStore.defaultDisplay
256                     .setOngoingProcessRequiresStatusBarVisible(true)
257             }
258             updateGestureListening()
259             sendStateChangeEvent()
260         } else {
261             // If we failed to update the chip, don't store the call info. Then [hasOngoingCall]
262             // will return false and we fall back to typical notification handling.
263             callNotificationInfo = null
264             logger.log(
265                 TAG,
266                 LogLevel.WARNING,
267                 {},
268                 { "Ongoing call chip view could not be found; Not displaying chip in status bar" },
269             )
270         }
271     }
272 
273     /** Returns true if the given [procState] represents a process that's visible to the user. */
274     private fun isProcessVisibleToUser(procState: Int): Boolean {
275         StatusBarChipsModernization.assertInLegacyMode()
276 
277         return procState <= ActivityManager.PROCESS_STATE_TOP
278     }
279 
280     private fun updateGestureListening() {
281         StatusBarChipsModernization.assertInLegacyMode()
282 
283         if (
284             callNotificationInfo == null ||
285                 callNotificationInfo?.statusBarSwipedAway == true ||
286                 !isFullscreen
287         ) {
288             swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
289         } else {
290             swipeStatusBarAwayGestureHandler.addOnGestureDetectedCallback(TAG) { _ ->
291                 onSwipeAwayGestureDetected()
292             }
293         }
294     }
295 
296     private fun removeChipInfo() {
297         StatusBarChipsModernization.assertInLegacyMode()
298 
299         callNotificationInfo = null
300         statusBarWindowControllerStore.defaultDisplay.setOngoingProcessRequiresStatusBarVisible(
301             false
302         )
303         swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
304         sendStateChangeEvent()
305         uidObserver.unregister()
306     }
307 
308     /** Tear down anything related to the chip view to prevent leaks. */
309     @VisibleForTesting fun tearDownChipView() = chipView?.getTimeView()?.stop()
310 
311     private fun View.getTimeView(): ChipChronometer? {
312         return this.findViewById(R.id.ongoing_activity_chip_time)
313     }
314 
315     /**
316      * If there's an active ongoing call, then we will force the status bar to always show, even if
317      * the user is in immersive mode. However, we also want to give users the ability to swipe away
318      * the status bar if they need to access the area under the status bar.
319      *
320      * This method updates the status bar window appropriately when the swipe away gesture is
321      * detected.
322      */
323     private fun onSwipeAwayGestureDetected() {
324         StatusBarChipsModernization.assertInLegacyMode()
325 
326         logger.log(TAG, LogLevel.DEBUG, {}, { "Swipe away gesture detected" })
327         callNotificationInfo = callNotificationInfo?.copy(statusBarSwipedAway = true)
328         statusBarWindowControllerStore.defaultDisplay.setOngoingProcessRequiresStatusBarVisible(
329             false
330         )
331         swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
332     }
333 
334     private fun sendStateChangeEvent() {
335         StatusBarChipsModernization.assertInLegacyMode()
336 
337         ongoingCallRepository.setOngoingCallState(getOngoingCallModel())
338         mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
339     }
340 
341     private data class CallNotificationInfo(
342         val key: String,
343         val callStartTime: Long,
344         /** The icon set as the [android.app.Notification.getSmallIcon] field. */
345         val notificationIconView: StatusBarIconView?,
346         val intent: PendingIntent?,
347         val uid: Int,
348         val appName: String,
349         val instanceId: InstanceId?,
350         /**
351          * If the call notification also meets promoted notification criteria, this field is filled
352          * in with the content related to promotion. Otherwise null.
353          */
354         val promotedContent: PromotedNotificationContentModels?,
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     override fun dump(pw: PrintWriter, args: Array<out String>) {
362         pw.println("Active call notification: $callNotificationInfo")
363         pw.println("Call app visible: ${uidObserver.isCallAppVisible}")
364     }
365 
366     /**
367      * Observer to tell us when the app that posted the ongoing call notification is visible so that
368      * we don't show the call chip at the same time (since the timers could be out-of-sync).
369      *
370      * For a more recommended architecture implementation, see
371      * [com.android.systemui.activity.data.repository.ActivityManagerRepository].
372      */
373     inner class CallAppUidObserver : UidObserver() {
374         /** True if the application managing the call is visible to the user. */
375         var isCallAppVisible: Boolean = false
376             private set
377 
378         /** The UID of the application managing the call. Null if there is no active call. */
379         private var callAppUid: Int? = null
380 
381         /**
382          * True if this observer is currently registered with the activity manager and false
383          * otherwise.
384          */
385         private var isRegistered = false
386 
387         /** Register this observer with the activity manager and the given [uid]. */
388         fun registerWithUid(uid: Int) {
389             if (callAppUid == uid) {
390                 return
391             }
392             callAppUid = uid
393 
394             try {
395                 isCallAppVisible =
396                     isProcessVisibleToUser(
397                         iActivityManager.getUidProcessState(uid, context.opPackageName)
398                     )
399                 logger.log(
400                     TAG,
401                     LogLevel.DEBUG,
402                     { bool1 = isCallAppVisible },
403                     { "On uid observer registration, isCallAppVisible=$bool1" },
404                 )
405                 if (isRegistered) {
406                     return
407                 }
408                 iActivityManager.registerUidObserver(
409                     uidObserver,
410                     ActivityManager.UID_OBSERVER_PROCSTATE,
411                     ActivityManager.PROCESS_STATE_UNKNOWN,
412                     context.opPackageName,
413                 )
414                 isRegistered = true
415             } catch (se: SecurityException) {
416                 logger.log(
417                     TAG,
418                     LogLevel.ERROR,
419                     {},
420                     { "Security exception when trying to set up uid observer" },
421                     se,
422                 )
423             }
424         }
425 
426         /** Unregister this observer with the activity manager. */
427         fun unregister() {
428             callAppUid = null
429             isRegistered = false
430             iActivityManager.unregisterUidObserver(uidObserver)
431         }
432 
433         override fun onUidStateChanged(
434             uid: Int,
435             procState: Int,
436             procStateSeq: Long,
437             capability: Int,
438         ) {
439             val currentCallAppUid = callAppUid ?: return
440             if (uid != currentCallAppUid) {
441                 return
442             }
443 
444             val oldIsCallAppVisible = isCallAppVisible
445             isCallAppVisible = isProcessVisibleToUser(procState)
446             if (oldIsCallAppVisible != isCallAppVisible) {
447                 logger.log(
448                     TAG,
449                     LogLevel.DEBUG,
450                     { bool1 = isCallAppVisible },
451                     { "#onUidStateChanged. isCallAppVisible=$bool1" },
452                 )
453                 // Animations may be run as a result of the call's state change, so ensure
454                 // the listener is notified on the main thread.
455                 mainExecutor.execute { sendStateChangeEvent() }
456             }
457         }
458     }
459 }
460 
461 private const val TAG = OngoingCallRepository.TAG
462