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.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.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 val backgroundView: OngoingCallBackgroundContainer? = 140 chipView.findViewById(R.id.ongoing_call_chip_background) 141 backgroundView?.maxHeightFetcher = { statusBarWindowController.get().statusBarHeight } 142 if (hasOngoingCall()) { 143 updateChip() 144 } 145 } 146 147 /** 148 * Called when the chip's visibility may have changed. 149 * 150 * Should only be called from [CollapsedStatusBarFragment]. 151 */ 152 fun notifyChipVisibilityChanged(chipIsVisible: Boolean) { 153 logger.logChipVisibilityChanged(chipIsVisible) 154 } 155 156 /** 157 * Returns true if there's an active ongoing call that should be displayed in a status bar chip. 158 */ 159 fun hasOngoingCall(): Boolean { 160 return callNotificationInfo?.isOngoing == true && 161 // When the user is in the phone app, don't show the chip. 162 !uidObserver.isCallAppVisible 163 } 164 165 override fun addCallback(listener: OngoingCallListener) { 166 synchronized(mListeners) { 167 if (!mListeners.contains(listener)) { 168 mListeners.add(listener) 169 } 170 } 171 } 172 173 override fun removeCallback(listener: OngoingCallListener) { 174 synchronized(mListeners) { 175 mListeners.remove(listener) 176 } 177 } 178 179 private fun updateChip() { 180 val currentCallNotificationInfo = callNotificationInfo ?: return 181 182 val currentChipView = chipView 183 val timeView = currentChipView?.getTimeView() 184 185 if (currentChipView != null && timeView != null) { 186 if (currentCallNotificationInfo.hasValidStartTime()) { 187 timeView.setShouldHideText(false) 188 timeView.base = currentCallNotificationInfo.callStartTime - 189 systemClock.currentTimeMillis() + 190 systemClock.elapsedRealtime() 191 timeView.start() 192 } else { 193 timeView.setShouldHideText(true) 194 timeView.stop() 195 } 196 updateChipClickListener() 197 198 uidObserver.registerWithUid(currentCallNotificationInfo.uid) 199 if (!currentCallNotificationInfo.statusBarSwipedAway) { 200 statusBarWindowController.ifPresent { 201 it.setOngoingProcessRequiresStatusBarVisible(true) 202 } 203 } 204 updateGestureListening() 205 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } 206 } else { 207 // If we failed to update the chip, don't store the call info. Then [hasOngoingCall] 208 // will return false and we fall back to typical notification handling. 209 callNotificationInfo = null 210 211 if (DEBUG) { 212 Log.w(TAG, "Ongoing call chip view could not be found; " + 213 "Not displaying chip in status bar") 214 } 215 } 216 } 217 218 private fun updateChipClickListener() { 219 if (callNotificationInfo == null) { return } 220 if (isFullscreen && !ongoingCallFlags.isInImmersiveChipTapEnabled()) { 221 chipView?.setOnClickListener(null) 222 } else { 223 val currentChipView = chipView 224 val backgroundView = 225 currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background) 226 val intent = callNotificationInfo?.intent 227 if (currentChipView != null && backgroundView != null && intent != null) { 228 currentChipView.setOnClickListener { 229 logger.logChipClicked() 230 activityStarter.postStartActivityDismissingKeyguard( 231 intent, 232 ActivityLaunchAnimator.Controller.fromView( 233 backgroundView, 234 InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP) 235 ) 236 } 237 } 238 } 239 } 240 241 /** Returns true if the given [procState] represents a process that's visible to the user. */ 242 private fun isProcessVisibleToUser(procState: Int): Boolean { 243 return procState <= ActivityManager.PROCESS_STATE_TOP 244 } 245 246 private fun updateGestureListening() { 247 if (callNotificationInfo == null || 248 callNotificationInfo?.statusBarSwipedAway == true || 249 !isFullscreen) { 250 swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) } 251 } else { 252 swipeStatusBarAwayGestureHandler.ifPresent { 253 it.addOnGestureDetectedCallback(TAG) { _ -> onSwipeAwayGestureDetected() } 254 } 255 } 256 } 257 258 private fun removeChip() { 259 callNotificationInfo = null 260 tearDownChipView() 261 statusBarWindowController.ifPresent { it.setOngoingProcessRequiresStatusBarVisible(false) } 262 swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) } 263 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } 264 uidObserver.unregister() 265 } 266 267 /** Tear down anything related to the chip view to prevent leaks. */ 268 @VisibleForTesting 269 fun tearDownChipView() = chipView?.getTimeView()?.stop() 270 271 private fun View.getTimeView(): OngoingCallChronometer? { 272 return this.findViewById(R.id.ongoing_call_chip_time) 273 } 274 275 /** 276 * If there's an active ongoing call, then we will force the status bar to always show, even if 277 * the user is in immersive mode. However, we also want to give users the ability to swipe away 278 * the status bar if they need to access the area under the status bar. 279 * 280 * This method updates the status bar window appropriately when the swipe away gesture is 281 * detected. 282 */ 283 private fun onSwipeAwayGestureDetected() { 284 if (DEBUG) { Log.d(TAG, "Swipe away gesture detected") } 285 callNotificationInfo = callNotificationInfo?.copy(statusBarSwipedAway = true) 286 statusBarWindowController.ifPresent { 287 it.setOngoingProcessRequiresStatusBarVisible(false) 288 } 289 swipeStatusBarAwayGestureHandler.ifPresent { 290 it.removeOnGestureDetectedCallback(TAG) 291 } 292 } 293 294 private val statusBarStateListener = object : StatusBarStateController.StateListener { 295 override fun onFullscreenStateChanged(isFullscreen: Boolean) { 296 this@OngoingCallController.isFullscreen = isFullscreen 297 updateChipClickListener() 298 updateGestureListening() 299 } 300 } 301 302 private data class CallNotificationInfo( 303 val key: String, 304 val callStartTime: Long, 305 val intent: PendingIntent?, 306 val uid: Int, 307 /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */ 308 val isOngoing: Boolean, 309 /** True if the user has swiped away the status bar while in this phone call. */ 310 val statusBarSwipedAway: Boolean 311 ) { 312 /** 313 * Returns true if the notification information has a valid call start time. 314 * See b/192379214. 315 */ 316 fun hasValidStartTime(): Boolean = callStartTime > 0 317 } 318 319 override fun dump(pw: PrintWriter, args: Array<out String>) { 320 pw.println("Active call notification: $callNotificationInfo") 321 pw.println("Call app visible: ${uidObserver.isCallAppVisible}") 322 } 323 324 /** Our implementation of a [IUidObserver]. */ 325 inner class CallAppUidObserver : UidObserver() { 326 /** True if the application managing the call is visible to the user. */ 327 var isCallAppVisible: Boolean = false 328 private set 329 330 /** The UID of the application managing the call. Null if there is no active call. */ 331 private var callAppUid: Int? = null 332 333 /** 334 * True if this observer is currently registered with the activity manager and false 335 * otherwise. 336 */ 337 private var isRegistered = false 338 339 /** Register this observer with the activity manager and the given [uid]. */ 340 fun registerWithUid(uid: Int) { 341 if (callAppUid == uid) { 342 return 343 } 344 callAppUid = uid 345 346 try { 347 isCallAppVisible = isProcessVisibleToUser( 348 iActivityManager.getUidProcessState(uid, context.opPackageName) 349 ) 350 if (isRegistered) { 351 return 352 } 353 iActivityManager.registerUidObserver( 354 uidObserver, 355 ActivityManager.UID_OBSERVER_PROCSTATE, 356 ActivityManager.PROCESS_STATE_UNKNOWN, 357 context.opPackageName 358 ) 359 isRegistered = true 360 } catch (se: SecurityException) { 361 Log.e(TAG, "Security exception when trying to set up uid observer: $se") 362 } 363 } 364 365 /** Unregister this observer with the activity manager. */ 366 fun unregister() { 367 callAppUid = null 368 isRegistered = false 369 iActivityManager.unregisterUidObserver(uidObserver) 370 } 371 372 override fun onUidStateChanged( 373 uid: Int, 374 procState: Int, 375 procStateSeq: Long, 376 capability: Int 377 ) { 378 val currentCallAppUid = callAppUid ?: return 379 if (uid != currentCallAppUid) { 380 return 381 } 382 383 val oldIsCallAppVisible = isCallAppVisible 384 isCallAppVisible = isProcessVisibleToUser(procState) 385 if (oldIsCallAppVisible != isCallAppVisible) { 386 // Animations may be run as a result of the call's state change, so ensure 387 // the listener is notified on the main thread. 388 mainExecutor.execute { 389 mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } 390 } 391 } 392 } 393 } 394 } 395 396 private fun isCallNotification(entry: NotificationEntry): Boolean { 397 return entry.sbn.notification.isStyle(Notification.CallStyle::class.java) 398 } 399 400 private const val TAG = "OngoingCallController" 401 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) 402