/*
* Copyright (c) 2014, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* 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 com.google.dart.tools.core.mobile;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.core.dart2js.ProcessRunner;
import com.google.dart.tools.core.utilities.io.PrintStringWriter;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* Instance of class {@code AndroidDevBridge} represents the AndroidDevBridge (adb) in the Android
* SDK
*/
public class AndroidDebugBridge {
/**
* Specialized process runner for ADB logcat
*/
class LogcatRunner extends ProcessRunner {
private final String msgPrefix;
private final PrintStringWriter output = new PrintStringWriter();
private final Object lock = new Object();
private IStatus result = null;
public LogcatRunner(ProcessBuilder processBuilder, String msgPrefix) {
super(processBuilder);
this.msgPrefix = msgPrefix;
}
public String getOutput() {
return output.toString();
}
/**
* Block until a result is available or the specified timeout expires.
*
* @return the status or {@code null} if the wait timed out
*/
public IStatus waitForResult(long timeout) {
synchronized (lock) {
long endTime = System.currentTimeMillis() + timeout;
while (result == null) {
long delta = endTime - System.currentTimeMillis();
if (delta <= 0) {
break;
}
try {
lock.wait(delta);
} catch (InterruptedException e) {
//$FALL-THROUGH$
}
}
return result;
}
}
@Override
protected void pipeStdout(InputStream in, StringBuilder builder) {
try {
LineNumberReader reader = new LineNumberReader(new InputStreamReader(in, "UTF-8"));
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
int index = line.indexOf(msgPrefix);
if (index >= 0) {
String msg = line.substring(index + msgPrefix.length() + 1).trim();
output.println(msg);
if (msg.equals("Success")) {
setResult(Status.OK_STATUS);
} else if (msg.startsWith("Error:")) {
setResult(new Status(IStatus.ERROR, DartCore.PLUGIN_ID, msg.substring(6).trim()));
}
}
}
} catch (UnsupportedEncodingException e) {
DartCore.logError(e);
} catch (IOException e) {
// This exception is expected.
}
}
private void setResult(IStatus status) {
synchronized (lock) {
result = status;
lock.notifyAll();
}
}
}
private static final String CONTENT_SHELL_APK_ID = "org.chromium.content_shell_apk";
private static final String CONNECTION_TEST_APK_ID = "com.google.dart.editor.mobile.connection.service";
private static final String[] LIST_PACKAGES_CMD = new String[] {
"shell", "pm", "list", "packages", "-3"};
private static final String[] INSTALL_CMD = new String[] {"install", "-r"};
private static final String[] DEVICES_CMD = new String[] {"devices"};
private static final String DEVICE_CONNECTED_SUFFIX = "\tdevice";
private static final String UNAUTHORIZED_SUFFIX = "\tunauthorized";
private static final String[] START_SERVICE = new String[] {"shell", "am", "startservice"};
private static String[] LAUNCH_URL_IN_CC_CMD = new String[] {
"shell", "am", "start", "-n", "org.chromium.content_shell_apk/.ContentShellActivity", "-d"};
private static String[] LAUNCH_URL_IN_BROWSER_CMD = new String[] {
"shell", "am", "start", "-n", "com.android.chrome/com.google.android.apps.chrome.Main", "-d"};
private static String[] STOP_APP_CMD = new String[] {
"shell", "am", "force-stop", CONTENT_SHELL_APK_ID};
private static final String CONTENT_SHELL_DEBUG_PORT = "localabstract:content_shell_devtools_remote";
private static final String CHROME_DEBUG_PORT = "localabstract:chrome_devtools_remote";
private static String[] PORT_FORWARD_CMD = new String[] {"forward"};
private static String[] UNINSTALL_CMD = new String[] {"shell", "pm", "uninstall", "-k"};
private static String[] START_SERVER_CMD = new String[] {"start-server"};
private static final String[] LOGCAT_CMD = new String[] {"logcat"};
private static AndroidDebugBridge androidDebugBridge = new AndroidDebugBridge(
AndroidSdkManager.getManager().getAdbExecutable());
public static AndroidDebugBridge getAndroidDebugBridge() {
return androidDebugBridge;
}
private File adbExecutable;
private ProcessRunner adbRunner;
private final HashMap<String, HashSet<String>> installSet = new HashMap<String, HashSet<String>>();
private AndroidDebugBridge(File adbExecutable) {
this.adbExecutable = adbExecutable;
}
/**
* Gets the first device connected and detected by adb
*
* @return the device or {@code null} if no device detected
*/
public AndroidDevice getConnectedDevice() {
List<String> args = buildAdbCommand(DEVICES_CMD);
if (runAdb(args)) {
//List of devices attached
//04f5385f95d80610 device
//T062873654 unauthorized
String unauthorized = null;
LineNumberReader reader = new LineNumberReader(new StringReader(adbRunner.getStdOut()));
try {
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
line = line.trim();
if (line.endsWith(DEVICE_CONNECTED_SUFFIX)) {
String id = line.substring(0, line.length() - DEVICE_CONNECTED_SUFFIX.length()).trim();
return new AndroidDevice(id, true);
}
if (line.endsWith(UNAUTHORIZED_SUFFIX)) {
unauthorized = line.substring(0, line.length() - UNAUTHORIZED_SUFFIX.length()).trim();
}
}
} catch (IOException e) {
//$FALL-THROUGH$
}
if (unauthorized != null) {
return new AndroidDevice(unauthorized, false);
}
}
return null;
}
/**
* Install the apk for the content shell onto the connected phone
* <p>
* adb install path/to/apk
* </p>
*
* @param device the device on which to install the content shell
* @return true if install was successful
*/
public boolean installContentShellApk(AndroidDevice device) {
return install(
device,
"dart content shell browser",
CONTENT_SHELL_APK_ID,
AndroidSdkManager.getManager().getContentShellApkLocation());
}
/**
* Determine if a mobile device is connected and authorized.
*/
public boolean isDeviceConnectedAndAuthorized() {
AndroidDevice device = getConnectedDevice();
return device != null && device.isAuthorized();
}
/**
* Determine if the given URL is accessible from the mobile device.
*/
public IStatus isHtmlPageAccessible(AndroidDevice device, String pageUrl) {
if (!installConnectionTestApk(device)) {
return new Status(IStatus.ERROR, DartCore.PLUGIN_ID, "Failed to install connection test");
}
String msgPrefix = "Connection test (" + System.currentTimeMillis() + ")";
LogcatRunner logcatRunner = new LogcatRunner(
new ProcessBuilder(buildAdbCommand(LOGCAT_CMD)),
msgPrefix);
try {
logcatRunner.runAsync();
} catch (IOException e) {
DartCore.logError(e);
DartCore.getConsole().println("Failed to launch ADB logcat");
return new Status(IStatus.ERROR, DartCore.PLUGIN_ID, "Failed to launch ADB logcat", e);
}
// Launch the service that tests the connection from mobile device to developer machine
List<String> args = buildAdbCommand(START_SERVICE);
args.add("-n");
args.add("com.google.dart.editor.mobile.connection.service/.ConnectionService");
args.add("-d");
args.add(pageUrl);
args.add("-e");
args.add("prefix");
args.add(msgPrefix);
// wait for dialog for connection to browser on mobile, and then
// start the check for port forwarding.
while (DartCore.allowConnectionDialogOpen == true) {
threadSleep(500);
}
try {
if (!runAdb(args, "ADB: check port forwarding")) {
return new Status(
IStatus.ERROR,
DartCore.PLUGIN_ID,
"Failed to launch port forwarding detection");
}
IStatus result = logcatRunner.waitForResult(3500);
if (result != null) {
if (!result.isOK()) {
//DartCore.getConsole().println(logcatRunner.getOutput());
DartCore.getConsole().println(result.getMessage());
}
return result;
}
return new Status(
IStatus.ERROR,
DartCore.PLUGIN_ID,
"Timeout waiting for port forwarding detection");
} finally {
logcatRunner.dispose();
}
}
/**
* Open the url in the chrome browser on the device
* <p>
* adb shell am start com.android.chrome/com.google.android.apps.chrome.Main -d url
* </p>
*/
public boolean launchChromeBrowser(String url) {
List<String> args = buildAdbCommand(LAUNCH_URL_IN_BROWSER_CMD);
args.add(url);
return runAdb(args, "ADB: launch browser");
}
/**
* Launch the browser on the phone and open url
* <p>
* adb shell am start -n org.chromium.content_shell_apk/.ContentShellActivity -d
* http://www.cheese.com
* </p>
*
* @return true if launch was successful
*/
public boolean launchContentShell(String deviceId, String url) {
List<String> args = buildAdbCommand(LAUNCH_URL_IN_CC_CMD);
if (deviceId != null) {
args.add(1, "-s");
args.add(2, deviceId);
}
args.add(url);
return runAdb(args, "ADB: launch dart content shell browser");
}
public void scanForDevices() {
// TODO(keertip): implement this
}
/**
* Setup port forwarding from machine to phone
* <p>
* adb forward tcp:<local-port> tcp:<remote-port>
* </p>
*/
public boolean setupPortForwarding(String port) {
List<String> args = buildAdbCommand(PORT_FORWARD_CMD);
args.add("tcp:" + port);
args.add(CONTENT_SHELL_DEBUG_PORT);
return runAdb(args, "");
}
/**
* Starts the adb server
*/
public void startAdbServer() {
List<String> args = buildAdbCommand(START_SERVER_CMD);
runAdb(args);
}
/**
* Force stop (close) the content shell on the connected phone
* <p>
* adb shell am force-stop org.chromium.content_shell_apk
* </p>
*
* @return true if stop was successful
*/
public boolean stopApplication() {
List<String> args = buildAdbCommand(STOP_APP_CMD);
return runAdb(args, "ADB: stop application");
}
public void uninstallConnectionTestApk(AndroidDevice device) {
if (!uninstall(device, CONNECTION_TEST_APK_ID)) {
DartCore.logError("Failed to uninstall " + CONNECTION_TEST_APK_ID);
}
}
/**
* Uninstall the content shell
*/
public void uninstallContentShellApk(AndroidDevice device) {
uninstall(device, CONTENT_SHELL_APK_ID);
}
private List<String> buildAdbCommand(String... cmds) {
List<String> args = new ArrayList<String>();
args.add(adbExecutable.getAbsolutePath());
if (cmds != null) {
for (String string : cmds) {
args.add(string);
}
}
return args;
}
/**
* Install the specified APK onto the mobile device if this is the first installation during this
* sessions --or-- if the APK has been removed since the beginning of this session.
*
* @param device the device onto which the APK will be installed
* @param apkName the human readable name of the APK
* @param apkId TODO
* @param apkLocation the APK file to be installed
* @return {@code true} if the APK was installed or already resides on the mobile device
*/
private boolean install(AndroidDevice device, String apkName, String apkId, String apkLocation) {
// Check if the APK has been installed and still resides on the mobile device
if (device != null) {
HashSet<String> apkIds = installSet.get(device.getDeviceId());
if (apkIds == null) {
apkIds = new HashSet<String>();
installSet.put(device.getDeviceId(), apkIds);
}
if (apkIds.contains(apkId) && isInstalled(device, apkId)) {
return true;
}
apkIds.add(apkId);
}
// Uninstall the old APK if it exists
//uninstall(device, apkId);
// Install the APK
// adb install -r <path> to replace previously installed APK if present
List<String> args = buildAdbCommand(INSTALL_CMD);
if (device != null) {
args.add(1, "-s");
args.add(2, device.getDeviceId());
}
args.add(apkLocation);
return runAdb(args, "ADB: install " + apkName, "This could take up to 30 seconds");
}
private boolean installConnectionTestApk(AndroidDevice device) {
return install(
device,
"connection test",
CONNECTION_TEST_APK_ID,
AndroidSdkManager.getManager().getConnectionTestApkLocation());
}
/**
* Query the mobile device to determine if the specified APK is installed.
*
* @param device the device
* @param apkId the APK identifier
* @return {@code true} if installed, else {@code false}
*/
private boolean isInstalled(AndroidDevice device, String apkId) {
List<String> args = buildAdbCommand(LIST_PACKAGES_CMD);
if (device != null) {
args.add(1, "-s");
args.add(2, device.getDeviceId());
}
if (runAdb(args)) {
// Output is list of apk identifiers prefixed by "package:"
//package:com.google.dart.editor.mobile.connection.service
//package:org.chromium.content_shell_apk
String target = "package:" + apkId;
LineNumberReader reader = new LineNumberReader(new StringReader(adbRunner.getStdOut()));
try {
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
line = line.trim();
if (line.equals(target)) {
return true;
}
}
} catch (IOException e) {
//$FALL-THROUGH$
}
}
return false;
}
private boolean runAdb(List<String> args, String... message) {
int exitCode = 1;
ProcessBuilder builder = new ProcessBuilder();
builder.command(args);
adbRunner = new ProcessRunner(builder);
for (int index = 0; index < message.length; index++) {
if (index == 0) {
DartCore.getConsole().printSeparator(message[index]);
} else {
DartCore.getConsole().println(message[index]);
}
}
try {
exitCode = adbRunner.runSync(null);
if (exitCode != 0) {
DartCore.getConsole().println(adbRunner.getStdErr());
}
} catch (IOException e) {
DartCore.logError(e);
}
return exitCode == 0 ? true : false;
}
private void threadSleep(long millisecs) {
try {
Thread.sleep(millisecs);
} catch (InterruptedException e) {
}
}
private boolean uninstall(AndroidDevice device, String apkId) {
List<String> args = buildAdbCommand(UNINSTALL_CMD);
if (device != null) {
args.add(1, "-s");
args.add(2, device.getDeviceId());
}
args.add(apkId);
return runAdb(args, "ADB: uninstall " + apkId);
}
}