diff --git a/plugins/sonic-android-apk.apk b/plugins/sonic-android-apk.apk index 58999bc2..ff7233ea 100644 Binary files a/plugins/sonic-android-apk.apk and b/plugins/sonic-android-apk.apk differ diff --git a/pom.xml b/pom.xml index 1aeeff10..668f4bf3 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.cloud.sonic sonic-agent - 1.3.1-beta + 1.3.1-release jar diff --git a/src/main/java/org/cloud/sonic/agent/automation/AndroidStepHandler.java b/src/main/java/org/cloud/sonic/agent/automation/AndroidStepHandler.java index 6979fc6e..98f45598 100644 --- a/src/main/java/org/cloud/sonic/agent/automation/AndroidStepHandler.java +++ b/src/main/java/org/cloud/sonic/agent/automation/AndroidStepHandler.java @@ -533,7 +533,7 @@ public void install(HandleDes handleDes, String path) { try { androidDriver.installApp(path, new AndroidInstallApplicationOptions() .withAllowTestPackagesEnabled().withReplaceEnabled() - .withGrantPermissionsEnabled().withTimeout(Duration.ofMillis(60000))); + .withGrantPermissionsEnabled().withTimeout(Duration.ofMillis(600000))); } catch (Exception e) { handleDes.setE(e); return; @@ -822,7 +822,7 @@ public void tap(HandleDes handleDes, String des, String xy) { } } - public void swipe(HandleDes handleDes, String des1, String xy1, String des2, String xy2) { + public void swipePoint(HandleDes handleDes, String des1, String xy1, String des2, String xy2) { int x1 = Integer.parseInt(xy1.substring(0, xy1.indexOf(","))); int y1 = Integer.parseInt(xy1.substring(xy1.indexOf(",") + 1)); int x2 = Integer.parseInt(xy2.substring(0, xy2.indexOf(","))); @@ -837,6 +837,23 @@ public void swipe(HandleDes handleDes, String des1, String xy1, String des2, Str } } + public void swipe(HandleDes handleDes, String des, String selector, String pathValue, String des2, String selector2, String pathValue2) { + WebElement webElement = findEle(selector, pathValue); + WebElement webElement2 = findEle(selector2, pathValue2); + int x1 = webElement.getLocation().getX(); + int y1 = webElement.getLocation().getY(); + int x2 = webElement2.getLocation().getX(); + int y2 = webElement2.getLocation().getY(); + handleDes.setStepDes("滑动拖拽" + des + "到" + des2); + handleDes.setDetail("拖动坐标(" + x1 + "," + y1 + ")到(" + x2 + "," + y2 + ")"); + try { + TouchAction ta = new TouchAction(androidDriver); + ta.press(PointOption.point(x1, y1)).waitAction(WaitOptions.waitOptions(Duration.ofMillis(300))).moveTo(PointOption.point(x2, y2)).release().perform(); + } catch (Exception e) { + handleDes.setE(e); + } + } + public void longPress(HandleDes handleDes, String des, String selector, String pathValue, int time) { handleDes.setStepDes("长按" + des); handleDes.setDetail("长按控件元素" + time + "毫秒 "); @@ -1440,9 +1457,13 @@ public void runStep(JSONObject stepJSON) throws Throwable { , eleList.getJSONObject(0).getString("eleValue"), Integer.parseInt(step.getString("content"))); break; case "swipe": - swipe(handleDes, eleList.getJSONObject(0).getString("eleName"), eleList.getJSONObject(0).getString("eleValue") + swipePoint(handleDes, eleList.getJSONObject(0).getString("eleName"), eleList.getJSONObject(0).getString("eleValue") , eleList.getJSONObject(1).getString("eleName"), eleList.getJSONObject(1).getString("eleValue")); break; + case "swipe2": + swipe(handleDes, eleList.getJSONObject(0).getString("eleName"), eleList.getJSONObject(0).getString("eleType"), eleList.getJSONObject(0).getString("eleValue") + , eleList.getJSONObject(1).getString("eleName"), eleList.getJSONObject(1).getString("eleType"), eleList.getJSONObject(1).getString("eleValue")); + break; case "tap": tap(handleDes, eleList.getJSONObject(0).getString("eleName"), eleList.getJSONObject(0).getString("eleValue")); break; diff --git a/src/main/java/org/cloud/sonic/agent/automation/IOSStepHandler.java b/src/main/java/org/cloud/sonic/agent/automation/IOSStepHandler.java index 5851752f..9cf71231 100644 --- a/src/main/java/org/cloud/sonic/agent/automation/IOSStepHandler.java +++ b/src/main/java/org/cloud/sonic/agent/automation/IOSStepHandler.java @@ -107,7 +107,7 @@ public void startIOSDriver(String udId, int wdaPort) throws InterruptedException iosDriver.setSetting(Setting.MJPEG_SERVER_FRAMERATE, 50); iosDriver.setSetting(Setting.MJPEG_SCALING_FACTOR, 50); iosDriver.setSetting(Setting.MJPEG_SERVER_SCREENSHOT_QUALITY, 10); - iosDriver.setSetting("snapshotMaxDepth",30); + iosDriver.setSetting("snapshotMaxDepth", 30); log.sendStepLog(StepType.PASS, "连接设备驱动成功", ""); } catch (Exception e) { log.sendStepLog(StepType.ERROR, "连接设备驱动失败!", ""); @@ -517,7 +517,7 @@ public void tap(HandleDes handleDes, String des, String xy) { } } - public void swipe(HandleDes handleDes, String des1, String xy1, String des2, String xy2) { + public void swipePoint(HandleDes handleDes, String des1, String xy1, String des2, String xy2) { int x1 = Integer.parseInt(xy1.substring(0, xy1.indexOf(","))); int y1 = Integer.parseInt(xy1.substring(xy1.indexOf(",") + 1)); int x2 = Integer.parseInt(xy2.substring(0, xy2.indexOf(","))); @@ -532,6 +532,23 @@ public void swipe(HandleDes handleDes, String des1, String xy1, String des2, Str } } + public void swipe(HandleDes handleDes, String des, String selector, String pathValue, String des2, String selector2, String pathValue2) { + WebElement webElement = findEle(selector, pathValue); + WebElement webElement2 = findEle(selector2, pathValue2); + int x1 = webElement.getLocation().getX(); + int y1 = webElement.getLocation().getY(); + int x2 = webElement2.getLocation().getX(); + int y2 = webElement2.getLocation().getY(); + handleDes.setStepDes("滑动拖拽" + des + "到" + des2); + handleDes.setDetail("拖动坐标(" + x1 + "," + y1 + ")到(" + x2 + "," + y2 + ")"); + try { + TouchAction ta = new TouchAction(iosDriver); + ta.press(PointOption.point(x1, y1)).waitAction(WaitOptions.waitOptions(Duration.ofMillis(300))).moveTo(PointOption.point(x2, y2)).release().perform(); + } catch (Exception e) { + handleDes.setE(e); + } + } + public void longPress(HandleDes handleDes, String des, String selector, String pathValue, int time) { handleDes.setStepDes("长按" + des); handleDes.setDetail("长按控件元素" + time + "毫秒 "); @@ -830,9 +847,13 @@ public void runStep(JSONObject stepJSON) throws Throwable { , eleList.getJSONObject(0).getString("eleValue"), Integer.parseInt(step.getString("content"))); break; case "swipe": - swipe(handleDes, eleList.getJSONObject(0).getString("eleName"), eleList.getJSONObject(0).getString("eleValue") + swipePoint(handleDes, eleList.getJSONObject(0).getString("eleName"), eleList.getJSONObject(0).getString("eleValue") , eleList.getJSONObject(1).getString("eleName"), eleList.getJSONObject(1).getString("eleValue")); break; + case "swipe2": + swipe(handleDes, eleList.getJSONObject(0).getString("eleName"), eleList.getJSONObject(0).getString("eleType"), eleList.getJSONObject(0).getString("eleValue") + , eleList.getJSONObject(1).getString("eleName"), eleList.getJSONObject(1).getString("eleType"), eleList.getJSONObject(1).getString("eleValue")); + break; case "tap": tap(handleDes, eleList.getJSONObject(0).getString("eleName"), eleList.getJSONObject(0).getString("eleValue")); break; diff --git a/src/main/java/org/cloud/sonic/agent/bridge/android/AndroidDeviceBridgeTool.java b/src/main/java/org/cloud/sonic/agent/bridge/android/AndroidDeviceBridgeTool.java index ebd68a2d..d55b27da 100644 --- a/src/main/java/org/cloud/sonic/agent/bridge/android/AndroidDeviceBridgeTool.java +++ b/src/main/java/org/cloud/sonic/agent/bridge/android/AndroidDeviceBridgeTool.java @@ -12,9 +12,14 @@ import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.Arrays; import java.util.concurrent.TimeUnit; /** @@ -133,7 +138,7 @@ public static void reboot(IDevice iDevice) { public static IDevice getIDeviceByUdId(String udId) { IDevice iDevice = null; IDevice[] iDevices = AndroidDeviceBridgeTool.getRealOnLineDevices(); - if(iDevices.length==0){ + if (iDevices.length == 0) { return null; } for (IDevice device : iDevices) { @@ -202,6 +207,15 @@ public static String executeCommand(IDevice iDevice, String command) { return output.getOutput(); } + public static boolean checkSonicApkVersion(IDevice iDevice) { + String all = executeCommand(iDevice, "dumpsys package org.cloud.sonic.android"); + if (!all.contains("versionName=1.3.2")) { + return false; + } else { + return true; + } + } + /** * @param iDevice * @param port diff --git a/src/main/java/org/cloud/sonic/agent/tests/android/SonicLocalThread.java b/src/main/java/org/cloud/sonic/agent/tests/android/SonicLocalThread.java index fa24a856..00f63c7d 100644 --- a/src/main/java/org/cloud/sonic/agent/tests/android/SonicLocalThread.java +++ b/src/main/java/org/cloud/sonic/agent/tests/android/SonicLocalThread.java @@ -162,7 +162,7 @@ public void run() { if (man == null) { return; } - if (!suc && iDevice != null && man.equals("Xiaomi")) { + if (!suc && iDevice != null && (man.equals("Xiaomi") || man.equals("deltainno") || man.equals("HUAWEI"))) { suc = runMiniCap("Xiaomi"); if (!suc && iDevice != null) { suc = runMiniCap("Xiaomi_NW"); diff --git a/src/main/java/org/cloud/sonic/agent/tools/AgentTool.java b/src/main/java/org/cloud/sonic/agent/tools/AgentTool.java index 2df2e085..d890f2d2 100644 --- a/src/main/java/org/cloud/sonic/agent/tools/AgentTool.java +++ b/src/main/java/org/cloud/sonic/agent/tools/AgentTool.java @@ -52,6 +52,19 @@ public static void sendByte(Session session, byte[] message) { } } + public static void sendByte(Session session, ByteBuffer message) { + if (session == null || !session.isOpen()) { + return; + } + synchronized (session) { + try { + session.getBasicRemote().sendBinary(message); + } catch (IllegalStateException | IOException e) { + log.error("WebSocket发送失败!连接已关闭!"); + } + } + } + public static void sendText(Session session, String message) { if (session == null || !session.isOpen()) { return; diff --git a/src/main/java/org/cloud/sonic/agent/websockets/AndroidWSServer.java b/src/main/java/org/cloud/sonic/agent/websockets/AndroidWSServer.java index adbd031c..89e4ab41 100644 --- a/src/main/java/org/cloud/sonic/agent/websockets/AndroidWSServer.java +++ b/src/main/java/org/cloud/sonic/agent/websockets/AndroidWSServer.java @@ -96,13 +96,15 @@ public void onOpen(Session session, @PathParam("key") String secretKey, .replaceAll("package:", "") .replaceAll("\n", "") .replaceAll("\t", ""); - if (path.length() > 0) { - logger.info("已安装Sonic插件"); + if (path.length() > 0 && AndroidDeviceBridgeTool.checkSonicApkVersion(iDevice)) { + logger.info("已安装Sonic插件,检查版本信息通过"); } else { + logger.info("未安装Sonic插件或版本不是最新,正在安装..."); try { iDevice.installPackage("plugins/sonic-android-apk.apk", true, new InstallReceiver(), 180L, 180L, TimeUnit.MINUTES , "-r", "-t", "-g"); + logger.info("Sonic插件安装完毕"); } catch (InstallException e) { e.printStackTrace(); logger.info("Sonic插件安装失败!"); diff --git a/src/main/java/org/cloud/sonic/agent/websockets/AudioWSServer.java b/src/main/java/org/cloud/sonic/agent/websockets/AudioWSServer.java index e6e1cdef..3ba32e03 100644 --- a/src/main/java/org/cloud/sonic/agent/websockets/AudioWSServer.java +++ b/src/main/java/org/cloud/sonic/agent/websockets/AudioWSServer.java @@ -1,34 +1,69 @@ package org.cloud.sonic.agent.websockets; +import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.android.ddmlib.IDevice; import org.cloud.sonic.agent.bridge.android.AndroidDeviceBridgeTool; -import org.cloud.sonic.agent.bridge.android.AndroidDeviceThreadPool; +import org.cloud.sonic.agent.maps.AndroidAPKMap; import org.cloud.sonic.agent.tools.AgentTool; import org.cloud.sonic.agent.tools.PortTool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import javax.websocket.OnOpen; -import javax.websocket.Session; +import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.nio.ByteBuffer; -import java.util.concurrent.Future; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @Component -@ServerEndpoint(value = "/websockets/audio/{udId}", configurator = MyEndpointConfigure.class) +@ServerEndpoint(value = "/websockets/audio/{key}/{udId}", configurator = MyEndpointConfigure.class) public class AudioWSServer { + private final Logger logger = LoggerFactory.getLogger(AudioWSServer.class); + @Value("${sonic.agent.key}") + private String key; + private Map udIdMap = new ConcurrentHashMap<>(); + private Map audioMap = new ConcurrentHashMap<>(); + @OnOpen - public void onOpen(Session session,@PathParam("udId") String udId) throws Exception { + public void onOpen(Session session, @PathParam("key") String secretKey, @PathParam("udId") String udId) throws Exception { + if (secretKey.length() == 0 || (!secretKey.equals(key))) { + logger.info("拦截访问!"); + return; + } IDevice iDevice = AndroidDeviceBridgeTool.getIDeviceByUdId(udId); + udIdMap.put(session, iDevice); + int wait = 0; + boolean isInstall = true; + while (AndroidAPKMap.getMap().get(udId) == null || (!AndroidAPKMap.getMap().get(udId))) { + Thread.sleep(500); + wait++; + if (wait >= 40) { + isInstall = false; + break; + } + } + if (!isInstall) { + logger.info("等待安装超时!"); + } else { + startAudio(session); + } + } + + private void startAudio(Session session) { + stopAudio(session); + IDevice iDevice = udIdMap.get(session); AndroidDeviceBridgeTool.executeCommand(iDevice, "appops set org.cloud.sonic.android PROJECT_MEDIA allow"); AndroidDeviceBridgeTool.executeCommand(iDevice, "am start -n org.cloud.sonic.android/.AudioActivity"); AndroidDeviceBridgeTool.pressKey(iDevice, 4); int appListPort = PortTool.getPort(); - new Thread(()-> { + Thread audio = new Thread(() -> { try { AndroidDeviceBridgeTool.forward(iDevice, appListPort, "sonicaudioservice"); Socket audioSocket = null; @@ -37,7 +72,7 @@ public void onOpen(Session session,@PathParam("udId") String udId) throws Except audioSocket = new Socket("localhost", appListPort); inputStream = audioSocket.getInputStream(); int len = 1024; - while (audioSocket.isConnected()) { + while (audioSocket.isConnected() && !Thread.interrupted()) { byte[] buffer = new byte[len]; int realLen; realLen = inputStream.read(buffer); @@ -49,7 +84,7 @@ public void onOpen(Session session,@PathParam("udId") String udId) throws Except byteBuffer.put(buffer); byteBuffer.flip(); //bug - sendText(session, byteBuffer); + AgentTool.sendByte(session, byteBuffer); } } } catch (IOException e) { @@ -73,15 +108,62 @@ public void onOpen(Session session,@PathParam("udId") String udId) throws Except } catch (Exception e) { } AndroidDeviceBridgeTool.removeForward(iDevice, appListPort, "sonicaudioservice"); - }).start(); + }); + audio.start(); + audioMap.put(session, audio); } - private void sendText(Session session, ByteBuffer message) { - synchronized (session) { - try { - session.getBasicRemote().sendBinary(message); - } catch (IllegalStateException | IOException e) { + private void stopAudio(Session session) { + if (audioMap.get(session) != null) { + audioMap.get(session).interrupt(); + int wait = 0; + while (!audioMap.get(session).isInterrupted()) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + wait++; + if (wait >= 3) { + break; + } } } + audioMap.remove(session); + } + +// @OnMessage +// public void onMessage(String message, Session session) { +// JSONObject msg = JSON.parseObject(message); +// logger.info(session.getId() + " 发送 " + msg); +// switch (msg.getString("type")) { +// case "start": +// startAudio(session); +// break; +// case "stop": +// stopAudio(session); +// break; +// } +// } + + @OnClose + public void onClose(Session session) { + exit(session); + } + + @OnError + public void onError(Session session, Throwable error) { + logger.error("音频socket发生错误,刷新瞬间可无视:", error); + } + + private void exit(Session session) { + stopAudio(session); + udIdMap.remove(session); + try { + session.close(); + } catch (IOException e) { + e.printStackTrace(); + } + logger.info(session.getId() + "退出"); } } diff --git a/src/main/java/org/cloud/sonic/agent/websockets/TerminalWSServer.java b/src/main/java/org/cloud/sonic/agent/websockets/TerminalWSServer.java index f281a663..5fa086aa 100644 --- a/src/main/java/org/cloud/sonic/agent/websockets/TerminalWSServer.java +++ b/src/main/java/org/cloud/sonic/agent/websockets/TerminalWSServer.java @@ -2,7 +2,8 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; -import com.android.ddmlib.*; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IShellOutputReceiver; import org.cloud.sonic.agent.bridge.android.AndroidDeviceBridgeTool; import org.cloud.sonic.agent.bridge.android.AndroidDeviceThreadPool; import org.cloud.sonic.agent.maps.AndroidAPKMap; @@ -32,6 +33,7 @@ @Component @ServerEndpoint(value = "/websockets/terminal/{key}/{udId}", configurator = MyEndpointConfigure.class) public class TerminalWSServer { + private final Logger logger = LoggerFactory.getLogger(TerminalWSServer.class); @Value("${sonic.agent.key}") private String key; @@ -39,7 +41,6 @@ public class TerminalWSServer { private Map> terminalMap = new ConcurrentHashMap<>(); private Map> appListMap = new ConcurrentHashMap<>(); private Map> logcatMap = new ConcurrentHashMap<>(); - private Map> audioMap = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("key") String secretKey, @PathParam("udId") String udId) throws Exception { @@ -214,15 +215,6 @@ private void exit(Session session) { } } terminalMap.remove(session); - Future audioPro = audioMap.get(session); - if (audioPro != null && (!audioPro.isDone() || !audioPro.isCancelled())) { - try { - audioPro.cancel(true); - } catch (Exception e) { - logger.error(e.getMessage()); - } - } - audioMap.remove(session); Future logcat = logcatMap.get(session); if (!logcat.isDone() || !logcat.isCancelled()) { try { @@ -232,6 +224,7 @@ private void exit(Session session) { } } logcatMap.remove(session); + udIdMap.remove(session); try { session.close(); } catch (IOException e) { @@ -250,69 +243,6 @@ private void sendText(Session session, String message) { } } -// public void getAudio(IDevice iDevice, Session session) { -// Future audioPro = audioMap.get(session); -// if (audioPro != null && (!audioPro.isDone() || !audioPro.isCancelled())) { -// try { -// audioPro.cancel(true); -// } catch (Exception e) { -// logger.error(e.getMessage()); -// } -// } -// audioPro = AndroidDeviceThreadPool.cachedThreadPool.submit(() -> { -// AndroidDeviceBridgeTool.executeCommand(iDevice, "appops set org.cloud.sonic.android PROJECT_MEDIA allow"); -// AndroidDeviceBridgeTool.executeCommand(iDevice, "am start -n org.cloud.sonic.android/.AudioActivity"); -// AndroidDeviceBridgeTool.pressKey(iDevice, 4); -// int appListPort = PortTool.getPort(); -// try { -// AndroidDeviceBridgeTool.forward(iDevice, appListPort, "sonicaudioservice"); -// Socket audioSocket = null; -// InputStream inputStream = null; -// try { -// audioSocket = new Socket("localhost", appListPort); -// inputStream = audioSocket.getInputStream(); -// int len = 1024; -// while (audioSocket.isConnected() && !Thread.interrupted()) { -// byte[] buffer = new byte[len]; -// int realLen; -// realLen = inputStream.read(buffer); -// if (buffer.length != realLen && realLen >= 0) { -// buffer = AgentTool.subByteArray(buffer, 0, realLen); -// } -// if (realLen >= 0) { -//// System.out.println(buffer); -// } -// } -// } catch (IOException e) { -// e.printStackTrace(); -// } finally { -// if (audioSocket != null && audioSocket.isConnected()) { -// try { -// audioSocket.close(); -// logger.info("audio socket已关闭"); -// } catch (IOException e) { -// e.printStackTrace(); -// } -// } -// if (inputStream != null) { -// try { -// inputStream.close(); -// logger.info("audio output流已关闭"); -// } catch (IOException e) { -// e.printStackTrace(); -// } -// } -// } -// } catch (Exception e) { -// logger.info("{} 设备远程音频服务启动异常!" -// , iDevice.getSerialNumber()); -// logger.error(e.getMessage()); -// } -// AndroidDeviceBridgeTool.removeForward(iDevice, appListPort, "sonicaudioservice"); -// }); -// audioMap.put(session, audioPro); -// } - public void getAppList(IDevice iDevice, Session session) { Future app = appListMap.get(session); if (app != null && (!app.isDone() || !app.isCancelled())) { @@ -333,26 +263,28 @@ public void getAppList(IDevice iDevice, Session session) { try { appListSocket = new Socket("localhost", appListPort); inputStream = appListSocket.getInputStream(); - int len = 1024; - String total = ""; while (appListSocket.isConnected()) { - byte[] buffer = new byte[len]; - int realLen; - realLen = inputStream.read(buffer); - if (buffer.length != realLen && realLen >= 0) { - buffer = AgentTool.subByteArray(buffer, 0, realLen); + // 获取长度 + byte[] lengthBytes = inputStream.readNBytes(32); + if (Thread.interrupted() || lengthBytes.length == 0) { + // lengthBytes.length基本就是中断的时候强制唤醒导致的 + break; } - if (realLen >= 0) { - String chunk = new String(buffer); - total += chunk; - if (chunk.contains("}")) { - JSONObject appListDetail = new JSONObject(); - appListDetail.put("msg", "appListDetail"); - appListDetail.put("detail", JSON.parseObject(total)); - AgentTool.sendText(session, appListDetail.toJSONString()); - total = ""; - } + // byte转字符串(二进制),然后再转长度 + StringBuffer binStr = new StringBuffer(); + for (byte lengthByte : lengthBytes) { + binStr.append(lengthByte); } + Integer readLen = Integer.valueOf(binStr.toString(), 2); + + // 根据长度读取数据体 + byte[] dataBytes = inputStream.readNBytes(readLen); + String dataJson = new String(dataBytes); + + JSONObject appListDetail = new JSONObject(); + appListDetail.put("msg", "appListDetail"); + appListDetail.put("detail", JSON.parseObject(dataJson)); + AgentTool.sendText(session, appListDetail.toJSONString()); } } catch (IOException e) { e.printStackTrace();