/* copyright This file is part of JKomasto2. Written in 2022 by Usawashi This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . copyright */ import java.util.List; import java.util.ArrayList; import java.io.IOException; import cafe.biskuteri.hinoki.Tree; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Clip; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.UnsupportedAudioFileException; import javax.sound.sampled.LineUnavailableException; import java.net.URL; class WindowUpdater { private JKomasto primaire; private MastodonApi api; // - -%- - private List timelineWindows; private List notificationWindows; private Clip notificationSound; private Connection publicConn, userConn; // ---%-@-%--- public synchronized void add(TimelineWindow updatee) { if (!timelineWindows.contains(updatee)) timelineWindows.add(updatee); publicConn.reevaluate(); userConn.reevaluate(); } public synchronized void add(NotificationsWindow updatee) { if (!notificationWindows.contains(updatee)) notificationWindows.add(updatee); userConn.reevaluate(); } public synchronized void remove(TimelineWindow updatee) { timelineWindows.remove(updatee); publicConn.reevaluate(); userConn.reevaluate(); } public synchronized void remove(NotificationsWindow updatee) { notificationWindows.remove(updatee); userConn.reevaluate(); } // - -%- - public static void printStackTrace(Thread thread) { for (StackTraceElement e: thread.getStackTrace()) System.err.println(e); } // ---%-@-%--- private class Connection implements Runnable, ServerSideEventsListener { private TimelineType type; // -=- private Thread thread; private boolean stopping; private StringBuilder event, data; // -=%=- public void start() { stopping = false; thread = new Thread(this); thread.setDaemon(true); try { synchronized (thread) { thread.start(); thread.wait(); } } catch (InterruptedException eIt) { assert false; } } public void stop() { stopping = true; thread.interrupt(); try { thread.join(3000); /* * That thread should notice it is * interrupted ppromptly, and close. */ if (thread.isAlive()) printStackTrace(thread); } catch (InterruptedException eIt) { assert false; } thread = null; } public void reevaluate() { boolean hasUpdatee = false; for (NotificationsWindow w: notificationWindows) if (responsibleFor(w)) hasUpdatee = true; for (TimelineWindow w: timelineWindows) if (responsibleFor(w)) hasUpdatee = true; if (!hasUpdatee && thread != null) stop(); if (hasUpdatee && thread == null) start(); } // -=- public void run() { synchronized (thread) { thread.notifyAll(); } event = new StringBuilder(); data = new StringBuilder(); api.monitorTimeline(type, this); // monitorTimeline should not return until // the connection is closed, or this thread // is interrupted. System.err.println( "Stopped monitoring." + thread + " " + Thread.currentThread() ); if (thread == Thread.currentThread()) thread = null; /* * This isn't thread safe. But I'd like the * restart after sleep mode, so. */ } public void lineReceived(String line) { if (line.startsWith(":")) return; if (line.isEmpty()) { handle(event.toString(), data.toString()); event.delete(0, event.length()); data.delete(0, event.length()); } if (line.startsWith("data: ")) data.append(line.substring("data: ".length())); if (line.startsWith("event: ")) event.append(line.substring("event: ".length())); /* * Note that I ignore https://html.spec.whatwg.org * /multipage/server-sent-events.html#dispatchMessage. * That is because I am not a browser. */ } private void handle(String event, String data) { assert !data.isEmpty(); if (event.isEmpty()) return; boolean newPost = event.equals("update"); boolean newNotif = event.equals("notification"); if (!(newPost || newNotif)) return; if (newNotif) { notificationSound.setFramePosition(0); notificationSound.start(); } synchronized (WindowUpdater.this) { for (TimelineWindow w: timelineWindows) { if (!responsibleFor(w)) continue; w.refresh(); } } synchronized (WindowUpdater.this) { for (NotificationsWindow w: notificationWindows) { if (!responsibleFor(w)) continue; w.refresh(); } } } private boolean responsibleFor(TimelineWindow updatee) { return type == updatee.getTimelineType(); } private boolean responsibleFor(NotificationsWindow updatee) { return type == TimelineType.HOME; } public void connectionFailed(IOException eIo) { // sais pas dois-je faire.. eIo.printStackTrace(); } public void requestFailed(int httpCode, Tree json) { // mo shiranu System.err.println(httpCode + ", " + json.get("error").value); } } // ---%-@-%--- WindowUpdater(JKomasto primaire) { this.primaire = primaire; this.api = primaire.getMastodonApi(); this.timelineWindows = new ArrayList<>(); this.notificationWindows = new ArrayList<>(); publicConn = new Connection(); publicConn.type = TimelineType.FEDERATED; userConn = new Connection(); userConn.type = TimelineType.HOME; loadNotificationSound(); } void loadNotificationSound() { URL url = getClass().getResource("KDE_Dialog_Appear.wav"); try { Clip clip = AudioSystem.getClip(); clip.open(AudioSystem.getAudioInputStream(url)); notificationSound = clip; } catch (LineUnavailableException eLu) { assert false; } catch (UnsupportedAudioFileException eUa) { assert false; } catch (IOException eIo) { assert false; } catch (IllegalArgumentException eIa) { assert false; } } }