rocks.xmpp.extensions.rtt.OutboundRealTimeMessage Maven / Gradle / Ivy
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2016 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.extensions.rtt;
import rocks.xmpp.core.session.SendTask;
import rocks.xmpp.core.stanza.model.Message;
import rocks.xmpp.extensions.rtt.model.RealTimeText;
import rocks.xmpp.im.chat.Chat;
import rocks.xmpp.util.XmppUtils;
import java.text.Normalizer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* An outbound real-time message.
*
* @author Christian Schudt
*/
public final class OutboundRealTimeMessage extends RealTimeMessage {
private final Collection actions = new ArrayDeque<>();
private final Chat chat;
private final ScheduledExecutorService transmissionExecutor;
private CharSequence text;
private ScheduledFuture> nextRefresh;
private ScheduledFuture> nextTransmission;
private long lastTextChange;
/**
* The message refresh SHOULD be transmitted at intervals during active typing or composing. The RECOMMENDED interval is 10 seconds.
*/
private final long refreshInterval;
/**
* For the best balance between interoperability and usability, the default transmission interval of {@code } elements for a continuously-changing message SHOULD be approximately 700 milliseconds.
*/
private final long transmissionInterval;
private boolean isNew = true;
/**
* @param chat The chat.
* @param id The message id.
* @param transmissionInterval The message refresh SHOULD be transmitted at intervals during active typing or composing. The RECOMMENDED interval is 10 seconds.
* @param refreshInterval For the best balance between interoperability and usability, the default transmission interval of {@code } elements for a continuously-changing message SHOULD be approximately 700 milliseconds.
*/
OutboundRealTimeMessage(Chat chat, String id, long transmissionInterval, long refreshInterval) {
this.chat = chat;
this.id = id;
this.transmissionInterval = transmissionInterval;
this.refreshInterval = refreshInterval;
// Set up two executors, which periodically send RTT messages and "refresh messages".
transmissionExecutor = Executors.newSingleThreadScheduledExecutor(XmppUtils.createNamedThreadFactory("Real-time Text Transmission Thread"));
// This executor periodically sends RTT messages in the preferred transmission interval.
nextTransmission = transmissionExecutor.schedule(new Runnable() {
@Override
public void run() {
synchronized (OutboundRealTimeMessage.this) {
if (!actions.isEmpty()) {
// If these are the first actions being sent, schedule a refresh message.
if (isNew) {
OutboundRealTimeMessage.this.sequence.set(generateSequenceNumber());
// This executor periodically sends "message refreshes" (4.7.3 Message Refresh)
nextRefresh = transmissionExecutor.schedule(new Runnable() {
@Override
public void run() {
synchronized (OutboundRealTimeMessage.this) {
// To save bandwidth, message refreshes SHOULD NOT occur continuously while the sender is idle.
if (System.currentTimeMillis() - lastTextChange < refreshInterval) {
reset();
}
// Reschedule
nextRefresh = transmissionExecutor.schedule(this, refreshInterval, TimeUnit.MILLISECONDS);
}
}
}, refreshInterval, TimeUnit.MILLISECONDS);
}
// 2. During every Transmission Interval, all buffered action elements are transmitted in element in a stanza. This is equivalent to transmitting a small sequence of typing at a time.
sendRttMessage(isNew ? RealTimeText.Event.NEW : RealTimeText.Event.EDIT);
isNew = false;
}
nextTransmission = transmissionExecutor.schedule(this, transmissionInterval, TimeUnit.MILLISECONDS);
// 3. If there are no message changes occurring, no unnecessary transmission takes place.
}
}
}, transmissionInterval, TimeUnit.MILLISECONDS);
}
/**
* Generates the starting value for the sequence number.
*
* Sender clients MAY use any new starting value for 'seq' when initializing a real-time message using event="new" or event="reset". Recipient clients receiving such elements MUST use this 'seq' value as the new starting value. A random starting value is RECOMMENDED to improve reliability of Keeping Real-Time Text Synchronized during Usage with Multi-User Chat and Simultaneous Logins.
*
*
* @return The sequence number.
*/
static int generateSequenceNumber() {
return ThreadLocalRandom.current().nextInt(100000);
}
/**
* Computes the action elements by comparing the old and the new text.
*
* @param oldText The old text.
* @param newText The new text.
* @return The actions.
*/
static List computeActionElements(CharSequence oldText, CharSequence newText) {
if (oldText == null && newText == null || oldText != null && oldText.equals(newText) || oldText == null && newText.length() == 0 || newText == null && oldText.length() == 0) {
return Collections.emptyList();
}
List actions = new ArrayList<>();
if (oldText == null) {
actions.add(new RealTimeText.InsertText(newText));
} else if (newText == null) {
actions.add(new RealTimeText.EraseText(oldText.length(), oldText.length()));
} else {
// In order to calculate what text changes took place, the first changed character and the last changed character are determined.
int[] bounds = determineBounds(oldText, newText);
int firstChangedCharacter = bounds[0];
int lastChangedCharacter = bounds[1];
int n = Character.codePointCount(oldText, firstChangedCharacter, lastChangedCharacter);
if (n > 0) {
actions.add(new RealTimeText.EraseText(n == 1 ? null : n, lastChangedCharacter == oldText.length() ? null : Character.codePointCount(oldText, 0, lastChangedCharacter)));
}
int endIndex = newText.length() - oldText.length() + lastChangedCharacter;
if (endIndex > firstChangedCharacter) {
actions.add(new RealTimeText.InsertText(newText.subSequence(firstChangedCharacter, endIndex), firstChangedCharacter == oldText.length() ? null : Character.codePointCount(oldText, 0, firstChangedCharacter)));
}
}
return actions;
}
/**
* Determines the first and last changed character of a string by comparing it to another string.
*
* @param oldText The old text.
* @param newText The new text.
* @return An array with two values containing the first and last changed character.
*/
static int[] determineBounds(CharSequence oldText, CharSequence newText) {
// In order to calculate what text changes took place, the first changed character and the last changed character are determined.
int firstChangedCharacter = 0;
while (firstChangedCharacter < oldText.length() && firstChangedCharacter < newText.length() && oldText.charAt(firstChangedCharacter) == newText.charAt(firstChangedCharacter)) {
firstChangedCharacter++;
}
int lastChangedCharacter = 0;
while (lastChangedCharacter < oldText.length()
&& lastChangedCharacter < newText.length()
&& firstChangedCharacter < newText.length()
&& firstChangedCharacter < oldText.length() - lastChangedCharacter
&& firstChangedCharacter < newText.length() - lastChangedCharacter
&& oldText.charAt(oldText.length() - 1 - lastChangedCharacter) == newText.charAt(newText.length() - 1 - lastChangedCharacter)) {
lastChangedCharacter++;
}
return new int[]{firstChangedCharacter, oldText.length() - lastChangedCharacter};
}
/**
* Updates the text. The passed text is the complete text of the text field / text area.
* Action elements are computed automatically and are sent to the recipient.
*
* @param text The text.
*/
public synchronized final void update(CharSequence text) {
if (complete) {
throw new IllegalStateException("Real-time message is already completed.");
}
// 1. Monitor for text changes in the sender’s message. Whenever a text change event occurs, compute action element(s) and append these action element(s) to a buffer.
long now = System.currentTimeMillis();
if (!actions.isEmpty() && now != lastTextChange) {
actions.add(new RealTimeText.WaitInterval(now - lastTextChange));
}
this.lastTextChange = now;
// Pre-processing before generating real-time text includes Unicode normalization,
// conversion of emoticons graphics to text, removal of illegal characters, line-break conversion,
// and any other necessary text modifications. For Unicode normalization,
// sender clients SHOULD ensure the message is in Unicode Normalization Form C [14] ("NFC")
// For the purpose of calculating Attribute Values, any line breaks MUST be treated as a single character.
text = Normalizer.normalize(text, Normalizer.Form.NFC).replace("\r\n", "\n");
actions.addAll(computeActionElements(this.text, text));
this.text = text;
}
/**
* Sends a message refresh. A new sequence id is generated and the current text is sent.
* This method is usually called automatically in during the refresh interval.
*
* @see 4.7.3 Message Refresh
*/
public synchronized final void reset() {
reset(null, text);
}
/**
* Sends a message refresh, if you want to switch the message, which is being edited.
* Use this method, if you are composing a new message and want to switch to another (previous) message.
*
* @param id The message id for the message which is edited.
* @param text The text to reset this message to.
* @see 7.5.3 Usage with Last Message Correction
*/
public synchronized final void reset(String id, CharSequence text) {
// Senders clients need to transmit a Message Refresh when transmitting for a different message than the previously transmitted (i.e., the value of the 'id' attribute changes, 'id' becomes included, or 'id' becomes not included). This keeps real-time text synchronized when beginning to edit a previously delivered message versus continuing to compose a new message.
this.id = id;
this.text = text;
// Generate a new sequence number for every message refresh.
this.sequence.set(generateSequenceNumber());
// Drop every outgoing actions, which are scheduled for the next transmission interval, because we reset the whole text.
actions.clear();
actions.add(new RealTimeText.InsertText(text));
sendRttMessage(RealTimeText.Event.RESET);
}
@Override
public synchronized final String getText() {
return text != null ? text.toString() : "";
}
/**
* Commits the real-time message.
*
* @return The final message.
*/
public final SendTask commit() {
if (complete) {
throw new IllegalStateException("Already committed.");
}
SendTask message = chat.sendMessage(getText());
complete = true;
synchronized (this) {
if (nextRefresh != null) {
nextRefresh.cancel(false);
}
if (nextTransmission != null) {
nextTransmission.cancel(false);
}
}
transmissionExecutor.shutdown();
return message;
}
/**
* Sends the RTT message.
*
* @param event The event type.
*/
private void sendRttMessage(RealTimeText.Event event) {
Message message = new Message();
RealTimeText realTimeText = new RealTimeText(event, actions, this.sequence.getAndIncrement(), id);
message.addExtension(realTimeText);
chat.sendMessage(message);
actions.clear();
}
/**
* Gets the refresh interval, after which a refresh message is sent to ensure real-time text is kept in sync. The default is 10 seconds.
*
* @return The refresh interval.
* @see 4.7.3 Message Refresh
*/
public final long getRefreshInterval() {
return refreshInterval;
}
/**
* Gets the transmission interval of real-time text. The default is 700 milliseconds.
*
* @return The refresh interval.
* @see 4.5 Transmission Interval
*/
public final long getTransmissionInterval() {
return transmissionInterval;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy