1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.chromium.latency.walt; 18 19 import android.content.Context; 20 import android.os.Handler; 21 import android.os.HandlerThread; 22 import android.util.Log; 23 24 import java.io.IOException; 25 import java.io.InputStream; 26 import java.io.OutputStream; 27 import java.net.InetAddress; 28 import java.net.InetSocketAddress; 29 import java.net.Socket; 30 import java.net.SocketTimeoutException; 31 32 33 public class WaltTcpConnection implements WaltConnection { 34 35 // Use a "reverse" port over adb. The server is running on the host to which we're attached. 36 private static final String SERVER_IP = "127.0.0.1"; 37 private static final int SERVER_PORT = 50007; 38 private static final int TCP_READ_TIMEOUT_MS = 200; 39 40 private final SimpleLogger logger; 41 private HandlerThread networkThread; 42 private Handler networkHandler; 43 private final Object readLock = new Object(); 44 private boolean messageReceived = false; 45 private Utils.ListenerState connectionState = Utils.ListenerState.STOPPED; 46 private int lastRetVal; 47 static final int BUFF_SIZE = 1024 * 4; 48 private byte[] buffer = new byte[BUFF_SIZE]; 49 50 private final Handler mainHandler = new Handler(); 51 private RemoteClockInfo remoteClock = new RemoteClockInfo(); 52 53 private Socket socket; 54 private OutputStream outputStream = null; 55 private InputStream inputStream = null; 56 57 private WaltConnection.ConnectionStateListener connectionStateListener; 58 59 // Singleton stuff 60 private static WaltTcpConnection instance; 61 private static final Object LOCK = new Object(); 62 getInstance(Context context)63 public static WaltTcpConnection getInstance(Context context) { 64 synchronized (LOCK) { 65 if (instance == null) { 66 instance = new WaltTcpConnection(context.getApplicationContext()); 67 } 68 return instance; 69 } 70 } 71 WaltTcpConnection(Context context)72 private WaltTcpConnection(Context context) { 73 logger = SimpleLogger.getInstance(context); 74 } 75 connect()76 public void connect() { 77 // If the singleton is already connected, do not kill the connection. 78 if (isConnected()) { 79 return; 80 } 81 connectionState = Utils.ListenerState.STARTING; 82 networkThread = new HandlerThread("NetworkThread"); 83 networkThread.start(); 84 networkHandler = new Handler(networkThread.getLooper()); 85 logger.log("Started network thread for TCP bridge"); 86 networkHandler.post(new Runnable() { 87 @Override 88 public void run() { 89 try { 90 InetAddress serverAddr = InetAddress.getByName(SERVER_IP); 91 socket = new Socket(serverAddr, SERVER_PORT); 92 socket.setKeepAlive(true); 93 socket.setSoTimeout(TCP_READ_TIMEOUT_MS); 94 outputStream = socket.getOutputStream(); 95 inputStream = socket.getInputStream(); 96 logger.log("TCP connection established"); 97 connectionState = Utils.ListenerState.RUNNING; 98 } catch (Exception e) { 99 e.printStackTrace(); 100 logger.log("Can't connect to TCP bridge: " + e.getMessage()); 101 connectionState = Utils.ListenerState.STOPPED; 102 return; 103 } 104 105 // Run the onConnect callback, but on main thread. 106 mainHandler.post(new Runnable() { 107 @Override 108 public void run() { 109 WaltTcpConnection.this.onConnect(); 110 } 111 }); 112 } 113 }); 114 115 } 116 onConnect()117 public void onConnect() { 118 if (connectionStateListener != null) { 119 connectionStateListener.onConnect(); 120 } 121 } 122 isConnected()123 public synchronized boolean isConnected() { 124 return connectionState == Utils.ListenerState.RUNNING; 125 } 126 sendByte(final char c)127 public void sendByte(final char c) throws IOException { 128 // All network accesses must occur on a separate thread. 129 networkHandler.post(new Runnable() { 130 @Override 131 public void run() { 132 try { 133 outputStream.write(Utils.char2byte(c)); 134 } catch (IOException e) { 135 e.printStackTrace(); 136 } 137 } 138 }); 139 } 140 sendString(final String s)141 public void sendString(final String s) throws IOException { 142 // All network accesses must occur on a separate thread. 143 networkHandler.post(new Runnable() { 144 @Override 145 public void run() { 146 try { 147 outputStream.write(s.getBytes("UTF-8")); 148 } catch (IOException e) { 149 e.printStackTrace(); 150 } 151 } 152 }); 153 } 154 blockingRead(byte[] buff)155 public synchronized int blockingRead(byte[] buff) { 156 157 messageReceived = false; 158 159 // All network accesses must occur on a separate thread. 160 networkHandler.post(new Runnable() { 161 @Override 162 public void run() { 163 lastRetVal = -1; 164 try { 165 synchronized (readLock) { 166 lastRetVal = inputStream.read(buffer); 167 messageReceived = true; 168 readLock.notifyAll(); 169 } 170 } catch (SocketTimeoutException e) { 171 messageReceived = true; 172 lastRetVal = -2; 173 } 174 catch (Exception e) { 175 e.printStackTrace(); 176 messageReceived = true; 177 lastRetVal = -1; 178 // TODO: better messaging / error handling here 179 } 180 } 181 }); 182 183 // TODO: make sure length is ok 184 // This blocks on readLock which is taken by the blocking read operation 185 try { 186 synchronized (readLock) { 187 while (!messageReceived) readLock.wait(TCP_READ_TIMEOUT_MS); 188 } 189 } catch (InterruptedException e) { 190 return -1; 191 } 192 193 if (lastRetVal > 0) { 194 System.arraycopy(buffer, 0, buff, 0, lastRetVal); 195 } 196 197 return lastRetVal; 198 } 199 updateClock(String cmd)200 private synchronized void updateClock(String cmd) throws IOException { 201 sendString(cmd); 202 int retval = blockingRead(buffer); 203 if (retval <= 0) { 204 throw new IOException("WaltTcpConnection, can't sync clocks"); 205 } 206 String s = new String(buffer, 0, retval); 207 String[] parts = s.trim().split("\\s+"); 208 // TODO: make sure reply starts with "clock" 209 // The bridge sends the time difference between when it sent the reply and when it zeroed 210 // the WALT's clock. We assume here that the reply transit time is negligible. 211 remoteClock.baseTime = RemoteClockInfo.microTime() - Long.parseLong(parts[1]); 212 remoteClock.minLag = Integer.parseInt(parts[2]); 213 remoteClock.maxLag = Integer.parseInt(parts[3]); 214 } 215 syncClock()216 public RemoteClockInfo syncClock() throws IOException { 217 updateClock("bridge sync"); 218 logger.log("Synced clocks via TCP bridge:\n" + remoteClock); 219 return remoteClock; 220 } 221 updateLag()222 public void updateLag() { 223 try { 224 updateClock("bridge update"); 225 } catch (IOException e) { 226 logger.log("Failed to update clock lag: " + e.getMessage()); 227 } 228 } 229 setConnectionStateListener(ConnectionStateListener connectionStateListener)230 public void setConnectionStateListener(ConnectionStateListener connectionStateListener) { 231 this.connectionStateListener = connectionStateListener; 232 } 233 234 // A way to test if there is a TCP bridge to decide whether to use it. 235 // Some thread dancing to get around the Android strict policy for no network on main thread. probe()236 public static boolean probe() { 237 ProbeThread probeThread = new ProbeThread(); 238 probeThread.start(); 239 try { 240 probeThread.join(); 241 } catch (Exception e) { 242 e.printStackTrace(); 243 } 244 return probeThread.isReachable; 245 } 246 247 private static class ProbeThread extends Thread { 248 public boolean isReachable = false; 249 private final String TAG = "ProbeThread"; 250 251 @Override run()252 public void run() { 253 Socket socket = new Socket(); 254 try { 255 InetSocketAddress remoteAddr = new InetSocketAddress(SERVER_IP, SERVER_PORT); 256 socket.connect(remoteAddr, 50 /* timeout in milliseconds */); 257 isReachable = true; 258 socket.close(); 259 } catch (Exception e) { 260 Log.i(TAG, "Probing TCP connection failed: " + e.getMessage()); 261 } 262 } 263 } 264 } 265