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