biskuteri-cafe-JKomasto2/WindowUpdater.java

337 lines
8.2 KiB
Java

/* copyright
This file is part of JKomasto2.
Written in 2022 by Usawashi <usawashi16@yahoo.co.jp>
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 <https://www.gnu.org/licenses/>.
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<TimelineWindow>
timelineWindows;
private List<NotificationsWindow>
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<String> 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;
}
}
}