1/* 2 * Copyright (C) 2014 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.services.telephony; 18 19import android.os.Handler; 20import android.telecom.Connection; 21import android.telecom.DisconnectCause; 22import android.telecom.PhoneCapabilities; 23 24import java.util.ArrayList; 25import java.util.List; 26 27/** 28 * Manages CDMA conference calls. CDMA conference calls are much more limited than GSM conference 29 * calls. Two main points of difference: 30 * 1) Users cannot manage individual calls within a conference 31 * 2) Whether a conference call starts off as a conference or as two distinct calls is a matter of 32 * physical location (some antennas are different than others). Worst still, there's no 33 * indication given to us as to what state they are in. 34 * 35 * To make life easier on the user we do the following: Whenever there exist 2 or more calls, we 36 * say that we are in a conference call with {@link Connection#CAPABILITY_GENERIC_CONFERENCE}. 37 * Generic indicates that this is a simple conference that doesn't support conference management. 38 * The conference call will also support "MERGE" to begin with and stop supporting it the first time 39 * we are asked to actually execute a merge. I emphasize when "we are asked" because we get no 40 * indication whether the merge succeeds from CDMA, we just assume it does. Thats the best we 41 * can do. Also, we do not kill a conference call once it is created unless all underlying 42 * connections also go away. 43 * 44 * Outgoing CDMA calls made while another call exists would normally trigger a conference to be 45 * created. To avoid this and make it seem like there is a "dialing" state, we fake it and prevent 46 * the conference from being created for 3 seconds. This is a more pleasant experience for the user. 47 */ 48final class CdmaConferenceController { 49 private final Connection.Listener mConnectionListener = new Connection.Listener() { 50 @Override 51 public void onStateChanged(Connection c, int state) { 52 recalculateConference(); 53 } 54 55 @Override 56 public void onDisconnected(Connection c, DisconnectCause disconnectCause) { 57 recalculateConference(); 58 } 59 60 @Override 61 public void onDestroyed(Connection c) { 62 remove((CdmaConnection) c); 63 } 64 }; 65 66 private static final int ADD_OUTGOING_CONNECTION_DELAY_MILLIS = 6000; 67 68 /** The known CDMA connections. */ 69 private final List<CdmaConnection> mCdmaConnections = new ArrayList<>(); 70 71 /** 72 * Newly added connections. We keep track of newly added outgoing connections because we do not 73 * create a conference until a second outgoing call has existing for 74 * {@link #ADD_OUTGOING_CONNECTION_DELAY_MILLIS} milliseconds. This allows the UI to show the 75 * call as "Dialing" for a certain amount of seconds. 76 */ 77 private final List<CdmaConnection> mPendingOutgoingConnections = new ArrayList<>(); 78 79 private final TelephonyConnectionService mConnectionService; 80 81 private final Handler mHandler = new Handler(); 82 83 public CdmaConferenceController(TelephonyConnectionService connectionService) { 84 mConnectionService = connectionService; 85 } 86 87 /** The CDMA conference connection object. */ 88 private CdmaConference mConference; 89 90 void add(final CdmaConnection connection) { 91 if (!mCdmaConnections.isEmpty() && connection.isOutgoing()) { 92 // There already exists a connection, so this will probably result in a conference once 93 // it is added. For outgoing connections which are added while another connection 94 // exists, we mark them as "dialing" for a set amount of time to give the user time to 95 // see their new call as "Dialing" before it turns into a conference call. 96 // During that time, we also mark the other calls as "held" or else it can cause issues 97 // due to having an ACTIVE and a DIALING call simultaneously. 98 connection.forceAsDialing(true); 99 final List<CdmaConnection> connectionsToReset = 100 new ArrayList<>(mCdmaConnections.size()); 101 for (CdmaConnection current : mCdmaConnections) { 102 if (current.setHoldingForConference()) { 103 connectionsToReset.add(current); 104 } 105 } 106 mHandler.postDelayed(new Runnable() { 107 @Override 108 public void run() { 109 connection.forceAsDialing(false); 110 addInternal(connection); 111 for (CdmaConnection current : connectionsToReset) { 112 current.resetStateForConference(); 113 } 114 } 115 }, ADD_OUTGOING_CONNECTION_DELAY_MILLIS); 116 } else { 117 // This is the first connection, or it is incoming, so let it flow through. 118 addInternal(connection); 119 } 120 } 121 122 private void addInternal(CdmaConnection connection) { 123 mCdmaConnections.add(connection); 124 connection.addConnectionListener(mConnectionListener); 125 recalculateConference(); 126 } 127 128 private void remove(CdmaConnection connection) { 129 connection.removeConnectionListener(mConnectionListener); 130 mCdmaConnections.remove(connection); 131 recalculateConference(); 132 } 133 134 private void recalculateConference() { 135 List<CdmaConnection> conferenceConnections = new ArrayList<>(mCdmaConnections.size()); 136 for (CdmaConnection connection : mCdmaConnections) { 137 // We do not include call-waiting calls in conferences. 138 if (!connection.isCallWaiting() && 139 connection.getState() != Connection.STATE_DISCONNECTED) { 140 conferenceConnections.add(connection); 141 } 142 } 143 144 Log.d(this, "recalculating conference calls %d", conferenceConnections.size()); 145 if (conferenceConnections.size() >= 2) { 146 boolean isNewlyCreated = false; 147 148 // There are two or more CDMA connections. Do the following: 149 // 1) Create a new conference connection if it doesn't exist. 150 if (mConference == null) { 151 Log.i(this, "Creating new Cdma conference call"); 152 CdmaConnection newConnection = mCdmaConnections.get(mCdmaConnections.size() - 1); 153 if (newConnection.isOutgoing()) { 154 // Only an outgoing call can be merged with an ongoing call. 155 mConference = new CdmaConference(null, PhoneCapabilities.MERGE_CONFERENCE); 156 } else { 157 // If the most recently added connection was an incoming call, enable 158 // swap instead of merge. 159 mConference = new CdmaConference(null, PhoneCapabilities.SWAP_CONFERENCE); 160 } 161 isNewlyCreated = true; 162 } 163 164 // 2) Add any new connections to the conference 165 List<Connection> existingChildConnections = 166 new ArrayList<>(mConference.getConnections()); 167 for (CdmaConnection connection : conferenceConnections) { 168 if (!existingChildConnections.contains(connection)) { 169 Log.i(this, "Adding connection to conference call: %s", connection); 170 mConference.addConnection(connection); 171 } 172 existingChildConnections.remove(connection); 173 } 174 175 // 3) Remove any lingering old/disconnected/destroyed connections 176 for (Connection oldConnection : existingChildConnections) { 177 mConference.removeConnection(oldConnection); 178 Log.i(this, "Removing connection from conference call: %s", oldConnection); 179 } 180 181 // 4) Add the conference to the connection service if it is new. 182 if (isNewlyCreated) { 183 Log.d(this, "Adding the conference call"); 184 mConnectionService.addConference(mConference); 185 } 186 } else if (conferenceConnections.isEmpty()) { 187 // There are no more connection so if we still have a conference, lets remove it. 188 if (mConference != null) { 189 Log.i(this, "Destroying the CDMA conference connection."); 190 mConference.destroy(); 191 } 192 } 193 } 194} 195