1 /* 2 * Copyright (C) 2023 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.bouncer.domain.interactor 18 19 import android.annotation.SuppressLint 20 import android.app.ActivityOptions 21 import android.app.ActivityTaskManager 22 import android.content.Context 23 import android.content.Intent 24 import android.os.UserHandle 25 import android.telecom.TelecomManager 26 import com.android.internal.R 27 import com.android.internal.logging.MetricsLogger 28 import com.android.internal.logging.nano.MetricsProto.MetricsEvent 29 import com.android.internal.util.EmergencyAffordanceManager 30 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor 31 import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository 32 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel 33 import com.android.systemui.dagger.SysUISingleton 34 import com.android.systemui.dagger.qualifiers.Application 35 import com.android.systemui.dagger.qualifiers.Background 36 import com.android.systemui.doze.DozeLogger 37 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository 38 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor 39 import com.android.systemui.user.domain.interactor.SelectedUserInteractor 40 import com.android.systemui.util.EmergencyDialerConstants 41 import javax.inject.Inject 42 import kotlinx.coroutines.CoroutineDispatcher 43 import kotlinx.coroutines.flow.Flow 44 import kotlinx.coroutines.flow.distinctUntilChanged 45 import kotlinx.coroutines.flow.flowOf 46 import kotlinx.coroutines.flow.map 47 import kotlinx.coroutines.flow.merge 48 import kotlinx.coroutines.withContext 49 50 /** 51 * Encapsulates business logic and application state for the bouncer action button. The action 52 * button can support multiple different actions, depending on device state. 53 */ 54 @SysUISingleton 55 class BouncerActionButtonInteractor 56 @Inject 57 constructor( 58 @Application private val applicationContext: Context, 59 @Background private val backgroundDispatcher: CoroutineDispatcher, 60 private val repository: EmergencyServicesRepository, 61 // TODO(b/307977401): Replace with `MobileConnectionsInteractor` when available. 62 private val mobileConnectionsRepository: MobileConnectionsRepository, 63 private val telephonyInteractor: TelephonyInteractor, 64 private val authenticationInteractor: AuthenticationInteractor, 65 private val selectedUserInteractor: SelectedUserInteractor, 66 private val activityTaskManager: ActivityTaskManager, 67 private val telecomManager: TelecomManager?, 68 private val emergencyAffordanceManager: EmergencyAffordanceManager, 69 private val emergencyDialerIntentFactory: EmergencyDialerIntentFactory, 70 private val metricsLogger: MetricsLogger, 71 private val dozeLogger: DozeLogger, 72 ) { 73 /** The bouncer action button. If `null`, the button should not be shown. */ 74 val actionButton: Flow<BouncerActionButtonModel?> = 75 if (telecomManager == null || !telephonyInteractor.hasTelephonyRadio) { 76 flowOf(null) 77 } else { 78 merge( 79 telephonyInteractor.isInCall.asUnitFlow, 80 mobileConnectionsRepository.isAnySimSecure.asUnitFlow, 81 authenticationInteractor.authenticationMethod.asUnitFlow, 82 repository.enableEmergencyCallWhileSimLocked.asUnitFlow, 83 ) <lambda>null84 .map { 85 when { 86 isReturnToCallButton() -> returnToCallButtonModel 87 isEmergencyCallButton() -> emergencyCallButtonModel 88 else -> null // Do not show the button. 89 } 90 } 91 .distinctUntilChanged() 92 } 93 <lambda>null94 private val returnToCallButtonModel: BouncerActionButtonModel by lazy { 95 BouncerActionButtonModel( 96 label = applicationContext.getString(R.string.lockscreen_return_to_call), 97 onClick = { 98 prepareToPerformAction() 99 returnToCall() 100 }, 101 onLongClick = null 102 ) 103 } 104 <lambda>null105 private val emergencyCallButtonModel: BouncerActionButtonModel by lazy { 106 BouncerActionButtonModel( 107 label = applicationContext.getString(R.string.lockscreen_emergency_call), 108 onClick = { 109 prepareToPerformAction() 110 dozeLogger.logEmergencyCall() 111 startEmergencyDialerActivity() 112 }, 113 // TODO(b/308001302): The long click detector doesn't work properly, investigate. 114 onLongClick = { 115 if (emergencyAffordanceManager.needsEmergencyAffordance()) { 116 prepareToPerformAction() 117 118 // TODO(b/298026988): Check that !longPressWasDragged before invoking. 119 emergencyAffordanceManager.performEmergencyCall() 120 } 121 } 122 ) 123 } 124 startEmergencyDialerActivitynull125 private fun startEmergencyDialerActivity() { 126 emergencyDialerIntentFactory()?.apply { 127 flags = 128 Intent.FLAG_ACTIVITY_NEW_TASK or 129 Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or 130 Intent.FLAG_ACTIVITY_CLEAR_TOP 131 132 putExtra( 133 EmergencyDialerConstants.EXTRA_ENTRY_TYPE, 134 EmergencyDialerConstants.ENTRY_TYPE_LOCKSCREEN_BUTTON, 135 ) 136 137 // TODO(b/25189994): Use the ActivityStarter interface instead. 138 applicationContext.startActivityAsUser( 139 this, 140 ActivityOptions.makeCustomAnimation(applicationContext, 0, 0).toBundle(), 141 UserHandle(selectedUserInteractor.getSelectedUserId()) 142 ) 143 } 144 } 145 isReturnToCallButtonnull146 private fun isReturnToCallButton() = telephonyInteractor.isInCall.value 147 148 private suspend fun isEmergencyCallButton(): Boolean { 149 return if (mobileConnectionsRepository.getIsAnySimSecure()) { 150 // Some countries can't handle emergency calls while SIM is locked. 151 repository.enableEmergencyCallWhileSimLocked.value 152 } else { 153 // Only show if there is a secure screen (password/pin/pattern/SIM pin/SIM puk). 154 withContext(backgroundDispatcher) { 155 authenticationInteractor.getAuthenticationMethod().isSecure 156 } 157 } 158 } 159 prepareToPerformActionnull160 private fun prepareToPerformAction() { 161 // TODO(b/308001302): Trigger occlusion and resetting bouncer state. 162 metricsLogger.action(MetricsEvent.ACTION_EMERGENCY_CALL) 163 activityTaskManager.stopSystemLockTaskMode() 164 } 165 166 @SuppressLint("MissingPermission") returnToCallnull167 private fun returnToCall() { 168 telecomManager?.showInCallScreen(/* showDialpad = */ false) 169 } 170 171 private val <T> Flow<T>.asUnitFlow: Flow<Unit> <lambda>null172 get() = map {} 173 } 174 175 /** 176 * Creates an intent to launch the Emergency Services dialer. If no [TelecomManager] is present, 177 * returns `null`. 178 */ 179 interface EmergencyDialerIntentFactory { invokenull180 operator fun invoke(): Intent? 181 } 182