1 /* 2 * Copyright (C) 2013 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.internal.telephony.imsphone; 18 19 import android.compat.annotation.UnsupportedAppUsage; 20 import android.os.Build; 21 import android.telephony.DisconnectCause; 22 import android.telephony.ims.ImsCallProfile; 23 import android.telephony.ims.ImsCallSession; 24 import android.telephony.ims.ImsStreamMediaProfile; 25 import android.util.Log; 26 27 import com.android.ims.ImsCall; 28 import com.android.ims.ImsException; 29 import com.android.ims.internal.ConferenceParticipant; 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.internal.telephony.Call; 32 import com.android.internal.telephony.CallStateException; 33 import com.android.internal.telephony.Connection; 34 import com.android.internal.telephony.Phone; 35 import com.android.telephony.Rlog; 36 37 import java.util.ArrayList; 38 import java.util.List; 39 40 /** 41 * {@hide} 42 */ 43 public class ImsPhoneCall extends Call { 44 private static final String LOG_TAG = "ImsPhoneCall"; 45 46 // This flag is meant to be used as a debugging tool to quickly see all logs 47 // regardless of the actual log level set on this component. 48 private static final boolean FORCE_DEBUG = false; /* STOPSHIP if true */ 49 private static final boolean DBG = FORCE_DEBUG || Rlog.isLoggable(LOG_TAG, Log.DEBUG); 50 private static final boolean VDBG = FORCE_DEBUG || Rlog.isLoggable(LOG_TAG, Log.VERBOSE); 51 52 /*************************** Instance Variables **************************/ 53 public static final String CONTEXT_UNKNOWN = "UK"; 54 public static final String CONTEXT_RINGING = "RG"; 55 public static final String CONTEXT_FOREGROUND = "FG"; 56 public static final String CONTEXT_BACKGROUND = "BG"; 57 public static final String CONTEXT_HANDOVER = "HO"; 58 59 /*package*/ ImsPhoneCallTracker mOwner; 60 61 private boolean mIsRingbackTonePlaying = false; 62 63 // Determines what type of ImsPhoneCall this is. ImsPhoneCallTracker uses instances of 64 // ImsPhoneCall to for fg, bg, etc calls. This is used as a convenience for logging so that it 65 // can be made clear whether a call being logged is the foreground, background, etc. 66 private final String mCallContext; 67 68 /****************************** Constructors *****************************/ 69 /*package*/ ImsPhoneCall()70 ImsPhoneCall() { 71 mCallContext = CONTEXT_UNKNOWN; 72 } 73 ImsPhoneCall(ImsPhoneCallTracker owner, String context)74 public ImsPhoneCall(ImsPhoneCallTracker owner, String context) { 75 mOwner = owner; 76 mCallContext = context; 77 } 78 dispose()79 public void dispose() { 80 try { 81 mOwner.hangup(this); 82 } catch (CallStateException ex) { 83 //Rlog.e(LOG_TAG, "dispose: unexpected error on hangup", ex); 84 //while disposing, ignore the exception and clean the connections 85 } finally { 86 List<Connection> connections = getConnections(); 87 for (Connection conn : connections) { 88 conn.onDisconnect(DisconnectCause.LOST_SIGNAL); 89 } 90 } 91 } 92 93 /************************** Overridden from Call *************************/ 94 95 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 96 @Override getConnections()97 public ArrayList<Connection> getConnections() { 98 return super.getConnections(); 99 } 100 101 @Override 102 public Phone getPhone()103 getPhone() { 104 return mOwner.getPhone(); 105 } 106 107 @Override 108 public boolean isMultiparty()109 isMultiparty() { 110 ImsCall imsCall = getImsCall(); 111 if (imsCall == null) { 112 return false; 113 } 114 115 return imsCall.isMultiparty(); 116 } 117 118 /** Please note: if this is the foreground call and a 119 * background call exists, the background call will be resumed. 120 */ 121 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 122 @Override 123 public void hangup()124 hangup() throws CallStateException { 125 mOwner.hangup(this); 126 } 127 128 @Override hangup(@ndroid.telecom.Call.RejectReason int rejectReason)129 public void hangup(@android.telecom.Call.RejectReason int rejectReason) 130 throws CallStateException { 131 mOwner.hangup(this, rejectReason); 132 } 133 134 @Override toString()135 public String toString() { 136 StringBuilder sb = new StringBuilder(); 137 List<Connection> connections = getConnections(); 138 sb.append("[ImsPhoneCall "); 139 sb.append(mCallContext); 140 sb.append(" state: "); 141 sb.append(mState.toString()); 142 sb.append(" "); 143 if (connections.size() > 1) { 144 sb.append(" ERROR_MULTIPLE "); 145 } 146 for (Connection conn : connections) { 147 sb.append(conn); 148 sb.append(" "); 149 } 150 151 sb.append("]"); 152 return sb.toString(); 153 } 154 155 @Override getConferenceParticipants()156 public List<ConferenceParticipant> getConferenceParticipants() { 157 if (!mOwner.isConferenceEventPackageEnabled()) { 158 return null; 159 } 160 ImsCall call = getImsCall(); 161 if (call == null) { 162 return null; 163 } 164 return call.getConferenceParticipants(); 165 } 166 167 //***** Called from ImsPhoneConnection 168 attach(Connection conn)169 public void attach(Connection conn) { 170 if (VDBG) { 171 Rlog.v(LOG_TAG, "attach : " + mCallContext + " conn = " + conn); 172 } 173 clearDisconnected(); 174 addConnection(conn); 175 176 mOwner.logState(); 177 } 178 179 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) attach(Connection conn, State state)180 public void attach(Connection conn, State state) { 181 if (VDBG) { 182 Rlog.v(LOG_TAG, "attach : " + mCallContext + " state = " + 183 state.toString()); 184 } 185 this.attach(conn); 186 mState = state; 187 } 188 189 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) attachFake(Connection conn, State state)190 public void attachFake(Connection conn, State state) { 191 attach(conn, state); 192 } 193 194 /** 195 * Called by ImsPhoneConnection when it has disconnected 196 */ connectionDisconnected(ImsPhoneConnection conn)197 public boolean connectionDisconnected(ImsPhoneConnection conn) { 198 if (mState != State.DISCONNECTED) { 199 /* If only disconnected connections remain, we are disconnected*/ 200 201 boolean hasOnlyDisconnectedConnections = true; 202 203 ArrayList<Connection> connections = getConnections(); 204 for (Connection cn : connections) { 205 if (cn.getState() != State.DISCONNECTED) { 206 hasOnlyDisconnectedConnections = false; 207 break; 208 } 209 } 210 211 if (hasOnlyDisconnectedConnections) { 212 synchronized(this) { 213 mState = State.DISCONNECTED; 214 } 215 if (VDBG) { 216 Rlog.v(LOG_TAG, "connectionDisconnected : " + mCallContext + " state = " + 217 mState); 218 } 219 return true; 220 } 221 } 222 223 return false; 224 } 225 detach(ImsPhoneConnection conn)226 public void detach(ImsPhoneConnection conn) { 227 if (VDBG) { 228 Rlog.v(LOG_TAG, "detach : " + mCallContext + " conn = " + conn); 229 } 230 removeConnection(conn); 231 clearDisconnected(); 232 233 mOwner.logState(); 234 } 235 236 /** 237 * @return true if there's no space in this call for additional 238 * connections to be added via "conference" 239 */ 240 /*package*/ boolean isFull()241 isFull() { 242 return getConnectionsCount() == ImsPhoneCallTracker.MAX_CONNECTIONS_PER_CALL; 243 } 244 245 //***** Called from ImsPhoneCallTracker 246 /** 247 * Called when this Call is being hung up locally (eg, user pressed "end") 248 */ 249 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 250 @VisibleForTesting onHangupLocal()251 public void onHangupLocal() { 252 ArrayList<Connection> connections = getConnections(); 253 for (Connection conn : connections) { 254 ImsPhoneConnection imsConn = (ImsPhoneConnection) conn; 255 imsConn.onHangupLocal(); 256 } 257 synchronized(this) { 258 if (mState.isAlive()) { 259 mState = State.DISCONNECTING; 260 } 261 } 262 if (VDBG) { 263 Rlog.v(LOG_TAG, "onHangupLocal : " + mCallContext + " state = " + mState); 264 } 265 } 266 267 @VisibleForTesting getFirstConnection()268 public ImsPhoneConnection getFirstConnection() { 269 List<Connection> connections = getConnections(); 270 if (connections.size() == 0) return null; 271 272 return (ImsPhoneConnection) connections.get(0); 273 } 274 275 /** 276 * Sets the mute state of the call. 277 * @param mute {@code true} if the call could be muted; {@code false} otherwise. 278 */ 279 @VisibleForTesting setMute(boolean mute)280 public void setMute(boolean mute) { 281 ImsPhoneConnection connection = getFirstConnection(); 282 ImsCall imsCall = connection == null ? null : connection.getImsCall(); 283 if (imsCall != null) { 284 try { 285 imsCall.setMute(mute); 286 } catch (ImsException e) { 287 Rlog.e(LOG_TAG, "setMute failed : " + e.getMessage()); 288 } 289 } 290 } 291 292 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 293 /* package */ void merge(ImsPhoneCall that, State state)294 merge(ImsPhoneCall that, State state) { 295 // This call is the conference host and the "that" call is the one being merged in. 296 // Set the connect time for the conference; this will have been determined when the 297 // conference was initially created. 298 ImsPhoneConnection imsPhoneConnection = getFirstConnection(); 299 if (imsPhoneConnection != null) { 300 long conferenceConnectTime = imsPhoneConnection.getConferenceConnectTime(); 301 if (conferenceConnectTime > 0) { 302 imsPhoneConnection.setConnectTime(conferenceConnectTime); 303 imsPhoneConnection.setConnectTimeReal(imsPhoneConnection.getConnectTimeReal()); 304 } else { 305 if (DBG) { 306 Rlog.d(LOG_TAG, "merge: conference connect time is 0"); 307 } 308 } 309 } 310 if (DBG) { 311 Rlog.d(LOG_TAG, "merge(" + mCallContext + "): " + that + "state = " 312 + state); 313 } 314 } 315 316 /** 317 * Retrieves the {@link ImsCall} for the current {@link ImsPhoneCall}. 318 * <p> 319 * Marked as {@code VisibleForTesting} so that the 320 * {@link com.android.internal.telephony.TelephonyTester} class can inject a test conference 321 * event package into a regular ongoing IMS call. 322 * 323 * @return The {@link ImsCall}. 324 */ 325 @VisibleForTesting 326 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getImsCall()327 public ImsCall getImsCall() { 328 ImsPhoneConnection connection = getFirstConnection(); 329 return (connection == null) ? null : connection.getImsCall(); 330 } 331 332 /** 333 * Retrieves the {@link ImsCallSession#getCallId()} for the current {@link ImsPhoneCall}. 334 */ 335 @VisibleForTesting getCallSessionId()336 public String getCallSessionId() { 337 return ((getImsCall() == null) ? null : getImsCall().getSession()) == null 338 ? null : getImsCall().getSession().getCallId(); 339 } 340 341 /** 342 * Retrieves the service type in {@link ImsCallProfile} for the current {@link ImsPhoneCall}. 343 */ 344 @VisibleForTesting getServiceType()345 public int getServiceType() { 346 if (getFirstConnection() == null) { 347 return ImsCallProfile.SERVICE_TYPE_NONE; 348 } else { 349 return getFirstConnection().isEmergencyCall() 350 ? ImsCallProfile.SERVICE_TYPE_EMERGENCY : ImsCallProfile.SERVICE_TYPE_NORMAL; 351 } 352 } 353 354 /** 355 * Retrieves the call type in {@link ImsCallProfile} for the current {@link ImsPhoneCall}. 356 */ 357 @VisibleForTesting getCallType()358 public int getCallType() { 359 if (getImsCall() == null) { 360 return ImsCallProfile.CALL_TYPE_NONE; 361 } else { 362 return getImsCall().isVideoCall() 363 ? ImsCallProfile.CALL_TYPE_VT : ImsCallProfile.CALL_TYPE_VOICE; 364 } 365 } 366 isLocalTone(ImsCall imsCall)367 /*package*/ static boolean isLocalTone(ImsCall imsCall) { 368 if ((imsCall == null) || (imsCall.getCallProfile() == null) 369 || (imsCall.getCallProfile().mMediaProfile == null)) { 370 return false; 371 } 372 373 ImsStreamMediaProfile mediaProfile = imsCall.getCallProfile().mMediaProfile; 374 boolean shouldPlayRingback = 375 (mediaProfile.mAudioDirection == ImsStreamMediaProfile.DIRECTION_INACTIVE) 376 ? true : false; 377 Rlog.i(LOG_TAG, "isLocalTone: audioDirection=" + mediaProfile.mAudioDirection 378 + ", playRingback=" + shouldPlayRingback); 379 return shouldPlayRingback; 380 } 381 update(ImsPhoneConnection conn, ImsCall imsCall, State state)382 public boolean update(ImsPhoneConnection conn, ImsCall imsCall, State state) { 383 boolean changed = false; 384 State oldState = mState; 385 386 // We will try to start or stop ringback whenever the call has major call state changes. 387 maybeChangeRingbackState(imsCall, state); 388 389 if ((state != mState) && (state != State.DISCONNECTED)) { 390 mState = state; 391 changed = true; 392 } else if (state == State.DISCONNECTED) { 393 changed = true; 394 } 395 396 if (VDBG) { 397 Rlog.v(LOG_TAG, "update : " + mCallContext + " state: " + oldState + " --> " + mState); 398 } 399 400 return changed; 401 } 402 403 /** 404 * Determines whether to change the ringback state for a call. 405 * @param imsCall The call. 406 */ maybeChangeRingbackState(ImsCall imsCall)407 public void maybeChangeRingbackState(ImsCall imsCall) { 408 maybeChangeRingbackState(imsCall, mState); 409 } 410 411 /** 412 * Determines whether local ringback should be playing for the call. We will play local 413 * ringback when a call is in an ALERTING state and the audio direction is DIRECTION_INACTIVE. 414 * @param imsCall The call the change pertains to. 415 * @param state The current state of the call. 416 */ maybeChangeRingbackState(ImsCall imsCall, State state)417 private void maybeChangeRingbackState(ImsCall imsCall, State state) { 418 //ImsCall.Listener.onCallProgressing can be invoked several times 419 //and ringback tone mode can be changed during the call setup procedure 420 Rlog.i(LOG_TAG, "maybeChangeRingbackState: state=" + state); 421 if (state == State.ALERTING) { 422 if (mIsRingbackTonePlaying && !isLocalTone(imsCall)) { 423 Rlog.i(LOG_TAG, "maybeChangeRingbackState: stop ringback"); 424 getPhone().stopRingbackTone(); 425 mIsRingbackTonePlaying = false; 426 } else if (!mIsRingbackTonePlaying && isLocalTone(imsCall)) { 427 Rlog.i(LOG_TAG, "maybeChangeRingbackState: start ringback"); 428 getPhone().startRingbackTone(); 429 mIsRingbackTonePlaying = true; 430 } 431 } else { 432 if (mIsRingbackTonePlaying) { 433 Rlog.i(LOG_TAG, "maybeChangeRingbackState: stop ringback"); 434 getPhone().stopRingbackTone(); 435 mIsRingbackTonePlaying = false; 436 } 437 } 438 } 439 440 /* package */ ImsPhoneConnection getHandoverConnection()441 getHandoverConnection() { 442 return (ImsPhoneConnection) getEarliestConnection(); 443 } 444 switchWith(ImsPhoneCall that)445 public void switchWith(ImsPhoneCall that) { 446 if (VDBG) { 447 Rlog.v(LOG_TAG, "switchWith : switchCall = " + this + " withCall = " + that); 448 } 449 synchronized (ImsPhoneCall.class) { 450 ImsPhoneCall tmp = new ImsPhoneCall(); 451 tmp.takeOver(this); 452 this.takeOver(that); 453 that.takeOver(tmp); 454 } 455 mOwner.logState(); 456 } 457 458 /** 459 * Stops ringback tone playing if it is playing. 460 */ maybeStopRingback()461 public void maybeStopRingback() { 462 if (mIsRingbackTonePlaying) { 463 getPhone().stopRingbackTone(); 464 mIsRingbackTonePlaying = false; 465 } 466 } 467 isRingbackTonePlaying()468 public boolean isRingbackTonePlaying() { 469 return mIsRingbackTonePlaying; 470 } 471 maybeClearRemotelyHeldStatus()472 public void maybeClearRemotelyHeldStatus() { 473 for (Connection conn : getConnections()) { 474 ImsPhoneConnection c = (ImsPhoneConnection) conn; 475 if (c.isHeldByRemote()) { 476 c.setRemotelyUnheld(); 477 } 478 } 479 } 480 takeOver(ImsPhoneCall that)481 private void takeOver(ImsPhoneCall that) { 482 copyConnectionFrom(that); 483 mState = that.mState; 484 for (Connection c : getConnections()) { 485 ((ImsPhoneConnection) c).changeParent(this); 486 } 487 } 488 } 489