1 /* 2 * Copyright (C) 2020 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.internal.telephony.d2d; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.telecom.Connection; 22 import android.telecom.Log; 23 24 import java.util.ArrayList; 25 import java.util.List; 26 import java.util.Objects; 27 import java.util.Optional; 28 import java.util.Set; 29 import java.util.stream.Collectors; 30 31 /** 32 * Responsible for facilitating device-to-device communication between both ends of a call. 33 */ 34 public class Communicator implements TransportProtocol.Callback { 35 36 /** 37 * Callback for events out of communicator. 38 */ 39 public interface Callback { onMessagesReceived(@onNull Set<Message> messages)40 void onMessagesReceived(@NonNull Set<Message> messages); onD2DAvailabilitychanged(boolean isAvailable)41 void onD2DAvailabilitychanged(boolean isAvailable); 42 } 43 44 public static final int MESSAGE_CALL_RADIO_ACCESS_TYPE = 1; 45 public static final int MESSAGE_CALL_AUDIO_CODEC = 2; 46 public static final int MESSAGE_DEVICE_BATTERY_STATE = 3; 47 public static final int MESSAGE_DEVICE_NETWORK_COVERAGE = 4; 48 49 public static final int RADIO_ACCESS_TYPE_LTE = 1; 50 public static final int RADIO_ACCESS_TYPE_IWLAN = 2; 51 public static final int RADIO_ACCESS_TYPE_NR = 3; 52 53 public static final int AUDIO_CODEC_EVS = 1; 54 public static final int AUDIO_CODEC_AMR_WB = 2; 55 public static final int AUDIO_CODEC_AMR_NB = 3; 56 57 public static final int BATTERY_STATE_LOW = 1; 58 public static final int BATTERY_STATE_GOOD = 2; 59 public static final int BATTERY_STATE_CHARGING = 3; 60 61 public static final int COVERAGE_POOR = 1; 62 public static final int COVERAGE_GOOD = 2; 63 64 /** 65 * Encapsulates a D2D communication message. 66 */ 67 public static class Message { 68 private int mType; 69 private int mValue; 70 Message(int type, int value)71 public Message(int type, int value) { 72 mType = type; 73 mValue = value; 74 } 75 getType()76 public int getType() { 77 return mType; 78 } 79 getValue()80 public int getValue() { 81 return mValue; 82 } 83 84 @Override equals(Object o)85 public boolean equals(Object o) { 86 if (this == o) return true; 87 if (o == null || getClass() != o.getClass()) return false; 88 Message message = (Message) o; 89 return mType == message.mType && mValue == message.mValue; 90 } 91 92 @Override hashCode()93 public int hashCode() { 94 return Objects.hash(mType, mValue); 95 } 96 97 @Override toString()98 public String toString() { 99 return "Message{" + "mType=" + messageToString(mType) +", mValue=" 100 + valueToString(mType, mValue) + '}'; 101 } 102 } 103 104 private boolean mIsNegotiated; 105 private boolean mIsNegotiationAttempted; 106 private TransportProtocol mActiveTransport; 107 private List<TransportProtocol> mTransportProtocols = new ArrayList<>(); 108 private Callback mCallback; 109 Communicator(@onNull List<TransportProtocol> transportProtocols, @NonNull Callback callback)110 public Communicator(@NonNull List<TransportProtocol> transportProtocols, 111 @NonNull Callback callback) { 112 Log.i(this, "Initializing communicator with transports: %s", 113 transportProtocols.stream().map(p -> p.getClass().getSimpleName()).collect( 114 Collectors.joining(","))); 115 mTransportProtocols.addAll(transportProtocols); 116 mTransportProtocols.forEach(p -> p.setCallback(this)); 117 mCallback = callback; 118 } 119 120 /** 121 * @return the active {@link TransportProtocol} which is being used for sending/receiving 122 * messages. 123 */ getActiveTransport()124 public @Nullable TransportProtocol getActiveTransport() { 125 return mActiveTransport; 126 } 127 128 /** 129 * Handles state changes for a call. 130 * @param id The call in question. 131 * @param state The new state. 132 */ onStateChanged(String id, @Connection.ConnectionState int state)133 public void onStateChanged(String id, @Connection.ConnectionState int state) { 134 Log.i(this, "onStateChanged: id=%s, newState=%d", id, state); 135 if (state == Connection.STATE_ACTIVE) { 136 // Protocol negotiation can start as we are active 137 if (mActiveTransport == null && !mIsNegotiationAttempted) { 138 mIsNegotiated = false; 139 mIsNegotiationAttempted = true; 140 Log.i(this, "onStateChanged: call active; negotiate D2D."); 141 negotiateNextProtocol(); 142 } 143 } 144 } 145 146 /** 147 * Called by a {@link TransportProtocol} when negotiation of that protocol has succeeded. 148 * @param protocol The protocol. 149 */ 150 @Override onNegotiationSuccess(@onNull TransportProtocol protocol)151 public void onNegotiationSuccess(@NonNull TransportProtocol protocol) { 152 if (protocol != mActiveTransport) { 153 // Uh oh, shouldn't happen. 154 String activeTransportName = mActiveTransport == null ? "none" 155 : mActiveTransport.getClass().getSimpleName(); 156 Log.w(this, "onNegotiationSuccess: ignored - %s negotiated but active transport is %s.", 157 protocol.getClass().getSimpleName(), activeTransportName); 158 } 159 Log.i(this, "onNegotiationSuccess: %s negotiated; setting active.", 160 protocol.getClass().getSimpleName()); 161 mIsNegotiated = true; 162 notifyD2DStatus(true /* isAvailable */); 163 } 164 165 /** 166 * Called by a {@link TransportProtocol} when negotiation of that protocol has failed. 167 * @param protocol The protocol. 168 */ 169 @Override onNegotiationFailed(@onNull TransportProtocol protocol)170 public void onNegotiationFailed(@NonNull TransportProtocol protocol) { 171 if (protocol != mActiveTransport) { 172 // Uh oh, shouldn't happen. 173 } 174 Log.i(this, "onNegotiationFailed: %s failed to negotiate.", 175 protocol.getClass().getSimpleName()); 176 mIsNegotiated = false; 177 negotiateNextProtocol(); 178 } 179 180 /** 181 * Called by a {@link TransportProtocol} to report incoming messages received via that 182 * transport. 183 * @param messages The received messages. 184 */ 185 @Override onMessagesReceived(@onNull Set<Message> messages)186 public void onMessagesReceived(@NonNull Set<Message> messages) { 187 if (mCallback != null) { 188 mCallback.onMessagesReceived(messages); 189 } 190 } 191 192 /** 193 * Use the {@link Communicator} to send a set of device-to-device messages. 194 * @param messages The {@link Message}s to send. 195 */ sendMessages(@onNull Set<Message> messages)196 public void sendMessages(@NonNull Set<Message> messages) { 197 if (mActiveTransport == null || !mIsNegotiated) { 198 Log.w(this, "sendMessages: no active transport"); 199 return; 200 } 201 202 Log.i(this, "sendMessages: msgs=%d, activeTransport=%s", 203 messages.size(), mActiveTransport.getClass().getSimpleName()); 204 mActiveTransport.sendMessages(messages); 205 } 206 207 /** 208 * Find a new protocol to use and start negotiation. 209 */ negotiateNextProtocol()210 private void negotiateNextProtocol() { 211 mActiveTransport = getNextCandidateProtocol(); 212 if (mActiveTransport == null) { 213 // No more protocols, exit. 214 Log.i(this, "negotiateNextProtocol: no remaining transports."); 215 notifyD2DStatus(false /* isAvailable */); 216 return; 217 } 218 Log.i(this, "negotiateNextProtocol: trying %s", 219 mActiveTransport.getClass().getSimpleName()); 220 mActiveTransport.startNegotiation(); 221 } 222 223 /** 224 * @return the next protocol to attempt to use. If there is no active protocol, use the first 225 * one; otherwise use the one after the currently active one. 226 */ getNextCandidateProtocol()227 private TransportProtocol getNextCandidateProtocol() { 228 TransportProtocol candidateProtocol = null; 229 if (mActiveTransport == null) { 230 if (mTransportProtocols.size() > 0) { 231 candidateProtocol = mTransportProtocols.get(0); 232 } else { 233 mIsNegotiated = false; 234 } 235 } else { 236 for (int ix = 0; ix < mTransportProtocols.size(); ix++) { 237 TransportProtocol protocol = mTransportProtocols.get(ix); 238 if (protocol == mActiveTransport) { 239 if (ix + 1 < mTransportProtocols.size()) { 240 // Next one is candidate 241 candidateProtocol = mTransportProtocols.get(ix + 1); 242 } 243 break; 244 } 245 } 246 } 247 return candidateProtocol; 248 } 249 250 /** 251 * Notifies listeners (okay, {@link com.android.services.telephony.TelephonyConnection} when 252 * the availability of D2D communication changes. 253 * @param isAvailable {@code true} if D2D is available, {@code false} otherwise. 254 */ notifyD2DStatus(boolean isAvailable)255 private void notifyD2DStatus(boolean isAvailable) { 256 if (mCallback != null) { 257 mCallback.onD2DAvailabilitychanged(isAvailable); 258 } 259 } 260 messageToString(int messageType)261 public static String messageToString(int messageType) { 262 switch (messageType) { 263 case MESSAGE_CALL_RADIO_ACCESS_TYPE: 264 return "MESSAGE_CALL_RADIO_ACCESS_TYPE"; 265 case MESSAGE_CALL_AUDIO_CODEC: 266 return "MESSAGE_CALL_AUDIO_CODEC"; 267 case MESSAGE_DEVICE_BATTERY_STATE: 268 return "MESSAGE_DEVICE_BATTERY_STATE"; 269 case MESSAGE_DEVICE_NETWORK_COVERAGE: 270 return "MESSAGE_DEVICE_NETWORK_COVERAGE"; 271 } 272 return ""; 273 } 274 valueToString(int messageType, int value)275 public static String valueToString(int messageType, int value) { 276 switch (messageType) { 277 case MESSAGE_CALL_RADIO_ACCESS_TYPE: 278 switch (value) { 279 case RADIO_ACCESS_TYPE_LTE: 280 return "RADIO_ACCESS_TYPE_LTE"; 281 case RADIO_ACCESS_TYPE_IWLAN: 282 return "RADIO_ACCESS_TYPE_IWLAN"; 283 case RADIO_ACCESS_TYPE_NR: 284 return "RADIO_ACCESS_TYPE_NR"; 285 } 286 return ""; 287 case MESSAGE_CALL_AUDIO_CODEC: 288 switch (value) { 289 case AUDIO_CODEC_EVS: 290 return "AUDIO_CODEC_EVS"; 291 case AUDIO_CODEC_AMR_WB: 292 return "AUDIO_CODEC_AMR_WB"; 293 case AUDIO_CODEC_AMR_NB: 294 return "AUDIO_CODEC_AMR_NB"; 295 } 296 return ""; 297 case MESSAGE_DEVICE_BATTERY_STATE: 298 switch (value) { 299 case BATTERY_STATE_LOW: 300 return "BATTERY_STATE_LOW"; 301 case BATTERY_STATE_GOOD: 302 return "BATTERY_STATE_GOOD"; 303 case BATTERY_STATE_CHARGING: 304 return "BATTERY_STATE_CHARGING"; 305 } 306 return ""; 307 case MESSAGE_DEVICE_NETWORK_COVERAGE: 308 switch (value) { 309 case COVERAGE_POOR: 310 return "COVERAGE_POOR"; 311 case COVERAGE_GOOD: 312 return "COVERAGE_GOOD"; 313 } 314 return ""; 315 } 316 return ""; 317 } 318 319 /** 320 * Test method used to force a transport type to be the active transport. 321 * @param transport The transport to activate. 322 */ setTransportActive(@onNull String transport)323 public void setTransportActive(@NonNull String transport) { 324 Optional<TransportProtocol> tp = mTransportProtocols.stream() 325 .filter(t -> t.getClass().getSimpleName().equals(transport)) 326 .findFirst(); 327 if (!tp.isPresent()) { 328 Log.w(this, "setTransportActive: %s is not a valid transport."); 329 return; 330 } 331 332 mTransportProtocols.stream() 333 .filter(p -> p != tp.get()) 334 .forEach(t -> t.forceNotNegotiated()); 335 tp.get().forceNegotiated(); 336 mActiveTransport = tp.get(); 337 mIsNegotiated = true; 338 Log.i(this, "setTransportActive: %s has been forced active.", transport); 339 } 340 341 /** 342 * @return the list of {@link TransportProtocol} which are configured at the current time. 343 */ getTransportProtocols()344 public @NonNull List<TransportProtocol> getTransportProtocols() { 345 return mTransportProtocols; 346 } 347 } 348