• 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.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