blob: de26037c3cab813481be7d10e295ec24f2717d00 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.telephony.d2d;
import android.annotation.NonNull;
import android.telecom.Log;
import android.util.ArraySet;
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.BiMap;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Implements a DTMF-based transport for use with device-to-device communication.
*
* The DTMF-based transport is negotiated using a probe digit sent and confirmed by the remote side
* of the call.
* The {@link #DMTF_PROTOCOL_VERSION} message is sent upon initiation of the negotiation process.
* The protocol
* is considered negotiated if a valid negotiation sequence consisting of A[ABC]+D is received,
* where the [ABC]+ digits represent the protocol version.
*
* Note: Per RFC2833, transmission of DTMF digits provides a level of guarantee that where DTMF
* digits can be transmitted between two devices that they will be transmitted successfully. Thus
* the digits only need to be sent once to see if the other party can potentially use DTMF as a
* transport mechanism.
*
* The {@link #DMTF_PROTOCOL_VERSION} is used to negotiate D2D communication using DTMF. The
* message format assumes new message types and values can be added in the future; there is an
* assumption that if one side of the D2D communication pathway is unable to handle messages, it
* will ignore them. The DTMF protocol version sent as the probe is used to help each side respond
* to changes in the protocol over time.
*
* Protocol format:
* The DTMF protocol follows the following regular expression:
* ^A[ABC]+D[ABC]+D$
* A - start of message indicator, {@link #DTMF_MESSAGE_START}
* [ABC]+ - one or more message type digits [A-C]
* D - message delimiter {@link #DTMF_MESSAGE_DELIMITER} separating the type and value.
* [ABC]+ - one or more message value digits [A-C]
* D - end message delimiter {@link #DTMF_MESSAGE_DELIMITER}
*
* Valid message types and values are:
* A - call RAT
* ....A - LTE
* ....B - IWLAN
* ....C - NR
* B - call Audio codec
* ....A - EVS
* ....B - AMR-WB
* ....C - AMR-NB
* C - device battery level
* ....A - low
* ....B - good
* ....C - charging
* AA - device service state
* ....A - good
* ....B - poor
*
* Example message:
* Call RAT - NR --> A AD CD
* Service State - poor --> A AAD BD
*/
public class DtmfTransport implements TransportProtocol {
/**
* The DTMF probe and version string.
* Can be a string consisting of characters A-C.
* Thus, the current version is A, and following versions are B, C, AA, AB, AC, BC, etc.
*/
public static final String DMTF_PROTOCOL_VERSION = "A";
/**
* All DTMF messages start with this digit.
*/
public static final char DTMF_MESSAGE_START = 'A';
/**
* Delimits components of a DTMF message and also terminates a message.
*/
public static final char DTMF_MESSAGE_DELIMITER = 'D';
/**
* The full DTMF probe message including the start digit, probe/version digit(s) and the message
* delimiter.
*/
public static final String DMTF_PROBE_MESSAGE = DTMF_MESSAGE_START + DMTF_PROTOCOL_VERSION
+ DTMF_MESSAGE_DELIMITER;
public static final String DTMF_MESSAGE_RAT = "A";
public static final String DTMF_MESSAGE_RAT_LTE = "A";
public static final String DTMF_MESSAGE_RAT_IWLAN = "B";
public static final String DTMF_MESSAGE_RAT_NR = "C";
public static final String DTMF_MESSAGE_CODEC = "B";
public static final String DTMF_MESSAGE_CODEC_EVS = "A";
public static final String DTMF_MESSAGE_CODEC_AMR_WB = "B";
public static final String DTMF_MESSAGE_CODEC_AMR_NB = "C";
public static final String DTMF_MESSAGE_BATERY = "C";
public static final String DTMF_MESSAGE_BATTERY_LOW = "A";
public static final String DTMF_MESSAGE_BATTERY_GOOD = "B";
public static final String DTMF_MESSAGE_BATTERY_CHARGING = "C";
public static final String DTMF_MESSAGE_SERVICE = "AA";
public static final String DTMF_MESSAGE_SERVICE_GOOD = "A";
public static final String DTMF_MESSAGE_SERVICE_POOR = "B";
public static final BiMap<Pair<String, String>, Communicator.Message> DTMF_TO_MESSAGE =
new BiMap<>();
static {
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_RAT, DTMF_MESSAGE_RAT_LTE),
new Communicator.Message(Communicator.MESSAGE_CALL_RADIO_ACCESS_TYPE,
Communicator.RADIO_ACCESS_TYPE_LTE));
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_RAT, DTMF_MESSAGE_RAT_IWLAN),
new Communicator.Message(Communicator.MESSAGE_CALL_RADIO_ACCESS_TYPE,
Communicator.RADIO_ACCESS_TYPE_IWLAN));
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_RAT, DTMF_MESSAGE_RAT_NR),
new Communicator.Message(Communicator.MESSAGE_CALL_RADIO_ACCESS_TYPE,
Communicator.RADIO_ACCESS_TYPE_NR));
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_CODEC, DTMF_MESSAGE_CODEC_EVS),
new Communicator.Message(Communicator.MESSAGE_CALL_AUDIO_CODEC,
Communicator.AUDIO_CODEC_EVS));
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_CODEC, DTMF_MESSAGE_CODEC_AMR_WB),
new Communicator.Message(Communicator.MESSAGE_CALL_AUDIO_CODEC,
Communicator.AUDIO_CODEC_AMR_WB));
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_CODEC, DTMF_MESSAGE_CODEC_AMR_NB),
new Communicator.Message(Communicator.MESSAGE_CALL_AUDIO_CODEC,
Communicator.AUDIO_CODEC_AMR_NB));
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_BATERY, DTMF_MESSAGE_BATTERY_LOW),
new Communicator.Message(Communicator.MESSAGE_DEVICE_BATTERY_STATE,
Communicator.BATTERY_STATE_LOW));
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_BATERY, DTMF_MESSAGE_BATTERY_GOOD),
new Communicator.Message(Communicator.MESSAGE_DEVICE_BATTERY_STATE,
Communicator.BATTERY_STATE_GOOD));
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_BATERY, DTMF_MESSAGE_BATTERY_CHARGING),
new Communicator.Message(Communicator.MESSAGE_DEVICE_BATTERY_STATE,
Communicator.BATTERY_STATE_CHARGING));
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_SERVICE, DTMF_MESSAGE_SERVICE_GOOD),
new Communicator.Message(Communicator.MESSAGE_DEVICE_NETWORK_COVERAGE,
Communicator.COVERAGE_GOOD));
DTMF_TO_MESSAGE.put(new Pair<>(DTMF_MESSAGE_SERVICE, DTMF_MESSAGE_SERVICE_POOR),
new Communicator.Message(Communicator.MESSAGE_DEVICE_NETWORK_COVERAGE,
Communicator.COVERAGE_POOR));
}
public static final int STATE_IDLE = 0;
public static final int STATE_NEGOTIATING = 1;
public static final int STATE_NEGOTIATED = 2;
public static final int STATE_NEGOTIATION_FAILED = 3;
/**
* Indicates no message is being received yet.
*/
public static final int RECEIVE_STATE_IDLE = 0;
/**
* Indicates receive of the message type is underway.
*/
public static final int RECEIVE_STATE_MESSAGE_TYPE = 1;
/**
* Inidicates receive of the message value is underway.
*/
public static final int RECEIVE_STATE_MESSAGE_VALUE = 2;
private final DtmfAdapter mDtmfAdapter;
private final long mIntervalBetweenDigitsMillis;
private final long mDurationOfDtmfMessageMillis;
private final long mDtmfDurationFuzzMillis;
private final long mNegotiationTimeoutMillis;
private final ScheduledExecutorService mScheduledExecutorService;
private TransportProtocol.Callback mCallback;
private int mTransportState = STATE_IDLE;
// The received probe digits
private StringBuffer mProbeDigits = new StringBuffer();
private String mProtocolVersion;
// Tracks the state of the received digits for an incoming message.
private int mMessageReceiveState = RECEIVE_STATE_IDLE;
private StringBuffer mMessageTypeDigits = new StringBuffer();
private StringBuffer mMessageValueDigits = new StringBuffer();
// Outgoing messages pending send.
private final ConcurrentLinkedQueue<char[]> mPendingMessages = new ConcurrentLinkedQueue<>();
// Locks to synchronize access to various data objects
private Object mProbeLock = new Object();
private Object mDtmfMessageTimeoutLock = new Object();
private Object mDigitSendLock = new Object();
private Object mNegotiationLock = new Object();
private Object mDigitsLock = new Object();
private ScheduledFuture<?> mNegotiationFuture;
private ScheduledFuture<?> mDigitSendScheduledFuture;
private ScheduledFuture<?> mDtmfMessageTimeoutFuture;
private char[] mMessageToSend;
private int mCharToSend = 0;
private Random mRandom = new Random();
public DtmfTransport(@NonNull DtmfAdapter dtmfAdapter, Timeouts.Adapter timeoutsAdapter,
ScheduledExecutorService executorService) {
mDtmfAdapter = dtmfAdapter;
mIntervalBetweenDigitsMillis = timeoutsAdapter.getDtmfMinimumIntervalMillis();
mDurationOfDtmfMessageMillis = timeoutsAdapter.getMaxDurationOfDtmfMessageMillis();
mDtmfDurationFuzzMillis = timeoutsAdapter.getDtmfDurationFuzzMillis();
mNegotiationTimeoutMillis = timeoutsAdapter.getDtmfNegotiationTimeoutMillis();
mScheduledExecutorService = executorService;
}
@Override
public void setCallback(Callback callback) {
mCallback = callback;
}
@Override
public void startNegotiation() {
if (mTransportState != STATE_IDLE) {
Log.w(this, "startNegotiation: can't start negotiation as not idle.");
return;
}
mTransportState = STATE_NEGOTIATING;
Log.i(this, "startNegotiation: starting negotiation.");
mPendingMessages.offer(DMTF_PROBE_MESSAGE.toCharArray());
maybeScheduleMessageSend();
scheduleNegotiationTimeout();
}
/**
* Given a set of messages to send, send them using the DTMF transport.
*
* @param messages the messages to send via the transport.
*/
@Override
public void sendMessages(Set<Communicator.Message> messages) {
for (Communicator.Message msg : messages) {
char[] digits = getMessageDigits(msg);
if (digits == null) continue;
Log.i(this, "sendMessages: queueing message: %s", String.valueOf(digits));
mPendingMessages.offer(digits);
}
if (mPendingMessages.size() > 0) {
maybeScheduleMessageSend();
}
}
/**
* Checks for pending messages and schedules send of a pending message if one is available.
*/
private void maybeScheduleMessageSend() {
synchronized (mDigitSendLock) {
if (mMessageToSend == null && mDigitSendScheduledFuture == null) {
mMessageToSend = mPendingMessages.poll();
mCharToSend = 0;
if (mMessageToSend != null) {
Log.i(this, "maybeScheduleMessageSend: toSend=%s",
String.valueOf(mMessageToSend));
// Schedule the message to send; the inital delay will be
// mDurationOfDtmfMessageMillis to ensure we separate messages with an
// adequate padding of space, and mIntervalBetweenDigitsMillis will be used to
// ensure there is enough time between each digit.
mDigitSendScheduledFuture = mScheduledExecutorService.scheduleAtFixedRate(
() -> {
handleDtmfSend();
}, mDurationOfDtmfMessageMillis + getDtmfDurationFuzzMillis(),
mIntervalBetweenDigitsMillis,
TimeUnit.MILLISECONDS);
}
}
}
}
/**
* @return random fuzz factor to add when delaying initial send of a DTMF message.
*/
private long getDtmfDurationFuzzMillis() {
if (mDtmfDurationFuzzMillis == 0) {
return 0;
}
return mRandom.nextLong() % mDtmfDurationFuzzMillis;
}
/**
* Runs at fixed {@link #mIntervalBetweenDigitsMillis} intervals to send the individual DTMF
* digits in {@link #mMessageToSend}. When sending completes, the scheduled task is cancelled
* and {@link #maybeScheduleMessageSend()} is called to schedule send of any other pending
* message.
*/
private void handleDtmfSend() {
if (mCharToSend < mMessageToSend.length) {
if (mDtmfAdapter != null) {
Log.i(this, "handleDtmfSend: char=%c", mMessageToSend[mCharToSend]);
mDtmfAdapter.sendDtmf(mMessageToSend[mCharToSend]);
}
mCharToSend++;
if (mCharToSend == mMessageToSend.length) {
Log.i(this, "handleDtmfSend: done");
synchronized (mDigitSendLock) {
mMessageToSend = null;
mDigitSendScheduledFuture.cancel(false);
mDigitSendScheduledFuture = null;
// If we're still in the negotiation phase, we can hold off on sending any other
// pending messages queued up.
if (mTransportState == STATE_NEGOTIATED) {
maybeScheduleMessageSend();
}
}
}
}
}
/**
* @return the current state of the transport.
*/
public int getTransportState() {
return mTransportState;
}
/**
* Called by Telephony when a DTMF digit is received from the network.
*
* @param digit The received DTMF digit.
*/
public void onDtmfReceived(char digit) {
if (!(digit >= 'A' && digit <= 'D')) {
Log.i(this, "onDtmfReceived: digit = %c ; invalid digit; not in A-D");
return;
}
if (mTransportState == STATE_NEGOTIATING) {
synchronized(mProbeLock) {
mProbeDigits.append(digit);
}
if (digit == DTMF_MESSAGE_DELIMITER) {
Log.i(this, "onDtmfReceived: received message %s", mProbeDigits);
handleProbeMessage();
}
} else {
handleReceivedDigit(digit);
}
}
/**
* Handles a received probe message by verifying that it is in a valid format and caching the
* version number indicated.
*/
private void handleProbeMessage() {
String probe;
synchronized(mProbeLock) {
probe = mProbeDigits.toString();
if (mProbeDigits.length() > 0) {
mProbeDigits.delete(0, mProbeDigits.length());
}
}
cancelNegotiationTimeout();
if (probe.startsWith(String.valueOf(DTMF_MESSAGE_START))
&& probe.endsWith(String.valueOf(DTMF_MESSAGE_DELIMITER))
&& probe.length() > 2) {
mProtocolVersion = probe.substring(1,probe.length() - 1);
Log.i(this, "handleProbeMessage: got valid probe, remote version %s negotiated.",
probe);
negotiationSucceeded();
} else {
Log.i(this, "handleProbeMessage: got invalid probe %s - negotiation failed.", probe);
negotiationFailed();
}
cancelNegotiationTimeout();
}
/**
* Upon initiation of negotiation, schedule a timeout within which we expect to receive the
* incoming probe.
*/
private void scheduleNegotiationTimeout() {
synchronized (mNegotiationLock) {
mNegotiationFuture = mScheduledExecutorService.schedule(() -> {
handleNegotiationTimeout();
},
mNegotiationTimeoutMillis, TimeUnit.MILLISECONDS);
}
}
/**
* Cancels a pending timeout for negotiation.
*/
private void cancelNegotiationTimeout() {
Log.i(this, "cancelNegotiationTimeout");
synchronized (mNegotiationLock) {
if (mNegotiationFuture != null) {
mNegotiationFuture.cancel(false);
}
mNegotiationFuture = null;
}
}
/**
* Handle scheduled negotiation timeout.
*/
private void handleNegotiationTimeout() {
Log.i(this, "handleNegotiationTimeout: no probe received, negotiation timeout.");
synchronized (mNegotiationLock) {
mNegotiationFuture = null;
}
negotiationFailed();
}
/**
* Handle failed negotiation by changing state and informing listeners.
*/
private void negotiationFailed() {
mTransportState = STATE_NEGOTIATION_FAILED;
Log.i(this, "notifyNegotiationFailed");
if (mCallback != null) {
mCallback.onNegotiationFailed(this);
}
}
/**
* Handle successful negotiation by changing state and informing listeners.
*/
private void negotiationSucceeded() {
mTransportState = STATE_NEGOTIATED;
Log.i(this, "negotiationSucceeded");
if (mCallback != null) {
mCallback.onNegotiationSuccess(this);
}
}
/**
* Handle received DTMF digits, taking into account current protocol state.
*
* @param digit the received digit.
*/
private void handleReceivedDigit(char digit) {
if (mMessageReceiveState == RECEIVE_STATE_IDLE) {
if (digit == DTMF_MESSAGE_START) {
// First digit; start the timer
Log.i(this, "handleReceivedDigit: digit = %c ; message timeout started.", digit);
mMessageReceiveState = RECEIVE_STATE_MESSAGE_TYPE;
scheduleDtmfMessageTimeout();
} else {
Log.w(this, "handleReceivedDigit: digit = %c ; unexpected start digit, ignoring.",
digit);
}
} else if (digit == DTMF_MESSAGE_DELIMITER) {
if (mMessageReceiveState == RECEIVE_STATE_MESSAGE_TYPE) {
Log.i(this, "handleReceivedDigit: digit = %c ; msg = %s ; awaiting value.", digit,
mMessageTypeDigits.toString());
mMessageReceiveState = RECEIVE_STATE_MESSAGE_VALUE;
} else if (mMessageReceiveState == RECEIVE_STATE_MESSAGE_VALUE) {
maybeCancelDtmfMessageTimeout();
String messageType;
String messageValue;
synchronized(mDigitsLock) {
messageType = mMessageTypeDigits.toString();
messageValue = mMessageValueDigits.toString();
}
Log.i(this, "handleReceivedDigit: digit = %c ; msg = %s ; value = %s ; full msg",
digit, messageType, messageValue);
handleIncomingMessage(messageType, messageValue);
resetIncomingMessage();
}
} else {
synchronized(mDigitsLock) {
if (mMessageReceiveState == RECEIVE_STATE_MESSAGE_TYPE) {
mMessageTypeDigits.append(digit);
Log.i(this, "handleReceivedDigit: typeDigit = %c ; msg = %s",
digit, mMessageTypeDigits.toString());
} else if (mMessageReceiveState == RECEIVE_STATE_MESSAGE_VALUE) {
mMessageValueDigits.append(digit);
Log.i(this, "handleReceivedDigit: valueDigit = %c ; value = %s",
digit, mMessageValueDigits.toString());
}
}
}
}
/**
* Schedule a timeout for receiving a complete DTMF message.
*/
private void scheduleDtmfMessageTimeout() {
synchronized (mDtmfMessageTimeoutLock) {
maybeCancelDtmfMessageTimeout();
mDtmfMessageTimeoutFuture = mScheduledExecutorService.schedule(() -> {
handleDtmfMessageTimeout();
},
mDurationOfDtmfMessageMillis, TimeUnit.MILLISECONDS);
}
}
/**
* Cancels any pending DTMF message timeout scheduled with
* {@link #scheduleDtmfMessageTimeout()}.
*/
private void maybeCancelDtmfMessageTimeout() {
synchronized (mDtmfMessageTimeoutLock) {
if (mDtmfMessageTimeoutFuture != null) {
Log.i(this, "scheduleDtmfMessageTimeout: timeout pending; cancelling");
mDtmfMessageTimeoutFuture.cancel(false);
mDtmfMessageTimeoutFuture = null;
}
}
}
/**
* Called when a scheduled DTMF message timeout occurs to cleanup the incoming message and
* prepare for receiving a new message.
*/
private void handleDtmfMessageTimeout() {
maybeCancelDtmfMessageTimeout();
Log.i(this, "handleDtmfMessageTimeout: timeout receiving DTMF string; got %s/%s so far",
mMessageTypeDigits.toString(), mMessageValueDigits.toString());
resetIncomingMessage();
}
/**
* Given a {@link Communicator.Message} to send, returns a string of DTMF digits to send for
* that message. This is the complete message, including the message start, and all delimiters.
*
* @param message The message to send.
* @return The DTMF digits to send, including all delimiters.
*/
@VisibleForTesting
public char[] getMessageDigits(@NonNull Communicator.Message message) {
Pair<String, String> foundSequence = DTMF_TO_MESSAGE.getKey(message);
if (foundSequence == null) {
return null;
}
StringBuilder theMessage = new StringBuilder();
theMessage.append(DTMF_MESSAGE_START);
theMessage.append(foundSequence.first);
theMessage.append(DTMF_MESSAGE_DELIMITER);
theMessage.append(foundSequence.second);
theMessage.append(DTMF_MESSAGE_DELIMITER);
return theMessage.toString().toCharArray();
}
/**
* Translate a string of DTMF digits into a communicator message.
*
* @param message The string of DTMF digits with digit 0 being the start of the string of
* digits.
* @return The message received, or {@code null} if no valid message found.
*/
@VisibleForTesting
public Communicator.Message extractMessage(String message, String value) {
return DTMF_TO_MESSAGE.getValue(new Pair<>(message, value));
}
/**
* Handles an incoming message received via DTMF digits; notifies interested parties of the
* message using the associated callback.
*/
private void handleIncomingMessage(String message, String value) {
Communicator.Message msg = extractMessage(message, value);
if (msg == null) {
Log.w(this, "handleIncomingMessage: msgDigits = %s, msgValueDigits = %s; invalid msg",
message, value);
return;
}
Log.i(this, "handleIncomingMessage: msgDigits = %s, msgValueDigits = %s", message, value);
Set<Communicator.Message> msgs = new ArraySet<>(1);
msgs.add(msg);
if (mCallback != null) {
mCallback.onMessagesReceived(msgs);
}
}
/**
* Moves message receive state back to idle and clears received digits.
*/
private void resetIncomingMessage() {
mMessageReceiveState = RECEIVE_STATE_IDLE;
synchronized(mDigitsLock) {
if (mMessageTypeDigits.length() != 0) {
mMessageTypeDigits.delete(0, mMessageTypeDigits.length());
}
if (mMessageValueDigits.length() != 0) {
mMessageValueDigits.delete(0, mMessageValueDigits.length());
}
}
}
@Override
public void forceNegotiated() {
}
@Override
public void forceNotNegotiated() {
}
}