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) {
    165             if (!isFullConference(mTelephonyConference)) {
    166                 List<Connection> nonConferencedConnections = mTelephonyConnections
    167                         .stream()
    168                         // Only retrieve Connections that are not in a conference (but support
    169                         // conferences).
    170                         .filter(c -> c.isConferenceSupported() && c.getConference() == null)
    171                         .collect(Collectors.toList());
    172                 mTelephonyConference.setConferenceableConnections(nonConferencedConnections);
    173             } else {
    174                 Log.d(this, "cannot merge anymore due it is full");
    175                 mTelephonyConference
    176                         .setConferenceableConnections(Collections.<Connection>emptyList());
    177             }
    178         }
    179         // TODO: Do not allow conferencing of already conferenced connections.
    180     }
    181 
    182     private void recalculateConference() {
    183         Set<Connection> conferencedConnections = new HashSet<>();
    184         int numGsmConnections = 0;
    185 
    186         for (TelephonyConnection connection : mTelephonyConnections) {
    187             com.android.internal.telephony.Connection radioConnection =
    188                 connection.getOriginalConnection();
    189             if (radioConnection != null) {
    190                 Call.State state = radioConnection.getState();
    191                 Call call = radioConnection.getCall();
    192                 if ((state == Call.State.ACTIVE || state == Call.State.HOLDING) &&
    193                         (call != null && call.isMultiparty())) {
    194                     numGsmConnections++;
    195                     conferencedConnections.add(connection);
    196                 }
    197             }
    198         }
    199 
    200         Log.d(this, "Recalculate conference calls %s %s.",
    201                 mTelephonyConference, conferencedConnections);
    202 
    203         // Check if all conferenced connections are in Connection Service
    204         boolean allConnInService = true;
    205         Collection<Connection> allConnections = mConnectionService.getAllConnections();
    206         for (Connection connection : conferencedConnections) {
    207             Log.v (this, "Finding connection in Connection Service for " + connection);
    208             if (!allConnections.contains(connection)) {
    209                 allConnInService = false;
    210                 Log.v(this, "Finding connection in Connection Service Failed");
    211                 break;
    212             }
    213         }
    214 
    215         Log.d(this, "Is there a match for all connections in connection service " +
    216             allConnInService);
    217 
    218         // If this is a GSM conference and the number of connections drops below 2, we will
    219         // terminate the conference.
    220         if (numGsmConnections < 2) {
    221             Log.d(this, "not enough connections to be a conference!");
    222 
    223             // No more connections are conferenced, destroy any existing conference.
    224             if (mTelephonyConference != null) {
    225                 Log.d(this, "with a conference to destroy!");
    226                 mTelephonyConference.destroy();
    227                 mTelephonyConference = null;
    228             }
    229         } else {
    230             if (mTelephonyConference != null) {
    231                 List<Connection> existingConnections = mTelephonyConference.getConnections();
    232                 // Remove any that no longer exist
    233                 for (Connection connection : existingConnections) {
    234                     if (connection instanceof TelephonyConnection &&
    235                             !conferencedConnections.contains(connection)) {
    236                         mTelephonyConference.removeConnection(connection);
    237                     }
    238                 }
    239                 if (allConnInService) {
    240                     mTriggerRecalculate = false;
    241                     // Add any new ones
    242                     for (Connection connection : conferencedConnections) {
    243                         if (!existingConnections.contains(connection)) {
    244                             mTelephonyConference.addConnection(connection);
    245                         }
    246                     }
    247                 } else {
    248                     Log.d(this, "Trigger recalculate later");
    249                     mTriggerRecalculate = true;
    250                 }
    251             } else {
    252                 if (allConnInService) {
    253                     mTriggerRecalculate = false;
    254 
    255                     // Get PhoneAccount from one of the conferenced connections and use it to set
    256                     // the phone account on the conference.
    257                     PhoneAccountHandle phoneAccountHandle = null;
    258                     if (!conferencedConnections.isEmpty()) {
    259                         TelephonyConnection telephonyConnection =
    260                                 (TelephonyConnection) conferencedConnections.iterator().next();
    261                         phoneAccountHandle = PhoneUtils.makePstnPhoneAccountHandle(
    262                                 telephonyConnection.getPhone());
    263                     }
    264 
    265                     mTelephonyConference = new TelephonyConference(phoneAccountHandle);
    266                     for (Connection connection : conferencedConnections) {
    267                         Log.d(this, "Adding a connection to a conference call: %s %s",
    268                                 mTelephonyConference, connection);
    269                         mTelephonyConference.addConnection(connection);
    270                     }
    271                     mConnectionService.addConference(mTelephonyConference);
    272                 } else {
    273                     Log.d(this, "Trigger recalculate later");
    274                     mTriggerRecalculate = true;
    275                 }
    276             }
    277             if (mTelephonyConference != null) {
    278                 Connection conferencedConnection = mTelephonyConference.getPrimaryConnection();
    279                 Log.v(this, "Primary Conferenced connection is " + conferencedConnection);
    280                 if (conferencedConnection != null) {
    281                     switch (conferencedConnection.getState()) {
    282                         case Connection.STATE_ACTIVE:
    283                             Log.v(this, "Setting conference to active");
    284                             mTelephonyConference.setActive();
    285                             break;
    286                         case Connection.STATE_HOLDING:
    287                             Log.v(this, "Setting conference to hold");
    288                             mTelephonyConference.setOnHold();
    289                             break;
    290                     }
    291                 }
    292             }
    293         }
    294     }
    295 }
    296