Java tutorial
/*Copyright (C) 2014 M. Steve Todd mstevetodd@enginedriver.rrclubs.org 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package jmri.enginedriver; // Main java file. /* TODO: see changelog-and-todo-list.txt for complete list of project to-do's */ import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.Application; import android.app.NotificationManager; import android.app.PendingIntent; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import java.net.*; import java.io.*; import android.support.v4.app.NotificationCompat; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import android.view.Menu; import android.webkit.CookieSyncManager; import android.widget.Toast; import javax.jmdns.*; import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.DefaultHttpClient; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import de.tavendo.autobahn.WebSocketConnection; import de.tavendo.autobahn.WebSocketHandler; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiInfo; import java.net.InetAddress; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.LinkedHashMap; import jmri.enginedriver.message_type; import jmri.enginedriver.Consist.ConLoco; import jmri.enginedriver.threaded_application.comm_thread.comm_handler; import jmri.enginedriver.Consist; import jmri.jmrit.roster.RosterEntry; import jmri.jmrit.roster.RosterLoader; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; //The application will start up a thread that will handle network communication in order to ensure that the UI is never blocked. //This thread will only act upon messages sent to it. The network communication needs to persist across activities, so that is why @SuppressLint("NewApi") public class threaded_application extends Application { public comm_thread commThread; String host_ip = null; //The IP address of the WiThrottle server. volatile int port = 0; //The TCP port that the WiThrottle server is running on Double withrottle_version = 0.0; //version of withrottle server private int web_server_port = 0; //default port for jmri web server private volatile boolean doFinish = false; // when true, tells any Activities that are being created/resumed to finish() //shared variables returned from the withrottle server, stored here for easy access by other activities volatile Consist consistT; volatile Consist consistS; volatile Consist consistG; LinkedHashMap<Integer, String> function_labels_T; //function#s and labels from roster for throttle #1 LinkedHashMap<Integer, String> function_labels_S; //function#s and labels from roster for throttle #2 LinkedHashMap<Integer, String> function_labels_G; //function#s and labels from roster for throttle #3 LinkedHashMap<Integer, String> function_labels_default; //function#s and labels from local settings boolean[] function_states_T; //current function states for first throttle boolean[] function_states_S; //current function states for second throttle boolean[] function_states_G; //current function states for second throttle String[] to_system_names; String[] to_user_names; String[] to_states; HashMap<String, String> to_state_names; String[] rt_system_names; String[] rt_user_names; String[] rt_states; HashMap<String, String> rt_state_names; HashMap<String, String> roster_entries; //roster sent by WiThrottle LinkedHashMap<String, String> consist_entries; private static DownloadRosterTask dlRosterTask = null; private static DownloadMetaTask dlMetadataTask = null; HashMap<String, RosterEntry> roster; //roster entries retrieved from roster.xml (null if not retrieved) public static HashMap<String, String> metadata; //metadata values (such as JMRIVERSION) retrieved from web server (null if not retrieved) ImageDownloader imageDownloader = new ImageDownloader(); String power_state; public boolean displayClock = false; public int androidVersion = 0; public final int minWebSocketVersion = 8; //minimum Android version for Autobahn websocket library static final int DEFAULT_HEARTBEAT_INTERVAL = 10; //interval for heartbeat when WiT heartbeat is disabled static final int MIN_OUTBOUND_HEARTBEAT_INTERVAL = 2; //minimum interval for outbound heartbeat generator static final int HEARTBEAT_RESPONSE_ALLOWANCE = 4; //worst case time delay for WiT to respond to a heartbeat message public int heartbeatInterval = 0; //WiT heartbeat interval setting public int turnouts_list_position = 0; //remember where user was in item lists public int routes_list_position = 0; String client_address; //address string of the client address //For communication to the comm_thread. public comm_handler comm_msg_handler = null; //For communication to each of the activities (set and unset by the activity) public volatile Handler connection_msg_handler; public volatile Handler throttle_msg_handler; public volatile Handler web_msg_handler; public volatile Handler select_loco_msg_handler; public volatile Handler turnouts_msg_handler; public volatile Handler routes_msg_handler; public volatile Handler consist_edit_msg_handler; public volatile Handler power_control_msg_handler; public volatile Handler reconnect_status_msg_handler; //these constants are used for onFling public static final int SWIPE_MIN_DISTANCE = 120; public static final int SWIPE_MAX_OFF_PATH = 250; public static final int SWIPE_THRESHOLD_VELOCITY = 200; public static int min_fling_distance; // pixel width needed for fling public static int min_fling_velocity; // velocity needed for fling private static final int ED_NOTIFICATION_ID = 416; //no significance to 416, just shouldn't be 0 private SharedPreferences prefs; public boolean EStopActivated = false; // Used to determine if user pressed the EStop button. //Used to tell set_Labels in Throttle not to update padding for throttle sliders after onCreate. public boolean firstCreate = true; class comm_thread extends Thread { JmDNS jmdns = null; volatile boolean endingJmdns = false; withrottle_listener listener; android.net.wifi.WifiManager.MulticastLock multicast_lock; socket_WiT socketWiT; PhoneListener phone; ClockWebSocketHandler clockWebSocket = null; heartbeat heart = new heartbeat(); volatile String currentTime = ""; comm_thread() { super("comm_thread"); } //Listen for a WiThrottle service advertisement on the LAN. public class withrottle_listener implements ServiceListener { public void serviceAdded(ServiceEvent event) { // Log.d("Engine_Driver", String.format("serviceAdded fired")); //A service has been added. If no details, ask for them Log.d("Engine_Driver", String.format("serviceAdded for '%s', Type='%s'", event.getName(), event.getType())); ServiceInfo si = jmdns.getServiceInfo(event.getType(), event.getName(), 0); if (si == null || si.getPort() == 0) { Log.d("Engine_Driver", String.format("serviceAdded, requesting details: '%s', Type='%s'", event.getName(), event.getType())); jmdns.requestServiceInfo(event.getType(), event.getName(), true, (long) 1000); } }; public void serviceRemoved(ServiceEvent event) { //Tell the UI thread to remove from the list of services available. sendMsg(connection_msg_handler, message_type.SERVICE_REMOVED, event.getName()); //send the service name to be removed Log.d("Engine_Driver", String.format("serviceRemoved: '%s'", event.getName())); }; public void serviceResolved(ServiceEvent event) { // Log.d("Engine_Driver", String.format("serviceResolved fired")); //A service's information has been resolved. Send the port and service name to connect to that service. int port = event.getInfo().getPort(); String host_name = event.getInfo().getName(); // Inet4Address[] ip_addresses = event.getInfo().getInet4Addresses(); //only get ipV4 address String ip_address = ip_addresses[0].toString().substring(1); //use first one, since WiThrottle is only putting one in (for now), and remove leading slash //Tell the UI thread to update the list of services available. HashMap<String, String> hm = new HashMap<String, String>(); hm.put("ip_address", ip_address); hm.put("port", ((Integer) port).toString()); hm.put("host_name", host_name); Message service_message = Message.obtain(); service_message.what = message_type.SERVICE_RESOLVED; service_message.arg1 = port; service_message.obj = hm; //send the hashmap as the payload boolean sent = false; try { sent = connection_msg_handler.sendMessage(service_message); } catch (Exception e) { } if (!sent) service_message.recycle(); Log.d("Engine_Driver", String.format("serviceResolved - %s(%s):%d -- %s", host_name, ip_address, port, event.toString().replace(System.getProperty("line.separator"), " "))); }; } Inet4Address getClientAddr() { int intaddr = 0; Inet4Address addr = null; //Set up to find a WiThrottle service via ZeroConf try { WifiManager wifi = (WifiManager) threaded_application.this.getSystemService(Context.WIFI_SERVICE); WifiInfo wifiinfo = wifi.getConnectionInfo(); intaddr = wifiinfo.getIpAddress(); if (intaddr != 0) { byte[] byteaddr = new byte[] { (byte) (intaddr & 0xff), (byte) (intaddr >> 8 & 0xff), (byte) (intaddr >> 16 & 0xff), (byte) (intaddr >> 24 & 0xff) }; addr = (Inet4Address) Inet4Address.getByAddress(byteaddr); client_address = addr.toString().substring(1); //strip off leading / } else { client_address = null; } } catch (Exception except) { Log.e("Engine_Driver", "getClientAddr - error gettting IP addr: " + except.getMessage()); client_address = null; } return addr; } void start_jmdns() { //Set up to find a WiThrottle service via ZeroConf try { Inet4Address addr = getClientAddr(); if (addr != null && client_address != null) { WifiManager wifi = (WifiManager) threaded_application.this .getSystemService(Context.WIFI_SERVICE); if (multicast_lock == null) { //do this only as needed multicast_lock = wifi.createMulticastLock("engine_driver"); multicast_lock.setReferenceCounted(true); } Log.d("Engine_Driver", "start_jmdns: local IP addr " + client_address); jmdns = JmDNS.create(addr, client_address); //pass ip as name to avoid hostname lookup attempt listener = new withrottle_listener(); Log.d("Engine_Driver", "start_jmdns: listener created"); } else { process_comm_error("No local IP Address found.\nCheck your WiFi connection."); } } catch (Exception except) { Log.e("Engine_Driver", "start_jmdns - Error creating withrottle listener: " + except.getMessage()); process_comm_error( "Error creating withrottle zeroconf listener: IOException: \n" + except.getMessage()); } } //end_jmdns() takes a long time, so put it in its own thread void end_jmdns() { Thread jmdnsThread = new Thread("EndJmdns") { @Override public void run() { try { Log.d("Engine_Driver", "removing jmdns listener"); jmdns.removeServiceListener("_withrottle._tcp.local.", listener); multicast_lock.release(); } catch (Exception e) { Log.d("Engine_Driver", "exception in jmdns.removeServiceListener()"); } try { Log.d("Engine_Driver", "calling jmdns.close()"); jmdns.close(); Log.d("Engine_Driver", "after jmdns.close()"); } catch (Exception e) { Log.d("Engine_Driver", "exception in jmdns.close()"); } jmdns = null; endingJmdns = false; } }; if (jmdnsIsActive()) { //only need to run one instance of this thread to terminate jmdns endingJmdns = true; jmdnsThread.start(); } } boolean jmdnsIsActive() { boolean isActive = jmdns != null && !endingJmdns; return isActive; } @SuppressLint("HandlerLeak") class comm_handler extends Handler { //All of the work of the communications thread is initiated from this function. /***future PowerLock private PowerManager.WakeLock wl = null; */ public void handleMessage(Message msg) { switch (msg.what) { //Start or Stop the WiThrottle listener and required jmdns stuff case message_type.SET_LISTENER: //arg1= 1 to turn on, arg1=0 to turn off if (msg.arg1 == 0) { end_jmdns(); } else { if (jmdns == null) { //start jmdns if not started // Log.d("Engine_Driver","comm_handler: jmdns not started, starting"); start_jmdns(); if (jmdns != null) { //don't bother if jmdns didn't start try { multicast_lock.acquire(); } catch (Exception e) { //log message, but keep going if this fails Log.d("Engine_Driver", "comm_handler: multicast_lock.acquire() failed"); } jmdns.addServiceListener("_withrottle._tcp.local.", listener); Log.d("Engine_Driver", "comm_handler: jmdns listener added"); } else { Log.d("Engine_Driver", "comm_handler: jmdns not running, didn't start listener"); } } else { Log.d("Engine_Driver", "comm_handler: jmdns already running"); } } break; //Connect to the WiThrottle server. case message_type.CONNECT: //avoid duplicate connects, seen when user clicks address multiple times quickly if (socketWiT != null && socketWiT.SocketGood()) { Log.d("Engine_Driver", "Duplicate CONNECT message received."); return; } //clear app.thread shared variables so they can be reinitialized initShared(); //The IP address is stored in the obj as a String, the port is stored in arg1. host_ip = msg.obj.toString(); host_ip = host_ip.trim(); port = msg.arg1; //attempt connection to WiThrottle server socketWiT = new socket_WiT(); if (socketWiT.connect() == true) { sendThrottleName(); sendMsg(connection_msg_handler, message_type.CONNECTED); phone = new PhoneListener(); /***future Notification showNotification(); ***/ /***future PowerLock PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Engine_Driver"); wl.acquire(); ***/ } else { host_ip = null; //clear vars if failed to connect port = 0; } currentTime = ""; break; //Release one or all locos on the specified throttle. addr is in msg (""==all), arg1 holds whichThrottle. case message_type.RELEASE: { String addr = msg.obj.toString(); final char whichThrottle = (char) msg.arg1; final boolean releaseAll = (addr.length() == 0); if (whichThrottle == 'T') { if (releaseAll || consistT.isEmpty()) { addr = ""; function_labels_T = new LinkedHashMap<Integer, String>(); function_states_T = new boolean[32]; } } else if (whichThrottle == 'G') { if (releaseAll || consistG.isEmpty()) { addr = ""; function_labels_G = new LinkedHashMap<Integer, String>(); function_states_G = new boolean[32]; } } else { if (releaseAll || consistS.isEmpty()) { addr = ""; function_labels_S = new LinkedHashMap<Integer, String>(); function_states_S = new boolean[32]; } } if (prefs.getBoolean("stop_on_release_preference", getResources().getBoolean(R.bool.prefStopOnReleaseDefaultValue))) { withrottle_send(whichThrottle + "V0" + (addr != "" ? "<;>" + addr : "")); //send stop command before releasing (if set in prefs) } releaseLoco(addr, whichThrottle); break; } //request speed. arg1 holds whichThrottle case message_type.REQ_VELOCITY: { final char whichThrottle = (char) msg.arg1; withrottle_send(whichThrottle + "qV"); break; } //request direction. arg1 hold whichThrottle case message_type.REQ_DIRECTION: { final char whichThrottle = (char) msg.arg1; // withrottle_send(whichThrottle+"qR"); //request updated direction withrottle_send(whichThrottle + "qR"); //request updated direction break; } //estop requested. arg1 holds whichThrottle case message_type.ESTOP: { final char whichThrottle = (char) msg.arg1; withrottle_send(whichThrottle + "X"); //send eStop request break; } //Disconnect from the WiThrottle server. case message_type.DISCONNECT: { Log.d("Engine_Driver", "TA Disconnect"); doFinish = true; heart.stopHeartbeat(); if (phone != null) phone.disable(); withrottle_send("Q"); if (heart.getInboundInterval() > 0 && withrottle_version > 0.0) { withrottle_send("*-"); //request to turn off heartbeat (if enabled in server prefs) } end_jmdns(); dlMetadataTask.stop(); dlRosterTask.stop(); if (clockWebSocket != null) { clockWebSocket.disconnect(); clockWebSocket = null; } Log.d("Engine_Driver", "TA Alert Activites Shutdown"); alert_activities(message_type.SHUTDOWN, ""); //tell all activities to finish() /***future PowerLock if(wl != null && wl.isHeld()) wl.release(); ***/ //give msgs a chance to xmit before closing socket if (!sendMsgDelay(comm_msg_handler, 4000L, message_type.SHUTDOWN)) { shutdown(); } break; } //Set up an engine to control. The address of the engine is given in msg.obj and whichThrottle is in arg1 //Optional rostername if present is separated from the address by the proper delimiter case message_type.LOCO_ADDR: { final String addr = msg.obj.toString(); final char whichThrottle = (char) msg.arg1; if (withrottle_version >= 2.0) { //don't pass rostername to older WiT if (prefs.getBoolean("drop_on_acquire_preference", getResources().getBoolean(R.bool.prefDropOnAcquireDefaultValue))) { withrottle_send(whichThrottle + "r"); //send release command for any already acquired locos (if set in prefs) } } acquireLoco(addr, whichThrottle); break; } // case message_type.ERROR: // break; //Adjust the locomotive's speed. whichThrottle is in arg 1 and arg2 holds the value of the speed to set. case message_type.VELOCITY: { final char whichThrottle = (char) msg.arg1; withrottle_send(String.format(whichThrottle + "V%d", msg.arg2)); break; } //Change direction. address in in msg, whichThrottle is in arg 1 and arg2 holds the direction to change to. case message_type.DIRECTION: { final String addr = msg.obj.toString(); final char whichThrottle = (char) msg.arg1; withrottle_send(String.format(whichThrottle + "R%d<;>" + addr, msg.arg2)); break; } //Set or unset a function. whichThrottle+addr is in the msg, arg1 is the function number, arg2 is set or unset. case message_type.FUNCTION: { String addr = msg.obj.toString(); final char whichThrottle = (char) addr.charAt(0); addr = addr.substring(1); withrottle_send(String.format(whichThrottle + "F%d%d<;>" + addr, msg.arg2, msg.arg1)); break; } //send command to change turnout. msg = (T)hrow, (C)lose or (2)(toggle) + systemName case message_type.TURNOUT: { final String cmd = msg.obj.toString(); withrottle_send("PTA" + cmd); break; } //send command to route turnout. msg = 2(toggle) + systemName case message_type.ROUTE: { final String cmd = msg.obj.toString(); withrottle_send("PRA" + cmd); break; } //send command to change power setting, new state is passed in arg1 case message_type.POWER_CONTROL: withrottle_send(String.format("PPA%d", msg.arg1)); break; //Current Time update request case message_type.CURRENT_TIME: if (!doFinish) { alert_activities(message_type.CURRENT_TIME, currentTime); } break; //Current Time clock display preference change case message_type.CLOCK_DISPLAY: if (!doFinish && clockWebSocket != null) { clockWebSocket.refresh(); alert_activities(message_type.CURRENT_TIME, currentTime); } break; // SHUTDOWN - terminate socketWiT and it's done case message_type.SHUTDOWN: shutdown(); break; // update of roster-related data completed in background case message_type.ROSTER_UPDATE: if (!doFinish) { alert_activities(message_type.ROSTER_UPDATE, ""); } break; // WiT socket is down and reconnect attempt in prog case message_type.WIT_CON_RETRY: /***future Notification hideNotification(); ***/ if (!doFinish) { alert_activities(message_type.WIT_CON_RETRY, msg.obj.toString()); } break; // WiT socket is back up case message_type.WIT_CON_RECONNECT: /***future Notification showNotification(); ***/ if (!doFinish) { sendThrottleName(); reacquireAllConsists(); alert_activities(message_type.WIT_CON_RECONNECT, msg.obj.toString()); } break; } }; } private void shutdown() { Log.d("Engine_Driver", "TA Shutdown"); end_jmdns(); //jmdns should already be down but no harm in making call if (socketWiT != null) { socketWiT.disconnect(true); //stop reading from the socket socketWiT = null; } host_ip = null; port = 0; doFinish = false; //ok for activities to run if restarted after this } private void sendThrottleName() { sendThrottleName(true); } private void sendThrottleName(Boolean sendHWID) { String s = prefs.getString("throttle_name_preference", getApplicationContext().getResources().getString(R.string.prefThrottleNameDefaultValue)); withrottle_send("N" + s); //send throttle name if (sendHWID == true) withrottle_send("HU" + s); //also send throttle name as the UDID } private void acquireLoco(String addr, char whichThrottle) { withrottle_send(whichThrottle + addr); if (withrottle_version >= 2.0) { //request current direction and speed (WiT 2.0+) withrottle_send("M" + whichThrottle + "A*<;>qV"); withrottle_send("M" + whichThrottle + "A*<;>qR"); } if (heart.getInboundInterval() > 0 && withrottle_version > 0.0) { withrottle_send("*+"); //request to turn on heartbeat (if enabled in server prefs) } } private void releaseLoco(String addr, char whichThrottle) { withrottle_send(whichThrottle + "r" + (addr != "" ? "<;>" + addr : "")); //send release command } private void reacquireAllConsists() { reacquireConsist(consistT, 'T'); reacquireConsist(consistS, 'S'); reacquireConsist(consistG, 'G'); } private void reacquireConsist(Consist c, char whichThrottle) { for (ConLoco l : c.getLocos()) // reacquire each loco in the consist { String addr = l.getAddress(); String desc = l.getDesc(); if (desc.length() > 0 && withrottle_version >= 1.6) // add roster selection info if present and supported addr += "<;>" + l.getDesc(); acquireLoco(addr, whichThrottle); } } //display error msg using Toast() private void process_comm_error(final String msg_txt) { Log.d("Engine_Driver", "TA comm error: " + msg_txt); //need to do Toast() on the main thread so create a handler Handler h = new Handler(Looper.getMainLooper()); h.post(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), msg_txt, Toast.LENGTH_SHORT).show(); } }); } private void process_response(String response_str) { /* see java/arc/jmri/jmrit/withrottle/deviceserver.java for server code and some documentation VN<Version#> T<EngineAddress>(<LongOrShort>) S<2ndEngineAddress>(<LongOrShort>) RL<RosterSize>]<RosterList> RF<RosterFunctionList> RS<2ndRosterFunctionList> *<HeartbeatIntervalInSeconds> PTL[<SystemTurnoutName><UserName><State>repeat] where state 1=Unknown. 2=Closed, 4=Thrown PTA<NewTurnoutState><SystemName> PPA<NewPowerState> where state 0=off, 1=on, 2=unknown TODO: add remaining items, or move examples into code below */ //send response to debug log for review Log.d("Engine_Driver", "<--:" + response_str); boolean skipAlert = false; //set to true if the Activities do not need to be Alerted switch (response_str.charAt(0)) { //handle responses from MultiThrottle function case 'M': { String sWhichThrottle = response_str.substring(1, 2); char whichThrottle = sWhichThrottle.charAt(0); String[] ls = splitByString(response_str, "<;>"); //drop off separator String addr = ls[0].substring(3); char com2 = response_str.charAt(2); //loco was successfully added to a throttle if (com2 == '+') { //"MT+L2591<;>" loco was added Consist con; if (whichThrottle == 'T') // indicate loco was Confirmed by WiT con = consistT; else if (whichThrottle == 'G') con = consistG; else con = consistS; if (con.getLoco(addr) != null) con.setConfirmed(addr); else Log.d("Engine_Driver", "loco " + addr + " not selected but assigned by WiT to " + whichThrottle); } else if (com2 == '-') { //"MS-L6318<;>" loco removed from throttle if (whichThrottle == 'T') { consistT.remove(addr); } else if (whichThrottle == 'G') { consistG.remove(addr); } else { consistS.remove(addr); } Log.d("Engine_Driver", "loco " + addr + " dropped from " + whichThrottle); } else if (com2 == 'L') { //list of function buttons String lead; if ('T' == whichThrottle) lead = consistT.getLeadAddr(); else if ('G' == whichThrottle) lead = consistG.getLeadAddr(); else lead = consistS.getLeadAddr(); if (lead.equals(addr)) //*** temp - only process if for lead engine in consist process_roster_function_string("RF29}|{1234(L)" + ls[1], sWhichThrottle); //prepend some stuff to match old-style } else if (com2 == 'A') { //process change in function value MTAL4805<;>F028 if ("F".equals(ls[1].substring(0, 1))) { process_function_state_20(sWhichThrottle, Integer.valueOf(ls[1].substring(2)), "1".equals(ls[1].substring(1, 2)) ? true : false); } } break; } case 'V': try { withrottle_version = Double.parseDouble(response_str.substring(2)); } catch (Exception e) { Log.d("Engine_Driver", "process response: invalid WiT version string"); withrottle_version = 0.0; } break; case '*': try { heartbeatInterval = Integer.parseInt(response_str.substring(1)); //set app variable } catch (Exception e) { Log.d("Engine_Driver", "process response: invalid WiT hearbeat string"); heartbeatInterval = 0; } heart.startHeartbeat(heartbeatInterval); break; case 'R': //Roster switch (response_str.charAt(1)) { case 'C': if (response_str.charAt(2) == 'C' || response_str.charAt(2) == 'L') { //RCC1 or RCL1 (treated the same in ED) clear_consist_list(); } else if (response_str.charAt(2) == 'D') { //RCD}|{88(S)}|{88(S)]\[2591(L)}|{true]\[3(S)}|{true]\[4805(L)}|{true process_consist_list(response_str); } break; case 'L': // roster_list_string = response_str.substring(2); //set app variable process_roster_list(response_str); //process roster list // dlRosterTask.get(); // not sure why we're doing this here break; case 'F': //RF29}|{2591(L)]\[Light]\[Bell]\[Horn]\[Air]\[Uncpl]\[BrkRls]\[]\[]\[]\[]\[]\[]\[Engine]\[]\[]\[]\[]\[]\[BellSel]\[HornSel]\[]\[]\[]\[]\[]\[]\[]\[]\[ process_roster_function_string(response_str.substring(2), "T"); break; case 'S': //RS29}|{4805(L)]\[Light]\[Bell]\[Horn]\[Air]\[Uncpl]\[BrkRls]\[]\[]\[]\[]\[]\[]\[Engine]\[]\[]\[]\[]\[]\[BellSel]\[HornSel]\[]\[]\[]\[]\[]\[]\[]\[]\[ process_roster_function_string(response_str.substring(2), "S"); break; case 'P': //Properties RPF}|{whichThrottle]\[function}|{state]\[function}|{state... if (response_str.charAt(2) == 'F') { //function state process_function_state(response_str); //process function state message (passing the whole message) } break; } //end switch inside R break; case 'P': //Panel switch (response_str.charAt(1)) { case 'T': //turnouts if (response_str.charAt(2) == 'T') { //turnout control allowed process_turnout_titles(response_str); } if (response_str.charAt(2) == 'L') { //list of turnouts process_turnout_list(response_str); //process turnout list } if (response_str.charAt(2) == 'A') { //action? changes to turnouts process_turnout_change(response_str); //process turnout changes } break; case 'R': //routes if (response_str.charAt(2) == 'T') { //route control allowed process_route_titles(response_str); } if (response_str.charAt(2) == 'L') { //list of routes process_route_list(response_str); //process route list } if (response_str.charAt(2) == 'A') { //action? changes to routes process_route_change(response_str); //process route changes } break; case 'P': //power if (response_str.charAt(2) == 'A') { //change power state String oldState = power_state; power_state = response_str.substring(3); if (power_state.equals(oldState)) { skipAlert = true; } } break; case 'W': //Web Server port int oldPort = web_server_port; try { web_server_port = Integer.parseInt(response_str.substring(2)); //set app variable } catch (Exception e) { Log.d("Engine_Driver", "process response: invalid web server port string"); } if (oldPort == web_server_port) { skipAlert = true; } else { dlMetadataTask.get(); // start background metadata update dlRosterTask.get(); // start background roster update if (androidVersion >= minWebSocketVersion) { if (clockWebSocket == null) clockWebSocket = new ClockWebSocketHandler(); clockWebSocket.refresh(); } } break; } //end switch inside P break; } //end switch if (!skipAlert) { alert_activities(message_type.RESPONSE, response_str); //send response to running activities } } //end of process_response //parse roster functions list into appropriate app variable array // //RF29}|{4805(L)]\[Light]\[Bell]\[Horn]\[Air]\[Uncpl]\[BrkRls]\[]\[]\[]\[]\[]\[]\[Engine]\[]\[]\[]\[]\[]\[BellSel]\[HornSel]\[]\[]\[]\[]\[]\[]\[]\[]\[ private void process_roster_function_string(String response_str, String whichThrottle) { Log.d("Engine_Driver", "processing function labels for " + whichThrottle); String[] ta = splitByString(response_str, "]\\["); //split into list of labels //clear the appropriate global variable if ("T".equals(whichThrottle)) { function_labels_T = new LinkedHashMap<Integer, String>(); } else if ("S".equals(whichThrottle)) { function_labels_S = new LinkedHashMap<Integer, String>(); } else { function_labels_G = new LinkedHashMap<Integer, String>(); } //initialize app arrays (skipping first) int i = 0; for (String ts : ta) { if (i > 0 && !"".equals(ts)) { //skip first chunk, which is length, and skip any blank entries if ("T".equals(whichThrottle)) { //populate the appropriate hashmap function_labels_T.put(i - 1, ts); //index is hashmap key, value is label string } else if ("S".equals(whichThrottle)) { function_labels_S.put(i - 1, ts); //index is hashmap key, value is label string } else { function_labels_G.put(i - 1, ts); //index is hashmap key, value is label string } } //end if i>0 i++; } //end for } //parse roster list into appropriate app variable array // RL2]\[NS2591}|{2591}|{L]\[NS4805}|{4805}|{L private void process_roster_list(String response_str) { //clear the global variable roster_entries = new HashMap<String, String>(); String[] ta = splitByString(response_str, "]\\["); //initial separation //initialize app arrays (skipping first) int i = 0; for (String ts : ta) { if (i > 0) { //skip first chunk String[] tv = splitByString(ts, "}|{"); //split these into name, address and length try { roster_entries.put(tv[0], tv[1] + "(" + tv[2] + ")"); //roster name is hashmap key, value is address(L or S), e.g. 2591(L) } catch (Exception e) { Log.d("Engine_Driver", "process_roster_list caught Exception"); //ignore any bad stuff in roster entries } } //end if i>0 i++; } //end for } //parse consist list into appropriate mainapp hashmap //RCD}|{88(S)}|{88(S)]\[2591(L)}|{true]\[3(S)}|{true]\[4805(L)}|{true private void process_consist_list(String response_str) { String consist_addr = null; String consist_desc = ""; String consist_name = ""; String[] ta = splitByString(response_str, "]\\["); //initial separation String plus = ""; //plus sign for a separator //initialize app arrays (skipping first) int i = 0; for (String ts : ta) { if (i == 0) { //first chunk is a "header" String[] tv = splitByString(ts, "}|{"); //split header chunk into header, address and name consist_addr = tv[1]; consist_name = tv[2]; } else { //list of locos in consist String[] tv = splitByString(ts, "}|{"); //split these into loco address and direction tv = splitByString(tv[0], "("); //split again to strip off address size (L) consist_desc += plus + tv[0]; plus = "+"; } //end if i==0 i++; } //end for if (!consist_name.equals(consist_addr) && (consist_name.length() > 4)) { consist_desc = consist_name; // use entered name if significant } consist_entries.put(consist_addr, consist_desc); } //clear out any stored consists private void clear_consist_list() { consist_entries.clear(); } //parse turnout change to update mainapp array entry // PTA<NewState><SystemName> // PTA2LT12 private void process_turnout_change(String response_str) { if (to_system_names == null) return; //ignore if turnouts not defined String newState = response_str.substring(3, 4); String systemName = response_str.substring(4); int pos = -1; for (String sn : to_system_names) { //TODO: rewrite for better lookup pos++; if (sn.equals(systemName)) { break; } } if (pos <= to_system_names.length) { //if found, update to new value to_states[pos] = newState; } } //end of process_turnout_change //parse turnout list into appropriate app variable array // PTL[<SystemName><UserName><State>repeat] where state 1=Unknown. 2=Closed, 4=Thrown // PTL]\[LT12}|{my12}|{1 private void process_turnout_list(String response_str) { String[] ta = splitByString(response_str, "]\\["); //initial separation //initialize app arrays (skipping first) to_system_names = new String[ta.length - 1]; to_user_names = new String[ta.length - 1]; to_states = new String[ta.length - 1]; int i = 0; for (String ts : ta) { if (i > 0) { //skip first chunk, just message id String[] tv = splitByString(ts, "}|{"); //split these into 3 parts, key and value to_system_names[i - 1] = tv[0]; to_user_names[i - 1] = tv[1]; to_states[i - 1] = tv[2]; } //end if i>0 i++; } //end for } private void process_turnout_titles(String response_str) { //PTT]\[Turnouts}|{Turnout]\[Closed}|{2]\[Thrown}|{4 //clear the global variable to_state_names = new HashMap<String, String>(); String[] ta = splitByString(response_str, "]\\["); //initial separation //initialize app arrays (skipping first) int i = 0; for (String ts : ta) { if (i > 1) { //skip first 2 chunks String[] tv = splitByString(ts, "}|{"); //split these into value and key to_state_names.put(tv[1], tv[0]); } //end if i>0 i++; } //end for } //parse route list into appropriate app variable array // PRA<NewState><SystemName> // PRA2LT12 private void process_route_change(String response_str) { String newState = response_str.substring(3, 4); String systemName = response_str.substring(4); int pos = -1; for (String sn : rt_system_names) { pos++; if (sn.equals(systemName)) { break; } } if (pos <= rt_system_names.length) { //if found, update to new value rt_states[pos] = newState; } } //end of process_route_change //parse route list into appropriate app variable array // PRL[<SystemName><UserName><State>repeat] where state 1=Unknown. 2=Closed, 4=Thrown // PRL]\[LT12}|{my12}|{1 private void process_route_list(String response_str) { String[] ta = splitByString(response_str, "]\\["); //initial separation //initialize app arrays (skipping first) rt_system_names = new String[ta.length - 1]; rt_user_names = new String[ta.length - 1]; rt_states = new String[ta.length - 1]; int i = 0; for (String ts : ta) { if (i > 0) { //skip first chunk, just message id String[] tv = splitByString(ts, "}|{"); //split these into 3 parts, key and value rt_system_names[i - 1] = tv[0]; rt_user_names[i - 1] = tv[1]; rt_states[i - 1] = tv[2]; } //end if i>0 i++; } //end for } private void process_route_titles(String response_str) { //PRT //clear the global variable rt_state_names = new HashMap<String, String>(); String[] ta = splitByString(response_str, "]\\["); //initial separation //initialize app arrays (skipping first) int i = 0; for (String ts : ta) { if (i > 1) { //skip first 2 chunks String[] tv = splitByString(ts, "}|{"); //split these into value and key rt_state_names.put(tv[1], tv[0]); } //end if i>0 i++; } //end for } //parse function state string into appropriate app variable array (format for WiT < 2.0) private void process_function_state(String response_str) { String whichThrottle = null; String[] sa = splitByString(response_str, "]\\[F"); //initial separation (note that I include the F just to strip it off and simplify later stuff int i = 0; for (String fs : sa) { String[] fa = splitByString(fs, "}|{"); //split these into 2 parts, key and value if (i == 0) { //first chunk is different, contains whichThrottle whichThrottle = fa[1]; } else { //all others have function#, then value int fn = Integer.parseInt(fa[0]); boolean fState = Boolean.parseBoolean(fa[1]); if ("T".equals(whichThrottle)) { function_states_T[fn] = fState; } else if ("S".equals(whichThrottle)) { function_states_S[fn] = fState; } else { function_states_G[fn] = fState; } } //end if i==0 i++; } //end for } //parse function state string into appropriate app variable array (format for WiT >= 2.0) private void process_function_state_20(String whichThrottle, Integer fn, boolean fState) { if ("T".equals(whichThrottle)) { function_states_T[fn] = fState; } else if ("S".equals(whichThrottle)) { function_states_S[fn] = fState; } else { function_states_G[fn] = fState; } } // // withrottle_send(String msg) // //send msg to the socket using multithrottle format // //msg format is generally whichThrottle+cmd+<;>addr //if <;>addr is omitted then command is sent to all locos on whichThrottle // //msg format for acquire loco is whichThrottle+addr+<;>rosterName //where <;>rosterName is optional // private void withrottle_send(String msg) { // Log.d("Engine_Driver", "WiT send " + msg); String newMsg = msg; boolean validMsg = (newMsg != null); if (!validMsg) { Log.d("Engine_Driver", "--> null msg"); } //convert msg to new MultiThrottle format if version >= 2.0 else if (withrottle_version >= 2.0) { try { char whichThrottle = msg.charAt(0); String cmd = msg.substring(1); char com = cmd.charAt(0); String addr = ""; if (cmd.length() > 0) { //check for loco address after the command String[] as = splitByString(cmd, "<;>"); if (as.length > 1) { addr = as[1]; cmd = as[0]; } } String prefix = "M" + whichThrottle; // use a multithrottle command if ('T' == whichThrottle || 'S' == whichThrottle || 'G' == whichThrottle) { //acquire loco if ('L' == com || 'S' == com) { //if address length String rosterName = new String(addr); addr = cmd; if (rosterName.length() > 0) { rosterName = "E" + rosterName; //use E to indicate rostername } else { rosterName = addr; } newMsg = prefix + "+" + addr + "<;>" + rosterName; //add requested loco to this throttle } else if ('r' == com) { //if release loco(s) if (addr.length() > 0) newMsg = prefix + "-" + addr + "<;> + addr"; //release one loco else newMsg = prefix + "-*<;>r"; //release all locos from this throttle } else { //if anything else if (addr.length() == 0) addr = "*"; newMsg = prefix + "A" + addr + "<;>" + cmd; } } } catch (Exception e) { validMsg = false; if ((socketWiT != null) && newMsg.equals("Q")) { Log.d("Engine_Driver", "Sent " + newMsg + " command to jmri WiFi Throttle"); socketWiT.Send(newMsg); //Sends quit command to JMRI. } else { Log.d("Engine_Driver", "--> invalid msg: " + newMsg); } } } if (validMsg) { //send response to debug log for review String lm = "-->:" + newMsg; if (newMsg != msg) { lm += " was(" + msg + ")"; } Log.d("Engine_Driver", lm); //perform the send if (socketWiT != null) { socketWiT.Send(newMsg); } } // start_read_timer(busyReadDelay); } //end withrottle_send() public void run() { Looper.prepare(); comm_msg_handler = new comm_handler(); Looper.loop(); }; class socket_WiT extends Thread { protected InetAddress host_address; protected Socket clientSocket = null; protected BufferedReader inputBR = null; protected PrintWriter outputPW = null; private volatile boolean endRead = false; //signals rcvr to terminate private volatile boolean socketGood = false; //indicates socket condition private volatile boolean inboundTimeout = false; //indicates inbound messages are not arriving from WiT private boolean firstConnect = false; //indicates initial socket connection was achieved socket_WiT() { super("socket_WiT"); } public boolean connect() { //use local socketOk instead of setting socketGood so that the rcvr doesn't resume until connect() is done boolean socketOk = HaveNetworkConnection(); //validate address if (socketOk) { try { host_address = InetAddress.getByName(host_ip); } catch (UnknownHostException except) { process_comm_error("Can't determine IP address of " + host_ip); socketOk = false; } } //socket if (socketOk) { try { //look for someone to answer on specified socket, and set timeout clientSocket = new Socket(); InetSocketAddress sa = new InetSocketAddress(host_ip, port); clientSocket.connect(sa, 3000); //TODO: adjust these timeouts, or set in prefs clientSocket.setSoTimeout(500); } catch (Exception except) { if (!firstConnect) { process_comm_error("Can't connect to host " + host_ip + " and port " + port + " from " + client_address + " - " + except.getMessage() + "\nCheck WiThrottle and network settings."); } socketOk = false; } } //rcvr if (socketOk) { try { inputBR = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); } catch (IOException except) { process_comm_error("Error creating input stream, IOException: " + except.getMessage()); socketOk = false; } } //start the socket_WiT thread. if (socketOk) { if (!this.isAlive()) { endRead = false; try { this.start(); } catch (IllegalThreadStateException except) { //ignore "already started" errors process_comm_error("Error starting socket_WiT thread: " + except.getMessage()); } } } //xmtr if (socketOk) { try { outputPW = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream()), true); if (outputPW.checkError()) { socketOk = false; } } catch (IOException e) { process_comm_error("Error creating output stream, IOException: " + e.getMessage()); socketOk = false; } } socketGood = socketOk; if (socketOk) firstConnect = true; return socketOk; } public void disconnect(boolean shutdown) { if (shutdown) { endRead = true; for (int i = 0; i < 5 && this.isAlive(); i++) { try { Thread.sleep(500); // give run() a chance to see endRead and exit } catch (InterruptedException e) { process_comm_error( "Error sleeping the thread, InterruptedException: " + e.getMessage()); } } } socketGood = false; //close socket if (clientSocket != null) { try { clientSocket.close(); } catch (Exception e) { Log.d("Engine_Driver", "Error closing the Socket: " + e.getMessage()); } } } //read the input buffer public void run() { String str = null; //continue reading until signaled to exit by endRead while (!endRead) { if (socketGood) { //skip read when the socket is down try { if ((str = inputBR.readLine()) != null) { if (str.length() > 0) { heart.restartInboundInterval(); process_response(str); } } } catch (SocketTimeoutException e) { socketGood = this.SocketCheck(); } catch (IOException e) { if (socketGood) { Log.d("Engine_Driver", "WiT rcvr error."); socketGood = false; //input buffer error so force reconnection on next send } } } if (!socketGood) { SystemClock.sleep(500L); //don't become compute bound here when the socket is down } } heart.stopHeartbeat(); } public void Send(String msg) { boolean reconInProg = false; //reconnect socket if needed // if(!socketGood || !this.SocketCheck()) { if (!socketGood || inboundTimeout) { String status; getClientAddr(); //update address in case network connection was lost if (client_address == null) { status = "Not connected to a network. Check WiFi settings.\n\nRetrying"; Log.d("Engine_Driver", "WiT send reconnection attempt."); } else if (inboundTimeout) { status = "No response from JMRI " + host_ip + ":" + port + " for " + heart.sGetInboundInterval() + " seconds. " + "Check that the JMRI Withrottle server is running.\n\nRetrying"; Log.d("Engine_Driver", "WiT receive reconnection attempt."); } else { status = "Unable to connect to JMRI at " + host_ip + ":" + port + " from " + client_address + ".\n\nRetrying"; Log.d("Engine_Driver", "WiT send reconnection attempt."); } socketGood = false; sendMsg(comm_msg_handler, message_type.WIT_CON_RETRY, status); //perform the reconnection sequence this.disconnect(false); //clean up socket but do not shut down the receiver this.connect(); //attempt to reestablish connection reconInProg = true; } //try to send the message if (socketGood) { try { outputPW.println(msg); outputPW.flush(); //we could restart outbound heartbeat timer here, but wit does not notify us of speed changes //(caused by other throttles for example) so just keep heartbeat going to get regular speed updates //heart.restartOutboundInterval(); // if we get here without an exception then the socket is ok if (reconInProg) { getClientAddr(); //update address in case network connection has changed String status = "Connected to JMRI WiThrottle Server at " + host_ip + ":" + port; sendMsg(comm_msg_handler, message_type.WIT_CON_RECONNECT, status); Log.d("Engine_Driver", "WiT reconnection successful."); inboundTimeout = false; heart.restartInboundInterval(); //socket is good so restart inbound heartbeat timer } } catch (Exception e) { Log.d("Engine_Driver", "WiT xmtr error."); socketGood = false; //output buffer error so force reconnection on next send } } if (!socketGood) { comm_msg_handler.postDelayed(heart.outboundHeartbeatTimer, 500L); //try connection again in 0.5 second } } // Attempt to determine if the socket connection is still good. // unfortunatley isConnected returns true if the Socket was disconnected other than by calling close() // so on signal loss it still returns true. // Eventually we just try to send and handle the IOException if the socket was disconnected. public boolean SocketCheck() { boolean status = clientSocket.isConnected() && !clientSocket.isInputShutdown() && !clientSocket.isOutputShutdown(); if (status) status = HaveNetworkConnection(); // can't trust the socket flags so try something else... return status; } // temporary - SocketCheck should determine whether socket connection is good however socket flags sometimes do not get updated // so it doesn't work. This is better than nothing though? private boolean HaveNetworkConnection() { boolean haveConnectedWifi = false; boolean haveConnectedMobile = false; ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo[] netInfo = cm.getAllNetworkInfo(); for (NetworkInfo ni : netInfo) { if ("WIFI".equalsIgnoreCase(ni.getTypeName())) if (ni.isConnected()) { haveConnectedWifi = true; } if ("MOBILE".equalsIgnoreCase(ni.getTypeName())) if (ni.isConnected()) { haveConnectedMobile = true; } } return haveConnectedWifi || haveConnectedMobile; } public boolean SocketGood() { return this.socketGood; } public void InboundTimeout() { inboundTimeout = true; comm_msg_handler.postDelayed(heart.outboundHeartbeatTimer, 200L); //force a send so the reconnection process start immediately } } class heartbeat { // outboundHeartbeat - send a periodic heartbeat to WiT to show that ED is alive. // // inboundHeartbeat - WiT doesn't send a heartbeat to ED, so send a periodic message to WiT that requires a response. // // If the WiT heartbeat interval = 0 then use DEFAULT_OUTBOUND_HEARTBEAT_INTERVAL. // // If the WiT heartbeat is >= (MIN_OUTBOUND_HEARTBEAT_INTERVAL + HEARTBEAT_RESPONSE_ALLOWANCE) // then set the outbound heartbeat rate to (WiT heartbeat - HEARTBEAT_RESPONSE_ALLOWANCE) // // Else the outbound heartbeat rate to MIN_OUTBOUND_HEARTBEAT_INTERVAL. // // The inbound heartbeat rate is set to (outbound heartbeat rate + HEARTBEAT_RESPONSE_ALLOWANCE) private int heartbeatIntervalSetpoint = 0; //WiT heartbeat interval in seconds private int heartbeatOutboundInterval = 0; //sends outbound heartbeat message at this rate (msec) private int heartbeatInboundInterval = 0; //alerts user if there was no inbound traffic for this long (msec) private String sInboundInterval = ""; //inbound heartbeat interval in seconds public int getInboundInterval() { return heartbeatInboundInterval; } public int getOutboundInterval() { return heartbeatOutboundInterval; } public String sGetInboundInterval() { return sInboundInterval; } //startHeartbeat(timeoutInterval in seconds) //calcs the inbound and outbound intervals and starts the beating public void startHeartbeat(int timeoutInterval) { //update interval timers only when the heartbeat timeout interval changed if (timeoutInterval != heartbeatIntervalSetpoint) { heartbeatIntervalSetpoint = timeoutInterval; int outInterval; if (heartbeatIntervalSetpoint == 0) { //wit heartbeat is disabled so use default outInterval = DEFAULT_HEARTBEAT_INTERVAL; } else { outInterval = heartbeatIntervalSetpoint - HEARTBEAT_RESPONSE_ALLOWANCE; } if (outInterval < MIN_OUTBOUND_HEARTBEAT_INTERVAL) outInterval = MIN_OUTBOUND_HEARTBEAT_INTERVAL; heartbeatOutboundInterval = outInterval * 1000; //convert to milliseconds heartbeatInboundInterval = (outInterval + HEARTBEAT_RESPONSE_ALLOWANCE) * 1000; //convert to milliseconds sInboundInterval = Integer.toString(outInterval + HEARTBEAT_RESPONSE_ALLOWANCE); restartOutboundInterval(); restartInboundInterval(); } } //restartOutboundInterval() //restarts the outbound interval timing - call this after sending anything to WiT that requires a response public void restartOutboundInterval() { comm_msg_handler.removeCallbacks(outboundHeartbeatTimer); //remove any pending requests if (heartbeatOutboundInterval > 0) { comm_msg_handler.postDelayed(outboundHeartbeatTimer, heartbeatOutboundInterval); //restart interval } } //restartInboundInterval() //restarts the inbound interval timing - call this after receiving anything from WiT public void restartInboundInterval() { comm_msg_handler.removeCallbacks(inboundHeartbeatTimer); if (heartbeatInboundInterval > 0) { comm_msg_handler.postDelayed(inboundHeartbeatTimer, heartbeatInboundInterval); } } public void stopHeartbeat() { comm_msg_handler.removeCallbacks(outboundHeartbeatTimer); //remove any pending requests comm_msg_handler.removeCallbacks(inboundHeartbeatTimer); heartbeatIntervalSetpoint = 0; } public void sendHeartbeat() { comm_msg_handler.post(outboundHeartbeatTimer); } //outboundHeartbeatTimer() //sends a periodic message to WiT private Runnable outboundHeartbeatTimer = new Runnable() { @Override public void run() { comm_msg_handler.removeCallbacks(this); //remove pending requests if (heartbeatIntervalSetpoint != 0) { boolean anySent = false; if (withrottle_version >= 2.0) { if (consistT.isActive()) { withrottle_send("MTA*<;>qV"); //request speed withrottle_send("MTA*<;>qR"); //request direction anySent = true; } if (consistS.isActive()) { withrottle_send("MSA*<;>qV"); //request speed withrottle_send("MSA*<;>qR"); //request direction anySent = true; } if (consistG.isActive()) { withrottle_send("MGA*<;>qV"); //request speed withrottle_send("MGA*<;>qR"); //request direction anySent = true; } } if (!anySent) { sendThrottleName(false); //send message that will get a response } comm_msg_handler.postDelayed(this, heartbeatOutboundInterval); //set next beat } } }; //inboundHeartbeatTimer() //display an alert message when there is no inbound traffic from WiT within required interval private Runnable inboundHeartbeatTimer = new Runnable() { @Override public void run() { comm_msg_handler.removeCallbacks(this); //remove pending requests if (heartbeatIntervalSetpoint != 0) { if (socketWiT != null && socketWiT.SocketGood()) { socketWiT.InboundTimeout(); } comm_msg_handler.postDelayed(this, heartbeatInboundInterval); //set next inbound timeout } } }; } class PhoneListener extends PhoneStateListener { private TelephonyManager telMgr; public PhoneListener() { telMgr = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); this.enable(); } public void disable() { telMgr.listen(this, PhoneStateListener.LISTEN_NONE); } public void enable() { telMgr.listen(this, PhoneStateListener.LISTEN_CALL_STATE); } @Override public void onCallStateChanged(int state, String incomingNumber) { if (state == TelephonyManager.CALL_STATE_OFFHOOK) { if (prefs.getBoolean("stop_on_phonecall_preference", getResources().getBoolean(R.bool.prefStopOnPhonecallDefaultValue))) { Log.d("Engine_Driver", "Phone is OffHook, Stopping Trains"); if (consistT.isActive()) { withrottle_send("MTA*<;>V0"); } if (consistG.isActive()) { withrottle_send("MGA*<;>V0"); } if (consistS.isActive()) { withrottle_send("MSA*<;>V0"); } } } } } class ClockWebSocketHandler extends WebSocketHandler { private final String sGetClockMemory = "{\"type\":\"memory\",\"data\":{\"name\":\"IMCURRENTTIME\"}}"; private final String sClockMemoryName = "IMCURRENTTIME"; private WebSocketConnection mConnection = new WebSocketConnection(); private int displayClockHrs = 0; private final SimpleDateFormat sdf12 = new SimpleDateFormat("h:mm a"); private final SimpleDateFormat sdf24 = new SimpleDateFormat("HH:mm"); @Override public void onOpen() { displayClock = true; try { Log.d("Engine_Driver", "ClockWebSocket open"); mConnection.sendTextMessage(sGetClockMemory); } catch (Exception e) { Log.d("Engine_Driver", "ClockWebSocket open error: " + e.toString()); } } @Override public void onTextMessage(String msg) { try { JSONObject currentTimeMemory = new JSONObject(msg); JSONObject data = currentTimeMemory.getJSONObject("data"); if (sClockMemoryName.equals(data.getString("name"))) { currentTime = data.getString("value"); if (currentTime.length() > 0) { String newTime; try { if (currentTime.indexOf("M") < 0) { // no AM or PM - in 24 hr format if (displayClockHrs == 1) { // display in 12 hr format newTime = sdf12.format(sdf24.parse(currentTime)); currentTime = newTime; } } else { // in 12 hr format if (displayClockHrs == 2) { // display in 24 hr format newTime = sdf24.format(sdf12.parse(currentTime)); currentTime = newTime; } } } catch (ParseException e) { } alert_activities(message_type.CURRENT_TIME, currentTime); //send the time update } } } catch (JSONException e) { // wasn't a clock memory message so just ignore it } } @Override public void onClose(int code, String closeReason) { // attempt reconnection unless finishing if (!doFinish && displayClock) { displayClock = false; this.connect(); } } private void connect() { try { Log.d("Engine_Driver", "ClockWebSocket attempt connect"); mConnection.connect(createUri(), this); } catch (Exception e) { Log.d("Engine_Driver", "ClockWebSocket connect error: " + e.toString()); } } public void disconnect() { displayClock = false; try { mConnection.disconnect(); } catch (Exception e) { Log.d("Engine_Driver", "ClockWebSocket disconnect error: " + e.toString()); } } public void refresh() { currentTime = ""; try { displayClockHrs = Integer.parseInt(prefs.getString("ClockDisplayTypePreference", "0")); } catch (NumberFormatException e) { displayClockHrs = 0; } if (displayClockHrs > 0) { if (mConnection.isConnected()) this.disconnect(); this.connect(); } else { this.disconnect(); } } } } /** * Display OnGoing Notification that indicates EngineDriver is Running. * Should only be called when ED is going into the background. * Currently call this from each activity onPause, passing the current intent * to return to when reopening. */ void addNotification(Intent notificationIntent) { NotificationCompat.Builder builder = new NotificationCompat.Builder(this).setSmallIcon(R.drawable.icon) .setContentTitle(this.getString(R.string.notification_title)) .setContentText(this.getString(R.string.notification_text)).setOngoing(true); PendingIntent contentIntent = PendingIntent.getActivity(this, ED_NOTIFICATION_ID, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); builder.setContentIntent(contentIntent); // Add as notification NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.notify(ED_NOTIFICATION_ID, builder.build()); } // Remove notification void removeNotification() { NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.cancel(ED_NOTIFICATION_ID); } @Override public void onCreate() { super.onCreate(); Log.d("Engine_Driver", "TA.onCreate()"); //When starting ED after it has been killed in the bkg, the OS restarts any activities that were running. //Since we aren't connected at this point, we want all those activities to finish() so we do 2 things: // doFinish=true tells activities (except CA) that aren't running yet to finish() when they reach onResume() // DISCONNECT message tells any activities (except CA) that are already running to finish() doFinish = true; port = 0; //indicate that no connection exists commThread = new comm_thread(); commThread.start(); alert_activities(message_type.DISCONNECT, ""); /***future Recovery //Normally CA is run via the manifest when ED is launched. //However when starting ED after it has been killed in the bkg, //CA may not be running (or may not be on top). //We need to ensure CA is running at this point in the code, //so start CA if it is not running else bring to top if already running. final Intent caIntent = new Intent(this, connection_activity.class); caIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(caIntent); ***/ androidVersion = android.os.Build.VERSION.SDK_INT; prefs = getSharedPreferences("jmri.enginedriver_preferences", 0); function_states_T = new boolean[32]; function_states_S = new boolean[32]; function_states_G = new boolean[32]; dlMetadataTask = new DownloadMetaTask(); dlRosterTask = new DownloadRosterTask(); //use worker thread to initialize default function labels from file so UI can continue new Thread(new Runnable() { public void run() { set_default_function_labels(); } }, "DefaultFunctionLabels").start(); CookieSyncManager.createInstance(this); //create this here so onPause/onResume for webViews can control it } public boolean isForcingFinish() { return doFinish; } public void cancelForcingFinish() { doFinish = false; } //init default function labels from the settings files or set to default private void set_default_function_labels() { function_labels_default = new LinkedHashMap<Integer, String>(); try { File sdcard_path = Environment.getExternalStorageDirectory(); File settings_file = new File(sdcard_path + "/engine_driver/function_settings.txt"); if (settings_file.exists()) { //if file found, use it for settings arrays BufferedReader settings_reader = new BufferedReader(new FileReader(settings_file)); //read settings into local arrays while (settings_reader.ready()) { String line = settings_reader.readLine(); String temp[] = line.split(":"); function_labels_default.put(Integer.parseInt(temp[1]), temp[0]); //put funcs and labels into global default } settings_reader.close(); } else { //hard-code some buttons and default the rest function_labels_default.put(0, "Light"); function_labels_default.put(1, "Bell"); function_labels_default.put(2, "Horn"); for (int k = 3; k <= 28; k++) { function_labels_default.put(k, Integer.toString(k)); //String.format("%d",k)); } } } catch (IOException except) { Log.e("settings_activity", "Could not read file " + except.getMessage()); } } public class DownloadRosterTask extends DownloadDataTask { @SuppressWarnings("unchecked") @Override void runMethod(Download dl) throws IOException { String rosterUrl = createUrl("roster/?format=xml"); HashMap<String, RosterEntry> rosterTemp = null; if (rosterUrl == null || rosterUrl == "" || dl.cancel) return; Log.d("Engine_Driver", "Background loading roster from " + rosterUrl); int rosterSize = 0; try { RosterLoader rl = new RosterLoader(rosterUrl); if (dl.cancel) return; rosterTemp = rl.parse(); rosterSize = rosterTemp.size(); //throws exception if still null if (!dl.cancel) roster = (HashMap<String, RosterEntry>) rosterTemp.clone(); } catch (Exception e) { throw new IOException(); } Log.d("Engine_Driver", "Loaded " + rosterSize + " entries from roster.xml."); } } public class DownloadMetaTask extends DownloadDataTask { @SuppressWarnings("unchecked") @Override void runMethod(Download dl) throws IOException { String metaUrl = createUrl("json/metadata"); if (metaUrl == null || metaUrl == "" || dl.cancel) return; Log.d("Engine_Driver", "Background loading metadata from " + metaUrl); HttpClient Client = new DefaultHttpClient(); HttpGet httpget = new HttpGet(metaUrl); ResponseHandler<String> responseHandler = new BasicResponseHandler(); String jsonResponse = ""; jsonResponse = Client.execute(httpget, responseHandler); Log.d("Engine_Driver", "Raw metadata retrieved: " + jsonResponse); HashMap<String, String> metadataTemp = new HashMap<String, String>(); try { JSONArray ja = new JSONArray(jsonResponse); for (int i = 0; i < ja.length(); i++) { JSONObject j = ja.optJSONObject(i); String metadataName = j.getJSONObject("data").getString("name"); String metadataValue = j.getJSONObject("data").getString("value"); metadataTemp.put(metadataName, metadataValue); } } catch (JSONException e) { Log.d("Engine_Driver", "exception trying to retrieve json metadata."); } catch (Exception e) { throw new IOException(); } if (metadataTemp.size() == 0) { Log.d("Engine_Driver", "did not retrieve any json metadata entries."); } else { metadata = (HashMap<String, String>) metadataTemp.clone(); // save the metadata in global variable Log.d("Engine_Driver", "Loaded " + metadata.size() + " metadata entries from json web server."); } } } abstract public class DownloadDataTask { private Download dl = null; abstract void runMethod(Download dl) throws IOException; public class Download extends Thread { public volatile boolean cancel = false; @Override public void run() { try { runMethod(this); if (!cancel) sendMsg(comm_msg_handler, message_type.ROSTER_UPDATE); //send message to alert other activities } catch (Throwable t) { Log.d("Engine_Driver", "Data fetch failed: " + t.getMessage()); } // background load of Data completed finally { if (cancel) Log.d("Engine_Driver", "Data fetch cancelled"); } } Download() { super("DownloadData"); } } void get() { if (dl != null) { dl.cancel = true; // try to stop any update that is in progress on old download thread } dl = new Download(); // create new download thread dl.start(); // start an update } void stop() { if (dl != null) { dl.cancel = true; } } } // get the roster name from address string 123(L). Return input if not found in roster or in consist public String getRosterNameFromAddress(String response_str) { if ((roster_entries != null) && (roster_entries.size() > 0)) { for (String rostername : roster_entries.keySet()) { // loop thru roster entries, if (roster_entries.get(rostername).equals(response_str)) { //looking for value = input parm return rostername; //if found, return the roster name (key) } } } if ((consist_entries != null) && (consist_entries.size() > 0)) { String consistname = consist_entries.get(response_str); //consists are keyed by address "123(L)" if (consistname != null) { //looking for value = input parm return consistname; //if found, return the consist name (value) } } return response_str; //return input if not found } //initialize shared variables private void initShared() { withrottle_version = 0.0; web_server_port = 0; power_state = null; to_states = null; to_system_names = null; to_user_names = null; to_state_names = null; rt_states = null; rt_system_names = null; rt_user_names = null; rt_state_names = null; consistT = new Consist(); consistS = new Consist(); consistG = new Consist(); function_labels_S = new LinkedHashMap<Integer, String>(); function_labels_T = new LinkedHashMap<Integer, String>(); function_labels_G = new LinkedHashMap<Integer, String>(); function_states_T = new boolean[32]; // also allocated in onCreate() ??? function_states_S = new boolean[32]; function_states_G = new boolean[32]; consist_entries = new LinkedHashMap<String, String>(); roster = null; roster_entries = null; metadata = null; doFinish = false; turnouts_list_position = 0; routes_list_position = 0; } // // utilities // /** ------ copied from jmri util code ------------------- * Split a string into an array of Strings, at a particular * divider. This is similar to the new String.split method, * except that this does not provide regular expression * handling; the divider string is just a string. * @param input String to split * @param divider Where to divide the input; this does not appear in output */ static public String[] splitByString(String input, String divider) { int size = 0; String temp = input; // count entries while (temp.length() > 0) { size++; int index = temp.indexOf(divider); if (index < 0) break; // break not found temp = temp.substring(index + divider.length()); if (temp.length() == 0) { // found at end size++; break; } } String[] result = new String[size]; // find entries temp = input; size = 0; while (temp.length() > 0) { int index = temp.indexOf(divider); if (index < 0) break; // done with all but last result[size] = temp.substring(0, index); temp = temp.substring(index + divider.length()); size++; } result[size] = temp; return result; } public void powerStateMenuButton() { int newState = 1; if ("1".equals(power_state)) { //toggle to opposite value 0=off, 1=on newState = 0; } sendMsg(comm_msg_handler, message_type.POWER_CONTROL, "", newState); } //TODO: get power_state from JMRI WiThrottle before UI starts up to display Power Layout Icon. //Then can remove displayPowerStateMenuButton2. // Also change in throttle. public void displayPowerStateMenuButton2(Menu menu) { if (prefs.getBoolean("show_layout_power_button_preference", false)) { menu.findItem(R.id.power_layout_button).setVisible(true); } else { menu.findItem(R.id.power_layout_button).setVisible(false); } } public void displayPowerStateMenuButton(Menu menu) { if (prefs.getBoolean("show_layout_power_button_preference", false) && (power_state != null)) { menu.findItem(R.id.power_layout_button).setVisible(true); } else { menu.findItem(R.id.power_layout_button).setVisible(false); } } public void setPowerStateButton(Menu menu) { if (menu != null) { if ((power_state == null) || (power_state.equals("2"))) { menu.findItem(R.id.power_layout_button).setIcon(R.drawable.power_yellow); if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { menu.findItem(R.id.power_layout_button).setTitle("Layout Power is UnKnown"); } } else if (power_state.equals("1")) { menu.findItem(R.id.power_layout_button).setIcon(R.drawable.power_green); if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { menu.findItem(R.id.power_layout_button).setTitle("Layout Power is ON"); } } else { menu.findItem(R.id.power_layout_button).setIcon(R.drawable.power_red); if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { menu.findItem(R.id.power_layout_button).setTitle("Layout Power is Off"); } } } } public void displayEStop(Menu menu) { if (prefs.getBoolean("show_emergency_stop_menu_preference", false)) { menu.findItem(R.id.EmerStop).setVisible(true); } else { menu.findItem(R.id.EmerStop).setVisible(false); } } public void sendEStopMsg() { if (withrottle_version >= 2.0) { if (consistT.isActive()) { sendMsg(comm_msg_handler, message_type.ESTOP, "", (int) 'T'); } if (consistS.isActive()) { sendMsg(comm_msg_handler, message_type.ESTOP, "", (int) 'S'); } if (consistG.isActive()) { sendMsg(comm_msg_handler, message_type.ESTOP, "", (int) 'G'); } } EStopActivated = true; } // forward a message to all running activities void alert_activities(int msgType, String msgBody) { try { sendMsg(connection_msg_handler, msgType, msgBody); } catch (Exception e) { } try { sendMsg(turnouts_msg_handler, msgType, msgBody); } catch (Exception e) { } try { sendMsg(routes_msg_handler, msgType, msgBody); } catch (Exception e) { } try { sendMsg(consist_edit_msg_handler, msgType, msgBody); } catch (Exception e) { } try { sendMsg(throttle_msg_handler, msgType, msgBody); } catch (Exception e) { } try { sendMsg(web_msg_handler, msgType, msgBody); } catch (Exception e) { } try { sendMsg(power_control_msg_handler, msgType, msgBody); } catch (Exception e) { } try { sendMsg(reconnect_status_msg_handler, msgType, msgBody); } catch (Exception e) { } try { sendMsg(select_loco_msg_handler, msgType, msgBody); } catch (Exception e) { } } public boolean sendMsg(Handler h, int msgType) { return sendMsgDelay(h, 0, msgType, "", 0, 0); } public boolean sendMsg(Handler h, int msgType, String msgBody) { return sendMsgDelay(h, 0, msgType, msgBody, 0, 0); } public boolean sendMsg(Handler h, int msgType, String msgBody, int msgArg1) { return sendMsgDelay(h, 0, msgType, msgBody, msgArg1, 0); } public boolean sendMsg(Handler h, int msgType, String msgBody, int msgArg1, int msgArg2) { return sendMsgDelay(h, 0, msgType, msgBody, msgArg1, msgArg2); } public boolean sendMsgDelay(Handler h, long delayMs, int msgType) { return sendMsgDelay(h, delayMs, msgType, "", 0, 0); } public boolean sendMsgDelay(Handler h, long delayMs, int msgType, String msgBody, int msgArg1, int msgArg2) { boolean sent = false; if (h != null) { Message msg = Message.obtain(); msg.what = msgType; msg.obj = new String(msgBody); msg.arg1 = msgArg1; msg.arg2 = msgArg2; try { sent = h.sendMessageDelayed(msg, delayMs); } catch (Exception e) { } if (!sent) msg.recycle(); } return sent; } // // methods for use by Activities // // build a full url // returns: full url if web_server_port is valid // null otherwise public String createUrl(String defaultUrl) { String url = ""; int port = web_server_port; if (port > 0) { if (defaultUrl.startsWith("http")) { //if url starts with http, use it as is url = defaultUrl; } else { //, else prepend servername and port and slash if needed url = "http://" + host_ip + ":" + port + (defaultUrl.startsWith("/") ? "" : "/") + defaultUrl; } } return url; } // build a full uri // returns: full uri if webServerPort is valid // null otherwise public String createUri() { String uri = ""; int port = web_server_port; if (port > 0) { uri = "ws://" + host_ip + ":" + port + "/json/"; } return uri; } /** * Set activity screen orientation based on prefs, check to avoid sending change when already there. * checks "auto Web on landscape" preference and returns false if orientation requires activity switch * * @param activity calling activity * @param webPref if absent or false, uses Throttle Orientation pref. * if true, uses Web Orientation pref * * @return true if the new orientation is ok for this activity. * false if "Auto Web on Landscape" is enabled and new orientation requires activity switch * */ public boolean setActivityOrientation(Activity activity) { return setActivityOrientation(activity, false); } public boolean setActivityOrientation(Activity activity, Boolean webPref) { String to; to = prefs.getString("ThrottleOrientation", activity.getApplicationContext().getResources() .getString(R.string.prefThrottleOrientationDefaultValue)); if (to.equals("Auto-Web")) { int orient = activity.getResources().getConfiguration().orientation; if ((webPref && orient == Configuration.ORIENTATION_PORTRAIT) || (!webPref && orient == Configuration.ORIENTATION_LANDSCAPE)) return (false); } else if (webPref) { to = prefs.getString("WebOrientation", activity.getApplicationContext().getResources() .getString(R.string.prefWebOrientationDefaultValue)); } int co = activity.getRequestedOrientation(); if (to.equals("Landscape") && (co != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)) activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); else if (to.equals("Auto-Rotate") && (co != ActivityInfo.SCREEN_ORIENTATION_SENSOR)) activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); else if (to.equals("Portrait") && (co != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)) activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); return true; } // prompt for Exit // must be called on the UI thread public void checkExit(final Activity activity) { final AlertDialog.Builder b = new AlertDialog.Builder(activity); b.setIcon(android.R.drawable.ic_dialog_alert); b.setTitle(R.string.exit_title); b.setMessage(R.string.exit_text); b.setCancelable(true); b.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { firstCreate = true; sendMsg(comm_msg_handler, message_type.DISCONNECT, ""); //trigger disconnect / shutdown sequence } }); b.setNegativeButton(R.string.no, null); AlertDialog alert = b.create(); alert.show(); } }