| /* |
| * 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() { |
| |
| } |
| } |