• 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.content.Intent
25 import android.util.Log
26 import android.view.View
27 import android.widget.Chronometer
28 import androidx.annotation.VisibleForTesting
29 import com.android.internal.jank.InteractionJankMonitor
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.plugins.ActivityStarter
35 import com.android.systemui.statusbar.FeatureFlags
36 import com.android.systemui.statusbar.notification.collection.NotificationEntry
37 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
38 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
39 import com.android.systemui.statusbar.policy.CallbackController
40 import com.android.systemui.util.time.SystemClock
41 import java.util.concurrent.Executor
42 import javax.inject.Inject
43 
44 /**
45  * A controller to handle the ongoing call chip in the collapsed status bar.
46  */
47 @SysUISingleton
48 class OngoingCallController @Inject constructor(
49     private val notifCollection: CommonNotifCollection,
50     private val featureFlags: FeatureFlags,
51     private val systemClock: SystemClock,
52     private val activityStarter: ActivityStarter,
53     @Main private val mainExecutor: Executor,
54     private val iActivityManager: IActivityManager,
55     private val logger: OngoingCallLogger
56 ) : CallbackController<OngoingCallListener> {
57 
58     /** Non-null if there's an active call notification. */
59     private var callNotificationInfo: CallNotificationInfo? = null
60     /** True if the application managing the call is visible to the user. */
61     private var isCallAppVisible: Boolean = true
62     private var chipView: View? = null
63     private var uidObserver: IUidObserver.Stub? = null
64 
65     private val mListeners: MutableList<OngoingCallListener> = mutableListOf()
66 
67     private val notifListener = object : NotifCollectionListener {
68         // Temporary workaround for b/178406514 for testing purposes.
69         //
70         // b/178406514 means that posting an incoming call notif then updating it to an ongoing call
71         // notif does not work (SysUI never receives the update). This workaround allows us to
72         // trigger the ongoing call chip when an ongoing call notif is *added* rather than
73         // *updated*, allowing us to test the chip.
74         //
75         // TODO(b/183229367): Remove this function override when b/178406514 is fixed.
76         override fun onEntryAdded(entry: NotificationEntry) {
77             onEntryUpdated(entry)
78         }
79 
80         override fun onEntryUpdated(entry: NotificationEntry) {
81             // We have a new call notification or our existing call notification has been updated.
82             // TODO(b/183229367): This likely won't work if you take a call from one app then
83             //  switch to a call from another app.
84             if (callNotificationInfo == null && isCallNotification(entry) ||
85                     (entry.sbn.key == callNotificationInfo?.key)) {
86                 val newOngoingCallInfo = CallNotificationInfo(
87                         entry.sbn.key,
88                         entry.sbn.notification.`when`,
89                         entry.sbn.notification.contentIntent?.intent,
90                         entry.sbn.uid,
91                         entry.sbn.notification.extras.getInt(
92                                 Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING
93                 )
94                 if (newOngoingCallInfo == callNotificationInfo) {
95                     return
96                 }
97 
98                 callNotificationInfo = newOngoingCallInfo
99                 if (newOngoingCallInfo.isOngoing) {
100                     updateChip()
101                 } else {
102                     removeChip()
103                 }
104             }
105         }
106 
107         override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
108             if (entry.sbn.key == callNotificationInfo?.key) {
109                 removeChip()
110             }
111         }
112     }
113 
114     fun init() {
115         if (featureFlags.isOngoingCallStatusBarChipEnabled) {
116             notifCollection.addCollectionListener(notifListener)
117         }
118     }
119 
120     /**
121      * Sets the chip view that will contain ongoing call information.
122      *
123      * Should only be called from [CollapsedStatusBarFragment].
124      */
125     fun setChipView(chipView: View) {
126         tearDownChipView()
127         this.chipView = chipView
128         if (hasOngoingCall()) {
129             updateChip()
130         }
131     }
132 
133 
134     /**
135      * Called when the chip's visibility may have changed.
136      *
137      * Should only be called from [CollapsedStatusBarFragment].
138      */
139     fun notifyChipVisibilityChanged(chipIsVisible: Boolean) {
140         logger.logChipVisibilityChanged(chipIsVisible)
141     }
142 
143     /**
144      * Returns true if there's an active ongoing call that should be displayed in a status bar chip.
145      */
146     fun hasOngoingCall(): Boolean {
147         return callNotificationInfo?.isOngoing == true &&
148                 // When the user is in the phone app, don't show the chip.
149                 !isCallAppVisible
150     }
151 
152     override fun addCallback(listener: OngoingCallListener) {
153         synchronized(mListeners) {
154             if (!mListeners.contains(listener)) {
155                 mListeners.add(listener)
156             }
157         }
158     }
159 
160     override fun removeCallback(listener: OngoingCallListener) {
161         synchronized(mListeners) {
162             mListeners.remove(listener)
163         }
164     }
165 
166     private fun updateChip() {
167         val currentCallNotificationInfo = callNotificationInfo ?: return
168 
169         val currentChipView = chipView
170         val timeView = currentChipView?.getTimeView()
171         val backgroundView =
172             currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background)
173 
174         if (currentChipView != null && timeView != null && backgroundView != null) {
175             if (currentCallNotificationInfo.hasValidStartTime()) {
176                 timeView.setShouldHideText(false)
177                 timeView.base = currentCallNotificationInfo.callStartTime -
178                         systemClock.currentTimeMillis() +
179                         systemClock.elapsedRealtime()
180                 timeView.start()
181             } else {
182                 timeView.setShouldHideText(true)
183                 timeView.stop()
184             }
185 
186             currentCallNotificationInfo.intent?.let { intent ->
187                 currentChipView.setOnClickListener {
188                     logger.logChipClicked()
189                     activityStarter.postStartActivityDismissingKeyguard(
190                             intent,
191                             0,
192                             ActivityLaunchAnimator.Controller.fromView(
193                                     backgroundView,
194                                     InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP)
195                     )
196                 }
197             }
198 
199             setUpUidObserver(currentCallNotificationInfo)
200 
201             mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
202         } else {
203             // If we failed to update the chip, don't store the call info. Then [hasOngoingCall]
204             // will return false and we fall back to typical notification handling.
205             callNotificationInfo = null
206 
207             if (DEBUG) {
208                 Log.w(TAG, "Ongoing call chip view could not be found; " +
209                         "Not displaying chip in status bar")
210             }
211         }
212     }
213 
214     /**
215      * Sets up an [IUidObserver] to monitor the status of the application managing the ongoing call.
216      */
217     private fun setUpUidObserver(currentCallNotificationInfo: CallNotificationInfo) {
218         isCallAppVisible = isProcessVisibleToUser(
219                 iActivityManager.getUidProcessState(currentCallNotificationInfo.uid, null))
220 
221         if (uidObserver != null) {
222             iActivityManager.unregisterUidObserver(uidObserver)
223         }
224 
225         uidObserver = object : IUidObserver.Stub() {
226             override fun onUidStateChanged(
227                     uid: Int, procState: Int, procStateSeq: Long, capability: Int) {
228                 if (uid == currentCallNotificationInfo.uid) {
229                     val oldIsCallAppVisible = isCallAppVisible
230                     isCallAppVisible = isProcessVisibleToUser(procState)
231                     if (oldIsCallAppVisible != isCallAppVisible) {
232                         // Animations may be run as a result of the call's state change, so ensure
233                         // the listener is notified on the main thread.
234                         mainExecutor.execute {
235                             mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
236                         }
237                     }
238                 }
239             }
240 
241             override fun onUidGone(uid: Int, disabled: Boolean) {}
242             override fun onUidActive(uid: Int) {}
243             override fun onUidIdle(uid: Int, disabled: Boolean) {}
244             override fun onUidCachedChanged(uid: Int, cached: Boolean) {}
245         }
246 
247         iActivityManager.registerUidObserver(
248                 uidObserver,
249                 ActivityManager.UID_OBSERVER_PROCSTATE,
250                 ActivityManager.PROCESS_STATE_UNKNOWN,
251                 null
252         )
253     }
254 
255     /** Returns true if the given [procState] represents a process that's visible to the user. */
256     private fun isProcessVisibleToUser(procState: Int): Boolean {
257         return procState <= ActivityManager.PROCESS_STATE_TOP
258     }
259 
260     private fun removeChip() {
261         callNotificationInfo = null
262         tearDownChipView()
263         mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
264         if (uidObserver != null) {
265             iActivityManager.unregisterUidObserver(uidObserver)
266         }
267     }
268 
269     /** Tear down anything related to the chip view to prevent leaks. */
270     @VisibleForTesting
271     fun tearDownChipView() = chipView?.getTimeView()?.stop()
272 
273     private fun View.getTimeView(): OngoingCallChronometer? {
274         return this.findViewById(R.id.ongoing_call_chip_time)
275     }
276 
277     private data class CallNotificationInfo(
278         val key: String,
279         val callStartTime: Long,
280         val intent: Intent?,
281         val uid: Int,
282         /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
283         val isOngoing: Boolean
284     ) {
285         /**
286          * Returns true if the notification information has a valid call start time.
287          * See b/192379214.
288          */
289         fun hasValidStartTime(): Boolean = callStartTime > 0
290     }
291 }
292 
isCallNotificationnull293 private fun isCallNotification(entry: NotificationEntry): Boolean {
294     return entry.sbn.notification.isStyle(Notification.CallStyle::class.java)
295 }
296 
297 private const val TAG = "OngoingCallController"
298 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
299