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