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