Home | History | Annotate | Download | only in telephony
      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 
     17 package com.android.services.telephony;
     18 
     19 import java.util.ArrayList;
     20 import java.util.Collection;
     21 import java.util.Collections;
     22 import java.util.HashMap;
     23 import java.util.HashSet;
     24 import java.util.List;
     25 import java.util.Set;
     26 import java.util.stream.Collectors;
     27 
     28 import android.net.Uri;
     29 import android.telecom.Conference;
     30 import android.telecom.ConferenceParticipant;
     31 import android.telecom.Conferenceable;
     32 import android.telecom.Connection;
     33 import android.telecom.DisconnectCause;
     34 import android.telecom.PhoneAccountHandle;
     35 import com.android.phone.PhoneUtils;
     36 
     37 import com.android.internal.telephony.Call;
     38 
     39 /**
     40  * Maintains a list of all the known TelephonyConnections connections and controls GSM and
     41  * default IMS conference call behavior. This functionality is characterized by the support of
     42  * two top-level calls, in contrast to a CDMA conference call which automatically starts a
     43  * conference when there are two calls.
     44  */
     45 final class TelephonyConferenceController {
     46     private static final int TELEPHONY_CONFERENCE_MAX_SIZE = 5;
     47 
     48     private final Connection.Listener mConnectionListener = new Connection.Listener() {
     49         @Override
     50         public void onStateChanged(Connection c, int state) {
     51             Log.v(this, "onStateChange triggered in Conf Controller : connection = "+ c
     52                  + " state = " + state);
     53             recalculate();
     54         }
     55 
     56         /** ${inheritDoc} */
     57         @Override
     58         public void onDisconnected(Connection c, DisconnectCause disconnectCause) {
     59             recalculate();
     60         }
     61 
     62         @Override
     63         public void onDestroyed(Connection connection) {
     64             remove(connection);
     65         }
     66     };
     67 
     68     /** The known connections. */
     69     private final List<TelephonyConnection> mTelephonyConnections = new ArrayList<>();
     70 
     71     private final TelephonyConnectionServiceProxy mConnectionService;
     72     private boolean mTriggerRecalculate = false;
     73 
     74     public TelephonyConferenceController(TelephonyConnectionServiceProxy connectionService) {
     75         mConnectionService = connectionService;
     76     }
     77     /** The TelephonyConference connection object. */
     78     private TelephonyConference mTelephonyConference;
     79 
     80     boolean shouldRecalculate() {
     81         Log.d(this, "shouldRecalculate is " + mTriggerRecalculate);
     82         return mTriggerRecalculate;
     83     }
     84 
     85     void add(TelephonyConnection connection) {
     86         if (mTelephonyConnections.contains(connection)) {
     87             // Adding a duplicate realistically shouldn't happen.
     88             Log.w(this, "add - connection already tracked; connection=%s", connection);
     89             return;
     90         }
     91         mTelephonyConnections.add(connection);
     92         connection.addConnectionListener(mConnectionListener);
     93         recalculate();
     94     }
     95 
     96     void remove(Connection connection) {
     97         if (!mTelephonyConnections.contains(connection)) {
     98             // Debug only since TelephonyConnectionService tries to clean up the connections tracked
     99             // when the original connection changes.  It does this proactively.
    100             Log.d(this, "remove - connection not tracked; connection=%s", connection);
    101             return;
    102         }
    103         connection.removeConnectionListener(mConnectionListener);
    104         mTelephonyConnections.remove(connection);
    105         recalculate();
    106     }
    107 
    108     void recalculate() {
    109         recalculateConference();
    110         recalculateConferenceable();
    111     }
    112 
    113     private boolean isFullConference(Conference conference) {
    114         return conference.getConnections().size() >= TELEPHONY_CONFERENCE_MAX_SIZE;
    115     }
    116 
    117     private boolean participatesInFullConference(Connection connection) {
    118         return connection.getConference() != null &&
    119                 isFullConference(connection.getConference());
    120     }
    121 
    122     /**
    123      * Calculates the conference-capable state of all GSM connections in this connection service.
    124      */
    125     private void recalculateConferenceable() {
    126         Log.v(this, "recalculateConferenceable : %d", mTelephonyConnections.size());
    127         HashSet<Connection> conferenceableConnections = new HashSet<>(mTelephonyConnections.size());
    128 
    129         // Loop through and collect all calls which are active or holding
    130         for (TelephonyConnection connection : mTelephonyConnections) {
    131             Log.d(this, "recalc - %s %s supportsConf? %s", connection.getState(), connection,
    132                     connection.isConferenceSupported());
    133 
    134             if (connection.isConferenceSupported() && !participatesInFullConference(connection)) {
    135                 switch (connection.getState()) {
    136                     case Connection.STATE_ACTIVE:
    137                         //fall through
    138                     case Connection.STATE_HOLDING:
    139                         conferenceableConnections.add(connection);
    140                         continue;
    141                     default:
    142                         break;
    143                 }
    144             }
    145 
    146             connection.setConferenceableConnections(Collections.<Connection>emptyList());
    147         }
    148 
    149         Log.v(this, "conferenceable: " + conferenceableConnections.size());
    150 
    151         // Go through all the conferenceable connections and add all other conferenceable
    152         // connections that is not the connection itself
    153         for (Connection c : conferenceableConnections) {
    154             List<Connection> connections = conferenceableConnections
    155                     .stream()
    156                     // Filter out this connection from the list of connections
    157                     .filter(connection -> c != connection)
    158                     .collect(Collectors.toList());
    159             c.setConferenceableConnections(connections);
    160         }
    161 
    162         // Set the conference as conferenceable with all of the connections that are not in the
    163         // conference.
    164         if (mTelephonyConference != null && !isFullConference(mTelephonyConference)) {
    165             List<Connection> nonConferencedConnections = mTelephonyConnections
    166                     .stream()
    167                     // Only retrieve Connections that are not in a conference (but support
    168                     // conferences).
    169                     .filter(c -> c.isConferenceSupported() && c.getConference() == null)
    170                     .collect(Collectors.toList());
    171             mTelephonyConference.setConferenceableConnections(nonConferencedConnections);
    172         }
    173         // TODO: Do not allow conferencing of already conferenced connections.
    174     }
    175 
    176     private void recalculateConference() {
    177         Set<Connection> conferencedConnections = new HashSet<>();
    178         int numGsmConnections = 0;
    179 
    180         for (TelephonyConnection connection : mTelephonyConnections) {
    181             com.android.internal.telephony.Connection radioConnection =
    182                 connection.getOriginalConnection();
    183             if (radioConnection != null) {
    184                 Call.State state = radioConnection.getState();
    185                 Call call = radioConnection.getCall();
    186                 if ((state == Call.State.ACTIVE || state == Call.State.HOLDING) &&
    187                         (call != null && call.isMultiparty())) {
    188                     numGsmConnections++;
    189                     conferencedConnections.add(connection);
    190                 }
    191             }
    192         }
    193 
    194         Log.d(this, "Recalculate conference calls %s %s.",
    195                 mTelephonyConference, conferencedConnections);
    196 
    197         // Check if all conferenced connections are in Connection Service
    198         boolean allConnInService = true;
    199         Collection<Connection> allConnections = mConnectionService.getAllConnections();
    200         for (Connection connection : conferencedConnections) {
    201             Log.v (this, "Finding connection in Connection Service for " + connection);
    202             if (!allConnections.contains(connection)) {
    203                 allConnInService = false;
    204                 Log.v(this, "Finding connection in Connection Service Failed");
    205                 break;
    206             }
    207         }
    208 
    209         Log.d(this, "Is there a match for all connections in connection service " +
    210             allConnInService);
    211 
    212         // If this is a GSM conference and the number of connections drops below 2, we will
    213         // terminate the conference.
    214         if (numGsmConnections < 2) {
    215             Log.d(this, "not enough connections to be a conference!");
    216 
    217             // No more connections are conferenced, destroy any existing conference.
    218             if (mTelephonyConference != null) {
    219                 Log.d(this, "with a conference to destroy!");
    220                 mTelephonyConference.destroy();
    221                 mTelephonyConference = null;
    222             }
    223         } else {
    224             if (mTelephonyConference != null) {
    225                 List<Connection> existingConnections = mTelephonyConference.getConnections();
    226                 // Remove any that no longer exist
    227                 for (Connection connection : existingConnections) {
    228                     if (connection instanceof TelephonyConnection &&
    229                             !conferencedConnections.contains(connection)) {
    230                         mTelephonyConference.removeConnection(connection);
    231                     }
    232                 }
    233                 if (allConnInService) {
    234                     mTriggerRecalculate = false;
    235                     // Add any new ones
    236                     for (Connection connection : conferencedConnections) {
    237                         if (!existingConnections.contains(connection)) {
    238                             mTelephonyConference.addConnection(connection);
    239                         }
    240                     }
    241                 } else {
    242                     Log.d(this, "Trigger recalculate later");
    243                     mTriggerRecalculate = true;
    244                 }
    245             } else {
    246                 if (allConnInService) {
    247                     mTriggerRecalculate = false;
    248 
    249                     // Get PhoneAccount from one of the conferenced connections and use it to set
    250                     // the phone account on the conference.
    251                     PhoneAccountHandle phoneAccountHandle = null;
    252                     if (!conferencedConnections.isEmpty()) {
    253                         TelephonyConnection telephonyConnection =
    254                                 (TelephonyConnection) conferencedConnections.iterator().next();
    255                         phoneAccountHandle = PhoneUtils.makePstnPhoneAccountHandle(
    256                                 telephonyConnection.getPhone());
    257                     }
    258 
    259                     mTelephonyConference = new TelephonyConference(phoneAccountHandle);
    260                     for (Connection connection : conferencedConnections) {
    261                         Log.d(this, "Adding a connection to a conference call: %s %s",
    262                                 mTelephonyConference, connection);
    263                         mTelephonyConference.addConnection(connection);
    264                     }
    265                     mConnectionService.addConference(mTelephonyConference);
    266                 } else {
    267                     Log.d(this, "Trigger recalculate later");
    268                     mTriggerRecalculate = true;
    269                 }
    270             }
    271             if (mTelephonyConference != null) {
    272                 Connection conferencedConnection = mTelephonyConference.getPrimaryConnection();
    273                 Log.v(this, "Primary Conferenced connection is " + conferencedConnection);
    274                 if (conferencedConnection != null) {
    275                     switch (conferencedConnection.getState()) {
    276                         case Connection.STATE_ACTIVE:
    277                             Log.v(this, "Setting conference to active");
    278                             mTelephonyConference.setActive();
    279                             break;
    280                         case Connection.STATE_HOLDING:
    281                             Log.v(this, "Setting conference to hold");
    282                             mTelephonyConference.setOnHold();
    283                             break;
    284                     }
    285                 }
    286             }
    287         }
    288     }
    289 }
    290