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