/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*/
package com.ubergeek42.WeechatAndroid.relay;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.ubergeek42.WeechatAndroid.service.Notificator;
import com.ubergeek42.WeechatAndroid.service.P;
import com.ubergeek42.WeechatAndroid.service.RelayService;
import com.ubergeek42.weechat.relay.RelayMessageHandler;
import com.ubergeek42.WeechatAndroid.service.RelayService.STATE;
import com.ubergeek42.weechat.relay.protocol.Array;
import com.ubergeek42.weechat.relay.protocol.Hashtable;
import com.ubergeek42.weechat.relay.protocol.Hdata;
import com.ubergeek42.weechat.relay.protocol.HdataEntry;
import com.ubergeek42.weechat.relay.protocol.RelayObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.ListIterator;
import java.util.Locale;
/** a class that holds information about buffers
** probably should be made static */
public class BufferList {
final private static Logger logger = LoggerFactory.getLogger("BufferList");
final private static boolean DEBUG_SYNCING = false;
final private static boolean DEBUG_HANDLERS = false;
private static @Nullable String filterLc = null;
private static @Nullable String filterUc = null;
private static @Nullable RelayService relay;
private static @Nullable BufferListEye buffersEye;
/** the mother variable. list of current buffers */
public static @NonNull ArrayList<Buffer> buffers = new ArrayList<>();
////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////
public static void launch(final RelayService relay) {
BufferList.relay = relay;
// handle buffer list changes
// including initial hotlist
addMessageHandler("listbuffers", bufferListWatcher);
addMessageHandler("_buffer_opened", bufferListWatcher);
addMessageHandler("_buffer_renamed", bufferListWatcher);
addMessageHandler("_buffer_title_changed", bufferListWatcher);
addMessageHandler("_buffer_localvar_added", bufferListWatcher);
addMessageHandler("_buffer_localvar_changed", bufferListWatcher);
addMessageHandler("_buffer_localvar_removed", bufferListWatcher);
addMessageHandler("_buffer_closing", bufferListWatcher);
addMessageHandler("_buffer_moved", bufferListWatcher);
addMessageHandler("_buffer_merged", bufferListWatcher);
addMessageHandler("hotlist", hotlistInitWatcher);
addMessageHandler("last_read_lines", lastReadLinesWatcher);
// handle newly arriving chat lines
// and chatlines we are reading in reverse
addMessageHandler("_buffer_line_added", newLineWatcher);
// handle nicklist init and changes
addMessageHandler("nicklist", nickListWatcher);
addMessageHandler("_nicklist", nickListWatcher);
addMessageHandler("_nicklist_diff", nickListWatcher);
// request a list of buffers current open, along with some information about them
relay.connection.sendMessage("listbuffers", "hdata", "buffer:gui_buffers(*) number,full_name,short_name,type,title,nicklist,local_variables,notify");
syncHotlist();
relay.connection.sendMessage(P.optimizeTraffic ? "sync * buffers,upgrade" : "sync");
}
public static void stop() {
relay = null;
messageHandlersMap.clear();
}
private static HashMap<String, LinkedHashSet<RelayMessageHandler>> messageHandlersMap = new HashMap<>();
private static void addMessageHandler(String id, RelayMessageHandler handler) {
LinkedHashSet<RelayMessageHandler> handlers = messageHandlersMap.get(id);
if (handlers == null) messageHandlersMap.put(id, handlers = new LinkedHashSet<>());
handlers.add(handler);
}
private static void removeMessageHandler(String id, RelayMessageHandler handler) {
LinkedHashSet<RelayMessageHandler> handlers = messageHandlersMap.get(id);
if (handlers != null) handlers.remove(handler);
}
public static void handleMessage(@Nullable RelayObject obj, String id) {
HashSet<RelayMessageHandler> handlers = messageHandlersMap.get(id);
if (handlers == null) return;
for (RelayMessageHandler handler : handlers) handler.handleMessage(obj, id);
}
/** send synchronization data to weechat and return true. if not connected, return false. */
public static boolean syncHotlist() {
if (relay == null || !relay.state.contains(STATE.AUTHENTICATED))
return false;
relay.connection.sendMessage("last_read_lines", "hdata", "buffer:gui_buffers(*)/own_lines/last_read_line/data buffer");
relay.connection.sendMessage("hotlist", "hdata", "hotlist:gui_hotlist(*) buffer,count");
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// called by the Eye
////////////////////////////////////////////////////////////////////////////////////////////////
private static @Nullable ArrayList<Buffer> sentBuffers = null;
/** returns an independent copy of the buffer list
** MIGHT return the same object (albeit sorted as needed) */
synchronized static public @NonNull ArrayList<Buffer> getBufferList() {
if (sentBuffers == null) {
sentBuffers = new ArrayList<>();
for (Buffer buffer : buffers) {
if (buffer.type == Buffer.HARD_HIDDEN) continue;
if (P.filterBuffers && buffer.type == Buffer.OTHER && buffer.highlights == 0 && buffer.unreads == 0) continue;
if (filterLc != null && filterUc != null && !buffer.fullName.toLowerCase().contains(filterLc) && !buffer.fullName.toUpperCase().contains(filterUc)) continue;
sentBuffers.add(buffer);
}
}
if (P.sortBuffers) Collections.sort(sentBuffers, sortByHotAndMessageCountComparator);
else Collections.sort(sentBuffers, sortByHotCountAndNumberComparator);
return sentBuffers;
}
static public boolean hasData() {
return buffers.size() > 0;
}
synchronized static public void setFilter(String filter) {
filterLc = (filter.length() == 0) ? null : filter.toLowerCase();
filterUc = (filter.length() == 0) ? null : filter.toUpperCase();
sentBuffers = null;
}
synchronized static public @Nullable Buffer findByFullName(@Nullable String fullName) {
if (fullName == null) return null;
for (Buffer buffer : buffers) if (buffer.fullName.equals(fullName)) return buffer;
return null;
}
/** sets or remove (using null) buffer list change watcher */
synchronized static public void setBufferListEye(@Nullable BufferListEye buffersEye) {
BufferList.buffersEye = buffersEye;
}
////////////////////////////////////////////////////////////////////////////////////////////////
/** returns a "random" hot buffer or null */
synchronized static public @Nullable Buffer getHotBuffer() {
for (Buffer buffer : buffers)
if ((buffer.type == Buffer.PRIVATE && buffer.unreads > 0) || buffer.highlights > 0)
return buffer;
return null;
}
//////////////////////////////////////////////////////////////////////////////////////////////// called on the Eye
//////////////////////////////////////////////////////////////////////////////////////////////// from this and Buffer (local)
//////////////////////////////////////////////////////////////////////////////////////////////// (also alert Buffer)
/** called when a buffer has been added or removed */
synchronized static private void notifyBuffersChanged() {
sentBuffers = null;
if (buffersEye != null) buffersEye.onBuffersChanged();
}
/** called when buffer data has been changed, but the no of buffers is the same
** otherMessagesChanged signifies if buffer type is OTHER and message count has changed
** used to temporarily display the said buffer if OTHER buffers are filtered */
synchronized static void notifyBuffersSlightlyChanged(boolean otherMessagesChanged) {
if (buffersEye != null) {
if (otherMessagesChanged && P.filterBuffers) sentBuffers = null;
buffersEye.onBuffersChanged();
}
}
synchronized static void notifyBuffersSlightlyChanged() {
notifyBuffersSlightlyChanged(false);
}
/** called when no buffers has been added or removed, but
** buffer changes are such that we should reorder the buffer list */
synchronized static private void notifyBufferPropertiesChanged(Buffer buffer) {
buffer.onPropertiesChanged();
sentBuffers = null;
if (buffersEye != null) buffersEye.onBuffersChanged();
}
/** process all open buffers and, if specified, notify them of the change
** practically notifying is only needed when pressing volume up/dn keys,
** which means we are not in the preferences window and the activity will not
** get re-rendered */
synchronized public static void notifyOpenBuffersMustBeProcessed(boolean notify) {
for (Buffer buffer : buffers)
if (buffer.isOpen) {
buffer.forceProcessAllMessages();
if (notify) buffer.onLinesChanged();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// called from Buffer & RelayService (local)
////////////////////////////////////////////////////////////////////////////////////////////////
/** send sync command to relay (if traffic is set to optimized) and
** add it to the synced buffers (=open buffers) list */
synchronized static void syncBuffer(Buffer buffer) {
if (P.optimizeTraffic) sendMessage(String.format("sync %s", buffer.hexPointer()));
}
synchronized static void desyncBuffer(Buffer buffer) {
if (P.optimizeTraffic) sendMessage(String.format("desync %s", buffer.hexPointer()));
}
private static int counter = 0;
private final static String MEOW = "(%d) hdata buffer:0x%x/own_lines/last_line(-%d)/data date,displayed,prefix,message,highlight,notify,tags_array";
public static void requestLinesForBufferByPointer(long pointer, int number) {
addMessageHandler(Integer.toString(counter), new BufferLineWatcher(counter, pointer));
sendMessage(String.format(Locale.ROOT, MEOW, counter, pointer, number));
counter++;
}
public static void requestNicklistForBufferByPointer(long pointer) {
sendMessage(String.format("(nicklist) nicklist 0x%x", pointer));
}
private static void sendMessage(String string) {
if (relay != null && relay.connection != null) relay.connection.sendMessage(string);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// hotlist stuff
////////////////////////////////////////////////////////////////////////////////////////////////
/** a list of most recent ACTUALLY RECEIVED hot messages
** each entry has a form of {"irc.free.#123", "<nick> hi there"} */
static public ArrayList<String[]> hotList = new ArrayList<>();
/** this is the value calculated from hotlist received from weechat
** it MIGHT BE GREATER than size of hotList
** initialized, it's -1, so that on service restart we know to remove notification 43 */
static private int hotCount = -1;
/** returns hot count or 0 if unknown */
static public int getHotCount() {
return hotCount == -1 ? 0 : hotCount;
}
/** called when a new new hot message just arrived */
synchronized static void newHotLine(final @NonNull Buffer buffer, final @NonNull Line line) {
hotList.add(new String[]{buffer.fullName, line.getNotificationString()});
if (processHotCountAndTellIfChanged())
notifyHotCountChanged(true);
}
/** called when buffer is read or closed
** must be called AFTER buffer hot count adjustments / buffer removal from the list */
synchronized static void removeHotMessagesForBuffer(final @NonNull Buffer buffer) {
for (Iterator<String[]> it = hotList.iterator(); it.hasNext(); )
if (it.next()[0].equals(buffer.fullName)) it.remove();
if (processHotCountAndTellIfChanged())
notifyHotCountChanged(false);
}
/** remove a number of messages for a given buffer, leaving last 'leave' messages
** DOES NOT notify anyone of the change */
synchronized static void adjustHotMessagesForBuffer(final @NonNull Buffer buffer, int leave) {
for (ListIterator<String[]> it = hotList.listIterator(hotList.size()); it.hasPrevious();) {
if (it.previous()[0].equals(buffer.fullName) && (leave-- <= 0))
it.remove();
}
}
synchronized static void onHotlistFinished() {
if (processHotCountAndTellIfChanged())
notifyHotCountChanged(false);
}
////////////////////////////////////////////////////////////////////////////////////////////////
/** HELPER. stores hotCount;
** returns true if hot count has changed */
static private boolean processHotCountAndTellIfChanged() {
int hot = 0;
for (Buffer buffer : buffers) {
hot += buffer.highlights;
if (buffer.type == Buffer.PRIVATE) hot += buffer.unreads;
}
return hotCount != (hotCount = hot); // har har
}
/** HELPER. notifies everyone interested of hotlist changes */
static private void notifyHotCountChanged(boolean newHighlight) {
Notificator.showHot(newHighlight);
if (buffersEye != null) buffersEye.onHotCountChanged();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// private stuffs
////////////////////////////////////////////////////////////////////////////////////////////////
synchronized static private @Nullable Buffer findByPointer(long pointer) {
for (Buffer buffer : buffers) if (buffer.pointer == pointer) return buffer;
return null;
}
static private final Comparator<Buffer> sortByHotCountAndNumberComparator = new Comparator<Buffer>() {
@Override public int compare(Buffer left, Buffer right) {
int l, r;
if ((l = left.highlights) != (r = right.highlights)) return r - l;
if ((l = left.type == Buffer.PRIVATE ? left.unreads : 0) !=
(r = right.type == Buffer.PRIVATE ? right.unreads : 0)) return r - l;
return left.number - right.number;
}
};
static private final Comparator<Buffer> sortByHotAndMessageCountComparator = new Comparator<Buffer>() {
@Override
public int compare(Buffer left, Buffer right) {
int l, r;
if ((l = left.highlights) != (r = right.highlights)) return r - l;
if ((l = left.type == Buffer.PRIVATE ? left.unreads : 0) !=
(r = right.type == Buffer.PRIVATE ? right.unreads : 0)) return r - l;
return right.unreads - left.unreads;
}
};
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// yay!! message handlers!! the joy
//////////////////////////////////////////////////////////////////////////////////////////////// buffer list
////////////////////////////////////////////////////////////////////////////////////////////////
static RelayMessageHandler bufferListWatcher = new RelayMessageHandler() {
final private Logger logger = LoggerFactory.getLogger("bufferListWatcher");
@Override public void handleMessage(RelayObject obj, String id) {
if (DEBUG_HANDLERS) logger.debug("handleMessage(..., {}) (hdata size = {})", id, ((Hdata) obj).getCount());
Hdata data = (Hdata) obj;
if (id.equals("listbuffers")) buffers.clear();
for (int i = 0, size = data.getCount(); i < size; i++) {
HdataEntry entry = data.getItem(i);
if (id.equals("listbuffers") || id.equals("_buffer_opened")) {
RelayObject r;
Buffer buffer = new Buffer(entry.getPointerLong(),
entry.getItem("number").asInt(),
entry.getItem("full_name").asString(),
entry.getItem("short_name").asString(),
entry.getItem("title").asString(),
((r = entry.getItem("notify")) != null) ? r.asInt() : 3, // TODO request notify level afterwards???
(Hashtable) entry.getItem("local_variables")); // TODO because _buffer_opened doesn't provide notify level
synchronized (BufferList.class) {buffers.add(buffer);}
notifyBuffersChanged();
} else {
Buffer buffer = findByPointer(entry.getPointerLong(0));
if (buffer == null) {
logger.warn("handleMessage(..., {}): buffer is not present", id);
} else {
if (id.equals("_buffer_renamed")) {
buffer.fullName = entry.getItem("full_name").asString();
String short_name = entry.getItem("short_name").asString();
buffer.shortName = (short_name != null) ? short_name : buffer.fullName;
buffer.localVars = (Hashtable) entry.getItem("local_variables");
notifyBufferPropertiesChanged(buffer);
} else if (id.equals("_buffer_title_changed")) {
buffer.title = entry.getItem("title").asString();
notifyBufferPropertiesChanged(buffer);
} else if (id.startsWith("_buffer_localvar_")) {
buffer.localVars = (Hashtable) entry.getItem("local_variables");
notifyBufferPropertiesChanged(buffer);
} else if (id.equals("_buffer_moved") || id.equals("_buffer_merged")) { // TODO if buffer is moved, reorder others?
buffer.number = entry.getItem("number").asInt(); // TODO this is not our issue; it's going to be resolved in future versions of weechat
notifyBufferPropertiesChanged(buffer);
} else if (id.equals("_buffer_closing")) {
synchronized (BufferList.class) {buffers.remove(buffer);}
buffer.onBufferClosed();
notifyBuffersChanged();
} else {
logger.warn("handleMessage(..., {}): unknown message id", id);
}
}
}
}
}
};
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// hotlist
////////////////////////////////////////////////////////////////////////////////////////////////
// last_read_lines
static RelayMessageHandler lastReadLinesWatcher = new RelayMessageHandler() {
final private Logger logger = LoggerFactory.getLogger("lastReadLinesWatcher");
@Override public void handleMessage(RelayObject obj, String id) {
if (DEBUG_HANDLERS) logger.debug("handleMessage(..., {})", id);
if (!(obj instanceof Hdata)) return;
HashMap<Long, Long> bufferToLrl = new HashMap<>();
Hdata data = (Hdata) obj;
for (int i = 0, size = data.getCount(); i < size; i++) {
HdataEntry entry = data.getItem(i);
long bufferPointer = entry.getItem("buffer").asPointerLong();
long linePointer = entry.getPointerLong();
bufferToLrl.put(bufferPointer, linePointer);
}
synchronized (BufferList.class) {
for (Buffer buffer : buffers) {
Long linePointer = bufferToLrl.get(buffer.pointer);
buffer.updateLastReadLine(linePointer == null ? -1 : linePointer);
}
}
}
};
// hotlist (ONLY)
static RelayMessageHandler hotlistInitWatcher = new RelayMessageHandler() {
final private Logger logger = LoggerFactory.getLogger("hotlistInitWatcher");
@Override public void handleMessage(RelayObject obj, String id) {
if (DEBUG_HANDLERS) logger.debug("handleMessage(..., {})", id);
if (!(obj instanceof Hdata)) return;
HashMap<Long, Array> bufferToHotlist = new HashMap<>();
Hdata data = (Hdata) obj;
for (int i = 0, size = data.getCount(); i < size; i++) {
HdataEntry entry = data.getItem(i);
long pointer = entry.getItem("buffer").asPointerLong();
Array count = entry.getItem("count").asArray();
bufferToHotlist.put(pointer, count);
}
for (Buffer buffer: buffers) {
Array count = bufferToHotlist.get(buffer.pointer);
int unreads = count == null ? 0 : count.get(1).asInt() + count.get(2).asInt(); // chat messages & private messages
int highlights = count == null ? 0 : count.get(3).asInt(); // highlights
buffer.updateHighlightsAndUnreads(highlights, unreads);
}
onHotlistFinished();
notifyBuffersSlightlyChanged();
}
};
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// line
////////////////////////////////////////////////////////////////////////////////////////////////
// _buffer_line_added
// listlines_reverse
static class BufferLineWatcher implements RelayMessageHandler {
final private Logger logger = LoggerFactory.getLogger("BufferLineWatcher");
final private long bufferPointer;
final private int id;
public BufferLineWatcher(int id, long bufferPointer) {
this.bufferPointer = bufferPointer;
this.id = id;
}
@Override public void handleMessage(RelayObject obj, String id) {
if (DEBUG_HANDLERS) logger.debug("handleMessage(..., {})", id);
if (!(obj instanceof Hdata)) return;
Hdata data = (Hdata) obj;
boolean isBottom = id.equals("_buffer_line_added");
Buffer buffer = findByPointer(isBottom ? data.getItem(0).getItem("buffer").asPointerLong() : bufferPointer);
if (buffer == null) {
logger.warn("handleMessage(..., {}): no buffer to update", id);
return;
}
for (int i = 0, size = data.getCount(); i < size; i++)
buffer.addLine(getLine(data.getItem(i)), isBottom);
if (!isBottom) {
buffer.onLinesListed();
removeMessageHandler(Integer.toString(this.id), this);
}
}
private static Line getLine(HdataEntry entry) {
String message = entry.getItem("message").asString();
String prefix = entry.getItem("prefix").asString();
boolean displayed = (entry.getItem("displayed").asChar() == 0x01);
Date time = entry.getItem("date").asTime();
RelayObject high = entry.getItem("highlight");
boolean highlight = (high != null && high.asChar() == 0x01);
RelayObject tagsobj = entry.getItem("tags_array");
String[] tags = (tagsobj != null && tagsobj.getType() == RelayObject.WType.ARR) ?
tagsobj.asArray().asStringArray() : null;
return new Line(entry.getPointerLong(), time, prefix, message, displayed, highlight, tags);
}
}
static BufferLineWatcher newLineWatcher = new BufferLineWatcher(-1, -1);
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// nicklist
////////////////////////////////////////////////////////////////////////////////////////////////
private static final char ADD = '+';
private static final char REMOVE = '-';
private static final char UPDATE = '*';
// the following two are rather the same thing. so check if it's not _diff
// nicklist
// _nicklist
// _nicklist_diff
static RelayMessageHandler nickListWatcher = new RelayMessageHandler() {
final private Logger logger = LoggerFactory.getLogger("nickListWatcher");
@Override public void handleMessage(RelayObject obj, String id) {
if (DEBUG_HANDLERS) logger.debug("handleMessage(..., {})", id);
if (!(obj instanceof Hdata)) return;
Hdata data = (Hdata) obj;
boolean diff = id.equals("_nicklist_diff");
HashSet<Buffer> renickedBuffers = new HashSet<>();
for (int i = 0, size = data.getCount(); i < size; i++) {
HdataEntry entry = data.getItem(i);
// find buffer
Buffer buffer = findByPointer(entry.getPointerLong(0));
if (buffer == null) {
if (DEBUG_HANDLERS) logger.warn("handleMessage(..., {}): no buffer to update", id);
continue;
}
// if buffer doesn't hold all nicknames yet, break execution, since full nicks will be requested anyway later
// erase nicklist if we have a full list here
if (diff && !buffer.holdsAllNicks) continue;
if (!diff && renickedBuffers.add(buffer)) buffer.removeAllNicks();
// decide whether it's adding, removing or updating nicks
// if _nicklist, treat as if we have _diff = '+'
char command = (diff) ? entry.getItem("_diff").asChar() : ADD;
// do the job, but
// care only for items that are visible (e.g. not 'root')
// and that are not grouping items
if (command == ADD || command == UPDATE) {
if (entry.getItem("visible").asChar() != 0 && entry.getItem("group").asChar() != 1) {
long pointer = entry.getPointerLong();
String prefix = entry.getItem("prefix").asString();
String name = entry.getItem("name").asString();
boolean away = entry.getItem("color").asString().contains("weechat.color.nicklist_away");
if (command == ADD)
buffer.addNick(pointer, prefix, name, away);
else
buffer.updateNick(pointer, prefix, name, away);
}
} else if (command == REMOVE) {
buffer.removeNick(entry.getPointerLong());
}
}
// sort nicknames when we receive them for the very first time
if (id.equals("nicklist"))
for (Buffer buffer : renickedBuffers) {
buffer.holdsAllNicks = true;
buffer.sortNicksByLines();
}
}
};
}