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