Source code: irate/plugin/lircremote/LircRemoteControlPlugin.java
1 // Copyright 2003 Stephen Blackheath
2
3 package irate.plugin.lircremote;
4
5 import irate.plugin.*;
6 import java.util.Enumeration;
7 import java.util.Hashtable;
8 import java.util.List;
9 import java.util.Vector;
10 import java.net.Socket;
11 import java.io.*;
12 import nanoxml.*;
13
14 /**
15 * Plugin for remote control based on lirc (Linux/Unix).
16 *
17 * @author Stephen Blackheath
18 */
19 public class LircRemoteControlPlugin
20 extends Plugin
21 implements LircRemoteControlListener
22 {
23 private String host;
24 private int port;
25 private Vector functions;
26
27 public LircRemoteControlPlugin()
28 {
29 host = "localhost";
30 port = 8765;
31 functions = new Vector();
32 functions.add(new Function() {
33 public String getID() {return "this-sux";}
34 public String getName() {return "Rate as 'This sux'";}
35 public void perform() {getApp().setRating(getApp().getSelectedTrack(), 0); getApp().skip();}
36 });
37 functions.add(new Function() {
38 public String getID() {return "yawn";}
39 public String getName() {return "Rate as 'Yawn'";}
40 public void perform() {getApp().setRating(getApp().getSelectedTrack(), 2);}
41 });
42 functions.add(new Function() {
43 public String getID() {return "not-bad";}
44 public String getName() {return "Rate as 'Not bad'";}
45 public void perform() {getApp().setRating(getApp().getSelectedTrack(), 5);}
46 });
47 functions.add(new Function() {
48 public String getID() {return "cool";}
49 public String getName() {return "Rate as 'Cool'";}
50 public void perform() {getApp().setRating(getApp().getSelectedTrack(), 7);}
51 });
52 functions.add(new Function() {
53 public String getID() {return "love-it";}
54 public String getName() {return "Rate as 'Love it'";}
55 public void perform() {getApp().setRating(getApp().getSelectedTrack(), 10);}
56 });
57 functions.add(new Function() {
58 public String getID() {return "pause/resume";}
59 public String getName() {return "Pause/Resume";}
60 public void perform() {getApp().setPaused(!getApp().isPaused());}
61 });
62 functions.add(new Function() {
63 public String getID() {return "skip";}
64 public String getName() {return "Skip";}
65 public void perform() {getApp().skip();}
66 });
67 }
68
69 public List getFunctions()
70 {
71 return functions;
72 }
73
74 /**
75 * Get a short identifier for this Plugin.
76 */
77 public String getIdentifier()
78 {
79 return "lirc-remote";
80 }
81
82 /**
83 * Get a short description of this plugin.
84 */
85 public String getDescription()
86 {
87 return "lirc remote control (Linux/Unix)";
88 }
89
90
91 // ------ Listeners ----------------------------------------------------
92
93 private Vector listeners = new Vector();
94
95 public void addLircRemoteControlListener(LircRemoteControlListener listener)
96 {
97 listeners.add(listener);
98 connect();
99 }
100
101 public void removeLircRemoteControlListener(LircRemoteControlListener listener)
102 {
103 if (listeners.size() == 1 && listeners.contains(listener))
104 disconnect();
105 listeners.remove(listener);
106 }
107
108 private void notifyConnectStatusChanged()
109 {
110 Vector listeners = (Vector) this.listeners.clone();
111 for (int i = 0; i < listeners.size(); i++) {
112 LircRemoteControlListener listener = (LircRemoteControlListener) listeners.get(i);
113 listener.connectStatusChanged(this, connectStatus);
114 }
115 }
116
117 private void notifyButtonPressed(Button button)
118 {
119 Vector listeners = (Vector) this.listeners.clone();
120 for (int i = 0; i < listeners.size(); i++) {
121 LircRemoteControlListener listener = (LircRemoteControlListener) listeners.get(i);
122 listener.buttonPressed(this, button);
123 }
124 }
125
126
127 // ------ I/O ----------------------------------------------------------
128
129 private boolean terminating;
130 private Object timer = new Object();
131 private IOThread ioThread;
132 private Socket s;
133 private BufferedReader r;
134
135 /**
136 * Subclasses to override to do real work of attaching.
137 * Application is available through getApp().
138 */
139 protected synchronized void doAttach()
140 {
141 addLircRemoteControlListener(this);
142 }
143
144 /**
145 * Subclasses to override to do real work of detaching.
146 * Application is available through getApp().
147 */
148 protected synchronized void doDetach()
149 {
150 removeLircRemoteControlListener(this);
151 }
152
153 /**
154 * True if we are attempting to connect.
155 */
156 private boolean connected = false;
157 private boolean isConnected() {return connected;}
158
159 /**
160 * True if we have actually established a connection.
161 */
162 private boolean connectStatus = false;
163
164 /**
165 * Return true if we have established a connection to the lirc daemon.
166 */
167 public boolean getConnectStatus() {return connectStatus;}
168
169 private void connect()
170 {
171 if (!connected) {
172 connected = true;
173 terminating = false;
174 ioThread = new IOThread();
175 ioThread.start();
176 }
177 }
178
179 private void disconnect()
180 {
181 if (connected) {
182 try {
183 if (ioThread != null) {
184 ioThread.terminating = true;
185 // Wake up the timer if it happens to be doing a timed wait.
186 synchronized (timer) {
187 timer.notifyAll();
188 }
189 Socket s = ioThread.s;
190 if (s != null)
191 try {s.close();} catch (IOException e) {}
192 // Work around GCJ bug: Closing the socket, as we do above, does not
193 // terminate the thread under GCJ, so Thread.join() deadlocks.
194 // Instead, we just leave it dangling. This works out not so bad,
195 // because I've written the code in such a way as to make it clean the
196 // dangling thread up the next time a remote control button is pressed.
197 //try {ioThread.join();} catch (InterruptedException e) {}
198 }
199 }
200 finally {
201 ioThread = null;
202 connected = false;
203 connectStatus = false;
204 notifyConnectStatusChanged();
205 }
206 }
207 }
208
209 public String getHost()
210 {
211 return host;
212 }
213
214 public synchronized void setHost(String host)
215 {
216 if (!this.host.equals(host)) {
217 this.host = host;
218 if (isConnected()) {
219 disconnect();
220 connect();
221 }
222 }
223 }
224
225 public int getPort()
226 {
227 return port;
228 }
229
230 public synchronized void setPort(int port)
231 {
232 if (this.port != port) {
233 this.port = port;
234 if (isConnected()) {
235 disconnect();
236 connect();
237 }
238 }
239 }
240
241 public class IOThread
242 extends Thread
243 {
244 IOThread()
245 {
246 }
247
248 public Socket s;
249 public boolean terminating = false;
250
251 public void run()
252 {
253 while (!terminating) {
254 long connectStart = System.currentTimeMillis();
255 try {
256 s = new Socket(host, port);
257 // This makes the thread wake up occasionally, which will allow any
258 // dead threads to be cleaned up. This can result from disconnect()
259 // being called at a bad time.
260 // Note that setSoTimeout does NOT work on GCJ (version 3.0).
261 s.setSoTimeout(10000);
262 try {
263 BufferedReader r = new BufferedReader(new InputStreamReader(s.getInputStream()));
264 try {
265 connectStatus = true;
266 notifyConnectStatusChanged();
267 while (!terminating) {
268 try {
269 String buttonText = r.readLine();
270 if (terminating || buttonText == null)
271 break;
272 int space1 = buttonText.indexOf(' ');
273 if (space1 >= 0) {
274 int space2 = buttonText.indexOf(' ', space1+1);
275 String repeatCountStr = buttonText.substring(space1+1, space2);
276 String idStr = buttonText.substring(space2+1);
277 Button button = new Button(idStr, Integer.parseInt(repeatCountStr));
278 notifyButtonPressed(button);
279 }
280 }
281 catch (InterruptedIOException e) {
282 }
283 }
284 }
285 finally {
286 if (!terminating) {
287 connectStatus = false;
288 notifyConnectStatusChanged();
289 }
290 try {r.close();} catch (IOException e) {}
291 }
292 }
293 finally {
294 try {s.close();} catch (IOException e) {}
295 s = null;
296 }
297 }
298 catch (NumberFormatException e) {
299 e.printStackTrace();
300 }
301 catch (IOException e) {
302 }
303 if (terminating)
304 break;
305 // If the failed connection took less than 20 seconds, then wait the remainder
306 // of the 20 seconds. This protects against getting stuck in a tight loop.
307 long duration = System.currentTimeMillis() - connectStart;
308 if (duration < 20000L && duration >= 0L) {
309 try {
310 synchronized (timer) {
311 timer.wait(20000L - duration);
312 }
313 }
314 catch (InterruptedException e) {}
315 }
316 }
317 }
318 }
319
320 /*
321 public static void main(String[] args)
322 {
323 LircRemoteControlPlugin plugin = new LircRemoteControlPlugin();
324 plugin.setHost("tui");
325 plugin.addLircRemoteControlListener(new LircRemoteControlListener() {
326 public void connectStatusChanged(LircRemoteControlPlugin plugin, boolean connected)
327 {
328 System.out.println(connected?"** Connected":"** Disconnected");
329 }
330
331 public void buttonPressed(LircRemoteControlPlugin plugin, LircRemoteControlPlugin.Button button)
332 {
333 System.out.println(button);
334 }
335 });
336 plugin.attach(null);
337 }
338 */
339
340
341 // ------ Translate button presses to actions --------------------------
342
343 public void connectStatusChanged(LircRemoteControlPlugin plugin, boolean connected)
344 {
345 System.out.println("lirc remote control: "+(connected?"connected":"disconnected"));
346 }
347
348 public void buttonPressed(LircRemoteControlPlugin plugin, Button button)
349 {
350 // System.out.println("lirc remote control: "+button);
351 Function function = null;
352 synchronized (this) {
353 outerLoop:
354 for (int i = 0; i < functions.size(); i++) {
355 Function thisFunction = (Function) functions.get(i);
356 for (int j = 0; j < thisFunction.buttons.size(); j++) {
357 Button thisButton = (Button) thisFunction.buttons.get(j);
358 if (thisButton.equals(button)) {
359 function = thisFunction;
360 break outerLoop;
361 }
362 }
363 }
364 }
365 if (function != null) {
366 System.out.println("lirc remote control: "+function.getName());
367 if (getApp() != null)
368 function.perform();
369 }
370 else {
371 if (button.getRepeatCount() == 0)
372 System.out.println("lirc remote control: unmapped button "+button);
373 }
374 }
375
376
377 // ------ Saving/load of configuration details -------------------------
378
379 /**
380 * Parse the configuration stored in the specified element.
381 */
382 public void parseConfig(XMLElement elt)
383 {
384 for (int i = 0; i < functions.size(); i++) {
385 Function func = (Function) functions.get(i);
386 func.clearConfig();
387 }
388
389 Enumeration enum = elt.enumerateChildren();
390 while (enum.hasMoreElements()) {
391 XMLElement funcElt = (XMLElement) enum.nextElement();
392 if (funcElt.getName().equals("function")) {
393 String id = funcElt.getStringAttribute("id");
394 for (int i = 0; i < functions.size(); i++) {
395 Function func = (Function) functions.get(i);
396 if (func.getID().equals(id))
397 func.parseXML(funcElt);
398 }
399 }
400 else
401 if (funcElt.getName().equals("connect")) {
402 host = funcElt.getStringAttribute("host");
403 port = Integer.parseInt(funcElt.getStringAttribute("port"));
404 }
405 }
406 }
407
408 /**
409 * Format the configuration of this plugin by modifying the specified
410 * element.
411 */
412 public void formatConfig(XMLElement elt)
413 {
414 XMLElement connElt = new XMLElement(new Hashtable(), false, false);
415 elt.addChild(connElt);
416 connElt.setName("connect");
417 connElt.setAttribute("host", host);
418 connElt.setAttribute("port", Integer.toString(port));
419
420 for (int i = 0; i < functions.size(); i++) {
421 Function func = (Function) functions.get(i);
422 elt.addChild(func.formatXML());
423 }
424 }
425 }
426