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