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     /*package*/ void
setMute(boolean mute)273     setMute(boolean mute) {
274         ImsCall imsCall = getFirstConnection() == null ?
275                 null : getFirstConnection().getImsCall();
276         if (imsCall != null) {
277             try {
278                 imsCall.setMute(mute);
279             } catch (ImsException e) {
280                 Rlog.e(LOG_TAG, "setMute failed : " + e.getMessage());
281             }
282         }
283     }
284 
285     @UnsupportedAppUsage
286     /* package */ void
merge(ImsPhoneCall that, State state)287     merge(ImsPhoneCall that, State state) {
288         // This call is the conference host and the "that" call is the one being merged in.
289         // Set the connect time for the conference; this will have been determined when the
290         // conference was initially created.
291         ImsPhoneConnection imsPhoneConnection = getFirstConnection();
292         if (imsPhoneConnection != null) {
293             long conferenceConnectTime = imsPhoneConnection.getConferenceConnectTime();
294             if (conferenceConnectTime > 0) {
295                 imsPhoneConnection.setConnectTime(conferenceConnectTime);
296                 imsPhoneConnection.setConnectTimeReal(imsPhoneConnection.getConnectTimeReal());
297             } else {
298                 if (DBG) {
299                     Rlog.d(LOG_TAG, "merge: conference connect time is 0");
300                 }
301             }
302         }
303         if (DBG) {
304             Rlog.d(LOG_TAG, "merge(" + mCallContext + "): " + that + "state = "
305                     + state);
306         }
307     }
308 
309     /**
310      * Retrieves the {@link ImsCall} for the current {@link ImsPhoneCall}.
311      * <p>
312      * Marked as {@code VisibleForTesting} so that the
313      * {@link com.android.internal.telephony.TelephonyTester} class can inject a test conference
314      * event package into a regular ongoing IMS call.
315      *
316      * @return The {@link ImsCall}.
317      */
318     @VisibleForTesting
319     @UnsupportedAppUsage
320     public ImsCall
getImsCall()321     getImsCall() {
322         return (getFirstConnection() == null) ? null : getFirstConnection().getImsCall();
323     }
324 
isLocalTone(ImsCall imsCall)325     /*package*/ static boolean isLocalTone(ImsCall imsCall) {
326         if ((imsCall == null) || (imsCall.getCallProfile() == null)
327                 || (imsCall.getCallProfile().mMediaProfile == null)) {
328             return false;
329         }
330 
331         ImsStreamMediaProfile mediaProfile = imsCall.getCallProfile().mMediaProfile;
332 
333         return (mediaProfile.mAudioDirection == ImsStreamMediaProfile.DIRECTION_INACTIVE)
334                 ? true : false;
335     }
336 
update(ImsPhoneConnection conn, ImsCall imsCall, State state)337     public boolean update (ImsPhoneConnection conn, ImsCall imsCall, State state) {
338         boolean changed = false;
339         State oldState = mState;
340 
341         //ImsCall.Listener.onCallProgressing can be invoked several times
342         //and ringback tone mode can be changed during the call setup procedure
343         if (state == State.ALERTING) {
344             if (mIsRingbackTonePlaying && !isLocalTone(imsCall)) {
345                 getPhone().stopRingbackTone();
346                 mIsRingbackTonePlaying = false;
347             } else if (!mIsRingbackTonePlaying && isLocalTone(imsCall)) {
348                 getPhone().startRingbackTone();
349                 mIsRingbackTonePlaying = true;
350             }
351         } else {
352             if (mIsRingbackTonePlaying) {
353                 getPhone().stopRingbackTone();
354                 mIsRingbackTonePlaying = false;
355             }
356         }
357 
358         if ((state != mState) && (state != State.DISCONNECTED)) {
359             mState = state;
360             changed = true;
361         } else if (state == State.DISCONNECTED) {
362             changed = true;
363         }
364 
365         if (VDBG) {
366             Rlog.v(LOG_TAG, "update : " + mCallContext + " state: " + oldState + " --> " + mState);
367         }
368 
369         return changed;
370     }
371 
372     /* package */ ImsPhoneConnection
getHandoverConnection()373     getHandoverConnection() {
374         return (ImsPhoneConnection) getEarliestConnection();
375     }
376 
switchWith(ImsPhoneCall that)377     public void switchWith(ImsPhoneCall that) {
378         if (VDBG) {
379             Rlog.v(LOG_TAG, "switchWith : switchCall = " + this + " withCall = " + that);
380         }
381         synchronized (ImsPhoneCall.class) {
382             ImsPhoneCall tmp = new ImsPhoneCall();
383             tmp.takeOver(this);
384             this.takeOver(that);
385             that.takeOver(tmp);
386         }
387         mOwner.logState();
388     }
389 
390     /**
391      * Stops ringback tone playing if it is playing.
392      */
maybeStopRingback()393     public void maybeStopRingback() {
394         if (mIsRingbackTonePlaying) {
395             getPhone().stopRingbackTone();
396             mIsRingbackTonePlaying = false;
397         }
398     }
399 
isRingbackTonePlaying()400     public boolean isRingbackTonePlaying() {
401         return mIsRingbackTonePlaying;
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