/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.appium.android.bootstrap;
import com.android.uiautomator.common.UiWatchers;
import io.appium.android.bootstrap.exceptions.CommandTypeException;
import io.appium.android.bootstrap.exceptions.SocketServerException;
import io.appium.android.bootstrap.handler.UpdateStrings;
import io.appium.android.bootstrap.utils.TheWatchers;
import org.json.JSONException;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Timer;
import java.util.TimerTask;
import java.util.NoSuchElementException;
/**
* The SocketServer class listens on a specific port for commands from Appium,
* and then passes them on to the {@link AndroidCommandExecutor} class. It will
* continue to listen until the command is sent to exit.
*/
class SocketServer {
ServerSocket server;
Socket client;
BufferedReader in;
BufferedWriter out;
boolean keepListening;
private final AndroidCommandExecutor executor;
private final TheWatchers watchers = TheWatchers.getInstance();
private final Timer timer = new Timer("WatchTimer");
/**
* Constructor
*
* @param port
* @throws SocketServerException
*/
public SocketServer(final int port) throws SocketServerException {
keepListening = true;
executor = new AndroidCommandExecutor();
try {
server = new ServerSocket(port);
Logger.debug("Socket opened on port " + port);
} catch (final IOException e) {
throw new SocketServerException(
"Could not start socket server listening on " + port);
}
}
/**
* Constructs an @{link AndroidCommand} and returns it.
*
* @param data
* @return @{link AndroidCommand}
* @throws JSONException
* @throws CommandTypeException
*/
private AndroidCommand getCommand(final String data) throws JSONException,
CommandTypeException {
return new AndroidCommand(data);
}
private StringBuilder input = new StringBuilder();
/**
* When data is available on the socket, this method is called to run the
* command or throw an error if it can't.
*
* @throws SocketServerException
*/
private void handleClientData() throws SocketServerException {
try {
input.setLength(0); // clear
String res;
int a;
// (char) -1 is not equal to -1.
// ready is checked to ensure the read call doesn't block.
while ((a = in.read()) != -1 && in.ready()) {
input.append((char) a);
}
String inputString = input.toString();
Logger.debug("Got data from client: " + inputString);
try {
AndroidCommand cmd = getCommand(inputString);
Logger.debug("Got command of type " + cmd.commandType().toString());
res = runCommand(cmd);
Logger.debug("Returning result: " + res);
} catch (final CommandTypeException e) {
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage())
.toString();
} catch (final JSONException e) {
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR,
"Error running and parsing command").toString();
}
out.write(res);
out.flush();
} catch (final IOException e) {
throw new SocketServerException("Error processing data to/from socket ("
+ e.toString() + ")");
}
}
/**
* Listens on the socket for data, and calls {@link #handleClientData()} when
* it's available.
*
* @throws SocketServerException
*/
public void listenForever(boolean disableAndroidWatchers, boolean acceptSSLCerts) throws SocketServerException {
Logger.debug("Appium Socket Server Ready");
UpdateStrings.loadStringsJson();
if (disableAndroidWatchers) {
Logger.debug("Skipped registering crash watchers.");
} else {
dismissCrashAlerts();
final TimerTask updateWatchers = new TimerTask() {
@Override
public void run() {
try {
watchers.check();
} catch (final Exception e) {
}
}
};
timer.scheduleAtFixedRate(updateWatchers, 100, 100);
}
if (acceptSSLCerts) {
Logger.debug("Accepting SSL certificate errors.");
acceptSSLCertificates();
}
try {
client = server.accept();
Logger.debug("Client connected");
in = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8"));
out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(), "UTF-8"));
while (keepListening) {
handleClientData();
}
in.close();
out.close();
client.close();
Logger.debug("Closed client connection");
} catch (final IOException e) {
throw new SocketServerException("Error when client was trying to connect");
}
}
public void dismissCrashAlerts() {
try {
new UiWatchers().registerAnrAndCrashWatchers();
Logger.debug("Registered crash watchers.");
} catch (Exception e) {
Logger.debug("Unable to register crash watchers.");
}
}
public void acceptSSLCertificates() {
try {
new UiWatchers().registerAcceptSSLCertWatcher();
Logger.debug("Registered SSL certificate error watcher.");
} catch (Exception e) {
Logger.debug("Unable to register SSL certificate error watcher.");
}
}
/**
* When {@link #handleClientData()} has valid data, this method delegates the
* command.
*
* @param cmd
* AndroidCommand
* @return Result
*/
private String runCommand(final AndroidCommand cmd) {
AndroidCommandResult res;
if (cmd.commandType() == AndroidCommandType.SHUTDOWN) {
keepListening = false;
res = new AndroidCommandResult(WDStatus.SUCCESS, "OK, shutting down");
} else if (cmd.commandType() == AndroidCommandType.ACTION) {
try {
res = executor.execute(cmd);
} catch (final NoSuchElementException e) {
res = new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage());
} catch (final Exception e) { // Here we catch all possible exceptions and return a JSON Wire Protocol UnknownError
// This prevents exceptions from halting the bootstrap app
Logger.debug("Command returned error:" + e);
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
}
} else {
// this code should never be executed, here for future-proofing
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR,
"Unknown command type, could not execute!");
}
return res.toString();
}
}