Index: dart/editor/tools/plugins/com.google.dart.tools.core/src/com/google/dart/tools/core/mobile/AndroidDebugBridge.java |
=================================================================== |
--- dart/editor/tools/plugins/com.google.dart.tools.core/src/com/google/dart/tools/core/mobile/AndroidDebugBridge.java (revision 37301) |
+++ dart/editor/tools/plugins/com.google.dart.tools.core/src/com/google/dart/tools/core/mobile/AndroidDebugBridge.java (working copy) |
@@ -13,14 +13,22 @@ |
*/ |
package com.google.dart.tools.core.mobile; |
+import com.google.dart.engine.utilities.io.PrintStringWriter; |
import com.google.dart.tools.core.DartCore; |
import com.google.dart.tools.core.dart2js.ProcessRunner; |
+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; |
@@ -30,14 +38,96 @@ |
*/ |
public class AndroidDebugBridge { |
- private File adb; |
+ /** |
+ * Specialized process runner for ADB logcat |
+ */ |
+ class LogcatRunner extends ProcessRunner { |
- private static String[] INSTALL_CMD = new String[] {"install"}; |
- private static String[] DEVICES_CMD = new String[] {"devices"}; |
+ 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"}; |
@@ -45,25 +135,19 @@ |
"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", "org.chromium.content_shell_apk"}; |
+ "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", "org.chromium.content_shell_apk"}; |
+ private static String[] UNINSTALL_CMD = new String[] {"shell", "pm", "uninstall", "-k"}; |
private static String[] START_SERVER_CMD = new String[] {"start-server"}; |
- private ProcessRunner runner; |
+ private static final String[] LOGCAT_CMD = new String[] {"logcat"}; |
- /** |
- * The identifiers of devices on which content shell has been installed during this session. |
- */ |
- private static HashSet<String> contentShellInstallSet = new HashSet<String>(); |
- |
private static AndroidDebugBridge androidDebugBridge = new AndroidDebugBridge( |
AndroidSdkManager.getManager().getAdbExecutable()); |
@@ -71,8 +155,12 @@ |
return androidDebugBridge; |
} |
- AndroidDebugBridge(File adbExecutable) { |
- this.adb = adbExecutable; |
+ 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; |
} |
/** |
@@ -87,7 +175,7 @@ |
//04f5385f95d80610 device |
//T062873654 unauthorized |
String unauthorized = null; |
- LineNumberReader reader = new LineNumberReader(new StringReader(runner.getStdOut())); |
+ LineNumberReader reader = new LineNumberReader(new StringReader(adbRunner.getStdOut())); |
try { |
while (true) { |
String line = reader.readLine(); |
@@ -119,31 +207,15 @@ |
* adb install path/to/apk |
* </p> |
* |
- * @param deviceId the identifier of the device on which to install the content shell |
+ * @param device the device on which to install the content shell |
* @return true if install was successful |
*/ |
- public boolean installContentShellApk(String deviceId) { |
- if (contentShellInstallSet.contains(deviceId)) { |
- return true; |
- } |
- // TODO(keertip): process error to check if apk is already installed |
- List<String> args = buildAdbCommand(INSTALL_CMD); |
- if (deviceId != null) { |
- args.add(1, "-s"); |
- args.add(2, deviceId); |
- } |
- args.add(AndroidSdkManager.getManager().getContentShellApkLocation()); |
- if (runAdb(args, "ADB: install dart content shell browser", "This could take up to 30 seconds")) { |
- String message = runner.getStdOut(); |
- // if the apk is present, message is Failure [INSTALL_FAILED_ALREADY_EXISTS] |
- if (message.toLowerCase().contains("already_exists")) { |
- // TODO(keertip): check version and reinstall |
- // DartCore.getConsole().println(message); |
- } |
- contentShellInstallSet.add(deviceId); |
- return true; |
- } |
- return false; |
+ public boolean installContentShellApk(AndroidDevice device) { |
+ return install( |
+ device, |
+ "dart content shell browser", |
+ CONTENT_SHELL_APK_ID, |
+ AndroidSdkManager.getManager().getContentShellApkLocation()); |
} |
/** |
@@ -155,6 +227,59 @@ |
} |
/** |
+ * 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); |
+ 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 |
@@ -208,7 +333,7 @@ |
*/ |
public void startAdbServer() { |
List<String> args = buildAdbCommand(START_SERVER_CMD); |
- runAdb(args, ""); |
+ runAdb(args); |
} |
/** |
@@ -224,17 +349,22 @@ |
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 apk from the device |
+ * Uninstall the content shell |
*/ |
- void uninstallContentShellApk() { |
- List<String> args = buildAdbCommand(UNINSTALL_CMD); |
- runAdb(args, "ADB: uninstall browser"); |
+ public void uninstallContentShellApk(AndroidDevice device) { |
+ uninstall(device, CONTENT_SHELL_APK_ID); |
} |
- private List<String> buildAdbCommand(String[] cmds) { |
+ private List<String> buildAdbCommand(String... cmds) { |
List<String> args = new ArrayList<String>(); |
- args.add(adb.getAbsolutePath()); |
+ args.add(adbExecutable.getAbsolutePath()); |
if (cmds != null) { |
for (String string : cmds) { |
args.add(string); |
@@ -243,11 +373,95 @@ |
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); |
- runner = new ProcessRunner(builder); |
+ adbRunner = new ProcessRunner(builder); |
for (int index = 0; index < message.length; index++) { |
if (index == 0) { |
DartCore.getConsole().printSeparator(message[index]); |
@@ -256,9 +470,9 @@ |
} |
} |
try { |
- exitCode = runner.runSync(null); |
+ exitCode = adbRunner.runSync(null); |
if (exitCode != 0) { |
- DartCore.getConsole().println(runner.getStdErr()); |
+ DartCore.getConsole().println(adbRunner.getStdErr()); |
} |
} catch (IOException e) { |
@@ -266,4 +480,14 @@ |
} |
return exitCode == 0 ? true : false; |
} |
+ |
+ 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); |
+ } |
} |