aboutsummaryrefslogtreecommitdiff
path: root/src/java/com/android/internal/telephony/d2d/DtmfTransport.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/java/com/android/internal/telephony/d2d/DtmfTransport.java')
-rw-r--r--src/java/com/android/internal/telephony/d2d/DtmfTransport.java624
1 files changed, 624 insertions, 0 deletions
diff --git a/src/java/com/android/internal/telephony/d2d/DtmfTransport.java b/src/java/com/android/internal/telephony/d2d/DtmfTransport.java
new file mode 100644
index 0000000000..de26037c3c
--- /dev/null
+++ b/src/java/com/android/internal/telephony/d2d/DtmfTransport.java
@@ -0,0 +1,624 @@
+/*
+ * 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() {
+
+ }
+}