Move sl4a to its own project.

BUG: 26914381
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..d6432d8
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,22 @@
+#
+##  Copyright (C) 2016 Google, Inc.
+#
+##  Licensed under the Apache License, Version 2.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.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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
+
diff --git a/Common/Android.mk b/Common/Android.mk
new file mode 100644
index 0000000..fc92181
--- /dev/null
+++ b/Common/Android.mk
@@ -0,0 +1,35 @@
+#
+#  Copyright (C) 2016 Google, Inc.
+#
+#  Licensed under the Apache License, Version 2.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.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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+
+LOCAL_MODULE := sl4a.Common
+LOCAL_MODULE_OWNER := google
+
+LOCAL_STATIC_JAVA_LIBRARIES := guava android-common sl4a.Utils
+LOCAL_JAVA_LIBRARIES := telephony-common
+LOCAL_JAVA_LIBRARIES += ims-common
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src/com/googlecode/android_scripting)
+LOCAL_SRC_FILES += $(call all-java-files-under, src/org/apache/commons/codec)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+include $(CLEAR_VARS)
+include $(BUILD_MULTI_PREBUILT)
diff --git a/Common/src/com/googlecode/android_scripting/BaseApplication.java b/Common/src/com/googlecode/android_scripting/BaseApplication.java
new file mode 100644
index 0000000..f0b6905
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/BaseApplication.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.app.Application;
+
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.trigger.TriggerRepository;
+
+public class BaseApplication extends Application {
+
+  private final FutureActivityTaskExecutor mTaskExecutor = new FutureActivityTaskExecutor(this);
+  private TriggerRepository mTriggerRepository;
+
+  protected InterpreterConfiguration mConfiguration;
+
+  public FutureActivityTaskExecutor getTaskExecutor() {
+    return mTaskExecutor;
+  }
+
+  @Override
+  public void onCreate() {
+    mConfiguration = new InterpreterConfiguration(this);
+    mConfiguration.startDiscovering();
+    mTriggerRepository = new TriggerRepository(this);
+  }
+
+  public InterpreterConfiguration getInterpreterConfiguration() {
+    return mConfiguration;
+  }
+
+  public TriggerRepository getTriggerRepository() {
+    return mTriggerRepository;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/Constants.java b/Common/src/com/googlecode/android_scripting/Constants.java
new file mode 100644
index 0000000..4b18bba
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/Constants.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.content.ComponentName;
+
+public interface Constants {
+
+  public static final String ACTION_LAUNCH_FOREGROUND_SCRIPT =
+      "com.googlecode.android_scripting.action.LAUNCH_FOREGROUND_SCRIPT";
+  public static final String ACTION_LAUNCH_BACKGROUND_SCRIPT =
+      "com.googlecode.android_scripting.action.LAUNCH_BACKGROUND_SCRIPT";
+  public static final String ACTION_LAUNCH_SCRIPT_FOR_RESULT =
+      "com.googlecode.android_scripting.action.ACTION_LAUNCH_SCRIPT_FOR_RESULT";
+  public static final String ACTION_LAUNCH_INTERPRETER =
+      "com.googlecode.android_scripting.action.LAUNCH_INTERPRETER";
+  public static final String ACTION_EDIT_SCRIPT =
+      "com.googlecode.android_scripting.action.EDIT_SCRIPT";
+  public static final String ACTION_SAVE_SCRIPT =
+      "com.googlecode.android_scripting.action.SAVE_SCRIPT";
+  public static final String ACTION_SAVE_AND_RUN_SCRIPT =
+      "com.googlecode.android_scripting.action.SAVE_AND_RUN_SCRIPT";
+  public static final String ACTION_KILL_PROCESS =
+      "com.googlecode.android_scripting.action.KILL_PROCESS";
+  public static final String ACTION_KILL_ALL = "com.googlecode.android_scripting.action.KILL_ALL";
+  public static final String ACTION_SHOW_RUNNING_SCRIPTS =
+      "com.googlecode.android_scripting.action.SHOW_RUNNING_SCRIPTS";
+  public static final String ACTION_CANCEL_NOTIFICATION =
+      "com.googlecode.android_scripting.action.CANCEL_NOTIFICAITON";
+  public static final String ACTION_ACTIVITY_RESULT =
+      "com.googlecode.android_scripting.action.ACTIVITY_RESULT";
+  public static final String ACTION_LAUNCH_SERVER =
+      "com.googlecode.android_scripting.action.LAUNCH_SERVER";
+
+  public static final String EXTRA_RESULT = "SCRIPT_RESULT";
+  public static final String EXTRA_SCRIPT_PATH =
+      "com.googlecode.android_scripting.extra.SCRIPT_PATH";
+  public static final String EXTRA_SCRIPT_CONTENT =
+      "com.googlecode.android_scripting.extra.SCRIPT_CONTENT";
+  public static final String EXTRA_INTERPRETER_NAME =
+      "com.googlecode.android_scripting.extra.INTERPRETER_NAME";
+
+  public static final String EXTRA_USE_EXTERNAL_IP =
+      "com.googlecode.android_scripting.extra.USE_PUBLIC_IP";
+  public static final String EXTRA_USE_SERVICE_PORT =
+      "com.googlecode.android_scripting.extra.USE_SERVICE_PORT";
+  public static final String EXTRA_SCRIPT_TEXT =
+      "com.googlecode.android_scripting.extra.SCRIPT_TEXT";
+  public static final String EXTRA_RPC_HELP_TEXT =
+      "com.googlecode.android_scripting.extra.RPC_HELP_TEXT";
+  public static final String EXTRA_API_PROMPT_RPC_NAME =
+      "com.googlecode.android_scripting.extra.API_PROMPT_RPC_NAME";
+  public static final String EXTRA_API_PROMPT_VALUES =
+      "com.googlecode.android_scripting.extra.API_PROMPT_VALUES";
+  public static final String EXTRA_PROXY_PORT = "com.googlecode.android_scripting.extra.PROXY_PORT";
+  public static final String EXTRA_PROCESS_ID =
+      "com.googlecode.android_scripting.extra.SCRIPT_PROCESS_ID";
+  public static final String EXTRA_IS_NEW_SCRIPT =
+      "com.googlecode.android_scripting.extra.IS_NEW_SCRIPT";
+  public static final String EXTRA_TRIGGER_ID =
+      "com.googlecode.android_scripting.extra.EXTRA_TRIGGER_ID";
+  public static final String EXTRA_LAUNCH_IN_BACKGROUND =
+      "com.googlecode.android_scripting.extra.EXTRA_LAUNCH_IN_BACKGROUND";
+  public static final String EXTRA_TASK_ID = "com.googlecode.android_scripting.extra.EXTRA_TASK_ID";
+
+  // BluetoothDeviceManager
+  public static final String EXTRA_DEVICE_ADDRESS =
+      "com.googlecode.android_scripting.extra.device_address";
+
+  public static final ComponentName SL4A_SERVICE_COMPONENT_NAME = new ComponentName(
+      "com.googlecode.android_scripting",
+      "com.googlecode.android_scripting.activity.ScriptingLayerService");
+  public static final ComponentName SL4A_SERVICE_LAUNCHER_COMPONENT_NAME = new ComponentName(
+      "com.googlecode.android_scripting",
+      "com.googlecode.android_scripting.activity.ScriptingLayerServiceLauncher");
+  public static final ComponentName BLUETOOTH_DEVICE_LIST_COMPONENT_NAME = new ComponentName(
+      "com.googlecode.android_scripting",
+      "com.googlecode.android_scripting.activity.BluetoothDeviceList");
+  public static final ComponentName TRIGGER_SERVICE_COMPONENT_NAME = new ComponentName(
+      "com.googlecode.android_scripting",
+      "com.googlecode.android_scripting.activity.TriggerService");
+
+  // Preference Keys
+
+  public static final String FORCE_BROWSER = "helpForceBrowser";
+  public final static String HIDE_NOTIFY = "hideServiceNotifications";
+}
\ No newline at end of file
diff --git a/Common/src/com/googlecode/android_scripting/Exec.java b/Common/src/com/googlecode/android_scripting/Exec.java
new file mode 100644
index 0000000..778b5a3
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/Exec.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import java.io.FileDescriptor;
+
+/**
+ * Tools for executing commands.
+ */
+public class Exec {
+  /**
+   * @param cmd
+   *          The command to execute
+   * @param arg0
+   *          The first argument to the command, may be null
+   * @param arg1
+   *          the second argument to the command, may be null
+   * @return the file descriptor of the started process.
+   *
+   */
+  public static FileDescriptor createSubprocess(String command, String[] arguments,
+      String[] environmentVariables, String workingDirectory) {
+    return createSubprocess(command, arguments, environmentVariables, workingDirectory, null);
+  }
+
+  /**
+   * @param cmd
+   *          The command to execute
+   * @param arguments
+   *          Array of arguments, may be null
+   * @param environmentVariables
+   *          Array of environment variables, may be null
+   * @param processId
+   *          A one-element array to which the process ID of the started process will be written.
+   * @return the file descriptor of the opened process's psuedo-terminal.
+   *
+   */
+  public static native FileDescriptor createSubprocess(String command, String[] arguments,
+      String[] environmentVariables, String workingDirectory, int[] processId);
+
+  public static native void setPtyWindowSize(FileDescriptor fd, int row, int col, int xpixel,
+      int ypixel);
+
+  /**
+   * Causes the calling thread to wait for the process associated with the receiver to finish
+   * executing.
+   *
+   * @return The exit value of the Process being waited on
+   *
+   */
+  public static native int waitFor(int processId);
+
+  static {
+    System.loadLibrary("com_googlecode_android_scripting_Exec");
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/FeaturedInterpreters.java b/Common/src/com/googlecode/android_scripting/FeaturedInterpreters.java
new file mode 100644
index 0000000..005560b
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/FeaturedInterpreters.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.content.Context;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class FeaturedInterpreters {
+  private static final Map<String, FeaturedInterpreter> mNameMap =
+      new HashMap<String, FeaturedInterpreter>();
+  private static final Map<String, FeaturedInterpreter> mExtensionMap =
+      new HashMap<String, FeaturedInterpreter>();
+
+  static {
+    try {
+      FeaturedInterpreter interpreters[] =
+          {
+            new FeaturedInterpreter("BeanShell 2.0b4", ".bsh",
+                "http://android-scripting.googlecode.com/files/beanshell_for_android_r2.apk"),
+            new FeaturedInterpreter("JRuby", ".rb",
+                "https://github.com/downloads/ruboto/sl4a_jruby_interpreter/JRubyForAndroid_r2dev.apk"),
+            new FeaturedInterpreter("Lua 5.1.4", ".lua",
+                "http://android-scripting.googlecode.com/files/lua_for_android_r1.apk"),
+            new FeaturedInterpreter("Perl 5.10.1", ".pl",
+                "http://android-scripting.googlecode.com/files/perl_for_android_r1.apk"),
+            new FeaturedInterpreter("Python 2.6.2", ".py",
+                "http://python-for-android.googlecode.com/files/PythonForAndroid_r5.apk"),
+            new FeaturedInterpreter("Rhino 1.7R2", ".js",
+                "http://android-scripting.googlecode.com/files/rhino_for_android_r2.apk"),
+            new FeaturedInterpreter("PHP 5.3.3", ".php",
+                "http://php-for-android.googlecode.com/files/phpforandroid_r1.apk") };
+      for (FeaturedInterpreter interpreter : interpreters) {
+        mNameMap.put(interpreter.mmName, interpreter);
+        mExtensionMap.put(interpreter.mmExtension, interpreter);
+      }
+    } catch (MalformedURLException e) {
+      Log.e(e);
+    }
+  }
+
+  public static List<String> getList() {
+    ArrayList<String> list = new ArrayList<String>(mNameMap.keySet());
+    Collections.sort(list);
+    return list;
+  }
+
+  public static URL getUrlForName(String name) {
+    if (!mNameMap.containsKey(name)) {
+      return null;
+    }
+    return mNameMap.get(name).mmUrl;
+  }
+
+  public static String getInterpreterNameForScript(String fileName) {
+    String extension = getExtension(fileName);
+    if (extension == null || !mExtensionMap.containsKey(extension)) {
+      return null;
+    }
+    return mExtensionMap.get(extension).mmName;
+  }
+
+  public static boolean isSupported(String fileName) {
+    String extension = getExtension(fileName);
+    return (extension != null) && (mExtensionMap.containsKey(extension));
+  }
+
+  public static int getInterpreterIcon(Context context, String key) {
+    String packageName = context.getPackageName();
+    String name = "_icon";
+    if (key.contains(".")) {
+      name = key.substring(key.lastIndexOf('.') + 1) + name;
+    } else {
+      name = key + name;
+    }
+    return context.getResources().getIdentifier(name, "drawable", packageName);
+  }
+
+  private static String getExtension(String fileName) {
+    int dotIndex = fileName.lastIndexOf('.');
+    if (dotIndex == -1) {
+      return null;
+    }
+    return fileName.substring(dotIndex);
+  }
+
+  private static class FeaturedInterpreter {
+    private final String mmName;
+    private final String mmExtension;
+    private final URL mmUrl;
+
+    private FeaturedInterpreter(String name, String extension, String url)
+        throws MalformedURLException {
+      mmName = name;
+      mmExtension = extension;
+      mmUrl = new URL(url);
+    }
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/FutureActivityTaskExecutor.java b/Common/src/com/googlecode/android_scripting/FutureActivityTaskExecutor.java
new file mode 100644
index 0000000..374d122
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/FutureActivityTaskExecutor.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.googlecode.android_scripting.activity.FutureActivity;
+import com.googlecode.android_scripting.future.FutureActivityTask;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class FutureActivityTaskExecutor {
+
+  private final Context mContext;
+  private final Map<Integer, FutureActivityTask<?>> mTaskMap =
+      new ConcurrentHashMap<Integer, FutureActivityTask<?>>();
+  private final AtomicInteger mIdGenerator = new AtomicInteger(0);
+
+  public FutureActivityTaskExecutor(Context context) {
+    mContext = context;
+  }
+
+  public void execute(FutureActivityTask<?> task) {
+    int id = mIdGenerator.incrementAndGet();
+    mTaskMap.put(id, task);
+    launchHelper(id);
+  }
+
+  public FutureActivityTask<?> getTask(int id) {
+    return mTaskMap.remove(id);
+  }
+
+  private void launchHelper(int id) {
+    Intent helper = new Intent(mContext, FutureActivity.class);
+    helper.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+    helper.putExtra(Constants.EXTRA_TASK_ID, id);
+    mContext.startActivity(helper);
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/IntentBuilders.java b/Common/src/com/googlecode/android_scripting/IntentBuilders.java
new file mode 100644
index 0000000..9941c4e
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/IntentBuilders.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Parcelable;
+
+import com.googlecode.android_scripting.interpreter.Interpreter;
+
+import java.io.File;
+
+public class IntentBuilders {
+  /** An arbitrary value that is used to identify pending intents for executing scripts. */
+  private static final int EXECUTE_SCRIPT_REQUEST_CODE = 0x12f412a;
+
+  private IntentBuilders() {
+    // Utility class.
+  }
+
+  public static Intent buildTriggerServiceIntent() {
+    Intent intent = new Intent();
+    intent.setComponent(Constants.TRIGGER_SERVICE_COMPONENT_NAME);
+    return intent;
+  }
+
+  /**
+   * Builds an intent that will launch a script in the background.
+   *
+   * @param script
+   *          the script to launch
+   * @return the intent that will launch the script
+   */
+  public static Intent buildStartInBackgroundIntent(File script) {
+    final ComponentName componentName = Constants.SL4A_SERVICE_LAUNCHER_COMPONENT_NAME;
+    Intent intent = new Intent();
+    intent.setComponent(componentName);
+    intent.setAction(Constants.ACTION_LAUNCH_BACKGROUND_SCRIPT);
+    intent.putExtra(Constants.EXTRA_SCRIPT_PATH, script.getAbsolutePath());
+    return intent;
+  }
+
+  /**
+   * Builds an intent that launches a script in a terminal.
+   *
+   * @param script
+   *          the script to launch
+   * @return the intent that will launch the script
+   */
+  public static Intent buildStartInTerminalIntent(File script) {
+    final ComponentName componentName = Constants.SL4A_SERVICE_LAUNCHER_COMPONENT_NAME;
+    Intent intent = new Intent();
+    intent.setComponent(componentName);
+    intent.setAction(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT);
+    intent.putExtra(Constants.EXTRA_SCRIPT_PATH, script.getAbsolutePath());
+    return intent;
+  }
+
+  /**
+   * Builds an intent that launches an interpreter.
+   *
+   * @param interpreterName
+   *          the interpreter to launch
+   * @return the intent that will launch the interpreter
+   */
+  public static Intent buildStartInterpreterIntent(String interpreterName) {
+    final ComponentName componentName = Constants.SL4A_SERVICE_LAUNCHER_COMPONENT_NAME;
+    Intent intent = new Intent();
+    intent.setComponent(componentName);
+    intent.setAction(Constants.ACTION_LAUNCH_INTERPRETER);
+    intent.putExtra(Constants.EXTRA_INTERPRETER_NAME, interpreterName);
+    return intent;
+  }
+
+  /**
+   * Builds an intent that creates a shortcut to launch the provided interpreter.
+   *
+   * @param interpreter
+   *          the interpreter to link to
+   * @param iconResource
+   *          the icon resource to associate with the shortcut
+   * @return the intent that will create the shortcut
+   */
+  public static Intent buildInterpreterShortcutIntent(Interpreter interpreter,
+      Parcelable iconResource) {
+    Intent intent = new Intent();
+    intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT,
+        buildStartInterpreterIntent(interpreter.getName()));
+    intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, interpreter.getNiceName());
+    intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
+    return intent;
+  }
+
+  /**
+   * Builds an intent that creates a shortcut to launch the provided script in the background.
+   *
+   * @param script
+   *          the script to link to
+   * @param iconResource
+   *          the icon resource to associate with the shortcut
+   * @return the intent that will create the shortcut
+   */
+  public static Intent buildBackgroundShortcutIntent(File script, Parcelable iconResource) {
+    Intent intent = new Intent();
+    intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, buildStartInBackgroundIntent(script));
+    intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, script.getName());
+    intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
+    return intent;
+  }
+
+  /**
+   * Builds an intent that creates a shortcut to launch the provided script in a terminal.
+   *
+   * @param script
+   *          the script to link to
+   * @param iconResource
+   *          the icon resource to associate with the shortcut
+   * @return the intent that will create the shortcut
+   */
+  public static Intent buildTerminalShortcutIntent(File script, Parcelable iconResource) {
+    Intent intent = new Intent();
+    intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, buildStartInTerminalIntent(script));
+    intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, script.getName());
+    intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
+    return intent;
+  }
+
+  /**
+   * Creates a pending intent that can be used to start the trigger service.
+   *
+   * @param context
+   *          the context under whose authority to launch the intent
+   *
+   * @return {@link PendingIntent} object for running the trigger service
+   */
+  public static PendingIntent buildTriggerServicePendingIntent(Context context) {
+    final Intent intent = buildTriggerServiceIntent();
+    return PendingIntent.getService(context, EXECUTE_SCRIPT_REQUEST_CODE, intent,
+        PendingIntent.FLAG_UPDATE_CURRENT);
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/Process.java b/Common/src/com/googlecode/android_scripting/Process.java
new file mode 100644
index 0000000..25a80be
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/Process.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import com.googlecode.android_scripting.interpreter.InterpreterConstants;
+import com.trilead.ssh2.StreamGobbler;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class Process {
+
+  private static final int DEFAULT_BUFFER_SIZE = 8192;
+
+  private final List<String> mArguments;
+  private final Map<String, String> mEnvironment;
+
+  private static final int PID_INIT_VALUE = -1;
+
+  private File mBinary;
+  private String mName;
+  private long mStartTime;
+  private long mEndTime;
+
+  protected final AtomicInteger mPid;
+  protected FileDescriptor mFd;
+  protected OutputStream mOut;
+  protected InputStream mIn;
+  protected File mLog;
+
+  public Process() {
+    mArguments = new ArrayList<String>();
+    mEnvironment = new HashMap<String, String>();
+    mPid = new AtomicInteger(PID_INIT_VALUE);
+  }
+
+  public void addArgument(String argument) {
+    mArguments.add(argument);
+  }
+
+  public void addAllArguments(List<String> arguments) {
+    mArguments.addAll(arguments);
+  }
+
+  public void putAllEnvironmentVariables(Map<String, String> environment) {
+    mEnvironment.putAll(environment);
+  }
+
+  public void putEnvironmentVariable(String key, String value) {
+    mEnvironment.put(key, value);
+  }
+
+  public void setBinary(File binary) {
+    if (!binary.exists()) {
+      throw new RuntimeException("Binary " + binary + " does not exist!");
+    }
+    mBinary = binary;
+  }
+
+  public Integer getPid() {
+    return mPid.get();
+  }
+
+  public FileDescriptor getFd() {
+    return mFd;
+  }
+
+  public OutputStream getOut() {
+    return mOut;
+  }
+
+  public OutputStream getErr() {
+    return getOut();
+  }
+
+  public File getLogFile() {
+    return mLog;
+  }
+
+  public InputStream getIn() {
+    return mIn;
+  }
+
+  public void start(final Runnable shutdownHook) {
+    if (isAlive()) {
+      throw new RuntimeException("Attempted to start process that is already running.");
+    }
+
+    String binaryPath = mBinary.getAbsolutePath();
+    Log.v("Executing " + binaryPath + " with arguments " + mArguments + " and with environment "
+        + mEnvironment.toString());
+
+    int[] pid = new int[1];
+    String[] argumentsArray = mArguments.toArray(new String[mArguments.size()]);
+    mLog = new File(String.format("%s/%s.log", InterpreterConstants.SDCARD_SL4A_ROOT, getName()));
+
+    mFd =
+        Exec.createSubprocess(binaryPath, argumentsArray, getEnvironmentArray(),
+            getWorkingDirectory(), pid);
+    mPid.set(pid[0]);
+    mOut = new FileOutputStream(mFd);
+    mIn = new StreamGobbler(new FileInputStream(mFd), mLog, DEFAULT_BUFFER_SIZE);
+    mStartTime = System.currentTimeMillis();
+
+    new Thread(new Runnable() {
+      public void run() {
+        int result = Exec.waitFor(mPid.get());
+        mEndTime = System.currentTimeMillis();
+        int pid = mPid.getAndSet(PID_INIT_VALUE);
+        Log.v("Process " + pid + " exited with result code " + result + ".");
+        try {
+          mIn.close();
+        } catch (IOException e) {
+          Log.e(e);
+        }
+        try {
+          mOut.close();
+        } catch (IOException e) {
+          Log.e(e);
+        }
+        if (shutdownHook != null) {
+          shutdownHook.run();
+        }
+      }
+    }).start();
+  }
+
+  private String[] getEnvironmentArray() {
+    List<String> environmentVariables = new ArrayList<String>();
+    for (Entry<String, String> entry : mEnvironment.entrySet()) {
+      environmentVariables.add(entry.getKey() + "=" + entry.getValue());
+    }
+    String[] environment = environmentVariables.toArray(new String[environmentVariables.size()]);
+    return environment;
+  }
+
+  public void kill() {
+    if (isAlive()) {
+      android.os.Process.killProcess(mPid.get());
+      Log.v("Killed process " + mPid);
+    }
+  }
+
+  public boolean isAlive() {
+    return (mFd != null && mFd.valid()) && mPid.get() != PID_INIT_VALUE;
+  }
+
+  public String getUptime() {
+    long ms;
+    if (!isAlive()) {
+      ms = mEndTime - mStartTime;
+    } else {
+      ms = System.currentTimeMillis() - mStartTime;
+    }
+    StringBuilder buffer = new StringBuilder();
+    int days = (int) (ms / (1000 * 60 * 60 * 24));
+    int hours = (int) (ms % (1000 * 60 * 60 * 24)) / 3600000;
+    int minutes = (int) (ms % 3600000) / 60000;
+    int seconds = (int) (ms % 60000) / 1000;
+    if (days != 0) {
+      buffer.append(String.format("%02d:%02d:", days, hours));
+    } else if (hours != 0) {
+      buffer.append(String.format("%02d:", hours));
+    }
+    buffer.append(String.format("%02d:%02d", minutes, seconds));
+    return buffer.toString();
+  }
+
+  public String getName() {
+    return mName;
+  }
+
+  public void setName(String name) {
+    mName = name;
+  }
+
+  public String getWorkingDirectory() {
+    return null;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/Version.java b/Common/src/com/googlecode/android_scripting/Version.java
new file mode 100644
index 0000000..4ef9fff
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/Version.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+public class Version {
+
+  private Version() {
+    // Utility class.
+  }
+
+  public static String getVersion(Context context) {
+    try {
+      PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+      return info.versionName;
+    } catch (PackageManager.NameNotFoundException e) {
+      Log.e("Package name not found", e);
+    }
+    return "?";
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/activity/FutureActivity.java b/Common/src/com/googlecode/android_scripting/activity/FutureActivity.java
new file mode 100644
index 0000000..20e1bf9
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/activity/FutureActivity.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.View;
+
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.FutureActivityTaskExecutor;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.future.FutureActivityTask;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+
+/**
+ * This {@link Activity} is launched by {@link RpcReceiver}s in order to perform operations that a
+ * {@link Service} is unable to do. For example: start another activity for result, show dialogs,
+ * etc.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class FutureActivity extends Activity {
+  private FutureActivityTask<?> mTask;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    Log.v("FutureActivity created.");
+    int id = getIntent().getIntExtra(Constants.EXTRA_TASK_ID, 0);
+    if (id == 0) {
+      throw new RuntimeException("FutureActivityTask ID is not specified.");
+    }
+    FutureActivityTaskExecutor taskQueue = ((BaseApplication) getApplication()).getTaskExecutor();
+    mTask = taskQueue.getTask(id);
+    if (mTask == null) { // TODO: (Robbie) This is now less of a kludge. Would still like to know
+                         // what is happening.
+      Log.w("FutureActivity has no task!");
+      try {
+        Intent intent = new Intent(Intent.ACTION_MAIN); // Should default to main of current app.
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        String packageName = getPackageName();
+        for (ResolveInfo resolve : getPackageManager().queryIntentActivities(intent, 0)) {
+          if (resolve.activityInfo.packageName.equals(packageName)) {
+            intent.setClassName(packageName, resolve.activityInfo.name);
+            break;
+          }
+        }
+        startActivity(intent);
+      } catch (Exception e) {
+        Log.e("Can't find main activity.");
+      }
+    } else {
+      mTask.setActivity(this);
+      mTask.onCreate();
+    }
+  }
+
+  @Override
+  protected void onStart() {
+    super.onStart();
+    if (mTask != null) {
+      mTask.onStart();
+    }
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    if (mTask != null) {
+      mTask.onResume();
+    }
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    if (mTask != null) {
+      mTask.onPause();
+    }
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    if (mTask != null) {
+      mTask.onStop();
+    }
+  }
+
+  @Override
+  protected void onDestroy() {
+    super.onDestroy();
+    if (mTask != null) {
+      mTask.onDestroy();
+    }
+  }
+
+  @Override
+  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+    if (mTask != null) {
+      mTask.onCreateContextMenu(menu, v, menuInfo);
+    }
+  }
+
+  @Override
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    super.onPrepareOptionsMenu(menu);
+    if (mTask == null) {
+      return false;
+    } else {
+      return mTask.onPrepareOptionsMenu(menu);
+    }
+  }
+
+  @Override
+  public void onActivityResult(int requestCode, int resultCode, Intent data) {
+    if (mTask != null) {
+      mTask.onActivityResult(requestCode, resultCode, data);
+    }
+  }
+
+  @Override
+  public boolean onKeyDown(int keyCode, KeyEvent event) {
+    if (mTask != null) {
+      return mTask.onKeyDown(keyCode, event);
+    }
+    return false;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/event/Event.java b/Common/src/com/googlecode/android_scripting/event/Event.java
new file mode 100644
index 0000000..3c0baf4
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/event/Event.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.event;
+
+import com.google.common.base.Preconditions;
+
+public class Event {
+
+  private String mName;
+  private Object mData;
+  private double mCreationTime;
+
+  public Event(String name, Object data) {
+    Preconditions.checkNotNull(name);
+    setName(name);
+    setData(data);
+    mCreationTime = System.currentTimeMillis();
+  }
+
+  public void setName(String name) {
+    mName = name;
+  }
+
+  public String getName() {
+    return mName;
+  }
+
+  public void setData(Object data) {
+    mData = data;
+  }
+
+  public Object getData() {
+    return mData;
+  }
+
+  public double getCreationTime() {
+    return mCreationTime;
+  }
+
+  public boolean nameEquals(String name) {
+    return mName.equals(name);
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/event/EventObserver.java b/Common/src/com/googlecode/android_scripting/event/EventObserver.java
new file mode 100644
index 0000000..6f4066e
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/event/EventObserver.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.event;
+
+
+public interface EventObserver {
+        public void onEventReceived(Event event);
+}
diff --git a/Common/src/com/googlecode/android_scripting/event/EventServer.java b/Common/src/com/googlecode/android_scripting/event/EventServer.java
new file mode 100644
index 0000000..325c740
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/event/EventServer.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.event;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.SimpleServer;
+import com.googlecode.android_scripting.jsonrpc.JsonBuilder;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.Vector;
+import java.util.concurrent.CountDownLatch;
+
+import org.json.JSONException;
+
+/**
+ * An Event Forwarding server that forwards events from the rpc queue in realtime to listener
+ * clients.
+ *
+ */
+public class EventServer extends SimpleServer implements EventObserver {
+  private static final Vector<Listener> mListeners = new Vector<Listener>();
+  private InetSocketAddress address = null;
+
+  public EventServer() {
+    this(0);
+  }
+
+  public EventServer(int port) {
+    address = startAllInterfaces(port);
+  }
+
+  public InetSocketAddress getAddress() {
+    return address;
+  }
+
+  @Override
+  public void shutdown() {
+    onEventReceived(new Event("sl4a", "{\"shutdown\": \"event-server\"}"));
+    for (Listener listener : mListeners) {
+      mListeners.remove(listener);
+      listener.lock.countDown();
+    }
+    super.shutdown();
+  }
+
+  @Override
+  protected void handleConnection(Socket socket) throws IOException {
+    Log.d("handle event connection.");
+    Listener l = new Listener(socket);
+    Log.v("Adding EventServer listener " + socket.getPort());
+    mListeners.add(l);
+    // we are running in the socket accept thread
+    // wait until the event dispatcher gets us the events
+    // or we die, what ever happens first
+    try {
+      l.lock.await();
+    } catch (InterruptedException e) {
+      e.printStackTrace();
+    }
+    try {
+      l.sock.close();
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+    Log.v("Ending EventServer listener " + socket.getPort());
+  }
+
+  @Override
+  public void onEventReceived(Event event) {
+    Object result = null;
+    try {
+      result = JsonBuilder.build(event);
+    } catch (JSONException e) {
+      return;
+    }
+
+    Log.v("EventServer dispatching " + result);
+
+    for (Listener listener : mListeners) {
+      if (!listener.out.checkError()) {
+        listener.out.write(result + "\n");
+        listener.out.flush();
+      } else {
+        // let the socket accept thread we're done
+        mListeners.remove(listener);
+        listener.lock.countDown();
+      }
+    }
+  }
+
+  private class Listener {
+    private Socket sock;
+    private PrintWriter out;
+    private CountDownLatch lock = new CountDownLatch(1);
+
+    public Listener(Socket l) throws IOException {
+      sock = l;
+      out = new PrintWriter(l.getOutputStream(), true);
+    }
+  }
+
+  @Override
+  protected void handleRPCConnection(Socket sock, Integer UID, BufferedReader reader, PrintWriter writer)
+      throws Exception {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ActivityResultFacade.java b/Common/src/com/googlecode/android_scripting/facade/ActivityResultFacade.java
new file mode 100644
index 0000000..a90fba4
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ActivityResultFacade.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.Activity;
+import android.content.Intent;
+
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.io.Serializable;
+
+/**
+ * Allows you to return results to a startActivityForResult call.
+ *
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public class ActivityResultFacade extends RpcReceiver {
+
+  private static final String sRpcDescription =
+      "Sets the result of a script execution. Whenever the script APK is called via "
+          + "startActivityForResult(), the resulting intent will contain " + Constants.EXTRA_RESULT
+          + " extra with the given value.";
+  private static final String sCodeDescription =
+      "The result code to propagate back to the originating activity, often RESULT_CANCELED (0) "
+          + "or RESULT_OK (-1)";
+
+  private Activity mActivity = null;
+  private Intent mResult = null;
+  private int mResultCode;
+
+  public ActivityResultFacade(FacadeManager manager) {
+    super(manager);
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultBoolean(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Boolean resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue.booleanValue());
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultByte(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Byte resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue.byteValue());
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultShort(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Short resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue.shortValue());
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultChar(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Character resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue.charValue());
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultInteger(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Integer resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue.intValue());
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultLong(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Long resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue.longValue());
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultFloat(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Float resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue.floatValue());
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultDouble(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Double resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue.doubleValue());
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultString(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") String resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultBooleanArray(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Boolean[] resultValue) {
+    mResult = new Intent();
+    boolean[] array = new boolean[resultValue.length];
+    for (int i = 0; i < resultValue.length; i++) {
+      array[i] = resultValue[i];
+    }
+    mResult.putExtra(Constants.EXTRA_RESULT, array);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultByteArray(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Byte[] resultValue) {
+    mResult = new Intent();
+    byte[] array = new byte[resultValue.length];
+    for (int i = 0; i < resultValue.length; i++) {
+      array[i] = resultValue[i];
+    }
+    mResult.putExtra(Constants.EXTRA_RESULT, array);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultShortArray(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Short[] resultValue) {
+    mResult = new Intent();
+    short[] array = new short[resultValue.length];
+    for (int i = 0; i < resultValue.length; i++) {
+      array[i] = resultValue[i];
+    }
+    mResult.putExtra(Constants.EXTRA_RESULT, array);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultCharArray(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Character[] resultValue) {
+    mResult = new Intent();
+    char[] array = new char[resultValue.length];
+    for (int i = 0; i < resultValue.length; i++) {
+      array[i] = resultValue[i];
+    }
+    mResult.putExtra(Constants.EXTRA_RESULT, array);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultIntegerArray(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Integer[] resultValue) {
+    mResult = new Intent();
+    int[] array = new int[resultValue.length];
+    for (int i = 0; i < resultValue.length; i++) {
+      array[i] = resultValue[i];
+    }
+    mResult.putExtra(Constants.EXTRA_RESULT, array);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultLongArray(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Long[] resultValue) {
+    mResult = new Intent();
+    long[] array = new long[resultValue.length];
+    for (int i = 0; i < resultValue.length; i++) {
+      array[i] = resultValue[i];
+    }
+    mResult.putExtra(Constants.EXTRA_RESULT, array);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultFloatArray(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Float[] resultValue) {
+    mResult = new Intent();
+    float[] array = new float[resultValue.length];
+    for (int i = 0; i < resultValue.length; i++) {
+      array[i] = resultValue[i];
+    }
+    mResult.putExtra(Constants.EXTRA_RESULT, array);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultDoubleArray(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Double[] resultValue) {
+    mResult = new Intent();
+    double[] array = new double[resultValue.length];
+    for (int i = 0; i < resultValue.length; i++) {
+      array[i] = resultValue[i];
+    }
+    mResult.putExtra(Constants.EXTRA_RESULT, array);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultStringArray(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") String[] resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  @Rpc(description = sRpcDescription)
+  public synchronized void setResultSerializable(
+      @RpcParameter(name = "resultCode", description = sCodeDescription) Integer resultCode,
+      @RpcParameter(name = "resultValue") Serializable resultValue) {
+    mResult = new Intent();
+    mResult.putExtra(Constants.EXTRA_RESULT, resultValue);
+    mResultCode = resultCode;
+    if (mActivity != null) {
+      setResult();
+    }
+  }
+
+  public synchronized void setActivity(Activity activity) {
+    mActivity = activity;
+    if (mResult != null) {
+      setResult();
+    }
+  }
+
+  private void setResult() {
+    mActivity.setResult(mResultCode, mResult);
+    mActivity.finish();
+  }
+
+  @Override
+  public void shutdown() {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/AndroidFacade.java b/Common/src/com/googlecode/android_scripting/facade/AndroidFacade.java
new file mode 100644
index 0000000..1de3055
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/AndroidFacade.java
@@ -0,0 +1,1037 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ClipData;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.StatFs;
+import android.os.Vibrator;
+import android.content.ClipboardManager;
+import android.text.InputType;
+import android.text.method.PasswordTransformationMethod;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.FileUtils;
+import com.googlecode.android_scripting.FutureActivityTaskExecutor;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.NotificationIdFactory;
+import com.googlecode.android_scripting.future.FutureActivityTask;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcDeprecated;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Some general purpose Android routines.<br>
+ * <h2>Intents</h2> Intents are returned as a map, in the following form:<br>
+ * <ul>
+ * <li><b>action</b> - action.
+ * <li><b>data</b> - url
+ * <li><b>type</b> - mime type
+ * <li><b>packagename</b> - name of package. If used, requires classname to be useful (optional)
+ * <li><b>classname</b> - name of class. If used, requires packagename to be useful (optional)
+ * <li><b>categories</b> - list of categories
+ * <li><b>extras</b> - map of extras
+ * <li><b>flags</b> - integer flags.
+ * </ul>
+ * <br>
+ * An intent can be built using the {@see #makeIntent} call, but can also be constructed exterally.
+ *
+ */
+public class AndroidFacade extends RpcReceiver {
+  /**
+   * An instance of this interface is passed to the facade. From this object, the resource IDs can
+   * be obtained.
+   */
+
+  public interface Resources {
+    int getLogo48();
+  }
+
+  private final Service mService;
+  private final Handler mHandler;
+  private final Intent mIntent;
+  private final FutureActivityTaskExecutor mTaskQueue;
+
+  private final Vibrator mVibrator;
+  private final NotificationManager mNotificationManager;
+
+  private final Resources mResources;
+  private ClipboardManager mClipboard = null;
+
+  @Override
+  public void shutdown() {
+  }
+
+  public AndroidFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mIntent = manager.getIntent();
+    BaseApplication application = ((BaseApplication) mService.getApplication());
+    mTaskQueue = application.getTaskExecutor();
+    mHandler = new Handler(mService.getMainLooper());
+    mVibrator = (Vibrator) mService.getSystemService(Context.VIBRATOR_SERVICE);
+    mNotificationManager =
+        (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE);
+    mResources = manager.getAndroidFacadeResources();
+  }
+
+  ClipboardManager getClipboardManager() {
+    Object clipboard = null;
+    if (mClipboard == null) {
+      try {
+        clipboard = mService.getSystemService(Context.CLIPBOARD_SERVICE);
+      } catch (Exception e) {
+        Looper.prepare(); // Clipboard manager won't work without this on higher SDK levels...
+        clipboard = mService.getSystemService(Context.CLIPBOARD_SERVICE);
+      }
+      mClipboard = (ClipboardManager) clipboard;
+      if (mClipboard == null) {
+        Log.w("Clipboard managed not accessible.");
+      }
+    }
+    return mClipboard;
+  }
+
+  public Intent startActivityForResult(final Intent intent) {
+    FutureActivityTask<Intent> task = new FutureActivityTask<Intent>() {
+      @Override
+      public void onCreate() {
+        super.onCreate();
+        try {
+          startActivityForResult(intent, 0);
+        } catch (Exception e) {
+          intent.putExtra("EXCEPTION", e.getMessage());
+          setResult(intent);
+        }
+      }
+
+      @Override
+      public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        setResult(data);
+      }
+    };
+    mTaskQueue.execute(task);
+
+    try {
+      return task.getResult();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    } finally {
+      task.finish();
+    }
+  }
+
+  public int startActivityForResultCodeWithTimeout(final Intent intent,
+    final int request, final int timeout) {
+    FutureActivityTask<Integer> task = new FutureActivityTask<Integer>() {
+      @Override
+      public void onCreate() {
+        super.onCreate();
+        try {
+          startActivityForResult(intent, request);
+        } catch (Exception e) {
+          intent.putExtra("EXCEPTION", e.getMessage());
+        }
+      }
+
+      @Override
+      public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (request == requestCode){
+            setResult(resultCode);
+        }
+      }
+    };
+    mTaskQueue.execute(task);
+
+    try {
+      return task.getResult(timeout, TimeUnit.SECONDS);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    } finally {
+      task.finish();
+    }
+  }
+
+  // TODO(damonkohler): Pull this out into proper argument deserialization and support
+  // complex/nested types being passed in.
+  public static void putExtrasFromJsonObject(JSONObject extras,
+                                             Intent intent) throws JSONException {
+    JSONArray names = extras.names();
+    for (int i = 0; i < names.length(); i++) {
+      String name = names.getString(i);
+      Object data = extras.get(name);
+      if (data == null) {
+        continue;
+      }
+      if (data instanceof Integer) {
+        intent.putExtra(name, (Integer) data);
+      }
+      if (data instanceof Float) {
+        intent.putExtra(name, (Float) data);
+      }
+      if (data instanceof Double) {
+        intent.putExtra(name, (Double) data);
+      }
+      if (data instanceof Long) {
+        intent.putExtra(name, (Long) data);
+      }
+      if (data instanceof String) {
+        intent.putExtra(name, (String) data);
+      }
+      if (data instanceof Boolean) {
+        intent.putExtra(name, (Boolean) data);
+      }
+      // Nested JSONObject
+      if (data instanceof JSONObject) {
+        Bundle nestedBundle = new Bundle();
+        intent.putExtra(name, nestedBundle);
+        putNestedJSONObject((JSONObject) data, nestedBundle);
+      }
+      // Nested JSONArray. Doesn't support mixed types in single array
+      if (data instanceof JSONArray) {
+        // Empty array. No way to tell what type of data to pass on, so skipping
+        if (((JSONArray) data).length() == 0) {
+          Log.e("Empty array not supported in JSONObject, skipping");
+          continue;
+        }
+        // Integer
+        if (((JSONArray) data).get(0) instanceof Integer) {
+          Integer[] integerArrayData = new Integer[((JSONArray) data).length()];
+          for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+            integerArrayData[j] = ((JSONArray) data).getInt(j);
+          }
+          intent.putExtra(name, integerArrayData);
+        }
+        // Double
+        if (((JSONArray) data).get(0) instanceof Double) {
+          Double[] doubleArrayData = new Double[((JSONArray) data).length()];
+          for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+            doubleArrayData[j] = ((JSONArray) data).getDouble(j);
+          }
+          intent.putExtra(name, doubleArrayData);
+        }
+        // Long
+        if (((JSONArray) data).get(0) instanceof Long) {
+          Long[] longArrayData = new Long[((JSONArray) data).length()];
+          for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+            longArrayData[j] = ((JSONArray) data).getLong(j);
+          }
+          intent.putExtra(name, longArrayData);
+        }
+        // String
+        if (((JSONArray) data).get(0) instanceof String) {
+          String[] stringArrayData = new String[((JSONArray) data).length()];
+          for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+            stringArrayData[j] = ((JSONArray) data).getString(j);
+          }
+          intent.putExtra(name, stringArrayData);
+        }
+        // Boolean
+        if (((JSONArray) data).get(0) instanceof Boolean) {
+          Boolean[] booleanArrayData = new Boolean[((JSONArray) data).length()];
+          for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+            booleanArrayData[j] = ((JSONArray) data).getBoolean(j);
+          }
+          intent.putExtra(name, booleanArrayData);
+        }
+      }
+    }
+  }
+
+  // Contributed by Emmanuel T
+  // Nested Array handling contributed by Sergey Zelenev
+  private static void putNestedJSONObject(JSONObject jsonObject, Bundle bundle)
+      throws JSONException {
+    JSONArray names = jsonObject.names();
+    for (int i = 0; i < names.length(); i++) {
+      String name = names.getString(i);
+      Object data = jsonObject.get(name);
+      if (data == null) {
+        continue;
+      }
+      if (data instanceof Integer) {
+        bundle.putInt(name, ((Integer) data).intValue());
+      }
+      if (data instanceof Float) {
+        bundle.putFloat(name, ((Float) data).floatValue());
+      }
+      if (data instanceof Double) {
+        bundle.putDouble(name, ((Double) data).doubleValue());
+      }
+      if (data instanceof Long) {
+        bundle.putLong(name, ((Long) data).longValue());
+      }
+      if (data instanceof String) {
+        bundle.putString(name, (String) data);
+      }
+      if (data instanceof Boolean) {
+        bundle.putBoolean(name, ((Boolean) data).booleanValue());
+      }
+      // Nested JSONObject
+      if (data instanceof JSONObject) {
+        Bundle nestedBundle = new Bundle();
+        bundle.putBundle(name, nestedBundle);
+        putNestedJSONObject((JSONObject) data, nestedBundle);
+      }
+      // Nested JSONArray. Doesn't support mixed types in single array
+      if (data instanceof JSONArray) {
+        // Empty array. No way to tell what type of data to pass on, so skipping
+        if (((JSONArray) data).length() == 0) {
+          Log.e("Empty array not supported in nested JSONObject, skipping");
+          continue;
+        }
+        // Integer
+        if (((JSONArray) data).get(0) instanceof Integer) {
+          int[] integerArrayData = new int[((JSONArray) data).length()];
+          for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+            integerArrayData[j] = ((JSONArray) data).getInt(j);
+          }
+          bundle.putIntArray(name, integerArrayData);
+        }
+        // Double
+        if (((JSONArray) data).get(0) instanceof Double) {
+          double[] doubleArrayData = new double[((JSONArray) data).length()];
+          for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+            doubleArrayData[j] = ((JSONArray) data).getDouble(j);
+          }
+          bundle.putDoubleArray(name, doubleArrayData);
+        }
+        // Long
+        if (((JSONArray) data).get(0) instanceof Long) {
+          long[] longArrayData = new long[((JSONArray) data).length()];
+          for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+            longArrayData[j] = ((JSONArray) data).getLong(j);
+          }
+          bundle.putLongArray(name, longArrayData);
+        }
+        // String
+        if (((JSONArray) data).get(0) instanceof String) {
+          String[] stringArrayData = new String[((JSONArray) data).length()];
+          for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+            stringArrayData[j] = ((JSONArray) data).getString(j);
+          }
+          bundle.putStringArray(name, stringArrayData);
+        }
+        // Boolean
+        if (((JSONArray) data).get(0) instanceof Boolean) {
+          boolean[] booleanArrayData = new boolean[((JSONArray) data).length()];
+          for (int j = 0; j < ((JSONArray) data).length(); ++j) {
+            booleanArrayData[j] = ((JSONArray) data).getBoolean(j);
+          }
+          bundle.putBooleanArray(name, booleanArrayData);
+        }
+      }
+    }
+  }
+
+  void startActivity(final Intent intent) {
+    try {
+      intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+      mService.startActivity(intent);
+    } catch (Exception e) {
+      Log.e("Failed to launch intent.", e);
+    }
+  }
+
+  private Intent buildIntent(String action, String uri, String type, JSONObject extras,
+      String packagename, String classname, JSONArray categories) throws JSONException {
+    Intent intent = new Intent();
+    if (action != null) {
+      intent.setAction(action);
+    }
+    intent.setDataAndType(uri != null ? Uri.parse(uri) : null, type);
+    if (packagename != null && classname != null) {
+      intent.setComponent(new ComponentName(packagename, classname));
+    }
+    if (extras != null) {
+      putExtrasFromJsonObject(extras, intent);
+    }
+    if (categories != null) {
+      for (int i = 0; i < categories.length(); i++) {
+        intent.addCategory(categories.getString(i));
+      }
+    }
+    return intent;
+  }
+
+  // TODO(damonkohler): It's unnecessary to add the complication of choosing between startActivity
+  // and startActivityForResult. It's probably better to just always use the ForResult version.
+  // However, this makes the call always blocking. We'd need to add an extra boolean parameter to
+  // indicate if we should wait for a result.
+  @Rpc(description = "Starts an activity and returns the result.",
+       returns = "A Map representation of the result Intent.")
+  public Intent startActivityForResult(
+      @RpcParameter(name = "action")
+      String action,
+      @RpcParameter(name = "uri")
+      @RpcOptional String uri,
+      @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
+      @RpcOptional String type,
+      @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
+      @RpcOptional JSONObject extras,
+      @RpcParameter(name = "packagename",
+                    description = "name of package. If used, requires classname to be useful")
+      @RpcOptional String packagename,
+      @RpcParameter(name = "classname",
+                    description = "name of class. If used, requires packagename to be useful")
+      @RpcOptional String classname
+      ) throws JSONException {
+    final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null);
+    return startActivityForResult(intent);
+  }
+
+  @Rpc(description = "Starts an activity and returns the result.",
+       returns = "A Map representation of the result Intent.")
+  public Intent startActivityForResultIntent(
+      @RpcParameter(name = "intent",
+                    description = "Intent in the format as returned from makeIntent")
+      Intent intent) {
+    return startActivityForResult(intent);
+  }
+
+  private void doStartActivity(final Intent intent, Boolean wait) throws Exception {
+    if (wait == null || wait == false) {
+      startActivity(intent);
+    } else {
+      FutureActivityTask<Intent> task = new FutureActivityTask<Intent>() {
+        private boolean mSecondResume = false;
+
+        @Override
+        public void onCreate() {
+          super.onCreate();
+          startActivity(intent);
+        }
+
+        @Override
+        public void onResume() {
+          if (mSecondResume) {
+            finish();
+          }
+          mSecondResume = true;
+        }
+
+        @Override
+        public void onDestroy() {
+          setResult(null);
+        }
+
+      };
+      mTaskQueue.execute(task);
+
+      try {
+        task.getResult();
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  /**
+   * Creates a new AndroidFacade that simplifies the interface to various Android APIs.
+   *
+   * @param service
+   *          is the {@link Context} the APIs will run under
+   */
+
+  @Rpc(description = "Put a text string in the clipboard.")
+  public void setTextClip(@RpcParameter(name = "text")
+                          String text,
+                          @RpcParameter(name = "label")
+                          @RpcOptional @RpcDefault(value = "copiedText")
+                          String label) {
+    getClipboardManager().setPrimaryClip(ClipData.newPlainText(label, text));
+  }
+
+  @Rpc(description = "Get the device serial number.")
+  public String getBuildSerial() {
+      return Build.SERIAL;
+  }
+
+  @Rpc(description = "Get the name of system bootloader version number.")
+  public String getBuildBootloader() {
+    return android.os.Build.BOOTLOADER;
+  }
+
+  @Rpc(description = "Get the name of the industrial design.")
+  public String getBuildIndustrialDesignName() {
+    return Build.DEVICE;
+  }
+
+  @Rpc(description = "Get the build ID string meant for displaying to the user")
+  public String getBuildDisplay() {
+    return Build.DISPLAY;
+  }
+
+  @Rpc(description = "Get the string that uniquely identifies this build.")
+  public String getBuildFingerprint() {
+    return Build.FINGERPRINT;
+  }
+
+  @Rpc(description = "Get the name of the hardware (from the kernel command "
+      + "line or /proc)..")
+  public String getBuildHardware() {
+    return Build.HARDWARE;
+  }
+
+  @Rpc(description = "Get the device host.")
+  public String getBuildHost() {
+    return Build.HOST;
+  }
+
+  @Rpc(description = "Get Either a changelist number, or a label like."
+      + " \"M4-rc20\".")
+  public String getBuildID() {
+    return android.os.Build.ID;
+  }
+
+  @Rpc(description = "Returns true if we are running a debug build such"
+      + " as \"user-debug\" or \"eng\".")
+  public boolean getBuildIsDebuggable() {
+    return Build.IS_DEBUGGABLE;
+  }
+
+  @Rpc(description = "Get the name of the overall product.")
+  public String getBuildProduct() {
+    return android.os.Build.PRODUCT;
+  }
+
+  @Rpc(description = "Get an ordered list of 32 bit ABIs supported by this "
+      + "device. The most preferred ABI is the first element in the list")
+  public String[] getBuildSupported32BitAbis() {
+    return Build.SUPPORTED_32_BIT_ABIS;
+  }
+
+  @Rpc(description = "Get an ordered list of 64 bit ABIs supported by this "
+      + "device. The most preferred ABI is the first element in the list")
+  public String[] getBuildSupported64BitAbis() {
+    return Build.SUPPORTED_64_BIT_ABIS;
+  }
+
+  @Rpc(description = "Get an ordered list of ABIs supported by this "
+      + "device. The most preferred ABI is the first element in the list")
+  public String[] getBuildSupportedBitAbis() {
+    return Build.SUPPORTED_ABIS;
+  }
+
+  @Rpc(description = "Get comma-separated tags describing the build,"
+      + " like \"unsigned,debug\".")
+  public String getBuildTags() {
+    return Build.TAGS;
+  }
+
+  @Rpc(description = "Get The type of build, like \"user\" or \"eng\".")
+  public String getBuildType() {
+    return Build.TYPE;
+  }
+  @Rpc(description = "Returns the board name.")
+  public String getBuildBoard() {
+    return Build.BOARD;
+  }
+
+  @Rpc(description = "Returns the brand name.")
+  public String getBuildBrand() {
+    return Build.BRAND;
+  }
+
+  @Rpc(description = "Returns the manufacturer name.")
+  public String getBuildManufacturer() {
+    return Build.MANUFACTURER;
+  }
+
+  @Rpc(description = "Returns the model name.")
+  public String getBuildModel() {
+    return Build.MODEL;
+  }
+
+  @Rpc(description = "Returns the build number.")
+  public String getBuildNumber() {
+    return Build.FINGERPRINT;
+  }
+
+  @Rpc(description = "Returns the SDK version.")
+  public Integer getBuildSdkVersion() {
+    return Build.VERSION.SDK_INT;
+  }
+
+  @Rpc(description = "Returns the current device time.")
+  public Long getBuildTime() {
+    return Build.TIME;
+  }
+
+  @Rpc(description = "Read all text strings copied by setTextClip from the clipboard.")
+  public List<String> getTextClip() {
+    ClipboardManager cm = getClipboardManager();
+    ArrayList<String> texts = new ArrayList<String>();
+    if(!cm.hasPrimaryClip()) {
+      return texts;
+    }
+    ClipData cd = cm.getPrimaryClip();
+    for(int i=0; i<cd.getItemCount(); i++) {
+      texts.add(cd.getItemAt(i).coerceToText(mService).toString());
+    }
+    return texts;
+  }
+
+  /**
+   * packagename and classname, if provided, are used in a 'setComponent' call.
+   */
+  @Rpc(description = "Starts an activity.")
+  public void startActivity(
+      @RpcParameter(name = "action")
+      String action,
+      @RpcParameter(name = "uri")
+      @RpcOptional String uri,
+      @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
+      @RpcOptional String type,
+      @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
+      @RpcOptional JSONObject extras,
+      @RpcParameter(name = "wait", description = "block until the user exits the started activity")
+      @RpcOptional Boolean wait,
+      @RpcParameter(name = "packagename",
+                    description = "name of package. If used, requires classname to be useful")
+      @RpcOptional String packagename,
+      @RpcParameter(name = "classname",
+                    description = "name of class. If used, requires packagename to be useful")
+      @RpcOptional String classname
+      ) throws Exception {
+    final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null);
+    doStartActivity(intent, wait);
+  }
+
+  @Rpc(description = "Send a broadcast.")
+  public void sendBroadcast(
+      @RpcParameter(name = "action")
+      String action,
+      @RpcParameter(name = "uri")
+      @RpcOptional String uri,
+      @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
+      @RpcOptional String type,
+      @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
+      @RpcOptional JSONObject extras,
+      @RpcParameter(name = "packagename",
+                    description = "name of package. If used, requires classname to be useful")
+      @RpcOptional String packagename,
+      @RpcParameter(name = "classname",
+                    description = "name of class. If used, requires packagename to be useful")
+      @RpcOptional String classname
+      ) throws JSONException {
+    final Intent intent = buildIntent(action, uri, type, extras, packagename, classname, null);
+    try {
+      mService.sendBroadcast(intent);
+    } catch (Exception e) {
+      Log.e("Failed to broadcast intent.", e);
+    }
+  }
+
+  @Rpc(description = "Starts a service.")
+  public void startService(
+      @RpcParameter(name = "uri")
+      @RpcOptional String uri,
+      @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
+      @RpcOptional JSONObject extras,
+      @RpcParameter(name = "packagename",
+                    description = "name of package. If used, requires classname to be useful")
+      @RpcOptional String packagename,
+      @RpcParameter(name = "classname",
+                    description = "name of class. If used, requires packagename to be useful")
+      @RpcOptional String classname
+      ) throws Exception {
+    final Intent intent = buildIntent(null /* action */, uri, null /* type */, extras, packagename,
+                                      classname, null /* categories */);
+    mService.startService(intent);
+  }
+
+  @Rpc(description = "Create an Intent.", returns = "An object representing an Intent")
+  public Intent makeIntent(
+      @RpcParameter(name = "action")
+      String action,
+      @RpcParameter(name = "uri")
+      @RpcOptional String uri,
+      @RpcParameter(name = "type", description = "MIME type/subtype of the URI")
+      @RpcOptional String type,
+      @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent")
+      @RpcOptional JSONObject extras,
+      @RpcParameter(name = "categories", description = "a List of categories to add to the Intent")
+      @RpcOptional JSONArray categories,
+      @RpcParameter(name = "packagename",
+                    description = "name of package. If used, requires classname to be useful")
+      @RpcOptional String packagename,
+      @RpcParameter(name = "classname",
+                    description = "name of class. If used, requires packagename to be useful")
+      @RpcOptional String classname,
+      @RpcParameter(name = "flags", description = "Intent flags")
+      @RpcOptional Integer flags
+      ) throws JSONException {
+    Intent intent = buildIntent(action, uri, type, extras, packagename, classname, categories);
+    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    if (flags != null) {
+      intent.setFlags(flags);
+    }
+    return intent;
+  }
+
+  @Rpc(description = "Start Activity using Intent")
+  public void startActivityIntent(
+      @RpcParameter(name = "intent",
+                    description = "Intent in the format as returned from makeIntent")
+      Intent intent,
+      @RpcParameter(name = "wait",
+                    description = "block until the user exits the started activity")
+      @RpcOptional Boolean wait
+      ) throws Exception {
+    doStartActivity(intent, wait);
+  }
+
+  @Rpc(description = "Send Broadcast Intent")
+  public void sendBroadcastIntent(
+      @RpcParameter(name = "intent",
+                    description = "Intent in the format as returned from makeIntent")
+      Intent intent
+      ) throws Exception {
+    mService.sendBroadcast(intent);
+  }
+
+  @Rpc(description = "Start Service using Intent")
+  public void startServiceIntent(
+      @RpcParameter(name = "intent",
+                    description = "Intent in the format as returned from makeIntent")
+      Intent intent
+      ) throws Exception {
+    mService.startService(intent);
+  }
+
+  @Rpc(description = "Vibrates the phone or a specified duration in milliseconds.")
+  public void vibrate(
+      @RpcParameter(name = "duration", description = "duration in milliseconds")
+      @RpcDefault("300")
+      Integer duration) {
+    mVibrator.vibrate(duration);
+  }
+
+  @Rpc(description = "Displays a short-duration Toast notification.")
+  public void makeToast(@RpcParameter(name = "message") final String message) {
+    mHandler.post(new Runnable() {
+      public void run() {
+        Toast.makeText(mService, message, Toast.LENGTH_SHORT).show();
+      }
+    });
+  }
+
+  private String getInputFromAlertDialog(final String title, final String message,
+      final boolean password) {
+    final FutureActivityTask<String> task = new FutureActivityTask<String>() {
+      @Override
+      public void onCreate() {
+        super.onCreate();
+        final EditText input = new EditText(getActivity());
+        if (password) {
+          input.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+          input.setTransformationMethod(new PasswordTransformationMethod());
+        }
+        AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
+        alert.setTitle(title);
+        alert.setMessage(message);
+        alert.setView(input);
+        alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
+          @Override
+          public void onClick(DialogInterface dialog, int whichButton) {
+            dialog.dismiss();
+            setResult(input.getText().toString());
+            finish();
+          }
+        });
+        alert.setOnCancelListener(new DialogInterface.OnCancelListener() {
+          @Override
+          public void onCancel(DialogInterface dialog) {
+            dialog.dismiss();
+            setResult(null);
+            finish();
+          }
+        });
+        alert.show();
+      }
+    };
+    mTaskQueue.execute(task);
+
+    try {
+      return task.getResult();
+    } catch (Exception e) {
+      Log.e("Failed to display dialog.", e);
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Rpc(description = "Queries the user for a text input.")
+  @RpcDeprecated(value = "dialogGetInput", release = "r3")
+  public String getInput(
+      @RpcParameter(name = "title", description = "title of the input box")
+      @RpcDefault("SL4A Input")
+      final String title,
+      @RpcParameter(name = "message", description = "message to display above the input box")
+      @RpcDefault("Please enter value:")
+      final String message) {
+    return getInputFromAlertDialog(title, message, false);
+  }
+
+  @Rpc(description = "Queries the user for a password.")
+  @RpcDeprecated(value = "dialogGetPassword", release = "r3")
+  public String getPassword(
+      @RpcParameter(name = "title", description = "title of the input box")
+      @RpcDefault("SL4A Password Input")
+      final String title,
+      @RpcParameter(name = "message", description = "message to display above the input box")
+      @RpcDefault("Please enter password:")
+      final String message) {
+    return getInputFromAlertDialog(title, message, true);
+  }
+
+  @Rpc(description = "Displays a notification that will be canceled when the user clicks on it.")
+  public void notify(@RpcParameter(name = "title", description = "title") String title,
+      @RpcParameter(name = "message") String message) {
+    // This contentIntent is a noop.
+    PendingIntent contentIntent = PendingIntent.getService(mService, 0, new Intent(), 0);
+    Notification.Builder builder = new Notification.Builder(mService);
+    builder.setSmallIcon(mResources.getLogo48())
+           .setTicker(message)
+           .setWhen(System.currentTimeMillis())
+           .setContentTitle(title)
+           .setContentText(message)
+           .setContentIntent(contentIntent);
+    Notification notification = builder.build();
+    notification.flags = Notification.FLAG_AUTO_CANCEL;
+    // Get a unique notification id from the application.
+    final int notificationId = NotificationIdFactory.create();
+    mNotificationManager.notify(notificationId, notification);
+  }
+
+  @Rpc(description = "Returns the intent that launched the script.")
+  public Object getIntent() {
+    return mIntent;
+  }
+
+  @Rpc(description = "Launches an activity that sends an e-mail message to a given recipient.")
+  public void sendEmail(
+      @RpcParameter(name = "to", description = "A comma separated list of recipients.")
+      final String to,
+      @RpcParameter(name = "subject") final String subject,
+      @RpcParameter(name = "body") final String body,
+      @RpcParameter(name = "attachmentUri")
+      @RpcOptional final String attachmentUri) {
+    final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
+    intent.setType("plain/text");
+    intent.putExtra(android.content.Intent.EXTRA_EMAIL, to.split(","));
+    intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
+    intent.putExtra(android.content.Intent.EXTRA_TEXT, body);
+    if (attachmentUri != null) {
+      intent.putExtra(android.content.Intent.EXTRA_STREAM, Uri.parse(attachmentUri));
+    }
+    startActivity(intent);
+  }
+
+  @Rpc(description = "Returns package version code.")
+  public int getPackageVersionCode(@RpcParameter(name = "packageName") final String packageName) {
+    int result = -1;
+    PackageInfo pInfo = null;
+    try {
+      pInfo =
+          mService.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA);
+    } catch (NameNotFoundException e) {
+      pInfo = null;
+    }
+    if (pInfo != null) {
+      result = pInfo.versionCode;
+    }
+    return result;
+  }
+
+  @Rpc(description = "Returns package version name.")
+  public String getPackageVersion(@RpcParameter(name = "packageName") final String packageName) {
+    PackageInfo packageInfo = null;
+    try {
+      packageInfo =
+          mService.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA);
+    } catch (NameNotFoundException e) {
+      return null;
+    }
+    if (packageInfo != null) {
+      return packageInfo.versionName;
+    }
+    return null;
+  }
+
+  @Rpc(description = "Checks if SL4A's version is >= the specified version.")
+  public boolean requiredVersion(
+          @RpcParameter(name = "requiredVersion") final Integer version) {
+    boolean result = false;
+    int packageVersion = getPackageVersionCode(
+            "com.googlecode.android_scripting");
+    if (version > -1) {
+      result = (packageVersion >= version);
+    }
+    return result;
+  }
+
+  @Rpc(description = "Writes message to logcat at verbose level")
+  public void logV(
+          @RpcParameter(name = "message")
+          String message) {
+      android.util.Log.v("SL4A: ", message);
+  }
+
+  @Rpc(description = "Writes message to logcat at info level")
+  public void logI(
+          @RpcParameter(name = "message")
+          String message) {
+      android.util.Log.i("SL4A: ", message);
+  }
+
+  @Rpc(description = "Writes message to logcat at debug level")
+  public void logD(
+          @RpcParameter(name = "message")
+          String message) {
+      android.util.Log.d("SL4A: ", message);
+  }
+
+  @Rpc(description = "Writes message to logcat at warning level")
+  public void logW(
+          @RpcParameter(name = "message")
+          String message) {
+      android.util.Log.w("SL4A: ", message);
+  }
+
+  @Rpc(description = "Writes message to logcat at error level")
+  public void logE(
+          @RpcParameter(name = "message")
+          String message) {
+      android.util.Log.e("SL4A: ", message);
+  }
+
+  @Rpc(description = "Writes message to logcat at wtf level")
+  public void logWTF(
+          @RpcParameter(name = "message")
+          String message) {
+      android.util.Log.wtf("SL4A: ", message);
+  }
+
+  /**
+   *
+   * Map returned:
+   *
+   * <pre>
+   *   TZ = Timezone
+   *     id = Timezone ID
+   *     display = Timezone display name
+   *     offset = Offset from UTC (in ms)
+   *   SDK = SDK Version
+   *   download = default download path
+   *   appcache = Location of application cache
+   *   sdcard = Space on sdcard
+   *     availblocks = Available blocks
+   *     blockcount = Total Blocks
+   *     blocksize = size of block.
+   * </pre>
+   */
+  @Rpc(description = "A map of various useful environment details")
+  public Map<String, Object> environment() {
+    Map<String, Object> result = new HashMap<String, Object>();
+    Map<String, Object> zone = new HashMap<String, Object>();
+    Map<String, Object> space = new HashMap<String, Object>();
+    TimeZone tz = TimeZone.getDefault();
+    zone.put("id", tz.getID());
+    zone.put("display", tz.getDisplayName());
+    zone.put("offset", tz.getOffset((new Date()).getTime()));
+    result.put("TZ", zone);
+    result.put("SDK", android.os.Build.VERSION.SDK_INT);
+    result.put("download", FileUtils.getExternalDownload().getAbsolutePath());
+    result.put("appcache", mService.getCacheDir().getAbsolutePath());
+    try {
+      StatFs fs = new StatFs("/sdcard");
+      space.put("availblocks", fs.getAvailableBlocksLong());
+      space.put("blocksize", fs.getBlockSizeLong());
+      space.put("blockcount", fs.getBlockCountLong());
+    } catch (Exception e) {
+      space.put("exception", e.toString());
+    }
+    result.put("sdcard", space);
+    return result;
+  }
+
+  @Rpc(description = "Get list of constants (static final fields) for a class")
+  public Bundle getConstants(
+      @RpcParameter(name = "classname", description = "Class to get constants from")
+      String classname)
+      throws Exception {
+    Bundle result = new Bundle();
+    int flags = Modifier.FINAL | Modifier.PUBLIC | Modifier.STATIC;
+    Class<?> clazz = Class.forName(classname);
+    for (Field field : clazz.getFields()) {
+      if ((field.getModifiers() & flags) == flags) {
+        Class<?> type = field.getType();
+        String name = field.getName();
+        if (type == int.class) {
+          result.putInt(name, field.getInt(null));
+        } else if (type == long.class) {
+          result.putLong(name, field.getLong(null));
+        } else if (type == double.class) {
+          result.putDouble(name, field.getDouble(null));
+        } else if (type == char.class) {
+          result.putChar(name, field.getChar(null));
+        } else if (type instanceof Object) {
+          result.putString(name, field.get(null).toString());
+        }
+      }
+    }
+    return result;
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ApplicationManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/ApplicationManagerFacade.java
new file mode 100644
index 0000000..69ed5ad
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ApplicationManagerFacade.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningAppProcessInfo;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Facade for managing Applications.
+ *
+ */
+public class ApplicationManagerFacade extends RpcReceiver {
+
+  private final Service mService;
+  private final AndroidFacade mAndroidFacade;
+  private final ActivityManager mActivityManager;
+  private final PackageManager mPackageManager;
+
+  public ApplicationManagerFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mAndroidFacade = manager.getReceiver(AndroidFacade.class);
+    mActivityManager = (ActivityManager) mService.getSystemService(Context.ACTIVITY_SERVICE);
+    mPackageManager = mService.getPackageManager();
+  }
+
+  @Rpc(description = "Returns a list of all launchable application class names.")
+  public Map<String, String> getLaunchableApplications() {
+    Intent intent = new Intent(Intent.ACTION_MAIN);
+    intent.addCategory(Intent.CATEGORY_LAUNCHER);
+    List<ResolveInfo> resolveInfos = mPackageManager.queryIntentActivities(intent, 0);
+    Map<String, String> applications = new HashMap<String, String>();
+    for (ResolveInfo info : resolveInfos) {
+      applications.put(info.loadLabel(mPackageManager).toString(), info.activityInfo.name);
+    }
+    return applications;
+  }
+
+  @Rpc(description = "Start activity with the given class name.")
+  public void launch(@RpcParameter(name = "className") String className) {
+    Intent intent = new Intent(Intent.ACTION_MAIN);
+    String packageName = className.substring(0, className.lastIndexOf("."));
+    intent.setClassName(packageName, className);
+    mAndroidFacade.startActivity(intent);
+  }
+
+  @Rpc(description = "Launch the specified app.")
+  public void appLaunch(@RpcParameter(name = "name") String name) {
+      Intent LaunchIntent = mPackageManager.getLaunchIntentForPackage(name);
+      mService.startActivity(LaunchIntent);
+  }
+
+  @Rpc(description = "Kill the specified app.")
+  public Boolean appKill(@RpcParameter(name = "name") String name) {
+      for (RunningAppProcessInfo info : mActivityManager.getRunningAppProcesses()) {
+          if (info.processName.contains(name)) {
+              Log.d("Killing " + info.processName);
+              android.os.Process.killProcess(info.pid);
+              android.os.Process.sendSignal(info.pid, android.os.Process.SIGNAL_KILL);
+              mActivityManager.killBackgroundProcesses(info.processName);
+              return true;
+          }
+      }
+      return false;
+  }
+
+  @Rpc(description = "Returns a list of packages running activities or services.", returns = "List of packages running activities.")
+  public List<String> getRunningPackages() {
+    Set<String> runningPackages = new HashSet<String>();
+    List<ActivityManager.RunningAppProcessInfo> appProcesses =
+        mActivityManager.getRunningAppProcesses();
+    for (ActivityManager.RunningAppProcessInfo info : appProcesses) {
+      runningPackages.addAll(Arrays.asList(info.pkgList));
+    }
+    List<ActivityManager.RunningServiceInfo> serviceProcesses =
+        mActivityManager.getRunningServices(Integer.MAX_VALUE);
+    for (ActivityManager.RunningServiceInfo info : serviceProcesses) {
+      runningPackages.add(info.service.getPackageName());
+    }
+    return new ArrayList<String>(runningPackages);
+  }
+
+  @Rpc(description = "Force stops a package.")
+  public void forceStopPackage(
+      @RpcParameter(name = "packageName", description = "name of package") String packageName) {
+    mActivityManager.restartPackage(packageName);
+  }
+
+  @Override
+  public void shutdown() {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/BatteryManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/BatteryManagerFacade.java
new file mode 100644
index 0000000..5d0b34e
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/BatteryManagerFacade.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.Bundle;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcStartEvent;
+import com.googlecode.android_scripting.rpc.RpcStopEvent;
+
+import java.lang.reflect.Field;
+
+/**
+ * Exposes Batterymanager API. Note that in order to use any of the batteryGet* functions, you need
+ * to batteryStartMonitoring, and then wait for a "battery" event. Sleeping for a second will
+ * usually work just as well.
+ *
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ * @author Robbie Matthews (rjmatthews62@gmail.com)
+ */
+public class BatteryManagerFacade extends RpcReceiver {
+
+  private final Service mService;
+  private final EventFacade mEventFacade;
+  private final int mSdkVersion;
+
+  private BatteryStateListener mReceiver;
+
+  private volatile Bundle mBatteryData = null;
+  private volatile Integer mBatteryStatus = null;
+  private volatile Integer mBatteryHealth = null;
+  private volatile Integer mPlugType = null;
+
+  private volatile Boolean mBatteryPresent = null;
+  private volatile Integer mBatteryLevel = null;
+  private volatile Integer mBatteryMaxLevel = null;
+  private volatile Integer mBatteryVoltage = null;
+  private volatile Integer mBatteryTemperature = null;
+  private volatile String mBatteryTechnology = null;
+
+  public BatteryManagerFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mSdkVersion = manager.getSdkLevel();
+    mEventFacade = manager.getReceiver(EventFacade.class);
+    mReceiver = null;
+    mBatteryData = null;
+  }
+
+  private class BatteryStateListener extends BroadcastReceiver {
+
+    private final EventFacade mmEventFacade;
+
+    private BatteryStateListener(EventFacade facade) {
+      mmEventFacade = facade;
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+      mBatteryStatus = intent.getIntExtra("status", 1);
+      mBatteryHealth = intent.getIntExtra("health", 1);
+      mPlugType = intent.getIntExtra("plugged", -1);
+      if (mSdkVersion >= 5) {
+        mBatteryPresent =
+            intent.getBooleanExtra(getBatteryManagerFieldValue("EXTRA_PRESENT"), false);
+        mBatteryLevel = intent.getIntExtra(getBatteryManagerFieldValue("EXTRA_LEVEL"), -1);
+        mBatteryMaxLevel = intent.getIntExtra(getBatteryManagerFieldValue("EXTRA_SCALE"), 0);
+        mBatteryVoltage = intent.getIntExtra(getBatteryManagerFieldValue("EXTRA_VOLTAGE"), -1);
+        mBatteryTemperature =
+            intent.getIntExtra(getBatteryManagerFieldValue("EXTRA_TEMPERATURE"), -1);
+        mBatteryTechnology = intent.getStringExtra(getBatteryManagerFieldValue("EXTRA_TECHNOLOGY"));
+      }
+      Bundle data = new Bundle();
+      data.putInt("status", mBatteryStatus);
+      data.putInt("health", mBatteryHealth);
+      data.putInt("plugged", mPlugType);
+      if (mSdkVersion >= 5) {
+        data.putBoolean("battery_present", mBatteryPresent);
+        if (mBatteryMaxLevel == null || mBatteryMaxLevel == 100 || mBatteryMaxLevel == 0) {
+          data.putInt("level", mBatteryLevel);
+        } else {
+          data.putInt("level", (int) (mBatteryLevel * 100.0 / mBatteryMaxLevel));
+        }
+        data.putInt("voltage", mBatteryVoltage);
+        data.putInt("temperature", mBatteryTemperature);
+        data.putString("technology", mBatteryTechnology);
+      }
+      mBatteryData = data;
+      mmEventFacade.postEvent("battery", mBatteryData.clone());
+    }
+  }
+
+  private String getBatteryManagerFieldValue(String name) {
+    try {
+      Field f = BatteryManager.class.getField(name);
+      return f.get(null).toString();
+    } catch (Exception e) {
+      Log.e(e);
+    }
+    return null;
+  }
+
+  @Rpc(description = "Returns the most recently recorded battery data.")
+  public Bundle readBatteryData() {
+    return mBatteryData;
+  }
+
+  /**
+   * throws "battery" events
+   */
+  @Rpc(description = "Starts tracking battery state.")
+  @RpcStartEvent("battery")
+  public void batteryStartMonitoring() {
+    if (mReceiver == null) {
+      IntentFilter filter = new IntentFilter();
+      filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+      mReceiver = new BatteryStateListener(mEventFacade);
+      mService.registerReceiver(mReceiver, filter);
+    }
+  }
+
+  @Rpc(description = "Stops tracking battery state.")
+  @RpcStopEvent("battery")
+  public void batteryStopMonitoring() {
+    if (mReceiver != null) {
+      mService.unregisterReceiver(mReceiver);
+      mReceiver = null;
+    }
+    mBatteryData = null;
+  }
+
+  @Override
+  public void shutdown() {
+    batteryStopMonitoring();
+  }
+
+  @Rpc(description = "Returns  the most recently received battery status data:" + "\n1 - unknown;"
+      + "\n2 - charging;" + "\n3 - discharging;" + "\n4 - not charging;" + "\n5 - full;")
+  public Integer batteryGetStatus() {
+    return mBatteryStatus;
+  }
+
+  @Rpc(description = "Returns the most recently received battery health data:" + "\n1 - unknown;"
+      + "\n2 - good;" + "\n3 - overheat;" + "\n4 - dead;" + "\n5 - over voltage;"
+      + "\n6 - unspecified failure;")
+  public Integer batteryGetHealth() {
+    return mBatteryHealth;
+  }
+
+  /** Power source is an AC charger. */
+  public static final int BATTERY_PLUGGED_AC = 1;
+  /** Power source is a USB port. */
+  public static final int BATTERY_PLUGGED_USB = 2;
+
+  @Rpc(description = "Returns the most recently received plug type data:" + "\n-1 - unknown"
+      + "\n0 - unplugged;" + "\n1 - power source is an AC charger"
+      + "\n2 - power source is a USB port")
+  public Integer batteryGetPlugType() {
+    return mPlugType;
+  }
+
+  @Rpc(description = "Returns the most recently received battery presence data.")
+  public Boolean batteryCheckPresent() {
+    return mBatteryPresent;
+  }
+
+  @Rpc(description = "Returns the most recently received battery level (percentage).")
+  public Integer batteryGetLevel() {
+    if (mBatteryMaxLevel == null || mBatteryMaxLevel == 100 || mBatteryMaxLevel == 0) {
+      return mBatteryLevel;
+    } else {
+      return (int) (mBatteryLevel * 100.0 / mBatteryMaxLevel);
+    }
+  }
+
+  @Rpc(description = "Returns the most recently received battery voltage.")
+  public Integer batteryGetVoltage() {
+    return mBatteryVoltage;
+  }
+
+  @Rpc(description = "Returns the most recently received battery temperature.")
+  public Integer batteryGetTemperature() {
+    return mBatteryTemperature;
+  }
+
+  @Rpc(description = "Returns the most recently received battery technology data.")
+  public String batteryGetTechnology() {
+    return mBatteryTechnology;
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/CameraFacade.java b/Common/src/com/googlecode/android_scripting/facade/CameraFacade.java
new file mode 100644
index 0000000..ff353f9
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/CameraFacade.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.Service;
+import android.content.Intent;
+import android.hardware.Camera;
+import android.hardware.Camera.AutoFocusCallback;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.PictureCallback;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.view.SurfaceHolder;
+import android.view.SurfaceHolder.Callback;
+import android.view.SurfaceView;
+import android.view.WindowManager;
+
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.FileUtils;
+import com.googlecode.android_scripting.FutureActivityTaskExecutor;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.future.FutureActivityTask;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Access Camera functions.
+ *
+ */
+public class CameraFacade extends RpcReceiver {
+
+  private final Service mService;
+  private final Parameters mParameters;
+
+  private class BooleanResult {
+    boolean mmResult = false;
+  }
+
+  public Camera openCamera(int cameraId) throws Exception {
+    int sSdkLevel = Integer.parseInt(android.os.Build.VERSION.SDK);
+    Camera result;
+    if (sSdkLevel < 9) {
+      result = Camera.open();
+    } else {
+      Method openCamera = Camera.class.getMethod("open", int.class);
+      result = (Camera) openCamera.invoke(null, cameraId);
+    }
+    return result;
+  }
+
+  public CameraFacade(FacadeManager manager) throws Exception {
+    super(manager);
+    mService = manager.getService();
+    Camera camera = openCamera(0);
+    try {
+      mParameters = camera.getParameters();
+    } finally {
+      camera.release();
+    }
+  }
+
+  @Rpc(description = "Take a picture and save it to the specified path.", returns = "A map of Booleans autoFocus and takePicture where True indicates success. cameraId also included.")
+  public Bundle cameraCapturePicture(
+      @RpcParameter(name = "targetPath") final String targetPath,
+      @RpcParameter(name = "useAutoFocus") @RpcDefault("true") Boolean useAutoFocus,
+      @RpcParameter(name = "cameraId", description = "Id of camera to use. SDK 9") @RpcDefault("0") Integer cameraId)
+      throws Exception {
+    final BooleanResult autoFocusResult = new BooleanResult();
+    final BooleanResult takePictureResult = new BooleanResult();
+    Camera camera = openCamera(cameraId);
+    camera.setParameters(mParameters);
+
+    try {
+      Method method = camera.getClass().getMethod("setDisplayOrientation", int.class);
+      method.invoke(camera, 90);
+    } catch (Exception e) {
+      Log.e(e);
+    }
+
+    try {
+      FutureActivityTask<SurfaceHolder> previewTask = setPreviewDisplay(camera);
+      camera.startPreview();
+      if (useAutoFocus) {
+        autoFocus(autoFocusResult, camera);
+      }
+      takePicture(new File(targetPath), takePictureResult, camera);
+      previewTask.finish();
+    } catch (Exception e) {
+      Log.e(e);
+    } finally {
+      camera.release();
+    }
+
+    Bundle result = new Bundle();
+    result.putBoolean("autoFocus", autoFocusResult.mmResult);
+    result.putBoolean("takePicture", takePictureResult.mmResult);
+    result.putInt("cameraId", cameraId);
+    return result;
+  }
+
+  private FutureActivityTask<SurfaceHolder> setPreviewDisplay(Camera camera) throws IOException,
+      InterruptedException {
+    FutureActivityTask<SurfaceHolder> task = new FutureActivityTask<SurfaceHolder>() {
+      @Override
+      public void onCreate() {
+        super.onCreate();
+        final SurfaceView view = new SurfaceView(getActivity());
+        getActivity().setContentView(view);
+        getActivity().getWindow().setSoftInputMode(
+            WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED);
+        view.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+        view.getHolder().addCallback(new Callback() {
+          @Override
+          public void surfaceDestroyed(SurfaceHolder holder) {
+          }
+
+          @Override
+          public void surfaceCreated(SurfaceHolder holder) {
+            setResult(view.getHolder());
+          }
+
+          @Override
+          public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+          }
+        });
+      }
+    };
+    FutureActivityTaskExecutor taskQueue =
+        ((BaseApplication) mService.getApplication()).getTaskExecutor();
+    taskQueue.execute(task);
+    camera.setPreviewDisplay(task.getResult());
+    return task;
+  }
+
+  private void takePicture(final File file, final BooleanResult takePictureResult,
+      final Camera camera) throws InterruptedException {
+    final CountDownLatch latch = new CountDownLatch(1);
+    camera.takePicture(null, null, new PictureCallback() {
+      @Override
+      public void onPictureTaken(byte[] data, Camera camera) {
+        if (!FileUtils.makeDirectories(file.getParentFile(), 0755)) {
+          takePictureResult.mmResult = false;
+          return;
+        }
+        try {
+          FileOutputStream output = new FileOutputStream(file);
+          output.write(data);
+          output.close();
+          takePictureResult.mmResult = true;
+        } catch (FileNotFoundException e) {
+          Log.e("Failed to save picture.", e);
+          takePictureResult.mmResult = false;
+          return;
+        } catch (IOException e) {
+          Log.e("Failed to save picture.", e);
+          takePictureResult.mmResult = false;
+          return;
+        } finally {
+          latch.countDown();
+        }
+      }
+    });
+    latch.await();
+  }
+
+  private void autoFocus(final BooleanResult result, final Camera camera)
+      throws InterruptedException {
+    final CountDownLatch latch = new CountDownLatch(1);
+    {
+      camera.autoFocus(new AutoFocusCallback() {
+        @Override
+        public void onAutoFocus(boolean success, Camera camera) {
+          result.mmResult = success;
+          latch.countDown();
+        }
+      });
+      latch.await();
+    }
+  }
+
+  @Override
+  public void shutdown() {
+    // Nothing to clean up.
+  }
+
+  @Rpc(description = "Starts the image capture application to take a picture and saves it to the specified path.")
+  public void cameraInteractiveCapturePicture(
+      @RpcParameter(name = "targetPath") final String targetPath) {
+    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+    File file = new File(targetPath);
+    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
+    AndroidFacade facade = mManager.getReceiver(AndroidFacade.class);
+    facade.startActivityForResult(intent);
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/CommonIntentsFacade.java b/Common/src/com/googlecode/android_scripting/facade/CommonIntentsFacade.java
new file mode 100644
index 0000000..ffa464c
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/CommonIntentsFacade.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.SearchManager;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.Contacts.People;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.io.File;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * A selection of commonly used intents. <br>
+ * <br>
+ * These can be used to trigger some common tasks.
+ *
+ */
+public class CommonIntentsFacade extends RpcReceiver {
+
+  private final AndroidFacade mAndroidFacade;
+
+  public CommonIntentsFacade(FacadeManager manager) {
+    super(manager);
+    mAndroidFacade = manager.getReceiver(AndroidFacade.class);
+  }
+
+  @Override
+  public void shutdown() {
+  }
+
+  @Rpc(description = "Display content to be picked by URI (e.g. contacts)", returns = "A map of result values.")
+  public Intent pick(@RpcParameter(name = "uri") String uri) throws JSONException {
+    return mAndroidFacade.startActivityForResult(Intent.ACTION_PICK, uri, null, null, null, null);
+  }
+
+  @Rpc(description = "Starts the barcode scanner.", returns = "A Map representation of the result Intent.")
+  public Intent scanBarcode() throws JSONException {
+    try {
+      return mAndroidFacade.startActivityForResult("com.google.zxing.client.android.SCAN", null,
+        null, null, null, null);
+    } catch (ActivityNotFoundException e) {
+        Log.e("No Activity found to scan a barcode!", e);
+        return null;
+    }
+  }
+
+  private void view(Uri uri, String type) {
+    Intent intent = new Intent(Intent.ACTION_VIEW);
+    intent.setDataAndType(uri, type);
+    mAndroidFacade.startActivity(intent);
+  }
+
+  @Rpc(description = "Start activity with view action by URI (i.e. browser, contacts, etc.).")
+  public void view(
+      @RpcParameter(name = "uri") String uri,
+      @RpcParameter(name = "type", description = "MIME type/subtype of the URI") @RpcOptional String type,
+      @RpcParameter(name = "extras", description = "a Map of extras to add to the Intent") @RpcOptional JSONObject extras)
+      throws Exception {
+    mAndroidFacade.startActivity(Intent.ACTION_VIEW, uri, type, extras, true, null, null);
+  }
+
+  @Rpc(description = "Opens a map search for query (e.g. pizza, 123 My Street).")
+  public void viewMap(@RpcParameter(name = "query, e.g. pizza, 123 My Street") String query)
+      throws Exception {
+    view("geo:0,0?q=" + query, null, null);
+  }
+
+  @Rpc(description = "Opens the list of contacts.")
+  public void viewContacts() throws JSONException {
+    view(People.CONTENT_URI, null);
+  }
+
+  @Rpc(description = "Opens the browser to display a local HTML file.")
+  public void viewHtml(
+      @RpcParameter(name = "path", description = "the path to the HTML file") String path)
+      throws JSONException {
+    File file = new File(path);
+    view(Uri.fromFile(file), "text/html");
+  }
+
+  @Rpc(description = "Starts a search for the given query.")
+  public void search(@RpcParameter(name = "query") String query) {
+    Intent intent = new Intent(Intent.ACTION_SEARCH);
+    intent.putExtra(SearchManager.QUERY, query);
+    mAndroidFacade.startActivity(intent);
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ConnectivityManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/ConnectivityManagerFacade.java
new file mode 100644
index 0000000..594c934
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ConnectivityManagerFacade.java
@@ -0,0 +1,721 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityManager.PacketKeepaliveCallback;
+import android.net.ConnectivityManager.PacketKeepalive;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.NetworkInfo;
+import android.provider.Settings.SettingNotFoundException;
+import android.os.Bundle;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.telephony.TelephonyConstants;
+import com.googlecode.android_scripting.facade.telephony.TelephonyEvents;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+
+/**
+ * Access ConnectivityManager functions.
+ */
+public class ConnectivityManagerFacade extends RpcReceiver {
+
+    public static int AIRPLANE_MODE_OFF = 0;
+    public static int AIRPLANE_MODE_ON = 1;
+
+    class ConnectivityReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+
+            if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+                Log.e("ConnectivityReceiver received non-connectivity action!");
+                return;
+            }
+
+            Bundle b = intent.getExtras();
+
+            if (b == null) {
+                Log.e("ConnectivityReceiver failed to receive extras!");
+                return;
+            }
+
+            int netType =
+                    b.getInt(ConnectivityManager.EXTRA_NETWORK_TYPE,
+                            ConnectivityManager.TYPE_NONE);
+
+            if (netType == ConnectivityManager.TYPE_NONE) {
+                Log.i("ConnectivityReceiver received change to TYPE_NONE.");
+                return;
+            }
+
+            /*
+             * Technically there is a race condition here, but retrieving the NetworkInfo from the
+             * bundle is deprecated. See ConnectivityManager.EXTRA_NETWORK_INFO
+             */
+            for (NetworkInfo info : mManager.getAllNetworkInfo()) {
+                if (info.getType() == netType) {
+                    mEventFacade.postEvent(TelephonyConstants.EventConnectivityChanged, info);
+                }
+            }
+        }
+    }
+
+    class PacketKeepaliveReceiver extends PacketKeepaliveCallback {
+        public static final int EVENT_INVALID = -1;
+        public static final int EVENT_NONE = 0;
+        public static final int EVENT_STARTED = 1 << 0;
+        public static final int EVENT_STOPPED = 1 << 1;
+        public static final int EVENT_ERROR = 1 << 2;
+        public static final int EVENT_ALL = EVENT_STARTED |
+                EVENT_STOPPED |
+                EVENT_ERROR;
+        private int mEvents;
+        public String mId;
+        public PacketKeepalive mPacketKeepalive;
+
+        public PacketKeepaliveReceiver(int events) {
+            super();
+            mEvents = events;
+            mId = this.toString();
+        }
+
+        public void startListeningForEvents(int events) {
+            mEvents |= events & EVENT_ALL;
+        }
+
+        public void stopListeningForEvents(int events) {
+            mEvents &= ~(events & EVENT_ALL);
+        }
+
+        @Override
+        public void onStarted() {
+            Log.d("PacketKeepaliveCallback on start!");
+            if ((mEvents & EVENT_STARTED) == EVENT_STARTED) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventPacketKeepaliveCallback,
+                    new TelephonyEvents.PacketKeepaliveEvent(
+                        mId,
+                        getPacketKeepaliveReceiverEventString(EVENT_STARTED)));
+            }
+        }
+
+        @Override
+        public void onStopped() {
+            Log.d("PacketKeepaliveCallback on stop!");
+            if ((mEvents & EVENT_STOPPED) == EVENT_STOPPED) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventPacketKeepaliveCallback,
+                    new TelephonyEvents.PacketKeepaliveEvent(
+                        mId,
+                        getPacketKeepaliveReceiverEventString(EVENT_STOPPED)));
+            }
+        }
+
+        @Override
+        public void onError(int error) {
+            Log.d("PacketKeepaliveCallback on error! - code:" + error);
+            if ((mEvents & EVENT_ERROR) == EVENT_ERROR) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventPacketKeepaliveCallback,
+                    new TelephonyEvents.PacketKeepaliveEvent(
+                        mId,
+                        getPacketKeepaliveReceiverEventString(EVENT_ERROR)));
+            }
+        }
+    }
+
+    class NetworkCallback extends ConnectivityManager.NetworkCallback {
+        public static final int EVENT_INVALID = -1;
+        public static final int EVENT_NONE = 0;
+        public static final int EVENT_PRECHECK = 1 << 0;
+        public static final int EVENT_AVAILABLE = 1 << 1;
+        public static final int EVENT_LOSING = 1 << 2;
+        public static final int EVENT_LOST = 1 << 3;
+        public static final int EVENT_UNAVAILABLE = 1 << 4;
+        public static final int EVENT_CAPABILITIES_CHANGED = 1 << 5;
+        public static final int EVENT_SUSPENDED = 1 << 6;
+        public static final int EVENT_RESUMED = 1 << 7;
+        public static final int EVENT_LINK_PROPERTIES_CHANGED = 1 << 8;
+        public static final int EVENT_ALL = EVENT_PRECHECK |
+                EVENT_AVAILABLE |
+                EVENT_LOSING |
+                EVENT_LOST |
+                EVENT_UNAVAILABLE |
+                EVENT_CAPABILITIES_CHANGED |
+                EVENT_SUSPENDED |
+                EVENT_RESUMED |
+                EVENT_LINK_PROPERTIES_CHANGED;
+
+        private int mEvents;
+        public String mId;
+
+        public NetworkCallback(int events) {
+            super();
+            mEvents = events;
+            mId = this.toString();
+        }
+
+        public void startListeningForEvents(int events) {
+            mEvents |= events & EVENT_ALL;
+        }
+
+        public void stopListeningForEvents(int events) {
+            mEvents &= ~(events & EVENT_ALL);
+        }
+
+        @Override
+        public void onPreCheck(Network network) {
+            Log.d("NetworkCallback onPreCheck");
+            if ((mEvents & EVENT_PRECHECK) == EVENT_PRECHECK) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventNetworkCallback,
+                    new TelephonyEvents.NetworkCallbackEvent(
+                        mId,
+                        getNetworkCallbackEventString(EVENT_PRECHECK),
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE,
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE));
+            }
+        }
+
+        @Override
+        public void onAvailable(Network network) {
+            Log.d("NetworkCallback onAvailable");
+            if ((mEvents & EVENT_AVAILABLE) == EVENT_AVAILABLE) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventNetworkCallback,
+                    new TelephonyEvents.NetworkCallbackEvent(
+                        mId,
+                        getNetworkCallbackEventString(EVENT_AVAILABLE),
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE,
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE));
+            }
+        }
+
+        @Override
+        public void onLosing(Network network, int maxMsToLive) {
+            Log.d("NetworkCallback onLosing");
+            if ((mEvents & EVENT_LOSING) == EVENT_LOSING) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventNetworkCallback,
+                    new TelephonyEvents.NetworkCallbackEvent(
+                        mId,
+                        getNetworkCallbackEventString(EVENT_LOSING),
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE,
+                        maxMsToLive));
+            }
+        }
+
+        @Override
+        public void onLost(Network network) {
+            Log.d("NetworkCallback onLost");
+            if ((mEvents & EVENT_LOST) == EVENT_LOST) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventNetworkCallback,
+                    new TelephonyEvents.NetworkCallbackEvent(
+                        mId,
+                        getNetworkCallbackEventString(EVENT_LOST),
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE,
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE));
+            }
+        }
+
+        @Override
+        public void onUnavailable() {
+            Log.d("NetworkCallback onUnavailable");
+            if ((mEvents & EVENT_UNAVAILABLE) == EVENT_UNAVAILABLE) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventNetworkCallback,
+                    new TelephonyEvents.NetworkCallbackEvent(
+                        mId,
+                        getNetworkCallbackEventString(EVENT_UNAVAILABLE),
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE,
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE));
+            }
+        }
+
+        @Override
+        public void onCapabilitiesChanged(Network network,
+                NetworkCapabilities networkCapabilities) {
+            Log.d("NetworkCallback onCapabilitiesChanged. RSSI:" +
+                    networkCapabilities.getSignalStrength());
+            if ((mEvents & EVENT_CAPABILITIES_CHANGED) == EVENT_CAPABILITIES_CHANGED) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventNetworkCallback,
+                    new TelephonyEvents.NetworkCallbackEvent(
+                        mId,
+                        getNetworkCallbackEventString(EVENT_CAPABILITIES_CHANGED),
+                        networkCapabilities.getSignalStrength(),
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE));
+            }
+        }
+
+        @Override
+        public void onNetworkSuspended(Network network) {
+            Log.d("NetworkCallback onNetworkSuspended");
+            if ((mEvents & EVENT_SUSPENDED) == EVENT_SUSPENDED) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventNetworkCallback,
+                    new TelephonyEvents.NetworkCallbackEvent(
+                        mId,
+                        getNetworkCallbackEventString(EVENT_SUSPENDED),
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE,
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE));
+            }
+        }
+
+        @Override
+        public void onLinkPropertiesChanged(Network network,
+                LinkProperties linkProperties) {
+            Log.d("NetworkCallback onLinkPropertiesChanged");
+            if ((mEvents & EVENT_LINK_PROPERTIES_CHANGED) == EVENT_LINK_PROPERTIES_CHANGED) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventNetworkCallback,
+                    new TelephonyEvents.NetworkCallbackEvent(
+                        mId,
+                        getNetworkCallbackEventString(EVENT_LINK_PROPERTIES_CHANGED),
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE,
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE));
+            }
+        }
+
+        @Override
+        public void onNetworkResumed(Network network) {
+            Log.d("NetworkCallback onNetworkResumed");
+            if ((mEvents & EVENT_RESUMED) == EVENT_RESUMED) {
+                mEventFacade.postEvent(
+                    TelephonyConstants.EventNetworkCallback,
+                    new TelephonyEvents.NetworkCallbackEvent(
+                        mId,
+                        getNetworkCallbackEventString(EVENT_RESUMED),
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE,
+                        TelephonyEvents.NetworkCallbackEvent.INVALID_VALUE));
+            }
+        }
+    }
+
+    private static int getNetworkCallbackEvent(String event) {
+        switch (event) {
+            case TelephonyConstants.NetworkCallbackPreCheck:
+                return NetworkCallback.EVENT_PRECHECK;
+            case TelephonyConstants.NetworkCallbackAvailable:
+                return NetworkCallback.EVENT_AVAILABLE;
+            case TelephonyConstants.NetworkCallbackLosing:
+                return NetworkCallback.EVENT_LOSING;
+            case TelephonyConstants.NetworkCallbackLost:
+                return NetworkCallback.EVENT_LOST;
+            case TelephonyConstants.NetworkCallbackUnavailable:
+                return NetworkCallback.EVENT_UNAVAILABLE;
+            case TelephonyConstants.NetworkCallbackCapabilitiesChanged:
+                return NetworkCallback.EVENT_CAPABILITIES_CHANGED;
+            case TelephonyConstants.NetworkCallbackSuspended:
+                return NetworkCallback.EVENT_SUSPENDED;
+            case TelephonyConstants.NetworkCallbackResumed:
+                return NetworkCallback.EVENT_RESUMED;
+            case TelephonyConstants.NetworkCallbackLinkPropertiesChanged:
+                return NetworkCallback.EVENT_LINK_PROPERTIES_CHANGED;
+        }
+        return NetworkCallback.EVENT_INVALID;
+    }
+
+    private static String getNetworkCallbackEventString(int event) {
+        switch (event) {
+            case NetworkCallback.EVENT_PRECHECK:
+                return TelephonyConstants.NetworkCallbackPreCheck;
+            case NetworkCallback.EVENT_AVAILABLE:
+                return TelephonyConstants.NetworkCallbackAvailable;
+            case NetworkCallback.EVENT_LOSING:
+                return TelephonyConstants.NetworkCallbackLosing;
+            case NetworkCallback.EVENT_LOST:
+                return TelephonyConstants.NetworkCallbackLost;
+            case NetworkCallback.EVENT_UNAVAILABLE:
+                return TelephonyConstants.NetworkCallbackUnavailable;
+            case NetworkCallback.EVENT_CAPABILITIES_CHANGED:
+                return TelephonyConstants.NetworkCallbackCapabilitiesChanged;
+            case NetworkCallback.EVENT_SUSPENDED:
+                return TelephonyConstants.NetworkCallbackSuspended;
+            case NetworkCallback.EVENT_RESUMED:
+                return TelephonyConstants.NetworkCallbackResumed;
+            case NetworkCallback.EVENT_LINK_PROPERTIES_CHANGED:
+                return TelephonyConstants.NetworkCallbackLinkPropertiesChanged;
+        }
+        return TelephonyConstants.NetworkCallbackInvalid;
+    }
+
+    private static int getPacketKeepaliveReceiverEvent(String event) {
+        switch (event) {
+            case TelephonyConstants.PacketKeepaliveCallbackStarted:
+                return PacketKeepaliveReceiver.EVENT_STARTED;
+            case TelephonyConstants.PacketKeepaliveCallbackStopped:
+                return PacketKeepaliveReceiver.EVENT_STOPPED;
+            case TelephonyConstants.PacketKeepaliveCallbackError:
+                return PacketKeepaliveReceiver.EVENT_ERROR;
+        }
+        return PacketKeepaliveReceiver.EVENT_INVALID;
+    }
+
+    private static String getPacketKeepaliveReceiverEventString(int event) {
+        switch (event) {
+            case PacketKeepaliveReceiver.EVENT_STARTED:
+                return TelephonyConstants.PacketKeepaliveCallbackStarted;
+            case PacketKeepaliveReceiver.EVENT_STOPPED:
+                return TelephonyConstants.PacketKeepaliveCallbackStopped;
+            case PacketKeepaliveReceiver.EVENT_ERROR:
+                return TelephonyConstants.PacketKeepaliveCallbackError;
+        }
+        return TelephonyConstants.PacketKeepaliveCallbackInvalid;
+    }
+
+    private final ConnectivityManager mManager;
+    private final Service mService;
+    private final Context mContext;
+    private final ConnectivityReceiver mConnectivityReceiver;
+    private final EventFacade mEventFacade;
+    private PacketKeepalive mPacketKeepalive;
+    private NetworkCallback mNetworkCallback;
+    private static HashMap<String, PacketKeepaliveReceiver> mPacketKeepaliveReceiverMap =
+            new HashMap<String, PacketKeepaliveReceiver>();
+    private static HashMap<String, NetworkCallback> mNetworkCallbackMap =
+            new HashMap<String, NetworkCallback>();
+    private boolean mTrackingConnectivityStateChange;
+
+    public ConnectivityManagerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mContext = mService.getBaseContext();
+        mManager = (ConnectivityManager) mService.getSystemService(Context.CONNECTIVITY_SERVICE);
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mConnectivityReceiver = new ConnectivityReceiver();
+        mTrackingConnectivityStateChange = false;
+    }
+
+    @Rpc(description = "Listen for connectivity changes")
+    public void connectivityStartTrackingConnectivityStateChange() {
+        if (!mTrackingConnectivityStateChange) {
+            mTrackingConnectivityStateChange = true;
+            mContext.registerReceiver(mConnectivityReceiver,
+                    new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+        }
+    }
+
+    @Rpc(description = "start natt keep alive")
+    public String connectivityStartNattKeepalive(Integer intervalSeconds, String srcAddrString,
+            Integer srcPort, String dstAddrString) throws UnknownHostException {
+        try {
+            Network mNetwork = mManager.getActiveNetwork();
+            InetAddress srcAddr = InetAddress.getByName(srcAddrString);
+            InetAddress dstAddr = InetAddress.getByName(dstAddrString);
+            Log.d("startNattKeepalive srcAddr:" + srcAddr.getHostAddress());
+            Log.d("startNattKeepalive dstAddr:" + dstAddr.getHostAddress());
+            Log.d("startNattKeepalive srcPort:" + srcPort);
+            Log.d("startNattKeepalive intervalSeconds:" + intervalSeconds);
+            PacketKeepaliveReceiver mPacketKeepaliveReceiver = new PacketKeepaliveReceiver(
+                    PacketKeepaliveReceiver.EVENT_ALL);
+            mPacketKeepalive = mManager.startNattKeepalive(mNetwork, (int) intervalSeconds,
+                    mPacketKeepaliveReceiver, srcAddr, (int) srcPort, dstAddr);
+            if (mPacketKeepalive != null) {
+                mPacketKeepaliveReceiver.mPacketKeepalive = mPacketKeepalive;
+                String key = mPacketKeepaliveReceiver.mId;
+                mPacketKeepaliveReceiverMap.put(key, mPacketKeepaliveReceiver);
+                return key;
+            } else {
+                Log.e("startNattKeepalive fail, startNattKeepalive return null");
+                return null;
+            }
+        } catch (UnknownHostException e) {
+            Log.e("startNattKeepalive UnknownHostException");
+            return null;
+        }
+    }
+
+    @Rpc(description = "stop natt keep alive")
+    public Boolean connectivityStopNattKeepalive(String key) {
+        PacketKeepaliveReceiver mPacketKeepaliveReceiver =
+                mPacketKeepaliveReceiverMap.get(key);
+        if (mPacketKeepaliveReceiver != null) {
+            mPacketKeepaliveReceiverMap.remove(key);
+            mPacketKeepaliveReceiver.mPacketKeepalive.stop();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Rpc(description = "start listening for NattKeepalive Event")
+    public Boolean connectivityNattKeepaliveStartListeningForEvent(String key, String eventString) {
+        PacketKeepaliveReceiver mPacketKeepaliveReceiver =
+                mPacketKeepaliveReceiverMap.get(key);
+        if (mPacketKeepaliveReceiver != null) {
+            int event = getPacketKeepaliveReceiverEvent(eventString);
+            if (event == PacketKeepaliveReceiver.EVENT_INVALID) {
+                return false;
+            }
+            mPacketKeepaliveReceiver.startListeningForEvents(event);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Rpc(description = "stop listening for NattKeepalive Event")
+    public Boolean connectivityNattKeepaliveStopListeningForEvent(String key, String eventString) {
+        PacketKeepaliveReceiver mPacketKeepaliveReceiver =
+                mPacketKeepaliveReceiverMap.get(key);
+        if (mPacketKeepaliveReceiver != null) {
+            int event = getPacketKeepaliveReceiverEvent(eventString);
+            if (event == PacketKeepaliveReceiver.EVENT_INVALID) {
+                return false;
+            }
+            mPacketKeepaliveReceiver.stopListeningForEvents(event);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Rpc(description = "start listening for NetworkCallback Event")
+    public Boolean connectivityNetworkCallbackStartListeningForEvent(String key, String eventString) {
+        NetworkCallback mNetworkCallback = mNetworkCallbackMap.get(key);
+        if (mNetworkCallback != null) {
+            int event = getNetworkCallbackEvent(eventString);
+            if (event == NetworkCallback.EVENT_INVALID) {
+                return false;
+            }
+            mNetworkCallback.startListeningForEvents(event);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Rpc(description = "stop listening for NetworkCallback Event")
+    public Boolean connectivityNetworkCallbackStopListeningForEvent(String key, String eventString) {
+        NetworkCallback mNetworkCallback = mNetworkCallbackMap.get(key);
+        if (mNetworkCallback != null) {
+            int event = getNetworkCallbackEvent(eventString);
+            if (event == NetworkCallback.EVENT_INVALID) {
+                return false;
+            }
+            mNetworkCallback.stopListeningForEvents(event);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Rpc(description = "Set Rssi Threshold Monitor")
+    public String connectivitySetRssiThresholdMonitor(Integer rssi) {
+        Log.d("SL4A:setRssiThresholdMonitor rssi = " + rssi);
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        builder.setSignalStrength((int) rssi);
+        builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+        NetworkRequest networkRequest = builder.build();
+        mNetworkCallback = new NetworkCallback(NetworkCallback.EVENT_ALL);
+        mManager.registerNetworkCallback(networkRequest, mNetworkCallback);
+        String key = mNetworkCallback.mId;
+        mNetworkCallbackMap.put(key, mNetworkCallback);
+        return key;
+    }
+
+    @Rpc(description = "Stop Rssi Threshold Monitor")
+    public Boolean connectivityStopRssiThresholdMonitor(String key) {
+        Log.d("SL4A:stopRssiThresholdMonitor key = " + key);
+        return connectivityUnregisterNetworkCallback(key);
+    }
+
+    private NetworkRequest buildNetworkRequestFromJson(JSONObject configJson)
+            throws JSONException {
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
+
+        if (configJson.has("TransportType")) {
+            Log.d("build TransportType" + configJson.getInt("TransportType"));
+            builder.addTransportType(configJson.getInt("TransportType"));
+        }
+        if (configJson.has("SignalStrength")) {
+            Log.d("build SignalStrength" + configJson.getInt("SignalStrength"));
+            builder.setSignalStrength(configJson.getInt("SignalStrength"));
+        }
+        if (configJson.has("Capability")) {
+            JSONArray capabilities = configJson.getJSONArray("Capability");
+            for (int i = 0; i < capabilities.length(); i++) {
+                Log.d("build Capability" + capabilities.getInt(i));
+                builder.addCapability(capabilities.getInt(i));
+            }
+        }
+        if (configJson.has("LinkUpstreamBandwidthKbps")) {
+            Log.d("build LinkUpstreamBandwidthKbps" + configJson.getInt(
+                    "LinkUpstreamBandwidthKbps"));
+            builder.setLinkUpstreamBandwidthKbps(configJson.getInt(
+                    "LinkUpstreamBandwidthKbps"));
+        }
+        if (configJson.has("LinkDownstreamBandwidthKbps")) {
+            Log.d("build LinkDownstreamBandwidthKbps" + configJson.getInt(
+                    "LinkDownstreamBandwidthKbps"));
+            builder.setLinkDownstreamBandwidthKbps(configJson.getInt(
+                    "LinkDownstreamBandwidthKbps"));
+        }
+        if (configJson.has("NetworkSpecifier")) {
+            Log.d("build NetworkSpecifier" + configJson.getString("NetworkSpecifier"));
+            builder.setNetworkSpecifier(configJson.getString(
+                    "NetworkSpecifier"));
+        }
+        NetworkRequest networkRequest = builder.build();
+        return networkRequest;
+    }
+
+    @Rpc(description = "register a network callback")
+    public String connectivityRegisterNetworkCallback(@RpcParameter(name = "configJson")
+    JSONObject configJson) throws JSONException {
+        NetworkRequest networkRequest = buildNetworkRequestFromJson(configJson);
+        mNetworkCallback = new NetworkCallback(NetworkCallback.EVENT_ALL);
+        mManager.registerNetworkCallback(networkRequest, mNetworkCallback);
+        String key = mNetworkCallback.mId;
+        mNetworkCallbackMap.put(key, mNetworkCallback);
+        return key;
+    }
+
+    @Rpc(description = "unregister a network callback")
+    public Boolean connectivityUnregisterNetworkCallback(@RpcParameter(name = "key")
+    String key) {
+        mNetworkCallback = mNetworkCallbackMap.get(key);
+        if (mNetworkCallback != null) {
+            mNetworkCallbackMap.remove(key);
+            mManager.unregisterNetworkCallback(mNetworkCallback);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Rpc(description = "request a network")
+    public String connectivityRequestNetwork(@RpcParameter(name = "configJson")
+    JSONObject configJson) throws JSONException {
+        NetworkRequest networkRequest = buildNetworkRequestFromJson(configJson);
+        mNetworkCallback = new NetworkCallback(NetworkCallback.EVENT_ALL);
+        mManager.requestNetwork(networkRequest, mNetworkCallback);
+        String key = mNetworkCallback.mId;
+        mNetworkCallbackMap.put(key, mNetworkCallback);
+        return key;
+    }
+
+    @Rpc(description = "Stop listening for connectivity changes")
+    public void connectivityStopTrackingConnectivityStateChange() {
+        if (mTrackingConnectivityStateChange) {
+            mTrackingConnectivityStateChange = false;
+            mContext.unregisterReceiver(mConnectivityReceiver);
+        }
+    }
+
+    @Rpc(description = "Get the extra information about the network state provided by lower network layers.")
+    public String connectivityNetworkGetActiveConnectionExtraInfo() {
+        NetworkInfo current = mManager.getActiveNetworkInfo();
+        if (current == null) {
+            Log.d("No network is active at the moment.");
+            return null;
+        }
+        return current.getExtraInfo();
+    }
+
+    @Rpc(description = "Return the subtype name of the current network, null if not connected")
+    public String connectivityNetworkGetActiveConnectionSubtypeName() {
+        NetworkInfo current = mManager.getActiveNetworkInfo();
+        if (current == null) {
+            Log.d("No network is active at the moment.");
+            return null;
+        }
+        return current.getSubtypeName();
+    }
+
+    @Rpc(description = "Return a human-readable name describe the type of the network, e.g. WIFI")
+    public String connectivityNetworkGetActiveConnectionTypeName() {
+        NetworkInfo current = mManager.getActiveNetworkInfo();
+        if (current == null) {
+            Log.d("No network is active at the moment.");
+            return null;
+        }
+        return current.getTypeName();
+    }
+
+    @Rpc(description = "Get connection status information about all network types supported by the device.")
+    public NetworkInfo[] connectivityNetworkGetAllInfo() {
+        return mManager.getAllNetworkInfo();
+    }
+
+    @Rpc(description = "Check whether the active network is connected to the Internet.")
+    public Boolean connectivityNetworkIsConnected() {
+        NetworkInfo current = mManager.getActiveNetworkInfo();
+        if (current == null) {
+            Log.d("No network is active at the moment.");
+            return false;
+        }
+        return current.isConnected();
+    }
+
+    @Rpc(description = "Checks the airplane mode setting.",
+            returns = "True if airplane mode is enabled.")
+    public Boolean connectivityCheckAirplaneMode() {
+        try {
+            return android.provider.Settings.System.getInt(mService.getContentResolver(),
+                    android.provider.Settings.Global.AIRPLANE_MODE_ON) == AIRPLANE_MODE_ON;
+        } catch (SettingNotFoundException e) {
+            return false;
+        }
+    }
+
+    @Rpc(description = "Toggles airplane mode on and off.",
+            returns = "True if airplane mode is enabled.")
+    public void connectivityToggleAirplaneMode(@RpcParameter(name = "enabled")
+    @RpcOptional
+    Boolean enabled) {
+        if (enabled == null) {
+            enabled = !connectivityCheckAirplaneMode();
+        }
+        mManager.setAirplaneMode(enabled);
+    }
+
+    @Rpc(description = "Check if tethering supported or not.",
+            returns = "True if tethering is supported.")
+    public boolean connectivityIsTetheringSupported() {
+        return mManager.isTetheringSupported();
+    }
+
+    @Override
+    public void shutdown() {
+        connectivityStopTrackingConnectivityStateChange();
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ContactsFacade.java b/Common/src/com/googlecode/android_scripting/facade/ContactsFacade.java
new file mode 100644
index 0000000..4ab4a1d
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ContactsFacade.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Provides access to contacts related functionality.
+ *
+ * @author MeanEYE.rcf (meaneye.rcf@gmail.com
+ */
+public class ContactsFacade extends RpcReceiver {
+  private static final Uri CONTACTS_URI = Uri.parse("content://contacts/people");
+  private final ContentResolver mContentResolver;
+  private final Service mService;
+  private final CommonIntentsFacade mCommonIntentsFacade;
+  public Uri mPhoneContent = null;
+  public String mContactId;
+  public String mPrimary;
+  public String mPhoneNumber;
+  public String mHasPhoneNumber;
+
+  public ContactsFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mContentResolver = mService.getContentResolver();
+    mCommonIntentsFacade = manager.getReceiver(CommonIntentsFacade.class);
+    try {
+      // Backward compatibility... get contract stuff using reflection
+      Class<?> phone = Class.forName("android.provider.ContactsContract$CommonDataKinds$Phone");
+      mPhoneContent = (Uri) phone.getField("CONTENT_URI").get(null);
+      mContactId = (String) phone.getField("CONTACT_ID").get(null);
+      mPrimary = (String) phone.getField("IS_PRIMARY").get(null);
+      mPhoneNumber = (String) phone.getField("NUMBER").get(null);
+      mHasPhoneNumber = (String) phone.getField("HAS_PHONE_NUMBER").get(null);
+    } catch (Exception e) {
+    }
+  }
+
+  private Uri buildUri(Integer id) {
+    Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
+    return uri;
+  }
+
+  @Rpc(description = "Displays a list of contacts to pick from.", returns = "A map of result values.")
+  public Intent pickContact() throws JSONException {
+    return mCommonIntentsFacade.pick("content://contacts/people");
+  }
+
+  @Rpc(description = "Displays a list of phone numbers to pick from.", returns = "The selected phone number.")
+  public String pickPhone() throws JSONException {
+    String result = null;
+    Intent data = mCommonIntentsFacade.pick("content://contacts/phones");
+    if (data != null) {
+      Uri phoneData = data.getData();
+      Cursor cursor = mService.getContentResolver().query(phoneData, null, null, null, null);
+      if (cursor != null) {
+        if (cursor.moveToFirst()) {
+          result = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.PhoneLookup.NUMBER));
+        }
+        cursor.close();
+      }
+    }
+    return result;
+  }
+
+  @Rpc(description = "Returns a List of all possible attributes for contacts.")
+  public List<String> contactsGetAttributes() {
+    List<String> result = new ArrayList<String>();
+    Cursor cursor = mContentResolver.query(CONTACTS_URI, null, null, null, null);
+    if (cursor != null) {
+      String[] columns = cursor.getColumnNames();
+      for (int i = 0; i < columns.length; i++) {
+        result.add(columns[i]);
+      }
+      cursor.close();
+    }
+    return result;
+  }
+
+  // TODO(MeanEYE.rcf): Add ability to narrow selection by providing named pairs of attributes.
+  @Rpc(description = "Returns a List of all contact IDs.")
+  public List<Integer> contactsGetIds() {
+    List<Integer> result = new ArrayList<Integer>();
+    String[] columns = { "_id" };
+    Cursor cursor = mContentResolver.query(CONTACTS_URI, columns, null, null, null);
+    if (cursor != null) {
+      while (cursor.moveToNext()) {
+        result.add(cursor.getInt(0));
+      }
+      cursor.close();
+    }
+    return result;
+  }
+
+  @Rpc(description = "Returns a List of all contacts.", returns = "a List of contacts as Maps")
+  public List<JSONObject> contactsGet(
+      @RpcParameter(name = "attributes") @RpcOptional JSONArray attributes) throws JSONException {
+    List<JSONObject> result = new ArrayList<JSONObject>();
+    String[] columns;
+    if (attributes == null || attributes.length() == 0) {
+      // In case no attributes are specified we set the default ones.
+      columns = new String[] { "_id", "name", "primary_phone", "primary_email", "type" };
+    } else {
+      // Convert selected attributes list into usable string list.
+      columns = new String[attributes.length()];
+      for (int i = 0; i < attributes.length(); i++) {
+        columns[i] = attributes.getString(i);
+      }
+    }
+    List<String> queryList = new ArrayList<String>();
+    for (String s : columns) {
+      queryList.add(s);
+    }
+    if (!queryList.contains("_id")) {
+      queryList.add("_id");
+    }
+
+    String[] query = queryList.toArray(new String[queryList.size()]);
+    Cursor cursor = mContentResolver.query(CONTACTS_URI, query, null, null, null);
+    if (cursor != null) {
+      int idIndex = cursor.getColumnIndex("_id");
+      while (cursor.moveToNext()) {
+        String id = cursor.getString(idIndex);
+        JSONObject message = new JSONObject();
+        for (int i = 0; i < columns.length; i++) {
+          String key = columns[i];
+          String value = cursor.getString(cursor.getColumnIndex(key));
+          if (mPhoneNumber != null) {
+            if (key.equals("primary_phone")) {
+              value = findPhone(id);
+            }
+          }
+          message.put(key, value);
+        }
+        result.add(message);
+      }
+      cursor.close();
+    }
+    return result;
+  }
+
+  private String findPhone(String id) {
+    String result = null;
+    if (id == null || id.equals("")) {
+      return result;
+    }
+    try {
+      if (Integer.parseInt(id) > 0) {
+        Cursor pCur =
+            mContentResolver.query(mPhoneContent, new String[] { mPhoneNumber }, mContactId
+                + " = ? and " + mPrimary + "=1", new String[] { id }, null);
+        if (pCur != null) {
+          pCur.getColumnNames();
+          while (pCur.moveToNext()) {
+            result = pCur.getString(0);
+            break;
+          }
+        }
+        pCur.close();
+      }
+    } catch (Exception e) {
+      return null;
+    }
+    return result;
+  }
+
+  @Rpc(description = "Returns contacts by ID.")
+  public JSONObject contactsGetById(@RpcParameter(name = "id") Integer id,
+      @RpcParameter(name = "attributes") @RpcOptional JSONArray attributes) throws JSONException {
+    JSONObject result = null;
+    Uri uri = buildUri(id);
+    String[] columns;
+    if (attributes == null || attributes.length() == 0) {
+      // In case no attributes are specified we set the default ones.
+      columns = new String[] { "_id", "name", "primary_phone", "primary_email", "type" };
+    } else {
+      // Convert selected attributes list into usable string list.
+      columns = new String[attributes.length()];
+      for (int i = 0; i < attributes.length(); i++) {
+        columns[i] = attributes.getString(i);
+      }
+    }
+    Cursor cursor = mContentResolver.query(uri, columns, null, null, null);
+    if (cursor != null) {
+      result = new JSONObject();
+      cursor.moveToFirst();
+      for (int i = 0; i < columns.length; i++) {
+        result.put(columns[i], cursor.getString(i));
+      }
+      cursor.close();
+    }
+    return result;
+  }
+
+  // TODO(MeanEYE.rcf): Add ability to narrow selection by providing named pairs of attributes.
+  @Rpc(description = "Returns the number of contacts.")
+  public Integer contactsGetCount() {
+    Integer result = 0;
+    Cursor cursor = mContentResolver.query(CONTACTS_URI, null, null, null, null);
+    if (cursor != null) {
+      result = cursor.getCount();
+      cursor.close();
+    }
+    return result;
+  }
+
+  private String[] jsonToArray(JSONArray array) throws JSONException {
+    String[] result = null;
+    if (array != null && array.length() > 0) {
+      result = new String[array.length()];
+      for (int i = 0; i < array.length(); i++) {
+        result[i] = array.getString(i);
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Exactly as per <a href=
+   * "http://developer.android.com/reference/android/content/ContentResolver.html#query%28android.net.Uri,%20java.lang.String[],%20java.lang.String,%20java.lang.String[],%20java.lang.String%29"
+   * >ContentResolver.query</a>
+   */
+  @Rpc(description = "Content Resolver Query", returns = "result of query as Maps")
+  public List<JSONObject> queryContent(
+      @RpcParameter(name = "uri", description = "The URI, using the content:// scheme, for the content to retrieve.") String uri,
+      @RpcParameter(name = "attributes", description = "A list of which columns to return. Passing null will return all columns") @RpcOptional JSONArray attributes,
+      @RpcParameter(name = "selection", description = "A filter declaring which rows to return") @RpcOptional String selection,
+      @RpcParameter(name = "selectionArgs", description = "You may include ?s in selection, which will be replaced by the values from selectionArgs") @RpcOptional JSONArray selectionArgs,
+      @RpcParameter(name = "order", description = "How to order the rows") @RpcOptional String order)
+      throws JSONException {
+    List<JSONObject> result = new ArrayList<JSONObject>();
+    String[] columns = jsonToArray(attributes);
+    String[] args = jsonToArray(selectionArgs);
+    Cursor cursor = mContentResolver.query(Uri.parse(uri), columns, selection, args, order);
+    if (cursor != null) {
+      String[] names = cursor.getColumnNames();
+      while (cursor.moveToNext()) {
+        JSONObject message = new JSONObject();
+        for (int i = 0; i < cursor.getColumnCount(); i++) {
+          String key = names[i];
+          String value = cursor.getString(i);
+          message.put(key, value);
+        }
+        result.add(message);
+      }
+      cursor.close();
+    }
+    return result;
+  }
+
+  @Rpc(description = "Content Resolver Query Attributes", returns = "a list of available columns for a given content uri")
+  public JSONArray queryAttributes(
+      @RpcParameter(name = "uri", description = "The URI, using the content:// scheme, for the content to retrieve.") String uri)
+      throws JSONException {
+    JSONArray result = new JSONArray();
+    Cursor cursor = mContentResolver.query(Uri.parse(uri), null, "1=0", null, null);
+    if (cursor != null) {
+      String[] names = cursor.getColumnNames();
+      for (String name : names) {
+        result.put(name);
+      }
+      cursor.close();
+    }
+    return result;
+  }
+
+  @Override
+  public void shutdown() {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/DisplayFacade.java b/Common/src/com/googlecode/android_scripting/facade/DisplayFacade.java
new file mode 100644
index 0000000..d0c472c
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/DisplayFacade.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import java.util.HashMap;
+
+import android.app.Service;
+import android.content.Context;
+import android.graphics.Point;
+import android.hardware.display.DisplayManager;
+import android.util.DisplayMetrics;
+import android.view.Display;
+
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+public class DisplayFacade extends RpcReceiver {
+
+    private final Service mService;
+    private final DisplayManager mDisplayManager;
+    private HashMap<Integer, Display> mDisplays;
+
+    public DisplayFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mDisplayManager = (DisplayManager) mService.getSystemService(Context.DISPLAY_SERVICE);
+        updateDisplays(mDisplayManager.getDisplays());
+    }
+
+    private void updateDisplays(Display[] displays) {
+        if (mDisplays == null) {
+            mDisplays = new HashMap<Integer, Display>();
+        }
+        mDisplays.clear();
+        for(Display d : displays) {
+            mDisplays.put(d.getDisplayId(), d);
+        }
+    }
+
+    @Rpc(description = "Get a list of IDs of the logical displays connected."
+                     + "Also updates the cached displays.")
+    public Integer[] displayGetDisplays() {
+        Display[] displays = mDisplayManager.getDisplays();
+        updateDisplays(displays);
+        Integer[] results = new Integer[displays.length];
+        for(int i = 0; i < displays.length; i++) {
+            results[i] = displays[i].getDisplayId();
+        }
+        return results;
+    }
+
+    @Rpc(description = "Get the size of the specified display in pixels.")
+    public Point displayGetSize(
+            @RpcParameter(name = "displayId")
+            @RpcDefault(value = "0")
+            Integer displayId) {
+        Point outSize = new Point();
+        Display d = mDisplays.get(displayId);
+        d.getSize(outSize);
+        return outSize;
+    }
+
+    @Rpc(description = "Get the maximum screen size dimension that will happen.")
+    public Integer displayGetMaximumSizeDimension(
+            @RpcParameter(name = "displayId")
+            @RpcDefault(value = "0")
+            Integer displayId) {
+        Display d = mDisplays.get(displayId);
+        return d.getMaximumSizeDimension();
+    }
+
+    @Rpc(description = "Get display metrics based on the real size of this display.")
+    public DisplayMetrics displayGetRealMetrics(
+            @RpcParameter(name = "displayId")
+            @RpcDefault(value = "0")
+            Integer displayId) {
+        Display d = mDisplays.get(displayId);
+        DisplayMetrics outMetrics = new DisplayMetrics();
+        d.getRealMetrics(outMetrics);
+        return outMetrics;
+    }
+
+    @Override
+    public void shutdown() {
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/EventFacade.java b/Common/src/com/googlecode/android_scripting/facade/EventFacade.java
new file mode 100644
index 0000000..36e620d
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/EventFacade.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+
+import org.json.JSONException;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.event.Event;
+import com.googlecode.android_scripting.event.EventObserver;
+import com.googlecode.android_scripting.event.EventServer;
+import com.googlecode.android_scripting.future.FutureResult;
+import com.googlecode.android_scripting.jsonrpc.JsonBuilder;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcDeprecated;
+import com.googlecode.android_scripting.rpc.RpcName;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * Manage the event queue. <br>
+ * <br>
+ * <b>Usage Notes:</b><br>
+ * EventFacade APIs interact with the Event Queue (a data buffer containing up to 1024 event
+ * entries).<br>
+ * Events are automatically entered into the Event Queue following API calls such as startSensing()
+ * and startLocating().<br>
+ * The Event Facade provides control over how events are entered into (and removed from) the Event
+ * Queue.<br>
+ * The Event Queue provides a useful means of recording background events (such as sensor data) when
+ * the phone is busy with foreground activities.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ */
+public class EventFacade extends RpcReceiver {
+    /**
+     * The maximum length of the event queue. Old events will be discarded when this limit is
+     * exceeded.
+     */
+    private static final int MAX_QUEUE_SIZE = 1024;
+    private final Queue<Event> mEventQueue = new ConcurrentLinkedQueue<Event>();
+    private final CopyOnWriteArrayList<EventObserver> mGlobalEventObservers =
+            new CopyOnWriteArrayList<EventObserver>();
+    private final Multimap<String, EventObserver> mNamedEventObservers = Multimaps
+            .synchronizedListMultimap(ArrayListMultimap.<String, EventObserver> create());
+    private EventServer mEventServer = null;
+    private final HashMap<String, BroadcastListener> mBroadcastListeners =
+            new HashMap<String, BroadcastListener>();
+    private final Context mContext;
+    private boolean bEventServerRunning;
+
+    public EventFacade(FacadeManager manager) {
+        super(manager);
+        mContext = manager.getService().getApplicationContext();
+        Log.v("Creating new EventFacade Instance()");
+        bEventServerRunning = false;
+    }
+
+    /**
+     * Example (python): droid.eventClearBuffer()
+     */
+    @Rpc(description = "Clears all events from the event buffer.")
+    public void eventClearBuffer() {
+        mEventQueue.clear();
+    }
+
+    /**
+     * Registers a listener for a new broadcast signal
+     */
+    @Rpc(description = "Registers a listener for a new broadcast signal")
+    public boolean eventRegisterForBroadcast(
+            @RpcParameter(name = "category") String category,
+            @RpcParameter(name = "enqueue",
+                    description = "Should this events be added to the event queue or only dispatched") @RpcDefault(value = "true") Boolean enqueue) {
+        if (mBroadcastListeners.containsKey(category)) {
+            return false;
+        }
+
+        BroadcastListener b = new BroadcastListener(this, enqueue.booleanValue());
+        IntentFilter c = new IntentFilter(category);
+        mContext.registerReceiver(b, c);
+        mBroadcastListeners.put(category, b);
+
+        return true;
+    }
+
+    @Rpc(description = "Stop listening for a broadcast signal")
+    public void eventUnregisterForBroadcast(
+            @RpcParameter(name = "category") String category) {
+        if (!mBroadcastListeners.containsKey(category)) {
+            return;
+        }
+
+        mContext.unregisterReceiver(mBroadcastListeners.get(category));
+        mBroadcastListeners.remove(category);
+    }
+
+    @Rpc(description = "Lists all the broadcast signals we are listening for")
+    public Set<String> eventGetBrodcastCategories() {
+        return mBroadcastListeners.keySet();
+    }
+
+    /**
+     * Actual data returned in the map will depend on the type of event.
+     *
+     * <pre>
+     * Example (python):
+     *     import android, time
+     *     droid = android.Android()
+     *     droid.startSensing()
+     *     time.sleep(1)
+     *     droid.eventClearBuffer()
+     *     time.sleep(1)
+     *     e = eventPoll(1).result
+     *     event_entry_number = 0
+     *     x = e[event_entry_ number]['data']['xforce']
+     * </pre>
+     *
+     * e has the format:<br>
+     * [{u'data': {u'accuracy': 0, u'pitch': -0.48766891956329345, u'xmag': -5.6875, u'azimuth':
+     * 0.3312483489513397, u'zforce': 8.3492730000000002, u'yforce': 4.5628165999999997, u'time':
+     * 1297072704.813, u'ymag': -11.125, u'zmag': -42.375, u'roll': -0.059393649548292161,
+     * u'xforce': 0.42223078000000003}, u'name': u'sensors', u'time': 1297072704813000L}]<br>
+     * x has the string value of the x force data (0.42223078000000003) at the time of the event
+     * entry. </pre>
+     */
+
+    @Rpc(description = "Returns and removes the oldest n events (i.e. location or sensor update, etc.) from the event buffer.",
+            returns = "A List of Maps of event properties.")
+    public List<Event> eventPoll(
+            @RpcParameter(name = "number_of_events") @RpcDefault("1") Integer number_of_events) {
+        List<Event> events = Lists.newArrayList();
+        for (int i = 0; i < number_of_events; i++) {
+            Event event = mEventQueue.poll();
+            if (event == null) {
+                break;
+            }
+            events.add(event);
+        }
+        return events;
+    }
+
+    @Rpc(description = "Blocks until an event with the supplied name occurs. Event is removed from the buffer if removeEvent is True.",
+            returns = "Map of event properties.")
+    public Event eventWaitFor(
+            @RpcParameter(name = "eventName")
+            final String eventName,
+            @RpcParameter(name = "removeEvent")
+            final Boolean removeEvent,
+            @RpcParameter(name = "timeout", description = "the maximum time to wait (in ms)") @RpcOptional Integer timeout)
+            throws InterruptedException {
+        Event result = null;
+        final FutureResult<Event> futureEvent;
+        synchronized (mEventQueue) { // First check to make sure it isn't already there
+            for (Event event : mEventQueue) {
+                if (event.getName().equals(eventName)) {
+                    result = event;
+                    if (removeEvent)
+                        mEventQueue.remove(event);
+                    return result;
+                }
+            }
+            futureEvent = new FutureResult<Event>();
+            addNamedEventObserver(eventName, new EventObserver() {
+                @Override
+                public void onEventReceived(Event event) {
+                    if (event.getName().equals(eventName)) {
+                        synchronized (futureEvent) {
+                            if (!futureEvent.isDone()) {
+                                futureEvent.set(event);
+                                // TODO(navtej) Remove log.
+                                Log.v(String.format("Removeing observer (%s) got event  (%s)",
+                                        this,
+                                        event));
+                                removeEventObserver(this);
+                            }
+                            if (removeEvent)
+                                mEventQueue.remove(event);
+                        }
+                    }
+                }
+            });
+        }
+        if (futureEvent != null) {
+            if (timeout != null) {
+                result = futureEvent.get(timeout, TimeUnit.MILLISECONDS);
+            } else {
+                result = futureEvent.get();
+            }
+        }
+        return result;
+    }
+
+    @Rpc(description = "Blocks until an event occurs. The returned event is removed from the buffer.",
+            returns = "Map of event properties.")
+    public Event eventWait(
+            @RpcParameter(name = "timeout", description = "the maximum time to wait") @RpcOptional Integer timeout)
+            throws InterruptedException {
+        Event result = null;
+        final FutureResult<Event> futureEvent = new FutureResult<Event>();
+        EventObserver observer;
+        synchronized (mEventQueue) { // Anything in queue?
+            if (mEventQueue.size() > 0) {
+                return mEventQueue.poll(); // return it.
+            }
+            observer = new EventObserver() {
+                @Override
+                public void onEventReceived(Event event) { // set up observer for any events.
+                    synchronized (futureEvent) {
+                        if (!futureEvent.isDone()) {
+                            futureEvent.set(event);
+                            // TODO(navtej) Remove log.
+                            Log.v(String.format("onEventReceived for event (%s)", event));
+                        }
+                    }
+                }
+            };
+            addGlobalEventObserver(observer);
+        }
+        if (timeout != null) {
+            result = futureEvent.get(timeout, TimeUnit.MILLISECONDS);
+        } else {
+            result = futureEvent.get();
+        }
+        if (result != null) {
+            mEventQueue.remove(result);
+        }
+        // TODO(navtej) Remove log.
+        Log.v(String.format("Removeing observer (%s) got event  (%s)", observer, result));
+        if (observer != null) {
+            removeEventObserver(observer); // Make quite sure this goes away.
+        }
+        return result;
+    }
+
+    /**
+     * <pre>
+     * Example:
+     *   import android
+     *   from datetime import datetime
+     *   droid = android.Android()
+     *   t = datetime.now()
+     *   droid.eventPost('Some Event', t)
+     * </pre>
+     */
+    @Rpc(description = "Post an event to the event queue.")
+    public void eventPost(
+            @RpcParameter(name = "name", description = "Name of event") String name,
+            @RpcParameter(name = "data", description = "Data contained in event.") String data,
+            @RpcParameter(name = "enqueue",
+                    description = "Set to False if you don't want your events to be added to the event queue, just dispatched.") @RpcOptional @RpcDefault("false") Boolean enqueue) {
+        postEvent(name, data, enqueue.booleanValue());
+    }
+
+    /**
+     * Post an event and queue it
+     */
+    public void postEvent(String name, Object data) {
+        postEvent(name, data, true);
+    }
+
+    /**
+     * Posts an event with to the event queue.
+     */
+    public void postEvent(String name, Object data, boolean enqueue) {
+        Event event = new Event(name, data);
+        if (enqueue != false) {
+            synchronized (mEventQueue) {
+                while (mEventQueue.size() >= MAX_QUEUE_SIZE) {
+                    mEventQueue.remove();
+                }
+                mEventQueue.add(event);
+            }
+            Log.v(String.format("postEvent(%s)", name));
+        }
+        synchronized (mNamedEventObservers) {
+            for (EventObserver observer : mNamedEventObservers.get(name)) {
+                observer.onEventReceived(event);
+            }
+        }
+        synchronized (mGlobalEventObservers) {
+            // TODO(navtej) Remove log.
+            Log.v(String.format("mGlobalEventObservers size (%s)", mGlobalEventObservers.size()));
+            for (EventObserver observer : mGlobalEventObservers) {
+                observer.onEventReceived(event);
+            }
+        }
+    }
+
+    @RpcDeprecated(value = "eventPost", release = "r4")
+    @Rpc(description = "Post an event to the event queue.")
+    @RpcName(name = "postEvent")
+    public void rpcPostEvent(
+            @RpcParameter(name = "name") String name,
+            @RpcParameter(name = "data") String data) {
+        postEvent(name, data);
+    }
+
+    @RpcDeprecated(value = "eventPoll", release = "r4")
+    @Rpc(description = "Returns and removes the oldest event (i.e. location or sensor update, etc.) from the event buffer.",
+            returns = "Map of event properties.")
+    public Event receiveEvent() {
+        return mEventQueue.poll();
+    }
+
+    @RpcDeprecated(value = "eventWaitFor", release = "r4")
+    @Rpc(description = "Blocks until an event with the supplied name occurs. Event is removed from the buffer if removeEvent is True.",
+            returns = "Map of event properties.")
+    public Event waitForEvent(
+            @RpcParameter(name = "eventName")
+            final String eventName,
+            @RpcOptional
+            final Boolean removeEvent,
+            @RpcParameter(name = "timeout", description = "the maximum time to wait") @RpcOptional Integer timeout)
+            throws InterruptedException {
+        return eventWaitFor(eventName, removeEvent, timeout);
+    }
+
+    @Rpc(description = "Opens up a socket where you can read for events posted")
+    public int startEventDispatcher(
+            @RpcParameter(name = "port", description = "Port to use") @RpcDefault("0") @RpcOptional() Integer port) {
+        if (mEventServer == null) {
+            if (port == null) {
+                port = 0;
+            }
+            mEventServer = new EventServer(port);
+            addGlobalEventObserver(mEventServer);
+            bEventServerRunning = true;
+        }
+        return mEventServer.getAddress().getPort();
+    }
+
+    @Rpc(description = "sl4a session is shutting down, send terminate event to client.")
+    public void closeSl4aSession() {
+        eventClearBuffer();
+        postEvent("EventDispatcherShutdown", null);
+    }
+
+    @Rpc(description = "Stops the event server, you can't read in the port anymore")
+    public void stopEventDispatcher() throws RuntimeException {
+        if (bEventServerRunning == true) {
+            if (mEventServer == null) {
+                throw new RuntimeException("Not running");
+            }
+            bEventServerRunning = false;
+            mEventServer.shutdown();
+            Log.v(String.format("stopEventDispatcher   (%s)", mEventServer));
+            removeEventObserver(mEventServer);
+            mEventServer = null;
+        }
+        return;
+    }
+
+    @Override
+    public void shutdown() {
+
+        try {
+            stopEventDispatcher();
+        } catch (Exception e) {
+            Log.e("Exception tearing down event dispatcher", e);
+        }
+        mGlobalEventObservers.clear();
+        mEventQueue.clear();
+    }
+
+    public void addNamedEventObserver(String eventName, EventObserver observer) {
+        mNamedEventObservers.put(eventName, observer);
+    }
+
+    public void addGlobalEventObserver(EventObserver observer) {
+        mGlobalEventObservers.add(observer);
+    }
+
+    public void removeEventObserver(EventObserver observer) {
+        mNamedEventObservers.removeAll(observer);
+        mGlobalEventObservers.remove(observer);
+    }
+
+    public class BroadcastListener extends android.content.BroadcastReceiver {
+        private EventFacade mParent;
+        private boolean mEnQueue;
+
+        public BroadcastListener(EventFacade parent, boolean enqueue) {
+            mParent = parent;
+            mEnQueue = enqueue;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Bundle data;
+            if (intent.getExtras() != null) {
+                data = (Bundle) intent.getExtras().clone();
+            } else {
+                data = new Bundle();
+            }
+            data.putString("action", intent.getAction());
+            try {
+                mParent.eventPost("sl4a", JsonBuilder.build(data).toString(), mEnQueue);
+            } catch (JSONException e) {
+                e.printStackTrace();
+            }
+        }
+
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/FacadeManager.java b/Common/src/com/googlecode/android_scripting/facade/FacadeManager.java
new file mode 100644
index 0000000..90de7f9
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/FacadeManager.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.Service;
+import android.content.Intent;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.exception.Sl4aException;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
+import com.googlecode.android_scripting.rpc.RpcDeprecated;
+import com.googlecode.android_scripting.rpc.RpcMinSdk;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collection;
+
+public class FacadeManager extends RpcReceiverManager {
+
+  private final Service mService;
+  private final Intent mIntent;
+  private int mSdkLevel;
+
+  public FacadeManager(int sdkLevel, Service service, Intent intent,
+      Collection<Class<? extends RpcReceiver>> classList) {
+    super(classList);
+    mSdkLevel = sdkLevel;
+    mService = service;
+    mIntent = intent;
+  }
+
+  public int getSdkLevel() {
+    return mSdkLevel;
+  }
+
+  public Service getService() {
+    return mService;
+  }
+
+  public Intent getIntent() {
+    return mIntent;
+  }
+
+  @Override
+  public Object invoke(Class<? extends RpcReceiver> clazz, Method method, Object[] args)
+      throws Exception {
+    try {
+      if (method.isAnnotationPresent(RpcDeprecated.class)) {
+        String replacedBy = method.getAnnotation(RpcDeprecated.class).value();
+        String title = method.getName() + " is deprecated";
+        Log.notify(mService, title, title, String.format("Please use %s instead.", replacedBy));
+      } else if (method.isAnnotationPresent(RpcMinSdk.class)) {
+        int requiredSdkLevel = method.getAnnotation(RpcMinSdk.class).value();
+        if (mSdkLevel < requiredSdkLevel) {
+          throw new Sl4aException(String.format("%s requires API level %d, current level is %d",
+              method.getName(), requiredSdkLevel, mSdkLevel));
+        }
+      }
+      return super.invoke(clazz, method, args);
+    } catch (InvocationTargetException e) {
+      if (e.getCause() instanceof SecurityException) {
+        Log.notify(mService, "RPC invoke failed...", mService.getPackageName(), e.getCause()
+            .getMessage());
+      }
+      throw e;
+    }
+  }
+
+  public AndroidFacade.Resources getAndroidFacadeResources() {
+    return new AndroidFacade.Resources() {
+      @Override
+      public int getLogo48() {
+        // TODO(Alexey): As an alternative, ask application for resource ids.
+        String packageName = mService.getApplication().getPackageName();
+        return mService.getResources().getIdentifier("script_logo_48", "drawable", packageName);
+      }
+    };
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/FacadeManagerFactory.java b/Common/src/com/googlecode/android_scripting/facade/FacadeManagerFactory.java
new file mode 100644
index 0000000..e436d03
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/FacadeManagerFactory.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.Service;
+import android.content.Intent;
+
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManagerFactory;
+
+import java.util.HashMap;
+import java.util.Collection;
+import java.util.Map;
+
+public class FacadeManagerFactory implements RpcReceiverManagerFactory {
+
+  private final int mSdkLevel;
+  private final Service mService;
+  private final Intent mIntent;
+  private final Collection<Class<? extends RpcReceiver>> mClassList;
+  private final Map<Integer, RpcReceiverManager> mFacadeManagers;
+
+  public FacadeManagerFactory(int sdkLevel, Service service, Intent intent,
+      Collection<Class<? extends RpcReceiver>> classList) {
+    mSdkLevel = sdkLevel;
+    mService = service;
+    mIntent = intent;
+    mClassList = classList;
+    mFacadeManagers = new HashMap<Integer, RpcReceiverManager>();
+  }
+
+  @Override
+  public FacadeManager create(Integer UID) {
+    FacadeManager facadeManager = new FacadeManager(mSdkLevel, mService, mIntent, mClassList);
+    mFacadeManagers.put(UID, facadeManager);
+    return facadeManager;
+  }
+
+  @Override
+  public Map<Integer, RpcReceiverManager> getRpcReceiverManagers() {
+    return mFacadeManagers;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/LocationFacade.java b/Common/src/com/googlecode/android_scripting/facade/LocationFacade.java
new file mode 100644
index 0000000..eed244e
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/LocationFacade.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.Service;
+import android.content.Context;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+
+import com.google.common.collect.Maps;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+import com.googlecode.android_scripting.rpc.RpcStartEvent;
+import com.googlecode.android_scripting.rpc.RpcStopEvent;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * This facade exposes the LocationManager related functionality.<br>
+ * <br>
+ * <b>Overview</b><br>
+ * Once activated by 'startLocating' the LocationFacade attempts to return location data collected
+ * via GPS or the cell network. If neither are available the last known location may be retrieved.
+ * If both are available the format of the returned data is:<br>
+ * {u'network': {u'altitude': 0, u'provider': u'network', u'longitude': -0.38509020000000002,
+ * u'time': 1297079691231L, u'latitude': 52.410557300000001, u'speed': 0, u'accuracy': 75}, u'gps':
+ * {u'altitude': 51, u'provider': u'gps', u'longitude': -0.38537094593048096, u'time':
+ * 1297079709000L, u'latitude': 52.41076922416687, u'speed': 0, u'accuracy': 24}}<br>
+ * If neither are available {} is returned. <br>
+ * Example (python):<br>
+ *
+ * <pre>
+ * import android, time
+ * droid = android.Android()
+ * droid.startLocating()
+ * time.sleep(15)
+ * loc = droid.readLocation().result
+ * if loc = {}:
+ *   loc = getLastKnownLocation().result
+ * if loc != {}:
+ *   try:
+ *     n = loc['gps']
+ *   except KeyError:
+ *     n = loc['network']
+ *   la = n['latitude']
+ *   lo = n['longitude']
+ *   address = droid.geocode(la, lo).result
+ * droid.stopLocating()
+ * </pre>
+ *
+ * The address format is:<br>
+ * [{u'thoroughfare': u'Some Street', u'locality': u'Some Town', u'sub_admin_area': u'Some Borough',
+ * u'admin_area': u'Some City', u'feature_name': u'House Numbers', u'country_code': u'GB',
+ * u'country_name': u'United Kingdom', u'postal_code': u'ST1 1'}]
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Felix Arends (felix.arends@gmail.com)
+ */
+public class LocationFacade extends RpcReceiver {
+  private final EventFacade mEventFacade;
+  private final Service mService;
+  private final Map<String, Location> mLocationUpdates;
+  private final LocationManager mLocationManager;
+  private final Geocoder mGeocoder;
+
+  private final LocationListener mLocationListener = new LocationListener() {
+    @Override
+    public synchronized void onLocationChanged(Location location) {
+      mLocationUpdates.put(location.getProvider(), location);
+      Map<String, Location> copy = Maps.newHashMap();
+      for (Entry<String, Location> entry : mLocationUpdates.entrySet()) {
+        copy.put(entry.getKey(), entry.getValue());
+      }
+      mEventFacade.postEvent("location", copy);
+    }
+
+    @Override
+    public void onProviderDisabled(String provider) {
+    }
+
+    @Override
+    public void onProviderEnabled(String provider) {
+    }
+
+    @Override
+    public void onStatusChanged(String provider, int status, Bundle extras) {
+    }
+  };
+
+  public LocationFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mEventFacade = manager.getReceiver(EventFacade.class);
+    mGeocoder = new Geocoder(mService);
+    mLocationManager = (LocationManager) mService.getSystemService(Context.LOCATION_SERVICE);
+    mLocationUpdates = new HashMap<String, Location>();
+  }
+
+  @Override
+  public void shutdown() {
+    stopLocating();
+  }
+
+  @Rpc(description = "Returns availables providers on the phone")
+  public List<String> locationProviders() {
+    return mLocationManager.getAllProviders();
+  }
+
+  @Rpc(description = "Ask if provider is enabled")
+  public boolean locationProviderEnabled(
+      @RpcParameter(name = "provider", description = "Name of location provider") String provider) {
+    return mLocationManager.isProviderEnabled(provider);
+  }
+
+  @Rpc(description = "Starts collecting location data.")
+  @RpcStartEvent("location")
+  public void startLocating(
+      @RpcParameter(name = "minDistance", description = "minimum time between updates in milliseconds") @RpcDefault("60000") Integer minUpdateTime,
+      @RpcParameter(name = "minUpdateDistance", description = "minimum distance between updates in meters") @RpcDefault("30") Integer minUpdateDistance) {
+    for (String provider : mLocationManager.getAllProviders()) {
+      mLocationManager.requestLocationUpdates(provider, minUpdateTime, minUpdateDistance,
+          mLocationListener, mService.getMainLooper());
+    }
+  }
+
+  @Rpc(description = "Returns the current location as indicated by all available providers.", returns = "A map of location information by provider.")
+  public Map<String, Location> readLocation() {
+    return mLocationUpdates;
+  }
+
+  @Rpc(description = "Stops collecting location data.")
+  @RpcStopEvent("location")
+  public synchronized void stopLocating() {
+    mLocationManager.removeUpdates(mLocationListener);
+    mLocationUpdates.clear();
+  }
+
+  @Rpc(description = "Returns the last known location of the device.", returns = "A map of location information by provider.")
+  public Map<String, Location> getLastKnownLocation() {
+    Map<String, Location> location = new HashMap<String, Location>();
+    for (String provider : mLocationManager.getAllProviders()) {
+      location.put(provider, mLocationManager.getLastKnownLocation(provider));
+    }
+    return location;
+  }
+
+  @Rpc(description = "Returns a list of addresses for the given latitude and longitude.", returns = "A list of addresses.")
+  public List<Address> geocode(
+      @RpcParameter(name = "latitude") Double latitude,
+      @RpcParameter(name = "longitude") Double longitude,
+      @RpcParameter(name = "maxResults", description = "maximum number of results") @RpcDefault("1") Integer maxResults)
+      throws IOException {
+    return mGeocoder.getFromLocation(latitude, longitude, maxResults);
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/PreferencesFacade.java b/Common/src/com/googlecode/android_scripting/facade/PreferencesFacade.java
new file mode 100644
index 0000000..31225ff
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/PreferencesFacade.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.Service;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * This facade allows access to the Preferences interface.
+ *
+ * <br>
+ * <b>Notes:</b> <br>
+ * <b>filename</b> - Filename indicates which preference file to refer to. If no filename is
+ * supplied (the default) then the SharedPreferences uses is the default for the SL4A application.<br>
+ * <b>prefPutValue</b> - uses "MODE_PRIVATE" when writing to preferences. Save values to the default
+ * shared preferences is explicitly disallowed.<br>
+ * <br>
+ * See <a
+ * href=http://developer.android.com/reference/java/util/prefs/Preferences.html>Preferences</a> and
+ * <a href=http://developer.android.com/guide/topics/data/data-storage.html#pref>Shared
+ * Preferences</a> in the android documentation on how preferences work.
+ *
+ * @author Robbie Matthews (rjmatthews62@gmail.com)
+ */
+
+public class PreferencesFacade extends RpcReceiver {
+
+  private Service mService;
+
+  public PreferencesFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+  }
+
+  @Rpc(description = "Read a value from shared preferences")
+  public Object prefGetValue(
+      @RpcParameter(name = "key") String key,
+      @RpcParameter(name = "filename", description = "Desired preferences file. If not defined, uses the default Shared Preferences.") @RpcOptional String filename) {
+    SharedPreferences p = getPref(filename);
+    return p.getAll().get(key);
+  }
+
+  @Rpc(description = "Write a value to shared preferences")
+  public void prefPutValue(
+      @RpcParameter(name = "key") String key,
+      @RpcParameter(name = "value") Object value,
+      @RpcParameter(name = "filename", description = "Desired preferences file. If not defined, uses the default Shared Preferences.") @RpcOptional String filename)
+      throws IOException {
+    if (filename == null || filename.equals("")) {
+      throw new IOException("Can't write to default preferences.");
+    }
+    SharedPreferences p = getPref(filename);
+    Editor e = p.edit();
+    if (value instanceof Boolean) {
+      e.putBoolean(key, (Boolean) value);
+    } else if (value instanceof Long) {
+      e.putLong(key, (Long) value);
+    } else if (value instanceof Integer) {
+      e.putLong(key, (Integer) value);
+    } else if (value instanceof Float) {
+      e.putFloat(key, (Float) value);
+    } else if (value instanceof Double) { // TODO: Not sure if this is a good idea
+      e.putFloat(key, ((Double) value).floatValue());
+    } else {
+      e.putString(key, value.toString());
+    }
+    e.commit();
+  }
+
+  @Rpc(description = "Get list of Shared Preference Values", returns = "Map of key,value")
+  public Map<String, ?> prefGetAll(
+      @RpcParameter(name = "filename", description = "Desired preferences file. If not defined, uses the default Shared Preferences.") @RpcOptional String filename) {
+    return getPref(filename).getAll();
+  }
+
+  private SharedPreferences getPref(String filename) {
+    if (filename == null || filename.equals("")) {
+      return PreferenceManager.getDefaultSharedPreferences(mService);
+    }
+    return mService.getSharedPreferences(filename, 0);
+
+  }
+
+  @Override
+  public void shutdown() {
+
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/SensorManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/SensorManagerFacade.java
new file mode 100644
index 0000000..6c4c7b7
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/SensorManagerFacade.java
@@ -0,0 +1,496 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Bundle;
+
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcDeprecated;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+import com.googlecode.android_scripting.rpc.RpcStartEvent;
+import com.googlecode.android_scripting.rpc.RpcStopEvent;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Exposes the SensorManager related functionality. <br>
+ * <br>
+ * <b>Guidance notes</b> <br>
+ * For reasons of economy the sensors on smart phones are usually low cost and, therefore, low
+ * accuracy (usually represented by 10 bit data). The floating point data values obtained from
+ * sensor readings have up to 16 decimal places, the majority of which are noise. On many phones the
+ * accelerometer is limited (by the phone manufacturer) to a maximum reading of 2g. The magnetometer
+ * (which also provides orientation readings) is strongly affected by the presence of ferrous metals
+ * and can give large errors in vehicles, on board ship etc.
+ *
+ * Following a startSensingTimed(A,B) api call sensor events are entered into the Event Queue (see
+ * EventFacade). For the A parameter: 1 = All Sensors, 2 = Accelerometer, 3 = Magnetometer and 4 =
+ * Light. The B parameter is the minimum delay between recordings in milliseconds. To avoid
+ * duplicate readings the minimum delay should be 20 milliseconds. The light sensor will probably be
+ * much slower (taking about 1 second to register a change in light level). Note that if the light
+ * level is constant no sensor events will be registered by the light sensor.
+ *
+ * Following a startSensingThreshold(A,B,C) api call sensor events greater than a given threshold
+ * are entered into the Event Queue. For the A parameter: 1 = Orientation, 2 = Accelerometer, 3 =
+ * Magnetometer and 4 = Light. The B parameter is the integer value of the required threshold level.
+ * For orientation sensing the integer threshold value is in milliradians. Since orientation events
+ * can exceed the threshold value for long periods only crossing and return events are recorded. The
+ * C parameter is the required axis (XYZ) of the sensor: 0 = No axis, 1 = X, 2 = Y, 3 = X+Y, 4 = Z,
+ * 5= X+Z, 6 = Y+Z, 7 = X+Y+Z. For orientation X = azimuth, Y = pitch and Z = roll. <br>
+ *
+ * <br>
+ * <b>Example (python)</b>
+ *
+ * <pre>
+ * import android, time
+ * droid = android.Android()
+ * droid.startSensingTimed(1, 250)
+ * time.sleep(1)
+ * s1 = droid.readSensors().result
+ * s2 = droid.sensorsGetAccuracy().result
+ * s3 = droid.sensorsGetLight().result
+ * s4 = droid.sensorsReadAccelerometer().result
+ * s5 = droid.sensorsReadMagnetometer().result
+ * s6 = droid.sensorsReadOrientation().result
+ * droid.stopSensing()
+ * </pre>
+ *
+ * Returns:<br>
+ * s1 = {u'accuracy': 3, u'pitch': -0.47323511242866517, u'xmag': 1.75, u'azimuth':
+ * -0.26701245009899138, u'zforce': 8.4718560000000007, u'yforce': 4.2495484000000001, u'time':
+ * 1297160391.2820001, u'ymag': -8.9375, u'zmag': -41.0625, u'roll': -0.031366908922791481,
+ * u'xforce': 0.23154590999999999}<br>
+ * s2 = 3 (Highest accuracy)<br>
+ * s3 = None ---(not available on many phones)<br>
+ * s4 = [0.23154590999999999, 4.2495484000000001, 8.4718560000000007] ----(x, y, z accelerations)<br>
+ * s5 = [1.75, -8.9375, -41.0625] -----(x, y, z magnetic readings)<br>
+ * s6 = [-0.26701245009899138, -0.47323511242866517, -0.031366908922791481] ---(azimuth, pitch, roll
+ * in radians)<br>
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Felix Arends (felix.arends@gmail.com)
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ * @author Robbie Mathews (rjmatthews62@gmail.com)
+ * @author John Karwatzki (jokar49@gmail.com)
+ */
+public class SensorManagerFacade extends RpcReceiver {
+  private final EventFacade mEventFacade;
+  private final SensorManager mSensorManager;
+
+  private volatile Bundle mSensorReadings;
+
+  private volatile Integer mAccuracy;
+  private volatile Integer mSensorNumber;
+  private volatile Integer mXAxis = 0;
+  private volatile Integer mYAxis = 0;
+  private volatile Integer mZAxis = 0;
+  private volatile Integer mThreshing = 0;
+  private volatile Integer mThreshOrientation = 0;
+  private volatile Integer mXCrossed = 0;
+  private volatile Integer mYCrossed = 0;
+  private volatile Integer mZCrossed = 0;
+
+  private volatile Float mThreshold;
+  private volatile Float mXForce;
+  private volatile Float mYForce;
+  private volatile Float mZForce;
+
+  private volatile Float mXMag;
+  private volatile Float mYMag;
+  private volatile Float mZMag;
+
+  private volatile Float mLight;
+
+  private volatile Double mAzimuth;
+  private volatile Double mPitch;
+  private volatile Double mRoll;
+
+  private volatile Long mLastTime;
+  private volatile Long mDelayTime;
+
+  private SensorEventListener mSensorListener;
+
+  public SensorManagerFacade(FacadeManager manager) {
+    super(manager);
+    mEventFacade = manager.getReceiver(EventFacade.class);
+    mSensorManager = (SensorManager) manager.getService().getSystemService(Context.SENSOR_SERVICE);
+  }
+
+  @Rpc(description = "Starts recording sensor data to be available for polling.")
+  @RpcStartEvent("sensors")
+  public void startSensingTimed(
+      @RpcParameter(name = "sensorNumber", description = "1 = All, 2 = Accelerometer, 3 = Magnetometer and 4 = Light") Integer sensorNumber,
+      @RpcParameter(name = "delayTime", description = "Minimum time between readings in milliseconds") Integer delayTime) {
+    mSensorNumber = sensorNumber;
+    if (delayTime < 20) {
+      delayTime = 20;
+    }
+    mDelayTime = (long) (delayTime);
+    mLastTime = System.currentTimeMillis();
+    if (mSensorListener == null) {
+      mSensorListener = new SensorValuesCollector();
+      mSensorReadings = new Bundle();
+      switch (mSensorNumber) {
+      case 1:
+        for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_ALL)) {
+          mSensorManager.registerListener(mSensorListener, sensor,
+              SensorManager.SENSOR_DELAY_FASTEST);
+        }
+        break;
+      case 2:
+        for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER)) {
+          mSensorManager.registerListener(mSensorListener, sensor,
+              SensorManager.SENSOR_DELAY_FASTEST);
+        }
+        break;
+      case 3:
+        for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD)) {
+          mSensorManager.registerListener(mSensorListener, sensor,
+              SensorManager.SENSOR_DELAY_FASTEST);
+        }
+        break;
+      case 4:
+        for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_LIGHT)) {
+          mSensorManager.registerListener(mSensorListener, sensor,
+              SensorManager.SENSOR_DELAY_FASTEST);
+        }
+      }
+    }
+  }
+
+  @Rpc(description = "Records to the Event Queue sensor data exceeding a chosen threshold.")
+  @RpcStartEvent("threshold")
+  public void startSensingThreshold(
+
+      @RpcParameter(name = "sensorNumber", description = "1 = Orientation, 2 = Accelerometer, 3 = Magnetometer and 4 = Light") Integer sensorNumber,
+      @RpcParameter(name = "threshold", description = "Threshold level for chosen sensor (integer)") Integer threshold,
+      @RpcParameter(name = "axis", description = "0 = No axis, 1 = X, 2 = Y, 3 = X+Y, 4 = Z, 5= X+Z, 6 = Y+Z, 7 = X+Y+Z") Integer axis) {
+    mSensorNumber = sensorNumber;
+    mXAxis = axis & 1;
+    mYAxis = axis & 2;
+    mZAxis = axis & 4;
+    if (mSensorNumber == 1) {
+      mThreshing = 0;
+      mThreshOrientation = 1;
+      mThreshold = ((float) threshold) / ((float) 1000);
+    } else {
+      mThreshing = 1;
+      mThreshold = (float) threshold;
+    }
+    startSensingTimed(mSensorNumber, 20);
+  }
+
+  @Rpc(description = "Returns the most recently recorded sensor data.")
+  public Bundle readSensors() {
+    if (mSensorReadings == null) {
+      return null;
+    }
+    synchronized (mSensorReadings) {
+      return new Bundle(mSensorReadings);
+    }
+  }
+
+  @Rpc(description = "Stops collecting sensor data.")
+  @RpcStopEvent("sensors")
+  public void stopSensing() {
+    mSensorManager.unregisterListener(mSensorListener);
+    mSensorListener = null;
+    mSensorReadings = null;
+    mThreshing = 0;
+    mThreshOrientation = 0;
+  }
+
+  @Rpc(description = "Returns the most recently received accuracy value.")
+  public Integer sensorsGetAccuracy() {
+    return mAccuracy;
+  }
+
+  @Rpc(description = "Returns the most recently received light value.")
+  public Float sensorsGetLight() {
+    return mLight;
+  }
+
+  @Rpc(description = "Returns the most recently received accelerometer values.", returns = "a List of Floats [(acceleration on the) X axis, Y axis, Z axis].")
+  public List<Float> sensorsReadAccelerometer() {
+    synchronized (mSensorReadings) {
+      return Arrays.asList(mXForce, mYForce, mZForce);
+    }
+  }
+
+  @Rpc(description = "Returns the most recently received magnetic field values.", returns = "a List of Floats [(magnetic field value for) X axis, Y axis, Z axis].")
+  public List<Float> sensorsReadMagnetometer() {
+    synchronized (mSensorReadings) {
+      return Arrays.asList(mXMag, mYMag, mZMag);
+    }
+  }
+
+  @Rpc(description = "Returns the most recently received orientation values.", returns = "a List of Doubles [azimuth, pitch, roll].")
+  public List<Double> sensorsReadOrientation() {
+    synchronized (mSensorReadings) {
+      return Arrays.asList(mAzimuth, mPitch, mRoll);
+    }
+  }
+
+  @Rpc(description = "Starts recording sensor data to be available for polling.")
+  @RpcDeprecated(value = "startSensingTimed or startSensingThreshhold", release = "4")
+  public void startSensing(
+      @RpcParameter(name = "sampleSize", description = "number of samples for calculating average readings") @RpcDefault("5") Integer sampleSize) {
+    if (mSensorListener == null) {
+      startSensingTimed(1, 220);
+    }
+  }
+
+  @Override
+  public void shutdown() {
+    stopSensing();
+  }
+
+  private class SensorValuesCollector implements SensorEventListener {
+    private final static int MATRIX_SIZE = 9;
+
+    private final RollingAverage mmAzimuth;
+    private final RollingAverage mmPitch;
+    private final RollingAverage mmRoll;
+
+    private float[] mmGeomagneticValues;
+    private float[] mmGravityValues;
+    private float[] mmR;
+    private float[] mmOrientation;
+
+    public SensorValuesCollector() {
+      mmAzimuth = new RollingAverage();
+      mmPitch = new RollingAverage();
+      mmRoll = new RollingAverage();
+    }
+
+    private void postEvent() {
+      mSensorReadings.putDouble("time", System.currentTimeMillis() / 1000.0);
+      mEventFacade.postEvent("sensors", mSensorReadings.clone());
+    }
+
+    @Override
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {
+      if (mSensorReadings == null) {
+        return;
+      }
+      synchronized (mSensorReadings) {
+        mSensorReadings.putInt("accuracy", accuracy);
+        mAccuracy = accuracy;
+
+      }
+    }
+
+    @Override
+    public void onSensorChanged(SensorEvent event) {
+      if (mSensorReadings == null) {
+        return;
+      }
+      synchronized (mSensorReadings) {
+        switch (event.sensor.getType()) {
+        case Sensor.TYPE_ACCELEROMETER:
+          mXForce = event.values[0];
+          mYForce = event.values[1];
+          mZForce = event.values[2];
+          if (mThreshing == 0) {
+            mSensorReadings.putFloat("xforce", mXForce);
+            mSensorReadings.putFloat("yforce", mYForce);
+            mSensorReadings.putFloat("zforce", mZForce);
+            if ((mSensorNumber == 2) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) {
+              mLastTime = System.currentTimeMillis();
+              postEvent();
+            }
+          }
+          if ((mThreshing == 1) && (mSensorNumber == 2)) {
+            if ((Math.abs(mXForce) > mThreshold) && (mXAxis == 1)) {
+              mSensorReadings.putFloat("xforce", mXForce);
+              postEvent();
+            }
+
+            if ((Math.abs(mYForce) > mThreshold) && (mYAxis == 2)) {
+              mSensorReadings.putFloat("yforce", mYForce);
+              postEvent();
+            }
+
+            if ((Math.abs(mZForce) > mThreshold) && (mZAxis == 4)) {
+              mSensorReadings.putFloat("zforce", mZForce);
+              postEvent();
+            }
+          }
+
+          mmGravityValues = event.values.clone();
+          break;
+        case Sensor.TYPE_MAGNETIC_FIELD:
+          mXMag = event.values[0];
+          mYMag = event.values[1];
+          mZMag = event.values[2];
+          if (mThreshing == 0) {
+            mSensorReadings.putFloat("xMag", mXMag);
+            mSensorReadings.putFloat("yMag", mYMag);
+            mSensorReadings.putFloat("zMag", mZMag);
+            if ((mSensorNumber == 3) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) {
+              mLastTime = System.currentTimeMillis();
+              postEvent();
+            }
+          }
+          if ((mThreshing == 1) && (mSensorNumber == 3)) {
+            if ((Math.abs(mXMag) > mThreshold) && (mXAxis == 1)) {
+              mSensorReadings.putFloat("xforce", mXMag);
+              postEvent();
+            }
+            if ((Math.abs(mYMag) > mThreshold) && (mYAxis == 2)) {
+              mSensorReadings.putFloat("yforce", mYMag);
+              postEvent();
+            }
+            if ((Math.abs(mZMag) > mThreshold) && (mZAxis == 4)) {
+              mSensorReadings.putFloat("zforce", mZMag);
+              postEvent();
+            }
+          }
+          mmGeomagneticValues = event.values.clone();
+          break;
+        case Sensor.TYPE_LIGHT:
+          mLight = event.values[0];
+          if (mThreshing == 0) {
+            mSensorReadings.putFloat("light", mLight);
+            if ((mSensorNumber == 4) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) {
+              mLastTime = System.currentTimeMillis();
+              postEvent();
+            }
+          }
+          if ((mThreshing == 1) && (mSensorNumber == 4)) {
+            if (mLight > mThreshold) {
+              mSensorReadings.putFloat("light", mLight);
+              postEvent();
+            }
+          }
+          break;
+
+        }
+        if (mSensorNumber == 1) {
+          if (mmGeomagneticValues != null && mmGravityValues != null) {
+            if (mmR == null) {
+              mmR = new float[MATRIX_SIZE];
+            }
+            if (SensorManager.getRotationMatrix(mmR, null, mmGravityValues, mmGeomagneticValues)) {
+              if (mmOrientation == null) {
+                mmOrientation = new float[3];
+              }
+              SensorManager.getOrientation(mmR, mmOrientation);
+              mmAzimuth.add(mmOrientation[0]);
+              mmPitch.add(mmOrientation[1]);
+              mmRoll.add(mmOrientation[2]);
+
+              mAzimuth = mmAzimuth.get();
+              mPitch = mmPitch.get();
+              mRoll = mmRoll.get();
+              if (mThreshOrientation == 0) {
+                mSensorReadings.putDouble("azimuth", mAzimuth);
+                mSensorReadings.putDouble("pitch", mPitch);
+                mSensorReadings.putDouble("roll", mRoll);
+                if ((mSensorNumber == 1) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) {
+                  mLastTime = System.currentTimeMillis();
+                  postEvent();
+                }
+              }
+              if ((mThreshOrientation == 1) && (mSensorNumber == 1)) {
+                if ((mXAxis == 1) && (mXCrossed == 0)) {
+                  if (Math.abs(mAzimuth) > ((double) mThreshold)) {
+                    mSensorReadings.putDouble("azimuth", mAzimuth);
+                    postEvent();
+                    mXCrossed = 1;
+                  }
+                }
+                if ((mXAxis == 1) && (mXCrossed == 1)) {
+                  if (Math.abs(mAzimuth) < ((double) mThreshold)) {
+                    mSensorReadings.putDouble("azimuth", mAzimuth);
+                    postEvent();
+                    mXCrossed = 0;
+                  }
+                }
+                if ((mYAxis == 2) && (mYCrossed == 0)) {
+                  if (Math.abs(mPitch) > ((double) mThreshold)) {
+                    mSensorReadings.putDouble("pitch", mPitch);
+                    postEvent();
+                    mYCrossed = 1;
+                  }
+                }
+                if ((mYAxis == 2) && (mYCrossed == 1)) {
+                  if (Math.abs(mPitch) < ((double) mThreshold)) {
+                    mSensorReadings.putDouble("pitch", mPitch);
+                    postEvent();
+                    mYCrossed = 0;
+                  }
+                }
+                if ((mZAxis == 4) && (mZCrossed == 0)) {
+                  if (Math.abs(mRoll) > ((double) mThreshold)) {
+                    mSensorReadings.putDouble("roll", mRoll);
+                    postEvent();
+                    mZCrossed = 1;
+                  }
+                }
+                if ((mZAxis == 4) && (mZCrossed == 1)) {
+                  if (Math.abs(mRoll) < ((double) mThreshold)) {
+                    mSensorReadings.putDouble("roll", mRoll);
+                    postEvent();
+                    mZCrossed = 0;
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  static class RollingAverage {
+    private final int mmSampleSize;
+    private final double mmData[];
+    private int mmIndex = 0;
+    private boolean mmFilled = false;
+    private double mmSum = 0.0;
+
+    public RollingAverage() {
+      mmSampleSize = 5;
+      mmData = new double[mmSampleSize];
+    }
+
+    public void add(double value) {
+      mmSum -= mmData[mmIndex];
+      mmData[mmIndex] = value;
+      mmSum += mmData[mmIndex];
+      ++mmIndex;
+      mmIndex %= mmSampleSize;
+      mmFilled = (!mmFilled) ? mmIndex == 0 : mmFilled;
+    }
+
+    public double get() throws IllegalStateException {
+      if (!mmFilled && mmIndex == 0) {
+        throw new IllegalStateException("No values to average.");
+      }
+      return (mmFilled) ? (mmSum / mmSampleSize) : (mmSum / mmIndex);
+    }
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/SettingsFacade.java b/Common/src/com/googlecode/android_scripting/facade/SettingsFacade.java
new file mode 100644
index 0000000..42e2fd9
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/SettingsFacade.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.app.AlarmManager;
+import android.app.Service;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings.SettingNotFoundException;
+import android.view.WindowManager;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.FutureActivityTaskExecutor;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.future.FutureActivityTask;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * Exposes phone settings functionality.
+ *
+ * @author Frank Spychalski (frank.spychalski@gmail.com)
+ */
+public class SettingsFacade extends RpcReceiver {
+
+    private final Service mService;
+    private final AndroidFacade mAndroidFacade;
+    private final AudioManager mAudio;
+    private final PowerManager mPower;
+    private final AlarmManager mAlarm;
+    private final LockPatternUtils mLockPatternUtils;
+
+    /**
+     * Creates a new SettingsFacade.
+     *
+     * @param service is the {@link Context} the APIs will run under
+     */
+    public SettingsFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mAndroidFacade = manager.getReceiver(AndroidFacade.class);
+        mAudio = (AudioManager) mService.getSystemService(Context.AUDIO_SERVICE);
+        mPower = (PowerManager) mService.getSystemService(Context.POWER_SERVICE);
+        mAlarm = (AlarmManager) mService.getSystemService(Context.ALARM_SERVICE);
+        mLockPatternUtils = new LockPatternUtils(mService);
+    }
+
+    @Rpc(description = "Sets the screen timeout to this number of seconds.",
+            returns = "The original screen timeout.")
+    public Integer setScreenTimeout(@RpcParameter(name = "value") Integer value) {
+        Integer oldValue = getScreenTimeout();
+        android.provider.Settings.System.putInt(mService.getContentResolver(),
+                android.provider.Settings.System.SCREEN_OFF_TIMEOUT, value * 1000);
+        return oldValue;
+    }
+
+    @Rpc(description = "Returns the current screen timeout in seconds.",
+            returns = "the current screen timeout in seconds.")
+    public Integer getScreenTimeout() {
+        try {
+            return android.provider.Settings.System.getInt(mService.getContentResolver(),
+                    android.provider.Settings.System.SCREEN_OFF_TIMEOUT) / 1000;
+        } catch (SettingNotFoundException e) {
+            return 0;
+        }
+    }
+
+    @Rpc(description = "Checks the ringer silent mode setting.",
+            returns = "True if ringer silent mode is enabled.")
+    public Boolean checkRingerSilentMode() {
+        return mAudio.getRingerMode() == AudioManager.RINGER_MODE_SILENT;
+    }
+
+    @Rpc(description = "Toggles ringer silent mode on and off.",
+            returns = "True if ringer silent mode is enabled.")
+    public Boolean toggleRingerSilentMode(
+            @RpcParameter(name = "enabled") @RpcOptional Boolean enabled) {
+        if (enabled == null) {
+            enabled = !checkRingerSilentMode();
+        }
+        mAudio.setRingerMode(enabled ? AudioManager.RINGER_MODE_SILENT
+                : AudioManager.RINGER_MODE_NORMAL);
+        return enabled;
+    }
+
+    @Rpc(description = "Set the ringer to a specified mode")
+    public void setRingerMode(@RpcParameter(name = "mode") Integer mode) throws Exception {
+        if (AudioManager.isValidRingerMode(mode)) {
+            mAudio.setRingerMode(mode);
+        } else {
+            throw new Exception("Ringer mode " + mode + " does not exist.");
+        }
+    }
+
+    @Rpc(description = "Returns the current ringtone mode.",
+            returns = "An integer representing the current ringer mode")
+    public Integer getRingerMode() {
+        return mAudio.getRingerMode();
+    }
+
+    @Rpc(description = "Returns the maximum ringer volume.")
+    public int getMaxRingerVolume() {
+        return mAudio.getStreamMaxVolume(AudioManager.STREAM_RING);
+    }
+
+    @Rpc(description = "Returns the current ringer volume.")
+    public int getRingerVolume() {
+        return mAudio.getStreamVolume(AudioManager.STREAM_RING);
+    }
+
+    @Rpc(description = "Sets the ringer volume.")
+    public void setRingerVolume(@RpcParameter(name = "volume") Integer volume) {
+        mAudio.setStreamVolume(AudioManager.STREAM_RING, volume, 0);
+    }
+
+    @Rpc(description = "Returns the maximum media volume.")
+    public int getMaxMediaVolume() {
+        return mAudio.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+    }
+
+    @Rpc(description = "Returns the current media volume.")
+    public int getMediaVolume() {
+        return mAudio.getStreamVolume(AudioManager.STREAM_MUSIC);
+    }
+
+    @Rpc(description = "Sets the media volume.")
+    public void setMediaVolume(@RpcParameter(name = "volume") Integer volume) {
+        mAudio.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
+    }
+
+    @Rpc(description = "Returns the screen backlight brightness.",
+            returns = "the current screen brightness between 0 and 255")
+    public Integer getScreenBrightness() {
+        try {
+            return android.provider.Settings.System.getInt(mService.getContentResolver(),
+                    android.provider.Settings.System.SCREEN_BRIGHTNESS);
+        } catch (SettingNotFoundException e) {
+            return 0;
+        }
+    }
+
+    @Rpc(description = "return the system time since boot in nanoseconds")
+    public long getSystemElapsedRealtimeNanos() {
+        return SystemClock.elapsedRealtimeNanos();
+    }
+
+    @Rpc(description = "Sets the the screen backlight brightness.",
+            returns = "the original screen brightness.")
+    public Integer setScreenBrightness(
+            @RpcParameter(name = "value", description = "brightness value between 0 and 255") Integer value) {
+        if (value < 0) {
+            value = 0;
+        } else if (value > 255) {
+            value = 255;
+        }
+        final int brightness = value;
+        Integer oldValue = getScreenBrightness();
+        android.provider.Settings.System.putInt(mService.getContentResolver(),
+                android.provider.Settings.System.SCREEN_BRIGHTNESS, brightness);
+
+        FutureActivityTask<Object> task = new FutureActivityTask<Object>() {
+            @Override
+            public void onCreate() {
+                super.onCreate();
+                WindowManager.LayoutParams lp = getActivity().getWindow().getAttributes();
+                lp.screenBrightness = brightness * 1.0f / 255;
+                getActivity().getWindow().setAttributes(lp);
+                setResult(null);
+                finish();
+            }
+        };
+
+        FutureActivityTaskExecutor taskExecutor =
+                ((BaseApplication) mService.getApplication()).getTaskExecutor();
+        taskExecutor.execute(task);
+
+        return oldValue;
+    }
+
+    @Rpc(description = "Returns true if the device is in an interactive state.")
+    public Boolean isDeviceInteractive() throws Exception {
+        return mPower.isInteractive();
+    }
+
+    @Rpc(description = "Issues a request to put the device to sleep after a delay.")
+    public void goToSleep(Integer delay) {
+        mPower.goToSleep(SystemClock.uptimeMillis() + delay);
+    }
+
+    @Rpc(description = "Issues a request to put the device to sleep right away.")
+    public void goToSleepNow() {
+        mPower.goToSleep(SystemClock.uptimeMillis());
+    }
+
+    @Rpc(description = "Issues a request to wake the device up right away.")
+    public void wakeUpNow() {
+        mPower.wakeUp(SystemClock.uptimeMillis());
+    }
+
+    @Rpc(description = "Get Up time of device.",
+            returns = "Long value of device up time in milliseconds.")
+    public long getDeviceUpTime() throws Exception {
+        return SystemClock.elapsedRealtime();
+    }
+
+    @Rpc(description = "Set a string password to the device.")
+    public void setDevicePassword(@RpcParameter(name = "password") String password) {
+        // mLockPatternUtils.setLockPatternEnabled(true, UserHandle.myUserId());
+        mLockPatternUtils.setLockScreenDisabled(false, UserHandle.myUserId());
+        mLockPatternUtils.setCredentialRequiredToDecrypt(true);
+        mLockPatternUtils.saveLockPassword(password, null,
+                DevicePolicyManager.PASSWORD_QUALITY_NUMERIC, UserHandle.myUserId());
+    }
+
+    @Rpc(description = "Disable screen lock password on the device.")
+    public void disableDevicePassword() {
+        mLockPatternUtils.clearEncryptionPassword();
+        // mLockPatternUtils.setLockPatternEnabled(false, UserHandle.myUserId());
+        mLockPatternUtils.setLockScreenDisabled(true, UserHandle.myUserId());
+        mLockPatternUtils.setCredentialRequiredToDecrypt(false);
+        mLockPatternUtils.clearEncryptionPassword();
+        mLockPatternUtils.clearLock(UserHandle.myUserId());
+        mLockPatternUtils.setLockScreenDisabled(true, UserHandle.myUserId());
+    }
+
+    @Rpc(description = "Set the system time in epoch.")
+    public void setTime(Long currentTime) {
+        mAlarm.setTime(currentTime);
+    }
+
+    @Rpc(description = "Set the system time zone.")
+    public void setTimeZone(@RpcParameter(name = "timeZone") String timeZone) {
+        mAlarm.setTimeZone(timeZone);
+    }
+
+    @Rpc(description = "Show Home Screen")
+    public void showHomeScreen() {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.addCategory(Intent.CATEGORY_HOME);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        try {
+            mAndroidFacade.startActivityIntent(intent, false);
+        } catch (RuntimeException e) {
+            Log.d("showHomeScreen RuntimeException" + e);
+        } catch (Exception e){
+            Log.d("showHomeScreen exception" + e);
+        }
+    }
+
+    @Override
+    public void shutdown() {
+        // Nothing to do yet.
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/SpeechRecognitionFacade.java b/Common/src/com/googlecode/android_scripting/facade/SpeechRecognitionFacade.java
new file mode 100644
index 0000000..5409002
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/SpeechRecognitionFacade.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.content.Intent;
+import android.speech.RecognizerIntent;
+
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.util.ArrayList;
+
+/**
+ * A facade containing RPC implementations related to the speech-to-text functionality of Android.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ *
+ */
+public class SpeechRecognitionFacade extends RpcReceiver {
+  private final AndroidFacade mAndroidFacade;
+
+  /**
+   * @param activityLauncher
+   *          a helper object that launches activities in a blocking manner
+   */
+  public SpeechRecognitionFacade(FacadeManager manager) {
+    super(manager);
+    mAndroidFacade = manager.getReceiver(AndroidFacade.class);
+  }
+
+  @Rpc(description = "Recognizes user's speech and returns the most likely result.", returns = "An empty string in case the speech cannot be recongnized.")
+  public String recognizeSpeech(
+      @RpcParameter(name = "prompt", description = "text prompt to show to the user when asking them to speak") @RpcOptional final String prompt,
+      @RpcParameter(name = "language", description = "language override to inform the recognizer that it should expect speech in a language different than the one set in the java.util.Locale.getDefault()") @RpcOptional final String language,
+      @RpcParameter(name = "languageModel", description = "informs the recognizer which speech model to prefer (see android.speech.RecognizeIntent)") @RpcOptional final String languageModel) {
+    final Intent recognitionIntent =
+        new Intent(android.speech.RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+
+    // Setup intent parameters (if provided).
+    if (language != null) {
+      recognitionIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "");
+    }
+    if (languageModel != null) {
+      recognitionIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, "");
+    }
+    if (prompt != null) {
+      recognitionIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, "");
+    }
+
+    // Run the activity an retrieve the result.
+    final Intent data = mAndroidFacade.startActivityForResult(recognitionIntent);
+
+    if (data.hasExtra(android.speech.RecognizerIntent.EXTRA_RESULTS)) {
+      // The result consists of an array-list containing one entry for each
+      // possible result. The most likely result is the first entry.
+      ArrayList<String> results =
+          data.getStringArrayListExtra(android.speech.RecognizerIntent.EXTRA_RESULTS);
+      return results.get(0);
+    }
+
+    return "";
+  }
+
+  @Override
+  public void shutdown() {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/TextToSpeechFacade.java b/Common/src/com/googlecode/android_scripting/facade/TextToSpeechFacade.java
new file mode 100644
index 0000000..26991fb
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/TextToSpeechFacade.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.os.SystemClock;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.TextToSpeech.OnInitListener;
+
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Provides Text To Speech services
+ */
+
+public class TextToSpeechFacade extends RpcReceiver {
+
+  private final TextToSpeech mTts;
+  private final CountDownLatch mOnInitLock;
+
+  public TextToSpeechFacade(FacadeManager manager) {
+    super(manager);
+    mOnInitLock = new CountDownLatch(1);
+    mTts = new TextToSpeech(manager.getService(), new OnInitListener() {
+      @Override
+      public void onInit(int arg0) {
+        mOnInitLock.countDown();
+      }
+    });
+  }
+
+  @Override
+  public void shutdown() {
+    while (mTts.isSpeaking()) {
+      SystemClock.sleep(100);
+    }
+    mTts.shutdown();
+  }
+
+  @Rpc(description = "Speaks the provided message via TTS.")
+  public void ttsSpeak(@RpcParameter(name = "message") String message) throws InterruptedException {
+    mOnInitLock.await();
+    if (message != null) {
+      mTts.speak(message, TextToSpeech.QUEUE_ADD, null);
+    }
+  }
+
+  @Rpc(description = "Returns True if speech is currently in progress.")
+  public Boolean ttsIsSpeaking() throws InterruptedException {
+    mOnInitLock.await();
+    return mTts.isSpeaking();
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ToneGeneratorFacade.java b/Common/src/com/googlecode/android_scripting/facade/ToneGeneratorFacade.java
new file mode 100644
index 0000000..c0bfe5f
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ToneGeneratorFacade.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * Generate DTMF tones.
+ *
+ */
+public class ToneGeneratorFacade extends RpcReceiver {
+
+  private final ToneGenerator mToneGenerator;
+
+  public ToneGeneratorFacade(FacadeManager manager) {
+    super(manager);
+    mToneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 100);
+  }
+
+  @Rpc(description = "Generate DTMF tones for the given phone number.")
+  public void generateDtmfTones(
+      @RpcParameter(name = "phoneNumber") String phoneNumber,
+      @RpcParameter(name = "toneDuration", description = "duration of each tone in milliseconds") @RpcDefault("100") Integer toneDuration)
+      throws InterruptedException {
+    try {
+      for (int i = 0; i < phoneNumber.length(); i++) {
+        switch (phoneNumber.charAt(i)) {
+        case '0':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_0);
+          Thread.sleep(toneDuration);
+          break;
+        case '1':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_1);
+          Thread.sleep(toneDuration);
+          break;
+        case '2':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_2);
+          Thread.sleep(toneDuration);
+          break;
+        case '3':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_3);
+          Thread.sleep(toneDuration);
+          break;
+        case '4':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_4);
+          Thread.sleep(toneDuration);
+          break;
+        case '5':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_5);
+          Thread.sleep(toneDuration);
+          break;
+        case '6':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_6);
+          Thread.sleep(toneDuration);
+          break;
+        case '7':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_7);
+          Thread.sleep(toneDuration);
+          break;
+        case '8':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_8);
+          Thread.sleep(toneDuration);
+          break;
+        case '9':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_9);
+          Thread.sleep(toneDuration);
+          break;
+        case '*':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_S);
+          Thread.sleep(toneDuration);
+          break;
+        case '#':
+          mToneGenerator.startTone(ToneGenerator.TONE_DTMF_P);
+          Thread.sleep(toneDuration);
+          break;
+        default:
+          throw new RuntimeException("Cannot generate tone for '" + phoneNumber.charAt(i) + "'");
+        }
+      }
+    } finally {
+      mToneGenerator.stopTone();
+    }
+  }
+
+  @Override
+  public void shutdown() {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/WakeLockFacade.java b/Common/src/com/googlecode/android_scripting/facade/WakeLockFacade.java
new file mode 100644
index 0000000..966e286
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/WakeLockFacade.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import android.content.Context;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * A facade exposing some of the functionality of the PowerManager, in particular wake locks.
+ *
+ * @author Felix Arends (felixarends@gmail.com)
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class WakeLockFacade extends RpcReceiver {
+
+    private final static String WAKE_LOCK_TAG =
+            "com.googlecode.android_scripting.facade.PowerManagerFacade";
+    private final PowerManager mmPowerManager;
+
+    private enum WakeLockType {
+        FULL, PARTIAL, BRIGHT, DIM
+    }
+
+    private class WakeLockManager {
+        private final Map<WakeLockType, WakeLock> mmLocks = new HashMap<WakeLockType, WakeLock>();
+
+        public WakeLockManager(PowerManager mmPowerManager) {
+            addWakeLock(WakeLockType.PARTIAL, PowerManager.PARTIAL_WAKE_LOCK);
+            addWakeLock(WakeLockType.FULL, PowerManager.FULL_WAKE_LOCK
+                    | PowerManager.ON_AFTER_RELEASE);
+            addWakeLock(WakeLockType.BRIGHT, PowerManager.SCREEN_BRIGHT_WAKE_LOCK
+                    | PowerManager.ON_AFTER_RELEASE);
+            addWakeLock(WakeLockType.DIM, PowerManager.SCREEN_DIM_WAKE_LOCK
+                    | PowerManager.ON_AFTER_RELEASE);
+        }
+
+        private void addWakeLock(WakeLockType type, int flags) {
+            WakeLock full = mmPowerManager.newWakeLock(flags, WAKE_LOCK_TAG);
+            full.setReferenceCounted(false);
+            mmLocks.put(type, full);
+        }
+
+        public void acquire(WakeLockType type) {
+            mmLocks.get(type).acquire();
+            for (Entry<WakeLockType, WakeLock> entry : mmLocks.entrySet()) {
+                if (entry.getKey() != type) {
+                    entry.getValue().release();
+                }
+            }
+        }
+
+        public void release() {
+            for (Entry<WakeLockType, WakeLock> entry : mmLocks.entrySet()) {
+                entry.getValue().release();
+            }
+        }
+    }
+
+    private final WakeLockManager mManager;
+
+    public WakeLockFacade(FacadeManager manager) {
+        super(manager);
+        mmPowerManager = (PowerManager) manager.getService()
+                .getSystemService(Context.POWER_SERVICE);
+        mManager = new WakeLockManager(mmPowerManager);
+    }
+
+    @Rpc(description = "Acquires a full wake lock (CPU on, screen bright, keyboard bright).")
+    public void wakeLockAcquireFull() {
+        mManager.acquire(WakeLockType.FULL);
+    }
+
+    @Rpc(description = "Acquires a partial wake lock (CPU on).")
+    public void wakeLockAcquirePartial() {
+        mManager.acquire(WakeLockType.PARTIAL);
+    }
+
+    @Rpc(description = "Acquires a bright wake lock (CPU on, screen bright).")
+    public void wakeLockAcquireBright() {
+        mManager.acquire(WakeLockType.BRIGHT);
+    }
+
+    @Rpc(description = "Acquires a dim wake lock (CPU on, screen dim).")
+    public void wakeLockAcquireDim() {
+        mManager.acquire(WakeLockType.DIM);
+    }
+
+    @Rpc(description = "Releases the wake lock.")
+    public void wakeLockRelease() {
+        mManager.release();
+    }
+
+    @Override
+    public void shutdown() {
+        wakeLockRelease();
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothA2dpFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothA2dpFacade.java
new file mode 100644
index 0000000..4a98d71
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothA2dpFacade.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import java.util.List;
+
+import android.app.Service;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.os.ParcelUuid;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+public class BluetoothA2dpFacade extends RpcReceiver {
+  static final ParcelUuid[] SINK_UUIDS = {
+    BluetoothUuid.AudioSink, BluetoothUuid.AdvAudioDist,
+  };
+  private final Service mService;
+  private final BluetoothAdapter mBluetoothAdapter;
+
+  private static boolean sIsA2dpReady = false;
+  private static BluetoothA2dp sA2dpProfile = null;
+
+  public BluetoothA2dpFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+    mBluetoothAdapter.getProfileProxy(mService, new A2dpServiceListener(),
+        BluetoothProfile.A2DP);
+  }
+
+  class A2dpServiceListener implements BluetoothProfile.ServiceListener {
+    @Override
+    public void onServiceConnected(int profile, BluetoothProfile proxy) {
+      sA2dpProfile = (BluetoothA2dp) proxy;
+      sIsA2dpReady = true;
+    }
+
+    @Override
+    public void onServiceDisconnected(int profile) {
+      sIsA2dpReady = false;
+    }
+  }
+
+  public Boolean a2dpConnect(BluetoothDevice device) {
+    List<BluetoothDevice> sinks = sA2dpProfile.getConnectedDevices();
+    if (sinks != null) {
+      for (BluetoothDevice sink : sinks) {
+        sA2dpProfile.disconnect(sink);
+      }
+    }
+    return sA2dpProfile.connect(device);
+  }
+
+  public Boolean a2dpDisconnect(BluetoothDevice device) {
+    if (sA2dpProfile.getPriority(device) > BluetoothProfile.PRIORITY_ON) {
+      sA2dpProfile.setPriority(device, BluetoothProfile.PRIORITY_ON);
+    }
+    return sA2dpProfile.disconnect(device);
+  }
+
+  /**
+   * Checks to see if the A2DP profile is ready for use.
+   *
+   * @return Returns true if the A2DP Profile is ready.
+   */
+  @Rpc(description = "Is A2dp profile ready.")
+  public Boolean bluetoothA2dpIsReady() {
+    return sIsA2dpReady;
+  }
+
+  /**
+   * Connect to remote device using the A2DP profile.
+   *
+   * @param deviceId the name or mac address of the remote Bluetooth device.
+   * @return True if connected successfully.
+   * @throws Exception
+   */
+  @Rpc(description = "Connect to an A2DP device.")
+  public Boolean bluetoothA2dpConnect(
+      @RpcParameter(name = "deviceID", description = "Name or MAC address of a bluetooth device.")
+      String deviceID)
+      throws Exception {
+    if (sA2dpProfile == null)
+      return false;
+    BluetoothDevice mDevice = BluetoothFacade.getDevice(
+        BluetoothFacade.DiscoveredDevices, deviceID);
+    Log.d("Connecting to device " + mDevice.getAliasName());
+    return a2dpConnect(mDevice);
+  }
+
+  /**
+   * Disconnect a remote device using the A2DP profile.
+   *
+   * @param deviceId the name or mac address of the remote Bluetooth device.
+   * @return True if connected successfully.
+   * @throws Exception
+   */
+  @Rpc(description = "Disconnect an A2DP device.")
+  public Boolean bluetoothA2dpDisconnect(
+      @RpcParameter(name = "deviceID", description = "Name or MAC address of a device.")
+      String deviceID)
+      throws Exception {
+    if (sA2dpProfile == null)
+      return false;
+    List<BluetoothDevice> connectedA2dpDevices = sA2dpProfile.getConnectedDevices();
+    Log.d("Connected a2dp devices " + connectedA2dpDevices);
+    BluetoothDevice mDevice = BluetoothFacade.getDevice(connectedA2dpDevices, deviceID);
+    return a2dpDisconnect(mDevice);
+  }
+
+  /**
+   * Get the list of devices connected through the A2DP profile.
+   *
+   * @return List of bluetooth devices that are in one of the following states:
+   *   connected, connecting, and disconnecting.
+   */
+  @Rpc(description = "Get all the devices connected through A2DP.")
+  public List<BluetoothDevice> bluetoothA2dpGetConnectedDevices() {
+    while (!sIsA2dpReady);
+    return sA2dpProfile.getDevicesMatchingConnectionStates(
+          new int[] {BluetoothProfile.STATE_CONNECTED,
+                     BluetoothProfile.STATE_CONNECTING,
+                     BluetoothProfile.STATE_DISCONNECTING});
+  }
+
+  @Override
+  public void shutdown() {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothAvrcpFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothAvrcpFacade.java
new file mode 100644
index 0000000..97dcdff
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothAvrcpFacade.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAvrcpController;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.os.ParcelUuid;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+public class BluetoothAvrcpFacade extends RpcReceiver {
+  static final ParcelUuid[] AVRCP_UUIDS = {
+    BluetoothUuid.AvrcpTarget, BluetoothUuid.AvrcpController
+  };
+  private final Service mService;
+  private final BluetoothAdapter mBluetoothAdapter;
+
+  private static boolean sIsAvrcpReady = false;
+  private static BluetoothAvrcpController sAvrcpProfile = null;
+
+  public BluetoothAvrcpFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+    mBluetoothAdapter.getProfileProxy(mService, new AvrcpServiceListener(),
+        BluetoothProfile.AVRCP_CONTROLLER);
+  }
+
+  class AvrcpServiceListener implements BluetoothProfile.ServiceListener {
+    @Override
+    public void onServiceConnected(int profile, BluetoothProfile proxy) {
+      sAvrcpProfile = (BluetoothAvrcpController) proxy;
+      sIsAvrcpReady = true;
+    }
+
+    @Override
+    public void onServiceDisconnected(int profile) {
+      sIsAvrcpReady = false;
+    }
+  }
+
+  @Rpc(description = "Is Avrcp profile ready.")
+  public Boolean bluetoothAvrcpIsReady() {
+    return sIsAvrcpReady;
+  }
+
+  @Rpc(description = "Get all the devices connected through AVRCP.")
+  public List<BluetoothDevice> bluetoothAvrcpGetConnectedDevices() {
+    if (!sIsAvrcpReady) {
+        Log.d("AVRCP profile is not ready.");
+        return null;
+    }
+    return sAvrcpProfile.getConnectedDevices();
+  }
+
+  @Rpc(description = "Close AVRCP connection.")
+  public void bluetoothAvrcpDisconnect() throws NoSuchMethodException,
+                                                IllegalAccessException,
+                                                IllegalArgumentException,
+                                                InvocationTargetException {
+      if (!sIsAvrcpReady) {
+          Log.d("AVRCP profile is not ready.");
+          return;
+      }
+      Method m = sAvrcpProfile.getClass().getMethod("close");
+      m.invoke(sAvrcpProfile);
+  }
+
+  @Rpc(description = "Send AVRPC passthrough command.")
+  public void bluetoothAvrcpSendPassThroughCmd(
+          @RpcParameter(name = "deviceID",
+                        description = "Name or MAC address of a bluetooth device.")
+          String deviceID,
+          @RpcParameter(name = "keyCode")
+          Integer keyCode,
+          @RpcParameter(name = "keyState")
+          Integer keyState) throws Exception {
+      if (!sIsAvrcpReady) {
+          Log.d("AVRCP profile is not ready.");
+          return;
+      }
+      BluetoothDevice mDevice = BluetoothFacade.getDevice(sAvrcpProfile.getConnectedDevices(),
+                                                          deviceID);
+      sAvrcpProfile.sendPassThroughCmd(mDevice, keyCode, keyState);
+  }
+
+  @Override
+  public void shutdown() {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothBroadcastHelper.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothBroadcastHelper.java
new file mode 100644
index 0000000..dc230cb
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothBroadcastHelper.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+public class BluetoothBroadcastHelper {
+
+  private static BroadcastReceiver mListener;
+  private final Context mContext;
+  private final BroadcastReceiver mReceiver;
+  private final String[] mActions = {BluetoothDevice.ACTION_FOUND,
+                                     BluetoothDevice.ACTION_UUID,
+                                     BluetoothAdapter.ACTION_DISCOVERY_STARTED,
+                                     BluetoothAdapter.ACTION_DISCOVERY_FINISHED};
+
+  public BluetoothBroadcastHelper(Context context, BroadcastReceiver listener) {
+    mContext = context;
+    mListener = listener;
+    mReceiver = new BluetoothReceiver();
+  }
+
+  public void startReceiver() {
+    IntentFilter mIntentFilter = new IntentFilter();
+    for(String action : mActions) {
+      mIntentFilter.addAction(action);
+    }
+    mContext.registerReceiver(mReceiver, mIntentFilter);
+  }
+
+  public static class BluetoothReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+      mListener.onReceive(context, intent);
+    }
+  }
+
+  public void stopReceiver() {
+    mContext.unregisterReceiver(mReceiver);
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java
new file mode 100644
index 0000000..7fb768f
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothConnectionFacade.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import android.app.Service;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothInputDevice;
+import android.bluetooth.BluetoothUuid;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+public class BluetoothConnectionFacade extends RpcReceiver {
+
+    private final Service mService;
+    private final BluetoothAdapter mBluetoothAdapter;
+    private final BluetoothPairingHelper mPairingHelper;
+    private final Map<String, BroadcastReceiver> listeningDevices;
+    private final EventFacade mEventFacade;
+
+    private final IntentFilter mDiscoverConnectFilter;
+    private final IntentFilter mPairingFilter;
+    private final IntentFilter mBondFilter;
+    private final IntentFilter mA2dpStateChangeFilter;
+    private final IntentFilter mHidStateChangeFilter;
+    private final IntentFilter mHspStateChangeFilter;
+
+    private final Bundle mGoodNews;
+    private final Bundle mBadNews;
+
+    private BluetoothA2dpFacade mA2dpProfile;
+    private BluetoothHidFacade mHidProfile;
+    private BluetoothHspFacade mHspProfile;
+
+    public BluetoothConnectionFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        mPairingHelper = new BluetoothPairingHelper();
+        // Use a synchronized map to avoid racing problems
+        listeningDevices = Collections.synchronizedMap(new HashMap<String, BroadcastReceiver>());
+
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mA2dpProfile = manager.getReceiver(BluetoothA2dpFacade.class);
+        mHidProfile = manager.getReceiver(BluetoothHidFacade.class);
+        mHspProfile = manager.getReceiver(BluetoothHspFacade.class);
+
+        mDiscoverConnectFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
+        mDiscoverConnectFilter.addAction(BluetoothDevice.ACTION_UUID);
+        mDiscoverConnectFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+
+        mPairingFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
+        mPairingFilter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST);
+        mPairingFilter.setPriority(999);
+
+        mBondFilter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+        mBondFilter.addAction(BluetoothDevice.ACTION_FOUND);
+        mBondFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+
+        mA2dpStateChangeFilter = new IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+        mHidStateChangeFilter = new IntentFilter(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED);
+        mHspStateChangeFilter = new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+
+        mGoodNews = new Bundle();
+        mGoodNews.putBoolean("Status", true);
+        mBadNews = new Bundle();
+        mBadNews.putBoolean("Status", false);
+    }
+
+    private void unregisterCachedListener(String listenerId) {
+        BroadcastReceiver listener = listeningDevices.remove(listenerId);
+        if (listener != null) {
+            mService.unregisterReceiver(listener);
+        }
+    }
+
+    /**
+     * Connect to a specific device upon its discovery
+     */
+    public class DiscoverConnectReceiver extends BroadcastReceiver {
+        private final String mDeviceID;
+        private BluetoothDevice mDevice;
+
+        /**
+         * Constructor
+         *
+         * @param deviceID Either the device alias name or mac address.
+         * @param bond If true, bond the device only.
+         */
+        public DiscoverConnectReceiver(String deviceID) {
+            super();
+            mDeviceID = deviceID;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            // The specified device is found.
+            if (action.equals(BluetoothDevice.ACTION_FOUND)) {
+                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
+                    Log.d("Found device " + device.getAliasName() + " for connection.");
+                    mBluetoothAdapter.cancelDiscovery();
+                    mDevice = device;
+                }
+            // After discovery stops.
+            } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
+                if (mDevice == null) {
+                    Log.d("Device " + mDeviceID + " not discovered.");
+                    mEventFacade.postEvent("Bond" + mDeviceID, mBadNews);
+                    return;
+                }
+                boolean status = mDevice.fetchUuidsWithSdp();
+                Log.d("Initiated ACL connection: " + status);
+            } else if (action.equals(BluetoothDevice.ACTION_UUID)) {
+                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
+                    Log.d("Initiating connections.");
+                    connectProfile(device, mDeviceID);
+                    mService.unregisterReceiver(listeningDevices.remove("Connect" + mDeviceID));
+                }
+            }
+        }
+    }
+
+    /**
+     * Connect to a specific device upon its discovery
+     */
+    public class DiscoverBondReceiver extends BroadcastReceiver {
+        private final String mDeviceID;
+        private BluetoothDevice mDevice = null;
+        private boolean started = false;
+
+        /**
+         * Constructor
+         *
+         * @param deviceID Either the device alias name or Mac address.
+         */
+        public DiscoverBondReceiver(String deviceID) {
+            super();
+            mDeviceID = deviceID;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            // The specified device is found.
+            if (action.equals(BluetoothDevice.ACTION_FOUND)) {
+                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
+                    Log.d("Found device " + device.getAliasName() + " for connection.");
+                    mBluetoothAdapter.cancelDiscovery();
+                    mDevice = device;
+                }
+            // After discovery stops.
+            } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
+                if (mDevice == null) {
+                    Log.d("Device " + mDeviceID + " was not discovered.");
+                    mEventFacade.postEvent("Bond", mBadNews);
+                    return;
+                }
+                // Attempt to initiate bonding.
+                if (!started) {
+                    Log.d("Bond with " + mDevice.getAliasName());
+                    if (mDevice.createBond()) {
+                        started = true;
+                        Log.d("Bonding started.");
+                    } else {
+                        Log.e("Failed to bond with " + mDevice.getAliasName());
+                        mEventFacade.postEvent("Bond", mBadNews);
+                        mService.unregisterReceiver(listeningDevices.remove("Bond" + mDeviceID));
+                    }
+                }
+            } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
+                Log.d("Bond state changing.");
+                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
+                    int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
+                    Log.d("New state is " + state);
+                    if (state == BluetoothDevice.BOND_BONDED) {
+                        Log.d("Bonding with " + mDeviceID + " successful.");
+                        mEventFacade.postEvent("Bond" + mDeviceID, mGoodNews);
+                        mService.unregisterReceiver(listeningDevices.remove("Bond" + mDeviceID));
+                    }
+                }
+            }
+        }
+    }
+
+    public class ConnectStateChangeReceiver extends BroadcastReceiver {
+        private final String mDeviceID;
+
+        public ConnectStateChangeReceiver(String deviceID) {
+            mDeviceID = deviceID;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+            // Check if received the specified device
+            if (!BluetoothFacade.deviceMatch(device, mDeviceID)) {
+                return;
+            }
+            if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
+                int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1);
+                if (state == BluetoothA2dp.STATE_CONNECTED) {
+                	Bundle a2dpGoodNews = (Bundle) mGoodNews.clone();
+                	a2dpGoodNews.putString("Type", "a2dp");
+                    mEventFacade.postEvent("A2dpConnect" + mDeviceID, a2dpGoodNews);
+                    unregisterCachedListener("A2dpConnecting" + mDeviceID);
+                } else if (state == BluetoothA2dp.STATE_CONNECTING) {
+                }
+            }else if (action.equals(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED)) {
+                int state = intent.getIntExtra(BluetoothInputDevice.EXTRA_STATE, -1);
+                if (state == BluetoothInputDevice.STATE_CONNECTED) {
+                    mEventFacade.postEvent("HidConnect" + mDeviceID, mGoodNews);
+                    unregisterCachedListener("HidConnecting" + mDeviceID);
+                }
+            } else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
+                int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -1);
+                if (state == BluetoothHeadset.STATE_CONNECTED) {
+                    mEventFacade.postEvent("HspConnect" + mDeviceID, mGoodNews);
+                    unregisterCachedListener("HspConnecting" + mDeviceID);
+                }
+            }
+        }
+    }
+
+    private void connectProfile(BluetoothDevice device, String deviceID) {
+        mService.registerReceiver(mPairingHelper, mPairingFilter);
+        ParcelUuid[] deviceUuids = device.getUuids();
+        Log.d("Device uuid is " + deviceUuids);
+        if (deviceUuids == null) {
+        	mEventFacade.postEvent("Connect", mBadNews);
+        }
+        if (BluetoothUuid.containsAnyUuid(BluetoothA2dpFacade.SINK_UUIDS, deviceUuids)) {
+            Log.d("Connecting to " + device.getAliasName());
+            boolean status = mA2dpProfile.a2dpConnect(device);
+            if (status) {
+                Log.d("Connecting A2dp...");
+                ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
+                mService.registerReceiver(receiver, mA2dpStateChangeFilter);
+                listeningDevices.put("A2dpConnecting" + deviceID, receiver);
+            } else {
+                Log.d("Failed starting A2dp connection.");
+                Bundle a2dpBadNews = (Bundle) mBadNews.clone();
+                a2dpBadNews.putString("Type", "a2dp");
+                mEventFacade.postEvent("Connect", a2dpBadNews);
+            }
+        }
+        if (BluetoothUuid.containsAnyUuid(BluetoothHidFacade.UUIDS, deviceUuids)) {
+            boolean status = mHidProfile.hidConnect(device);
+            if (status) {
+                Log.d("Connecting Hid...");
+                ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
+                mService.registerReceiver(receiver, mHidStateChangeFilter);
+                listeningDevices.put("HidConnecting" + deviceID, receiver);
+            } else {
+                Log.d("Failed starting Hid connection.");
+                mEventFacade.postEvent("HidConnect" + deviceID, mBadNews);
+            }
+        }
+        if (BluetoothUuid.containsAnyUuid(BluetoothHspFacade.UUIDS, deviceUuids)) {
+            boolean status = mHspProfile.hspConnect(device);
+            if (status) {
+                Log.d("Connecting Hsp...");
+                ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
+                mService.registerReceiver(receiver, mHspStateChangeFilter);
+                listeningDevices.put("HspConnecting" + deviceID, receiver);
+            } else {
+                Log.d("Failed starting Hsp connection.");
+                mEventFacade.postEvent("HspConnect" + deviceID, mBadNews);
+            }
+        }
+        mService.unregisterReceiver(mPairingHelper);
+    }
+
+    @Rpc(description = "Start intercepting all bluetooth connection pop-ups.")
+    public void bluetoothStartPairingHelper() {
+        mService.registerReceiver(mPairingHelper, mPairingFilter);
+    }
+
+    @Rpc(description = "Return a list of devices connected through bluetooth")
+    public List<BluetoothDevice> bluetoothGetConnectedDevices() {
+        ArrayList<BluetoothDevice> results = new ArrayList<BluetoothDevice>();
+        for (BluetoothDevice bd : mBluetoothAdapter.getBondedDevices()) {
+            if (bd.isConnected()) {
+                results.add(bd);
+            }
+        }
+        return results;
+    }
+
+    @Rpc(description = "Return true if a bluetooth device is connected.")
+    public Boolean bluetoothIsDeviceConnected(String deviceID) {
+        for (BluetoothDevice bd : mBluetoothAdapter.getBondedDevices()) {
+            if (BluetoothFacade.deviceMatch(bd, deviceID)) {
+                return bd.isConnected();
+            }
+        }
+        return false;
+    }
+
+    @Rpc(description = "Connect to a specified device once it's discovered.",
+         returns = "Whether discovery started successfully.")
+    public Boolean bluetoothDiscoverAndConnect(
+            @RpcParameter(name = "deviceID",
+                          description = "Name or MAC address of a bluetooth device.")
+            String deviceID) {
+        mBluetoothAdapter.cancelDiscovery();
+        if (listeningDevices.containsKey(deviceID)) {
+            Log.d("This device is already in the process of discovery and connecting.");
+            return true;
+        }
+        DiscoverConnectReceiver receiver = new DiscoverConnectReceiver(deviceID);
+        listeningDevices.put("Connect" + deviceID, receiver);
+        mService.registerReceiver(receiver, mDiscoverConnectFilter);
+        return mBluetoothAdapter.startDiscovery();
+    }
+
+    @Rpc(description = "Bond to a specified device once it's discovered.",
+         returns = "Whether discovery started successfully. ")
+    public Boolean bluetoothDiscoverAndBond(
+            @RpcParameter(name = "deviceID",
+                          description = "Name or MAC address of a bluetooth device.")
+            String deviceID) {
+        mBluetoothAdapter.cancelDiscovery();
+        if (listeningDevices.containsKey(deviceID)) {
+            Log.d("This device is already in the process of discovery and bonding.");
+            return true;
+        }
+        if (BluetoothFacade.deviceExists(mBluetoothAdapter.getBondedDevices(), deviceID)) {
+            Log.d("Device " + deviceID + " is already bonded.");
+            mEventFacade.postEvent("Bond" + deviceID, mGoodNews);
+            return true;
+        }
+        DiscoverBondReceiver receiver = new DiscoverBondReceiver(deviceID);
+        if (listeningDevices.containsKey("Bond" + deviceID)) {
+            mService.unregisterReceiver(listeningDevices.remove("Bond" + deviceID));
+        }
+        listeningDevices.put("Bond" + deviceID, receiver);
+        mService.registerReceiver(receiver, mBondFilter);
+        Log.d("Start discovery for bonding.");
+        return mBluetoothAdapter.startDiscovery();
+    }
+
+    @Rpc(description = "Unbond a device.",
+         returns = "Whether the device was successfully unbonded.")
+    public Boolean bluetoothUnbond(
+            @RpcParameter(name = "deviceID",
+                          description = "Name or MAC address of a bluetooth device.")
+            String deviceID) throws Exception {
+        BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
+                deviceID);
+        return mDevice.removeBond();
+    }
+
+    @Rpc(description = "Connect to a device that is already bonded.")
+    public void bluetoothConnectBonded(
+            @RpcParameter(name = "deviceID",
+                          description = "Name or MAC address of a bluetooth device.")
+            String deviceID) throws Exception {
+        BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
+                deviceID);
+        connectProfile(mDevice, deviceID);
+    }
+
+    @Override
+    public void shutdown() {
+        for(BroadcastReceiver receiver : listeningDevices.values()) {
+            mService.unregisterReceiver(receiver);
+        }
+        listeningDevices.clear();
+        mService.unregisterReceiver(mPairingHelper);
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothDiscoveryHelper.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothDiscoveryHelper.java
new file mode 100644
index 0000000..f625f03
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothDiscoveryHelper.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import java.util.Set;
+
+import com.googlecode.android_scripting.Log;
+
+public class BluetoothDiscoveryHelper {
+
+  public static interface BluetoothDiscoveryListener {
+    public void addBondedDevice(String name, String address);
+
+    public void addDevice(String name, String address);
+
+    public void scanDone();
+  }
+
+  private final Context mContext;
+  private final BluetoothDiscoveryListener mListener;
+  private final BroadcastReceiver mReceiver;
+
+  public BluetoothDiscoveryHelper(Context context, BluetoothDiscoveryListener listener) {
+    mContext = context;
+    mListener = listener;
+    mReceiver = new BluetoothReceiver();
+  }
+
+  private class BluetoothReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+      final String action = intent.getAction();
+
+      if (BluetoothDevice.ACTION_FOUND.equals(action)) {
+        // Get the BluetoothDevice object from the Intent.
+        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+        Log.d("Found device " + device.getAliasName());
+        // If it's already paired, skip it, because it's been listed already.
+        if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
+          mListener.addDevice(device.getName(), device.getAddress());
+        }
+      } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
+        mListener.scanDone();
+      }
+    }
+  }
+
+  public void startDiscovery() {
+    BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+
+    if (bluetoothAdapter.isDiscovering()) {
+      bluetoothAdapter.cancelDiscovery();
+    }
+
+    Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
+    for (BluetoothDevice device : pairedDevices) {
+      mListener.addBondedDevice(device.getName(), device.getAddress());
+    }
+
+    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
+    filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+    mContext.registerReceiver(mReceiver, filter);
+
+    if (!bluetoothAdapter.isEnabled()) {
+      bluetoothAdapter.enable();
+    }
+
+    bluetoothAdapter.startDiscovery();
+  }
+
+  public void cancel() {
+    mContext.unregisterReceiver(mReceiver);
+    mListener.scanDone();
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothFacade.java
new file mode 100644
index 0000000..474e6d9
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothFacade.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import android.app.Service;
+import android.bluetooth.BluetoothActivityEnergyInfo;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.MainThread;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Basic Bluetooth functions.
+ */
+public class BluetoothFacade extends RpcReceiver {
+    private final Service mService;
+    private final BroadcastReceiver mDiscoveryReceiver;
+    private final IntentFilter discoveryFilter;
+    private final EventFacade mEventFacade;
+    private final BluetoothStateReceiver mStateReceiver;
+    private final BleStateReceiver mBleStateReceiver;
+    private Map<String, BluetoothConnection> connections =
+            new HashMap<String, BluetoothConnection>();
+    private BluetoothAdapter mBluetoothAdapter;
+
+    public static ConcurrentHashMap<String, BluetoothDevice> DiscoveredDevices;
+
+    public BluetoothFacade(FacadeManager manager) {
+        super(manager);
+        mBluetoothAdapter = MainThread.run(manager.getService(), new Callable<BluetoothAdapter>() {
+            @Override
+            public BluetoothAdapter call() throws Exception {
+                return BluetoothAdapter.getDefaultAdapter();
+            }
+        });
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mService = manager.getService();
+
+        DiscoveredDevices = new ConcurrentHashMap<String, BluetoothDevice>();
+        discoveryFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
+        discoveryFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+        mDiscoveryReceiver = new DiscoveryCacheReceiver();
+        mStateReceiver = new BluetoothStateReceiver();
+        mBleStateReceiver = new BleStateReceiver();
+    }
+
+    class DiscoveryCacheReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(BluetoothDevice.ACTION_FOUND)) {
+                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                Log.d("Found device " + device.getAliasName());
+                if (!DiscoveredDevices.containsKey(device.getAddress())) {
+                    String name = device.getAliasName();
+                    if (name != null) {
+                        DiscoveredDevices.put(device.getAliasName(), device);
+                    }
+                    DiscoveredDevices.put(device.getAddress(), device);
+                }
+            } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
+                mEventFacade.postEvent("BluetoothDiscoveryFinished", new Bundle());
+                mService.unregisterReceiver(mDiscoveryReceiver);
+            }
+        }
+    }
+
+    class BluetoothStateReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
+                final int state = mBluetoothAdapter.getState();
+                Bundle msg = new Bundle();
+                if (state == BluetoothAdapter.STATE_ON) {
+                    msg.putString("State", "ON");
+                    mEventFacade.postEvent("BluetoothStateChangedOn", msg);
+                    mService.unregisterReceiver(mStateReceiver);
+                } else if(state == BluetoothAdapter.STATE_OFF) {
+                    msg.putString("State", "OFF");
+                    mEventFacade.postEvent("BluetoothStateChangedOff", msg);
+                    mService.unregisterReceiver(mStateReceiver);
+                }
+                msg.clear();
+            }
+        }
+    }
+
+    class BleStateReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(BluetoothAdapter.ACTION_BLE_STATE_CHANGED)) {
+                int state = mBluetoothAdapter.getLeState();
+                if (state == BluetoothAdapter.STATE_BLE_ON) {
+                    mEventFacade.postEvent("BleStateChangedOn", new Bundle());
+                    mService.unregisterReceiver(mBleStateReceiver);
+                } else if (state == BluetoothAdapter.STATE_OFF) {
+                    mEventFacade.postEvent("BleStateChangedOff", new Bundle());
+                    mService.unregisterReceiver(mBleStateReceiver);
+                }
+            }
+        }
+    }
+
+
+    public static boolean deviceMatch(BluetoothDevice device, String deviceID) {
+        return deviceID.equals(device.getAliasName()) || deviceID.equals(device.getAddress());
+    }
+
+    public static <T> BluetoothDevice getDevice(ConcurrentHashMap<String, T> devices, String device)
+            throws Exception {
+        if (devices.containsKey(device)) {
+            return (BluetoothDevice) devices.get(device);
+        } else {
+            throw new Exception("Can't find device " + device);
+        }
+    }
+
+    public static BluetoothDevice getDevice(Collection<BluetoothDevice> devices, String deviceID)
+            throws Exception {
+        Log.d("Looking for " + deviceID);
+        for (BluetoothDevice bd : devices) {
+            Log.d(bd.getAliasName() + " " + bd.getAddress());
+            if (deviceMatch(bd, deviceID)) {
+                Log.d("Found match " + bd.getAliasName() + " " + bd.getAddress());
+                return bd;
+            }
+        }
+        throw new Exception("Can't find device " + deviceID);
+    }
+
+    public static boolean deviceExists(Collection<BluetoothDevice> devices, String deviceID) {
+        for (BluetoothDevice bd : devices) {
+            if (deviceMatch(bd, deviceID)) {
+                Log.d("Found match " + bd.getAliasName() + " " + bd.getAddress());
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Rpc(description = "Requests that the device be made connectable.")
+    public void bluetoothMakeConnectable() {
+        mBluetoothAdapter
+                .setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+    }
+
+    @Rpc(description = "Requests that the device be discoverable for Bluetooth connections.")
+    public void bluetoothMakeDiscoverable(
+            @RpcParameter(name = "duration",
+                          description = "period of time, in seconds,"
+                                      + "during which the device should be discoverable")
+            @RpcDefault("300")
+            Integer duration) {
+        Log.d("Making discoverable for " + duration + " seconds.\n");
+        mBluetoothAdapter
+                .setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, duration);
+    }
+
+    @Rpc(description = "Requests that the device be not discoverable.")
+    public void bluetoothMakeUndiscoverable() {
+        Log.d("Making undiscoverable\n");
+        mBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_NONE);
+    }
+
+    @Rpc(description = "Queries a remote device for it's name or null if it can't be resolved")
+    public String bluetoothGetRemoteDeviceName(
+            @RpcParameter(name = "address", description = "Bluetooth Address For Target Device")
+            String address) {
+        try {
+            BluetoothDevice mDevice;
+            mDevice = mBluetoothAdapter.getRemoteDevice(address);
+            return mDevice.getName();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    @Rpc(description = "Get local Bluetooth device name")
+    public String bluetoothGetLocalName() {
+        return mBluetoothAdapter.getName();
+    }
+
+    @Rpc(description = "Sets the Bluetooth visible device name", returns = "true on success")
+    public boolean bluetoothSetLocalName(
+        @RpcParameter(name = "name", description = "New local name")
+        String name) {
+        return mBluetoothAdapter.setName(name);
+    }
+
+    @Rpc(description = "Returns the hardware address of the local Bluetooth adapter. ")
+    public String bluetoothGetLocalAddress() {
+        return mBluetoothAdapter.getAddress();
+    }
+
+    @Rpc(description = "Returns the UUIDs supported by local Bluetooth adapter.")
+    public ParcelUuid[] bluetoothGetLocalUuids() {
+        return mBluetoothAdapter.getUuids();
+    }
+
+    @Rpc(description = "Gets the scan mode for the local dongle.\r\n" + "Return values:\r\n"
+            + "\t-1 when Bluetooth is disabled.\r\n"
+            + "\t0 if non discoverable and non connectable.\r\n"
+            + "\r1 connectable non discoverable." + "\r3 connectable and discoverable.")
+    public int bluetoothGetScanMode() {
+        if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF
+                || mBluetoothAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF) {
+            return -1;
+        }
+        switch (mBluetoothAdapter.getScanMode()) {
+            case BluetoothAdapter.SCAN_MODE_NONE:
+                return 0;
+            case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
+                return 1;
+            case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
+                return 3;
+            default:
+                return mBluetoothAdapter.getScanMode() - 20;
+        }
+    }
+
+    @Rpc(description = "Return the set of BluetoothDevice that are paired to the local adapter.")
+    public Set<BluetoothDevice> bluetoothGetBondedDevices() {
+        return mBluetoothAdapter.getBondedDevices();
+    }
+
+    @Rpc(description = "Checks Bluetooth state.", returns = "True if Bluetooth is enabled.")
+    public Boolean bluetoothCheckState() {
+        return mBluetoothAdapter.isEnabled();
+    }
+
+    @Rpc(description = "Toggle Bluetooth on and off.", returns = "True if Bluetooth is enabled.")
+    public Boolean bluetoothToggleState(@RpcParameter(name = "enabled")
+    @RpcOptional
+    Boolean enabled,
+            @RpcParameter(name = "prompt",
+                          description = "Prompt the user to confirm changing the Bluetooth state.")
+            @RpcDefault("false")
+            Boolean prompt) {
+        mService.registerReceiver(mStateReceiver,
+                                  new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
+        if (enabled == null) {
+            enabled = !bluetoothCheckState();
+        }
+        if (enabled) {
+            mBluetoothAdapter.enable();
+        } else {
+            shutdown();
+            mBluetoothAdapter.disable();
+        }
+        return enabled;
+    }
+
+
+    @Rpc(description = "Start the remote device discovery process. ",
+         returns = "true on success, false on error")
+    public Boolean bluetoothStartDiscovery() {
+        DiscoveredDevices.clear();
+        mService.registerReceiver(mDiscoveryReceiver, discoveryFilter);
+        return mBluetoothAdapter.startDiscovery();
+    }
+
+    @Rpc(description = "Cancel the current device discovery process.",
+         returns = "true on success, false on error")
+    public Boolean bluetoothCancelDiscovery() {
+        try {
+            mService.unregisterReceiver(mDiscoveryReceiver);
+        } catch (IllegalArgumentException e) {
+            Log.d("IllegalArgumentExeption found when trying to unregister reciever");
+        }
+        return mBluetoothAdapter.cancelDiscovery();
+    }
+
+    @Rpc(description = "If the local Bluetooth adapter is currently"
+                     + "in the device discovery process.")
+    public Boolean bluetoothIsDiscovering() {
+        return mBluetoothAdapter.isDiscovering();
+    }
+
+    @Rpc(description = "Get all the discovered bluetooth devices.")
+    public Collection<BluetoothDevice> bluetoothGetDiscoveredDevices() {
+        while (bluetoothIsDiscovering())
+            ;
+        return DiscoveredDevices.values();
+    }
+
+    @Rpc(description = "Enable or disable the Bluetooth HCI snoop log")
+    public boolean bluetoothConfigHciSnoopLog(
+            @RpcParameter(name = "value", description = "enable or disable log")
+            Boolean value
+            ) {
+        return mBluetoothAdapter.configHciSnoopLog(value);
+    }
+
+    @Rpc(description = "Get Bluetooth controller activity energy info.")
+    public String bluetoothGetControllerActivityEnergyInfo(
+        @RpcParameter(name = "value")
+        Integer value
+            ) {
+        BluetoothActivityEnergyInfo energyInfo = mBluetoothAdapter
+            .getControllerActivityEnergyInfo(value);
+        while (energyInfo == null) {
+          energyInfo = mBluetoothAdapter.getControllerActivityEnergyInfo(value);
+        }
+        return energyInfo.toString();
+    }
+
+    @Rpc(description = "Return true if hardware has entries" +
+            "available for matching beacons.")
+    public boolean bluetoothIsHardwareTrackingFiltersAvailable() {
+        return mBluetoothAdapter.isHardwareTrackingFiltersAvailable();
+    }
+
+    @Rpc(description = "Gets the current state of LE.")
+    public int bluetoothGetLeState() {
+        return mBluetoothAdapter.getLeState();
+    }
+
+    @Rpc(description = "Enables BLE functionalities.")
+    public boolean bluetoothEnableBLE() {
+        mService.registerReceiver(mBleStateReceiver,
+            new IntentFilter(BluetoothAdapter.ACTION_BLE_STATE_CHANGED));
+        return mBluetoothAdapter.enableBLE();
+    }
+
+    @Rpc(description = "Disables BLE functionalities.")
+    public boolean bluetoothDisableBLE() {
+        mService.registerReceiver(mBleStateReceiver,
+            new IntentFilter(BluetoothAdapter.ACTION_BLE_STATE_CHANGED));
+        return mBluetoothAdapter.disableBLE();
+    }
+
+    @Override
+    public void shutdown() {
+        for (Map.Entry<String, BluetoothConnection> entry : connections.entrySet()) {
+            entry.getValue().stop();
+        }
+        connections.clear();
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidFacade.java
new file mode 100644
index 0000000..8db50f1
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHidFacade.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import java.util.List;
+
+import android.app.Service;
+import android.bluetooth.BluetoothInputDevice;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.os.ParcelUuid;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+public class BluetoothHidFacade extends RpcReceiver {
+  public final static ParcelUuid[] UUIDS = { BluetoothUuid.Hid };
+
+  private final Service mService;
+  private final BluetoothAdapter mBluetoothAdapter;
+
+  private static boolean sIsHidReady = false;
+  private static BluetoothInputDevice sHidProfile = null;
+
+  public BluetoothHidFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+    mBluetoothAdapter.getProfileProxy(mService, new HidServiceListener(),
+        BluetoothProfile.INPUT_DEVICE);
+  }
+
+  class HidServiceListener implements BluetoothProfile.ServiceListener {
+    @Override
+    public void onServiceConnected(int profile, BluetoothProfile proxy) {
+      sHidProfile = (BluetoothInputDevice) proxy;
+      sIsHidReady = true;
+    }
+
+    @Override
+    public void onServiceDisconnected(int profile) {
+      sIsHidReady = false;
+    }
+  }
+
+  public Boolean hidConnect(BluetoothDevice device) {
+    if (sHidProfile == null) return false;
+    return sHidProfile.connect(device);
+  }
+
+  public Boolean hidDisconnect(BluetoothDevice device) {
+    if (sHidProfile == null) return false;
+    return sHidProfile.disconnect(device);
+  }
+
+  @Rpc(description = "Is Hid profile ready.")
+  public Boolean bluetoothHidIsReady() {
+    return sIsHidReady;
+  }
+
+  @Rpc(description = "Connect to an HID device.")
+  public Boolean bluetoothHidConnect(
+      @RpcParameter(name = "device", description = "Name or MAC address of a bluetooth device.")
+      String device)
+      throws Exception {
+    if (sHidProfile == null)
+      return false;
+    BluetoothDevice mDevice = BluetoothFacade.getDevice(BluetoothFacade.DiscoveredDevices, device);
+    Log.d("Connecting to device " + mDevice.getAliasName());
+    return hidConnect(mDevice);
+  }
+
+  @Rpc(description = "Disconnect an HID device.")
+  public Boolean bluetoothHidDisconnect(
+      @RpcParameter(name = "device", description = "Name or MAC address of a device.")
+      String device)
+      throws Exception {
+    if (sHidProfile == null)
+      return false;
+    Log.d("Connected devices: " + sHidProfile.getConnectedDevices());
+    BluetoothDevice mDevice = BluetoothFacade.getDevice(sHidProfile.getConnectedDevices(),
+                                                        device);
+    return hidDisconnect(mDevice);
+  }
+
+  @Rpc(description = "Get all the devices connected through HID.")
+  public List<BluetoothDevice> bluetoothHidGetConnectedDevices() {
+    while (!sIsHidReady);
+    return sHidProfile.getConnectedDevices();
+  }
+
+  @Rpc(description = "Get the connection status of a device.")
+  public Integer bluetoothHidGetConnectionStatus(
+          @RpcParameter(name = "deviceID",
+                        description = "Name or MAC address of a bluetooth device.")
+          String deviceID) {
+      if (sHidProfile == null) {
+          return BluetoothProfile.STATE_DISCONNECTED;
+      }
+      List<BluetoothDevice> deviceList = sHidProfile.getConnectedDevices();
+      BluetoothDevice device;
+      try {
+          device = BluetoothFacade.getDevice(deviceList, deviceID);
+      } catch (Exception e) {
+          return BluetoothProfile.STATE_DISCONNECTED;
+      }
+      return sHidProfile.getConnectionState(device);
+  }
+
+  @Rpc(description = "Send Set_Report command to the connected HID input device.")
+  public Boolean bluetoothHidSetReport(
+          @RpcParameter(name = "deviceID",
+          description = "Name or MAC address of a bluetooth device.")
+          String deviceID,
+          @RpcParameter(name = "type")
+          @RpcDefault(value = "1")
+          String type,
+          @RpcParameter(name = "report")
+          String report) throws Exception {
+      BluetoothDevice device = BluetoothFacade.getDevice(sHidProfile.getConnectedDevices(),
+              deviceID);
+      Log.d("type " + type.getBytes()[0]);
+      return sHidProfile.setReport(device, type.getBytes()[0], report);
+  }
+
+  @Rpc(description = "Send Get_Report command to the connected HID input device.")
+  public Boolean bluetoothHidGetReport(
+          @RpcParameter(name = "deviceID",
+          description = "Name or MAC address of a bluetooth device.")
+          String deviceID,
+          @RpcParameter(name = "type")
+          @RpcDefault(value = "1")
+          String type,
+          @RpcParameter(name = "reportId")
+          String reportId,
+          @RpcParameter(name = "buffSize")
+          Integer buffSize) throws Exception {
+      BluetoothDevice device = BluetoothFacade.getDevice(sHidProfile.getConnectedDevices(),
+              deviceID);
+      Log.d("type " + type.getBytes()[0] + "reportId " + reportId.getBytes()[0]);
+      return sHidProfile.getReport(device, type.getBytes()[0], reportId.getBytes()[0], buffSize);
+  }
+
+  @Rpc(description = "Send data to a connected HID device.")
+  public Boolean bluetoothHidSendData(
+          @RpcParameter(name = "deviceID",
+          description = "Name or MAC address of a bluetooth device.")
+          String deviceID,
+          @RpcParameter(name = "report")
+          String report) throws Exception {
+      BluetoothDevice device = BluetoothFacade.getDevice(sHidProfile.getConnectedDevices(),
+              deviceID);
+      return sHidProfile.sendData(device, report);
+  }
+
+  @Rpc(description = "Send virtual unplug to a connected HID device.")
+  public Boolean bluetoothHidVirtualUnplug(
+          @RpcParameter(name = "deviceID",
+          description = "Name or MAC address of a bluetooth device.")
+          String deviceID) throws Exception {
+      BluetoothDevice device = BluetoothFacade.getDevice(sHidProfile.getConnectedDevices(),
+              deviceID);
+      return sHidProfile.virtualUnplug(device);
+  }
+
+  @Rpc(description = "Test byte transfer.")
+  public byte[] testByte() {
+      byte[] bts = {0b01,0b10,0b11,0b100};
+      return bts;
+  }
+
+  @Override
+  public void shutdown() {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHspFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHspFacade.java
new file mode 100644
index 0000000..f4514cc
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothHspFacade.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import java.util.List;
+
+import android.app.Service;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.os.ParcelUuid;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+public class BluetoothHspFacade extends RpcReceiver {
+  static final ParcelUuid[] UUIDS = {
+    BluetoothUuid.HSP, BluetoothUuid.Handsfree
+  };
+
+  private final Service mService;
+  private final BluetoothAdapter mBluetoothAdapter;
+
+  private static boolean sIsHspReady = false;
+  private static BluetoothHeadset sHspProfile = null;
+
+  public BluetoothHspFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+    mBluetoothAdapter.getProfileProxy(mService, new HspServiceListener(),
+        BluetoothProfile.HEADSET);
+  }
+
+  class HspServiceListener implements BluetoothProfile.ServiceListener {
+    @Override
+    public void onServiceConnected(int profile, BluetoothProfile proxy) {
+      sHspProfile = (BluetoothHeadset) proxy;
+      sIsHspReady = true;
+    }
+
+    @Override
+    public void onServiceDisconnected(int profile) {
+      sIsHspReady = false;
+    }
+  }
+
+  public Boolean hspConnect(BluetoothDevice device) {
+    if (sHspProfile == null) return false;
+    return sHspProfile.connect(device);
+  }
+
+  public Boolean hspDisconnect(BluetoothDevice device) {
+    if (sHspProfile == null) return false;
+    return sHspProfile.disconnect(device);
+  }
+
+  @Rpc(description = "Is Hsp profile ready.")
+  public Boolean bluetoothHspIsReady() {
+    return sIsHspReady;
+  }
+
+  @Rpc(description = "Connect to an HSP device.")
+  public Boolean bluetoothHspConnect(
+      @RpcParameter(name = "device", description = "Name or MAC address of a bluetooth device.")
+      String device)
+      throws Exception {
+    if (sHspProfile == null)
+      return false;
+    BluetoothDevice mDevice = BluetoothFacade.getDevice(BluetoothFacade.DiscoveredDevices, device);
+    Log.d("Connecting to device " + mDevice.getAliasName());
+    return hspConnect(mDevice);
+  }
+
+  @Rpc(description = "Disconnect an HSP device.")
+  public Boolean bluetoothHspDisconnect(
+      @RpcParameter(name = "device", description = "Name or MAC address of a device.")
+      String device)
+      throws Exception {
+    if (sHspProfile == null)
+      return false;
+    Log.d("Connected devices: " + sHspProfile.getConnectedDevices());
+    BluetoothDevice mDevice = BluetoothFacade.getDevice(sHspProfile.getConnectedDevices(),
+                                                        device);
+    return hspDisconnect(mDevice);
+  }
+
+  @Rpc(description = "Get all the devices connected through HSP.")
+  public List<BluetoothDevice> bluetoothHspGetConnectedDevices() {
+    while (!sIsHspReady);
+    return sHspProfile.getConnectedDevices();
+  }
+
+  @Rpc(description = "Get the connection status of a device.")
+  public Integer bluetoothHspGetConnectionStatus(
+          @RpcParameter(name = "deviceID",
+                        description = "Name or MAC address of a bluetooth device.")
+          String deviceID) {
+      if (sHspProfile == null) {
+          return BluetoothProfile.STATE_DISCONNECTED;
+      }
+      List<BluetoothDevice> deviceList = sHspProfile.getConnectedDevices();
+      BluetoothDevice device;
+      try {
+          device = BluetoothFacade.getDevice(deviceList, deviceID);
+      } catch (Exception e) {
+          return BluetoothProfile.STATE_DISCONNECTED;
+      }
+      return sHspProfile.getConnectionState(device);
+  }
+
+  @Override
+  public void shutdown() {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothLeAdvertiseFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothLeAdvertiseFacade.java
new file mode 100644
index 0000000..3537c9a
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothLeAdvertiseFacade.java
@@ -0,0 +1,605 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseData.Builder;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.MainThread;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+import com.googlecode.android_scripting.ConvertUtils;
+
+/**
+ * BluetoothLe Advertise functions.
+ */
+
+public class BluetoothLeAdvertiseFacade extends RpcReceiver {
+
+    private final EventFacade mEventFacade;
+    private BluetoothAdapter mBluetoothAdapter;
+    private static int BleAdvertiseCallbackCount;
+    private static int BleAdvertiseSettingsCount;
+    private static int BleAdvertiseDataCount;
+    private final HashMap<Integer, myAdvertiseCallback> mAdvertiseCallbackList;
+    private final BluetoothLeAdvertiser mAdvertise;
+    private final Service mService;
+    private Builder mAdvertiseDataBuilder;
+    private android.bluetooth.le.AdvertiseSettings.Builder mAdvertiseSettingsBuilder;
+    private final HashMap<Integer, AdvertiseData> mAdvertiseDataList;
+    private final HashMap<Integer, AdvertiseSettings> mAdvertiseSettingsList;
+
+    public BluetoothLeAdvertiseFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mBluetoothAdapter = MainThread.run(mService,
+                new Callable<BluetoothAdapter>() {
+                    @Override
+                    public BluetoothAdapter call() throws Exception {
+                        return BluetoothAdapter.getDefaultAdapter();
+                    }
+                });
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mAdvertiseCallbackList = new HashMap<Integer, myAdvertiseCallback>();
+        mAdvertise = mBluetoothAdapter.getBluetoothLeAdvertiser();
+        mAdvertiseDataList = new HashMap<Integer, AdvertiseData>();
+        mAdvertiseSettingsList = new HashMap<Integer, AdvertiseSettings>();
+        mAdvertiseDataBuilder = new Builder();
+        mAdvertiseSettingsBuilder = new android.bluetooth.le.AdvertiseSettings.Builder();
+    }
+
+    /**
+     * Constructs a myAdvertiseCallback obj and returns its index
+     *
+     * @return myAdvertiseCallback.index
+     */
+    @Rpc(description = "Generate a new myAdvertisement Object")
+    public Integer bleGenBleAdvertiseCallback() {
+        BleAdvertiseCallbackCount += 1;
+        int index = BleAdvertiseCallbackCount;
+        myAdvertiseCallback mCallback = new myAdvertiseCallback(index);
+        mAdvertiseCallbackList.put(mCallback.index,
+                mCallback);
+        return mCallback.index;
+    }
+
+    /**
+     * Constructs a AdvertiseData obj and returns its index
+     *
+     * @return index
+     */
+    @Rpc(description = "Constructs a new Builder obj for AdvertiseData and returns its index")
+    public Integer bleBuildAdvertiseData() {
+        BleAdvertiseDataCount += 1;
+        int index = BleAdvertiseDataCount;
+        mAdvertiseDataList.put(index,
+                mAdvertiseDataBuilder.build());
+        mAdvertiseDataBuilder = new Builder();
+        return index;
+    }
+
+    /**
+     * Constructs a Advertise Settings obj and returns its index
+     *
+     * @return index
+     */
+    @Rpc(description = "Constructs a new Builder obj for AdvertiseData and returns its index")
+    public Integer bleBuildAdvertiseSettings() {
+        BleAdvertiseSettingsCount += 1;
+        int index = BleAdvertiseSettingsCount;
+        mAdvertiseSettingsList.put(index,
+                mAdvertiseSettingsBuilder.build());
+        mAdvertiseSettingsBuilder = new android.bluetooth.le.AdvertiseSettings.Builder();
+        return index;
+    }
+
+    /**
+     * Stops a ble advertisement
+     *
+     * @param index the id of the advertisement to stop advertising on
+     * @throws Exception
+     */
+    @Rpc(description = "Stops an ongoing ble advertisement")
+    public void bleStopBleAdvertising(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        if (mAdvertiseCallbackList.get(index) != null) {
+            Log.d("bluetooth_le mAdvertise " + index);
+            mAdvertise.stopAdvertising(mAdvertiseCallbackList
+                    .get(index));
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Starts ble advertising
+     *
+     * @param callbackIndex The advertisementCallback index
+     * @param dataIndex the AdvertiseData index
+     * @param settingsIndex the advertisementsettings index
+     * @throws Exception
+     */
+    @Rpc(description = "Starts ble advertisement")
+    public void bleStartBleAdvertising(
+            @RpcParameter(name = "callbackIndex")
+            Integer callbackIndex,
+            @RpcParameter(name = "dataIndex")
+            Integer dataIndex,
+            @RpcParameter(name = "settingsIndex")
+            Integer settingsIndex
+            ) throws Exception {
+        AdvertiseData mData = new AdvertiseData.Builder().build();
+        AdvertiseSettings mSettings = new AdvertiseSettings.Builder().build();
+        if (mAdvertiseDataList.get(dataIndex) != null) {
+            mData = mAdvertiseDataList.get(dataIndex);
+        } else {
+            throw new Exception("Invalid dataIndex input:" + Integer.toString(dataIndex));
+        }
+        if (mAdvertiseSettingsList.get(settingsIndex) != null) {
+            mSettings = mAdvertiseSettingsList.get(settingsIndex);
+        } else {
+            throw new Exception("Invalid settingsIndex input:" + Integer.toString(settingsIndex));
+        }
+        if (mAdvertiseCallbackList.get(callbackIndex) != null) {
+            Log.d("bluetooth_le starting a background advertisement on callback index: "
+                    + Integer.toString(callbackIndex));
+            mAdvertise
+                    .startAdvertising(mSettings, mData, mAdvertiseCallbackList.get(callbackIndex));
+        } else {
+            throw new Exception("Invalid callbackIndex input" + Integer.toString(callbackIndex));
+        }
+    }
+
+    /**
+     * Starts ble advertising with a scanResponse. ScanResponses are created in the same way
+     * AdvertiseData is created since they share the same object type.
+     *
+     * @param callbackIndex The advertisementCallback index
+     * @param dataIndex the AdvertiseData index
+     * @param settingsIndex the advertisementsettings index
+     * @param scanResponseIndex the scanResponse index
+     * @throws Exception
+     */
+    @Rpc(description = "Starts ble advertisement")
+    public void bleStartBleAdvertisingWithScanResponse(
+            @RpcParameter(name = "callbackIndex")
+            Integer callbackIndex,
+            @RpcParameter(name = "dataIndex")
+            Integer dataIndex,
+            @RpcParameter(name = "settingsIndex")
+            Integer settingsIndex,
+            @RpcParameter(name = "scanResponseIndex")
+            Integer scanResponseIndex
+            ) throws Exception {
+        AdvertiseData mData = new AdvertiseData.Builder().build();
+        AdvertiseSettings mSettings = new AdvertiseSettings.Builder().build();
+        AdvertiseData mScanResponse = new AdvertiseData.Builder().build();
+
+        if (mAdvertiseDataList.get(dataIndex) != null) {
+            mData = mAdvertiseDataList.get(dataIndex);
+        } else {
+            throw new Exception("Invalid dataIndex input:" + Integer.toString(dataIndex));
+        }
+        if (mAdvertiseSettingsList.get(settingsIndex) != null) {
+            mSettings = mAdvertiseSettingsList.get(settingsIndex);
+        } else {
+            throw new Exception("Invalid settingsIndex input:" + Integer.toString(settingsIndex));
+        }
+        if (mAdvertiseDataList.get(scanResponseIndex) != null) {
+            mScanResponse = mAdvertiseDataList.get(scanResponseIndex);
+        } else {
+            throw new Exception("Invalid scanResponseIndex input:"
+                    + Integer.toString(settingsIndex));
+        }
+        if (mAdvertiseCallbackList.get(callbackIndex) != null) {
+            Log.d("bluetooth_le starting a background advertise on callback index: "
+                    + Integer.toString(callbackIndex));
+            mAdvertise
+                    .startAdvertising(mSettings, mData, mScanResponse,
+                            mAdvertiseCallbackList.get(callbackIndex));
+        } else {
+            throw new Exception("Invalid callbackIndex input" + Integer.toString(callbackIndex));
+        }
+    }
+
+    /**
+     * Get ble advertisement settings mode
+     *
+     * @param index the advertise settings object to use
+     * @return the mode of the advertise settings object
+     * @throws Exception
+     */
+    @Rpc(description = "Get ble advertisement settings mode")
+    public int bleGetAdvertiseSettingsMode(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        if (mAdvertiseSettingsList.get(index) != null) {
+            AdvertiseSettings mSettings = mAdvertiseSettingsList.get(index);
+            return mSettings.getMode();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ble advertisement settings tx power level
+     *
+     * @param index the advertise settings object to use
+     * @return the tx power level of the advertise settings object
+     * @throws Exception
+     */
+    @Rpc(description = "Get ble advertisement settings tx power level")
+    public int bleGetAdvertiseSettingsTxPowerLevel(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        if (mAdvertiseSettingsList.get(index) != null) {
+            AdvertiseSettings mSettings = mAdvertiseSettingsList.get(index);
+            return mSettings.getTxPowerLevel();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ble advertisement settings isConnectable value
+     *
+     * @param index the advertise settings object to use
+     * @return the boolean value whether the advertisement will indicate
+     * connectable.
+     * @throws Exception
+     */
+    @Rpc(description = "Get ble advertisement settings isConnectable value")
+    public boolean bleGetAdvertiseSettingsIsConnectable(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        if (mAdvertiseSettingsList.get(index) != null) {
+            AdvertiseSettings mSettings = mAdvertiseSettingsList.get(index);
+            return mSettings.isConnectable();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ble advertisement data include tx power level
+     *
+     * @param index the advertise data object to use
+     * @return True if include tx power level, false otherwise
+     * @throws Exception
+     */
+    @Rpc(description = "Get ble advertisement data include tx power level")
+    public Boolean bleGetAdvertiseDataIncludeTxPowerLevel(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        if (mAdvertiseDataList.get(index) != null) {
+            AdvertiseData mData = mAdvertiseDataList.get(index);
+            return mData.getIncludeTxPowerLevel();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ble advertisement data manufacturer specific data
+     *
+     * @param index the advertise data object to use
+     * @param manufacturerId the id that corresponds to the manufacturer specific data.
+     * @return the corresponding manufacturer specific data to the manufacturer id.
+     * @throws Exception
+     */
+    @Rpc(description = "Get ble advertisement data manufacturer specific data")
+    public String bleGetAdvertiseDataManufacturerSpecificData(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "manufacturerId")
+            Integer manufacturerId) throws Exception {
+        if (mAdvertiseDataList.get(index) != null) {
+            AdvertiseData mData = mAdvertiseDataList.get(index);
+            if (mData.getManufacturerSpecificData() != null) {
+                return ConvertUtils.convertByteArrayToString(mData.getManufacturerSpecificData().get(manufacturerId));
+            } else {
+                throw new Exception("Invalid manufacturerId input:" + Integer.toString(manufacturerId));
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+
+        }
+    }
+
+    /**
+     * Get ble advertisement data include device name
+     *
+     * @param index the advertise data object to use
+     * @return the advertisement data's include device name
+     * @throws Exception
+     */
+    @Rpc(description = "Get ble advertisement include device name")
+    public Boolean bleGetAdvertiseDataIncludeDeviceName(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        if (mAdvertiseDataList.get(index) != null) {
+            AdvertiseData mData = mAdvertiseDataList.get(index);
+            return mData.getIncludeDeviceName();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ble advertisement Service Data
+     *
+     * @param index the advertise data object to use
+     * @param serviceUuid the uuid corresponding to the service data.
+     * @return the advertisement data's service data
+     * @throws Exception
+     */
+    @Rpc(description = "Get ble advertisement Service Data")
+    public String bleGetAdvertiseDataServiceData(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "serviceUuid")
+            String serviceUuid) throws Exception {
+        ParcelUuid uuidKey = ParcelUuid.fromString(serviceUuid);
+        if (mAdvertiseDataList.get(index) != null) {
+            AdvertiseData mData = mAdvertiseDataList.get(index);
+            if (mData.getServiceData().containsKey(uuidKey)) {
+                return ConvertUtils.convertByteArrayToString(mData.getServiceData().get(uuidKey));
+            } else {
+                throw new Exception("Invalid serviceUuid input:" + serviceUuid);
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ble advertisement Service Uuids
+     *
+     * @param index the advertise data object to use
+     * @return the advertisement data's Service Uuids
+     * @throws Exception
+     */
+    @Rpc(description = "Get ble advertisement Service Uuids")
+    public List<ParcelUuid> bleGetAdvertiseDataServiceUuids(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        if (mAdvertiseDataList.get(index) != null) {
+            AdvertiseData mData = mAdvertiseDataList.get(index);
+            return mData.getServiceUuids();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Set ble advertisement data service uuids
+     *
+     * @param uuidList
+     * @throws Exception
+     */
+    @Rpc(description = "Set ble advertisement data service uuids")
+    public void bleSetAdvertiseDataSetServiceUuids(
+            @RpcParameter(name = "uuidList")
+            String[] uuidList
+            ) {
+        for (String uuid : uuidList) {
+            mAdvertiseDataBuilder.addServiceUuid(ParcelUuid.fromString(uuid));
+        }
+    }
+
+    /**
+     * Set ble advertise data service uuids
+     *
+     * @param serviceDataUuid
+     * @param serviceData
+     * @throws Exception
+     */
+    @Rpc(description = "Set ble advertise data service uuids")
+    public void bleAddAdvertiseDataServiceData(
+            @RpcParameter(name = "serviceDataUuid")
+            String serviceDataUuid,
+            @RpcParameter(name = "serviceData")
+            String serviceData
+            ) {
+        mAdvertiseDataBuilder.addServiceData(
+                ParcelUuid.fromString(serviceDataUuid),
+                ConvertUtils.convertStringToByteArray(serviceData));
+    }
+
+    /**
+     * Set ble advertise data manufacturer id
+     *
+     * @param manufacturerId the manufacturer id to set
+     * @param manufacturerSpecificData the manufacturer specific data to set
+     * @throws Exception
+     */
+    @Rpc(description = "Set ble advertise data manufacturerId")
+    public void bleAddAdvertiseDataManufacturerId(
+            @RpcParameter(name = "manufacturerId")
+            Integer manufacturerId,
+            @RpcParameter(name = "manufacturerSpecificData")
+            String manufacturerSpecificData
+            ) {
+        mAdvertiseDataBuilder.addManufacturerData(manufacturerId,
+                ConvertUtils.convertStringToByteArray(manufacturerSpecificData));
+    }
+
+    /**
+     * Set ble advertise settings advertise mode
+     *
+     * @param advertiseMode
+     * @throws Exception
+     */
+    @Rpc(description = "Set ble advertise settings advertise mode")
+    public void bleSetAdvertiseSettingsAdvertiseMode(
+            @RpcParameter(name = "advertiseMode")
+            Integer advertiseMode
+            ) {
+        mAdvertiseSettingsBuilder.setAdvertiseMode(advertiseMode);
+    }
+
+    /**
+     * Set ble advertise settings tx power level
+     *
+     * @param txPowerLevel the tx power level to set
+     * @throws Exception
+     */
+    @Rpc(description = "Set ble advertise settings tx power level")
+    public void bleSetAdvertiseSettingsTxPowerLevel(
+            @RpcParameter(name = "txPowerLevel")
+            Integer txPowerLevel
+            ) {
+        mAdvertiseSettingsBuilder.setTxPowerLevel(txPowerLevel);
+    }
+
+    /**
+     * Set ble advertise settings the isConnectable value
+     *
+     * @param type the isConnectable value
+     * @throws Exception
+     */
+    @Rpc(description = "Set ble advertise settings isConnectable value")
+    public void bleSetAdvertiseSettingsIsConnectable(
+            @RpcParameter(name = "value")
+            Boolean value
+            ) {
+        mAdvertiseSettingsBuilder.setConnectable(value);
+    }
+
+    /**
+     * Set ble advertisement data include tx power level
+     *
+     * @param includeTxPowerLevel boolean whether to include the tx power level or not in the
+     *            advertisement
+     */
+    @Rpc(description = "Set ble advertisement data include tx power level")
+    public void bleSetAdvertiseDataIncludeTxPowerLevel(
+            @RpcParameter(name = "includeTxPowerLevel")
+            Boolean includeTxPowerLevel
+            ) {
+        mAdvertiseDataBuilder.setIncludeTxPowerLevel(includeTxPowerLevel);
+    }
+
+    /**
+     * Set ble advertisement settings set timeout
+     *
+     * @param timeoutSeconds Limit advertising to a given amount of time.
+     */
+    @Rpc(description = "Set ble advertisement data include tx power level")
+    public void bleSetAdvertiseSettingsTimeout(
+            @RpcParameter(name = "timeoutSeconds")
+            Integer timeoutSeconds
+            ) {
+        mAdvertiseSettingsBuilder.setTimeout(timeoutSeconds);
+    }
+
+    /**
+     * Set ble advertisement data include device name
+     *
+     * @param includeDeviceName boolean whether to include device name or not in the
+     *            advertisement
+     */
+    @Rpc(description = "Set ble advertisement data include device name")
+    public void bleSetAdvertiseDataIncludeDeviceName(
+            @RpcParameter(name = "includeDeviceName")
+            Boolean includeDeviceName
+            ) {
+        mAdvertiseDataBuilder.setIncludeDeviceName(includeDeviceName);
+    }
+
+    private class myAdvertiseCallback extends AdvertiseCallback {
+        public Integer index;
+        private final Bundle mResults;
+        String mEventType;
+
+        public myAdvertiseCallback(int idx) {
+            index = idx;
+            mEventType = "BleAdvertise";
+            mResults = new Bundle();
+        }
+
+        @Override
+        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+            Log.d("bluetooth_le_advertisement onSuccess " + mEventType + " "
+                    + index);
+            mResults.putString("Type", "onSuccess");
+            mResults.putParcelable("SettingsInEffect", settingsInEffect);
+            mEventFacade.postEvent(mEventType + index + "onSuccess", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onStartFailure(int errorCode) {
+            String errorString = "UNKNOWN_ERROR_CODE";
+            if (errorCode == AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED) {
+                errorString = "ADVERTISE_FAILED_ALREADY_STARTED";
+            } else if (errorCode == AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE) {
+                errorString = "ADVERTISE_FAILED_DATA_TOO_LARGE";
+            } else if (errorCode == AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED) {
+                errorString = "ADVERTISE_FAILED_FEATURE_UNSUPPORTED";
+            } else if (errorCode == AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR) {
+                errorString = "ADVERTISE_FAILED_INTERNAL_ERROR";
+            } else if (errorCode == AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS) {
+                errorString = "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS";
+            }
+            Log.d("bluetooth_le_advertisement onFailure " + mEventType + " "
+                    + index + " error " + errorString);
+            mResults.putString("Type", "onFailure");
+            mResults.putInt("ErrorCode", errorCode);
+            mResults.putString("Error", errorString);
+            mEventFacade.postEvent(mEventType + index + "onFailure",
+                    mResults.clone());
+            mResults.clear();
+        }
+    }
+
+    @Override
+    public void shutdown() {
+        if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
+            for (myAdvertiseCallback mAdvertise : mAdvertiseCallbackList
+                .values()) {
+                if (mAdvertise != null) {
+                    try{
+                        mBluetoothAdapter.getBluetoothLeAdvertiser()
+                            .stopAdvertising(mAdvertise);
+                    } catch (NullPointerException e) {
+                        Log.e("Failed to stop ble advertising.", e);
+                    }
+                }
+            }
+        }
+        mAdvertiseCallbackList.clear();
+        mAdvertiseSettingsList.clear();
+        mAdvertiseDataList.clear();
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothLeScanFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothLeScanFacade.java
new file mode 100644
index 0000000..7ae78a2
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothLeScanFacade.java
@@ -0,0 +1,924 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.BluetoothAdapter.LeScanCallback;
+import android.bluetooth.le.ScanFilter.Builder;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+
+import com.googlecode.android_scripting.ConvertUtils;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.MainThread;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * BluetoothLe Scan functions.
+ */
+
+public class BluetoothLeScanFacade extends RpcReceiver {
+
+    private final EventFacade mEventFacade;
+
+    private BluetoothAdapter mBluetoothAdapter;
+    private static int ScanCallbackCount;
+    private static int FilterListCount;
+    private static int LeScanCallbackCount;
+    private static int ScanSettingsCount;
+    private final Service mService;
+    private final BluetoothLeScanner mScanner;
+    private android.bluetooth.le.ScanSettings.Builder mScanSettingsBuilder;
+    private Builder mScanFilterBuilder;
+    private final HashMap<Integer, myScanCallback> mScanCallbackList;
+    private final HashMap<Integer, myLeScanCallback> mLeScanCallbackList;
+    private final HashMap<Integer, ArrayList<ScanFilter>> mScanFilterList;
+    private final HashMap<Integer, ScanSettings> mScanSettingsList;
+
+    public BluetoothLeScanFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mBluetoothAdapter = MainThread.run(mService,
+                new Callable<BluetoothAdapter>() {
+                    @Override
+                    public BluetoothAdapter call() throws Exception {
+                        return BluetoothAdapter.getDefaultAdapter();
+                    }
+                });
+        mScanner = mBluetoothAdapter.getBluetoothLeScanner();
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mScanFilterList = new HashMap<Integer, ArrayList<ScanFilter>>();
+        mLeScanCallbackList = new HashMap<Integer, myLeScanCallback>();
+        mScanSettingsList = new HashMap<Integer, ScanSettings>();
+        mScanCallbackList = new HashMap<Integer, myScanCallback>();
+        mScanFilterBuilder = new Builder();
+        mScanSettingsBuilder = new android.bluetooth.le.ScanSettings.Builder();
+    }
+
+    /**
+     * Constructs a myScanCallback obj and returns its index
+     *
+     * @return Integer myScanCallback.index
+     */
+    @Rpc(description = "Generate a new myScanCallback Object")
+    public Integer bleGenScanCallback() {
+        ScanCallbackCount += 1;
+        int index = ScanCallbackCount;
+        myScanCallback mScan = new myScanCallback(index);
+        mScanCallbackList.put(mScan.index, mScan);
+        return mScan.index;
+    }
+
+    /**
+     * Constructs a myLeScanCallback obj and returns its index
+     *
+     * @return Integer myScanCallback.index
+     */
+    @Rpc(description = "Generate a new myScanCallback Object")
+    public Integer bleGenLeScanCallback() {
+        LeScanCallbackCount += 1;
+        int index = LeScanCallbackCount;
+        myLeScanCallback mScan = new myLeScanCallback(index);
+        mLeScanCallbackList.put(mScan.index, mScan);
+        return mScan.index;
+    }
+
+    /**
+     * Constructs a new filter list array and returns its index
+     *
+     * @return Integer index
+     */
+    @Rpc(description = "Generate a new Filter list")
+    public Integer bleGenFilterList() {
+        FilterListCount += 1;
+        int index = FilterListCount;
+        mScanFilterList.put(index, new ArrayList<ScanFilter>());
+        return index;
+    }
+
+    /**
+     * Constructs a new filter list array and returns its index
+     *
+     * @return Integer index
+     */
+    @Rpc(description = "Generate a new Filter list")
+    public Integer bleBuildScanFilter(
+            @RpcParameter(name = "filterIndex")
+            Integer filterIndex
+            ) {
+        mScanFilterList.get(filterIndex).add(mScanFilterBuilder.build());
+        mScanFilterBuilder = new Builder();
+        return mScanFilterList.get(filterIndex).size()-1;
+    }
+
+    /**
+     * Constructs a new scan setting and returns its index
+     *
+     * @return Integer index
+     */
+    @Rpc(description = "Generate a new scan settings Object")
+    public Integer bleBuildScanSetting() {
+        ScanSettingsCount += 1;
+        int index = ScanSettingsCount;
+        mScanSettingsList.put(index, mScanSettingsBuilder.build());
+        mScanSettingsBuilder = new android.bluetooth.le.ScanSettings.Builder();
+        return index;
+    }
+
+    /**
+     * Stops a ble scan
+     *
+     * @param index the id of the myScan whose ScanCallback to stop
+     * @throws Exception
+     */
+    @Rpc(description = "Stops an ongoing ble advertisement scan")
+    public void bleStopBleScan(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        Log.d("bluetooth_le_scan mScanCallback " + index);
+        if (mScanCallbackList.get(index) != null) {
+            myScanCallback mScanCallback = mScanCallbackList.get(index);
+            mScanner.stopScan(mScanCallback);
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Stops a classic ble scan
+     *
+     * @param index the id of the myScan whose LeScanCallback to stop
+     * @throws Exception
+     */
+    @Rpc(description = "Stops an ongoing classic ble scan")
+    public void bleStopClassicBleScan(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        Log.d("bluetooth_le_scan mLeScanCallback " + index);
+        if (mLeScanCallbackList.get(index) != null) {
+            myLeScanCallback mLeScanCallback = mLeScanCallbackList.get(index);
+            mBluetoothAdapter.stopLeScan(mLeScanCallback);
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Starts a ble scan
+     *
+     * @param index the id of the myScan whose ScanCallback to start
+     * @throws Exception
+     */
+    @Rpc(description = "Starts a ble advertisement scan")
+    public void bleStartBleScan(
+            @RpcParameter(name = "filterListIndex")
+            Integer filterListIndex,
+            @RpcParameter(name = "scanSettingsIndex")
+            Integer scanSettingsIndex,
+            @RpcParameter(name = "callbackIndex")
+            Integer callbackIndex
+            ) throws Exception {
+        Log.d("bluetooth_le_scan starting a background scan");
+        ArrayList<ScanFilter> mScanFilters = new ArrayList<ScanFilter>();
+        mScanFilters.add(new ScanFilter.Builder().build());
+        ScanSettings mScanSettings = new ScanSettings.Builder().build();
+        if (mScanFilterList.get(filterListIndex) != null) {
+            mScanFilters = mScanFilterList.get(filterListIndex);
+        } else {
+            throw new Exception("Invalid filterListIndex input:"
+                    + Integer.toString(filterListIndex));
+        }
+        if (mScanSettingsList.get(scanSettingsIndex) != null) {
+            mScanSettings = mScanSettingsList.get(scanSettingsIndex);
+        } else if (!mScanSettingsList.isEmpty()) {
+            throw new Exception("Invalid scanSettingsIndex input:"
+                    + Integer.toString(scanSettingsIndex));
+        }
+        if (mScanCallbackList.get(callbackIndex) != null) {
+            mScanner.startScan(mScanFilters, mScanSettings, mScanCallbackList.get(callbackIndex));
+        } else {
+            throw new Exception("Invalid filterListIndex input:"
+                    + Integer.toString(filterListIndex));
+        }
+    }
+
+    /**
+     * Starts a classic ble scan
+     *
+     * @param index the id of the myScan whose ScanCallback to start
+     * @throws Exception
+     */
+    @Rpc(description = "Starts a classic ble advertisement scan")
+    public boolean bleStartClassicBleScan(
+            @RpcParameter(name = "leCallbackIndex")
+            Integer leCallbackIndex
+            ) throws Exception {
+        Log.d("bluetooth_le_scan starting a background scan");
+        boolean result = false;
+        if (mLeScanCallbackList.get(leCallbackIndex) != null) {
+            result = mBluetoothAdapter.startLeScan(mLeScanCallbackList.get(leCallbackIndex));
+        } else {
+            throw new Exception("Invalid leCallbackIndex input:"
+                    + Integer.toString(leCallbackIndex));
+        }
+        return result;
+    }
+
+    /**
+     * Starts a classic ble scan with service Uuids
+     *
+     * @param index the id of the myScan whose ScanCallback to start
+     * @throws Exception
+     */
+    @Rpc(description = "Starts a classic ble advertisement scan with service Uuids")
+    public boolean bleStartClassicBleScanWithServiceUuids(
+            @RpcParameter(name = "leCallbackIndex")
+            Integer leCallbackIndex,
+            @RpcParameter(name = "serviceUuids")
+            String[] serviceUuidList
+            ) throws Exception {
+        Log.d("bluetooth_le_scan starting a background scan");
+        UUID[] serviceUuids = new UUID[serviceUuidList.length];
+        for (int i = 0; i < serviceUuidList.length; i++) {
+            serviceUuids[i] = UUID.fromString(serviceUuidList[i]);
+        }
+        boolean result = false;
+        if (mLeScanCallbackList.get(leCallbackIndex) != null) {
+            result = mBluetoothAdapter.startLeScan(serviceUuids,
+                    mLeScanCallbackList.get(leCallbackIndex));
+            System.out.println(result);
+        } else {
+            throw new Exception("Invalid leCallbackIndex input:"
+                    + Integer.toString(leCallbackIndex));
+        }
+        System.out.println(result);
+        return result;
+    }
+
+    /**
+     * Trigger onBatchScanResults
+     *
+     * @throws Exception
+     */
+    @Rpc(description = "Gets the results of the ble ScanCallback")
+    public void bleFlushPendingScanResults(
+            @RpcParameter(name = "callbackIndex")
+            Integer callbackIndex
+            ) throws Exception {
+        if (mScanCallbackList.get(callbackIndex) != null) {
+            mBluetoothAdapter
+                    .getBluetoothLeScanner().flushPendingScanResults(
+                            mScanCallbackList.get(callbackIndex));
+        } else {
+            throw new Exception("Invalid callbackIndex input:"
+                    + Integer.toString(callbackIndex));
+        }
+    }
+
+    /**
+     * Set scanSettings for ble scan. Note: You have to set all variables at once.
+     *
+     * @param callbackType Bluetooth LE scan callback type
+     * @param reportDelaySeconds Time of delay for reporting the scan result
+     * @param scanMode Bluetooth LE scan mode.
+     * @param scanResultType Bluetooth LE scan result type
+     * @throws Exception
+     */
+
+    /**
+     * Set the scan setting's callback type
+     * @param callbackType Bluetooth LE scan callback type
+     */
+    @Rpc(description = "Set the scan setting's callback type")
+    public void bleSetScanSettingsCallbackType(
+            @RpcParameter(name = "callbackType")
+            Integer callbackType) {
+        mScanSettingsBuilder.setCallbackType(callbackType);
+    }
+
+    /**
+     * Set the scan setting's report delay millis
+     * @param reportDelayMillis Time of delay for reporting the scan result
+     */
+    @Rpc(description = "Set the scan setting's report delay millis")
+    public void bleSetScanSettingsReportDelayMillis(
+            @RpcParameter(name = "reportDelayMillis")
+            Long reportDelayMillis) {
+        mScanSettingsBuilder.setReportDelay(reportDelayMillis);
+    }
+
+    /**
+     * Set the scan setting's scan mode
+     * @param scanMode Bluetooth LE scan mode.
+     */
+    @Rpc(description = "Set the scan setting's scan mode")
+    public void bleSetScanSettingsScanMode(
+            @RpcParameter(name = "scanMode")
+            Integer scanMode) {
+        mScanSettingsBuilder.setScanMode(scanMode);
+    }
+
+    /**
+     * Set the scan setting's scan result type
+     * @param scanResultType Bluetooth LE scan result type
+     */
+    @Rpc(description = "Set the scan setting's scan result type")
+    public void bleSetScanSettingsResultType(
+            @RpcParameter(name = "scanResultType")
+            Integer scanResultType) {
+        mScanSettingsBuilder.setScanResultType(scanResultType);
+    }
+    /**
+     * Get ScanSetting's callback type
+     *
+     * @param index the ScanSetting object to use
+     * @return the ScanSetting's callback type
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanSetting's callback type")
+    public Integer bleGetScanSettingsCallbackType(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mScanSettingsList.get(index) != null) {
+            ScanSettings mScanSettings = mScanSettingsList.get(index);
+            return mScanSettings.getCallbackType();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanSetting's report delay in milli seconds
+     *
+     * @param index the ScanSetting object to useSystemClock
+     * @return the ScanSetting's report delay in milliseconds
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanSetting's report delay milliseconds")
+    public Long bleGetScanSettingsReportDelayMillis(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        if (mScanSettingsList.get(index) != null) {
+            ScanSettings mScanSettings = mScanSettingsList.get(index);
+            return mScanSettings.getReportDelayMillis();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanSetting's scan mode
+     *
+     * @param index the ScanSetting object to use
+     * @return the ScanSetting's scan mode
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanSetting's scan mode")
+    public Integer bleGetScanSettingsScanMode(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        if (mScanSettingsList.get(index) != null) {
+            ScanSettings mScanSettings = mScanSettingsList.get(index);
+            return mScanSettings.getScanMode();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanSetting's scan result type
+     *
+     * @param index the ScanSetting object to use
+     * @return the ScanSetting's scan result type
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanSetting's scan result type")
+    public Integer bleGetScanSettingsScanResultType(
+            @RpcParameter(name = "index")
+            Integer index) throws Exception {
+        if (mScanSettingsList.get(index) != null) {
+            ScanSettings mScanSettings = mScanSettingsList.get(index);
+            return mScanSettings.getScanResultType();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanFilter's Manufacturer Id
+     *
+     * @param index the ScanFilter object to use
+     * @return the ScanFilter's manufacturer id
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanFilter's Manufacturer Id")
+    public Integer bleGetScanFilterManufacturerId(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "filterIndex")
+            Integer filterIndex)
+            throws Exception {
+        if (mScanFilterList.get(index) != null) {
+            if (mScanFilterList.get(index).get(filterIndex) != null) {
+                return mScanFilterList.get(index)
+                        .get(filterIndex).getManufacturerId();
+            } else {
+                throw new Exception("Invalid filterIndex input:" + Integer.toString(filterIndex));
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanFilter's device address
+     *
+     * @param index the ScanFilter object to use
+     * @return the ScanFilter's device address
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanFilter's device address")
+    public String bleGetScanFilterDeviceAddress(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "filterIndex")
+            Integer filterIndex)
+            throws Exception {
+        if (mScanFilterList.get(index) != null) {
+            if (mScanFilterList.get(index).get(filterIndex) != null) {
+                return mScanFilterList.get(index).get(filterIndex).getDeviceAddress();
+            } else {
+                throw new Exception("Invalid filterIndex input:" + Integer.toString(filterIndex));
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanFilter's device name
+     *
+     * @param index the ScanFilter object to use
+     * @return the ScanFilter's device name
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanFilter's device name")
+    public String bleGetScanFilterDeviceName(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "filterIndex")
+            Integer filterIndex)
+            throws Exception {
+        if (mScanFilterList.get(index) != null) {
+            if (mScanFilterList.get(index).get(filterIndex) != null) {
+                return mScanFilterList.get(index).get(filterIndex).getDeviceName();
+            } else {
+                throw new Exception("Invalid filterIndex input:" + Integer.toString(filterIndex));
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanFilter's manufacturer data
+     *
+     * @param index the ScanFilter object to use
+     * @return the ScanFilter's manufacturer data
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanFilter's manufacturer data")
+    public String bleGetScanFilterManufacturerData(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "filterIndex")
+            Integer filterIndex)
+            throws Exception {
+        if (mScanFilterList.get(index) != null) {
+            if (mScanFilterList.get(index).get(filterIndex) != null) {
+                return ConvertUtils.convertByteArrayToString(mScanFilterList.get(index)
+                        .get(filterIndex).getManufacturerData());
+            } else {
+                throw new Exception("Invalid filterIndex input:" + Integer.toString(filterIndex));
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanFilter's manufacturer data mask
+     *
+     * @param index the ScanFilter object to use
+     * @return the ScanFilter's manufacturer data mask
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanFilter's manufacturer data mask")
+    public String bleGetScanFilterManufacturerDataMask(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "filterIndex")
+            Integer filterIndex)
+            throws Exception {
+        if (mScanFilterList.get(index) != null) {
+            if (mScanFilterList.get(index).get(filterIndex) != null) {
+                return ConvertUtils.convertByteArrayToString(mScanFilterList.get(index)
+                        .get(filterIndex).getManufacturerDataMask());
+            } else {
+                throw new Exception("Invalid filterIndex input:" + Integer.toString(filterIndex));
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanFilter's service data
+     *
+     * @param index the ScanFilter object to use
+     * @return the ScanFilter's service data
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanFilter's service data")
+    public String bleGetScanFilterServiceData(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "filterIndex")
+            Integer filterIndex)
+            throws Exception {
+        if (mScanFilterList.get(index) != null) {
+            if (mScanFilterList.get(index).get(filterIndex) != null) {
+                return ConvertUtils.convertByteArrayToString(mScanFilterList
+                        .get(index).get(filterIndex).getServiceData());
+            } else {
+                throw new Exception("Invalid filterIndex input:" + Integer.toString(filterIndex));
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanFilter's service data mask
+     *
+     * @param index the ScanFilter object to use
+     * @return the ScanFilter's service data mask
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanFilter's service data mask")
+    public String bleGetScanFilterServiceDataMask(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "filterIndex")
+            Integer filterIndex)
+            throws Exception {
+        if (mScanFilterList.get(index) != null) {
+            if (mScanFilterList.get(index).get(filterIndex) != null) {
+                return ConvertUtils.convertByteArrayToString(mScanFilterList.get(index)
+                        .get(filterIndex).getServiceDataMask());
+            } else {
+                throw new Exception("Invalid filterIndex input:" + Integer.toString(filterIndex));
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanFilter's service uuid
+     *
+     * @param index the ScanFilter object to use
+     * @return the ScanFilter's service uuid
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanFilter's service uuid")
+    public String bleGetScanFilterServiceUuid(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "filterIndex")
+            Integer filterIndex)
+            throws Exception {
+        if (mScanFilterList.get(index) != null) {
+            if (mScanFilterList.get(index).get(filterIndex) != null) {
+                if (mScanFilterList.get(index).get(filterIndex).getServiceUuid() != null) {
+                    return mScanFilterList.get(index).get(filterIndex).getServiceUuid().toString();
+                } else {
+                    throw new Exception("No Service Uuid set for filter:"
+                            + Integer.toString(filterIndex));
+                }
+            } else {
+                throw new Exception("Invalid filterIndex input:" + Integer.toString(filterIndex));
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get ScanFilter's service uuid mask
+     *
+     * @param index the ScanFilter object to use
+     * @return the ScanFilter's service uuid mask
+     * @throws Exception
+     */
+    @Rpc(description = "Get ScanFilter's service uuid mask")
+    public String bleGetScanFilterServiceUuidMask(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "filterIndex")
+            Integer filterIndex)
+            throws Exception {
+        if (mScanFilterList.get(index) != null) {
+            if (mScanFilterList.get(index).get(filterIndex) != null) {
+                if (mScanFilterList.get(index).get(filterIndex).getServiceUuidMask() != null) {
+                    return mScanFilterList.get(index).get(filterIndex).getServiceUuidMask()
+                            .toString();
+                } else {
+                    throw new Exception("No Service Uuid Mask set for filter:"
+                            + Integer.toString(filterIndex));
+                }
+            } else {
+                throw new Exception("Invalid filterIndex input:" + Integer.toString(filterIndex));
+            }
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Add filter "macAddress" to existing ScanFilter
+     *
+     * @param macAddress the macAddress to filter against
+     * @throws Exception
+     */
+    @Rpc(description = "Add filter \"macAddress\" to existing ScanFilter")
+    public void bleSetScanFilterDeviceAddress(
+            @RpcParameter(name = "macAddress")
+            String macAddress
+            ) {
+            mScanFilterBuilder.setDeviceAddress(macAddress);
+    }
+
+    /**
+     * Add filter "manufacturereDataId and/or manufacturerData" to existing ScanFilter
+     *
+     * @param manufacturerDataId the manufacturer data id to filter against
+     * @param manufacturerDataMask the manufacturere data mask to filter against
+     * @throws Exception
+     */
+    @Rpc(description = "Add filter \"manufacturereDataId and/or manufacturerData\" to existing ScanFilter")
+    public void bleSetScanFilterManufacturerData(
+            @RpcParameter(name = "manufacturerDataId")
+            Integer manufacturerDataId,
+            @RpcParameter(name = "manufacturerData")
+            String manufacturerData,
+            @RpcParameter(name = "manufacturerDataMask")
+            @RpcOptional
+            String manufacturerDataMask
+            ){
+        if (manufacturerDataMask != null) {
+            mScanFilterBuilder.setManufacturerData(manufacturerDataId,
+                    ConvertUtils.convertStringToByteArray(manufacturerData),
+                    ConvertUtils.convertStringToByteArray(manufacturerDataMask));
+        } else {
+            mScanFilterBuilder.setManufacturerData(manufacturerDataId,
+                    ConvertUtils.convertStringToByteArray(manufacturerData));
+        }
+    }
+
+    /**
+     * Add filter "serviceData and serviceDataMask" to existing ScanFilter
+     *
+     * @param serviceData the service data to filter against
+     * @param serviceDataMask the servie data mask to filter against
+     * @throws Exception
+     */
+    @Rpc(description = "Add filter \"serviceData and serviceDataMask\" to existing ScanFilter ")
+    public void bleSetScanFilterServiceData(
+            @RpcParameter(name = "serviceUuid")
+            String serviceUuid,
+            @RpcParameter(name = "serviceData")
+            String serviceData,
+            @RpcParameter(name = "serviceDataMask")
+            @RpcOptional
+            String serviceDataMask
+            ) {
+        if (serviceDataMask != null) {
+            mScanFilterBuilder
+                    .setServiceData(
+                            ParcelUuid.fromString(serviceUuid),
+                            ConvertUtils.convertStringToByteArray(serviceData),
+                            ConvertUtils.convertStringToByteArray(
+                                serviceDataMask));
+        } else {
+            mScanFilterBuilder.setServiceData(ParcelUuid.fromString(serviceUuid),
+                    ConvertUtils.convertStringToByteArray(serviceData));
+        }
+    }
+
+    /**
+     * Add filter "serviceUuid and/or serviceMask" to existing ScanFilter
+     *
+     * @param serviceUuid the service uuid to filter against
+     * @param serviceMask the service mask to filter against
+     * @throws Exception
+     */
+    @Rpc(description = "Add filter \"serviceUuid and/or serviceMask\" to existing ScanFilter")
+    public void bleSetScanFilterServiceUuid(
+            @RpcParameter(name = "serviceUuid")
+            String serviceUuid,
+            @RpcParameter(name = "serviceMask")
+            @RpcOptional
+            String serviceMask
+            ) {
+        if (serviceMask != null) {
+            mScanFilterBuilder
+                    .setServiceUuid(ParcelUuid.fromString(serviceUuid),
+                            ParcelUuid.fromString(serviceMask));
+        } else {
+            mScanFilterBuilder.setServiceUuid(ParcelUuid.fromString(serviceUuid));
+        }
+    }
+
+    /**
+     * Add filter "device name" to existing ScanFilter
+     *
+     * @param name the device name to filter against
+     * @throws Exception
+     */
+    @Rpc(description = "Sets the scan filter's device name")
+    public void bleSetScanFilterDeviceName(
+            @RpcParameter(name = "name")
+            String name
+            ) {
+            mScanFilterBuilder.setDeviceName(name);
+    }
+
+    @Rpc(description = "Set the scan setting's match mode")
+    public void bleSetScanSettingsMatchMode(
+            @RpcParameter(name = "mode") Integer mode) {
+        mScanSettingsBuilder.setMatchMode(mode);
+    }
+
+    @Rpc(description = "Get the scan setting's match mode")
+    public int bleGetScanSettingsMatchMode(
+            @RpcParameter(name = "scanSettingsIndex") Integer scanSettingsIndex
+            ) {
+        return mScanSettingsList.get(scanSettingsIndex).getMatchMode();
+    }
+
+    @Rpc(description = "Set the scan setting's number of matches")
+    public void bleSetScanSettingsNumOfMatches(
+            @RpcParameter(name = "matches") Integer matches) {
+        mScanSettingsBuilder.setNumOfMatches(matches);
+    }
+
+    @Rpc(description = "Get the scan setting's number of matches")
+    public int bleGetScanSettingsNumberOfMatches(
+            @RpcParameter(name = "scanSettingsIndex")
+            Integer scanSettingsIndex) {
+        return  mScanSettingsList.get(scanSettingsIndex).getNumOfMatches();
+    }
+
+    private class myScanCallback extends ScanCallback {
+        public Integer index;
+        String mEventType;
+        private final Bundle mResults;
+
+        public myScanCallback(Integer idx) {
+            index = idx;
+            mEventType = "BleScan";
+            mResults = new Bundle();
+        }
+
+        @Override
+        public void onScanFailed(int errorCode) {
+            String errorString = "UNKNOWN_ERROR_CODE";
+            if (errorCode == ScanCallback.SCAN_FAILED_ALREADY_STARTED) {
+                errorString = "SCAN_FAILED_ALREADY_STARTED";
+            } else if (errorCode == ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED) {
+                errorString = "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED";
+            } else if (errorCode == ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED) {
+                errorString = "SCAN_FAILED_FEATURE_UNSUPPORTED";
+            } else if (errorCode == ScanCallback.SCAN_FAILED_INTERNAL_ERROR) {
+                errorString = "SCAN_FAILED_INTERNAL_ERROR";
+            }
+            Log.d("bluetooth_le_scan change onScanFailed " + mEventType + " " + index + " error "
+                    + errorString);
+            mResults.putInt("ID", index);
+            mResults.putString("Type", "onScanFailed");
+            mResults.putInt("ErrorCode", errorCode);
+            mResults.putString("Error", errorString);
+            mEventFacade.postEvent(mEventType + index + "onScanFailed",
+                    mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onScanResult(int callbackType, ScanResult result) {
+            Log.d("bluetooth_le_scan change onUpdate " + mEventType + " " + index);
+            mResults.putInt("ID", index);
+            mResults.putInt("CallbackType", callbackType);
+            mResults.putString("Type", "onScanResult");
+            mResults.putParcelable("Result", result);
+            mEventFacade.postEvent(mEventType + index + "onScanResults", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onBatchScanResults(List<ScanResult> results) {
+            Log.d("reportResult " + mEventType + " " + index);
+            mResults.putLong("Timestamp", System.currentTimeMillis() / 1000);
+            mResults.putInt("ID", index);
+            mResults.putString("Type", "onBatchScanResults");
+            mResults.putParcelableList("Results", results);
+            mEventFacade.postEvent(mEventType + index + "onBatchScanResult", mResults.clone());
+            mResults.clear();
+        }
+    }
+
+    private class myLeScanCallback implements LeScanCallback {
+        public Integer index;
+        String mEventType;
+        private final Bundle mResults;
+
+        public myLeScanCallback(Integer idx) {
+            index = idx;
+            mEventType = "ClassicBleScan";
+            mResults = new Bundle();
+        }
+
+        @Override
+        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
+            Log.d("bluetooth_classic_le_scan " + mEventType + " " + index);
+            mResults.putParcelable("Device", device);
+            mResults.putInt("Rssi", rssi);
+            mResults.putString("ScanRecord", ConvertUtils.convertByteArrayToString(scanRecord));
+            mResults.putString("Type", "onLeScan");
+            mEventFacade.postEvent(mEventType + index + "onLeScan", mResults.clone());
+            mResults.clear();
+        }
+    }
+
+    @Override
+    public void shutdown() {
+      if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
+          for (myScanCallback mScanCallback : mScanCallbackList.values()) {
+              if (mScanCallback != null) {
+                  try {
+                    mBluetoothAdapter.getBluetoothLeScanner()
+                      .stopScan(mScanCallback);
+                  } catch (NullPointerException e) {
+                    Log.e("Failed to stop ble scan callback.", e);
+                  }
+              }
+          }
+          for (myLeScanCallback mLeScanCallback : mLeScanCallbackList.values()) {
+              if (mLeScanCallback != null) {
+                  try {
+                      mBluetoothAdapter.stopLeScan(mLeScanCallback);
+                  } catch (NullPointerException e) {
+                    Log.e("Failed to stop classic ble scan callback.", e);
+                  }
+              }
+          }
+      }
+      mScanCallbackList.clear();
+      mScanFilterList.clear();
+      mScanSettingsList.clear();
+      mLeScanCallbackList.clear();
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothMapFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothMapFacade.java
new file mode 100644
index 0000000..f1a593d
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothMapFacade.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import java.util.List;
+
+import android.app.Service;
+import android.bluetooth.BluetoothMap;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.os.ParcelUuid;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+public class BluetoothMapFacade extends RpcReceiver {
+  static final ParcelUuid[] MAP_UUIDS = {
+      BluetoothUuid.MAP,
+      BluetoothUuid.MNS,
+      BluetoothUuid.MAS,
+  };
+  private final Service mService;
+  private final BluetoothAdapter mBluetoothAdapter;
+
+  private static boolean sIsMapReady = false;
+  private static BluetoothMap sMapProfile = null;
+
+  public BluetoothMapFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+    mBluetoothAdapter.getProfileProxy(mService, new MapServiceListener(),
+        BluetoothProfile.MAP);
+  }
+
+  class MapServiceListener implements BluetoothProfile.ServiceListener {
+    @Override
+    public void onServiceConnected(int profile, BluetoothProfile proxy) {
+      sMapProfile = (BluetoothMap) proxy;
+      sIsMapReady = true;
+    }
+
+    @Override
+    public void onServiceDisconnected(int profile) {
+      sIsMapReady = false;
+    }
+  }
+
+  public Boolean mapDisconnect(BluetoothDevice device) {
+    if (sMapProfile.getPriority(device) > BluetoothProfile.PRIORITY_ON) {
+      sMapProfile.setPriority(device, BluetoothProfile.PRIORITY_ON);
+    }
+    return sMapProfile.disconnect(device);
+  }
+
+  @Rpc(description = "Is Map profile ready.")
+  public Boolean bluetoothMapIsReady() {
+    return sIsMapReady;
+  }
+
+  @Rpc(description = "Disconnect an MAP device.")
+  public Boolean bluetoothMapDisconnect(
+      @RpcParameter(name = "deviceID", description = "Name or MAC address of a device.")
+      String deviceID)
+      throws Exception {
+    if (sMapProfile == null) return false;
+    List<BluetoothDevice> connectedMapDevices = sMapProfile.getConnectedDevices();
+    Log.d("Connected map devices: " + connectedMapDevices);
+    BluetoothDevice mDevice = BluetoothFacade.getDevice(connectedMapDevices, deviceID);
+    if (!connectedMapDevices.isEmpty() && connectedMapDevices.get(0).equals(mDevice)) {
+        if (sMapProfile.getPriority(mDevice) > BluetoothProfile.PRIORITY_ON) {
+            sMapProfile.setPriority(mDevice, BluetoothProfile.PRIORITY_ON);
+        }
+        return sMapProfile.disconnect(mDevice);
+    } else {
+        return false;
+    }
+  }
+
+  @Rpc(description = "Get all the devices connected through MAP.")
+  public List<BluetoothDevice> bluetoothMapGetConnectedDevices() {
+    while (!sIsMapReady);
+    return sMapProfile.getDevicesMatchingConnectionStates(
+          new int[] {BluetoothProfile.STATE_CONNECTED,
+                     BluetoothProfile.STATE_CONNECTING,
+                     BluetoothProfile.STATE_DISCONNECTING});
+  }
+
+  @Rpc(description = "Get the currently connected remote Bluetooth device (PCE).")
+  public BluetoothDevice bluetoothMapGetClient() {
+    if (sMapProfile == null) { return null; }
+    return sMapProfile.getClient();
+  }
+
+  @Override
+  public void shutdown() {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothPairingHelper.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothPairingHelper.java
new file mode 100644
index 0000000..dbefd70
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothPairingHelper.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.googlecode.android_scripting.Log;
+
+public class BluetoothPairingHelper extends BroadcastReceiver {
+  public BluetoothPairingHelper() {
+    super();
+    Log.d("Pairing helper created.");
+  }
+  /**
+   * Blindly confirm bluetooth connection/bonding requests.
+   */
+  @Override
+  public void onReceive(Context c, Intent intent) {
+    String action = intent.getAction();
+    Log.d("Bluetooth pairing intent received: " + action);
+    BluetoothDevice mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+    if(action.equals(BluetoothDevice.ACTION_PAIRING_REQUEST)) {
+      int type = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR);
+      Log.d("Processing Action Paring Request with type " + type);
+      if(type == BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION ||
+         type == BluetoothDevice.PAIRING_VARIANT_CONSENT) {
+        mDevice.setPairingConfirmation(true);
+        Log.d("Connection confirmed");
+        abortBroadcast(); // Abort the broadcast so Settings app doesn't get it.
+      }
+    }
+    else if(action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST)) {
+      int type = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, BluetoothDevice.ERROR);
+      Log.d("Processing Action Connection Access Request type " + type);
+      if(type == BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS ||
+         type == BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS ||
+         type == BluetoothDevice.REQUEST_TYPE_PROFILE_CONNECTION) {
+    	  Intent newIntent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY);
+    	  String mReturnPackage = intent.getStringExtra(BluetoothDevice.EXTRA_PACKAGE_NAME);
+          String mReturnClass = intent.getStringExtra(BluetoothDevice.EXTRA_CLASS_NAME);
+          int mRequestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
+                  BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS);
+          if (mReturnPackage != null && mReturnClass != null) {
+              newIntent.setClassName(mReturnPackage, mReturnClass);
+          }
+    	  newIntent.putExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT,
+    			             BluetoothDevice.CONNECTION_ACCESS_YES);
+          newIntent.putExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, true);
+    	  newIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+    	  newIntent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, mRequestType);
+          Log.d("Sending connection access acceptance intent.");
+          abortBroadcast();
+          c.sendBroadcast(newIntent, android.Manifest.permission.BLUETOOTH_ADMIN);
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothRfcommFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothRfcommFacade.java
new file mode 100644
index 0000000..484fba5
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/BluetoothRfcommFacade.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.content.IntentFilter;
+import android.os.ParcelFileDescriptor;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.lang.reflect.Field;
+
+import org.apache.commons.codec.binary.Base64Codec;
+
+/**
+ * Bluetooth functions.
+ *
+ */
+// Discovery functions added by Eden Sayag
+
+public class BluetoothRfcommFacade extends RpcReceiver {
+
+  // UUID for SL4A.
+  private static final String DEFAULT_UUID = "457807c0-4897-11df-9879-0800200c9a66";
+  private static final String SDP_NAME = "SL4A";
+  private final Service mService;
+  private final BluetoothPairingHelper mPairingReceiver;
+  private final BluetoothAdapter mBluetoothAdapter;
+  private Map<String, BluetoothConnection>
+          connections = new HashMap<String, BluetoothConnection>();
+
+  public BluetoothRfcommFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mPairingReceiver = new BluetoothPairingHelper();
+    mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+  }
+
+  private BluetoothConnection getConnection(String connID) throws IOException {
+    BluetoothConnection conn = null;
+    if (connID.trim().length() > 0) {
+      conn = connections.get(connID);
+    } else if (connections.size() == 1) {
+      conn = (BluetoothConnection) connections.values().toArray()[0];
+    }
+    if (conn == null) {
+      throw new IOException("Bluetooth not ready for this connID.");
+    }
+    return conn;
+  }
+
+  private String addConnection(BluetoothConnection conn) {
+    String uuid = UUID.randomUUID().toString();
+    connections.put(uuid, conn);
+    conn.setUUID(uuid);
+    return uuid;
+  }
+
+  @Rpc(description = "Connect to a device over Bluetooth. "
+                   + "Blocks until the connection is established or fails.",
+       returns = "True if the connection was established successfully.")
+  public String bluetoothRfcommConnect(
+      @RpcParameter(name = "address", description = "The mac address of the device to connect to.")
+      String address,
+      @RpcParameter(name = "uuid",
+      description = "The UUID passed here must match the UUID used by the server device.")
+      @RpcDefault(DEFAULT_UUID)
+      String uuid)
+      throws IOException {
+    BluetoothDevice mDevice;
+    BluetoothSocket mSocket;
+    BluetoothConnection conn;
+    mDevice = mBluetoothAdapter.getRemoteDevice(address);
+    mSocket = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid));
+
+    // Register a broadcast receiver to bypass manual confirmation
+    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
+    mService.registerReceiver(mPairingReceiver, filter);
+
+    // Always cancel discovery because it will slow down a connection.
+    mBluetoothAdapter.cancelDiscovery();
+    mSocket.connect();
+    conn = new BluetoothConnection(mSocket);
+
+    mService.unregisterReceiver(mPairingReceiver);
+    return addConnection(conn);
+  }
+
+  @Rpc(description = "Returns active Bluetooth connections.")
+  public Map<String, String> bluetoothRfcommActiveConnections() {
+    Map<String, String> out = new HashMap<String, String>();
+    for (Map.Entry<String, BluetoothConnection> entry : connections.entrySet()) {
+      if (entry.getValue().isConnected()) {
+        out.put(entry.getKey(), entry.getValue().getRemoteBluetoothAddress());
+      }
+    }
+    return out;
+  }
+
+  @Rpc(description = "Returns the name of the connected device.")
+  public String bluetoothRfcommGetConnectedDeviceName(
+      @RpcParameter(name = "connID", description = "Connection id")
+      @RpcOptional @RpcDefault("")
+      String connID)
+      throws IOException {
+    BluetoothConnection conn = getConnection(connID);
+    return conn.getConnectedDeviceName();
+  }
+
+  @Rpc(description = "Listens for and accepts a Bluetooth connection."
+                   + "Blocks until the connection is established or fails.")
+  public String bluetoothRfcommAccept(
+      @RpcParameter(name = "uuid") @RpcDefault(DEFAULT_UUID) String uuid,
+      @RpcParameter(name = "timeout",
+                    description = "How long to wait for a new connection, 0 is wait for ever")
+      @RpcDefault("0") Integer timeout)
+      throws IOException {
+    Log.d("Accept bluetooth connection");
+    BluetoothServerSocket mServerSocket;
+    mServerSocket =
+        mBluetoothAdapter.listenUsingRfcommWithServiceRecord(SDP_NAME, UUID.fromString(uuid));
+    // Register a broadcast receiver to bypass manual confirmation
+    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
+    mService.registerReceiver(mPairingReceiver, filter);
+
+    BluetoothSocket mSocket = mServerSocket.accept(timeout.intValue());
+    BluetoothConnection conn = new BluetoothConnection(mSocket, mServerSocket);
+    mService.unregisterReceiver(mPairingReceiver);
+    return addConnection(conn);
+  }
+
+  @Rpc(description = "Sends ASCII characters over the currently open Bluetooth connection.")
+  public void bluetoothRfcommWrite(@RpcParameter(name = "ascii") String ascii,
+      @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") String connID)
+      throws IOException {
+    BluetoothConnection conn = getConnection(connID);
+    try {
+      conn.write(ascii);
+    } catch (IOException e) {
+      connections.remove(conn.getUUID());
+      throw e;
+    }
+  }
+
+  @Rpc(description = "Read up to bufferSize ASCII characters.")
+  public String bluetoothRfcommRead(
+      @RpcParameter(name = "bufferSize") @RpcDefault("4096") Integer bufferSize,
+      @RpcParameter(name = "connID", description = "Connection id") @RpcOptional @RpcDefault("")
+      String connID)
+      throws IOException {
+    BluetoothConnection conn = getConnection(connID);
+    try {
+      return conn.read(bufferSize);
+    } catch (IOException e) {
+      connections.remove(conn.getUUID());
+      throw e;
+    }
+  }
+
+  @Rpc(description = "Send bytes over the currently open Bluetooth connection.")
+  public void bluetoothRfcommWriteBinary(
+      @RpcParameter(name = "base64",
+                    description = "A base64 encoded String of the bytes to be sent.")
+      String base64,
+      @RpcParameter(name = "connID", description = "Connection id")
+      @RpcDefault("") @RpcOptional
+      String connID)
+      throws IOException {
+    BluetoothConnection conn = getConnection(connID);
+    try {
+      conn.write(Base64Codec.decodeBase64(base64));
+    } catch (IOException e) {
+      connections.remove(conn.getUUID());
+      throw e;
+    }
+  }
+
+  @Rpc(description = "Read up to bufferSize bytes and return a chunked, base64 encoded string.")
+  public String bluetoothRfcommReadBinary(
+      @RpcParameter(name = "bufferSize") @RpcDefault("4096") Integer bufferSize,
+      @RpcParameter(name = "connID", description = "Connection id")
+      @RpcDefault("") @RpcOptional
+      String connID)
+      throws IOException {
+
+    BluetoothConnection conn = getConnection(connID);
+    try {
+      return Base64Codec.encodeBase64String(conn.readBinary(bufferSize));
+    } catch (IOException e) {
+      connections.remove(conn.getUUID());
+      throw e;
+    }
+  }
+
+  @Rpc(description = "Returns True if the next read is guaranteed not to block.")
+  public Boolean bluetoothRfcommReadReady(
+      @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") @RpcOptional
+      String connID)
+      throws IOException {
+    BluetoothConnection conn = getConnection(connID);
+    try {
+      return conn.readReady();
+    } catch (IOException e) {
+      connections.remove(conn.getUUID());
+      throw e;
+    }
+  }
+
+  @Rpc(description = "Read the next line.")
+  public String bluetoothRfcommReadLine(
+      @RpcParameter(name = "connID", description = "Connection id") @RpcOptional @RpcDefault("")
+      String connID)
+      throws IOException {
+    BluetoothConnection conn = getConnection(connID);
+    try {
+      return conn.readLine();
+    } catch (IOException e) {
+      connections.remove(conn.getUUID());
+      throw e;
+    }
+  }
+
+  @Rpc(description = "Stops Bluetooth connection.")
+  public void bluetoothRfcommStop(
+      @RpcParameter
+      (name = "connID", description = "Connection id") @RpcOptional @RpcDefault("")
+      String connID) {
+    BluetoothConnection conn;
+    try {
+      conn = getConnection(connID);
+    } catch (IOException e) {
+      e.printStackTrace();
+      return;
+    }
+    if (conn == null) {
+      return;
+    }
+
+    conn.stop();
+    connections.remove(conn.getUUID());
+  }
+
+  @Override
+  public void shutdown() {
+    for (Map.Entry<String, BluetoothConnection> entry : connections.entrySet()) {
+      entry.getValue().stop();
+    }
+    connections.clear();
+  }
+}
+
+class BluetoothConnection {
+  private BluetoothSocket mSocket;
+  private BluetoothDevice mDevice;
+  private OutputStream mOutputStream;
+  private InputStream mInputStream;
+  private BufferedReader mReader;
+  private BluetoothServerSocket mServerSocket;
+  private String UUID;
+
+  public BluetoothConnection(BluetoothSocket mSocket) throws IOException {
+    this(mSocket, null);
+  }
+
+  public BluetoothConnection(BluetoothSocket mSocket, BluetoothServerSocket mServerSocket)
+      throws IOException {
+    this.mSocket = mSocket;
+    mOutputStream = mSocket.getOutputStream();
+    mInputStream = mSocket.getInputStream();
+    mDevice = mSocket.getRemoteDevice();
+    mReader = new BufferedReader(new InputStreamReader(mInputStream, "ASCII"));
+    this.mServerSocket = mServerSocket;
+  }
+
+  public void setUUID(String UUID) {
+    this.UUID = UUID;
+  }
+
+  public String getUUID() {
+    return UUID;
+  }
+
+  public String getRemoteBluetoothAddress() {
+    return mDevice.getAddress();
+  }
+
+  public boolean isConnected() {
+    if (mSocket == null) {
+      return false;
+    }
+    try {
+      mSocket.getRemoteDevice();
+      mInputStream.available();
+      mReader.ready();
+      return true;
+    } catch (Exception e) {
+      return false;
+    }
+  }
+
+  public void write(byte[] out) throws IOException {
+    if (mOutputStream != null) {
+      mOutputStream.write(out);
+    } else {
+      throw new IOException("Bluetooth not ready.");
+    }
+  }
+
+  public void write(String out) throws IOException {
+    this.write(out.getBytes());
+  }
+
+  public Boolean readReady() throws IOException {
+    if (mReader != null) {
+      return mReader.ready();
+    }
+    throw new IOException("Bluetooth not ready.");
+  }
+
+  public byte[] readBinary() throws IOException {
+    return this.readBinary(4096);
+  }
+
+  public byte[] readBinary(int bufferSize) throws IOException {
+    if (mReader != null) {
+      byte[] buffer = new byte[bufferSize];
+      int bytesRead = mInputStream.read(buffer);
+      if (bytesRead == -1) {
+        Log.e("Read failed.");
+        throw new IOException("Read failed.");
+      }
+      byte[] truncatedBuffer = new byte[bytesRead];
+      System.arraycopy(buffer, 0, truncatedBuffer, 0, bytesRead);
+      return truncatedBuffer;
+    }
+
+    throw new IOException("Bluetooth not ready.");
+
+  }
+
+  public String read() throws IOException {
+    return this.read(4096);
+  }
+
+  public String read(int bufferSize) throws IOException {
+    if (mReader != null) {
+      char[] buffer = new char[bufferSize];
+      int bytesRead = mReader.read(buffer);
+      if (bytesRead == -1) {
+        Log.e("Read failed.");
+        throw new IOException("Read failed.");
+      }
+      return new String(buffer, 0, bytesRead);
+    }
+    throw new IOException("Bluetooth not ready.");
+  }
+
+  public String readLine() throws IOException {
+    if (mReader != null) {
+      return mReader.readLine();
+    }
+    throw new IOException("Bluetooth not ready.");
+  }
+
+  public String getConnectedDeviceName() {
+    return mDevice.getName();
+  }
+
+  private synchronized void clearFileDescriptor() {
+    try {
+      Field field = BluetoothSocket.class.getDeclaredField("mPfd");
+      field.setAccessible(true);
+      ParcelFileDescriptor mPfd = (ParcelFileDescriptor) field.get(mSocket);
+      if (mPfd == null)
+        return;
+      mPfd.close();
+      mPfd = null;
+      try { field.set(mSocket, mPfd); }
+      catch(Exception e) {
+          Log.d("Exception setting mPfd = null in cleanCloseFix(): " + e.toString());
+      }
+    } catch (Exception e) {
+        Log.w("ParcelFileDescriptor could not be cleanly closed.", e);
+    }
+  }
+
+  public void stop() {
+    if (mSocket != null) {
+      try {
+        clearFileDescriptor();
+        mSocket.close();
+      } catch (IOException e) {
+        Log.e(e);
+      }
+    }
+    mSocket = null;
+    if (mServerSocket != null) {
+      try {
+        mServerSocket.close();
+      } catch (IOException e) {
+        Log.e(e);
+      }
+    }
+    mServerSocket = null;
+
+    if (mInputStream != null) {
+      try {
+        mInputStream.close();
+      } catch (IOException e) {
+        Log.e(e);
+      }
+    }
+    mInputStream = null;
+    if (mOutputStream != null) {
+      try {
+        mOutputStream.close();
+      } catch (IOException e) {
+        Log.e(e);
+      }
+    }
+    mOutputStream = null;
+    if (mReader != null) {
+      try {
+        mReader.close();
+      } catch (IOException e) {
+        Log.e(e);
+      }
+    }
+    mReader = null;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/GattClientFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/GattClientFacade.java
new file mode 100644
index 0000000..0a96cd9
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/GattClientFacade.java
@@ -0,0 +1,956 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.os.Bundle;
+
+import com.googlecode.android_scripting.ConvertUtils;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.MainThread;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+import com.googlecode.android_scripting.rpc.RpcStopEvent;
+
+public class GattClientFacade extends RpcReceiver {
+    private final EventFacade mEventFacade;
+    private BluetoothAdapter mBluetoothAdapter;
+    private BluetoothManager mBluetoothManager;
+    private final Service mService;
+    private final Context mContext;
+    private final HashMap<Integer, myBluetoothGattCallback> mGattCallbackList;
+    private final HashMap<Integer, BluetoothGatt> mBluetoothGattList;
+    private final HashMap<Integer, BluetoothGattCharacteristic> mCharacteristicList;
+    private final HashMap<Integer, BluetoothGattDescriptor> mDescriptorList;
+    private final HashMap<Integer, BluetoothGattService> mGattServiceList;
+    private final HashMap<Integer, List<BluetoothGattService>> mBluetoothGattDiscoveredServicesList;
+    private final HashMap<Integer, List<BluetoothDevice>> mGattServerDiscoveredDevicesList;
+    private static int GattCallbackCount;
+    private static int BluetoothGattDiscoveredServicesCount;
+    private static int BluetoothGattCount;
+    private static int CharacteristicCount;
+    private static int DescriptorCount;
+    private static int GattServerCallbackCount;
+    private static int GattServerCount;
+    private static int GattServiceCount;
+
+    public GattClientFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mContext = mService.getApplicationContext();
+        mBluetoothAdapter = MainThread.run(mService,
+                new Callable<BluetoothAdapter>() {
+                    @Override
+                    public BluetoothAdapter call() throws Exception {
+                        return BluetoothAdapter.getDefaultAdapter();
+                    }
+                });
+        mBluetoothManager = (BluetoothManager) mContext.getSystemService(Service.BLUETOOTH_SERVICE);
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mGattCallbackList = new HashMap<Integer, myBluetoothGattCallback>();
+        mCharacteristicList = new HashMap<Integer, BluetoothGattCharacteristic>();
+        mBluetoothGattList = new HashMap<Integer, BluetoothGatt>();
+        mDescriptorList = new HashMap<Integer, BluetoothGattDescriptor>();
+        mGattServiceList = new HashMap<Integer, BluetoothGattService>();
+        mBluetoothGattDiscoveredServicesList = new HashMap<Integer, List<BluetoothGattService>>();
+        mGattServerDiscoveredDevicesList = new HashMap<Integer, List<BluetoothDevice>>();
+    }
+
+    /**
+     * Create a BluetoothGatt connection
+     *
+     * @param index of the callback to start a connection on
+     * @param macAddress the mac address of the ble device
+     * @param autoConnect Whether to directly connect to the remote device (false) or to
+     *            automatically connect as soon as the remote device becomes available (true)
+     * @return the index of the BluetoothGatt object
+     * @throws Exception
+     */
+    @Rpc(description = "Create a gatt connection")
+    public int gattClientConnectGatt(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "macAddress")
+            String macAddress,
+            @RpcParameter(name = "autoConnect")
+            Boolean autoConnect
+            ) throws Exception {
+        if (mGattCallbackList.get(index) != null) {
+            BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(macAddress);
+            BluetoothGatt mBluetoothGatt = device.connectGatt(mService.getApplicationContext(),
+                    autoConnect,
+                    mGattCallbackList.get(index));
+            BluetoothGattCount += 1;
+            mBluetoothGattList.put(BluetoothGattCount, mBluetoothGatt);
+            return BluetoothGattCount;
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Trigger discovering of services on the BluetoothGatt object
+     *
+     * @param index The BluetoothGatt object index
+     * @return true, if the remote service discovery has been started
+     * @throws Exception
+     */
+    @Rpc(description = "Trigger discovering of services on the BluetoothGatt object")
+    public boolean gattClientDiscoverServices(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            return mBluetoothGattList.get(index).discoverServices();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Get the services from the BluetoothGatt object
+     *
+     * @param index The BluetoothGatt object index
+     * @return a list of BluetoothGattServices
+     * @throws Exception
+     */
+    @Rpc(description = "Get the services from the BluetoothGatt object")
+    public List<BluetoothGattService> gattClientGetServices(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            return mBluetoothGattList.get(index).getServices();
+        } else {
+            throw new Exception("Invalid index input:" + Integer.toString(index));
+        }
+    }
+
+    /**
+     * Abort reliable write of a bluetooth gatt
+     *
+     * @param index the bluetooth gatt index
+     * @throws Exception
+     */
+    @Rpc(description = "Abort reliable write of a bluetooth gatt")
+    public void gattClientAbortReliableWrite(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            mBluetoothGattList.get(index).abortReliableWrite();
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Begin reliable write of a bluetooth gatt
+     *
+     * @param index the bluetooth gatt index
+     * @return
+     * @throws Exception
+     */
+    @Rpc(description = "Begin reliable write of a bluetooth gatt")
+    public boolean gattClientBeginReliableWrite(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            return mBluetoothGattList.get(index).beginReliableWrite();
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Configure a bluetooth gatt's MTU
+     *
+     * @param index the bluetooth gatt index
+     * @param mtu the MTU to set
+     * @return
+     * @throws Exception
+     */
+    @Rpc(description = "true, if the new MTU value has been requested successfully")
+    public boolean gattClientRequestMtu(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "mtu")
+            Integer mtu
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            return mBluetoothGattList.get(index).requestMtu(mtu);
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Disconnect a bluetooth gatt
+     *
+     * @param index the bluetooth gatt index
+     * @throws Exception
+     */
+    @Rpc(description = "Disconnect a bluetooth gatt")
+    @RpcStopEvent("GattConnect")
+    public void gattClientDisconnect(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            mBluetoothGattList.get(index).disconnect();
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Execute reliable write on a bluetooth gatt
+     *
+     * @param index the bluetooth gatt index
+     * @return true, if the request to execute the transaction has been sent
+     * @throws Exception
+     */
+    @Rpc(description = "Execute reliable write on a bluetooth gatt")
+    public boolean gattExecuteReliableWrite(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            return mBluetoothGattList.get(index).executeReliableWrite();
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Get a list of Bluetooth Devices connnected to the bluetooth gatt
+     *
+     * @param index the bluetooth gatt index
+     * @return List of BluetoothDevice Objects
+     * @throws Exception
+     */
+    @Rpc(description = "Get a list of Bluetooth Devices connnected to the bluetooth gatt")
+    public List<BluetoothDevice> gattClientGetConnectedDevices(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            return mBluetoothGattList.get(index).getConnectedDevices();
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Get the remote bluetooth device this GATT client targets to
+     *
+     * @param index the bluetooth gatt index
+     * @return the remote bluetooth device this gatt client targets to
+     * @throws Exception
+     */
+    @Rpc(description = "Get the remote bluetooth device this GATT client targets to")
+    public BluetoothDevice gattGetDevice(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            return mBluetoothGattList.get(index).getDevice();
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Get the bluetooth devices matching input connection states
+     *
+     * @param index the bluetooth gatt index
+     * @param states the list of states to match
+     * @return The list of BluetoothDevice objects that match the states
+     * @throws Exception
+     */
+    @Rpc(description = "Get the bluetooth devices matching input connection states")
+    public List<BluetoothDevice> gattClientGetDevicesMatchingConnectionStates(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "states")
+            int[] states
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            return mBluetoothGattList.get(index).getDevicesMatchingConnectionStates(states);
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Get the service from an input UUID
+     *
+     * @param index the bluetooth gatt index
+     * @return BluetoothGattService related to the bluetooth gatt
+     * @throws Exception
+     */
+    @Rpc(description = "Get the service from an input UUID")
+    public ArrayList<String> gattClientGetServiceUuidList(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            ArrayList<String> serviceUuidList = new ArrayList<String>();
+            for (BluetoothGattService service : mBluetoothGattList.get(index).getServices()) {
+                serviceUuidList.add(service.getUuid().toString());
+            }
+            return serviceUuidList;
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Reads the requested characteristic from the associated remote device.
+     * @param gattIndex the BluetoothGatt server accociated with the device
+     * @param discoveredServiceListIndex the index returned from the discovered
+     * services callback
+     * @param serviceIndex the service index of the discovered services
+     * @param characteristicUuid the characteristic uuid to read
+     * @return true, if the read operation was initiated successfully
+     * @throws Exception
+     */
+    @Rpc(description = "Reads the requested characteristic from the associated remote device.")
+    public boolean gattClientReadCharacteristic(
+        @RpcParameter(name = "gattIndex") Integer gattIndex,
+        @RpcParameter(name = "discoveredServiceListIndex") Integer discoveredServiceListIndex,
+        @RpcParameter(name = "serviceIndex") Integer serviceIndex,
+        @RpcParameter(name = "characteristicUuid") String characteristicUuid) throws Exception {
+      BluetoothGatt bluetoothGatt = mBluetoothGattList.get(gattIndex);
+      if (bluetoothGatt == null) {
+        throw new Exception("Invalid gattIndex " + gattIndex);
+      }
+      List<BluetoothGattService> discoveredServiceList =
+          mBluetoothGattDiscoveredServicesList.get(discoveredServiceListIndex);
+      if (discoveredServiceList == null) {
+        throw new Exception("Invalid discoveredServiceListIndex " + discoveredServiceListIndex);
+      }
+      BluetoothGattService gattService = discoveredServiceList.get(serviceIndex);
+      if (gattService == null) {
+        throw new Exception("Invalid serviceIndex " + serviceIndex);
+      }
+      UUID cUuid = UUID.fromString(characteristicUuid);
+      BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(cUuid);
+      if (gattCharacteristic == null) {
+        throw new Exception("Invalid characteristic uuid: " + characteristicUuid);
+      }
+      return bluetoothGatt.readCharacteristic(gattCharacteristic);
+    }
+
+    /**
+     * Reads the value for a given descriptor from the associated remote device
+     * @param gattIndex - the gatt index to use
+     * @param discoveredServiceListIndex - the discvered serivice list index
+     * @param serviceIndex - the servce index of the discoveredServiceListIndex
+     * @param characteristicUuid - the characteristic uuid in which the descriptor is
+     * @param descriptorUuid - the descriptor uuid to read
+     * @return
+     * @throws Exception
+     */
+    @Rpc(description = "Reads the value for a given descriptor from the associated remote device")
+    public boolean gattClientReadDescriptor(@RpcParameter(name = "gattIndex") Integer gattIndex,
+        @RpcParameter(name = "discoveredServiceListIndex") Integer discoveredServiceListIndex,
+        @RpcParameter(name = "serviceIndex") Integer serviceIndex,
+        @RpcParameter(name = "characteristicUuid") String characteristicUuid,
+        @RpcParameter(name = "descriptorUuid") String descriptorUuid) throws Exception {
+      BluetoothGatt bluetoothGatt = mBluetoothGattList.get(gattIndex);
+      if (bluetoothGatt == null) {
+        throw new Exception("Invalid gattIndex " + gattIndex);
+      }
+      List<BluetoothGattService> gattServiceList = mBluetoothGattDiscoveredServicesList.get(
+          discoveredServiceListIndex);
+      if (gattServiceList == null) {
+        throw new Exception("Invalid discoveredServiceListIndex " + discoveredServiceListIndex);
+      }
+      BluetoothGattService gattService = gattServiceList.get(serviceIndex);
+      if (gattService == null) {
+        throw new Exception("Invalid serviceIndex " + serviceIndex);
+      }
+      UUID cUuid = UUID.fromString(characteristicUuid);
+      BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(cUuid);
+      if (gattCharacteristic == null) {
+        throw new Exception("Invalid characteristic uuid: " + characteristicUuid);
+      }
+      UUID dUuid = UUID.fromString(descriptorUuid);
+      BluetoothGattDescriptor gattDescriptor = gattCharacteristic.getDescriptor(dUuid);
+      if (gattDescriptor == null) {
+        throw new Exception("Invalid descriptor uuid: " + descriptorUuid);
+      }
+      return bluetoothGatt.readDescriptor(gattDescriptor);
+    }
+
+    /**
+     * Write the value of a given descriptor to the associated remote device
+     *
+     * @param index the bluetooth gatt index
+     * @param serviceIndex the service index to write to
+     * @param characteristicUuid the uuid where the descriptor lives
+     * @param descriptorIndex the descriptor index
+     * @return true, if the write operation was initiated successfully
+     * @throws Exception
+     */
+    @Rpc(description = "Write the value of a given descriptor to the associated remote device")
+    public boolean gattClientWriteDescriptor(@RpcParameter(name = "gattIndex") Integer gattIndex,
+        @RpcParameter(name = "discoveredServiceListIndex") Integer discoveredServiceListIndex,
+        @RpcParameter(name = "serviceIndex") Integer serviceIndex,
+        @RpcParameter(name = "characteristicUuid") String characteristicUuid,
+        @RpcParameter(name = "descriptorUuid") String descriptorUuid) throws Exception {
+      BluetoothGatt bluetoothGatt = mBluetoothGattList.get(gattIndex);
+      if (bluetoothGatt == null) {
+        throw new Exception("Invalid gattIndex " + gattIndex);
+      }
+      List<BluetoothGattService> discoveredServiceList =
+          mBluetoothGattDiscoveredServicesList.get(discoveredServiceListIndex);
+      if (discoveredServiceList == null) {
+        throw new Exception("Invalid discoveredServiceListIndex " + discoveredServiceListIndex);
+      }
+      BluetoothGattService gattService = discoveredServiceList.get(serviceIndex);
+      if (gattService == null) {
+        throw new Exception("Invalid serviceIndex " + serviceIndex);
+      }
+      UUID cUuid = UUID.fromString(characteristicUuid);
+      BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(cUuid);
+      if (gattCharacteristic == null) {
+        throw new Exception("Invalid characteristic uuid: " + characteristicUuid);
+      }
+      UUID dUuid = UUID.fromString(descriptorUuid);
+      BluetoothGattDescriptor gattDescriptor = gattCharacteristic.getDescriptor(dUuid);
+      if (gattDescriptor == null) {
+        throw new Exception("Invalid descriptor uuid: " + descriptorUuid);
+      }
+      return bluetoothGatt.writeDescriptor(gattDescriptor);
+    }
+
+    /**
+     * Write the value to a discovered descriptor.
+     * @param gattIndex - the gatt index to use
+     * @param discoveredServiceListIndex - the discovered service list index
+     * @param serviceIndex - the service index of the discoveredServiceListIndex
+     * @param characteristicUuid - the characteristic uuid in which the descriptor is
+     * @param descriptorUuid - the descriptor uuid to read
+     * @param value - the value to set the descriptor to
+     * @return true is the value was set to the descriptor
+     * @throws Exception
+     */
+    @Rpc(description = "Write the value of a given descriptor to the associated remote device")
+    public boolean gattClientDescriptorSetValue(@RpcParameter(name = "gattIndex") Integer gattIndex,
+        @RpcParameter(name = "discoveredServiceListIndex") Integer discoveredServiceListIndex,
+        @RpcParameter(name = "serviceIndex") Integer serviceIndex,
+        @RpcParameter(name = "characteristicUuid") String characteristicUuid,
+        @RpcParameter(name = "descriptorUuid") String descriptorUuid,
+        @RpcParameter(name = "value") String value) throws Exception {
+      if (mBluetoothGattList.get(gattIndex) == null) {
+        throw new Exception("Invalid gattIndex " + gattIndex);
+      }
+      List<BluetoothGattService> discoveredServiceList =
+          mBluetoothGattDiscoveredServicesList.get(discoveredServiceListIndex);
+      if (discoveredServiceList == null) {
+        throw new Exception("Invalid discoveredServiceListIndex " + discoveredServiceListIndex);
+      }
+      BluetoothGattService gattService = discoveredServiceList.get(serviceIndex);
+      if (gattService == null) {
+        throw new Exception("Invalid serviceIndex " + serviceIndex);
+      }
+      UUID cUuid = UUID.fromString(characteristicUuid);
+      BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(cUuid);
+      if (gattCharacteristic == null) {
+        throw new Exception("Invalid characteristic uuid: " + characteristicUuid);
+      }
+      UUID dUuid = UUID.fromString(descriptorUuid);
+      BluetoothGattDescriptor gattDescriptor = gattCharacteristic.getDescriptor(dUuid);
+      if (gattDescriptor == null) {
+        throw new Exception("Invalid descriptor uuid: " + descriptorUuid);
+      }
+        byte[] byteArray = ConvertUtils.convertStringToByteArray(value);
+        return gattDescriptor.setValue(byteArray);
+    }
+
+    /**
+     * Write the value of a given characteristic to the associated remote device
+     *
+     * @param index the bluetooth gatt index
+     * @param serviceIndex the service where the characteristic lives
+     * @param characteristicUuid the characteristic uuid to write to
+     * @return true, if the write operation was successful
+     * @throws Exception
+     */
+    @Rpc(description = "Write the value of a given characteristic to the associated remote device")
+    public boolean gattClientWriteCharacteristic(@RpcParameter(name = "gattIndex") Integer gattIndex,
+        @RpcParameter(name = "discoveredServiceListIndex") Integer discoveredServiceListIndex,
+        @RpcParameter(name = "serviceIndex") Integer serviceIndex,
+        @RpcParameter(name = "characteristicUuid") String characteristicUuid) throws Exception {
+      BluetoothGatt bluetoothGatt = mBluetoothGattList.get(gattIndex);
+      if (bluetoothGatt == null) {
+        throw new Exception("Invalid gattIndex " + gattIndex);
+      }
+      List<BluetoothGattService> discoveredServiceList =
+          mBluetoothGattDiscoveredServicesList.get(discoveredServiceListIndex);
+      if (discoveredServiceList == null) {
+        throw new Exception("Invalid discoveredServiceListIndex " + discoveredServiceListIndex);
+      }
+      BluetoothGattService gattService = discoveredServiceList.get(serviceIndex);
+      if (gattService == null) {
+        throw new Exception("Invalid serviceIndex " + serviceIndex);
+      }
+      UUID cUuid = UUID.fromString(characteristicUuid);
+      BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(cUuid);
+      if (gattCharacteristic == null) {
+        throw new Exception("Invalid characteristic uuid: " + characteristicUuid);
+      }
+      return bluetoothGatt.writeCharacteristic(gattCharacteristic);
+    }
+
+    /**
+     * Write the value to a discovered characteristic.
+     * @param gattIndex - the gatt index to use
+     * @param discoveredServiceListIndex - the discovered service list index
+     * @param serviceIndex - the service index of the discoveredServiceListIndex
+     * @param characteristicUuid - the characteristic uuid in which the descriptor is
+     * @param value - the value to set the characteristic to
+     * @return true, if the value was set to the characteristic
+     * @throws Exception
+     */
+    @Rpc(description = "Write the value of a given characteristic to the associated remote device")
+    public boolean gattClientCharacteristicSetValue(@RpcParameter(name = "gattIndex") Integer gattIndex,
+        @RpcParameter(name = "discoveredServiceListIndex") Integer discoveredServiceListIndex,
+        @RpcParameter(name = "serviceIndex") Integer serviceIndex,
+        @RpcParameter(name = "characteristicUuid") String characteristicUuid,
+        @RpcParameter(name = "value") String value) throws Exception {
+      if (mBluetoothGattList.get(gattIndex) == null) {
+        throw new Exception("Invalid gattIndex " + gattIndex);
+      }
+      List<BluetoothGattService> discoveredServiceList =
+          mBluetoothGattDiscoveredServicesList.get(discoveredServiceListIndex);
+      if (discoveredServiceList == null) {
+        throw new Exception("Invalid discoveredServiceListIndex " + discoveredServiceListIndex);
+      }
+      BluetoothGattService gattService = discoveredServiceList.get(serviceIndex);
+      if (gattService == null) {
+        throw new Exception("Invalid serviceIndex " + serviceIndex);
+      }
+      UUID cUuid = UUID.fromString(characteristicUuid);
+      BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(cUuid);
+      if (gattCharacteristic == null) {
+        throw new Exception("Invalid characteristic uuid: " + characteristicUuid);
+      }
+      byte[] byteArray = ConvertUtils.convertStringToByteArray(value);
+      return gattCharacteristic.setValue(byteArray);
+    }
+    /**
+     * Read the RSSI for a connected remote device
+     *
+     * @param index the bluetooth gatt index
+     * @return true, if the RSSI value has been requested successfully
+     * @throws Exception
+     */
+    @Rpc(description = "Read the RSSI for a connected remote device")
+    public boolean gattClientReadRSSI(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            return mBluetoothGattList.get(index).readRemoteRssi();
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Clears the internal cache and forces a refresh of the services from the remote device
+     *
+     * @param index the bluetooth gatt index
+     * @return Clears the internal cache and forces a refresh of the services from the remote
+     *         device.
+     * @throws Exception
+     */
+    @Rpc(description = "Clears the internal cache and forces a refresh of the services from the remote device")
+    public boolean gattClientRefresh(
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattList.get(index) != null) {
+            return mBluetoothGattList.get(index).refresh();
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Request a connection parameter update.
+     * @param index the bluetooth gatt index
+     * @param connectionPriority connection priority
+     * @return boolean True if successful False otherwise.
+     * @throws Exception
+     */
+    @Rpc(description = "Request a connection parameter update. from the Bluetooth Gatt")
+    public boolean gattClientRequestConnectionPriority(
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "connectionPriority")
+            Integer connectionPriority
+            ) throws Exception {
+        boolean result = false;
+        if (mBluetoothGattList.get(index) != null) {
+            result = mBluetoothGattList.get(index).requestConnectionPriority(
+                    connectionPriority);
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+        return result;
+    }
+
+    /**
+     * Sets the characteristic notification of a bluetooth gatt
+     *
+     * @param index the bluetooth gatt index
+     * @param characteristicIndex the characteristic index
+     * @param enable Enable or disable notifications/indications for a given characteristic
+     * @return true, if the requested notification status was set successfully
+     * @throws Exception
+     */
+    @Rpc(description = "Sets the characteristic notification of a bluetooth gatt")
+    public boolean gattClientSetCharacteristicNotification(
+            @RpcParameter(name = "gattIndex")
+            Integer gattIndex,
+            @RpcParameter(name = "discoveredServiceListIndex")
+            Integer discoveredServiceListIndex,
+            @RpcParameter(name = "serviceIndex")
+            Integer serviceIndex,
+            @RpcParameter(name = "characteristicUuid")
+            String characteristicUuid,
+            @RpcParameter(name = "enable")
+            Boolean enable
+            ) throws Exception {
+        if (mBluetoothGattList.get(gattIndex) != null) {
+            if(mBluetoothGattDiscoveredServicesList.get(discoveredServiceListIndex) != null) {
+                List<BluetoothGattService> discoveredServiceList =
+                    mBluetoothGattDiscoveredServicesList.get(discoveredServiceListIndex);
+                if (discoveredServiceList.get(serviceIndex) != null) {
+                    UUID cUuid = UUID.fromString(characteristicUuid);
+                    if (discoveredServiceList.get(serviceIndex).getCharacteristic(cUuid) != null) {
+                        return mBluetoothGattList.get(gattIndex).setCharacteristicNotification(
+                                discoveredServiceList.get(serviceIndex).getCharacteristic(cUuid), enable);
+                    } else {
+                        throw new Exception ("Invalid characteristic uuid: " + characteristicUuid);
+                    }
+                } else {
+                    throw new Exception ("Invalid serviceIndex " + serviceIndex);
+                }
+            } else {
+                throw new Exception("Invalid discoveredServiceListIndex: " + discoveredServiceListIndex);
+            }
+        } else {
+            throw new Exception("Invalid gattIndex input: " + gattIndex);
+        }
+    }
+
+    /**
+     * Create a new GattCallback object
+     *
+     * @return the index of the callback object
+     */
+    @Rpc(description = "Create a new GattCallback object")
+    public Integer gattCreateGattCallback() {
+        GattCallbackCount += 1;
+        int index = GattCallbackCount;
+        mGattCallbackList.put(index, new myBluetoothGattCallback(index));
+        return index;
+    }
+
+    /**
+     * Returns the list of discovered Bluetooth Gatt Services.
+     * @throws Exception
+     */
+    @Rpc(description = "Get Bluetooth Gatt Services")
+    public int gattClientGetDiscoveredServicesCount (
+            @RpcParameter(name = "index")
+            Integer index
+            ) throws Exception {
+        if (mBluetoothGattDiscoveredServicesList.get(index) != null) {
+            return mBluetoothGattDiscoveredServicesList.get(index).size();
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Returns the discovered Bluetooth Gatt Service Uuid.
+     * @throws Exception
+     */
+    @Rpc(description = "Get Bluetooth Gatt Service Uuid")
+    public String gattClientGetDiscoveredServiceUuid (
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "serviceIndex")
+            Integer serviceIndex
+            ) throws Exception {
+        List<BluetoothGattService> mBluetoothServiceList =
+            mBluetoothGattDiscoveredServicesList.get(index);
+        if (mBluetoothServiceList != null) {
+            return mBluetoothServiceList.get(serviceIndex).getUuid().toString();
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Get discovered characteristic uuids from the pheripheral device.
+     * @param index the index of the bluetooth gatt discovered services list
+     * @param serviceIndex the service to get
+     * @return the list of characteristic uuids
+     * @throws Exception
+     */
+    @Rpc(description = "Get Bluetooth Gatt Services")
+    public ArrayList<String> gattClientGetDiscoveredCharacteristicUuids (
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "serviceIndex")
+            Integer serviceIndex
+            ) throws Exception {
+        if (mBluetoothGattDiscoveredServicesList.get(index) != null) {
+            if (mBluetoothGattDiscoveredServicesList.get(index).get(serviceIndex) != null) {
+                ArrayList<String> uuidList = new ArrayList<String>();
+                List<BluetoothGattCharacteristic> charList = mBluetoothGattDiscoveredServicesList.get(index).get(serviceIndex).getCharacteristics();
+                for (BluetoothGattCharacteristic mChar : charList) {
+                    uuidList.add(mChar.getUuid().toString());
+                }
+                return uuidList;
+            } else {
+                throw new Exception("Invalid serviceIndex input:" + index);
+            }
+        } else {
+            throw new Exception("Invalid index input:" + index);
+        }
+    }
+
+    /**
+     * Get discovered descriptor uuids from the pheripheral device.
+     * @param index the discovered services list index
+     * @param serviceIndex the service index of the discovered services list
+     * @param characteristicUuid the characteristicUuid to select from the
+     * discovered service which contains the list of descriptors.
+     * @return the list of descriptor uuids
+     * @throws Exception
+     */
+    @Rpc(description = "Get Bluetooth Gatt Services")
+    public ArrayList<String> gattClientGetDiscoveredDescriptorUuids (
+            @RpcParameter(name = "index")
+            Integer index,
+            @RpcParameter(name = "serviceIndex")
+            Integer serviceIndex,
+            @RpcParameter(name = "characteristicUuid")
+            String characteristicUuid
+            ) throws Exception {
+        if (mBluetoothGattDiscoveredServicesList.get(index) != null) {
+            if (mBluetoothGattDiscoveredServicesList.get(index).get(serviceIndex) != null) {
+                BluetoothGattService service = mBluetoothGattDiscoveredServicesList.get(index).get(serviceIndex);
+                UUID cUuid = UUID.fromString(characteristicUuid);
+                if (service.getCharacteristic(cUuid) != null) {
+                    ArrayList<String> uuidList = new ArrayList<String>();
+                    for (BluetoothGattDescriptor mDesc : service.getCharacteristic(cUuid).getDescriptors()) {
+                        uuidList.add(mDesc.getUuid().toString());
+                    }
+                    return uuidList;
+                } else {
+                    throw new Exception("Invalid characeristicUuid : "
+                            + characteristicUuid);
+                }
+            } else {
+                throw new Exception("Invalid serviceIndex input:"
+                        + index);
+            }
+        } else {
+            throw new Exception("Invalid index input:"
+                    + index);
+        }
+    }
+
+    private class myBluetoothGattCallback extends BluetoothGattCallback {
+        private final Bundle mResults;
+        private final int index;
+        private final String mEventType;
+
+        public myBluetoothGattCallback(int idx) {
+            mResults = new Bundle();
+            mEventType = "GattConnect";
+            index = idx;
+        }
+
+        @Override
+        public void onConnectionStateChange(BluetoothGatt gatt, int status,
+                int newState) {
+            Log.d("gatt_connect change onConnectionStateChange " + mEventType + " " + index);
+            if (newState == BluetoothProfile.STATE_CONNECTED) {
+                Log.d("State Connected to mac address "
+                        + gatt.getDevice().getAddress() + " status " + status);
+            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+                Log.d("State Disconnected from mac address "
+                        + gatt.getDevice().getAddress() + " status " + status);
+            } else if (newState == BluetoothProfile.STATE_CONNECTING) {
+                Log.d("State Connecting to mac address "
+                        + gatt.getDevice().getAddress() + " status " + status);
+            } else if (newState == BluetoothProfile.STATE_DISCONNECTING) {
+                Log.d("State Disconnecting from mac address "
+                        + gatt.getDevice().getAddress() + " status " + status);
+            }
+            mResults.putInt("Status", status);
+            mResults.putInt("State", newState);
+            mEventFacade
+                    .postEvent(mEventType + index + "onConnectionStateChange", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+            Log.d("gatt_connect change onServicesDiscovered " + mEventType + " " + index);
+            int idx = BluetoothGattDiscoveredServicesCount++;
+            mBluetoothGattDiscoveredServicesList.put(idx, gatt.getServices());
+            mResults.putInt("ServicesIndex", idx);
+            mResults.putInt("Status", status);
+            for (BluetoothGattService se: gatt.getServices()) {
+                System.out.println("SWAG: " + se.getUuid().toString());
+            }
+            mEventFacade
+                    .postEvent(mEventType + index + "onServicesDiscovered", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onCharacteristicRead(BluetoothGatt gatt,
+                BluetoothGattCharacteristic characteristic,
+                int status) {
+            Log.d("gatt_connect change onCharacteristicRead " + mEventType + " " + index);
+            mResults.putInt("Status", status);
+            mResults.putString("CharacteristicUuid", characteristic.getUuid().toString());
+            mEventFacade
+                    .postEvent(mEventType + index + "onCharacteristicRead", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onCharacteristicWrite(BluetoothGatt gatt,
+                BluetoothGattCharacteristic characteristic, int status) {
+            Log.d("gatt_connect change onCharacteristicWrite " + mEventType + " " + index);
+            mResults.putInt("Status", status);
+            mResults.putString("CharacteristicUuid", characteristic.getUuid().toString());
+            mEventFacade
+                    .postEvent(mEventType + index + "onCharacteristicWrite", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onCharacteristicChanged(BluetoothGatt gatt,
+                BluetoothGattCharacteristic characteristic) {
+            Log.d("gatt_connect change onCharacteristicChanged " + mEventType + " " + index);
+            mResults.putInt("ID", index);
+            mResults.putString("CharacteristicUuid", characteristic.getUuid().toString());
+            mEventFacade
+                    .postEvent(mEventType + index + "onCharacteristicChanged", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
+                int status) {
+            Log.d("gatt_connect change onServicesDiscovered " + mEventType + " " + index);
+            mResults.putInt("Status", status);
+            mResults.putString("DescriptorUuid", descriptor.getUuid().toString());
+            mEventFacade
+                    .postEvent(mEventType + index + "onDescriptorRead", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
+                int status) {
+            Log.d("gatt_connect change onDescriptorWrite " + mEventType + " " + index);
+            mResults.putInt("ID", index);
+            mResults.putInt("Status", status);
+            mResults.putString("DescriptorUuid", descriptor.getUuid().toString());
+            mEventFacade
+                    .postEvent(mEventType + index + "onDescriptorWrite", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
+            Log.d("gatt_connect change onReliableWriteCompleted " + mEventType + " "
+                    + index);
+            mResults.putInt("Status", status);
+            mEventFacade
+                    .postEvent(mEventType + index + "onReliableWriteCompleted", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
+            Log.d("gatt_connect change onReadRemoteRssi " + mEventType + " " + index);
+            mResults.putInt("Status", status);
+            mResults.putInt("Rssi", rssi);
+            mEventFacade
+                    .postEvent(mEventType + index + "onReadRemoteRssi", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+            Log.d("gatt_connect change onMtuChanged " + mEventType + " " + index);
+            mResults.putInt("Status", status);
+            mResults.putInt("MTU", mtu);
+            mEventFacade
+                    .postEvent(mEventType + index + "onMtuChanged", mResults.clone());
+            mResults.clear();
+        }
+    }
+
+    @Override
+    public void shutdown() {
+        if (!mBluetoothGattList.isEmpty()) {
+            if (mBluetoothGattList.values() != null) {
+                for (BluetoothGatt mBluetoothGatt : mBluetoothGattList.values()) {
+                    mBluetoothGatt.close();
+                }
+            }
+        }
+        mGattCallbackList.clear();
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/bluetooth/GattServerFacade.java b/Common/src/com/googlecode/android_scripting/facade/bluetooth/GattServerFacade.java
new file mode 100644
index 0000000..df03fe2
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/bluetooth/GattServerFacade.java
@@ -0,0 +1,535 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.bluetooth;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.os.Bundle;
+
+import com.googlecode.android_scripting.ConvertUtils;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.MainThread;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+public class GattServerFacade extends RpcReceiver {
+  private final EventFacade mEventFacade;
+  private BluetoothAdapter mBluetoothAdapter;
+  private BluetoothManager mBluetoothManager;
+  private final Service mService;
+  private final Context mContext;
+  private final HashMap<Integer, BluetoothGattCharacteristic> mCharacteristicList;
+  private final HashMap<Integer, BluetoothGattDescriptor> mDescriptorList;
+  private final HashMap<Integer, BluetoothGattServer> mBluetoothGattServerList;
+  private final HashMap<Integer, myBluetoothGattServerCallback> mBluetoothGattServerCallbackList;
+  private final HashMap<Integer, BluetoothGattService> mGattServiceList;
+  private final HashMap<Integer, List<BluetoothGattService>> mBluetoothGattDiscoveredServicesList;
+  private final HashMap<Integer, List<BluetoothDevice>> mGattServerDiscoveredDevicesList;
+  private static int CharacteristicCount;
+  private static int DescriptorCount;
+  private static int GattServerCallbackCount;
+  private static int GattServerCount;
+  private static int GattServiceCount;
+
+  public GattServerFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mContext = mService.getApplicationContext();
+    mBluetoothAdapter = MainThread.run(mService, new Callable<BluetoothAdapter>() {
+      @Override
+      public BluetoothAdapter call() throws Exception {
+        return BluetoothAdapter.getDefaultAdapter();
+      }
+    });
+    mBluetoothManager = (BluetoothManager) mContext.getSystemService(Service.BLUETOOTH_SERVICE);
+    mEventFacade = manager.getReceiver(EventFacade.class);
+    mCharacteristicList = new HashMap<Integer, BluetoothGattCharacteristic>();
+    mDescriptorList = new HashMap<Integer, BluetoothGattDescriptor>();
+    mBluetoothGattServerList = new HashMap<Integer, BluetoothGattServer>();
+    mBluetoothGattServerCallbackList = new HashMap<Integer, myBluetoothGattServerCallback>();
+    mGattServiceList = new HashMap<Integer, BluetoothGattService>();
+    mBluetoothGattDiscoveredServicesList = new HashMap<Integer, List<BluetoothGattService>>();
+    mGattServerDiscoveredDevicesList = new HashMap<Integer, List<BluetoothDevice>>();
+  }
+
+  /**
+   * Open a new Gatt server.
+   *
+   * @param index the bluetooth gatt server callback to open on
+   * @return the index of the newly opened gatt server
+   * @throws Exception
+   */
+  @Rpc(description = "Open new gatt server")
+  public int gattServerOpenGattServer(@RpcParameter(name = "index") Integer index)
+      throws Exception {
+    if (mBluetoothGattServerCallbackList.get(index) != null) {
+      BluetoothGattServer mGattServer =
+          mBluetoothManager.openGattServer(mContext, mBluetoothGattServerCallbackList.get(index));
+      GattServerCount += 1;
+      int in = GattServerCount;
+      mBluetoothGattServerList.put(in, mGattServer);
+      return in;
+    } else {
+      throw new Exception("Invalid index input:" + Integer.toString(index));
+    }
+  }
+
+  /**
+   * Add a service to a bluetooth gatt server
+   *
+   * @param index the bluetooth gatt server to add a service to
+   * @param serviceIndex the service to add to the bluetooth gatt server
+   * @throws Exception
+   */
+  @Rpc(description = "Add service to bluetooth gatt server")
+  public void gattServerAddService(@RpcParameter(name = "index") Integer index,
+      @RpcParameter(name = "serviceIndex") Integer serviceIndex) throws Exception {
+    if (mBluetoothGattServerList.get(index) != null) {
+      if (mGattServiceList.get(serviceIndex) != null) {
+        mBluetoothGattServerList.get(index).addService(mGattServiceList.get(serviceIndex));
+      } else {
+        throw new Exception("Invalid serviceIndex input:" + Integer.toString(serviceIndex));
+      }
+    } else {
+      throw new Exception("Invalid index input:" + Integer.toString(index));
+    }
+  }
+
+  /**
+   * Get connected devices of the gatt server
+   *
+   * @param gattServerIndex the gatt server index
+   * @throws Exception
+   */
+  @Rpc(description = "Return a list of connected gatt devices.")
+  public List<BluetoothDevice> gattServerGetConnectedDevices(
+      @RpcParameter(name = "gattServerIndex") Integer gattServerIndex) throws Exception {
+    if (mBluetoothGattServerList.get(gattServerIndex) == null) {
+      throw new Exception("Invalid gattServerIndex: " + Integer.toString(gattServerIndex));
+    }
+    List<BluetoothDevice> connectedDevices =
+        mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
+    mGattServerDiscoveredDevicesList.put(gattServerIndex, connectedDevices);
+    return connectedDevices;
+  }
+
+  /**
+   * Get connected devices of the gatt server
+   *
+   * @param gattServerIndex the gatt server index
+   * @param bluetoothDeviceIndex the remotely connected bluetooth device
+   * @param requestId the ID of the request that was received with the callback
+   * @param status the status of the request to be sent to the remote devices
+   * @param offset value offset for partial read/write response
+   * @param value the value of the attribute that was read/written
+   * @throws Exception
+   */
+  @Rpc(description = "Send a response after a write.")
+  public void gattServerSendResponse(
+      @RpcParameter(name = "gattServerIndex") Integer gattServerIndex,
+      @RpcParameter(name = "bluetoothDeviceIndex") Integer bluetoothDeviceIndex,
+      @RpcParameter(name = "requestId") Integer requestId,
+      @RpcParameter(name = "status") Integer status, @RpcParameter(name = "offset") Integer offset,
+      @RpcParameter(name = "value") String value) throws Exception {
+
+    BluetoothGattServer gattServer = mBluetoothGattServerList.get(gattServerIndex);
+    if (gattServer == null)
+      throw new Exception("Invalid gattServerIndex: " + Integer.toString(gattServerIndex));
+    List<BluetoothDevice> connectedDevices = mGattServerDiscoveredDevicesList.get(gattServerIndex);
+    if (connectedDevices == null)
+      throw new Exception(
+          "Connected device list empty for gattServerIndex:" + Integer.toString(gattServerIndex));
+    BluetoothDevice bluetoothDevice = connectedDevices.get(bluetoothDeviceIndex);
+    if (bluetoothDevice == null)
+      throw new Exception(
+          "Invalid bluetoothDeviceIndex: " + Integer.toString(bluetoothDeviceIndex));
+    gattServer.sendResponse(bluetoothDevice, requestId, status, offset,
+        ConvertUtils.convertStringToByteArray(value));
+  }
+
+  /**
+   * Create a new bluetooth gatt service
+   *
+   * @param uuid the UUID that characterises the service
+   * @param serviceType the service type
+   * @return The index of the new bluetooth gatt service
+   */
+  @Rpc(description = "Create new bluetooth gatt service")
+  public int gattServerCreateService(@RpcParameter(name = "uuid") String uuid,
+      @RpcParameter(name = "serviceType") Integer serviceType) {
+    GattServiceCount += 1;
+    int index = GattServiceCount;
+    mGattServiceList.put(index, new BluetoothGattService(UUID.fromString(uuid), serviceType));
+    return index;
+  }
+
+  /**
+   * Add a characteristic to a bluetooth gatt service
+   *
+   * @param index the bluetooth gatt service index
+   * @param serviceUuid the service Uuid to get
+   * @param characteristicIndex the character index to use
+   * @throws Exception
+   */
+  @Rpc(description = "Add a characteristic to a bluetooth gatt service")
+  public void gattServiceAddCharacteristic(@RpcParameter(name = "index") Integer index,
+      @RpcParameter(name = "serviceUuid") String serviceUuid,
+      @RpcParameter(name = "characteristicIndex") Integer characteristicIndex) throws Exception {
+    if (mBluetoothGattServerList.get(index) != null
+        && mBluetoothGattServerList.get(index).getService(UUID.fromString(serviceUuid)) != null
+        && mCharacteristicList.get(characteristicIndex) != null) {
+      mBluetoothGattServerList.get(index).getService(UUID.fromString(serviceUuid))
+          .addCharacteristic(mCharacteristicList.get(characteristicIndex));
+    } else {
+      if (mBluetoothGattServerList.get(index) == null) {
+        throw new Exception("Invalid index input:" + index);
+      } else if (mCharacteristicList.get(characteristicIndex) == null) {
+        throw new Exception("Invalid characteristicIndex input:" + characteristicIndex);
+      } else {
+        throw new Exception("Invalid serviceUuid input:" + serviceUuid);
+      }
+    }
+  }
+
+  /**
+   * Add a characteristic to a bluetooth gatt service
+   *
+   * @param index the bluetooth gatt service to add a characteristic to
+   * @param characteristicIndex the characteristic to add
+   * @throws Exception
+   */
+  @Rpc(description = "Add a characteristic to a bluetooth gatt service")
+  public void gattServerAddCharacteristicToService(@RpcParameter(name = "index") Integer index,
+      @RpcParameter(name = "characteristicIndex") Integer characteristicIndex
+
+  ) throws Exception {
+    if (mGattServiceList.get(index) != null) {
+      if (mCharacteristicList.get(characteristicIndex) != null) {
+        mGattServiceList.get(index).addCharacteristic(mCharacteristicList.get(characteristicIndex));
+      } else {
+        throw new Exception("Invalid index input:" + index);
+      }
+    } else {
+      throw new Exception("Invalid index input:" + index);
+    }
+  }
+
+  /**
+   * Close a bluetooth gatt
+   *
+   * @param index the bluetooth gatt index to close
+   * @throws Exception
+   */
+  @Rpc(description = "Close a bluetooth gatt")
+  public void gattServerClose(@RpcParameter(name = "index") Integer index) throws Exception {
+    if (mBluetoothGattServerList.get(index) != null) {
+      mBluetoothGattServerList.get(index).close();
+    } else {
+      throw new Exception("Invalid index input:" + index);
+    }
+  }
+
+  /**
+   * Get a list of Bluetooth Devices connnected to the bluetooth gatt
+   *
+   * @param index the bluetooth gatt index
+   * @return List of BluetoothDevice Objects
+   * @throws Exception
+   */
+  @Rpc(description = "Get a list of Bluetooth Devices connnected to the bluetooth gatt")
+  public List<BluetoothDevice> gattGetConnectedDevices(@RpcParameter(name = "index") Integer index)
+      throws Exception {
+    if (mBluetoothGattServerList.get(index) != null) {
+      return mBluetoothGattServerList.get(index).getConnectedDevices();
+    } else {
+      throw new Exception("Invalid index input:" + index);
+    }
+  }
+
+  /**
+   * Get the service from an input UUID
+   *
+   * @param index the bluetooth gatt index
+   * @return BluetoothGattService related to the bluetooth gatt
+   * @throws Exception
+   */
+  @Rpc(description = "Get the service from an input UUID")
+  public ArrayList<String> gattGetServiceUuidList(@RpcParameter(name = "index") Integer index)
+      throws Exception {
+    if (mBluetoothGattServerList.get(index) != null) {
+      ArrayList<String> serviceUuidList = new ArrayList<String>();
+      for (BluetoothGattService service : mBluetoothGattServerList.get(index).getServices()) {
+        serviceUuidList.add(service.getUuid().toString());
+      }
+      return serviceUuidList;
+    } else {
+      throw new Exception("Invalid index input:" + index);
+    }
+  }
+
+  /**
+   * Get the service from an input UUID
+   *
+   * @param index the bluetooth gatt index
+   * @param uuid the String uuid that matches the service
+   * @return BluetoothGattService related to the bluetooth gatt
+   * @throws Exception
+   */
+  @Rpc(description = "Get the service from an input UUID")
+  public BluetoothGattService gattGetService(@RpcParameter(name = "index") Integer index,
+      @RpcParameter(name = "uuid") String uuid) throws Exception {
+    if (mBluetoothGattServerList.get(index) != null) {
+      return mBluetoothGattServerList.get(index).getService(UUID.fromString(uuid));
+    } else {
+      throw new Exception("Invalid index input:" + index);
+    }
+  }
+
+  /**
+   * Add a descriptor to a bluetooth gatt characteristic
+   *
+   * @param index the bluetooth gatt characteristic to add a descriptor to
+   * @param descriptorIndex the descritor index to add to the characteristic
+   * @throws Exception
+   */
+  @Rpc(description = "add descriptor to blutooth gatt characteristic")
+  public void gattServerCharacteristicAddDescriptor(@RpcParameter(name = "index") Integer index,
+      @RpcParameter(name = "descriptorIndex") Integer descriptorIndex) throws Exception {
+    if (mCharacteristicList.get(index) != null) {
+      if (mDescriptorList.get(descriptorIndex) != null) {
+        mCharacteristicList.get(index).addDescriptor(mDescriptorList.get(descriptorIndex));
+      } else {
+        throw new Exception("Invalid descriptorIndex input:" + descriptorIndex);
+      }
+    } else {
+      throw new Exception("Invalid index input:" + index);
+    }
+  }
+
+  /**
+   * Create a new Characteristic object
+   *
+   * @param characteristicUuid uuid The UUID for this characteristic
+   * @param property Properties of this characteristic
+   * @param permission permissions Permissions for this characteristic
+   * @return
+   */
+  @Rpc(description = "Create a new Characteristic object")
+  public int gattServerCreateBluetoothGattCharacteristic(
+      @RpcParameter(name = "characteristicUuid") String characteristicUuid,
+      @RpcParameter(name = "property") Integer property,
+      @RpcParameter(name = "permission") Integer permission) {
+    CharacteristicCount += 1;
+    int index = CharacteristicCount;
+    BluetoothGattCharacteristic characteristic =
+        new BluetoothGattCharacteristic(UUID.fromString(characteristicUuid), property, permission);
+    mCharacteristicList.put(index, characteristic);
+    return index;
+  }
+
+  /**
+   * Create a new GattCallback object
+   *
+   * @return the index of the callback object
+   */
+  @Rpc(description = "Create a new GattCallback object")
+  public Integer gattServerCreateGattServerCallback() {
+    GattServerCallbackCount += 1;
+    int index = GattServerCallbackCount;
+    mBluetoothGattServerCallbackList.put(index, new myBluetoothGattServerCallback(index));
+    return index;
+  }
+
+  /**
+   * Create a new Descriptor object
+   *
+   * @param descriptorUuid the UUID for this descriptor
+   * @param permissions Permissions for this descriptor
+   * @return the index of the Descriptor object
+   */
+  @Rpc(description = "Create a new Descriptor object")
+  public int gattServerCreateBluetoothGattDescriptor(
+      @RpcParameter(name = "descriptorUuid") String descriptorUuid,
+      @RpcParameter(name = "permissions") Integer permissions) {
+    DescriptorCount += 1;
+    int index = DescriptorCount;
+    BluetoothGattDescriptor descriptor =
+        new BluetoothGattDescriptor(UUID.fromString(descriptorUuid), permissions);
+    mDescriptorList.put(index, descriptor);
+    return index;
+  }
+
+  private class myBluetoothGattServerCallback extends BluetoothGattServerCallback {
+    private final Bundle mResults;
+    private final int index;
+    private final String mEventType;
+
+    public myBluetoothGattServerCallback(int idx) {
+      mResults = new Bundle();
+      mEventType = "GattServer";
+      index = idx;
+    }
+
+    @Override
+    public void onServiceAdded(int status, BluetoothGattService service) {
+      Log.d("gatt_server change onServiceAdded " + mEventType + " " + index);
+      mResults.putString("serviceUuid", service.getUuid().toString());
+      mResults.putInt("instanceId", service.getInstanceId());
+      mEventFacade.postEvent(mEventType + index + "onServiceAdded", mResults.clone());
+      mResults.clear();
+    }
+
+    @Override
+    public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
+        BluetoothGattCharacteristic characteristic) {
+      Log.d("gatt_server change onCharacteristicReadRequest " + mEventType + " " + index);
+      mResults.putInt("requestId", requestId);
+      mResults.putInt("offset", offset);
+      mResults.putInt("instanceId", characteristic.getInstanceId());
+      mResults.putInt("properties", characteristic.getProperties());
+      mResults.putString("uuid", characteristic.getUuid().toString());
+      mResults.putInt("permissions", characteristic.getPermissions());
+      mEventFacade.postEvent(mEventType + index + "onCharacteristicReadRequest", mResults.clone());
+      mResults.clear();
+    }
+
+    @Override
+    public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
+        BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded,
+        int offset, byte[] value) {
+      Log.d("gatt_server change onCharacteristicWriteRequest " + mEventType + " " + index);
+      mResults.putInt("requestId", requestId);
+      mResults.putInt("offset", offset);
+      mResults.putParcelable("BluetoothDevice", device);
+      mResults.putBoolean("preparedWrite", preparedWrite);
+      mResults.putBoolean("responseNeeded", responseNeeded);
+      mResults.putString("value", ConvertUtils.convertByteArrayToString(value));
+      mResults.putInt("instanceId", characteristic.getInstanceId());
+      mResults.putInt("properties", characteristic.getProperties());
+      mResults.putString("uuid", characteristic.getUuid().toString());
+      mResults.putInt("permissions", characteristic.getPermissions());
+      mEventFacade.postEvent(mEventType + index + "onCharacteristicWriteRequest", mResults.clone());
+      mResults.clear();
+
+    }
+
+    @Override
+    public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
+        BluetoothGattDescriptor descriptor) {
+      Log.d("gatt_server change onDescriptorReadRequest " + mEventType + " " + index);
+      mResults.putInt("requestId", requestId);
+      mResults.putInt("offset", offset);
+      mResults.putParcelable("BluetoothDevice", device);
+      mResults.putInt("instanceId", descriptor.getInstanceId());
+      mResults.putInt("permissions", descriptor.getPermissions());
+      mResults.putString("uuid", descriptor.getUuid().toString());
+      mEventFacade.postEvent(mEventType + index + "onDescriptorReadRequest", mResults.clone());
+      mResults.clear();
+    }
+
+    @Override
+    public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
+        BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded,
+        int offset, byte[] value) {
+      Log.d("gatt_server change onDescriptorWriteRequest " + mEventType + " " + index);
+      mResults.putInt("requestId", requestId);
+      mResults.putInt("offset", offset);
+      mResults.putParcelable("BluetoothDevice", device);
+      mResults.putBoolean("preparedWrite", preparedWrite);
+      mResults.putBoolean("responseNeeded", responseNeeded);
+      mResults.putString("value", ConvertUtils.convertByteArrayToString(value));
+      mResults.putInt("instanceId", descriptor.getInstanceId());
+      mResults.putInt("permissions", descriptor.getPermissions());
+      mResults.putString("uuid", descriptor.getUuid().toString());
+      mEventFacade.postEvent(mEventType + index + "onDescriptorWriteRequest", mResults.clone());
+      mResults.clear();
+    }
+
+    @Override
+    public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
+      Log.d("gatt_server change onExecuteWrite " + mEventType + " " + index);
+      mResults.putParcelable("BluetoothDevice", device);
+      mResults.putInt("requestId", requestId);
+      mResults.putBoolean("execute", execute);
+      mEventFacade.postEvent(mEventType + index + "onExecuteWrite", mResults.clone());
+      mResults.clear();
+    }
+
+    @Override
+    public void onNotificationSent(BluetoothDevice device, int status) {
+      Log.d("gatt_server change onNotificationSent " + mEventType + " " + index);
+      mResults.putParcelable("BluetoothDevice", device);
+      mResults.putInt("status", status);
+      mEventFacade.postEvent(mEventType + index + "onNotificationSent", mResults.clone());
+      mResults.clear();
+    }
+
+    @Override
+    public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
+      Log.d("gatt_server change onConnectionStateChange " + mEventType + " " + index);
+      if (newState == BluetoothProfile.STATE_CONNECTED) {
+        Log.d("State Connected to mac address " + device.getAddress() + " status " + status);
+      } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+        Log.d("State Disconnected from mac address " + device.getAddress() + " status " + status);
+      }
+      mResults.putParcelable("BluetoothDevice", device);
+      mResults.putInt("status", status);
+      mResults.putInt("newState", newState);
+      mEventFacade.postEvent(mEventType + index + "onConnectionStateChange", mResults.clone());
+      mResults.clear();
+    }
+
+    @Override
+    public void onMtuChanged(BluetoothDevice device, int mtu) {
+      Log.d("gatt_server change onMtuChanged " + mEventType + " " + index);
+      mResults.putParcelable("BluetoothDevice", device);
+      mResults.putInt("mtu", mtu);
+      mEventFacade.postEvent(mEventType + index + "onMtuChanged", mResults.clone());
+      mResults.clear();
+    }
+  }
+
+  @Override
+  public void shutdown() {
+    if (!mBluetoothGattServerList.isEmpty()) {
+      if (mBluetoothGattServerList.values() != null) {
+        for (BluetoothGattServer mBluetoothGattServer : mBluetoothGattServerList.values()) {
+          mBluetoothGattServer.close();
+        }
+      }
+    }
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/media/AudioManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/media/AudioManagerFacade.java
new file mode 100644
index 0000000..b611678
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/media/AudioManagerFacade.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.media;
+
+import android.app.Service;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+
+public class AudioManagerFacade extends RpcReceiver {
+
+    private final Service mService;
+    private final EventFacade mEventFacade;
+    private final AudioManager mAudio;
+    private final OnAudioFocusChangeListener mFocusChangeListener;
+    private boolean mIsFocused;
+
+    public AudioManagerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mAudio = (AudioManager) mService.getSystemService(Context.AUDIO_SERVICE);
+        mFocusChangeListener = new OnAudioFocusChangeListener() {
+            public void onAudioFocusChange(int focusChange) {
+                if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+                    mIsFocused = false;
+                } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+                    mIsFocused = true;
+                }
+            }
+        };
+    }
+
+    @Rpc(description = "Checks whether any music is active.")
+    public Boolean audioIsMusicActive() {
+        return mAudio.isMusicActive();
+    }
+
+    @Rpc(description = "Checks whether A2DP audio routing to the Bluetooth headset is on or off.")
+    public Boolean audioIsBluetoothA2dpOn() {
+        return mAudio.isBluetoothA2dpOn();
+    }
+
+    @Rpc(description = "Request audio focus for sl4a.")
+    public Boolean audioRequestAudioFocus() {
+        int status = mAudio.requestAudioFocus(mFocusChangeListener,
+                                              AudioManager.STREAM_MUSIC,
+                                              AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
+        if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            mIsFocused = true;
+            return true;
+        }
+        mIsFocused = false;
+        return false;
+    }
+
+    @Rpc(description = "Whether sl4a has the audio focus or not.")
+    public Boolean audioIsFocused() {
+        return mIsFocused;
+    }
+
+    @Override
+    public void shutdown() {
+        mAudio.abandonAudioFocus(mFocusChangeListener);
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/media/MediaButtonCallback.java b/Common/src/com/googlecode/android_scripting/facade/media/MediaButtonCallback.java
new file mode 100644
index 0000000..fc7c11e
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/media/MediaButtonCallback.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.media;
+
+import android.content.Intent;
+import android.media.session.MediaSession;
+import android.os.Bundle;
+import android.view.KeyEvent;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.EventFacade;
+
+public class MediaButtonCallback extends MediaSession.Callback {
+    private final EventFacade mEventFacade;
+    public MediaButtonCallback(EventFacade eventFacade) {
+        this.mEventFacade = eventFacade;
+    }
+    private void handleKeyEvent(KeyEvent event) {
+        int keyCode = event.getKeyCode();
+        Log.d("Received ACTION_DOWN with keycode " + keyCode);
+        Bundle msg = new Bundle();
+        if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+            msg.putString("ButtonPressed", "Play");
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+            msg.putString("ButtonPressed", "Pause");
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
+            msg.putString("ButtonPressed", "PlayPause");
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP) {
+            msg.putString("ButtonPressed", "Stop");
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_NEXT) {
+            msg.putString("ButtonPressed", "Next");
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS) {
+            msg.putString("ButtonPressed", "Previous");
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
+            msg.putString("ButtonPressed", "Forward");
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
+            msg.putString("ButtonPressed", "Rewind");
+        }
+        Log.d("Sending MediaButton event with ButtonPressed value "
+              + msg.getString("ButtonPressed"));
+        this.mEventFacade.postEvent("MediaButton", msg);
+    }
+
+    @Override
+    public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
+        String action = mediaButtonIntent.getAction();
+        Log.d("Received intent with action " + action);
+        if (action.equals(Intent.ACTION_MEDIA_BUTTON)) {
+            KeyEvent event = (KeyEvent) mediaButtonIntent
+                                        .getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+            int keyAction = event.getAction();
+            Log.d("Received KeyEvent with action " + keyAction);
+            if (keyAction == KeyEvent.ACTION_DOWN) {
+                handleKeyEvent(event);
+            } else if (keyAction == KeyEvent.ACTION_UP) {
+                handleKeyEvent(event);
+            }
+            return true;
+        }
+        return super.onMediaButtonEvent(mediaButtonIntent);
+    }
+}
\ No newline at end of file
diff --git a/Common/src/com/googlecode/android_scripting/facade/media/MediaPlayerFacade.java b/Common/src/com/googlecode/android_scripting/facade/media/MediaPlayerFacade.java
new file mode 100644
index 0000000..3925a38
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/media/MediaPlayerFacade.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.media;
+
+import android.app.Service;
+import android.media.MediaPlayer;
+import android.net.Uri;
+
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * This facade exposes basic mediaPlayer functionality. <br>
+ * <br>
+ * <b>Usage Notes:</b><br>
+ * mediaPlayerFacade maintains a list of media streams, identified by a user supplied tag. If the
+ * tag is null or blank, this tag defaults to "default"<br>
+ * Basic operation is: mediaPlayOpen("file:///sdcard/MP3/sample.mp3","mytag",true)<br>
+ * This will look for a media file at /sdcard/MP3/sample.mp3. Other urls should work. If the file
+ * exists and is playable, this will return a true otherwise it will return a false. <br>
+ * If play=true, then the media file will play immediately, otherwise it will wait for a
+ * {@link #mediaPlayStart mediaPlayerStart} command. <br>
+ * When done with the resource, use {@link #mediaPlayClose mediaPlayClose} <br>
+ * You can get information about the loaded media with {@link #mediaPlayInfo mediaPlayInfo} This
+ * returns a map with the following elements:
+ * <ul>
+ * <li>"tag" - user supplied tag identifying this mediaPlayer.
+ * <li>"loaded" - true if loaded, false if not. If false, no other elements are returned.
+ * <li>"duration" - length of the media in milliseconds.
+ * <li>"position" - current position of playback in milliseconds. Controlled by
+ * {@link #mediaPlaySeek mediaPlaySeek}
+ * <li>"isplaying" - shows whether media is playing. Controlled by {@link #mediaPlayPause
+ * mediaPlayPause} and {@link #mediaPlayStart mediaPlayStart}
+ * <li>"url" - the url used to open this media.
+ * <li>"looping" - whether media will loop. Controlled by {@link #mediaPlaySetLooping
+ * mediaPlaySetLooping}
+ * </ul>
+ * <br>
+ * You can use {@link #mediaPlayList mediaPlayList} to get a list of the loaded tags. <br>
+ * {@link #mediaIsPlaying mediaIsPlaying} will return true if the media is playing.<br>
+ * <b>Events:</b><br>
+ * A playing media will throw a <b>"media"</b> event on completion. NB: In remote mode, a media file
+ * will continue playing after the script has finished unless an explicit {@link #mediaPlayClose
+ * mediaPlayClose} event is called.
+ *
+ * @author Robbie Matthews (rjmatthews62@gmail.com)
+ */
+
+public class MediaPlayerFacade extends RpcReceiver implements MediaPlayer.OnCompletionListener {
+
+    private final Service mService;
+    static private final Map<String, MediaPlayer> mPlayers = new Hashtable<String, MediaPlayer>();
+    static private final Map<String, String> mUrls = new Hashtable<String, String>();
+
+    private final EventFacade mEventFacade;
+
+    public MediaPlayerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mEventFacade = manager.getReceiver(EventFacade.class);
+    }
+
+    private String getDefault(String tag) {
+        return (tag == null || tag.equals("")) ? "default" : tag;
+    }
+
+    private MediaPlayer getPlayer(String tag) {
+        tag = getDefault(tag);
+        return mPlayers.get(tag);
+    }
+
+    private String getUrl(String tag) {
+        tag = getDefault(tag);
+        return mUrls.get(tag);
+    }
+
+    private void putMp(String tag, MediaPlayer player, String url) {
+        tag = getDefault(tag);
+        mPlayers.put(tag, player);
+        mUrls.put(tag, url);
+    }
+
+    private void removeMp(String tag) {
+        tag = getDefault(tag);
+        MediaPlayer player = mPlayers.get(tag);
+        if (player != null) {
+            player.stop();
+            player.release();
+        }
+        mPlayers.remove(tag);
+        mUrls.remove(tag);
+    }
+
+    @Rpc(description = "Open a media file", returns = "true if play successful")
+    public synchronized boolean mediaPlayOpen(@RpcParameter(name = "url",
+                                                            description = "url of media resource")
+    String url, @RpcParameter(name = "tag", description = "string identifying resource")
+    @RpcDefault(value = "default")
+    String tag, @RpcParameter(name = "play", description = "start playing immediately")
+    @RpcDefault(value = "true")
+    Boolean play) {
+        removeMp(tag);
+        MediaPlayer player = getPlayer(tag);
+        player = MediaPlayer.create(mService, Uri.parse(url));
+        if (player != null) {
+            putMp(tag, player, url);
+            player.setOnCompletionListener(this);
+            if (play) {
+                player.start();
+            }
+        }
+        return player != null;
+    }
+
+    @Rpc(description = "pause playing media file", returns = "true if successful")
+    public synchronized boolean mediaPlayPause(
+            @RpcParameter(name = "tag", description = "string identifying resource")
+            @RpcDefault(value = "default")
+            String tag) {
+        MediaPlayer player = getPlayer(tag);
+        if (player == null) {
+            return false;
+        }
+        player.pause();
+        return true;
+    }
+
+    @Rpc(description = "Start playing media file.", returns = "true if successful")
+    public synchronized boolean mediaPlayStart(
+            @RpcParameter(name = "tag", description = "string identifying resource")
+            @RpcDefault(value = "default")
+            String tag) {
+        MediaPlayer player = getPlayer(tag);
+        if (player == null) {
+            return false;
+        }
+        player.start();
+        return mediaIsPlaying(tag);
+    }
+
+    @Rpc(description = "Stop playing media file.", returns = "true if successful")
+    public synchronized boolean mediaPlayStop(
+            @RpcParameter(name = "tag", description = "string identifying resource")
+            @RpcDefault(value = "default")
+            String tag) {
+        MediaPlayer player = getPlayer(tag);
+        if (player == null) {
+            return false;
+        }
+        player.stop();
+        return !mediaIsPlaying(tag) && player.getCurrentPosition() == 0;
+    }
+
+    @Rpc(description = "Stop all players.")
+    public synchronized void mediaPlayStopAll() {
+        for (MediaPlayer p : mPlayers.values()) {
+            p.stop();
+        }
+    }
+
+    @Rpc(description = "Seek To Position", returns = "New Position (in ms)")
+    public synchronized int mediaPlaySeek(@RpcParameter(name = "msec",
+                                                        description = "Position in millseconds")
+    Integer msec, @RpcParameter(name = "tag", description = "string identifying resource")
+    @RpcDefault(value = "default")
+    String tag) {
+        MediaPlayer player = getPlayer(tag);
+        if (player == null) {
+            return 0;
+        }
+        player.seekTo(msec);
+        return player.getCurrentPosition();
+    }
+
+    @Rpc(description = "Close media file", returns = "true if successful")
+    public synchronized boolean mediaPlayClose(
+            @RpcParameter(name = "tag", description = "string identifying resource")
+            @RpcDefault(value = "default")
+            String tag) throws Exception {
+        if (!mPlayers.containsKey(tag)) {
+            return false;
+        }
+        removeMp(tag);
+        return true;
+    }
+
+    @Rpc(description = "Checks if media file is playing.", returns = "true if playing")
+    public synchronized boolean mediaIsPlaying(
+            @RpcParameter(name = "tag", description = "string identifying resource")
+            @RpcDefault(value = "default")
+            String tag) {
+        MediaPlayer player = getPlayer(tag);
+        return (player == null) ? false : player.isPlaying();
+    }
+
+    @Rpc(description = "Information on current media", returns = "Media Information")
+    public synchronized Map<String, Object> mediaPlayGetInfo(
+            @RpcParameter(name = "tag", description = "string identifying resource")
+            @RpcDefault(value = "default")
+            String tag) {
+        Map<String, Object> result = new HashMap<String, Object>();
+        MediaPlayer player = getPlayer(tag);
+        result.put("tag", getDefault(tag));
+        if (player == null) {
+            result.put("loaded", false);
+        } else {
+            result.put("loaded", true);
+            result.put("duration", player.getDuration());
+            result.put("position", player.getCurrentPosition());
+            result.put("isplaying", player.isPlaying());
+            result.put("url", getUrl(tag));
+            result.put("looping", player.isLooping());
+        }
+        return result;
+    }
+
+    @Rpc(description = "Lists currently loaded media", returns = "List of Media Tags")
+    public Set<String> mediaPlayList() {
+        return mPlayers.keySet();
+    }
+
+    @Rpc(description = "Set Looping", returns = "True if successful")
+    public synchronized boolean mediaPlaySetLooping(@RpcParameter(name = "enabled")
+    @RpcDefault(value = "true")
+    Boolean enabled, @RpcParameter(name = "tag", description = "string identifying resource")
+    @RpcDefault(value = "default")
+    String tag) {
+        MediaPlayer player = getPlayer(tag);
+        if (player == null) {
+            return false;
+        }
+        player.setLooping(enabled);
+        return true;
+    }
+
+    @Rpc(description = "Checks if media file is playing.", returns = "true if playing")
+    public synchronized void mediaSetNext(
+            @RpcParameter(name = "tag", description = "string identifying resource")
+            @RpcDefault(value = "default")
+            String tag,
+            @RpcParameter(name = "next", description = "tag of the next track to play.")
+            String next) {
+        MediaPlayer player = getPlayer(tag);
+        MediaPlayer nPlayer = getPlayer(next);
+        if (player == null) {
+            throw new NullPointerException("Non-existent player tag " + tag);
+        }
+        if (nPlayer == null) {
+            throw new NullPointerException("Non-existent player tag " + next);
+        }
+        player.setNextMediaPlayer(nPlayer);
+    }
+
+    @Override
+    public synchronized void shutdown() {
+        for (String key : mPlayers.keySet()) {
+            MediaPlayer player = mPlayers.get(key);
+            if (player != null) {
+                player.stop();
+                player.release();
+                player = null;
+            }
+        }
+        mPlayers.clear();
+        mUrls.clear();
+    }
+
+    @Override
+    public void onCompletion(MediaPlayer player) {
+        String tag = getTag(player);
+        if (tag != null) {
+            Map<String, Object> data = new HashMap<String, Object>();
+            data.put("action", "complete");
+            data.put("tag", tag);
+            mEventFacade.postEvent("media", data);
+        }
+    }
+
+    private String getTag(MediaPlayer player) {
+        for (Entry<String, MediaPlayer> m : mPlayers.entrySet()) {
+            if (m.getValue() == player) {
+                return m.getKey();
+            }
+        }
+        return null;
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/media/MediaRecorderFacade.java b/Common/src/com/googlecode/android_scripting/facade/media/MediaRecorderFacade.java
new file mode 100644
index 0000000..ee67fe6
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/media/MediaRecorderFacade.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.media;
+
+import android.app.Service;
+import android.content.Intent;
+import android.media.MediaRecorder;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.view.SurfaceHolder;
+import android.view.SurfaceHolder.Callback;
+import android.view.SurfaceView;
+import android.view.WindowManager;
+
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.FutureActivityTaskExecutor;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.AndroidFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.future.FutureActivityTask;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A facade for recording media.
+ *
+ * Guidance notes: Use e.g. '/sdcard/file.ext' for your media destination file. A file extension of
+ * mpg will use the default settings for format and codec (often h263 which won't work with common
+ * PC media players). A file extension of mp4 or 3gp will use the appropriate format with the (more
+ * common) h264 codec. A video player such as QQPlayer (from the android market) plays both codecs
+ * and uses the composition matrix (embedded in the video file) to correct for image rotation. Many
+ * PC based media players ignore this matrix. Standard video sizes may be specified.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author John Karwatzki (jokar49@gmail.com)
+ */
+public class MediaRecorderFacade extends RpcReceiver {
+
+  private final MediaRecorder mMediaRecorder = new MediaRecorder();
+  private final Service mService;
+
+  public MediaRecorderFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+  }
+
+  @Rpc(description = "Records audio from the microphone and saves it to the given location.")
+  public void recorderStartMicrophone(@RpcParameter(name = "targetPath") String targetPath)
+      throws IOException {
+    startAudioRecording(targetPath, MediaRecorder.AudioSource.MIC);
+  }
+
+  @Rpc(description = "Records video from the camera and saves it to the given location. "
+      + "\nDuration specifies the maximum duration of the recording session. "
+      + "\nIf duration is 0 this method will return and the recording will only be stopped "
+      + "\nwhen recorderStop is called or when a scripts exits. "
+      + "\nOtherwise it will block for the time period equal to the duration argument."
+      + "\nvideoSize: 0=160x120, 1=320x240, 2=352x288, 3=640x480, 4=800x480.")
+  public void recorderStartVideo(@RpcParameter(name = "targetPath") String targetPath,
+      @RpcParameter(name = "duration") @RpcDefault("0") Integer duration,
+      @RpcParameter(name = "videoSize") @RpcDefault("1") Integer videoSize) throws Exception {
+    int ms = convertSecondsToMilliseconds(duration);
+    startVideoRecording(new File(targetPath), ms, videoSize);
+  }
+
+  private void startVideoRecording(File file, int milliseconds, int videoSize) throws Exception {
+    mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
+
+    int audioSource = MediaRecorder.AudioSource.MIC;
+    try {
+      Field source = Class.forName("android.media.MediaRecorder$AudioSource").getField("CAMCORDER");
+      source.getInt(null);
+    } catch (Exception e) {
+      Log.e(e);
+    }
+    int xSize;
+    int ySize;
+    switch (videoSize) {
+    case 0:
+      xSize = 160;
+      ySize = 120;
+      break;
+    case 1:
+      xSize = 320;
+      ySize = 240;
+      break;
+    case 2:
+      xSize = 352;
+      ySize = 288;
+      break;
+    case 3:
+      xSize = 640;
+      ySize = 480;
+      break;
+    case 4:
+      xSize = 800;
+      ySize = 480;
+      break;
+    default:
+      xSize = 320;
+      ySize = 240;
+      break;
+    }
+
+    mMediaRecorder.setAudioSource(audioSource);
+    String extension = file.toString().split("\\.")[1];
+    if (extension.equals("mp4")) {
+      mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
+      mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
+      mMediaRecorder.setVideoSize(xSize, ySize);
+      mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
+    } else if (extension.equals("3gp")) {
+      mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
+      mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
+      mMediaRecorder.setVideoSize(xSize, ySize);
+      mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
+    } else {
+
+      mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
+      mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
+      mMediaRecorder.setVideoSize(xSize, ySize);
+      mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
+    }
+
+    mMediaRecorder.setOutputFile(file.getAbsolutePath());
+    if (milliseconds > 0) {
+      mMediaRecorder.setMaxDuration(milliseconds);
+    }
+    FutureActivityTask<Exception> prepTask = prepare();
+    mMediaRecorder.start();
+    if (milliseconds > 0) {
+      new CountDownLatch(1).await(milliseconds, TimeUnit.MILLISECONDS);
+    }
+    prepTask.finish();
+  }
+
+  @Rpc(description = "Records video (and optionally audio) from the camera and saves it to the given location. "
+      + "\nDuration specifies the maximum duration of the recording session. "
+      + "\nIf duration is not provided this method will return immediately and the recording will only be stopped "
+      + "\nwhen recorderStop is called or when a scripts exits. "
+      + "\nOtherwise it will block for the time period equal to the duration argument.")
+  public void recorderCaptureVideo(@RpcParameter(name = "targetPath") String targetPath,
+      @RpcParameter(name = "duration") @RpcOptional Integer duration,
+      @RpcParameter(name = "recordAudio") @RpcDefault("true") Boolean recordAudio) throws Exception {
+    int ms = convertSecondsToMilliseconds(duration);
+    startVideoRecording(new File(targetPath), ms, recordAudio);
+  }
+
+  private void startVideoRecording(File file, int milliseconds, boolean withAudio) throws Exception {
+    mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
+    if (withAudio) {
+      int audioSource = MediaRecorder.AudioSource.MIC;
+      try {
+        Field source =
+            Class.forName("android.media.MediaRecorder$AudioSource").getField("CAMCORDER");
+        audioSource = source.getInt(null);
+      } catch (Exception e) {
+        Log.e(e);
+      }
+      mMediaRecorder.setAudioSource(audioSource);
+      mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
+      mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
+    } else {
+      mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
+    }
+    mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
+    mMediaRecorder.setOutputFile(file.getAbsolutePath());
+    if (milliseconds > 0) {
+      mMediaRecorder.setMaxDuration(milliseconds);
+    }
+    FutureActivityTask<Exception> prepTask = prepare();
+    mMediaRecorder.start();
+    if (milliseconds > 0) {
+      new CountDownLatch(1).await(milliseconds, TimeUnit.MILLISECONDS);
+    }
+    prepTask.finish();
+  }
+
+  private void startAudioRecording(String targetPath, int source) throws IOException {
+    mMediaRecorder.setAudioSource(source);
+    mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
+    mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
+    mMediaRecorder.setOutputFile(targetPath);
+    mMediaRecorder.prepare();
+    mMediaRecorder.start();
+  }
+
+  @Rpc(description = "Stops a previously started recording.")
+  public void recorderStop() {
+    mMediaRecorder.stop();
+    mMediaRecorder.reset();
+  }
+
+  @Rpc(description = "Starts the video capture application to record a video and saves it to the specified path.")
+  public void startInteractiveVideoRecording(@RpcParameter(name = "path") final String path) {
+    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+    File file = new File(path);
+    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
+    AndroidFacade facade = mManager.getReceiver(AndroidFacade.class);
+    facade.startActivityForResult(intent);
+  }
+
+  @Override
+  public void shutdown() {
+    mMediaRecorder.release();
+  }
+
+  // TODO(damonkohler): This shares a lot of code with the CameraFacade. It's probably worth moving
+  // it there.
+  private FutureActivityTask<Exception> prepare() throws Exception {
+    FutureActivityTask<Exception> task = new FutureActivityTask<Exception>() {
+      @Override
+      public void onCreate() {
+        super.onCreate();
+        final SurfaceView view = new SurfaceView(getActivity());
+        getActivity().setContentView(view);
+        getActivity().getWindow().setSoftInputMode(
+            WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED);
+        view.getHolder().addCallback(new Callback() {
+          @Override
+          public void surfaceDestroyed(SurfaceHolder holder) {
+          }
+
+          @Override
+          public void surfaceCreated(SurfaceHolder holder) {
+            try {
+              mMediaRecorder.setPreviewDisplay(view.getHolder().getSurface());
+              mMediaRecorder.prepare();
+              setResult(null);
+            } catch (IOException e) {
+              setResult(e);
+            }
+          }
+
+          @Override
+          public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+          }
+        });
+      }
+    };
+
+    FutureActivityTaskExecutor taskExecutor =
+        ((BaseApplication) mService.getApplication()).getTaskExecutor();
+    taskExecutor.execute(task);
+
+    Exception e = task.getResult();
+    if (e != null) {
+      throw e;
+    }
+    return task;
+  }
+
+  private int convertSecondsToMilliseconds(Integer seconds) {
+    if (seconds == null) {
+      return 0;
+    }
+    return (int) (seconds * 1000L);
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/media/MediaScannerFacade.java b/Common/src/com/googlecode/android_scripting/facade/media/MediaScannerFacade.java
new file mode 100644
index 0000000..fd885c7
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/media/MediaScannerFacade.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.media;
+
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.MediaScanner;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * Expose functionalities of MediaScanner related APIs.
+ */
+public class MediaScannerFacade extends RpcReceiver {
+
+    private final Service mService;
+    private final MediaScanner mScanService;
+    private final EventFacade mEventFacade;
+    private final MediaScannerReceiver mReceiver;
+
+    public MediaScannerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mScanService = new MediaScanner(mService, "external");
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mReceiver = new MediaScannerReceiver();
+    }
+
+    public class MediaScannerReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if(action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
+                Log.d("Scan finished, posting event.");
+                mEventFacade.postEvent("MediaScanFinished", new Bundle());
+                mService.unregisterReceiver(mReceiver);
+            }
+        }
+    }
+
+    @Rpc(description = "Scan external storage for media files.")
+    public void mediaScanForFiles() {
+        mService.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,
+                               Uri.parse("file://" + Environment.getExternalStorageDirectory())));
+        mService.registerReceiver(mReceiver,
+                                  new IntentFilter(Intent.ACTION_MEDIA_SCANNER_FINISHED));
+    }
+
+    @Rpc(description = "Scan for a media file.")
+    public void mediaScanForOneFile(@RpcParameter(name = "path") String path) {
+        mService.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse(path)));
+    }
+
+    @Override
+    public void shutdown() {
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/media/MediaSessionFacade.java b/Common/src/com/googlecode/android_scripting/facade/media/MediaSessionFacade.java
new file mode 100644
index 0000000..130b392
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/media/MediaSessionFacade.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.media;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.MediaSessionManager;
+import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
+import android.media.session.PlaybackState;
+import android.media.session.MediaSession.Callback;
+import android.view.KeyEvent;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.MainThread;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * Expose functionalities of MediaSession related classes
+ * like MediaSession, MediaSessionManager, MediaController.
+ *
+ */
+public class MediaSessionFacade extends RpcReceiver {
+
+    private final Service mService;
+    private final EventFacade mEventFacade;
+    private final MediaSession mSession;
+    private final MediaSessionManager mManager;
+    private final OnActiveSessionsChangedListener mSessionListener;
+    private final Callback mCallback;
+
+    private List<MediaController> mActiveControllers = null;
+
+    public MediaSessionFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        Log.d("Creating MediaSession.");
+        mSession = new MediaSession(mService, "SL4A");
+        mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
+        mManager = (MediaSessionManager) mService.getSystemService(Context.MEDIA_SESSION_SERVICE);
+        mCallback = new MediaButtonCallback(mEventFacade);
+        mSessionListener = new MediaSessionListener();
+        mManager.addOnActiveSessionsChangedListener(mSessionListener,
+                new ComponentName(mService.getPackageName(), this.getClass().getName()));
+        mSession.setActive(true);
+    }
+
+    private class MediaSessionListener implements OnActiveSessionsChangedListener {
+
+        @Override
+        public void onActiveSessionsChanged(List<MediaController> controllers) {
+            Log.d("Active MediaSessions have changed. Update current controller.");
+            int size = controllers.size();
+            for (int i = 0; i < size; i++) {
+                MediaController controller = controllers.get(i);
+                long flags = controller.getFlags();
+                // We only care about sessions that handle transport controls,
+                // which will be true for apps using RCC
+                if ((flags & MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS) != 0) {
+                    Log.d("The current active MediaSessions is " + controller.getTag());
+                    return;
+                }
+            }
+        }
+    }
+
+    @Rpc(description = "Retrieve a list of active sessions.")
+    public List<String> mediaGetActiveSessions() {
+        mActiveControllers = mManager.getActiveSessions(null);
+        List<String> results = new ArrayList<String>();
+        for (MediaController mc : mActiveControllers) {
+            results.add(mc.getTag());
+        }
+        return results;
+    }
+
+    @Rpc(description = "Add callback to media session.")
+    public void mediaSessionAddCallback() {
+        MainThread.run(mService, new Callable<Object>() {
+            @Override
+            public Object call() throws Exception {
+                Log.d("Adding callback.");
+                mSession.setCallback(mCallback);
+                PlaybackState.Builder bob = new PlaybackState.Builder();
+                bob.setActions(PlaybackState.ACTION_PLAY |
+                               PlaybackState.ACTION_PAUSE |
+                               PlaybackState.ACTION_STOP);
+                bob.setState(PlaybackState.STATE_PLAYING, 0, 1);
+                mSession.setPlaybackState(bob.build());
+                return null;
+            }
+        });
+    }
+
+    @Rpc(description = "Whether current media session is active.")
+    public Boolean mediaSessionIsActive() {
+        return mSession.isActive();
+    }
+
+    @Rpc(description = "Simulate a media key press.")
+    public void mediaDispatchMediaKeyEvent(String key) {
+        int keyCode;
+        if (key.equals("Play")) {
+            keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
+        } else if (key.equals("Pause")) {
+            keyCode = KeyEvent.KEYCODE_MEDIA_PAUSE;
+        } else if (key.equals("Stop")) {
+            keyCode = KeyEvent.KEYCODE_MEDIA_STOP;
+        } else if (key.equals("Next")) {
+            keyCode = KeyEvent.KEYCODE_MEDIA_NEXT;
+        } else if (key.equals("Previous")) {
+            keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS;
+        } else if (key.equals("Forward")) {
+            keyCode = KeyEvent.KEYCODE_MEDIA_FAST_FORWARD;
+        } else if (key.equals("Rewind")) {
+            keyCode = KeyEvent.KEYCODE_MEDIA_REWIND;
+        } else {
+            Log.d("Unrecognized media key.");
+            return;
+        }
+        KeyEvent keyDown = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
+        KeyEvent keyUp = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
+        mManager.dispatchMediaKeyEvent(keyDown);
+        mManager.dispatchMediaKeyEvent(keyUp);
+    }
+
+    private MediaController getMediaController(int idx) {
+        return mActiveControllers.get(idx);
+    }
+
+    @Rpc(description = "Call Play on the currently active media session.")
+    public void mediaSessionPlay(@RpcParameter(name = "index") @RpcDefault(value = "0")
+                                 Integer idx) {
+        getMediaController(idx).getTransportControls().play();
+    }
+
+    @Rpc(description = "Call Pause on the currently active media session.")
+    public void mediaSessionPause(@RpcParameter(name = "index") @RpcDefault(value = "0")
+                                  Integer idx) {
+        getMediaController(idx).getTransportControls().pause();
+    }
+
+    @Rpc(description = "Call Stop on the currently active media session.")
+    public void mediaSessionStop(@RpcParameter(name = "index") @RpcDefault(value = "0")
+                                 Integer idx) {
+        getMediaController(idx).getTransportControls().stop();
+    }
+
+    @Rpc(description = "Call Next on the currently active media session.")
+    public void mediaSessionNext(@RpcParameter(name = "index") @RpcDefault(value = "0")
+                                 Integer idx) {
+        getMediaController(idx).getTransportControls().skipToNext();
+    }
+
+    @Override
+    public void shutdown() {
+        mSession.setCallback(null);
+        mSession.release();
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/CarrierConfigFacade.java b/Common/src/com/googlecode/android_scripting/facade/telephony/CarrierConfigFacade.java
new file mode 100644
index 0000000..fc4d061
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/CarrierConfigFacade.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.telephony.CarrierConfigManager;
+
+import com.googlecode.android_scripting.facade.AndroidFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.MainThread;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+
+public class CarrierConfigFacade extends RpcReceiver {
+    private final Service mService;
+    private final AndroidFacade mAndroidFacade;
+    private final CarrierConfigManager mCarrierConfigManager;
+
+    public CarrierConfigFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mAndroidFacade = manager.getReceiver(AndroidFacade.class);
+        mCarrierConfigManager =
+            (CarrierConfigManager)mService.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+    }
+
+    @Rpc(description = "Tethering Entitlement Check")
+    public boolean carrierConfigIsTetheringModeAllowed(String mode, Integer timeout) {
+        String[] mProvisionApp = mService.getResources().getStringArray(
+                com.android.internal.R.array.config_mobile_hotspot_provision_app);
+        /* following check defined in
+        frameworks/base/packages/SettingsLib/src/com/android/settingslib/TetherUtil.java
+        isProvisioningNeeded
+        */
+        if ((mProvisionApp == null) || (mProvisionApp.length != 2)){
+            Log.d("carrierConfigIsTetheringModeAllowed: no check is present.");
+            return true;
+        }
+        Log.d("carrierConfigIsTetheringModeAllowed mProvisionApp 0 " + mProvisionApp[0]);
+        Log.d("carrierConfigIsTetheringModeAllowed mProvisionApp 1 " + mProvisionApp[1]);
+
+        /* defined in frameworks/base/packages/SettingsLib/src/com/android/settingslib/TetherUtil.java
+        public static final int INVALID             = -1;
+        public static final int WIFI_TETHERING      = 0;
+        public static final int USB_TETHERING       = 1;
+        public static final int BLUETOOTH_TETHERING = 2;
+        */
+        // TODO: b/26273844 need to use android.settingslib.TetherUtil to
+        // replace those private defines.
+        final int INVALID             = -1;
+        final int WIFI_TETHERING      = 0;
+        final int USB_TETHERING       = 1;
+        final int BLUETOOTH_TETHERING = 2;
+
+        /* defined in packages/apps/Settings/src/com/android/settings/TetherSettings.java
+        private static final int PROVISION_REQUEST = 0;
+        */
+        final int PROVISION_REQUEST = 0;
+
+        int mTetherChoice = INVALID;
+        if (mode.equals("wifi")){
+            mTetherChoice = WIFI_TETHERING;
+        } else if (mode.equals("usb")) {
+            mTetherChoice = USB_TETHERING;
+        } else if (mode.equals("bluetooth")) {
+            mTetherChoice = BLUETOOTH_TETHERING;
+        }
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setClassName(mProvisionApp[0], mProvisionApp[1]);
+        intent.putExtra("TETHER_TYPE", mTetherChoice);
+        int result;
+        try{
+            result = mAndroidFacade.startActivityForResultCodeWithTimeout(
+                intent, PROVISION_REQUEST, timeout);
+        } catch (Exception e) {
+            Log.d("phoneTetherCheck exception" + e.toString());
+            return false;
+        }
+
+        if (result == Activity.RESULT_OK) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public void shutdown() {
+
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/ImsManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/telephony/ImsManagerFacade.java
new file mode 100755
index 0000000..0a67c34
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/ImsManagerFacade.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.telephony.SubscriptionManager;
+
+import com.android.ims.ImsException;
+import com.android.ims.ImsManager;
+import com.android.ims.ImsConfig;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * Exposes ImsManager functionality.
+ */
+public class ImsManagerFacade extends RpcReceiver {
+
+    private final Service mService;
+    private final Context mContext;
+    private ImsManager mImsManager;
+
+    public ImsManagerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mContext = mService.getBaseContext();
+        mImsManager = ImsManager.getInstance(mContext,
+                SubscriptionManager.getDefaultVoicePhoneId());
+    }
+
+    @Rpc(description = "Return True if Enhanced 4g Lte mode is enabled by platform.")
+    public boolean imsIsEnhanced4gLteModeSettingEnabledByPlatform() {
+        return ImsManager.isVolteEnabledByPlatform(mContext);
+    }
+
+    @Rpc(description = "Return True if Enhanced 4g Lte mode is enabled by user.")
+    public boolean imsIsEnhanced4gLteModeSettingEnabledByUser() {
+        return ImsManager.isEnhanced4gLteModeSettingEnabledByUser(mContext);
+    }
+
+    @Rpc(description = "Set Enhanced 4G mode.")
+    public void imsSetEnhanced4gMode(
+            @RpcParameter(name = "enable") Boolean enable) {
+        ImsManager.setEnhanced4gLteModeSetting(mContext, enable);
+    }
+
+    @Rpc(description = "Check for VoLTE Provisioning.")
+    public boolean imsIsVolteProvisionedOnDevice() {
+        return mImsManager.isVolteProvisionedOnDevice(mContext);
+    }
+
+    @Rpc(description = "Set Modem Provisioning for VoLTE")
+    public void imsSetVolteProvisioning(
+            @RpcParameter(name = "enable") Boolean enable)
+            throws ImsException{
+        mImsManager.getConfigInterface().setProvisionedValue(
+                ImsConfig.ConfigConstants.VLT_SETTING_ENABLED,
+                enable? 1 : 0);
+    }
+
+    /**************************
+     * Begin WFC Calling APIs
+     **************************/
+
+    @Rpc(description = "Return True if WiFi Calling is enabled for platform.")
+    public boolean imsIsWfcEnabledByPlatform() {
+        return ImsManager.isWfcEnabledByPlatform(mContext);
+    }
+
+    @Rpc(description = "Set whether or not WFC is enabled during roaming")
+    public void imsSetWfcRoamingSetting(
+                        @RpcParameter(name = "enable")
+            Boolean enable) {
+        ImsManager.setWfcRoamingSetting(mContext, enable);
+
+    }
+
+    @Rpc(description = "Return True if WiFi Calling is enabled during roaming.")
+    public boolean imsIsWfcRoamingEnabledByUser() {
+        return ImsManager.isWfcRoamingEnabledByUser(mContext);
+    }
+
+    @Rpc(description = "Set the Wifi Calling Mode of operation")
+    public void imsSetWfcMode(
+                        @RpcParameter(name = "mode")
+            String mode)
+            throws IllegalArgumentException {
+
+        int mode_val;
+
+        switch (mode.toUpperCase()) {
+            case TelephonyConstants.WFC_MODE_WIFI_ONLY:
+                mode_val =
+                        ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY;
+                break;
+            case TelephonyConstants.WFC_MODE_CELLULAR_PREFERRED:
+                mode_val =
+                        ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED;
+                break;
+            case TelephonyConstants.WFC_MODE_WIFI_PREFERRED:
+                mode_val =
+                        ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED;
+                break;
+            case TelephonyConstants.WFC_MODE_DISABLED:
+                if (ImsManager.isWfcEnabledByPlatform(mContext) &&
+                        ImsManager.isWfcEnabledByUser(mContext) == true) {
+                    ImsManager.setWfcSetting(mContext, false);
+                }
+                return;
+            default:
+                throw new IllegalArgumentException("Invalid WfcMode");
+        }
+
+        ImsManager.setWfcMode(mContext, mode_val);
+        if (ImsManager.isWfcEnabledByPlatform(mContext) &&
+                ImsManager.isWfcEnabledByUser(mContext) == false) {
+            ImsManager.setWfcSetting(mContext, true);
+        }
+
+        return;
+    }
+
+    @Rpc(description = "Return current WFC Mode if Enabled.")
+    public String imsGetWfcMode() {
+        if(ImsManager.isWfcEnabledByUser(mContext) == false) {
+            return TelephonyConstants.WFC_MODE_DISABLED;
+        }
+        return TelephonyUtils.getWfcModeString(
+            ImsManager.getWfcMode(mContext));
+    }
+
+    @Rpc(description = "Return True if WiFi Calling is enabled by user.")
+    public boolean imsIsWfcEnabledByUser() {
+        return ImsManager.isWfcEnabledByUser(mContext);
+    }
+
+    @Rpc(description = "Set whether or not WFC is enabled")
+    public void imsSetWfcSetting(
+                        @RpcParameter(name = "enable")  Boolean enable) {
+        ImsManager.setWfcSetting(mContext,enable);
+    }
+
+    /**************************
+     * Begin VT APIs
+     **************************/
+
+    @Rpc(description = "Return True if Video Calling is enabled by the platform.")
+    public boolean imsIsVtEnabledByPlatform() {
+        return ImsManager.isVtEnabledByPlatform(mContext);
+    }
+
+    @Override
+    public void shutdown() {
+
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java b/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java
new file mode 100644
index 0000000..965735a
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java
@@ -0,0 +1,1416 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import android.telecom.Call;
+import android.telecom.Call.Details;
+import android.telecom.CallAudioState;
+import android.telecom.Conference;
+import android.telecom.Connection;
+import android.telecom.ConnectionService;
+import android.telecom.InCallService;
+import android.telecom.Phone;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+
+import com.googlecode.android_scripting.Log;
+
+import com.googlecode.android_scripting.facade.EventFacade;
+
+public class InCallServiceImpl extends InCallService {
+
+    private static InCallServiceImpl mService = null;
+
+    public static InCallServiceImpl getService() {
+        return mService;
+    }
+
+    private static Object mLock = new Object();
+
+    // Provides a return value for getCallState when no call is active
+    public static final int STATE_INVALID = -1;
+
+    // Provides a return value for getCallQuality when input is invalid
+    public static final int QUALITY_INVALID = -1;
+
+    // Provides a return value for getAudioRoute when input is invalid
+    public static final int INVALID_AUDIO_ROUTE = -1;
+
+    public static final int VIDEO_STATE_AUDIO_ONLY = VideoProfile.STATE_AUDIO_ONLY;
+
+    public static final int VIDEO_STATE_TX_ENABLED = VideoProfile.STATE_TX_ENABLED;
+
+    public static final int VIDEO_STATE_RX_ENABLED = VideoProfile.STATE_RX_ENABLED;
+
+    public static final int VIDEO_STATE_BIDIRECTIONAL = VideoProfile.STATE_BIDIRECTIONAL;
+
+    public static final int VIDEO_STATE_TX_PAUSED =
+            VideoProfile.STATE_TX_ENABLED | VideoProfile.STATE_PAUSED;
+
+    public static final int VIDEO_STATE_RX_PAUSED =
+            VideoProfile.STATE_RX_ENABLED | VideoProfile.STATE_PAUSED;
+
+    public static final int VIDEO_STATE_BIDIRECTIONAL_PAUSED =
+            VideoProfile.STATE_BIDIRECTIONAL | VideoProfile.STATE_PAUSED;
+
+    // Container class to return the call ID along with the event
+    public class CallEvent<EventType> {
+
+        private final String mCallId;
+        private final EventType mEvent;
+
+        CallEvent(String callId, EventType event) {
+            mCallId = callId;
+            mEvent = event;
+        }
+
+        public String getCallId() {
+            return mCallId;
+        }
+
+        public EventType getEvent() {
+            return mEvent;
+        }
+    }
+
+    // Currently the same as a call event... here for future use
+    public class VideoCallEvent<EventType> extends CallEvent<EventType> {
+        VideoCallEvent(String callId, EventType event) {
+            super(callId, event);
+        }
+    }
+
+    private class CallCallback extends Call.Callback {
+
+        // Invalid video state (valid >= 0)
+        public static final int STATE_INVALID = InCallServiceImpl.STATE_INVALID;
+
+        public static final int EVENT_INVALID = -1;
+        public static final int EVENT_NONE = 0;
+        public static final int EVENT_STATE_CHANGED = 1 << 0;
+        public static final int EVENT_PARENT_CHANGED = 1 << 1;
+        public static final int EVENT_CHILDREN_CHANGED = 1 << 2;
+        public static final int EVENT_DETAILS_CHANGED = 1 << 3;
+        public static final int EVENT_CANNED_TEXT_RESPONSES_LOADED = 1 << 4;
+        public static final int EVENT_POST_DIAL_WAIT = 1 << 5;
+        public static final int EVENT_VIDEO_CALL_CHANGED = 1 << 6;
+        public static final int EVENT_CALL_DESTROYED = 1 << 7;
+        public static final int EVENT_CONFERENCABLE_CALLS_CHANGED = 1 << 8;
+
+        public static final int EVENT_ALL = EVENT_STATE_CHANGED |
+                EVENT_PARENT_CHANGED |
+                EVENT_CHILDREN_CHANGED |
+                EVENT_DETAILS_CHANGED |
+                EVENT_CANNED_TEXT_RESPONSES_LOADED |
+                EVENT_POST_DIAL_WAIT |
+                EVENT_VIDEO_CALL_CHANGED |
+                EVENT_DETAILS_CHANGED |
+                EVENT_CALL_DESTROYED |
+                EVENT_CONFERENCABLE_CALLS_CHANGED;
+
+        private int mEvents;
+        private String mCallId;
+
+        public CallCallback(String callId, int events) {
+            super();
+            mEvents = events & EVENT_ALL;
+            mCallId = callId;
+        }
+
+        public void startListeningForEvents(int events) {
+            mEvents |= events & EVENT_ALL;
+        }
+
+        public void stopListeningForEvents(int events) {
+            mEvents &= ~(events & EVENT_ALL);
+        }
+
+        @Override
+        public void onStateChanged(
+                Call call, int state) {
+            Log.d("CallCallback:onStateChanged()");
+            if ((mEvents & EVENT_STATE_CHANGED)
+                    == EVENT_STATE_CHANGED) {
+                servicePostEvent(TelephonyConstants.EventTelecomCallStateChanged,
+                        new CallEvent<String>(mCallId, getCallStateString(state)));
+            }
+        }
+
+        @Override
+        public void onParentChanged(
+                Call call, Call parent) {
+            Log.d("CallCallback:onParentChanged()");
+            if ((mEvents & EVENT_PARENT_CHANGED)
+                    == EVENT_PARENT_CHANGED) {
+                servicePostEvent(TelephonyConstants.EventTelecomCallParentChanged,
+                        new CallEvent<String>(mCallId, getCallId(parent)));
+            }
+        }
+
+        @Override
+        public void onChildrenChanged(
+                Call call, List<Call> children) {
+            Log.d("CallCallback:onChildrenChanged()");
+
+            if ((mEvents & EVENT_CHILDREN_CHANGED)
+                    == EVENT_CHILDREN_CHANGED) {
+                List<String> childList = new ArrayList<String>();
+
+                for (Call child : children) {
+                    childList.add(getCallId(child));
+                }
+                servicePostEvent(TelephonyConstants.EventTelecomCallChildrenChanged,
+                        new CallEvent<List<String>>(mCallId, childList));
+            }
+        }
+
+        @Override
+        public void onDetailsChanged(
+                Call call, Details details) {
+            Log.d("CallCallback:onDetailsChanged()");
+
+            if ((mEvents & EVENT_DETAILS_CHANGED)
+                    == EVENT_DETAILS_CHANGED) {
+                servicePostEvent(TelephonyConstants.EventTelecomCallDetailsChanged,
+                        new CallEvent<Details>(mCallId, details));
+            }
+        }
+
+        @Override
+        public void onCannedTextResponsesLoaded(
+                Call call, List<String> cannedTextResponses) {
+            Log.d("CallCallback:onCannedTextResponsesLoaded()");
+            if ((mEvents & EVENT_CANNED_TEXT_RESPONSES_LOADED)
+                    == EVENT_CANNED_TEXT_RESPONSES_LOADED) {
+                servicePostEvent(TelephonyConstants.EventTelecomCallCannedTextResponsesLoaded,
+                        new CallEvent<List<String>>(mCallId, cannedTextResponses));
+            }
+        }
+
+        @Override
+        public void onPostDialWait(
+                Call call, String remainingPostDialSequence) {
+            Log.d("CallCallback:onPostDialWait()");
+            if ((mEvents & EVENT_POST_DIAL_WAIT)
+                    == EVENT_POST_DIAL_WAIT) {
+                servicePostEvent(TelephonyConstants.EventTelecomCallPostDialWait,
+                        new CallEvent<String>(mCallId, remainingPostDialSequence));
+            }
+        }
+
+        @Override
+        public void onVideoCallChanged(
+                Call call, InCallService.VideoCall videoCall) {
+
+            /*
+             * There is a race condition such that the lifetime of the VideoCall is not aligned with
+             * the lifetime of the underlying call object. We are using the onVideoCallChanged
+             * method as a way of determining the lifetime of the VideoCall object rather than
+             * onCallAdded/onCallRemoved.
+             */
+            Log.d("CallCallback:onVideoCallChanged()");
+
+            if (call != null) {
+                String callId = getCallId(call);
+                CallContainer cc = mCallContainerMap.get(callId);
+                if (cc == null) {
+                    Log.d(String.format("Call container returned null for callId %s", callId));
+                }
+                else {
+                    synchronized (mLock) {
+                        if (videoCall == null) {
+                            Log.d("Yo dawg, I heard you like null video calls.");
+                            // Try and see if the videoCall has been added/changed after firing the
+                            // callback
+                            // This probably won't work.
+                            videoCall = call.getVideoCall();
+                        }
+                        if (cc.getVideoCall() != videoCall) {
+                            if (videoCall == null) {
+                                // VideoCall object deleted
+                                cc.updateVideoCall(null, null);
+                                Log.d("Removing video call from call.");
+                            }
+                            else if (cc.getVideoCall() != null) {
+                                // Somehow we have a mismatched VideoCall ID!
+                                Log.d("Mismatched video calls for same call ID.");
+                            }
+                            else {
+                                Log.d("Huzzah, we have a video call!");
+
+                                VideoCallCallback videoCallCallback =
+                                        new VideoCallCallback(callId, VideoCallCallback.EVENT_NONE);
+
+                                videoCall.registerCallback(videoCallCallback);
+
+                                cc.updateVideoCall(
+                                        videoCall,
+                                        videoCallCallback);
+                            }
+                        }
+                        else {
+                            Log.d("Change to existing video call.");
+                        }
+
+                    }
+                }
+            }
+            else {
+                Log.d("passed null call pointer to call callback");
+            }
+
+            if ((mEvents & EVENT_VIDEO_CALL_CHANGED)
+                    == EVENT_VIDEO_CALL_CHANGED) {
+                // TODO: b/26273778 Need to determine what to return;
+                // probably not the whole video call
+                servicePostEvent(TelephonyConstants.EventTelecomCallVideoCallChanged,
+                        new CallEvent<String>(mCallId, videoCall.toString()));
+            }
+        }
+
+        @Override
+        public void onCallDestroyed(Call call) {
+            Log.d("CallCallback:onCallDestroyed()");
+
+            if ((mEvents & EVENT_CALL_DESTROYED)
+                    == EVENT_CALL_DESTROYED) {
+                servicePostEvent(TelephonyConstants.EventTelecomCallDestroyed,
+                        new CallEvent<Call>(mCallId, call));
+            }
+        }
+
+        @Override
+        public void onConferenceableCallsChanged(
+                Call call, List<Call> conferenceableCalls) {
+            Log.d("CallCallback:onConferenceableCallsChanged()");
+
+            if ((mEvents & EVENT_CONFERENCABLE_CALLS_CHANGED)
+                    == EVENT_CONFERENCABLE_CALLS_CHANGED) {
+                List<String> confCallList = new ArrayList<String>();
+                for (Call cc : conferenceableCalls) {
+                    confCallList.add(getCallId(cc));
+                }
+                servicePostEvent(TelephonyConstants.EventTelecomCallConferenceableCallsChanged,
+                        new CallEvent<List<String>>(mCallId, confCallList));
+            }
+        }
+    }
+
+    private class VideoCallCallback extends InCallService.VideoCall.Callback {
+
+        public static final int EVENT_INVALID = -1;
+        public static final int EVENT_NONE = 0;
+        public static final int EVENT_SESSION_MODIFY_REQUEST_RECEIVED = 1 << 0;
+        public static final int EVENT_SESSION_MODIFY_RESPONSE_RECEIVED = 1 << 1;
+        public static final int EVENT_SESSION_EVENT = 1 << 2;
+        public static final int EVENT_PEER_DIMENSIONS_CHANGED = 1 << 3;
+        public static final int EVENT_VIDEO_QUALITY_CHANGED = 1 << 4;
+        public static final int EVENT_DATA_USAGE_CHANGED = 1 << 5;
+        public static final int EVENT_CAMERA_CAPABILITIES_CHANGED = 1 << 6;
+        public static final int EVENT_ALL =
+                EVENT_SESSION_MODIFY_REQUEST_RECEIVED |
+                EVENT_SESSION_MODIFY_RESPONSE_RECEIVED |
+                EVENT_SESSION_EVENT |
+                EVENT_PEER_DIMENSIONS_CHANGED |
+                EVENT_VIDEO_QUALITY_CHANGED |
+                EVENT_DATA_USAGE_CHANGED |
+                EVENT_CAMERA_CAPABILITIES_CHANGED;
+
+        private String mCallId;
+        private int mEvents;
+
+        public VideoCallCallback(String callId, int listeners) {
+
+            mCallId = callId;
+            mEvents = listeners & EVENT_ALL;
+        }
+
+        public void startListeningForEvents(int events) {
+            Log.d(String.format(
+                    "VideoCallCallback(%s):startListeningForEvents(%x): events:%x",
+                    mCallId, events, mEvents));
+
+            mEvents |= events & EVENT_ALL;
+
+        }
+
+        public void stopListeningForEvents(int events) {
+            mEvents &= ~(events & EVENT_ALL);
+        }
+
+        @Override
+        public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
+            Log.d(String.format("VideoCallCallback(%s):onSessionModifyRequestReceived()", mCallId));
+
+            if ((mEvents & EVENT_SESSION_MODIFY_REQUEST_RECEIVED)
+                    == EVENT_SESSION_MODIFY_REQUEST_RECEIVED) {
+                servicePostEvent(TelephonyConstants.EventTelecomVideoCallSessionModifyRequestReceived,
+                        new VideoCallEvent<VideoProfile>(mCallId, videoProfile));
+            }
+
+        }
+
+        @Override
+        public void onSessionModifyResponseReceived(int status,
+                VideoProfile requestedProfile, VideoProfile responseProfile) {
+            Log.d("VideoCallCallback:onSessionModifyResponseReceived()");
+
+            if ((mEvents & EVENT_SESSION_MODIFY_RESPONSE_RECEIVED)
+                    == EVENT_SESSION_MODIFY_RESPONSE_RECEIVED) {
+
+                HashMap<String, VideoProfile> smrrInfo = new HashMap<String, VideoProfile>();
+
+                smrrInfo.put("RequestedProfile", requestedProfile);
+                smrrInfo.put("ResponseProfile", responseProfile);
+
+                servicePostEvent(TelephonyConstants.EventTelecomVideoCallSessionModifyResponseReceived,
+                        new VideoCallEvent<HashMap<String, VideoProfile>>(mCallId, smrrInfo));
+            }
+        }
+
+        @Override
+        public void onCallSessionEvent(int event) {
+            Log.d("VideoCallCallback:onCallSessionEvent()");
+
+            String eventString = getVideoCallSessionEventString(event);
+
+            if ((mEvents & EVENT_SESSION_EVENT)
+                    == EVENT_SESSION_EVENT) {
+                servicePostEvent(TelephonyConstants.EventTelecomVideoCallSessionEvent,
+                        new VideoCallEvent<String>(mCallId, eventString));
+            }
+        }
+
+        @Override
+        public void onPeerDimensionsChanged(int width, int height) {
+            Log.d("VideoCallCallback:onPeerDimensionsChanged()");
+
+            if ((mEvents & EVENT_PEER_DIMENSIONS_CHANGED)
+                    == EVENT_PEER_DIMENSIONS_CHANGED) {
+
+                HashMap<String, Integer> temp = new HashMap<String, Integer>();
+                temp.put("Width", width);
+                temp.put("Height", height);
+
+                servicePostEvent(TelephonyConstants.EventTelecomVideoCallPeerDimensionsChanged,
+                        new VideoCallEvent<HashMap<String, Integer>>(mCallId, temp));
+            }
+        }
+
+        @Override
+        public void onVideoQualityChanged(int videoQuality) {
+            Log.d("VideoCallCallback:onVideoQualityChanged()");
+
+            if ((mEvents & EVENT_VIDEO_QUALITY_CHANGED)
+                    == EVENT_VIDEO_QUALITY_CHANGED) {
+                servicePostEvent(TelephonyConstants.EventTelecomVideoCallVideoQualityChanged,
+                        new VideoCallEvent<String>(mCallId,
+                                getVideoCallQualityString(videoQuality)));
+            }
+        }
+
+        @Override
+        public void onCallDataUsageChanged(long dataUsage) {
+            Log.d("VideoCallCallback:onCallDataUsageChanged()");
+
+            if ((mEvents & EVENT_DATA_USAGE_CHANGED)
+                    == EVENT_DATA_USAGE_CHANGED) {
+                servicePostEvent(TelephonyConstants.EventTelecomVideoCallDataUsageChanged,
+                        new VideoCallEvent<Long>(mCallId, dataUsage));
+            }
+        }
+
+        @Override
+        public void onCameraCapabilitiesChanged(
+                CameraCapabilities cameraCapabilities) {
+            Log.d("VideoCallCallback:onCallDataUsageChanged()");
+
+            if ((mEvents & EVENT_DATA_USAGE_CHANGED)
+                    == EVENT_DATA_USAGE_CHANGED) {
+                servicePostEvent(TelephonyConstants.EventTelecomVideoCallCameraCapabilities,
+                        new VideoCallEvent<CameraCapabilities>(mCallId, cameraCapabilities));
+            }
+
+        }
+    }
+
+    /*
+     * Container Class for Call and CallCallback Objects
+     */
+    private class CallContainer {
+
+        /*
+         * Call Container Members
+         */
+
+        private Call mCall;
+        private CallCallback mCallCallback;
+        private VideoCall mVideoCall;
+        private VideoCallCallback mVideoCallCallback;
+
+        /*
+         * Call Container Functions
+         */
+
+        public CallContainer(Call call,
+                CallCallback callback,
+                VideoCall videoCall,
+                VideoCallCallback videoCallCallback) {
+            mCall = call;
+            mCallCallback = callback;
+            mVideoCall = videoCall;
+            mVideoCallCallback = videoCallCallback;
+        }
+
+        public Call getCall() {
+            return mCall;
+        }
+
+        public CallCallback getCallback() {
+            return mCallCallback;
+        }
+
+        public InCallService.VideoCall getVideoCall() {
+            return mVideoCall;
+        }
+
+        public VideoCallCallback getVideoCallCallback() {
+            return mVideoCallCallback;
+        }
+
+        public void updateVideoCall(VideoCall videoCall, VideoCallCallback videoCallCallback) {
+            if (videoCall == null && videoCallCallback != null) {
+                Log.d("UpdateVideoCall: videoCall and videoCallCallback are null.");
+                return;
+            }
+            mVideoCall = videoCall;
+            mVideoCallCallback = videoCallCallback;
+        }
+    }
+
+    /*
+     * TODO: b/26272583 Refactor so that these are instance members of the
+     * incallservice. Then we can perform null checks using the design pattern
+     * of the "manager" classes.
+     */
+
+    private static EventFacade mEventFacade = null;
+    private static HashMap<String, CallContainer> mCallContainerMap =
+            new HashMap<String, CallContainer>();
+
+    @Override
+    public void onCallAdded(Call call) {
+        Log.d("onCallAdded: " + call.toString());
+        String id = getCallId(call);
+        Log.d("Adding " + id);
+        CallCallback callCallback = new CallCallback(id, CallCallback.EVENT_NONE);
+
+        call.registerCallback(callCallback);
+
+        VideoCall videoCall = call.getVideoCall();
+        VideoCallCallback videoCallCallback = null;
+
+        if (videoCall != null) {
+            synchronized (mLock) {
+                if (getVideoCallById(id) == null) {
+                    videoCallCallback = new VideoCallCallback(id, VideoCallCallback.EVENT_NONE);
+                    videoCall.registerCallback(videoCallCallback);
+                }
+            }
+        }
+        else {
+            // No valid video object
+            Log.d("No Video Call provided to InCallService.");
+        }
+
+        mCallContainerMap.put(id,
+                new CallContainer(call,
+                        callCallback,
+                        videoCall,
+                        videoCallCallback));
+
+        /*
+         * Once we have a call active, anchor the inCallService instance as a psuedo-singleton.
+         * Because object lifetime is not guaranteed we shouldn't do this in the
+         * constructor/destructor.
+         */
+        if (mService == null) {
+            mService = this;
+        }
+        else if (mService != this) {
+            Log.e("Multiple InCall Services Active in SL4A!");
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        Log.d("onCallRemoved: " + call.toString());
+        String id = getCallId(call);
+        Log.d("Removing " + id);
+
+        mCallContainerMap.remove(id);
+
+        if (mCallContainerMap.size() == 0) {
+            mService = null;
+        }
+    }
+
+    public static void setEventFacade(EventFacade facade) {
+        Log.d(String.format("setEventFacade(): Settings SL4A event facade to %s",
+                (facade != null) ? facade.toString() : "null"));
+        mEventFacade = facade;
+    }
+
+    private static boolean servicePostEvent(String eventName, Object event) {
+
+        if (mEventFacade == null) {
+            Log.d("servicePostEvent():SL4A eventFacade Is Null!!");
+            return false;
+        }
+
+        mEventFacade.postEvent(eventName, event);
+
+        return true;
+    }
+
+    public static String getCallId(Call call) {
+        if (call != null) {
+            return call.toString();
+        }
+        else
+            return "";
+    }
+
+    public static String getVideoCallId(InCallServiceImpl.VideoCall videoCall) {
+        if (videoCall != null)
+            return videoCall.toString();
+        else
+            return "";
+    }
+
+    private static Call getCallById(String callId) {
+
+        CallContainer cc = mCallContainerMap.get(callId);
+
+        if (cc != null) {
+            return cc.getCall();
+        }
+
+        return null;
+    }
+
+    private static CallCallback getCallCallbackById(String callId) {
+
+        CallContainer cc = mCallContainerMap.get(callId);
+
+        if (cc != null) {
+            return cc.getCallback();
+        }
+
+        return null;
+    }
+
+    private static InCallService.VideoCall getVideoCallById(String callId) {
+
+        CallContainer cc = mCallContainerMap.get(callId);
+
+        if (cc != null) {
+            return cc.getVideoCall();
+
+        }
+
+        return null;
+    }
+
+    private static VideoCallCallback
+            getVideoCallListenerById(String callId) {
+
+        CallContainer cc = mCallContainerMap.get(callId);
+
+        if (cc != null) {
+            return cc.getVideoCallCallback();
+        }
+
+        return null;
+    }
+
+    /*
+     * Public Call/Phone Functions
+     */
+
+    public static void callDisconnect(String callId) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            Log.d("callDisconnect: callId is null");
+            return;
+        }
+
+        c.disconnect();
+    }
+
+    public static void holdCall(String callId) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            Log.d("holdCall: callId is null");
+            return;
+        }
+        c.hold();
+    }
+
+    public static void mergeCallsInConference(String callId) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            Log.d("mergeCallsInConference: callId is null");
+            return;
+        }
+        c.mergeConference();
+    }
+
+    public static void splitCallFromConf(String callId) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            Log.d("splitCallFromConf: callId is null");
+            return;
+        }
+        c.splitFromConference();
+    }
+
+    public static void unholdCall(String callId) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            Log.d("unholdCall: callId is null");
+            return;
+        }
+        c.unhold();
+    }
+
+    public static void joinCallsInConf(String callIdOne, String callIdTwo) {
+        Call callOne = getCallById(callIdOne);
+        Call callTwo = getCallById(callIdTwo);
+
+        if (callOne == null || callTwo == null) {
+            Log.d("joinCallsInConf: callOne or CallTwo is null");
+            return;
+        }
+
+        callOne.conference(callTwo);
+    }
+
+    public static Set<String> getCallIdList() {
+        return mCallContainerMap.keySet();
+    }
+
+    public static void clearCallList() {
+        mCallContainerMap.clear();
+    }
+
+    public static String callGetState(String callId) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            return getCallStateString(STATE_INVALID);
+        }
+
+        return getCallStateString(c.getState());
+    }
+
+    public static Call.Details callGetDetails(String callId) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            Log.d(String.format("Couldn't find an active call with ID:%s", callId));
+            return null;
+        }
+
+        return c.getDetails();
+    }
+
+    public static List<String> callGetCallProperties(String callId) {
+        Call.Details details = callGetDetails(callId);
+
+        if (details == null) {
+            return null;
+        }
+
+        return getCallPropertiesString(details.getCallProperties());
+    }
+
+    public static List<String> callGetCallCapabilities(String callId) {
+        Call.Details details = callGetDetails(callId);
+
+        if (details == null) {
+            return null;
+        }
+
+        return getCallCapabilitiesString(details.getCallCapabilities());
+    }
+
+    @SuppressWarnings("deprecation")
+    public static void overrideProximitySensor(Boolean screenOn) {
+        InCallServiceImpl svc = getService();
+        if (svc == null) {
+            Log.d("overrideProximitySensor: InCallServiceImpl is null.");
+            return;
+        }
+
+        Phone phone = svc.getPhone();
+        if (phone == null) {
+            Log.d("overrideProximitySensor: phone is null.");
+            return;
+        }
+
+        phone.setProximitySensorOff(screenOn);
+    }
+
+    public static CallAudioState serviceGetCallAudioState() {
+        InCallServiceImpl svc = getService();
+
+        if (svc != null) {
+            return svc.getCallAudioState();
+        }
+        else {
+            return null;
+        }
+    }
+
+    // Wonky name due to conflict with internal function
+    public static void serviceSetAudioRoute(String route) {
+        InCallServiceImpl svc = getService();
+
+        if (svc == null) {
+            Log.d("serviceSetAudioRoute: InCallServiceImpl is null.");
+            return;
+        }
+
+        int r = getAudioRoute(route);
+
+        Log.d(String.format("Setting Audio Route to %s:%d", route, r));
+
+        if (r == INVALID_AUDIO_ROUTE) {
+            Log.d(String.format("Invalid Audio route %s:%d", route, r));
+            return;
+        }
+        svc.setAudioRoute(r);
+    }
+
+    public static void callStartListeningForEvent(String callId, String strEvent) {
+
+        CallCallback cl = getCallCallbackById(callId);
+
+        if (cl == null) {
+            Log.d("callStartListeningForEvent: CallCallback is null.");
+            return;
+        }
+
+        int event = getCallCallbackEvent(strEvent);
+
+        if (event == CallCallback.EVENT_INVALID) {
+            Log.d("callStartListeningForEvent: event is invalid.");
+            return;
+        }
+
+        cl.startListeningForEvents(event);
+    }
+
+    public static void callStopListeningForEvent(String callId, String strEvent) {
+        CallCallback cl = getCallCallbackById(callId);
+
+        if (cl == null) {
+            Log.d("callStopListeningForEvent: CallCallback is null.");
+            return;
+        }
+
+        int event = getCallCallbackEvent(strEvent);
+
+        if (event == CallCallback.EVENT_INVALID) {
+            Log.d("callStopListeningForEvent: event is invalid.");
+            return;
+        }
+
+        cl.stopListeningForEvents(event);
+    }
+
+    public static void videoCallStartListeningForEvent(String callId, String strEvent) {
+        VideoCallCallback cl = getVideoCallListenerById(callId);
+
+        if (cl == null) {
+            Log.d(String.format("Couldn't find a call with call id:%s", callId));
+            return;
+        }
+
+        int event = getVideoCallCallbackEvent(strEvent);
+
+        if (event == VideoCallCallback.EVENT_INVALID) {
+            Log.d(String.format("Failed to find a valid event:[%s]", strEvent));
+            return;
+        }
+
+        cl.startListeningForEvents(event);
+    }
+
+    public static void videoCallStopListeningForEvent(String callId, String strEvent) {
+        VideoCallCallback cl = getVideoCallListenerById(callId);
+
+        if (cl == null) {
+            Log.d("videoCallStopListeningForEvent: CallCallback is null.");
+            return;
+        }
+
+        int event = getVideoCallCallbackEvent(strEvent);
+
+        if (event == VideoCallCallback.EVENT_INVALID) {
+            Log.d("getVideoCallCallbackEvent: event is invalid.");
+            return;
+        }
+
+        cl.stopListeningForEvents(event);
+    }
+
+    public static String videoCallGetState(String callId) {
+        Call c = getCallById(callId);
+
+        int state = CallCallback.STATE_INVALID;
+
+        if (c == null) {
+            Log.d("videoCallGetState: call is null.");
+        }
+        else {
+            state = c.getDetails().getVideoState();
+        }
+
+        return getVideoCallStateString(state);
+    }
+
+    public static void videoCallSendSessionModifyRequest(
+            String callId, String videoStateString, String videoQualityString) {
+        VideoCall vc = getVideoCallById(callId);
+
+        if (vc == null) {
+            Log.d("Invalid video call for call ID");
+            return;
+        }
+
+        int videoState = getVideoCallState(videoStateString);
+        int videoQuality = getVideoCallQuality(videoQualityString);
+
+        Log.d(String.format("Sending Modify request for %s:%d, %s:%d",
+                videoStateString, videoState, videoQualityString, videoQuality));
+
+        if (videoState == CallCallback.STATE_INVALID ||
+                videoQuality == QUALITY_INVALID || videoQuality == VideoProfile.QUALITY_UNKNOWN) {
+            Log.d("Invalid session modify request!");
+            return;
+        }
+
+        vc.sendSessionModifyRequest(new VideoProfile(videoState, videoQuality));
+    }
+
+    public static void videoCallSendSessionModifyResponse(
+            String callId, String videoStateString, String videoQualityString) {
+        VideoCall vc = getVideoCallById(callId);
+
+        if (vc == null) {
+            Log.d("Invalid video call for call ID");
+            return;
+        }
+
+        int videoState = getVideoCallState(videoStateString);
+        int videoQuality = getVideoCallQuality(videoQualityString);
+
+        Log.d(String.format("Sending Modify request for %s:%d, %s:%d",
+                videoStateString, videoState, videoQualityString, videoQuality));
+
+        if (videoState == CallCallback.STATE_INVALID ||
+                videoQuality == QUALITY_INVALID || videoQuality == VideoProfile.QUALITY_UNKNOWN) {
+            Log.d("Invalid session modify request!");
+            return;
+        }
+
+        vc.sendSessionModifyResponse(new VideoProfile(videoState, videoQuality));
+    }
+
+    public static void callAnswer(String callId, String videoState) {
+        Call c = getCallById(callId);
+
+        if (c == null) {
+            Log.d("callAnswer: call is null.");
+        }
+
+        int state = getVideoCallState(videoState);
+
+        if (state == CallCallback.STATE_INVALID) {
+            Log.d("callAnswer: video state is invalid.");
+            state = VideoProfile.STATE_AUDIO_ONLY;
+        }
+
+        c.answer(state);
+    }
+
+    public static void callReject(String callId, String message) {
+        Call c = getCallById(callId);
+
+        if (c == null) {
+            Log.d("callReject: call is null.");
+        }
+
+        c.reject((message != null) ? true : false, message);
+    }
+
+    public static String getCallParent(String callId) {
+        Call c = getCallById(callId);
+
+        if (c == null) {
+            Log.d("getCallParent: call is null.");
+            return null;
+        }
+        Call callParent = c.getParent();
+        return getCallId(callParent);
+    }
+
+    public static List<String> getCallChildren(String callId) {
+        Call c = getCallById(callId);
+
+        if (c == null) {
+            Log.d("getCallChildren: call is null.");
+            return null;
+        }
+        List<String> childrenList = new ArrayList<String>();
+        List<Call> callChildren = c.getChildren();
+        for (Call call : callChildren) {
+            childrenList.add(getCallId(call));
+        }
+        return childrenList;
+    }
+
+    public static void swapCallsInConference(String callId) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            Log.d("swapCallsInConference: call is null.");
+            return;
+        }
+        c.swapConference();
+    }
+
+    public static void callPlayDtmfTone(String callId, char digit) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            Log.d("callPlayDtmfTone: call is null.");
+            return;
+        }
+        c.playDtmfTone(digit);
+    }
+
+    public static void callStopDtmfTone(String callId) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            Log.d("callStopDtmfTone: call is null.");
+            return;
+        }
+        c.stopDtmfTone();
+    }
+
+    public static List<String> callGetCannedTextResponses(String callId) {
+        Call c = getCallById(callId);
+        if (c == null) {
+            return null;
+        }
+
+        return c.getCannedTextResponses();
+    }
+
+    /*
+     * String Mapping Functions for Facade Parameter Translation
+     */
+
+    public static String getVideoCallStateString(int state) {
+        switch (state) {
+            case VIDEO_STATE_AUDIO_ONLY:
+                return TelephonyConstants.VT_STATE_AUDIO_ONLY;
+            case VIDEO_STATE_TX_ENABLED:
+                return TelephonyConstants.VT_STATE_TX_ENABLED;
+            case VIDEO_STATE_RX_ENABLED:
+                return TelephonyConstants.VT_STATE_RX_ENABLED;
+            case VIDEO_STATE_BIDIRECTIONAL:
+                return TelephonyConstants.VT_STATE_BIDIRECTIONAL;
+            case VIDEO_STATE_TX_PAUSED:
+                return TelephonyConstants.VT_STATE_TX_PAUSED;
+            case VIDEO_STATE_RX_PAUSED:
+                return TelephonyConstants.VT_STATE_RX_PAUSED;
+            case VIDEO_STATE_BIDIRECTIONAL_PAUSED:
+                return TelephonyConstants.VT_STATE_BIDIRECTIONAL_PAUSED;
+            default:
+        }
+        Log.d("getVideoCallStateString: state is invalid.");
+        return TelephonyConstants.VT_STATE_STATE_INVALID;
+    }
+
+    public static int getVideoCallState(String state) {
+        switch (state.toUpperCase()) {
+            case TelephonyConstants.VT_STATE_AUDIO_ONLY:
+                return VIDEO_STATE_AUDIO_ONLY;
+            case TelephonyConstants.VT_STATE_TX_ENABLED:
+                return VIDEO_STATE_TX_ENABLED;
+            case TelephonyConstants.VT_STATE_RX_ENABLED:
+                return VIDEO_STATE_RX_ENABLED;
+            case TelephonyConstants.VT_STATE_BIDIRECTIONAL:
+                return VIDEO_STATE_BIDIRECTIONAL;
+            case TelephonyConstants.VT_STATE_TX_PAUSED:
+                return VIDEO_STATE_TX_PAUSED;
+            case TelephonyConstants.VT_STATE_RX_PAUSED:
+                return VIDEO_STATE_RX_PAUSED;
+            case TelephonyConstants.VT_STATE_BIDIRECTIONAL_PAUSED:
+                return VIDEO_STATE_BIDIRECTIONAL_PAUSED;
+
+            default:
+        }
+        Log.d("getVideoCallState: state is invalid.");
+        return CallCallback.STATE_INVALID;
+    }
+
+    private static int getVideoCallQuality(String quality) {
+
+        switch (quality.toUpperCase()) {
+            case TelephonyConstants.VT_VIDEO_QUALITY_UNKNOWN:
+                return VideoProfile.QUALITY_UNKNOWN;
+            case TelephonyConstants.VT_VIDEO_QUALITY_HIGH:
+                return VideoProfile.QUALITY_HIGH;
+            case TelephonyConstants.VT_VIDEO_QUALITY_MEDIUM:
+                return VideoProfile.QUALITY_MEDIUM;
+            case TelephonyConstants.VT_VIDEO_QUALITY_LOW:
+                return VideoProfile.QUALITY_LOW;
+            case TelephonyConstants.VT_VIDEO_QUALITY_DEFAULT:
+                return VideoProfile.QUALITY_DEFAULT;
+            default:
+        }
+        Log.d("getVideoCallQuality: quality is invalid.");
+        return QUALITY_INVALID;
+    }
+
+    public static String getVideoCallQualityString(int quality) {
+        switch (quality) {
+            case VideoProfile.QUALITY_UNKNOWN:
+                return TelephonyConstants.VT_VIDEO_QUALITY_UNKNOWN;
+            case VideoProfile.QUALITY_HIGH:
+                return TelephonyConstants.VT_VIDEO_QUALITY_HIGH;
+            case VideoProfile.QUALITY_MEDIUM:
+                return TelephonyConstants.VT_VIDEO_QUALITY_MEDIUM;
+            case VideoProfile.QUALITY_LOW:
+                return TelephonyConstants.VT_VIDEO_QUALITY_LOW;
+            case VideoProfile.QUALITY_DEFAULT:
+                return TelephonyConstants.VT_VIDEO_QUALITY_DEFAULT;
+            default:
+        }
+        Log.d("getVideoCallQualityString: quality is invalid.");
+        return TelephonyConstants.VT_VIDEO_QUALITY_INVALID;
+    }
+
+    private static int getCallCallbackEvent(String event) {
+
+        switch (event.toUpperCase()) {
+            case "EVENT_STATE_CHANGED":
+                return CallCallback.EVENT_STATE_CHANGED;
+            case "EVENT_PARENT_CHANGED":
+                return CallCallback.EVENT_PARENT_CHANGED;
+            case "EVENT_CHILDREN_CHANGED":
+                return CallCallback.EVENT_CHILDREN_CHANGED;
+            case "EVENT_DETAILS_CHANGED":
+                return CallCallback.EVENT_DETAILS_CHANGED;
+            case "EVENT_CANNED_TEXT_RESPONSES_LOADED":
+                return CallCallback.EVENT_CANNED_TEXT_RESPONSES_LOADED;
+            case "EVENT_POST_DIAL_WAIT":
+                return CallCallback.EVENT_POST_DIAL_WAIT;
+            case "EVENT_VIDEO_CALL_CHANGED":
+                return CallCallback.EVENT_VIDEO_CALL_CHANGED;
+            case "EVENT_CALL_DESTROYED":
+                return CallCallback.EVENT_CALL_DESTROYED;
+            case "EVENT_CONFERENCABLE_CALLS_CHANGED":
+                return CallCallback.EVENT_CONFERENCABLE_CALLS_CHANGED;
+        }
+        Log.d("getCallCallbackEvent: event is invalid.");
+        return CallCallback.EVENT_INVALID;
+    }
+
+    public static String getCallCallbackEventString(int event) {
+
+        switch (event) {
+            case CallCallback.EVENT_STATE_CHANGED:
+                return "EVENT_STATE_CHANGED";
+            case CallCallback.EVENT_PARENT_CHANGED:
+                return "EVENT_PARENT_CHANGED";
+            case CallCallback.EVENT_CHILDREN_CHANGED:
+                return "EVENT_CHILDREN_CHANGED";
+            case CallCallback.EVENT_DETAILS_CHANGED:
+                return "EVENT_DETAILS_CHANGED";
+            case CallCallback.EVENT_CANNED_TEXT_RESPONSES_LOADED:
+                return "EVENT_CANNED_TEXT_RESPONSES_LOADED";
+            case CallCallback.EVENT_POST_DIAL_WAIT:
+                return "EVENT_POST_DIAL_WAIT";
+            case CallCallback.EVENT_VIDEO_CALL_CHANGED:
+                return "EVENT_VIDEO_CALL_CHANGED";
+            case CallCallback.EVENT_CALL_DESTROYED:
+                return "EVENT_CALL_DESTROYED";
+            case CallCallback.EVENT_CONFERENCABLE_CALLS_CHANGED:
+                return "EVENT_CONFERENCABLE_CALLS_CHANGED";
+        }
+        Log.d("getCallCallbackEventString: event is invalid.");
+        return "EVENT_INVALID";
+    }
+
+    private static int getVideoCallCallbackEvent(String event) {
+
+        switch (event) {
+            case TelephonyConstants.EVENT_VIDEO_SESSION_MODIFY_REQUEST_RECEIVED:
+                return VideoCallCallback.EVENT_SESSION_MODIFY_REQUEST_RECEIVED;
+            case TelephonyConstants.EVENT_VIDEO_SESSION_MODIFY_RESPONSE_RECEIVED:
+                return VideoCallCallback.EVENT_SESSION_MODIFY_RESPONSE_RECEIVED;
+            case TelephonyConstants.EVENT_VIDEO_SESSION_EVENT:
+                return VideoCallCallback.EVENT_SESSION_EVENT;
+            case TelephonyConstants.EVENT_VIDEO_PEER_DIMENSIONS_CHANGED:
+                return VideoCallCallback.EVENT_PEER_DIMENSIONS_CHANGED;
+            case TelephonyConstants.EVENT_VIDEO_QUALITY_CHANGED:
+                return VideoCallCallback.EVENT_VIDEO_QUALITY_CHANGED;
+            case TelephonyConstants.EVENT_VIDEO_DATA_USAGE_CHANGED:
+                return VideoCallCallback.EVENT_DATA_USAGE_CHANGED;
+            case TelephonyConstants.EVENT_VIDEO_CAMERA_CAPABILITIES_CHANGED:
+                return VideoCallCallback.EVENT_CAMERA_CAPABILITIES_CHANGED;
+        }
+        Log.d("getVideoCallCallbackEvent: event is invalid.");
+        return CallCallback.EVENT_INVALID;
+    }
+
+    public static String getVideoCallCallbackEventString(int event) {
+
+        switch (event) {
+            case VideoCallCallback.EVENT_SESSION_MODIFY_REQUEST_RECEIVED:
+                return TelephonyConstants.EVENT_VIDEO_SESSION_MODIFY_REQUEST_RECEIVED;
+            case VideoCallCallback.EVENT_SESSION_MODIFY_RESPONSE_RECEIVED:
+                return TelephonyConstants.EVENT_VIDEO_SESSION_MODIFY_RESPONSE_RECEIVED;
+            case VideoCallCallback.EVENT_SESSION_EVENT:
+                return TelephonyConstants.EVENT_VIDEO_SESSION_EVENT;
+            case VideoCallCallback.EVENT_PEER_DIMENSIONS_CHANGED:
+                return TelephonyConstants.EVENT_VIDEO_PEER_DIMENSIONS_CHANGED;
+            case VideoCallCallback.EVENT_VIDEO_QUALITY_CHANGED:
+                return TelephonyConstants.EVENT_VIDEO_QUALITY_CHANGED;
+            case VideoCallCallback.EVENT_DATA_USAGE_CHANGED:
+                return TelephonyConstants.EVENT_VIDEO_DATA_USAGE_CHANGED;
+            case VideoCallCallback.EVENT_CAMERA_CAPABILITIES_CHANGED:
+                return TelephonyConstants.EVENT_VIDEO_CAMERA_CAPABILITIES_CHANGED;
+        }
+        Log.d("getVideoCallCallbackEventString: event is invalid.");
+        return TelephonyConstants.EVENT_VIDEO_INVALID;
+    }
+
+    public static String getCallStateString(int state) {
+        switch (state) {
+            case Call.STATE_NEW:
+                return TelephonyConstants.CALL_STATE_NEW;
+            case Call.STATE_DIALING:
+                return TelephonyConstants.CALL_STATE_DIALING;
+            case Call.STATE_RINGING:
+                return TelephonyConstants.CALL_STATE_RINGING;
+            case Call.STATE_HOLDING:
+                return TelephonyConstants.CALL_STATE_HOLDING;
+            case Call.STATE_ACTIVE:
+                return TelephonyConstants.CALL_STATE_ACTIVE;
+            case Call.STATE_DISCONNECTED:
+                return TelephonyConstants.CALL_STATE_DISCONNECTED;
+            case Call.STATE_PRE_DIAL_WAIT:
+                return TelephonyConstants.CALL_STATE_PRE_DIAL_WAIT;
+            case Call.STATE_CONNECTING:
+                return TelephonyConstants.CALL_STATE_CONNECTING;
+            case Call.STATE_DISCONNECTING:
+                return TelephonyConstants.CALL_STATE_DISCONNECTING;
+            case STATE_INVALID:
+                return TelephonyConstants.CALL_STATE_INVALID;
+            default:
+                return TelephonyConstants.CALL_STATE_UNKNOWN;
+        }
+    }
+
+    private static int getAudioRoute(String audioRoute) {
+        switch (audioRoute.toUpperCase()) {
+            case TelephonyConstants.AUDIO_ROUTE_BLUETOOTH:
+                return CallAudioState.ROUTE_BLUETOOTH;
+            case TelephonyConstants.AUDIO_ROUTE_EARPIECE:
+                return CallAudioState.ROUTE_EARPIECE;
+            case TelephonyConstants.AUDIO_ROUTE_SPEAKER:
+                return CallAudioState.ROUTE_SPEAKER;
+            case TelephonyConstants.AUDIO_ROUTE_WIRED_HEADSET:
+                return CallAudioState.ROUTE_WIRED_HEADSET;
+            case TelephonyConstants.AUDIO_ROUTE_WIRED_OR_EARPIECE:
+                return CallAudioState.ROUTE_WIRED_OR_EARPIECE;
+            default:
+                return INVALID_AUDIO_ROUTE;
+        }
+    }
+
+    public static String getAudioRouteString(int audioRoute) {
+        return CallAudioState.audioRouteToString(audioRoute);
+    }
+
+    public static String getVideoCallSessionEventString(int event) {
+
+        switch (event) {
+            case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
+                return TelephonyConstants.SESSION_EVENT_RX_PAUSE;
+            case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
+                return TelephonyConstants.SESSION_EVENT_RX_RESUME;
+            case Connection.VideoProvider.SESSION_EVENT_TX_START:
+                return TelephonyConstants.SESSION_EVENT_TX_START;
+            case Connection.VideoProvider.SESSION_EVENT_TX_STOP:
+                return TelephonyConstants.SESSION_EVENT_TX_STOP;
+            case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
+                return TelephonyConstants.SESSION_EVENT_CAMERA_FAILURE;
+            case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
+                return TelephonyConstants.SESSION_EVENT_CAMERA_READY;
+            default:
+                return TelephonyConstants.SESSION_EVENT_UNKNOWN;
+        }
+    }
+
+    public static String getCallCapabilityString(int capability) {
+        switch (capability) {
+            case Call.Details.CAPABILITY_HOLD:
+                return TelephonyConstants.CALL_CAPABILITY_HOLD;
+            case Call.Details.CAPABILITY_SUPPORT_HOLD:
+                return TelephonyConstants.CALL_CAPABILITY_SUPPORT_HOLD;
+            case Call.Details.CAPABILITY_MERGE_CONFERENCE:
+                return TelephonyConstants.CALL_CAPABILITY_MERGE_CONFERENCE;
+            case Call.Details.CAPABILITY_SWAP_CONFERENCE:
+                return TelephonyConstants.CALL_CAPABILITY_SWAP_CONFERENCE;
+            case Call.Details.CAPABILITY_UNUSED_1:
+                return TelephonyConstants.CALL_CAPABILITY_UNUSED_1;
+            case Call.Details.CAPABILITY_RESPOND_VIA_TEXT:
+                return TelephonyConstants.CALL_CAPABILITY_RESPOND_VIA_TEXT;
+            case Call.Details.CAPABILITY_MUTE:
+                return TelephonyConstants.CALL_CAPABILITY_MUTE;
+            case Call.Details.CAPABILITY_MANAGE_CONFERENCE:
+                return TelephonyConstants.CALL_CAPABILITY_MANAGE_CONFERENCE;
+            case Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_RX:
+                return TelephonyConstants.CALL_CAPABILITY_SUPPORTS_VT_LOCAL_RX;
+            case Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX:
+                return TelephonyConstants.CALL_CAPABILITY_SUPPORTS_VT_LOCAL_TX;
+            case Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL:
+                return TelephonyConstants.CALL_CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL;
+            case Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX:
+                return TelephonyConstants.CALL_CAPABILITY_SUPPORTS_VT_REMOTE_RX;
+            case Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_TX:
+                return TelephonyConstants.CALL_CAPABILITY_SUPPORTS_VT_REMOTE_TX;
+            case Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL:
+                return TelephonyConstants.CALL_CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL;
+            case Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE:
+                return TelephonyConstants.CALL_CAPABILITY_SEPARATE_FROM_CONFERENCE;
+            case Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE:
+                return TelephonyConstants.CALL_CAPABILITY_DISCONNECT_FROM_CONFERENCE;
+            case Call.Details.CAPABILITY_SPEED_UP_MT_AUDIO:
+                return TelephonyConstants.CALL_CAPABILITY_SPEED_UP_MT_AUDIO;
+            case Call.Details.CAPABILITY_CAN_UPGRADE_TO_VIDEO:
+                return TelephonyConstants.CALL_CAPABILITY_CAN_UPGRADE_TO_VIDEO;
+            case Call.Details.CAPABILITY_CAN_PAUSE_VIDEO:
+                return TelephonyConstants.CALL_CAPABILITY_CAN_PAUSE_VIDEO;
+        }
+        return TelephonyConstants.CALL_CAPABILITY_UNKOWN;
+    }
+
+    public static List<String> getCallCapabilitiesString(int capabilities) {
+        final int[] capabilityConstants = new int[] {
+                Call.Details.CAPABILITY_HOLD,
+                Call.Details.CAPABILITY_SUPPORT_HOLD,
+                Call.Details.CAPABILITY_MERGE_CONFERENCE,
+                Call.Details.CAPABILITY_SWAP_CONFERENCE,
+                Call.Details.CAPABILITY_UNUSED_1,
+                Call.Details.CAPABILITY_RESPOND_VIA_TEXT,
+                Call.Details.CAPABILITY_MUTE,
+                Call.Details.CAPABILITY_MANAGE_CONFERENCE,
+                Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_RX,
+                Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX,
+                Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL,
+                Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX,
+                Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_TX,
+                Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL,
+                Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE,
+                Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE,
+                Call.Details.CAPABILITY_SPEED_UP_MT_AUDIO,
+                Call.Details.CAPABILITY_CAN_UPGRADE_TO_VIDEO,
+                Call.Details.CAPABILITY_CAN_PAUSE_VIDEO
+        };
+
+        List<String> capabilityList = new ArrayList<String>();
+
+        for (int capability : capabilityConstants) {
+            if ((capabilities & capability) == capability) {
+                capabilityList.add(getCallCapabilityString(capability));
+            }
+        }
+        return capabilityList;
+    }
+
+    public static String getCallPropertyString(int property) {
+
+        switch (property) {
+            case Call.Details.PROPERTY_CONFERENCE:
+                return TelephonyConstants.CALL_PROPERTY_CONFERENCE;
+            case Call.Details.PROPERTY_GENERIC_CONFERENCE:
+                return TelephonyConstants.CALL_PROPERTY_GENERIC_CONFERENCE;
+            case Call.Details.PROPERTY_EMERGENCY_CALLBACK_MODE:
+                return TelephonyConstants.CALL_PROPERTY_EMERGENCY_CALLBACK_MODE;
+            case Call.Details.PROPERTY_WIFI:
+                return TelephonyConstants.CALL_PROPERTY_WIFI;
+            case Call.Details.PROPERTY_HIGH_DEF_AUDIO:
+                return TelephonyConstants.CALL_PROPERTY_HIGH_DEF_AUDIO;
+            default:
+                return TelephonyConstants.CALL_PROPERTY_UNKNOWN;
+        }
+    }
+
+    public static List<String> getCallPropertiesString(int properties) {
+        final int[] propertyConstants = new int[] {
+                Call.Details.PROPERTY_CONFERENCE,
+                Call.Details.PROPERTY_GENERIC_CONFERENCE,
+                Call.Details.PROPERTY_EMERGENCY_CALLBACK_MODE,
+                Call.Details.PROPERTY_WIFI,
+                Call.Details.PROPERTY_HIGH_DEF_AUDIO
+        };
+
+        List<String> propertyList = new ArrayList<String>();
+
+        for (int property : propertyConstants) {
+            if ((properties & property) == property) {
+                propertyList.add(getCallPropertyString(property));
+            }
+        }
+
+        return propertyList;
+    }
+
+    public static String getCallPresentationInfoString(int presentation) {
+        switch (presentation) {
+            case TelecomManager.PRESENTATION_ALLOWED:
+                return TelephonyConstants.CALL_PRESENTATION_ALLOWED;
+            case TelecomManager.PRESENTATION_RESTRICTED:
+                return TelephonyConstants.CALL_PRESENTATION_RESTRICTED;
+            case TelecomManager.PRESENTATION_PAYPHONE:
+                return TelephonyConstants.CALL_PRESENTATION_PAYPHONE;
+            default:
+                return TelephonyConstants.CALL_PRESENTATION_UNKNOWN;
+        }
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/SmsFacade.java b/Common/src/com/googlecode/android_scripting/facade/telephony/SmsFacade.java
new file mode 100644
index 0000000..1744d2b
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/SmsFacade.java
@@ -0,0 +1,990 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+import com.google.android.mms.ContentType;
+import com.google.android.mms.InvalidHeaderValueException;
+import com.google.android.mms.pdu.CharacterSets;
+import com.google.android.mms.pdu.EncodedStringValue;
+import com.google.android.mms.pdu.GenericPdu;
+import com.google.android.mms.pdu.PduBody;
+import com.google.android.mms.pdu.PduComposer;
+import com.google.android.mms.pdu.PduHeaders;
+import com.google.android.mms.pdu.PduParser;
+import com.google.android.mms.pdu.PduPart;
+import com.google.android.mms.pdu.PduPersister;
+import com.google.android.mms.pdu.SendConf;
+import com.google.android.mms.pdu.SendReq;
+import com.google.android.mms.MmsException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Telephony.Sms.Intents;
+import android.provider.Telephony.Mms;
+import android.telephony.SmsManager;
+import android.telephony.SmsMessage;
+import android.telephony.SmsCbMessage;
+import com.android.internal.telephony.gsm.SmsCbConstants;
+import com.android.internal.telephony.cdma.sms.SmsEnvelope;
+import android.telephony.SmsCbEtwsInfo;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+import com.googlecode.android_scripting.facade.telephony.TelephonyConstants;
+
+//FIXME: Change the build order to use constants defined in here
+//import com.googlecode.android_scripting.provider.TelephonyTestProvider;
+
+/**
+ * Exposes SmsManager functionality.
+ */
+public class SmsFacade extends RpcReceiver {
+
+    static final boolean DBG = false;
+
+    private final EventFacade mEventFacade;
+    private final SmsManager mSms;
+    private final Context mContext;
+    private final Service mService;
+    private BroadcastReceiver mSmsSendListener;
+    private BroadcastReceiver mSmsIncomingListener;
+    private int mNumExpectedSentEvents;
+    private int mNumExpectedDeliveredEvents;
+    private boolean mListeningIncomingSms;
+    private IntentFilter mEmergencyCBMessage;
+    private BroadcastReceiver mGsmEmergencyCBMessageListener;
+    private BroadcastReceiver mCdmaEmergencyCBMessageListener;
+    private boolean mGsmEmergencyCBListenerRegistered;
+    private boolean mCdmaEmergencyCBListenerRegistered;
+    private boolean mSentReceiversRegistered;
+    private Object lock = new Object();
+
+    private BroadcastReceiver mMmsSendListener;
+    private BroadcastReceiver mMmsIncomingListener;
+    private boolean mListeningIncomingMms;
+
+    TelephonyManager mTelephonyManager;
+
+    private static final String SMS_MESSAGE_STATUS_DELIVERED_ACTION =
+            "com.googlecode.android_scripting.sms.MESSAGE_STATUS_DELIVERED";
+    private static final String SMS_MESSAGE_SENT_ACTION =
+            "com.googlecode.android_scripting.sms.MESSAGE_SENT";
+
+    private static final String EMERGENCY_CB_MESSAGE_RECEIVED_ACTION =
+            "android.provider.Telephony.SMS_EMERGENCY_CB_RECEIVED";
+
+    private static final String MMS_MESSAGE_SENT_ACTION =
+            "com.googlecode.android_scripting.mms.MESSAGE_SENT";
+
+    private final int MAX_MESSAGE_LENGTH = 160;
+    private final int INTERNATIONAL_NUMBER_LENGTH = 12;
+    private final int DOMESTIC_NUMBER_LENGTH = 10;
+
+    private static final String DEFAULT_FROM_PHONE_NUMBER = new String("8675309");
+
+    private final int[] mGsmCbMessageIdList = {
+            SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_WARNING,
+            SmsCbConstants.MESSAGE_ID_ETWS_TSUNAMI_WARNING,
+            SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_AND_TSUNAMI_WARNING,
+            SmsCbConstants.MESSAGE_ID_ETWS_TEST_MESSAGE,
+            SmsCbConstants.MESSAGE_ID_ETWS_OTHER_EMERGENCY_TYPE,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST,
+            SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE
+    };
+
+    private final int[] mCdmaCbMessageIdList = {
+            SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT,
+            SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
+            SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT,
+            SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY,
+            SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE
+    };
+
+    public SmsFacade(FacadeManager manager) {
+
+        super(manager);
+        mService = manager.getService();
+        mContext = mService;
+        mSms = SmsManager.getDefault();
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mSmsSendListener = new SmsSendListener();
+        mSmsIncomingListener = new SmsIncomingListener();
+        mNumExpectedSentEvents = 0;
+        mNumExpectedDeliveredEvents = 0;
+        mListeningIncomingSms = false;
+        mGsmEmergencyCBMessageListener = new SmsEmergencyCBMessageListener();
+        mCdmaEmergencyCBMessageListener = new SmsEmergencyCBMessageListener();
+        mGsmEmergencyCBListenerRegistered = false;
+        mCdmaEmergencyCBListenerRegistered = false;
+        mSentReceiversRegistered = false;
+
+        mMmsIncomingListener = new MmsIncomingListener();
+        mMmsSendListener = new MmsSendListener();
+
+        mListeningIncomingMms = false;
+
+        IntentFilter smsFilter = new IntentFilter(SMS_MESSAGE_SENT_ACTION);
+        smsFilter.addAction(SMS_MESSAGE_STATUS_DELIVERED_ACTION);
+
+        IntentFilter mmsFilter = new IntentFilter(MMS_MESSAGE_SENT_ACTION);
+
+        synchronized (lock) {
+            mService.registerReceiver(mSmsSendListener, smsFilter);
+            mService.registerReceiver(mMmsSendListener, mmsFilter);
+            mSentReceiversRegistered = true;
+        }
+
+        mTelephonyManager =
+                (TelephonyManager) mService.getSystemService(Context.TELEPHONY_SERVICE);
+    }
+
+    // FIXME: Move to a utility class
+    // FIXME: remove the MODE_WORLD_READABLE once we verify the use case
+    @SuppressWarnings("deprecation")
+    private boolean writeBytesToFile(String fileName, byte[] pdu) {
+        FileOutputStream writer = null;
+        try {
+            writer = mContext.openFileOutput(fileName, Context.MODE_WORLD_READABLE);
+            writer.write(pdu);
+            return true;
+        } catch (final IOException e) {
+            return false;
+        } finally {
+            if (writer != null) {
+                try {
+                    writer.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+    }
+
+    // FIXME: Move to a utility class
+    private boolean writeBytesToCacheFile(String fileName, byte[] pdu) {
+        File mmsFile = new File(mContext.getCacheDir(), fileName);
+        Log.d(String.format("filename:%s, directory:%s", fileName,
+                mContext.getCacheDir().toString()));
+        FileOutputStream writer = null;
+        try {
+            writer = new FileOutputStream(mmsFile);
+            writer.write(pdu);
+            return true;
+        } catch (final IOException e) {
+            Log.d("writeBytesToCacheFile() failed with " + e.toString());
+            return false;
+        } finally {
+            if (writer != null) {
+                try {
+                    writer.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+    }
+
+    @Deprecated
+    @Rpc(description = "Starts tracking incoming SMS.")
+    public void smsStartTrackingIncomingMessage() {
+        Log.d("Using Deprecated smsStartTrackingIncomingMessage!");
+        smsStartTrackingIncomingSmsMessage();
+    }
+
+    @Rpc(description = "Starts tracking incoming SMS.")
+    public void smsStartTrackingIncomingSmsMessage() {
+        mService.registerReceiver(mSmsIncomingListener,
+                new IntentFilter(Intents.SMS_RECEIVED_ACTION));
+        mListeningIncomingSms = true;
+    }
+
+    @Deprecated
+    @Rpc(description = "Stops tracking incoming SMS.")
+    public void smsStopTrackingIncomingMessage() {
+        Log.d("Using Deprecated smsStopTrackingIncomingMessage!");
+        smsStopTrackingIncomingSmsMessage();
+    }
+
+    @Rpc(description = "Stops tracking incoming SMS.")
+    public void smsStopTrackingIncomingSmsMessage() {
+        if (mListeningIncomingSms) {
+            mListeningIncomingSms = false;
+            try {
+                mService.unregisterReceiver(mSmsIncomingListener);
+            } catch (Exception e) {
+                Log.e("Tried to unregister nonexistent SMS Listener!");
+            }
+        }
+    }
+
+    @Rpc(description = "Starts tracking incoming MMS.")
+    public void smsStartTrackingIncomingMmsMessage() {
+        IntentFilter mmsReceived = new IntentFilter(Intents.MMS_DOWNLOADED_ACTION);
+        mmsReceived.addAction(Intents.WAP_PUSH_RECEIVED_ACTION);
+        mmsReceived.addAction(Intents.DATA_SMS_RECEIVED_ACTION);
+        mService.registerReceiver(mMmsIncomingListener, mmsReceived);
+        mListeningIncomingSms = true;
+    }
+
+    @Rpc(description = "Stops tracking incoming MMS.")
+    public void smsStopTrackingIncomingMmsMessage() {
+        if (mListeningIncomingMms) {
+            mListeningIncomingMms = false;
+            try {
+                mService.unregisterReceiver(mMmsIncomingListener);
+            } catch (Exception e) {
+                Log.e("Tried to unregister nonexistent MMS Listener!");
+            }
+        }
+    }
+
+    // Currently requires 'adb shell su root setenforce 0'
+    @Rpc(description = "Send a multimedia message to a specified number.")
+    public void smsSendMultimediaMessage(
+                        @RpcParameter(name = "toPhoneNumber")
+            String toPhoneNumber,
+                        @RpcParameter(name = "subject")
+            String subject,
+                        @RpcParameter(name = "message")
+            String message,
+            @RpcParameter(name = "fromPhoneNumber")
+            @RpcOptional
+            String fromPhoneNumber,
+            @RpcParameter(name = "fileName")
+            @RpcOptional
+            String fileName) {
+
+        MmsBuilder mms = new MmsBuilder();
+
+        mms.setToPhoneNumber(toPhoneNumber);
+        if (fromPhoneNumber == null) {
+            mTelephonyManager.getLine1Number(); //TODO: b/21592513 - multi-sim awareness
+        }
+
+        if (DBG) {
+            Log.d(String.format(
+                    "Params:toPhoneNumber(%s),subject(%s),message(%s),fromPhoneNumber(%s),filename(%s)",
+                    toPhoneNumber, subject, message,
+                    (fromPhoneNumber != null) ? fromPhoneNumber : "",
+                            (fileName != null) ? fileName : ""));
+        }
+
+        mms.setFromPhoneNumber((fromPhoneNumber != null) ? fromPhoneNumber : DEFAULT_FROM_PHONE_NUMBER);
+        mms.setSubject(subject);
+        mms.setDate();
+        mms.addMessageBody(message);
+        mms.setMessageClass(MmsBuilder.MESSAGE_CLASS_PERSONAL);
+        mms.setMessagePriority(MmsBuilder.DEFAULT_PRIORITY);
+        mms.setDeliveryReport(true);
+        mms.setReadReport(true);
+        // Default to 1 week;
+        mms.setExpirySeconds(MmsBuilder.DEFAULT_EXPIRY_TIME);
+
+        Uri contentUri = null;
+
+        String randomFileName = "mms." + String.valueOf(System.currentTimeMillis()) + ".dat";
+
+        byte[] mmsBytes = mms.build();
+        if (mmsBytes.length == 0) {
+            Log.e("Failed to build PDU!");
+            return;
+        }
+
+        if (writeBytesToCacheFile(randomFileName, mmsBytes) == false) {
+            Log.e("Failed to write PDU to file");
+            return;
+        }
+
+        contentUri = (new Uri.Builder())
+                .authority("com.googlecode.android_scripting.provider.telephonytestprovider")
+                .path("mms/" + randomFileName)
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .build();
+
+        if (contentUri != null) {
+            Log.d(String.format("URI String: %s", contentUri.toString()));
+
+            SmsManager.getDefault().sendMultimediaMessage(mContext,
+                    contentUri, null/* locationUrl */, null/* configOverrides */,
+                    PendingIntent.getBroadcast(mService, 0,
+                            new Intent(MMS_MESSAGE_SENT_ACTION), 0)
+                    );
+        }
+        else {
+            Log.d("smsSendMultimediaMessage():Content URI String is null");
+        }
+    }
+
+    @Rpc(description = "Send a text message to a specified number.")
+    public void smsSendTextMessage(
+                        @RpcParameter(name = "phoneNumber")
+            String phoneNumber,
+                        @RpcParameter(name = "message")
+            String message,
+                        @RpcParameter(name = "deliveryReportRequired")
+            Boolean deliveryReportRequired) {
+
+        if (message.length() > MAX_MESSAGE_LENGTH) {
+            ArrayList<String> messagesParts = mSms.divideMessage(message);
+            mNumExpectedSentEvents = mNumExpectedDeliveredEvents = messagesParts.size();
+            ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>();
+            ArrayList<PendingIntent> deliveredIntents = new ArrayList<PendingIntent>();
+            for (int i = 0; i < messagesParts.size(); i++) {
+                sentIntents.add(PendingIntent.getBroadcast(mService, 0,
+                        new Intent(SMS_MESSAGE_SENT_ACTION), 0));
+                if (deliveryReportRequired) {
+                    deliveredIntents.add(
+                            PendingIntent.getBroadcast(mService, 0,
+                                    new Intent(SMS_MESSAGE_STATUS_DELIVERED_ACTION), 0));
+                }
+            }
+            mSms.sendMultipartTextMessage(
+                    phoneNumber, null, messagesParts,
+                    sentIntents, deliveryReportRequired ? deliveredIntents : null);
+        } else {
+            mNumExpectedSentEvents = mNumExpectedDeliveredEvents = 1;
+            PendingIntent sentIntent = PendingIntent.getBroadcast(mService, 0,
+                    new Intent(SMS_MESSAGE_SENT_ACTION), 0);
+            PendingIntent deliveredIntent = PendingIntent.getBroadcast(mService, 0,
+                    new Intent(SMS_MESSAGE_STATUS_DELIVERED_ACTION), 0);
+            mSms.sendTextMessage(
+                    phoneNumber, null, message, sentIntent,
+                    deliveryReportRequired ? deliveredIntent : null);
+        }
+    }
+
+    @Rpc(description = "Retrieves all messages currently stored on ICC.")
+    public ArrayList<SmsMessage> smsGetAllMessagesFromIcc() {
+        return SmsManager.getDefault().getAllMessagesFromIcc();
+    }
+
+    @Rpc(description = "Starts tracking GSM Emergency CB Messages.")
+    public void smsStartTrackingGsmEmergencyCBMessage() {
+        if (!mGsmEmergencyCBListenerRegistered) {
+            for (int messageId : mGsmCbMessageIdList) {
+                mSms.enableCellBroadcast(
+                        messageId,
+                        SmsManager.CELL_BROADCAST_RAN_TYPE_GSM);
+            }
+
+            mEmergencyCBMessage = new IntentFilter(EMERGENCY_CB_MESSAGE_RECEIVED_ACTION);
+            mService.registerReceiver(mGsmEmergencyCBMessageListener,
+                    mEmergencyCBMessage);
+            mGsmEmergencyCBListenerRegistered = true;
+        }
+    }
+
+    @Rpc(description = "Stop tracking GSM Emergency CB Messages")
+    public void smsStopTrackingGsmEmergencyCBMessage() {
+        if (mGsmEmergencyCBListenerRegistered) {
+            mService.unregisterReceiver(mGsmEmergencyCBMessageListener);
+            mGsmEmergencyCBListenerRegistered = false;
+            for (int messageId : mGsmCbMessageIdList) {
+                mSms.disableCellBroadcast(
+                        messageId,
+                        SmsManager.CELL_BROADCAST_RAN_TYPE_GSM);
+            }
+        }
+    }
+
+    @Rpc(description = "Starts tracking CDMA Emergency CB Messages")
+    public void smsStartTrackingCdmaEmergencyCBMessage() {
+        if (!mCdmaEmergencyCBListenerRegistered) {
+            for (int messageId : mCdmaCbMessageIdList) {
+                mSms.enableCellBroadcast(
+                        messageId,
+                        SmsManager.CELL_BROADCAST_RAN_TYPE_CDMA);
+            }
+            mEmergencyCBMessage = new IntentFilter(EMERGENCY_CB_MESSAGE_RECEIVED_ACTION);
+            mService.registerReceiver(mCdmaEmergencyCBMessageListener,
+                    mEmergencyCBMessage);
+            mCdmaEmergencyCBListenerRegistered = true;
+        }
+    }
+
+    @Rpc(description = "Stop tracking CDMA Emergency CB Message.")
+    public void smsStopTrackingCdmaEmergencyCBMessage() {
+        if (mCdmaEmergencyCBListenerRegistered) {
+            mService.unregisterReceiver(mCdmaEmergencyCBMessageListener);
+            mCdmaEmergencyCBListenerRegistered = false;
+            for (int messageId : mCdmaCbMessageIdList) {
+                mSms.disableCellBroadcast(
+                        messageId,
+                        SmsManager.CELL_BROADCAST_RAN_TYPE_CDMA);
+            }
+        }
+    }
+
+    private class SmsSendListener extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Bundle event = new Bundle();
+            event.putString("Type", "SmsDeliverStatus");
+            String action = intent.getAction();
+            int resultCode = getResultCode();
+            if (SMS_MESSAGE_STATUS_DELIVERED_ACTION.equals(action)) {
+                if (resultCode == Activity.RESULT_OK) {
+                    if (mNumExpectedDeliveredEvents == 1) {
+                        Log.d("SMS Message delivered successfully");
+                        mEventFacade.postEvent(TelephonyConstants.EventSmsDeliverSuccess, event);
+                    }
+                    if (mNumExpectedDeliveredEvents > 0) {
+                        mNumExpectedDeliveredEvents--;
+                    }
+                } else {
+                    Log.e("SMS Message delivery failed");
+                    // TODO . Need to find the reason for failure from pdu
+                    mEventFacade.postEvent(TelephonyConstants.EventSmsDeliverFailure, event);
+                }
+            } else if (SMS_MESSAGE_SENT_ACTION.equals(action)) {
+                if (resultCode == Activity.RESULT_OK) {
+                    if (mNumExpectedSentEvents == 1) {
+                        event.putString("Type", "SmsSentSuccess");
+                        Log.d("SMS Message sent successfully");
+                        mEventFacade.postEvent(TelephonyConstants.EventSmsSentSuccess, event);
+                    }
+                    if (mNumExpectedSentEvents > 0) {
+                        mNumExpectedSentEvents--;
+                    }
+                } else {
+                    Log.e("SMS Message send failed");
+                    event.putString("Type", "SmsSentFailure");
+                    switch (resultCode) {
+                        case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
+                            event.putString("Reason", "GenericFailure");
+                            break;
+                        case SmsManager.RESULT_ERROR_RADIO_OFF:
+                            event.putString("Reason", "RadioOff");
+                            break;
+                        case SmsManager.RESULT_ERROR_NULL_PDU:
+                            event.putString("Reason", "NullPdu");
+                            break;
+                        case SmsManager.RESULT_ERROR_NO_SERVICE:
+                            event.putString("Reason", "NoService");
+                            break;
+                        case SmsManager.RESULT_ERROR_LIMIT_EXCEEDED:
+                            event.putString("Reason", "LimitExceeded");
+                            break;
+                        case SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE:
+                            event.putString("Reason", "FdnCheckFailure");
+                            break;
+                        default:
+                            event.putString("Reason", "Unknown");
+                            break;
+                    }
+                    mEventFacade.postEvent(TelephonyConstants.EventSmsSentFailure, event);
+                }
+            }
+        }
+    }
+
+    private class SmsIncomingListener extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intents.SMS_RECEIVED_ACTION.equals(action)) {
+                Log.d("New SMS Received");
+                Bundle extras = intent.getExtras();
+                int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+                if (extras != null) {
+                    Bundle event = new Bundle();
+                    event.putString("Type", "NewSmsReceived");
+                    SmsMessage[] msgs = Intents.getMessagesFromIntent(intent);
+                    StringBuilder smsMsg = new StringBuilder();
+
+                    SmsMessage sms = msgs[0];
+                    String sender = sms.getOriginatingAddress();
+                    event.putString("Sender", formatPhoneNumber(sender));
+
+                    for (int i = 0; i < msgs.length; i++) {
+                        sms = msgs[i];
+                        smsMsg.append(sms.getMessageBody());
+                    }
+                    event.putString("Text", smsMsg.toString());
+                    // TODO
+                    // Need to explore how to get subId information.
+                    event.putInt("subscriptionId", subId);
+                    mEventFacade.postEvent(TelephonyConstants.EventSmsReceived, event);
+                }
+            }
+        }
+    }
+
+    private class MmsSendListener extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Bundle event = new Bundle();
+            String action = intent.getAction();
+            int resultCode = getResultCode();
+            event.putString("ResultCode", Integer.toString(resultCode));
+            if (MMS_MESSAGE_SENT_ACTION.equals(action)) {
+                if (resultCode == Activity.RESULT_OK) {
+                    Log.d("MMS Message sent successfully");
+                    mEventFacade.postEvent(TelephonyConstants.EventMmsSentSuccess, event);
+                } else {
+                    Log.e(String.format("MMS Message send failed: %d", resultCode));
+                    mEventFacade.postEvent(TelephonyConstants.EventMmsSentFailure, event);
+                }
+            } else {
+                Log.e("MMS Send Listener Received Invalid Event" + intent.toString());
+            }
+        }
+    }
+
+    // b/21569494 - Never receiving ANY of these events: requires debugging
+    private class MmsIncomingListener extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.d("MmsIncomingListener Received an Intent " + intent.toString());
+            String action = intent.getAction();
+            if (Intents.MMS_DOWNLOADED_ACTION.equals(action)) {
+                Log.d("New MMS Downloaded");
+                mEventFacade.postEvent(TelephonyConstants.EventMmsDownloaded, new Bundle());
+            }
+            else if (Intents.WAP_PUSH_RECEIVED_ACTION.equals(action)) {
+                Log.d("New Wap Push Received");
+                mEventFacade.postEvent(TelephonyConstants.EventWapPushReceived, new Bundle());
+            }
+            else if (Intents.DATA_SMS_RECEIVED_ACTION.equals(action)) {
+                Log.d("New Data SMS Received");
+                mEventFacade.postEvent(TelephonyConstants.EventDataSmsReceived, new Bundle());
+            }
+            else {
+                Log.e("MmsIncomingListener Received Unexpected Event" + intent.toString());
+            }
+        }
+    }
+
+    String formatPhoneNumber(String phoneNumber) {
+        String senderNumberStr = null;
+        int len = phoneNumber.length();
+        if (len > 0) {
+            /**
+             * Currently this incomingNumber modification is specific for US numbers.
+             */
+            if ((INTERNATIONAL_NUMBER_LENGTH == len) && ('+' == phoneNumber.charAt(0))) {
+                senderNumberStr = phoneNumber.substring(1);
+            } else if (DOMESTIC_NUMBER_LENGTH == len) {
+                senderNumberStr = '1' + phoneNumber;
+            } else {
+                senderNumberStr = phoneNumber;
+            }
+        }
+        return senderNumberStr;
+    }
+
+    private class SmsEmergencyCBMessageListener extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (EMERGENCY_CB_MESSAGE_RECEIVED_ACTION.equals(intent.getAction())) {
+                Bundle extras = intent.getExtras();
+                if (extras != null) {
+                    Bundle event = new Bundle();
+                    String eventName = null;
+                    SmsCbMessage message = (SmsCbMessage) extras.get("message");
+                    if (message != null) {
+                        if (message.isEmergencyMessage()) {
+                            event.putString("geographicalScope", getGeographicalScope(
+                                    message.getGeographicalScope()));
+                            event.putInt("serialNumber", message.getSerialNumber());
+                            event.putString("location", message.getLocation().toString());
+                            event.putInt("serviceCategory", message.getServiceCategory());
+                            event.putString("language", message.getLanguageCode());
+                            event.putString("message", message.getMessageBody());
+                            event.putString("priority", getPriority(message.getMessagePriority()));
+                            if (message.isCmasMessage()) {
+                                // CMAS message
+                                eventName = TelephonyConstants.EventCmasReceived;
+                                event.putString("cmasMessageClass", getCMASMessageClass(
+                                        message.getCmasWarningInfo().getMessageClass()));
+                                event.putString("cmasCategory", getCMASCategory(
+                                        message.getCmasWarningInfo().getCategory()));
+                                event.putString("cmasResponseType", getCMASResponseType(
+                                        message.getCmasWarningInfo().getResponseType()));
+                                event.putString("cmasSeverity", getCMASSeverity(
+                                        message.getCmasWarningInfo().getSeverity()));
+                                event.putString("cmasUrgency", getCMASUrgency(
+                                        message.getCmasWarningInfo().getUrgency()));
+                                event.putString("cmasCertainty", getCMASCertainty(
+                                        message.getCmasWarningInfo().getCertainty()));
+                            } else if (message.isEtwsMessage()) {
+                                // ETWS message
+                                eventName = TelephonyConstants.EventEtwsReceived;
+                                event.putString("etwsWarningType", getETWSWarningType(
+                                        message.getEtwsWarningInfo().getWarningType()));
+                                event.putBoolean("etwsIsEmergencyUserAlert",
+                                        message.getEtwsWarningInfo().isEmergencyUserAlert());
+                                event.putBoolean("etwsActivatePopup",
+                                        message.getEtwsWarningInfo().isPopupAlert());
+                            } else {
+                                Log.d("Received message is not CMAS or ETWS");
+                            }
+                            if (eventName != null)
+                                mEventFacade.postEvent(eventName, event);
+                        }
+                    }
+                } else {
+                    Log.d("Received  Emergency CB without extras");
+                }
+            }
+        }
+    }
+
+    private static String getETWSWarningType(int type) {
+        switch (type) {
+            case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE:
+                return "EARTHQUAKE";
+            case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI:
+                return "TSUNAMI";
+            case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
+                return "EARTHQUAKE_AND_TSUNAMI";
+            case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE:
+                return "TEST_MESSAGE";
+            case SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY:
+                return "OTHER_EMERGENCY";
+        }
+        return "UNKNOWN";
+    }
+
+    private static String getCMASMessageClass(int messageclass) {
+        switch (messageclass) {
+            case SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT:
+                return "PRESIDENTIAL_LEVEL_ALERT";
+            case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT:
+                return "EXTREME_THREAT";
+            case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT:
+                return "SEVERE_THREAT";
+            case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY:
+                return "CHILD_ABDUCTION_EMERGENCY";
+            case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST:
+                return "REQUIRED_MONTHLY_TEST";
+            case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE:
+                return "CMAS_EXERCISE";
+        }
+        return "UNKNOWN";
+    }
+
+    private static String getCMASCategory(int category) {
+        switch (category) {
+            case SmsCbCmasInfo.CMAS_CATEGORY_GEO:
+                return "GEOPHYSICAL";
+            case SmsCbCmasInfo.CMAS_CATEGORY_MET:
+                return "METEOROLOGICAL";
+            case SmsCbCmasInfo.CMAS_CATEGORY_SAFETY:
+                return "SAFETY";
+            case SmsCbCmasInfo.CMAS_CATEGORY_SECURITY:
+                return "SECURITY";
+            case SmsCbCmasInfo.CMAS_CATEGORY_RESCUE:
+                return "RESCUE";
+            case SmsCbCmasInfo.CMAS_CATEGORY_FIRE:
+                return "FIRE";
+            case SmsCbCmasInfo.CMAS_CATEGORY_HEALTH:
+                return "HEALTH";
+            case SmsCbCmasInfo.CMAS_CATEGORY_ENV:
+                return "ENVIRONMENTAL";
+            case SmsCbCmasInfo.CMAS_CATEGORY_TRANSPORT:
+                return "TRANSPORTATION";
+            case SmsCbCmasInfo.CMAS_CATEGORY_INFRA:
+                return "INFRASTRUCTURE";
+            case SmsCbCmasInfo.CMAS_CATEGORY_CBRNE:
+                return "CHEMICAL";
+            case SmsCbCmasInfo.CMAS_CATEGORY_OTHER:
+                return "OTHER";
+        }
+        return "UNKNOWN";
+    }
+
+    private static String getCMASResponseType(int type) {
+        switch (type) {
+            case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_SHELTER:
+                return "SHELTER";
+            case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_EVACUATE:
+                return "EVACUATE";
+            case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_PREPARE:
+                return "PREPARE";
+            case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_EXECUTE:
+                return "EXECUTE";
+            case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_MONITOR:
+                return "MONITOR";
+            case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_AVOID:
+                return "AVOID";
+            case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_ASSESS:
+                return "ASSESS";
+            case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_NONE:
+                return "NONE";
+        }
+        return "UNKNOWN";
+    }
+
+    private static String getCMASSeverity(int severity) {
+        switch (severity) {
+            case SmsCbCmasInfo.CMAS_SEVERITY_EXTREME:
+                return "EXTREME";
+            case SmsCbCmasInfo.CMAS_SEVERITY_SEVERE:
+                return "SEVERE";
+        }
+        return "UNKNOWN";
+    }
+
+    private static String getCMASUrgency(int urgency) {
+        switch (urgency) {
+            case SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE:
+                return "IMMEDIATE";
+            case SmsCbCmasInfo.CMAS_URGENCY_EXPECTED:
+                return "EXPECTED";
+        }
+        return "UNKNOWN";
+    }
+
+    private static String getCMASCertainty(int certainty) {
+        switch (certainty) {
+            case SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED:
+                return "IMMEDIATE";
+            case SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY:
+                return "LIKELY";
+        }
+        return "UNKNOWN";
+    }
+
+    private static String getGeographicalScope(int scope) {
+        switch (scope) {
+            case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
+                return "CELL_WIDE_IMMEDIATE";
+            case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
+                return "PLMN_WIDE ";
+            case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE:
+                return "LA_WIDE";
+            case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
+                return "CELL_WIDE";
+        }
+        return "UNKNOWN";
+    }
+
+    private static String getPriority(int priority) {
+        switch (priority) {
+            case SmsCbMessage.MESSAGE_PRIORITY_NORMAL:
+                return "NORMAL";
+            case SmsCbMessage.MESSAGE_PRIORITY_INTERACTIVE:
+                return "INTERACTIVE";
+            case SmsCbMessage.MESSAGE_PRIORITY_URGENT:
+                return "URGENT";
+            case SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY:
+                return "EMERGENCY";
+        }
+        return "UNKNOWN";
+    }
+
+    @Override
+    public void shutdown() {
+
+        smsStopTrackingIncomingSmsMessage();
+        smsStopTrackingIncomingMmsMessage();
+        smsStopTrackingGsmEmergencyCBMessage();
+        smsStopTrackingCdmaEmergencyCBMessage();
+
+        synchronized (lock) {
+            if (mSentReceiversRegistered) {
+                mService.unregisterReceiver(mSmsSendListener);
+                mService.unregisterReceiver(mMmsSendListener);
+                mSentReceiversRegistered = false;
+            }
+        }
+    }
+
+    private class MmsBuilder {
+
+        public static final String MESSAGE_CLASS_PERSONAL =
+                PduHeaders.MESSAGE_CLASS_PERSONAL_STR;
+
+        public static final String MESSAGE_CLASS_ADVERTISEMENT =
+                PduHeaders.MESSAGE_CLASS_ADVERTISEMENT_STR;
+
+        public static final String MESSAGE_CLASS_INFORMATIONAL =
+                PduHeaders.MESSAGE_CLASS_INFORMATIONAL_STR;
+
+        public static final String MESSAGE_CLASS_AUTO =
+                PduHeaders.MESSAGE_CLASS_AUTO_STR;
+
+        public static final int MESSAGE_PRIORITY_LOW = PduHeaders.PRIORITY_LOW;
+        public static final int MESSAGE_PRIORITY_NORMAL = PduHeaders.PRIORITY_LOW;
+        public static final int MESSAGE_PRIORITY_HIGH = PduHeaders.PRIORITY_LOW;
+
+        private static final int DEFAULT_EXPIRY_TIME = 7 * 24 * 60 * 60;
+        private static final int DEFAULT_PRIORITY = PduHeaders.PRIORITY_NORMAL;
+
+        private SendReq mRequest;
+        private PduBody mBody;
+
+        // FIXME: Eventually this should be exposed as a parameter
+        private static final String TEMP_CONTENT_FILE_NAME = "text0.txt";
+
+        // Synchronized Multimedia Internet Language
+        // Fragment for compatibility
+        private static final String sSmilText =
+                "<smil>" +
+                        "<head>" +
+                        "<layout>" +
+                        "<root-layout/>" +
+                        "<region height=\"100%%\" id=\"Text\" left=\"0%%\"" +
+                        " top=\"0%%\" width=\"100%%\"/>" +
+                        "</layout>" +
+                        "</head>" +
+                        "<body>" +
+                        "<par dur=\"8000ms\">" +
+                        "<text src=\"%s\" region=\"Text\"/>" +
+                        "</par>" +
+                        "</body>" +
+                        "</smil>";
+
+        public MmsBuilder() {
+            mRequest = new SendReq();
+            mBody = new PduBody();
+        }
+
+        public void setFromPhoneNumber(String number) {
+            mRequest.setFrom(new EncodedStringValue(number));
+        }
+
+        public void setToPhoneNumber(String number) {
+            mRequest.setTo(new EncodedStringValue[] {
+                    new EncodedStringValue(number) });
+        }
+
+        public void setToPhoneNumbers(List<String> number) {
+            mRequest.setTo(EncodedStringValue.encodeStrings((String[]) number.toArray()));
+        }
+
+        public void setSubject(String subject) {
+            mRequest.setSubject(new EncodedStringValue(subject));
+        }
+
+        public void setDate() {
+            setDate(System.currentTimeMillis() / 1000);
+        }
+
+        public void setDate(long time) {
+            mRequest.setDate(time);
+        }
+
+        public void addMessageBody(String message) {
+            addMessageBody(message, true);
+        }
+
+        public void setMessageClass(String messageClass) {
+            mRequest.setMessageClass(messageClass.getBytes());
+        }
+
+        public void setMessagePriority(int priority) {
+            try {
+                mRequest.setPriority(priority);
+            } catch (InvalidHeaderValueException e) {
+                Log.e("Invalid Header Value "+e.toString());
+            }
+        }
+
+        public void setDeliveryReport(boolean report) {
+            try {
+                mRequest.setDeliveryReport((report) ? PduHeaders.VALUE_YES : PduHeaders.VALUE_NO);
+            } catch (InvalidHeaderValueException e) {
+                Log.e("Invalid Header Value "+e.toString());
+            }
+        }
+
+        public void setReadReport(boolean report) {
+            try {
+                mRequest.setReadReport((report) ? PduHeaders.VALUE_YES : PduHeaders.VALUE_NO);
+            } catch (InvalidHeaderValueException e) {
+                Log.e("Invalid Header Value "+e.toString());
+            }
+        }
+
+        public void setExpirySeconds(int seconds) {
+            mRequest.setExpiry(seconds);
+        }
+
+        public byte[] build() {
+            mRequest.setBody(mBody);
+
+            int msgSize = 0;
+            for (int i = 0; i < mBody.getPartsNum(); i++) {
+                msgSize += mBody.getPart(i).getDataLength();
+            }
+            mRequest.setMessageSize(msgSize);
+
+            return new PduComposer(mContext, mRequest).make();
+        }
+
+        public void addMessageBody(String message, boolean addSmilFragment) {
+            final PduPart part = new PduPart();
+            part.setCharset(CharacterSets.UTF_8);
+            part.setContentType(ContentType.TEXT_PLAIN.getBytes());
+            part.setContentLocation("text0".getBytes());
+            int index = TEMP_CONTENT_FILE_NAME.lastIndexOf(".");
+            String contentId = (index == -1) ? TEMP_CONTENT_FILE_NAME
+                    : TEMP_CONTENT_FILE_NAME.substring(0, index);
+            part.setContentId(contentId.getBytes());
+            part.setContentId("txt".getBytes());
+            part.setData(message.getBytes());
+            mBody.addPart(part);
+            if (addSmilFragment) {
+                addSmilTextFragment(TEMP_CONTENT_FILE_NAME);
+            }
+        }
+
+        private void addSmilTextFragment(String contentFilename) {
+
+            final String smil = String.format(sSmilText, contentFilename);
+            final PduPart smilPart = new PduPart();
+            smilPart.setContentId("smil".getBytes());
+            smilPart.setContentLocation("smil.xml".getBytes());
+            smilPart.setContentType(ContentType.APP_SMIL.getBytes());
+            smilPart.setData(smil.getBytes());
+            mBody.addPart(0, smilPart);
+        }
+    }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/SubscriptionManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/telephony/SubscriptionManagerFacade.java
new file mode 100644
index 0000000..0031bc1
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/SubscriptionManagerFacade.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+import android.app.Service;
+import android.content.Context;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionInfo;
+
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.util.List;
+
+/**
+ * Exposes SubscriptionManager functionality.
+ */
+public class SubscriptionManagerFacade extends RpcReceiver {
+
+    private final Service mService;
+    private final Context mContext;
+    private final SubscriptionManager mSubscriptionManager;
+
+    public SubscriptionManagerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mContext = mService.getBaseContext();
+        mSubscriptionManager = SubscriptionManager.from(mContext);
+    }
+
+    @Rpc(description = "Return the default subscription ID")
+    public Integer subscriptionGetDefaultSubId() {
+        return SubscriptionManager.getDefaultSubscriptionId();
+    }
+
+    @Rpc(description = "Return the default data subscription ID")
+    public Integer subscriptionGetDefaultDataSubId() {
+        return SubscriptionManager.getDefaultDataSubscriptionId();
+    }
+
+    @Rpc(description = "Set the default data subscription ID")
+    public void subscriptionSetDefaultDataSubId(
+            @RpcParameter(name = "subId")
+            Integer subId) {
+        mSubscriptionManager.setDefaultDataSubId(subId);
+    }
+
+    @Rpc(description = "Return the default voice subscription ID")
+    public Integer subscriptionGetDefaultVoiceSubId() {
+        return SubscriptionManager.getDefaultVoiceSubscriptionId();
+    }
+
+    @Rpc(description = "Set the default voice subscription ID")
+    public void subscriptionSetDefaultVoiceSubId(
+            @RpcParameter(name = "subId")
+            Integer subId) {
+        mSubscriptionManager.setDefaultVoiceSubId(subId);
+    }
+
+    @Rpc(description = "Return the default sms subscription ID")
+    public Integer subscriptionGetDefaultSmsSubId() {
+        return SubscriptionManager.getDefaultSmsSubscriptionId();
+    }
+
+    @Rpc(description = "Set the default sms subscription ID")
+    public void subscriptionSetDefaultSmsSubId(
+            @RpcParameter(name = "subId")
+            Integer subId) {
+        mSubscriptionManager.setDefaultSmsSubId(subId);
+    }
+
+    @Rpc(description = "Return a List of all Subscription Info Records")
+    public List<SubscriptionInfo> subscriptionGetAllSubInfoList() {
+        return mSubscriptionManager.getAllSubscriptionInfoList();
+    }
+
+    @Rpc(description = "Return a List of all Active Subscription Info Records")
+    public List<SubscriptionInfo> subscriptionGetActiveSubInfoList() {
+        return mSubscriptionManager.getActiveSubscriptionInfoList();
+    }
+
+    @Rpc(description = "Return the Subscription Info for a Particular Subscription ID")
+    public SubscriptionInfo subscriptionGetSubInfoForSubscriber(
+            @RpcParameter(name = "subId")
+            Integer subId) {
+        return mSubscriptionManager.getActiveSubscriptionInfo(subId);
+    }
+
+    @Rpc(description = "Set Data Roaming Enabled or Disabled for a particular Subscription ID")
+    public Integer subscriptionSetDataRoaming(Integer roaming, Integer subId) {
+        if (roaming != SubscriptionManager.DATA_ROAMING_DISABLE) {
+            return mSubscriptionManager.setDataRoaming(
+                    SubscriptionManager.DATA_ROAMING_ENABLE, subId);
+        } else {
+            return mSubscriptionManager.setDataRoaming(
+                    SubscriptionManager.DATA_ROAMING_DISABLE, subId);
+        }
+    }
+
+    @Override
+    public void shutdown() {
+
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelecomCallFacade.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelecomCallFacade.java
new file mode 100644
index 0000000..070e649
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelecomCallFacade.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import android.app.Service;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.VideoProfile;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * Exposes TelecomManager functionality.
+ */
+public class TelecomCallFacade extends RpcReceiver {
+
+    private final Service mService;
+
+    private List<PhoneAccountHandle> mEnabledAccountHandles = null;
+
+    public TelecomCallFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+
+        InCallServiceImpl.setEventFacade(
+                manager.getReceiver(EventFacade.class));
+    }
+
+    @Override
+    public void shutdown() {
+        InCallServiceImpl.setEventFacade(null);
+    }
+
+    /**
+     * Returns an identifier of the call. When a phone number is available, the number will be
+     * returned. Otherwise, the standard object toString result of the Call object. e.g. A
+     * conference call does not have a single number associated with it, thus the toString Id will
+     * be returned.
+     *
+     * @param call
+     * @return String
+     */
+
+    @Rpc(description = "Disconnect call by callId.")
+    public void telecomCallDisconnect(
+                        @RpcParameter(name = "callId")
+            String callId) {
+        InCallServiceImpl.callDisconnect(callId);
+    }
+
+    @Rpc(description = "Hold call by callId")
+    public void telecomCallHold(
+                        @RpcParameter(name = "callId")
+            String callId) {
+        InCallServiceImpl.holdCall(callId);
+    }
+
+    @Rpc(description = "Merge call to conference by callId")
+    public void telecomCallMergeToConf(
+                        @RpcParameter(name = "callId")
+            String callId) {
+        InCallServiceImpl.mergeCallsInConference(callId);
+    }
+
+    @Rpc(description = "Split call from conference by callId.")
+    public void telecomCallSplitFromConf(
+                        @RpcParameter(name = "callId")
+            String callId) {
+        InCallServiceImpl.splitCallFromConf(callId);
+    }
+
+    @Rpc(description = "Unhold call by callId")
+    public void telecomCallUnhold(
+                        @RpcParameter(name = "callId")
+            String callId) {
+        InCallServiceImpl.unholdCall(callId);
+    }
+
+    @Rpc(description = "Joins two calls into a conference call. "
+            + "Calls are identified by their "
+            + "IDs listed by telecomPhoneGetCallIds")
+    public void telecomCallJoinCallsInConf(
+                        @RpcParameter(name = "callIdOne")
+            String callIdOne,
+                        @RpcParameter(name = "callIdTwo")
+            String callIdTwo) {
+        InCallServiceImpl.joinCallsInConf(callIdOne, callIdTwo);
+    }
+
+    @Rpc(description = "Obtains the current call audio state of the phone.")
+    public CallAudioState telecomCallGetAudioState() {
+        return InCallServiceImpl.serviceGetCallAudioState();
+    }
+
+    @Rpc(description = "Lists the IDs (phone numbers or hex hashes) "
+            + "of the current calls.")
+    public Set<String> telecomCallGetCallIds() {
+        return InCallServiceImpl.getCallIdList();
+    }
+    @Rpc(description = "Get callId's children")
+    public List<String> telecomCallGetCallChildren(
+                        @RpcParameter(name = "callId") String callId) {
+        return InCallServiceImpl.getCallChildren(callId);
+    }
+    @Rpc(description = "Get callId's parent")
+    public String telecomCallGetCallParent(
+                        @RpcParameter(name = "callId") String callId) {
+        return InCallServiceImpl.getCallParent(callId);
+    }
+    @Rpc(description = "Swaps the calls within this conference")
+    public void telecomCallSwapCallsInConference(
+                        @RpcParameter(name = "callId") String callId) {
+        InCallServiceImpl.swapCallsInConference(callId);
+    }
+    @Rpc(description = "Play a dual-tone multi-frequency signaling (DTMF) tone")
+    public void telecomCallPlayDtmfTone(
+                        @RpcParameter(name = "callId") String callId,
+                        @RpcParameter(name = "digit") String digitString) {
+        for(int i = 0; i < digitString.length(); i++) {
+            char c = digitString.charAt(i);
+            InCallServiceImpl.callPlayDtmfTone(callId, c);
+        }
+    }
+    @Rpc(description = "Stop any dual-tone multi-frequency signaling (DTMF) tone")
+    public void telecomCallStopDtmfTone(
+                        @RpcParameter(name = "callId") String callId) {
+        InCallServiceImpl.callStopDtmfTone(callId);
+    }
+    @Rpc(description = "Obtains a list of text message, user to reject call.")
+    public List<String> telecomCallGetCannedTextResponses(
+                        @RpcParameter(name = "callId") String callId) {
+        return InCallServiceImpl.callGetCannedTextResponses(callId);
+    }
+    @Rpc(description = "Reset the Call List.")
+    public void telecomCallClearCallList() {
+        InCallServiceImpl.clearCallList();
+    }
+
+    @Rpc(description = "Get the state of a call according to call id.")
+    public String telecomCallGetCallState(
+                        @RpcParameter(name = "callId")
+            String callId) {
+
+        return InCallServiceImpl.callGetState(callId);
+    }
+
+    @Rpc(description = "Sets the audio route (SPEAKER, BLUETOOTH, etc...).")
+    public void telecomCallSetAudioRoute(
+                        @RpcParameter(name = "route")
+            String route) {
+
+        InCallServiceImpl.serviceSetAudioRoute(route);
+    }
+
+    @Rpc(description = "Turns the proximity sensor off. "
+            + "If screenOnImmediately is true, "
+            + "the screen will be turned on immediately")
+    public void telecomCallOverrideProximitySensor(
+                        @RpcParameter(name = "screenOn")
+            Boolean screenOn) {
+        InCallServiceImpl.overrideProximitySensor(screenOn);
+    }
+
+    @Rpc(description = "Answer a call of a specified id, with video state")
+    public void telecomCallAnswer(
+                        @RpcParameter(name = "call")
+            String callId,
+                        @RpcParameter(name = "videoState")
+            String videoState) {
+        InCallServiceImpl.callAnswer(callId, videoState);
+    }
+
+    @Rpc(description = "Answer a call of a specified id, with video state")
+    public void telecomCallReject(
+                        @RpcParameter(name = "call")
+            String callId,
+                        @RpcParameter(name = "message")
+            String message) {
+        InCallServiceImpl.callReject(callId, message);
+    }
+
+    @Rpc(description = "Start Listening for a VideoCall Event")
+    public void telecomCallStartListeningForEvent(
+                        @RpcParameter(name = "call")
+            String callId,
+                        @RpcParameter(name = "event")
+            String event) {
+        InCallServiceImpl.callStartListeningForEvent(callId, event);
+    }
+
+    @Rpc(description = "Stop Listening for a Call Event")
+    public void telecomCallStopListeningForEvent(
+                        @RpcParameter(name = "call")
+            String callId,
+                        @RpcParameter(name = "event")
+            String event) {
+        InCallServiceImpl.callStopListeningForEvent(callId, event);
+    }
+
+    @Rpc(description = "Get the detailed information about a call")
+    public Call.Details telecomCallGetDetails(
+                        @RpcParameter(name = "callId")
+            String callId) {
+        return InCallServiceImpl.callGetDetails(callId);
+    }
+
+    @Rpc(description = "Return the capabilities for a call")
+    public  List<String> telecomCallGetCapabilities(
+                        @RpcParameter(name = "callId")
+            String callId) {
+        return InCallServiceImpl.callGetCallCapabilities(callId);
+    }
+
+    @Rpc(description = "Return the properties for a call")
+    public  List<String> telecomCallGetProperties(
+                        @RpcParameter(name = "callId")
+            String callId) {
+        return InCallServiceImpl.callGetCallProperties(callId);
+    }
+
+    @Rpc(description = "Start Listening for a VideoCall Event")
+    public void telecomCallVideoStartListeningForEvent(
+                        @RpcParameter(name = "call")
+            String callId,
+                        @RpcParameter(name = "event")
+            String event) {
+        InCallServiceImpl.videoCallStartListeningForEvent(callId, event);
+    }
+
+    @Rpc(description = "Stop Listening for a VideoCall Event")
+    public void telecomCallVideoStopListeningForEvent(
+                        @RpcParameter(name = "call")
+            String callId,
+                        @RpcParameter(name = "event")
+            String event) {
+        InCallServiceImpl.videoCallStopListeningForEvent(callId, event);
+    }
+
+    @Rpc(description = "Get the Video Call State")
+    public String telecomCallVideoGetState(
+                        @RpcParameter(name = "call")
+            String callId) {
+        return InCallServiceImpl.videoCallGetState(callId);
+    }
+
+    @Rpc(description = "Send a request to modify the video call session parameters")
+    public void telecomCallVideoSendSessionModifyRequest(
+                        @RpcParameter(name = "call")
+            String callId,
+                        @RpcParameter(name = "videoState")
+            String videoState,
+                        @RpcParameter(name = "videoQuality")
+            String videoQuality) {
+        InCallServiceImpl.videoCallSendSessionModifyRequest(callId, videoState, videoQuality);
+    }
+
+    @Rpc(description = "Send a response to a modify the video call session request")
+    public void telecomCallVideoSendSessionModifyResponse(
+                        @RpcParameter(name = "call")
+            String callId,
+                        @RpcParameter(name = "videoState")
+            String videoState,
+                        @RpcParameter(name = "videoQuality")
+            String videoQuality) {
+        InCallServiceImpl.videoCallSendSessionModifyResponse(callId, videoState, videoQuality);
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelecomManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelecomManagerFacade.java
new file mode 100644
index 0000000..caa60ab
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelecomManagerFacade.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Field;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.database.Cursor;
+import android.telecom.AudioState;
+import android.telecom.Call;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.AndroidFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * Exposes TelecomManager functionality.
+ */
+public class TelecomManagerFacade extends RpcReceiver {
+
+    private final Service mService;
+    private final AndroidFacade mAndroidFacade;
+
+    private final TelecomManager mTelecomManager;
+    private final TelephonyManager mTelephonyManager;
+
+    private List<PhoneAccountHandle> mEnabledAccountHandles = null;
+
+    public TelecomManagerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mTelecomManager = new TelecomManager(mService);
+        mTelephonyManager = new TelephonyManager(mService);
+        mAndroidFacade = manager.getReceiver(AndroidFacade.class);
+    }
+
+    @Override
+    public void shutdown() {
+    }
+
+    @Rpc(description = "If there's a ringing call, accept on behalf of the user.")
+    public void telecomAcceptRingingCall(
+            @RpcOptional
+            String videoState) {
+
+        if (videoState == null) {
+            mTelecomManager.acceptRingingCall();
+        }
+        else {
+            int state = InCallServiceImpl.getVideoCallState(videoState);
+
+            if (state == InCallServiceImpl.STATE_INVALID) {
+                Log.e("telecomAcceptRingingCall: video state is invalid!");
+                return;
+            }
+
+            mTelecomManager.acceptRingingCall(state);
+        }
+    }
+
+    @Rpc(description = "Removes the missed-call notification if one is present.")
+    public void telecomCancelMissedCallsNotification() {
+        mTelecomManager.cancelMissedCallsNotification();
+    }
+
+    @Rpc(description = "Remove all Accounts that belong to the calling package from the system.")
+    public void telecomClearAccounts() {
+        mTelecomManager.clearAccounts();
+    }
+
+    @Rpc(description = "End an ongoing call.")
+    public Boolean telecomEndCall() {
+        return mTelecomManager.endCall();
+    }
+
+    @Rpc(description = "Get a list of all PhoneAccounts.")
+    public List<PhoneAccount> telecomGetAllPhoneAccounts() {
+        return mTelecomManager.getAllPhoneAccounts();
+    }
+
+    @Rpc(description = "Get the current call state.")
+    public String telecomGetCallState() {
+        int state = mTelecomManager.getCallState();
+        return TelephonyUtils.getTelephonyCallStateString(state);
+    }
+
+    @Rpc(description = "Get the current tty mode.")
+    public String telecomGetCurrentTtyMode() {
+        int mode = mTelecomManager.getCurrentTtyMode();
+        return TelephonyUtils.getTtyModeString(mode);
+    }
+
+    @Rpc(description = "Bring incallUI to foreground.")
+    public void telecomShowInCallScreen(
+            @RpcParameter(name = "showDialpad")
+            @RpcOptional
+            @RpcDefault("false")
+            Boolean showDialpad) {
+        mTelecomManager.showInCallScreen(showDialpad);
+    }
+
+    @Rpc(description = "Get the list of PhoneAccountHandles with calling capability.")
+    public List<PhoneAccountHandle> telecomGetEnabledPhoneAccounts() {
+        mEnabledAccountHandles = mTelecomManager.getCallCapablePhoneAccounts();
+        return mEnabledAccountHandles;
+    }
+
+    @Rpc(description = "Set the user-chosen default PhoneAccount for making outgoing phone calls.")
+    public void telecomSetUserSelectedOutgoingPhoneAccount(
+                        @RpcParameter(name = "phoneAccountHandleId")
+            String phoneAccountHandleId) throws Exception {
+
+        List<PhoneAccountHandle> accountHandles = mTelecomManager
+                .getAllPhoneAccountHandles();
+        for (PhoneAccountHandle handle : accountHandles) {
+            if (handle.getId().equals(phoneAccountHandleId)) {
+                mTelecomManager.setUserSelectedOutgoingPhoneAccount(handle);
+                Log.d(String.format("Set default Outgoing Phone Account(%s)",
+                        phoneAccountHandleId));
+                return;
+            }
+        }
+        Log.d(String.format(
+                "Failed to find a matching phoneAccountHandleId(%s).",
+                phoneAccountHandleId));
+        throw new Exception(String.format(
+                "Failed to find a matching phoneAccountHandleId(%s).",
+                phoneAccountHandleId));
+    }
+
+    @Rpc(description = "Get the user-chosen default PhoneAccount for making outgoing phone calls.")
+    public PhoneAccountHandle telecomGetUserSelectedOutgoingPhoneAccount() {
+        return mTelecomManager.getUserSelectedOutgoingPhoneAccount();
+    }
+
+    @Rpc(description = "Set the PhoneAccount corresponding to user selected subscription id " +
+                       " for making outgoing phone calls.")
+    public void telecomSetUserSelectedOutgoingPhoneAccountBySubId(
+                        @RpcParameter(name = "subId")
+                        Integer subId) throws Exception {
+          Iterator<PhoneAccountHandle> phoneAccounts =
+               mTelecomManager.getCallCapablePhoneAccounts().listIterator();
+
+          while (phoneAccounts.hasNext()) {
+              PhoneAccountHandle phoneAccountHandle = phoneAccounts.next();
+              PhoneAccount phoneAccount =
+                       mTelecomManager.getPhoneAccount(phoneAccountHandle);
+              if (subId == mTelephonyManager.getSubIdForPhoneAccount(phoneAccount)) {
+                  mTelecomManager.setUserSelectedOutgoingPhoneAccount(phoneAccountHandle);
+                  Log.d(String.format(
+                      "Set default Outgoing Phone Account for subscription(%s)", subId));
+                  return;
+              }
+          }
+          Log.d(String.format(
+                  "Failed to find a matching Phone Account for subscription (%s).",
+                  subId));
+          throw new Exception(String.format(
+                  "Failed to find a matching Phone Account for subscription (%s).",
+                   subId));
+    }
+
+    @Rpc(description = "Returns whether there is an ongoing phone call.")
+    public Boolean telecomIsInCall() {
+        return mTelecomManager.isInCall();
+    }
+
+    @Rpc(description = "Returns whether there is a ringing incoming call.")
+    public Boolean telecomIsRinging() {
+        return mTelecomManager.isRinging();
+    }
+
+    @Rpc(description = "Silences the rigner if there's a ringing call.")
+    public void telecomSilenceRinger() {
+        mTelecomManager.silenceRinger();
+    }
+
+    @Rpc(description = "Swap two calls")
+    public void telecomSwapCalls() {
+        // TODO: b/26273475 Add logic to swap the foreground and back ground calls
+    }
+
+    @Rpc(description = "Toggles call waiting feature on or off for default voice subscription id.")
+    public void toggleCallWaiting(
+            @RpcParameter(name = "enabled")
+            @RpcOptional
+            Boolean enabled) {
+        toggleCallWaitingForSubscription(
+                SubscriptionManager.getDefaultVoiceSubscriptionId(), enabled);
+    }
+
+    @Rpc(description = "Toggles call waiting feature on or off for specified subscription id.")
+    public void toggleCallWaitingForSubscription(
+            @RpcParameter(name = "subId")
+            @RpcOptional
+            Integer subId,
+            @RpcParameter(name = "enabled")
+            @RpcOptional
+            Boolean enabled) {
+        // TODO: b/26273478 Enable or Disable the call waiting feature
+    }
+
+    @Rpc(description = "Sends an MMI string to Telecom for processing")
+    public void telecomHandleMmi(
+                        @RpcParameter(name = "dialString")
+            String dialString) {
+        mTelecomManager.handleMmi(dialString);
+    }
+
+    // TODO: b/20917712 add support to pass arbitrary "Extras" object
+    // for videoCall parameter
+    @Deprecated
+    @Rpc(description = "Calls a phone by resolving a generic URI.")
+    public void telecomCall(
+                        @RpcParameter(name = "uriString")
+            final String uriString,
+            @RpcParameter(name = "videoCall")
+            @RpcOptional
+            @RpcDefault("false")
+            Boolean videoCall) throws Exception {
+
+        Log.w("Function telecomCall is deprecated; please use a URI-specific call");
+
+        Uri uri = Uri.parse(uriString);
+        if (uri.getScheme().equals("content")) {
+            telecomCallContentUri(uriString, videoCall);
+        }
+        else {
+            telecomCallNumber(uriString, videoCall);
+        }
+    }
+
+    // TODO: b/20917712 add support to pass arbitrary "Extras" object
+    // for videoCall parameter
+    @Rpc(description = "Calls a phone by resolving a Content-type URI.")
+    public void telecomCallContentUri(
+                        @RpcParameter(name = "uriString")
+            final String uriString,
+            @RpcParameter(name = "videoCall")
+            @RpcOptional
+            @RpcDefault("false")
+            Boolean videoCall)
+            throws Exception {
+        Uri uri = Uri.parse(uriString);
+        if (!uri.getScheme().equals("content")) {
+            Log.e("Invalid URI!!");
+            return;
+        }
+
+        String phoneNumberColumn = ContactsContract.PhoneLookup.NUMBER;
+        String selectWhere = null;
+        if ((FacadeManager.class.cast(mManager)).getSdkLevel() >= 5) {
+            Class<?> contactsContract_Data_class =
+                    Class.forName("android.provider.ContactsContract$Data");
+            Field RAW_CONTACT_ID_field =
+                    contactsContract_Data_class.getField("RAW_CONTACT_ID");
+            selectWhere = RAW_CONTACT_ID_field.get(null).toString() + "="
+                    + uri.getLastPathSegment();
+            Field CONTENT_URI_field =
+                    contactsContract_Data_class.getField("CONTENT_URI");
+            uri = Uri.parse(CONTENT_URI_field.get(null).toString());
+            Class<?> ContactsContract_CommonDataKinds_Phone_class =
+                    Class.forName("android.provider.ContactsContract$CommonDataKinds$Phone");
+            Field NUMBER_field =
+                    ContactsContract_CommonDataKinds_Phone_class.getField("NUMBER");
+            phoneNumberColumn = NUMBER_field.get(null).toString();
+        }
+        ContentResolver resolver = mService.getContentResolver();
+        Cursor c = resolver.query(uri, new String[] {
+                phoneNumberColumn
+        },
+                selectWhere, null, null);
+        String number = "";
+        if (c.moveToFirst()) {
+            number = c.getString(c.getColumnIndexOrThrow(phoneNumberColumn));
+        }
+        c.close();
+        telecomCallNumber(number, videoCall);
+    }
+
+    // TODO: b/20917712 add support to pass arbitrary "Extras" object
+    // for videoCall parameter
+    @Rpc(description = "Calls a phone number.")
+    public void telecomCallNumber(
+                        @RpcParameter(name = "number")
+            final String number,
+            @RpcParameter(name = "videoCall")
+            @RpcOptional
+            @RpcDefault("false")
+            Boolean videoCall)
+            throws Exception {
+        telecomCallTelUri("tel:" + URLEncoder.encode(number, "ASCII"), videoCall);
+    }
+
+    // TODO: b/20917712 add support to pass arbitrary "Extras" object
+    // for videoCall parameter
+    @Rpc(description = "Calls a phone by Tel-URI.")
+    public void telecomCallTelUri(
+            @RpcParameter(name = "uriString")
+    final String uriString,
+            @RpcParameter(name = "videoCall")
+            @RpcOptional
+            @RpcDefault("false")
+            Boolean videoCall) throws Exception {
+        if (!uriString.startsWith("tel:")) {
+            Log.w("Invalid tel URI" + uriString);
+            return;
+        }
+
+        Intent intent = new Intent(Intent.ACTION_CALL);
+        intent.setDataAndType(Uri.parse(uriString).normalizeScheme(), null);
+
+        if (videoCall) {
+            Log.d("Placing a bi-directional video call");
+            intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                    VideoProfile.STATE_BIDIRECTIONAL);
+        }
+
+        mAndroidFacade.startActivityIntent(intent, false);
+    }
+
+    @Rpc(description = "Calls an Emergency number.")
+    public void telecomCallEmergencyNumber(
+                        @RpcParameter(name = "number")
+            final String number)
+            throws Exception {
+        String uriString = "tel:" + URLEncoder.encode(number, "ASCII");
+        mAndroidFacade.startActivity(Intent.ACTION_CALL_PRIVILEGED, uriString,
+                null, null, null, null, null);
+    }
+
+    @Rpc(description = "Dials a contact/phone number by URI.")
+    public void telecomDial(
+            @RpcParameter(name = "uri")
+    final String uri)
+            throws Exception {
+        mAndroidFacade.startActivity(Intent.ACTION_DIAL, uri, null, null, null,
+                null, null);
+    }
+
+    @Rpc(description = "Dials a phone number.")
+    public void telecomDialNumber(@RpcParameter(name = "phone number")
+    final String number)
+            throws Exception, UnsupportedEncodingException {
+        telecomDial("tel:" + URLEncoder.encode(number, "ASCII"));
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java
new file mode 100644
index 0000000..4c1b466
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+public class TelephonyConstants {
+    /**
+     * Constant for WiFi Calling WFC mode
+     * **/
+    public static final String WFC_MODE_WIFI_ONLY = "WIFI_ONLY";
+    public static final String WFC_MODE_CELLULAR_PREFERRED = "CELLULAR_PREFERRED";
+    public static final String WFC_MODE_WIFI_PREFERRED = "WIFI_PREFERRED";
+    public static final String WFC_MODE_DISABLED = "DISABLED";
+    public static final String WFC_MODE_UNKNOWN = "UNKNOWN";
+
+    /**
+     * Constant for Video Telephony VT state
+     * **/
+    public static final String VT_STATE_AUDIO_ONLY = "AUDIO_ONLY";
+    public static final String VT_STATE_TX_ENABLED = "TX_ENABLED";
+    public static final String VT_STATE_RX_ENABLED = "RX_ENABLED";
+    public static final String VT_STATE_BIDIRECTIONAL = "BIDIRECTIONAL";
+    public static final String VT_STATE_TX_PAUSED = "TX_PAUSED";
+    public static final String VT_STATE_RX_PAUSED = "RX_PAUSED";
+    public static final String VT_STATE_BIDIRECTIONAL_PAUSED = "BIDIRECTIONAL_PAUSED";
+    public static final String VT_STATE_STATE_INVALID = "INVALID";
+
+    /**
+     * Constant for Video Telephony Video quality
+     * **/
+    public static final String VT_VIDEO_QUALITY_DEFAULT = "DEFAULT";
+    public static final String VT_VIDEO_QUALITY_UNKNOWN = "UNKNOWN";
+    public static final String VT_VIDEO_QUALITY_HIGH = "HIGH";
+    public static final String VT_VIDEO_QUALITY_MEDIUM = "MEDIUM";
+    public static final String VT_VIDEO_QUALITY_LOW = "LOW";
+    public static final String VT_VIDEO_QUALITY_INVALID = "INVALID";
+
+    /**
+     * Constant for Call State (for call object)
+     * **/
+    public static final String CALL_STATE_ACTIVE = "ACTIVE";
+    public static final String CALL_STATE_NEW = "NEW";
+    public static final String CALL_STATE_DIALING = "DIALING";
+    public static final String CALL_STATE_RINGING = "RINGING";
+    public static final String CALL_STATE_HOLDING = "HOLDING";
+    public static final String CALL_STATE_DISCONNECTED = "DISCONNECTED";
+    public static final String CALL_STATE_PRE_DIAL_WAIT = "PRE_DIAL_WAIT";
+    public static final String CALL_STATE_CONNECTING = "CONNECTING";
+    public static final String CALL_STATE_DISCONNECTING = "DISCONNECTING";
+    public static final String CALL_STATE_UNKNOWN = "UNKNOWN";
+    public static final String CALL_STATE_INVALID = "INVALID";
+
+    /**
+     * Constant for PRECISE Call State (for call object)
+     * **/
+    public static final String PRECISE_CALL_STATE_ACTIVE = "ACTIVE";
+    public static final String PRECISE_CALL_STATE_ALERTING = "ALERTING";
+    public static final String PRECISE_CALL_STATE_DIALING = "DIALING";
+    public static final String PRECISE_CALL_STATE_INCOMING = "INCOMING";
+    public static final String PRECISE_CALL_STATE_HOLDING = "HOLDING";
+    public static final String PRECISE_CALL_STATE_DISCONNECTED = "DISCONNECTED";
+    public static final String PRECISE_CALL_STATE_WAITING = "WAITING";
+    public static final String PRECISE_CALL_STATE_DISCONNECTING = "DISCONNECTING";
+    public static final String PRECISE_CALL_STATE_IDLE = "IDLE";
+    public static final String PRECISE_CALL_STATE_UNKNOWN = "UNKNOWN";
+    public static final String PRECISE_CALL_STATE_INVALID = "INVALID";
+
+    /**
+     * Constant for DC POWER STATE
+     * **/
+    public static final String DC_POWER_STATE_LOW = "LOW";
+    public static final String DC_POWER_STATE_HIGH = "HIGH";
+    public static final String DC_POWER_STATE_MEDIUM = "MEDIUM";
+    public static final String DC_POWER_STATE_UNKNOWN = "UNKNOWN";
+
+    /**
+     * Constant for Audio Route
+     * **/
+    public static final String AUDIO_ROUTE_EARPIECE = "EARPIECE";
+    public static final String AUDIO_ROUTE_BLUETOOTH = "BLUETOOTH";
+    public static final String AUDIO_ROUTE_SPEAKER = "SPEAKER";
+    public static final String AUDIO_ROUTE_WIRED_HEADSET = "WIRED_HEADSET";
+    public static final String AUDIO_ROUTE_WIRED_OR_EARPIECE = "WIRED_OR_EARPIECE";
+
+    /**
+     * Constant for Call Capability
+     * **/
+    public static final String CALL_CAPABILITY_HOLD = "HOLD";
+    public static final String CALL_CAPABILITY_SUPPORT_HOLD = "SUPPORT_HOLD";
+    public static final String CALL_CAPABILITY_MERGE_CONFERENCE = "MERGE_CONFERENCE";
+    public static final String CALL_CAPABILITY_SWAP_CONFERENCE = "SWAP_CONFERENCE";
+    public static final String CALL_CAPABILITY_UNUSED_1 = "UNUSED_1";
+    public static final String CALL_CAPABILITY_RESPOND_VIA_TEXT = "RESPOND_VIA_TEXT";
+    public static final String CALL_CAPABILITY_MUTE = "MUTE";
+    public static final String CALL_CAPABILITY_MANAGE_CONFERENCE = "MANAGE_CONFERENCE";
+    public static final String CALL_CAPABILITY_SUPPORTS_VT_LOCAL_RX = "SUPPORTS_VT_LOCAL_RX";
+    public static final String CALL_CAPABILITY_SUPPORTS_VT_LOCAL_TX = "SUPPORTS_VT_LOCAL_TX";
+    public static final String CALL_CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL = "SUPPORTS_VT_LOCAL_BIDIRECTIONAL";
+    public static final String CALL_CAPABILITY_SUPPORTS_VT_REMOTE_RX = "SUPPORTS_VT_REMOTE_RX";
+    public static final String CALL_CAPABILITY_SUPPORTS_VT_REMOTE_TX = "SUPPORTS_VT_REMOTE_TX";
+    public static final String CALL_CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL = "SUPPORTS_VT_REMOTE_BIDIRECTIONAL";
+    public static final String CALL_CAPABILITY_SEPARATE_FROM_CONFERENCE = "SEPARATE_FROM_CONFERENCE";
+    public static final String CALL_CAPABILITY_DISCONNECT_FROM_CONFERENCE = "DISCONNECT_FROM_CONFERENCE";
+    public static final String CALL_CAPABILITY_SPEED_UP_MT_AUDIO = "SPEED_UP_MT_AUDIO";
+    public static final String CALL_CAPABILITY_CAN_UPGRADE_TO_VIDEO = "CAN_UPGRADE_TO_VIDEO";
+    public static final String CALL_CAPABILITY_CAN_PAUSE_VIDEO = "CAN_PAUSE_VIDEO";
+    public static final String CALL_CAPABILITY_UNKOWN = "UNKOWN";
+
+    /**
+     * Constant for Call Property
+     * **/
+    public static final String CALL_PROPERTY_HIGH_DEF_AUDIO = "HIGH_DEF_AUDIO";
+    public static final String CALL_PROPERTY_CONFERENCE = "CONFERENCE";
+    public static final String CALL_PROPERTY_GENERIC_CONFERENCE = "GENERIC_CONFERENCE";
+    public static final String CALL_PROPERTY_WIFI = "WIFI";
+    public static final String CALL_PROPERTY_EMERGENCY_CALLBACK_MODE = "EMERGENCY_CALLBACK_MODE";
+    public static final String CALL_PROPERTY_UNKNOWN = "UNKNOWN";
+
+    /**
+     * Constant for Call Presentation
+     * **/
+    public static final String CALL_PRESENTATION_ALLOWED = "ALLOWED";
+    public static final String CALL_PRESENTATION_RESTRICTED = "RESTRICTED";
+    public static final String CALL_PRESENTATION_PAYPHONE = "PAYPHONE";
+    public static final String CALL_PRESENTATION_UNKNOWN = "UNKNOWN";
+
+    /**
+     * Constant for Network RAT
+     * **/
+    public static final String RAT_IWLAN = "IWLAN";
+    public static final String RAT_LTE = "LTE";
+    public static final String RAT_4G = "4G";
+    public static final String RAT_3G = "3G";
+    public static final String RAT_2G = "2G";
+    public static final String RAT_WCDMA = "WCDMA";
+    public static final String RAT_UMTS = "UMTS";
+    public static final String RAT_1XRTT = "1XRTT";
+    public static final String RAT_EDGE = "EDGE";
+    public static final String RAT_GPRS = "GPRS";
+    public static final String RAT_HSDPA = "HSDPA";
+    public static final String RAT_HSUPA = "HSUPA";
+    public static final String RAT_CDMA = "CDMA";
+    public static final String RAT_EVDO = "EVDO";
+    public static final String RAT_EVDO_0 = "EVDO_0";
+    public static final String RAT_EVDO_A = "EVDO_A";
+    public static final String RAT_EVDO_B = "EVDO_B";
+    public static final String RAT_IDEN = "IDEN";
+    public static final String RAT_EHRPD = "EHRPD";
+    public static final String RAT_HSPA = "HSPA";
+    public static final String RAT_HSPAP = "HSPAP";
+    public static final String RAT_GSM = "GSM";
+    public static final String RAT_TD_SCDMA = "TD_SCDMA";
+    public static final String RAT_GLOBAL = "GLOBAL";
+    public static final String RAT_UNKNOWN = "UNKNOWN";
+
+    /**
+     * Constant for Phone Type
+     * **/
+    public static final String PHONE_TYPE_GSM = "GSM";
+    public static final String PHONE_TYPE_NONE = "NONE";
+    public static final String PHONE_TYPE_CDMA = "CDMA";
+    public static final String PHONE_TYPE_SIP = "SIP";
+
+    /**
+     * Constant for SIM State
+     * **/
+    public static final String SIM_STATE_READY = "READY";
+    public static final String SIM_STATE_UNKNOWN = "UNKNOWN";
+    public static final String SIM_STATE_ABSENT = "ABSENT";
+    public static final String SIM_STATE_PUK_REQUIRED = "PUK_REQUIRED";
+    public static final String SIM_STATE_PIN_REQUIRED = "PIN_REQUIRED";
+    public static final String SIM_STATE_NETWORK_LOCKED = "NETWORK_LOCKED";
+    public static final String SIM_STATE_NOT_READY = "NOT_READY";
+    public static final String SIM_STATE_PERM_DISABLED = "PERM_DISABLED";
+    public static final String SIM_STATE_CARD_IO_ERROR = "CARD_IO_ERROR";
+
+    /**
+     * Constant for Data Connection State
+     * **/
+    public static final String DATA_STATE_CONNECTED = "CONNECTED";
+    public static final String DATA_STATE_DISCONNECTED = "DISCONNECTED";
+    public static final String DATA_STATE_CONNECTING = "CONNECTING";
+    public static final String DATA_STATE_SUSPENDED = "SUSPENDED";
+    public static final String DATA_STATE_UNKNOWN = "UNKNOWN";
+
+    /**
+     * Constant for Telephony Manager Call State
+     * **/
+    public static final String TELEPHONY_STATE_RINGING = "RINGING";
+    public static final String TELEPHONY_STATE_IDLE = "IDLE";
+    public static final String TELEPHONY_STATE_OFFHOOK = "OFFHOOK";
+    public static final String TELEPHONY_STATE_UNKNOWN = "UNKNOWN";
+
+    /**
+     * Constant for TTY Mode
+     * **/
+    public static final String TTY_MODE_FULL = "FULL";
+    public static final String TTY_MODE_HCO = "HCO";
+    public static final String TTY_MODE_OFF = "OFF";
+    public static final String TTY_MODE_VCO ="VCO";
+
+    /**
+     * Constant for Service State
+     * **/
+    public static final String SERVICE_STATE_EMERGENCY_ONLY = "EMERGENCY_ONLY";
+    public static final String SERVICE_STATE_IN_SERVICE = "IN_SERVICE";
+    public static final String SERVICE_STATE_OUT_OF_SERVICE = "OUT_OF_SERVICE";
+    public static final String SERVICE_STATE_POWER_OFF = "POWER_OFF";
+    public static final String SERVICE_STATE_UNKNOWN = "UNKNOWN";
+
+    /**
+     * Constant for VoLTE Hand-over Service State
+     * **/
+    public static final String VOLTE_SERVICE_STATE_HANDOVER_STARTED = "STARTED";
+    public static final String VOLTE_SERVICE_STATE_HANDOVER_COMPLETED = "COMPLETED";
+    public static final String VOLTE_SERVICE_STATE_HANDOVER_FAILED = "FAILED";
+    public static final String VOLTE_SERVICE_STATE_HANDOVER_CANCELED = "CANCELED";
+    public static final String VOLTE_SERVICE_STATE_HANDOVER_UNKNOWN = "UNKNOWN";
+
+    /**
+     * Constant for precise call state state listen level
+     * **/
+    public static final String PRECISE_CALL_STATE_LISTEN_LEVEL_FOREGROUND = "FOREGROUND";
+    public static final String PRECISE_CALL_STATE_LISTEN_LEVEL_RINGING = "RINGING";
+    public static final String PRECISE_CALL_STATE_LISTEN_LEVEL_BACKGROUND = "BACKGROUND";
+
+    /**
+     * Constant for Video Call Session Event Name
+     * **/
+    public static final String SESSION_EVENT_RX_PAUSE = "SESSION_EVENT_RX_PAUSE";
+    public static final String SESSION_EVENT_RX_RESUME = "SESSION_EVENT_RX_RESUME";
+    public static final String SESSION_EVENT_TX_START = "SESSION_EVENT_TX_START";
+    public static final String SESSION_EVENT_TX_STOP = "SESSION_EVENT_TX_STOP";
+    public static final String SESSION_EVENT_CAMERA_FAILURE = "SESSION_EVENT_CAMERA_FAILURE";
+    public static final String SESSION_EVENT_CAMERA_READY = "SESSION_EVENT_CAMERA_READY";
+    public static final String SESSION_EVENT_UNKNOWN = "SESSION_EVENT_UNKNOWN";
+
+    /**
+     * Constants used to Register or de-register for Video Call Callbacks
+     * **/
+    public static final String EVENT_VIDEO_SESSION_MODIFY_REQUEST_RECEIVED = "EVENT_VIDEO_SESSION_MODIFY_REQUEST_RECEIVED";
+    public static final String EVENT_VIDEO_SESSION_MODIFY_RESPONSE_RECEIVED = "EVENT_VIDEO_SESSION_MODIFY_RESPONSE_RECEIVED";
+    public static final String EVENT_VIDEO_SESSION_EVENT = "EVENT_VIDEO_SESSION_EVENT";
+    public static final String EVENT_VIDEO_PEER_DIMENSIONS_CHANGED = "EVENT_VIDEO_PEER_DIMENSIONS_CHANGED";
+    public static final String EVENT_VIDEO_QUALITY_CHANGED = "EVENT_VIDEO_QUALITY_CHANGED";
+    public static final String EVENT_VIDEO_DATA_USAGE_CHANGED = "EVENT_VIDEO_DATA_USAGE_CHANGED";
+    public static final String EVENT_VIDEO_CAMERA_CAPABILITIES_CHANGED = "EVENT_VIDEO_CAMERA_CAPABILITIES_CHANGED";
+    public static final String EVENT_VIDEO_INVALID = "EVENT_VIDEO_INVALID";
+
+    /**
+     * Constant for Network Preference
+     * **/
+    public static final String NETWORK_MODE_WCDMA_PREF = "NETWORK_MODE_WCDMA_PREF";
+    public static final String NETWORK_MODE_GSM_ONLY = "NETWORK_MODE_GSM_ONLY";
+    public static final String NETWORK_MODE_WCDMA_ONLY = "NETWORK_MODE_WCDMA_ONLY";
+    public static final String NETWORK_MODE_GSM_UMTS = "NETWORK_MODE_GSM_UMTS";
+    public static final String NETWORK_MODE_CDMA = "NETWORK_MODE_CDMA";
+    public static final String NETWORK_MODE_CDMA_NO_EVDO = "NETWORK_MODE_CDMA_NO_EVDO";
+    public static final String NETWORK_MODE_EVDO_NO_CDMA = "NETWORK_MODE_EVDO_NO_CDMA";
+    public static final String NETWORK_MODE_GLOBAL = "NETWORK_MODE_GLOBAL";
+    public static final String NETWORK_MODE_LTE_CDMA_EVDO = "NETWORK_MODE_LTE_CDMA_EVDO";
+    public static final String NETWORK_MODE_LTE_GSM_WCDMA = "NETWORK_MODE_LTE_GSM_WCDMA";
+    public static final String NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA = "NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA";
+    public static final String NETWORK_MODE_LTE_ONLY = "NETWORK_MODE_LTE_ONLY";
+    public static final String NETWORK_MODE_LTE_WCDMA = "NETWORK_MODE_LTE_WCDMA";
+    public static final String NETWORK_MODE_TDSCDMA_ONLY = "NETWORK_MODE_TDSCDMA_ONLY";
+    public static final String NETWORK_MODE_TDSCDMA_WCDMA = "NETWORK_MODE_TDSCDMA_WCDMA";
+    public static final String NETWORK_MODE_LTE_TDSCDMA = "NETWORK_MODE_LTE_TDSCDMA";
+    public static final String NETWORK_MODE_TDSCDMA_GSM = "NETWORK_MODE_TDSCDMA_GSM";
+    public static final String NETWORK_MODE_LTE_TDSCDMA_GSM = "NETWORK_MODE_LTE_TDSCDMA_GSM";
+    public static final String NETWORK_MODE_TDSCDMA_GSM_WCDMA = "NETWORK_MODE_TDSCDMA_GSM_WCDMA";
+    public static final String NETWORK_MODE_LTE_TDSCDMA_WCDMA = "NETWORK_MODE_LTE_TDSCDMA_WCDMA";
+    public static final String NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA = "NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA";
+    public static final String NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA = "NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA";
+    public static final String NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA = "NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA";
+    public static final String NETWORK_MODE_INVALID = "INVALID";
+
+    /**
+     * Constant for Messaging Event Name
+     * **/
+    public static final String EventSmsDeliverSuccess = "SmsDeliverSuccess";
+    public static final String EventSmsDeliverFailure = "SmsDeliverFailure";
+    public static final String EventSmsSentSuccess = "SmsSentSuccess";
+    public static final String EventSmsSentFailure = "SmsSentFailure";
+    public static final String EventSmsReceived = "SmsReceived";
+    public static final String EventMmsSentSuccess = "MmsSentSuccess";
+    public static final String EventMmsSentFailure = "MmsSentFailure";
+    public static final String EventMmsDownloaded = "MmsDownloaded";
+    public static final String EventWapPushReceived = "WapPushReceived";
+    public static final String EventDataSmsReceived = "DataSmsReceived";
+    public static final String EventCmasReceived = "CmasReceived";
+    public static final String EventEtwsReceived = "EtwsReceived";
+
+    /**
+     * Constant for Telecom Call Event Name
+     * **/
+    public static final String EventTelecomCallStateChanged = "TelecomCallStateChanged";
+    public static final String EventTelecomCallParentChanged = "TelecomCallParentChanged";
+    public static final String EventTelecomCallChildrenChanged = "TelecomCallChildrenChanged";
+    public static final String EventTelecomCallDetailsChanged = "TelecomCallDetailsChanged";
+    public static final String EventTelecomCallCannedTextResponsesLoaded = "TelecomCallCannedTextResponsesLoaded";
+    public static final String EventTelecomCallPostDialWait = "TelecomCallPostDialWait";
+    public static final String EventTelecomCallVideoCallChanged = "TelecomCallVideoCallChanged";
+    public static final String EventTelecomCallDestroyed = "TelecomCallDestroyed";
+    public static final String EventTelecomCallConferenceableCallsChanged = "TelecomCallConferenceableCallsChanged";
+
+    /**
+     * Constant for Video Call Event Name
+     * **/
+    public static final String EventTelecomVideoCallSessionModifyRequestReceived = "TelecomVideoCallSessionModifyRequestReceived";
+    public static final String EventTelecomVideoCallSessionModifyResponseReceived = "TelecomVideoCallSessionModifyResponseReceived";
+    public static final String EventTelecomVideoCallSessionEvent = "TelecomVideoCallSessionEvent";
+    public static final String EventTelecomVideoCallPeerDimensionsChanged = "TelecomVideoCallPeerDimensionsChanged";
+    public static final String EventTelecomVideoCallVideoQualityChanged = "TelecomVideoCallVideoQualityChanged";
+    public static final String EventTelecomVideoCallDataUsageChanged = "TelecomVideoCallDataUsageChanged";
+    public static final String EventTelecomVideoCallCameraCapabilities = "TelecomVideoCallCameraCapabilities";
+
+    /**
+     * Constant for Other Event Name
+     * **/
+    public static final String EventCellInfoChanged = "CellInfoChanged";
+    public static final String EventCallStateChanged = "CallStateChanged";
+    public static final String EventPreciseStateChanged = "PreciseStateChanged";
+    public static final String EventDataConnectionRealTimeInfoChanged = "DataConnectionRealTimeInfoChanged";
+    public static final String EventDataConnectionStateChanged = "DataConnectionStateChanged";
+    public static final String EventServiceStateChanged = "ServiceStateChanged";
+    public static final String EventSignalStrengthChanged = "SignalStrengthChanged";
+    public static final String EventVolteServiceStateChanged = "VolteServiceStateChanged";
+    public static final String EventMessageWaitingIndicatorChanged = "MessageWaitingIndicatorChanged";
+    public static final String EventConnectivityChanged = "ConnectivityChanged";
+
+    /**
+     * Constant for Packet Keep Alive Call Back
+     * **/
+    public static final String EventPacketKeepaliveCallback = "PacketKeepaliveCallback";
+
+    /*Sub-Event Names*/
+    public static final String PacketKeepaliveCallbackStarted = "Started";
+    public static final String PacketKeepaliveCallbackStopped = "Stopped";
+    public static final String PacketKeepaliveCallbackError = "Error";
+    public static final String PacketKeepaliveCallbackInvalid = "Invalid";
+
+    /**
+     * Constant for Network Call Back
+     * **/
+    public static final String EventNetworkCallback = "NetworkCallback";
+
+    /*Sub-Event Names*/
+    public static final String NetworkCallbackPreCheck = "PreCheck";
+    public static final String NetworkCallbackAvailable = "Available";
+    public static final String NetworkCallbackLosing = "Losing";
+    public static final String NetworkCallbackLost = "Lost";
+    public static final String NetworkCallbackUnavailable = "Unavailable";
+    public static final String NetworkCallbackCapabilitiesChanged = "CapabilitiesChanged";
+    public static final String NetworkCallbackSuspended = "Suspended";
+    public static final String NetworkCallbackResumed = "Resumed";
+    public static final String NetworkCallbackLinkPropertiesChanged = "LinkPropertiesChanged";
+    public static final String NetworkCallbackInvalid = "Invalid";
+
+    /**
+     * Constant for Signal Strength fields
+     * **/
+    public static class SignalStrengthContainer {
+        public static final String SIGNAL_STRENGTH_GSM = "gsmSignalStrength";
+        public static final String SIGNAL_STRENGTH_GSM_DBM = "gsmDbm";
+        public static final String SIGNAL_STRENGTH_GSM_LEVEL = "gsmLevel";
+        public static final String SIGNAL_STRENGTH_GSM_ASU_LEVEL = "gsmAsuLevel";
+        public static final String SIGNAL_STRENGTH_GSM_BIT_ERROR_RATE = "gsmBitErrorRate";
+        public static final String SIGNAL_STRENGTH_CDMA_DBM = "cdmaDbm";
+        public static final String SIGNAL_STRENGTH_CDMA_LEVEL = "cdmaLevel";
+        public static final String SIGNAL_STRENGTH_CDMA_ASU_LEVEL = "cdmaAsuLevel";
+        public static final String SIGNAL_STRENGTH_CDMA_ECIO = "cdmaEcio";
+        public static final String SIGNAL_STRENGTH_EVDO_DBM = "evdoDbm";
+        public static final String SIGNAL_STRENGTH_EVDO_ECIO = "evdoEcio";
+        public static final String SIGNAL_STRENGTH_LTE = "lteSignalStrength";
+        public static final String SIGNAL_STRENGTH_LTE_DBM = "lteDbm";
+        public static final String SIGNAL_STRENGTH_LTE_LEVEL = "lteLevel";
+        public static final String SIGNAL_STRENGTH_LTE_ASU_LEVEL = "lteAsuLevel";
+        public static final String SIGNAL_STRENGTH_DBM = "dbm";
+        public static final String SIGNAL_STRENGTH_LEVEL = "level";
+        public static final String SIGNAL_STRENGTH_ASU_LEVEL = "asuLevel";
+    }
+
+    public static class CallStateContainer {
+        public static final String INCOMING_NUMBER = "incomingNumber";
+        public static final String SUBSCRIPTION_ID = "subscriptionId";
+        public static final String CALL_STATE = "callState";
+    }
+
+    public static class PreciseCallStateContainer {
+        public static final String TYPE = "type";
+        public static final String CAUSE = "cause";
+        public static final String SUBSCRIPTION_ID = "subscriptionId";
+        public static final String PRECISE_CALL_STATE = "preciseCallState";
+    }
+
+    public static class DataConnectionRealTimeInfoContainer {
+        public static final String TYPE = "type";
+        public static final String TIME = "time";
+        public static final String SUBSCRIPTION_ID = "subscriptionId";
+        public static final String DATA_CONNECTION_POWER_STATE = "dataConnectionPowerState";
+    }
+
+    public static class DataConnectionStateContainer {
+        public static final String TYPE = "type";
+        public static final String DATA_NETWORK_TYPE = "dataNetworkType";
+        public static final String STATE_CODE = "stateCode";
+        public static final String SUBSCRIPTION_ID = "subscriptionId";
+        public static final String DATA_CONNECTION_STATE = "dataConnectionState";
+    }
+
+    public static class ServiceStateContainer {
+        public static final String VOICE_REG_STATE = "voiceRegState";
+        public static final String VOICE_NETWORK_TYPE = "voiceNetworkType";
+        public static final String DATA_REG_STATE = "dataRegState";
+        public static final String DATA_NETWORK_TYPE = "dataNetworkType";
+        public static final String OPERATOR_NAME = "operatorName";
+        public static final String OPERATOR_ID = "operatorId";
+        public static final String IS_MANUAL_NW_SELECTION = "isManualNwSelection";
+        public static final String ROAMING = "roaming";
+        public static final String IS_EMERGENCY_ONLY = "isEmergencyOnly";
+        public static final String NETWORK_ID = "networkId";
+        public static final String SYSTEM_ID = "systemId";
+        public static final String SUBSCRIPTION_ID = "subscriptionId";
+        public static final String SERVICE_STATE = "serviceState";
+    }
+
+    public static class MessageWaitingIndicatorContainer {
+        public static final String IS_MESSAGE_WAITING = "isMessageWaiting";
+    }
+
+    public static class VoLteServiceStateContainer {
+        public static final String SRVCC_STATE = "srvccState";
+    }
+
+    public static class PacketKeepaliveContainer {
+        public static final String ID = "id";
+        public static final String PACKET_KEEPALIVE_EVENT = "packetKeepaliveEvent";
+    }
+
+    public static class NetworkCallbackContainer {
+        public static final String ID = "id";
+        public static final String NETWORK_CALLBACK_EVENT = "networkCallbackEvent";
+        public static final String MAX_MS_TO_LIVE = "maxMsToLive";
+        public static final String RSSI = "rssi";
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyEvents.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyEvents.java
new file mode 100644
index 0000000..2c2e621
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyEvents.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import android.telephony.DataConnectionRealTimeInfo;
+import android.telephony.PreciseCallState;
+import android.telephony.ServiceState;
+import com.googlecode.android_scripting.jsonrpc.JsonSerializable;
+import com.googlecode.android_scripting.facade.telephony.TelephonyConstants;
+import com.googlecode.android_scripting.facade.telephony.TelephonyUtils;
+
+public class TelephonyEvents {
+
+    public static class CallStateEvent implements JsonSerializable {
+        private String mCallState;
+        private String mIncomingNumber;
+        private int mSubscriptionId;
+
+        CallStateEvent(int state, String incomingNumber, int subscriptionId) {
+            mCallState = null;
+            mIncomingNumber = TelephonyUtils.formatIncomingNumber(
+                    incomingNumber);
+            mCallState = TelephonyUtils.getTelephonyCallStateString(
+                    state);
+            mSubscriptionId = subscriptionId;
+        }
+
+        public String getIncomingNumber() {
+            return mIncomingNumber;
+        }
+
+        public int getSubscriptionId() {
+            return mSubscriptionId;
+        }
+
+        public JSONObject toJSON() throws JSONException {
+            JSONObject callState = new JSONObject();
+
+            callState.put(
+                    TelephonyConstants.CallStateContainer.SUBSCRIPTION_ID,
+                    mSubscriptionId);
+            callState.put(
+                    TelephonyConstants.CallStateContainer.INCOMING_NUMBER,
+                    mIncomingNumber);
+            callState.put(TelephonyConstants.CallStateContainer.CALL_STATE,
+                    mCallState);
+
+            return callState;
+        }
+    }
+
+    public static class PreciseCallStateEvent implements JsonSerializable {
+        private PreciseCallState mPreciseCallState;
+        private String mPreciseCallStateString;
+        private String mType;
+        private int mCause;
+        private int mSubscriptionId;
+
+        PreciseCallStateEvent(int newState, String type,
+                PreciseCallState preciseCallState, int subscriptionId) {
+            mPreciseCallStateString = TelephonyUtils.getPreciseCallStateString(
+                    newState);
+            mPreciseCallState = preciseCallState;
+            mType = type;
+            mSubscriptionId = subscriptionId;
+            mCause = preciseCallState.getPreciseDisconnectCause();
+        }
+
+        public String getType() {
+            return mType;
+        }
+
+        public int getSubscriptionId() {
+            return mSubscriptionId;
+        }
+
+        public PreciseCallState getPreciseCallState() {
+            return mPreciseCallState;
+        }
+
+        public int getCause() {
+            return mCause;
+        }
+
+        public JSONObject toJSON() throws JSONException {
+            JSONObject preciseCallState = new JSONObject();
+
+            preciseCallState.put(
+                    TelephonyConstants.PreciseCallStateContainer.SUBSCRIPTION_ID,
+                    mSubscriptionId);
+            preciseCallState.put(
+                    TelephonyConstants.PreciseCallStateContainer.TYPE, mType);
+            preciseCallState.put(
+                    TelephonyConstants.PreciseCallStateContainer.PRECISE_CALL_STATE,
+                    mPreciseCallStateString);
+            preciseCallState.put(
+                    TelephonyConstants.PreciseCallStateContainer.CAUSE, mCause);
+
+            return preciseCallState;
+        }
+    }
+
+    public static class DataConnectionRealTimeInfoEvent implements JsonSerializable {
+        private DataConnectionRealTimeInfo mDataConnectionRealTimeInfo;
+        private String mDataConnectionPowerState;
+        private int mSubscriptionId;
+        private long mTime;
+
+        DataConnectionRealTimeInfoEvent(
+                DataConnectionRealTimeInfo dataConnectionRealTimeInfo,
+                int subscriptionId) {
+            mTime = dataConnectionRealTimeInfo.getTime();
+            mSubscriptionId = subscriptionId;
+            mDataConnectionPowerState = TelephonyUtils.getDcPowerStateString(
+                    dataConnectionRealTimeInfo.getDcPowerState());
+            mDataConnectionRealTimeInfo = dataConnectionRealTimeInfo;
+        }
+
+        public int getSubscriptionId() {
+            return mSubscriptionId;
+        }
+
+        public long getTime() {
+            return mTime;
+        }
+
+        public JSONObject toJSON() throws JSONException {
+            JSONObject dataConnectionRealTimeInfo = new JSONObject();
+
+            dataConnectionRealTimeInfo.put(
+                    TelephonyConstants.DataConnectionRealTimeInfoContainer.SUBSCRIPTION_ID,
+                    mSubscriptionId);
+            dataConnectionRealTimeInfo.put(
+                    TelephonyConstants.DataConnectionRealTimeInfoContainer.TIME,
+                    mTime);
+            dataConnectionRealTimeInfo.put(
+                    TelephonyConstants.DataConnectionRealTimeInfoContainer.DATA_CONNECTION_POWER_STATE,
+                    mDataConnectionPowerState);
+
+            return dataConnectionRealTimeInfo;
+        }
+    }
+
+    public static class DataConnectionStateEvent implements JsonSerializable {
+        private String mDataConnectionState;
+        private int mSubscriptionId;
+        private int mState;
+        private String mDataNetworkType;
+
+        DataConnectionStateEvent(int state, String dataNetworkType,
+                int subscriptionId) {
+            mSubscriptionId = subscriptionId;
+            mDataConnectionState = TelephonyUtils.getDataConnectionStateString(
+                    state);
+            mDataNetworkType = dataNetworkType;
+            mState = state;
+        }
+
+        public int getSubscriptionId() {
+            return mSubscriptionId;
+        }
+
+        public int getState() {
+            return mState;
+        }
+
+        public String getDataNetworkType() {
+            return mDataNetworkType;
+        }
+
+        public JSONObject toJSON() throws JSONException {
+            JSONObject dataConnectionState = new JSONObject();
+
+            dataConnectionState.put(
+                    TelephonyConstants.DataConnectionStateContainer.SUBSCRIPTION_ID,
+                    mSubscriptionId);
+            dataConnectionState.put(
+                    TelephonyConstants.DataConnectionStateContainer.DATA_CONNECTION_STATE,
+                    mDataConnectionState);
+            dataConnectionState.put(
+                    TelephonyConstants.DataConnectionStateContainer.DATA_NETWORK_TYPE,
+                    mDataNetworkType);
+            dataConnectionState.put(
+                    TelephonyConstants.DataConnectionStateContainer.STATE_CODE,
+                    mState);
+
+            return dataConnectionState;
+        }
+    }
+
+    public static class ServiceStateEvent implements JsonSerializable {
+        private String mServiceStateString;
+        private int mSubscriptionId;
+        private ServiceState mServiceState;
+
+        ServiceStateEvent(ServiceState serviceState, int subscriptionId) {
+            mServiceState = serviceState;
+            mSubscriptionId = subscriptionId;
+            mServiceStateString = TelephonyUtils.getNetworkStateString(
+                    serviceState.getState());
+            if (mServiceStateString.equals(
+                    TelephonyConstants.SERVICE_STATE_OUT_OF_SERVICE) &&
+                    serviceState.isEmergencyOnly()) {
+                mServiceStateString = TelephonyConstants.SERVICE_STATE_EMERGENCY_ONLY;
+            }
+        }
+
+        public int getSubscriptionId() {
+            return mSubscriptionId;
+        }
+
+        public ServiceState getServiceState() {
+            return mServiceState;
+        }
+
+        public JSONObject toJSON() throws JSONException {
+            JSONObject serviceState = new JSONObject();
+
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.SUBSCRIPTION_ID,
+                    mSubscriptionId);
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.VOICE_REG_STATE,
+                    TelephonyUtils.getNetworkStateString(
+                            mServiceState.getVoiceRegState()));
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.VOICE_NETWORK_TYPE,
+                    TelephonyUtils.getNetworkTypeString(
+                            mServiceState.getVoiceNetworkType()));
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.DATA_REG_STATE,
+                    TelephonyUtils.getNetworkStateString(
+                            mServiceState.getDataRegState()));
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.DATA_NETWORK_TYPE,
+                    TelephonyUtils.getNetworkTypeString(
+                            mServiceState.getDataNetworkType()));
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.OPERATOR_NAME,
+                    mServiceState.getOperatorAlphaLong());
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.OPERATOR_ID,
+                    mServiceState.getOperatorNumeric());
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.IS_MANUAL_NW_SELECTION,
+                    mServiceState.getIsManualSelection());
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.ROAMING,
+                    mServiceState.getRoaming());
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.IS_EMERGENCY_ONLY,
+                    mServiceState.isEmergencyOnly());
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.NETWORK_ID,
+                    mServiceState.getNetworkId());
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.SYSTEM_ID,
+                    mServiceState.getSystemId());
+            serviceState.put(
+                    TelephonyConstants.ServiceStateContainer.SERVICE_STATE,
+                    mServiceStateString);
+
+            return serviceState;
+        }
+    }
+
+    public static class MessageWaitingIndicatorEvent implements JsonSerializable {
+        private boolean mMessageWaitingIndicator;
+
+        MessageWaitingIndicatorEvent(boolean messageWaitingIndicator) {
+            mMessageWaitingIndicator = messageWaitingIndicator;
+        }
+
+        public boolean getMessageWaitingIndicator() {
+            return mMessageWaitingIndicator;
+        }
+
+        public JSONObject toJSON() throws JSONException {
+            JSONObject messageWaitingIndicator = new JSONObject();
+
+            messageWaitingIndicator.put(
+                    TelephonyConstants.MessageWaitingIndicatorContainer.IS_MESSAGE_WAITING,
+                    mMessageWaitingIndicator);
+
+            return messageWaitingIndicator;
+        }
+    }
+
+    public static class PacketKeepaliveEvent implements JsonSerializable {
+        private String mId;
+        private String mPacketKeepaliveEvent;
+
+        public PacketKeepaliveEvent(String id, String event) {
+            mId = id;
+            mPacketKeepaliveEvent = event;
+        }
+
+        public JSONObject toJSON() throws JSONException {
+            JSONObject packetKeepalive = new JSONObject();
+
+            packetKeepalive.put(
+                    TelephonyConstants.PacketKeepaliveContainer.ID,
+                    mId);
+            packetKeepalive.put(
+                    TelephonyConstants.PacketKeepaliveContainer.PACKET_KEEPALIVE_EVENT,
+                    mPacketKeepaliveEvent);
+
+            return packetKeepalive;
+        }
+    }
+
+    public static class NetworkCallbackEvent implements JsonSerializable {
+        public static final Integer INVALID_VALUE = null;
+        private String mId;
+        private String mNetworkCallbackEvent;
+        private Integer mRssi;
+        private Integer mMaxMsToLive;
+
+        public NetworkCallbackEvent(String id, String event, Integer rssi, Integer maxMsToLive) {
+            mId = id;
+            mNetworkCallbackEvent = event;
+            mRssi = rssi;
+            mMaxMsToLive = maxMsToLive;
+        }
+
+        public JSONObject toJSON() throws JSONException {
+            JSONObject networkCallback = new JSONObject();
+
+            networkCallback.put(
+                    TelephonyConstants.NetworkCallbackContainer.ID,
+                    mId);
+            networkCallback.put(
+                    TelephonyConstants.NetworkCallbackContainer.NETWORK_CALLBACK_EVENT,
+                    mNetworkCallbackEvent);
+            networkCallback.put(
+                    TelephonyConstants.NetworkCallbackContainer.MAX_MS_TO_LIVE,
+                    mMaxMsToLive);
+            networkCallback.put(
+                    TelephonyConstants.NetworkCallbackContainer.RSSI,
+                    mRssi);
+
+            return networkCallback;
+        }
+    }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java
new file mode 100644
index 0000000..333274a
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java
@@ -0,0 +1,1183 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.telephony.CellInfo;
+import android.telephony.CellLocation;
+import android.telephony.ModemActivityInfo;
+import android.telephony.NeighboringCellInfo;
+import android.telephony.PhoneStateListener;
+import android.telephony.SignalStrength;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.provider.Telephony;
+import android.telephony.SubscriptionInfo;
+import android.telecom.VideoProfile;
+import android.telecom.TelecomManager;
+import android.util.Base64;
+
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.RILConstants;
+import com.android.internal.telephony.TelephonyProperties;
+import com.google.common.io.BaseEncoding;
+
+import android.content.ContentValues;
+import android.os.SystemProperties;
+
+import com.googlecode.android_scripting.facade.AndroidFacade;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.facade.telephony.TelephonyStateListeners
+                                                   .CallStateChangeListener;
+import com.googlecode.android_scripting.facade.telephony.TelephonyStateListeners
+                                                   .CellInfoChangeListener;
+import com.googlecode.android_scripting.facade.telephony.TelephonyStateListeners
+                                                   .DataConnectionRealTimeInfoChangeListener;
+import com.googlecode.android_scripting.facade.telephony.TelephonyStateListeners
+                                                   .DataConnectionStateChangeListener;
+import com.googlecode.android_scripting.facade.telephony.TelephonyStateListeners
+                                                   .ServiceStateChangeListener;
+import com.googlecode.android_scripting.facade.telephony.TelephonyStateListeners
+                                                   .SignalStrengthChangeListener;
+import com.googlecode.android_scripting.facade.telephony.TelephonyStateListeners
+                                                   .VoiceMailStateChangeListener;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.MainThread;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Field;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.HashMap;
+
+/**
+ * Exposes TelephonyManager functionality.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Felix Arends (felix.arends@gmail.com)
+ */
+public class TelephonyManagerFacade extends RpcReceiver {
+
+    private final Service mService;
+    private final AndroidFacade mAndroidFacade;
+    private final EventFacade mEventFacade;
+    private final TelephonyManager mTelephonyManager;
+    private final SubscriptionManager mSubscriptionManager;
+    private List<SubscriptionInfo> mSubInfos;
+    private HashMap<Integer, StateChangeListener> StateChangeListeners =
+                             new HashMap<Integer, StateChangeListener>();
+
+    private static final String[] sProjection = new String[] {
+            Telephony.Carriers._ID, // 0
+            Telephony.Carriers.NAME, // 1
+            Telephony.Carriers.APN, // 2
+            Telephony.Carriers.PROXY, // 3
+            Telephony.Carriers.PORT, // 4
+            Telephony.Carriers.USER, // 5
+            Telephony.Carriers.SERVER, // 6
+            Telephony.Carriers.PASSWORD, // 7
+            Telephony.Carriers.MMSC, // 8
+            Telephony.Carriers.MCC, // 9
+            Telephony.Carriers.MNC, // 10
+            Telephony.Carriers.NUMERIC, // 11
+            Telephony.Carriers.MMSPROXY,// 12
+            Telephony.Carriers.MMSPORT, // 13
+            Telephony.Carriers.AUTH_TYPE, // 14
+            Telephony.Carriers.TYPE, // 15
+            Telephony.Carriers.PROTOCOL, // 16
+            Telephony.Carriers.CARRIER_ENABLED, // 17
+            Telephony.Carriers.BEARER_BITMASK, // 18
+            Telephony.Carriers.ROAMING_PROTOCOL, // 19
+            Telephony.Carriers.MVNO_TYPE, // 20
+            Telephony.Carriers.MVNO_MATCH_DATA // 21
+    };
+
+    public TelephonyManagerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mTelephonyManager =
+                (TelephonyManager) mService.getSystemService(Context.TELEPHONY_SERVICE);
+        mAndroidFacade = manager.getReceiver(AndroidFacade.class);
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mSubscriptionManager = SubscriptionManager.from(mService);
+        mSubInfos = mSubscriptionManager.getAllSubscriptionInfoList();
+        MainThread.run(manager.getService(), new Callable<Object>() {
+            @Override
+            public Object call() throws Exception {
+                // Creating listeners for all subscription IDs
+                for (int i = 0; i < mSubInfos.size(); i++) {
+                    int subId = mSubInfos.get(i).getSubscriptionId();
+                    StateChangeListener tempStateListener =
+                                                     new StateChangeListener();
+                    tempStateListener.mServiceStateChangeListener =
+                        new ServiceStateChangeListener(mEventFacade, subId);
+                    tempStateListener.mSignalStrengthChangeListener =
+                        new SignalStrengthChangeListener(mEventFacade, subId);
+                    tempStateListener.mDataConnectionStateChangeListener =
+                        new DataConnectionStateChangeListener(mEventFacade,
+                                                      mTelephonyManager, subId);
+                    tempStateListener.mCallStateChangeListener =
+                        new CallStateChangeListener(mEventFacade, subId);
+                    tempStateListener.mCellInfoChangeListener =
+                        new CellInfoChangeListener(mEventFacade, subId);
+                    tempStateListener.mDataConnectionRTInfoChangeListener =
+                        new DataConnectionRealTimeInfoChangeListener(mEventFacade,
+                                                                     subId);
+                    tempStateListener.mVoiceMailStateChangeListener =
+                        new VoiceMailStateChangeListener(mEventFacade, subId);
+
+                    StateChangeListeners.put(subId, tempStateListener);
+                }
+                return null;
+            }
+        });
+    }
+
+    @Rpc(description = "Set network preference.")
+    public boolean telephonySetPreferredNetworkTypes(
+        @RpcParameter(name = "nwPreference") String nwPreference) {
+        return telephonySetPreferredNetworkTypesForSubscription(nwPreference,
+                SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Set network preference for subscription.")
+    public boolean telephonySetPreferredNetworkTypesForSubscription(
+            @RpcParameter(name = "nwPreference") String nwPreference,
+            @RpcParameter(name = "subId") Integer subId) {
+        int networkPreferenceInt = TelephonyUtils.getNetworkModeIntfromString(
+            nwPreference);
+        if (RILConstants.RIL_ERRNO_INVALID_RESPONSE != networkPreferenceInt) {
+            return mTelephonyManager.setPreferredNetworkType(
+                subId, networkPreferenceInt);
+        } else {
+            return false;
+        }
+    }
+
+    @Rpc(description = "Get network preference.")
+    public String telephonyGetPreferredNetworkTypes() {
+        return telephonyGetPreferredNetworkTypesForSubscription(
+                SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Get network preference for subscription.")
+    public String telephonyGetPreferredNetworkTypesForSubscription(
+            @RpcParameter(name = "subId") Integer subId) {
+        int networkPreferenceInt = mTelephonyManager.getPreferredNetworkType(subId);
+        return TelephonyUtils.getNetworkModeStringfromInt(networkPreferenceInt);
+    }
+
+    @Rpc(description = "Get current voice network type")
+    public String telephonyGetCurrentVoiceNetworkType() {
+        return telephonyGetCurrentVoiceNetworkTypeForSubscription(
+                SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Get current voice network type for subscription")
+    public String telephonyGetCurrentVoiceNetworkTypeForSubscription(
+            @RpcParameter(name = "subId") Integer subId) {
+        return TelephonyUtils.getNetworkTypeString(
+            mTelephonyManager.getVoiceNetworkType(subId));
+    }
+
+    @Rpc(description = "Get current data network type")
+    public String telephonyGetCurrentDataNetworkType() {
+        return telephonyGetCurrentDataNetworkTypeForSubscription(
+                SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Get current data network type for subscription")
+    public String telephonyGetCurrentDataNetworkTypeForSubscription(
+            @RpcParameter(name = "subId") Integer subId) {
+        return TelephonyUtils.getNetworkTypeString(
+            mTelephonyManager.getDataNetworkType(subId));
+    }
+
+    @Rpc(description = "Get if phone have voice capability")
+    public boolean telephonyIsVoiceCapable() {
+        return mTelephonyManager.isVoiceCapable();
+    }
+
+    @Rpc(description = "Get preferred network setting for " +
+                       "default subscription ID .Return value is integer.")
+    public int telephonyGetPreferredNetworkTypeInteger() {
+        return telephonyGetPreferredNetworkTypeIntegerForSubscription(
+                                         SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Get preferred network setting for " +
+                       "specified subscription ID .Return value is integer.")
+    public int telephonyGetPreferredNetworkTypeIntegerForSubscription(
+               @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getPreferredNetworkType(subId);
+    }
+
+    @Rpc(description = "Starts tracking call state change" +
+                       "for default subscription ID.")
+    public Boolean telephonyStartTrackingCallState() {
+        return telephonyStartTrackingCallStateForSubscription(
+                              SubscriptionManager.getDefaultVoiceSubscriptionId());
+    }
+
+    @Rpc(description = "Starts tracking call state change" +
+                       "for specified subscription ID.")
+    public Boolean telephonyStartTrackingCallStateForSubscription(
+                @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mCallStateChangeListener,
+                CallStateChangeListener.sListeningStates);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Starts tracking cell info change" +
+                       "for default subscription ID.")
+    public Boolean telephonyStartTrackingCellInfoChange() {
+        return telephonyStartTrackingCellInfoChangeForSubscription(
+                              SubscriptionManager.getDefaultVoiceSubscriptionId());
+    }
+
+    @Rpc(description = "Starts tracking cell info change" +
+                       "for specified subscription ID.")
+    public Boolean telephonyStartTrackingCellInfoChangeForSubscription(
+                @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mCellInfoChangeListener,
+                PhoneStateListener.LISTEN_CELL_INFO);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Turn on/off precise listening on fore/background or" +
+                       " ringing calls for default voice subscription ID.")
+    public Boolean telephonyAdjustPreciseCallStateListenLevel(String type,
+                                                          Boolean listen) {
+        return telephonyAdjustPreciseCallStateListenLevelForSubscription(type, listen,
+                                 SubscriptionManager.getDefaultVoiceSubscriptionId());
+    }
+
+    @Rpc(description = "Turn on/off precise listening on fore/background or" +
+                       " ringing calls for specified subscription ID.")
+    public Boolean telephonyAdjustPreciseCallStateListenLevelForSubscription(String type,
+                   Boolean listen,
+                   @RpcParameter(name = "subId") Integer subId) {
+        try {
+            if (type.equals(TelephonyConstants.PRECISE_CALL_STATE_LISTEN_LEVEL_FOREGROUND)) {
+                StateChangeListeners.get(subId).mCallStateChangeListener.listenForeground = listen;
+            } else if (type.equals(TelephonyConstants.PRECISE_CALL_STATE_LISTEN_LEVEL_RINGING)) {
+                StateChangeListeners.get(subId).mCallStateChangeListener.listenRinging = listen;
+            } else if (type.equals(TelephonyConstants.PRECISE_CALL_STATE_LISTEN_LEVEL_BACKGROUND)) {
+                StateChangeListeners.get(subId).mCallStateChangeListener.listenBackground = listen;
+            }
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Stops tracking cell info change " +
+            "for default voice subscription ID.")
+    public Boolean telephonyStopTrackingCellInfoChange() {
+        return telephonyStopTrackingCellInfoChangeForSubscription(
+                SubscriptionManager.getDefaultVoiceSubscriptionId());
+    }
+
+    @Rpc(description = "Stops tracking cell info change " +
+                       "for specified subscription ID.")
+    public Boolean telephonyStopTrackingCellInfoChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mCellInfoChangeListener,
+                PhoneStateListener.LISTEN_NONE);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+    @Rpc(description = "Stops tracking call state change " +
+            "for default voice subscription ID.")
+    public Boolean telephonyStopTrackingCallStateChange() {
+        return telephonyStopTrackingCallStateChangeForSubscription(
+                SubscriptionManager.getDefaultVoiceSubscriptionId());
+    }
+
+    @Rpc(description = "Stops tracking call state change " +
+                       "for specified subscription ID.")
+    public Boolean telephonyStopTrackingCallStateChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mCallStateChangeListener,
+                PhoneStateListener.LISTEN_NONE);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Starts tracking data connection real time info change" +
+                       "for default subscription ID.")
+    public Boolean telephonyStartTrackingDataConnectionRTInfoChange() {
+        return telephonyStartTrackingDataConnectionRTInfoChangeForSubscription(
+                                 SubscriptionManager.getDefaultDataSubscriptionId());
+    }
+
+    @Rpc(description = "Starts tracking data connection real time info change" +
+                       "for specified subscription ID.")
+    public Boolean telephonyStartTrackingDataConnectionRTInfoChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mDataConnectionRTInfoChangeListener,
+                DataConnectionRealTimeInfoChangeListener.sListeningStates);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Stops tracking data connection real time info change" +
+                       "for default subscription ID.")
+    public Boolean telephonyStopTrackingDataConnectionRTInfoChange() {
+        return telephonyStopTrackingDataConnectionRTInfoChangeForSubscription(
+                                 SubscriptionManager.getDefaultDataSubscriptionId());
+    }
+
+    @Rpc(description = "Stops tracking data connection real time info change" +
+                       "for specified subscription ID.")
+    public Boolean telephonyStopTrackingDataConnectionRTInfoChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mDataConnectionRTInfoChangeListener,
+                PhoneStateListener.LISTEN_NONE);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Starts tracking data connection state change" +
+                       "for default subscription ID..")
+    public Boolean telephonyStartTrackingDataConnectionStateChange() {
+        return telephonyStartTrackingDataConnectionStateChangeForSubscription(
+                                 SubscriptionManager.getDefaultDataSubscriptionId());
+    }
+
+    @Rpc(description = "Starts tracking data connection state change" +
+                       "for specified subscription ID.")
+    public Boolean telephonyStartTrackingDataConnectionStateChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mDataConnectionStateChangeListener,
+                DataConnectionStateChangeListener.sListeningStates);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Stops tracking data connection state change " +
+                       "for default subscription ID..")
+    public Boolean telephonyStopTrackingDataConnectionStateChange() {
+        return telephonyStopTrackingDataConnectionStateChangeForSubscription(
+                                 SubscriptionManager.getDefaultDataSubscriptionId());
+    }
+
+    @Rpc(description = "Stops tracking data connection state change " +
+                       "for specified subscription ID..")
+    public Boolean telephonyStopTrackingDataConnectionStateChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mDataConnectionStateChangeListener,
+                PhoneStateListener.LISTEN_NONE);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Starts tracking service state change " +
+                       "for default subscription ID.")
+    public Boolean telephonyStartTrackingServiceStateChange() {
+        return telephonyStartTrackingServiceStateChangeForSubscription(
+                                 SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Starts tracking service state change " +
+                       "for specified subscription ID.")
+    public Boolean telephonyStartTrackingServiceStateChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+         try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mServiceStateChangeListener,
+                ServiceStateChangeListener.sListeningStates);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Stops tracking service state change " +
+                       "for default subscription ID.")
+    public Boolean telephonyStopTrackingServiceStateChange() {
+        return telephonyStopTrackingServiceStateChangeForSubscription(
+                                 SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Stops tracking service state change " +
+                       "for specified subscription ID.")
+    public Boolean telephonyStopTrackingServiceStateChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mServiceStateChangeListener,
+                PhoneStateListener.LISTEN_NONE);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Starts tracking signal strength change " +
+                       "for default subscription ID.")
+    public Boolean telephonyStartTrackingSignalStrengthChange() {
+        return telephonyStartTrackingSignalStrengthChangeForSubscription(
+                                 SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Starts tracking signal strength change " +
+                       "for specified subscription ID.")
+    public Boolean telephonyStartTrackingSignalStrengthChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+         try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mSignalStrengthChangeListener,
+                SignalStrengthChangeListener.sListeningStates);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Stops tracking signal strength change " +
+                       "for default subscription ID.")
+    public Boolean telephonyStopTrackingSignalStrengthChange() {
+        return telephonyStopTrackingSignalStrengthChangeForSubscription(
+                                 SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Stops tracking signal strength change " +
+                       "for specified subscription ID.")
+    public Boolean telephonyStopTrackingSignalStrengthChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mSignalStrengthChangeListener,
+                PhoneStateListener.LISTEN_NONE);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Starts tracking voice mail state change " +
+                       "for default subscription ID.")
+    public Boolean telephonyStartTrackingVoiceMailStateChange() {
+        return telephonyStartTrackingVoiceMailStateChangeForSubscription(
+                                 SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Starts tracking voice mail state change " +
+                       "for specified subscription ID.")
+    public Boolean telephonyStartTrackingVoiceMailStateChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+         try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mVoiceMailStateChangeListener,
+                VoiceMailStateChangeListener.sListeningStates);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Stops tracking voice mail state change " +
+                       "for default subscription ID.")
+    public Boolean telephonyStopTrackingVoiceMailStateChange() {
+        return telephonyStopTrackingVoiceMailStateChangeForSubscription(
+                                 SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Stops tracking voice mail state change " +
+                       "for specified subscription ID.")
+    public Boolean telephonyStopTrackingVoiceMailStateChangeForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        try {
+            mTelephonyManager.listen(
+                StateChangeListeners.get(subId).mVoiceMailStateChangeListener,
+                PhoneStateListener.LISTEN_NONE);
+            return true;
+        } catch (Exception e) {
+            Log.e("Invalid subscription ID");
+            return false;
+        }
+    }
+
+    @Rpc(description = "Answers an incoming ringing call.")
+    public void telephonyAnswerCall() throws RemoteException {
+        mTelephonyManager.silenceRinger();
+        mTelephonyManager.answerRingingCall();
+    }
+
+    @Rpc(description = "Returns the current cell location.")
+    public CellLocation telephonyGetCellLocation() {
+        return mTelephonyManager.getCellLocation();
+    }
+
+    @Rpc(description = "Returns the numeric name (MCC+MNC) of registered operator." +
+                       "for default subscription ID")
+    public String telephonyGetNetworkOperator() {
+        return telephonyGetNetworkOperatorForSubscription(
+                        SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the numeric name (MCC+MNC) of registered operator" +
+                       "for specified subscription ID.")
+    public String telephonyGetNetworkOperatorForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getNetworkOperator(subId);
+    }
+
+    @Rpc(description = "Returns the alphabetic name of current registered operator" +
+                       "for specified subscription ID.")
+    public String telephonyGetNetworkOperatorName() {
+        return telephonyGetNetworkOperatorNameForSubscription(
+                        SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the alphabetic name of registered operator " +
+                       "for specified subscription ID.")
+    public String telephonyGetNetworkOperatorNameForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getNetworkOperatorName(subId);
+    }
+
+    @Rpc(description = "Returns the current RAT in use on the device.+" +
+                       "for default subscription ID")
+    public String telephonyGetNetworkType() {
+
+        Log.d("sl4a:getNetworkType() is deprecated!" +
+                "Please use getVoiceNetworkType()" +
+                " or getDataNetworkTpe()");
+
+        return telephonyGetNetworkTypeForSubscription(
+                       SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the current RAT in use on the device" +
+            " for a given Subscription.")
+    public String telephonyGetNetworkTypeForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+
+        Log.d("sl4a:getNetworkTypeForSubscriber() is deprecated!" +
+                "Please use getVoiceNetworkType()" +
+                " or getDataNetworkTpe()");
+
+        return TelephonyUtils.getNetworkTypeString(
+            mTelephonyManager.getNetworkType(subId));
+    }
+
+    @Rpc(description = "Returns the current voice RAT for" +
+            " the default voice subscription.")
+    public String telephonyGetVoiceNetworkType() {
+        return telephonyGetVoiceNetworkTypeForSubscription(
+                         SubscriptionManager.getDefaultVoiceSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the current voice RAT for" +
+            " the specified voice subscription.")
+    public String telephonyGetVoiceNetworkTypeForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return TelephonyUtils.getNetworkTypeString(
+            mTelephonyManager.getVoiceNetworkType(subId));
+    }
+
+    @Rpc(description = "Returns the current data RAT for" +
+            " the defaut data subscription")
+    public String telephonyGetDataNetworkType() {
+        return telephonyGetDataNetworkTypeForSubscription(
+                         SubscriptionManager.getDefaultDataSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the current data RAT for" +
+            " the specified data subscription")
+    public String telephonyGetDataNetworkTypeForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return TelephonyUtils.getNetworkTypeString(
+            mTelephonyManager.getDataNetworkType(subId));
+    }
+
+    @Rpc(description = "Returns the device phone type.")
+    public String telephonyGetPhoneType() {
+        return TelephonyUtils.getPhoneTypeString(
+            mTelephonyManager.getPhoneType());
+    }
+
+    @Rpc(description = "Returns the MCC for default subscription ID")
+    public String telephonyGetSimCountryIso() {
+         return telephonyGetSimCountryIsoForSubscription(
+                      SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Get the latest power consumption stats from the modem")
+    public ModemActivityInfo telephonyGetModemActivityInfo() {
+        ModemActivityInfo info = mTelephonyManager.getModemActivityInfo();
+        return info;
+    }
+
+    @Rpc(description = "Returns the MCC for specified subscription ID")
+    public String telephonyGetSimCountryIsoForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getSimCountryIso(subId);
+    }
+
+    @Rpc(description = "Returns the MCC+MNC for default subscription ID")
+    public String telephonyGetSimOperator() {
+        return telephonyGetSimOperatorForSubscription(
+                  SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the MCC+MNC for specified subscription ID")
+    public String telephonyGetSimOperatorForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getSimOperator(subId);
+    }
+
+    @Rpc(description = "Returns the Service Provider Name (SPN)" +
+                       "for default subscription ID")
+    public String telephonyGetSimOperatorName() {
+        return telephonyGetSimOperatorNameForSubscription(
+                  SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the Service Provider Name (SPN)" +
+                       " for specified subscription ID.")
+    public String telephonyGetSimOperatorNameForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getSimOperatorName(subId);
+    }
+
+    @Rpc(description = "Returns the serial number of the SIM for " +
+                       "default subscription ID, or Null if unavailable")
+    public String telephonyGetSimSerialNumber() {
+        return telephonyGetSimSerialNumberForSubscription(
+                  SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the serial number of the SIM for " +
+                       "specified subscription ID, or Null if unavailable")
+    public String telephonyGetSimSerialNumberForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getSimSerialNumber(subId);
+    }
+
+    @Rpc(description = "Returns the state of the SIM card for default slot ID.")
+    public String telephonyGetSimState() {
+        return telephonyGetSimStateForSlotId(
+                  mTelephonyManager.getDefaultSim());
+    }
+
+    @Rpc(description = "Returns the state of the SIM card for specified slot ID.")
+    public String telephonyGetSimStateForSlotId(
+                  @RpcParameter(name = "slotId") Integer slotId) {
+        return TelephonyUtils.getSimStateString(
+            mTelephonyManager.getSimState(slotId));
+    }
+
+    @Rpc(description = "Get Authentication Challenge Response from a " +
+            "given SIM Application")
+    public String telephonyGetIccSimChallengeResponse(
+        @RpcParameter(name = "appType") Integer appType,
+            @RpcParameter(name = "hexChallenge") String hexChallenge) {
+        return telephonyGetIccSimChallengeResponseForSubscription(
+                SubscriptionManager.getDefaultSubscriptionId(), appType, hexChallenge);
+    }
+
+    @Rpc(description = "Get Authentication Challenge Response from a " +
+            "given SIM Application for a specified Subscription")
+    public String telephonyGetIccSimChallengeResponseForSubscription(
+            @RpcParameter(name = "subId") Integer subId,
+            @RpcParameter(name = "appType") Integer appType,
+            @RpcParameter(name = "hexChallenge") String hexChallenge) {
+
+        try {
+            String b64Data = BaseEncoding.base64().encode(BaseEncoding.base16().decode(hexChallenge));
+            String b64Result = mTelephonyManager.getIccSimChallengeResponse(subId, appType, b64Data);
+            return (b64Result != null)
+                    ? BaseEncoding.base16().encode(BaseEncoding.base64().decode(b64Result)) : null;
+        } catch( Exception e) {
+            Log.e("Exception in phoneGetIccSimChallengeResponseForSubscription" + e.toString());
+            return null;
+        }
+    }
+
+    @Rpc(description = "Returns the unique subscriber ID (such as IMSI) " +
+            "for default subscription ID, or null if unavailable")
+    public String telephonyGetSubscriberId() {
+        return telephonyGetSubscriberIdForSubscription(
+                SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the unique subscriber ID (such as IMSI) " +
+                       "for specified subscription ID, or null if unavailable")
+    public String telephonyGetSubscriberIdForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getSubscriberId(subId);
+    }
+
+    @Rpc(description = "Retrieves the alphabetic id associated with the" +
+                       " voice mail number for default subscription ID.")
+    public String telephonyGetVoiceMailAlphaTag() {
+        return telephonyGetVoiceMailAlphaTagForSubscription(
+                   SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+
+    @Rpc(description = "Retrieves the alphabetic id associated with the " +
+                       "voice mail number for specified subscription ID.")
+    public String telephonyGetVoiceMailAlphaTagForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getVoiceMailAlphaTag(subId);
+    }
+
+    @Rpc(description = "Returns the voice mail number " +
+                       "for default subscription ID; null if unavailable.")
+    public String telephonyGetVoiceMailNumber() {
+        return telephonyGetVoiceMailNumberForSubscription(
+                   SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the voice mail number " +
+                        "for specified subscription ID; null if unavailable.")
+    public String telephonyGetVoiceMailNumberForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getVoiceMailNumber(subId);
+    }
+
+    @Rpc(description = "Get voice message count for specified subscription ID.")
+    public Integer telephonyGetVoiceMailCountForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getVoiceMessageCount(subId);
+    }
+
+    @Rpc(description = "Get voice message count for default subscription ID.")
+    public Integer telephonyGetVoiceMailCount() {
+        return mTelephonyManager.getVoiceMessageCount();
+    }
+
+    @Rpc(description = "Returns true if the device is in  roaming state" +
+                       "for default subscription ID")
+    public Boolean telephonyCheckNetworkRoaming() {
+        return telephonyCheckNetworkRoamingForSubscription(
+                             SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns true if the device is in roaming state " +
+                       "for specified subscription ID")
+    public Boolean telephonyCheckNetworkRoamingForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.isNetworkRoaming(subId);
+    }
+
+    @Rpc(description = "Returns the unique device ID such as MEID or IMEI " +
+                       "for deault sim slot ID, null if unavailable")
+    public String telephonyGetDeviceId() {
+        return telephonyGetDeviceIdForSlotId(mTelephonyManager.getDefaultSim());
+    }
+
+    @Rpc(description = "Returns the unique device ID such as MEID or IMEI for" +
+                       " specified slot ID, null if unavailable")
+    public String telephonyGetDeviceIdForSlotId(
+                  @RpcParameter(name = "slotId")
+                  Integer slotId){
+        return mTelephonyManager.getDeviceId(slotId);
+    }
+
+    @Rpc(description = "Returns the modem sw version, such as IMEI-SV;" +
+                       " null if unavailable")
+    public String telephonyGetDeviceSoftwareVersion() {
+        return mTelephonyManager.getDeviceSoftwareVersion();
+    }
+
+    @Rpc(description = "Returns phone # string \"line 1\", such as MSISDN " +
+                       "for default subscription ID; null if unavailable")
+    public String telephonyGetLine1Number() {
+        return mTelephonyManager.getLine1Number();
+    }
+
+    @Rpc(description = "Returns phone # string \"line 1\", such as MSISDN " +
+                       "for specified subscription ID; null if unavailable")
+    public String telephonyGetLine1NumberForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getLine1Number(subId);
+    }
+
+    @Rpc(description = "Returns the Alpha Tag for the default subscription " +
+                       "ID; null if unavailable")
+    public String telephonyGetLine1AlphaTag() {
+        return mTelephonyManager.getLine1AlphaTag();
+    }
+
+    @Rpc(description = "Returns the Alpha Tag for the specified subscription " +
+                       "ID; null if unavailable")
+    public String telephonyGetLine1AlphaTagForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getLine1AlphaTag(subId);
+    }
+
+    @Rpc(description = "Set the Line1-number (phone number) and Alpha Tag" +
+                       "for the default subscription")
+    public Boolean telephonySetLine1Number(
+                @RpcParameter(name = "number") String number,
+                @RpcOptional
+                @RpcParameter(name = "alphaTag") String alphaTag) {
+        return mTelephonyManager.setLine1NumberForDisplay(alphaTag, number);
+    }
+
+    @Rpc(description = "Set the Line1-number (phone number) and Alpha Tag" +
+                       "for the specified subscription")
+    public Boolean telephonySetLine1NumberForSubscription(
+                @RpcParameter(name = "subId") Integer subId,
+                @RpcParameter(name = "number") String number,
+                @RpcOptional
+                @RpcParameter(name = "alphaTag") String alphaTag) {
+        return mTelephonyManager.setLine1NumberForDisplay(subId, alphaTag, number);
+    }
+
+    @Rpc(description = "Returns the neighboring cell information of the device.")
+    public List<NeighboringCellInfo> telephonyGetNeighboringCellInfo() {
+        return mTelephonyManager.getNeighboringCellInfo();
+    }
+
+    @Rpc(description =  "Sets the minimum reporting interval for CellInfo" +
+                        "0-as quickly as possible, 0x7FFFFFF-off")
+    public void telephonySetCellInfoListRate(
+                @RpcParameter(name = "rate") Integer rate
+            ) {
+        mTelephonyManager.setCellInfoListRate(rate);
+    }
+
+    @Rpc(description = "Returns all observed cell information from all radios"+
+                       "on the device including the primary and neighboring cells")
+    public List<CellInfo> telephonyGetAllCellInfo() {
+        return mTelephonyManager.getAllCellInfo();
+    }
+
+    @Rpc(description = "Returns True if cellular data is enabled for" +
+                       "default data subscription ID.")
+    public Boolean telephonyIsDataEnabled() {
+        return telephonyIsDataEnabledForSubscription(
+                   SubscriptionManager.getDefaultDataSubscriptionId());
+    }
+
+    @Rpc(description = "Returns True if data connection is enabled.")
+    public Boolean telephonyIsDataEnabledForSubscription(
+                   @RpcParameter(name = "subId") Integer subId) {
+        return mTelephonyManager.getDataEnabled(subId);
+    }
+
+    @Rpc(description = "Toggles data connection on /off for" +
+                       " default data subscription ID.")
+    public void telephonyToggleDataConnection(
+                @RpcParameter(name = "enabled")
+                @RpcOptional Boolean enabled) {
+        telephonyToggleDataConnectionForSubscription(
+                         SubscriptionManager.getDefaultDataSubscriptionId(), enabled);
+    }
+
+    @Rpc(description = "Toggles data connection on/off for" +
+                       " specified subscription ID")
+    public void telephonyToggleDataConnectionForSubscription(
+                @RpcParameter(name = "subId") Integer subId,
+                @RpcParameter(name = "enabled")
+                @RpcOptional Boolean enabled) {
+        if (enabled == null) {
+            enabled = !telephonyIsDataEnabledForSubscription(subId);
+        }
+        mTelephonyManager.setDataEnabled(subId, enabled);
+    }
+
+    @Rpc(description = "Sets an APN and make that as preferred APN.")
+    public void telephonySetAPN(@RpcParameter(name = "name") final String name,
+                       @RpcParameter(name = "apn") final String apn,
+                       @RpcParameter(name = "type") @RpcOptional @RpcDefault("")
+                       final String type,
+                       @RpcParameter(name = "subId") @RpcOptional Integer subId) {
+        //TODO: b/26273471 Need to find out how to set APN for specific subId
+        Uri uri;
+        Cursor cursor;
+
+        String mcc = "";
+        String mnc = "";
+
+        String numeric = SystemProperties.get(TelephonyProperties.PROPERTY_ICC_OPERATOR_NUMERIC);
+        // MCC is first 3 chars and then in 2 - 3 chars of MNC
+        if (numeric != null && numeric.length() > 4) {
+            // Country code
+            mcc = numeric.substring(0, 3);
+            // Network code
+            mnc = numeric.substring(3);
+        }
+
+        uri = mService.getContentResolver().insert(
+                Telephony.Carriers.CONTENT_URI, new ContentValues());
+        if (uri == null) {
+            Log.w("Failed to insert new provider into " + Telephony.Carriers.CONTENT_URI);
+            return;
+        }
+
+        cursor = mService.getContentResolver().query(uri, sProjection, null, null, null);
+        cursor.moveToFirst();
+
+        ContentValues values = new ContentValues();
+
+        values.put(Telephony.Carriers.NAME, name);
+        values.put(Telephony.Carriers.APN, apn);
+        values.put(Telephony.Carriers.PROXY, "");
+        values.put(Telephony.Carriers.PORT, "");
+        values.put(Telephony.Carriers.MMSPROXY, "");
+        values.put(Telephony.Carriers.MMSPORT, "");
+        values.put(Telephony.Carriers.USER, "");
+        values.put(Telephony.Carriers.SERVER, "");
+        values.put(Telephony.Carriers.PASSWORD, "");
+        values.put(Telephony.Carriers.MMSC, "");
+        values.put(Telephony.Carriers.TYPE, type);
+        values.put(Telephony.Carriers.MCC, mcc);
+        values.put(Telephony.Carriers.MNC, mnc);
+        values.put(Telephony.Carriers.NUMERIC, mcc + mnc);
+
+        int ret = mService.getContentResolver().update(uri, values, null, null);
+        Log.d("after update " + ret);
+        cursor.close();
+
+        // Make this APN as the preferred
+        String where = "name=\"" + name + "\"";
+
+        Cursor c = mService.getContentResolver().query(
+                Telephony.Carriers.CONTENT_URI,
+                new String[] {
+                        "_id", "name", "apn", "type"
+                }, where, null,
+                Telephony.Carriers.DEFAULT_SORT_ORDER);
+        if (c != null) {
+            c.moveToFirst();
+            String key = c.getString(0);
+            final String PREFERRED_APN_URI = "content://telephony/carriers/preferapn";
+            ContentResolver resolver = mService.getContentResolver();
+            ContentValues prefAPN = new ContentValues();
+            prefAPN.put("apn_id", key);
+            resolver.update(Uri.parse(PREFERRED_APN_URI), prefAPN, null, null);
+        }
+        c.close();
+    }
+
+    @Rpc(description = "Returns the number of APNs defined")
+    public int telephonyGetNumberOfAPNs(
+               @RpcParameter(name = "subId")
+               @RpcOptional Integer subId) {
+        //TODO: b/26273471 Need to find out how to get Number of APNs for specific subId
+        int result = 0;
+        String where = "numeric=\"" + android.os.SystemProperties.get(
+                        TelephonyProperties.PROPERTY_ICC_OPERATOR_NUMERIC, "") + "\"";
+
+        Cursor cursor = mService.getContentResolver().query(
+                Telephony.Carriers.CONTENT_URI,
+                new String[] {"_id", "name", "apn", "type"}, where, null,
+                Telephony.Carriers.DEFAULT_SORT_ORDER);
+
+        if (cursor != null) {
+            result = cursor.getCount();
+        }
+        cursor.close();
+        return result;
+    }
+
+    @Rpc(description = "Returns the currently selected APN name")
+    public String telephonyGetSelectedAPN(
+                  @RpcParameter(name = "subId")
+                  @RpcOptional Integer subId) {
+        //TODO: b/26273471 Need to find out how to get selected APN for specific subId
+        String key = null;
+        int ID_INDEX = 0;
+        final String PREFERRED_APN_URI = "content://telephony/carriers/preferapn";
+
+        Cursor cursor = mService.getContentResolver().query(Uri.parse(PREFERRED_APN_URI),
+                new String[] {"name"}, null, null, Telephony.Carriers.DEFAULT_SORT_ORDER);
+
+        if (cursor.getCount() > 0) {
+            cursor.moveToFirst();
+            key = cursor.getString(ID_INDEX);
+        }
+        cursor.close();
+        return key;
+    }
+
+    @Rpc(description = "Returns the current data connection state")
+    public String telephonyGetDataConnectionState() {
+        return TelephonyUtils.getDataConnectionStateString(
+            mTelephonyManager.getDataState());
+    }
+
+    @Rpc(description = "Enables or Disables Video Calling()")
+    public void telephonyEnableVideoCalling(boolean enable) {
+        mTelephonyManager.enableVideoCalling(enable);
+    }
+
+    @Rpc(description = "Returns a boolean of whether or not " +
+            "video calling setting is enabled by the user")
+    public Boolean telephonyIsVideoCallingEnabled() {
+        return mTelephonyManager.isVideoCallingEnabled();
+    }
+
+    @Rpc(description = "Returns a boolean of whether video calling is available for use")
+    public Boolean telephonyIsVideoCallingAvailable() {
+        return mTelephonyManager.isVideoTelephonyAvailable();
+    }
+
+    @Rpc(description = "Returns a boolean of whether or not the device is ims registered")
+    public Boolean telephonyIsImsRegistered() {
+        return mTelephonyManager.isImsRegistered();
+    }
+
+    @Rpc(description = "Returns a boolean of whether or not volte calling is available for use")
+    public Boolean telephonyIsVolteAvailable() {
+        return mTelephonyManager.isVolteAvailable();
+    }
+
+    @Rpc(description = "Returns a boolean of whether or not wifi calling is available for use")
+    public Boolean telephonyIsWifiCallingAvailable() {
+        return mTelephonyManager.isWifiCallingAvailable();
+    }
+
+    @Rpc(description = "Returns the service state for default subscription ID")
+    public String telephonyGetServiceState() {
+        //TODO: b/26273807 need to have framework API to get service state.
+        return telephonyGetServiceStateForSubscription(
+                                 SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the service state for specified subscription ID")
+    public String telephonyGetServiceStateForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        //TODO: b/26273807 need to have framework API to get service state.
+        return null;
+    }
+
+    @Rpc(description = "Returns the call state for default subscription ID")
+    public String telephonyGetCallState() {
+        return telephonyGetCallStateForSubscription(
+                               SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns the call state for specified subscription ID")
+    public String telephonyGetCallStateForSubscription(
+                  @RpcParameter(name = "subId") Integer subId) {
+        return TelephonyUtils.getTelephonyCallStateString(
+            mTelephonyManager.getCallState(subId));
+    }
+
+    @Rpc(description = "Returns current signal strength for default subscription ID.")
+    public SignalStrength telephonyGetSignalStrength() {
+        return telephonyGetSignalStrengthForSubscription(
+                               SubscriptionManager.getDefaultSubscriptionId());
+    }
+
+    @Rpc(description = "Returns current signal strength for specified subscription ID.")
+    public SignalStrength telephonyGetSignalStrengthForSubscription(
+                    @RpcParameter(name = "subId") Integer subId) {
+        return StateChangeListeners.get(subId).mSignalStrengthChangeListener.mSignalStrengths;
+    }
+
+    @Rpc(description = "Returns the sim count.")
+    public int telephonyGetSimCount() {
+        return mTelephonyManager.getSimCount();
+    }
+
+    private static class StateChangeListener {
+        private ServiceStateChangeListener mServiceStateChangeListener;
+        private SignalStrengthChangeListener mSignalStrengthChangeListener;
+        private CallStateChangeListener mCallStateChangeListener;
+        private CellInfoChangeListener mCellInfoChangeListener;
+        private DataConnectionStateChangeListener
+                           mDataConnectionStateChangeListener;
+        private DataConnectionRealTimeInfoChangeListener
+                           mDataConnectionRTInfoChangeListener;
+        private VoiceMailStateChangeListener
+                           mVoiceMailStateChangeListener;
+    }
+
+    @Override
+    public void shutdown() {
+        for (int i = 0; i < mSubInfos.size(); i++) {
+           int subId = mSubInfos.get(i).getSubscriptionId();
+           telephonyStopTrackingCallStateChangeForSubscription(subId);
+           telephonyStopTrackingDataConnectionRTInfoChangeForSubscription(subId);
+           telephonyStopTrackingServiceStateChangeForSubscription(subId);
+           telephonyStopTrackingSignalStrengthChangeForSubscription(subId);
+           telephonyStopTrackingDataConnectionStateChangeForSubscription(subId);
+        }
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyStateListeners.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyStateListeners.java
new file mode 100644
index 0000000..cb9db74
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyStateListeners.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.telephony.TelephonyEvents;
+import android.os.Bundle;
+import android.telephony.CellInfo;
+import android.telephony.DataConnectionRealTimeInfo;
+import android.telephony.PhoneStateListener;
+import android.telephony.PreciseCallState;
+import android.telephony.ServiceState;
+import android.telephony.SignalStrength;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.VoLteServiceState;
+
+import java.util.List;
+
+/**
+ * Store all subclasses of PhoneStateListener here.
+ */
+public class TelephonyStateListeners {
+
+    public static class CallStateChangeListener extends PhoneStateListener {
+
+        private final EventFacade mEventFacade;
+        public static final int sListeningStates = PhoneStateListener.LISTEN_CALL_STATE |
+                                                   PhoneStateListener.LISTEN_PRECISE_CALL_STATE;
+
+        public boolean listenForeground = true;
+        public boolean listenRinging = false;
+        public boolean listenBackground = false;
+        public int subscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+        public CallStateChangeListener(EventFacade ef) {
+            super();
+            mEventFacade = ef;
+            subscriptionId = SubscriptionManager.getDefaultVoiceSubscriptionId();
+        }
+
+        public CallStateChangeListener(EventFacade ef, int subId) {
+            super(subId);
+            mEventFacade = ef;
+            subscriptionId = subId;
+        }
+
+        @Override
+        public void onCallStateChanged(int state, String incomingNumber) {
+            mEventFacade.postEvent(TelephonyConstants.EventCallStateChanged,
+                new TelephonyEvents.CallStateEvent(
+                    state, incomingNumber, subscriptionId));
+        }
+
+        @Override
+        public void onPreciseCallStateChanged(PreciseCallState callState) {
+            int foregroundState = callState.getForegroundCallState();
+            int ringingState = callState.getRingingCallState();
+            int backgroundState = callState.getBackgroundCallState();
+            if (listenForeground &&
+                foregroundState != PreciseCallState.PRECISE_CALL_STATE_NOT_VALID) {
+                processCallState(foregroundState,
+                        TelephonyConstants.PRECISE_CALL_STATE_LISTEN_LEVEL_FOREGROUND,
+                        callState);
+            }
+            if (listenRinging &&
+                ringingState != PreciseCallState.PRECISE_CALL_STATE_NOT_VALID) {
+                processCallState(ringingState,
+                        TelephonyConstants.PRECISE_CALL_STATE_LISTEN_LEVEL_RINGING,
+                        callState);
+            }
+            if (listenBackground &&
+                backgroundState != PreciseCallState.PRECISE_CALL_STATE_NOT_VALID) {
+                processCallState(backgroundState,
+                        TelephonyConstants.PRECISE_CALL_STATE_LISTEN_LEVEL_BACKGROUND,
+                        callState);
+            }
+        }
+
+        private void processCallState(
+            int newState, String which, PreciseCallState callState) {
+            mEventFacade.postEvent(TelephonyConstants.EventPreciseStateChanged,
+                new TelephonyEvents.PreciseCallStateEvent(
+                    newState, which, callState, subscriptionId));
+        }
+    }
+
+    public static class DataConnectionRealTimeInfoChangeListener extends PhoneStateListener {
+
+        private final EventFacade mEventFacade;
+        public static final int sListeningStates =
+                PhoneStateListener.LISTEN_DATA_CONNECTION_REAL_TIME_INFO;
+        public int subscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+        public DataConnectionRealTimeInfoChangeListener(EventFacade ef) {
+            super();
+            mEventFacade = ef;
+            subscriptionId = SubscriptionManager.getDefaultDataSubscriptionId();
+        }
+
+        public DataConnectionRealTimeInfoChangeListener(EventFacade ef, int subId) {
+            super(subId);
+            mEventFacade = ef;
+            subscriptionId = subId;
+        }
+
+        @Override
+        public void onDataConnectionRealTimeInfoChanged(
+            DataConnectionRealTimeInfo dcRtInfo) {
+            mEventFacade.postEvent(
+                TelephonyConstants.EventDataConnectionRealTimeInfoChanged,
+                new TelephonyEvents.DataConnectionRealTimeInfoEvent(
+                    dcRtInfo, subscriptionId));
+        }
+    }
+
+    public static class DataConnectionStateChangeListener extends PhoneStateListener {
+
+        private final EventFacade mEventFacade;
+        private final TelephonyManager mTelephonyManager;
+        public static final int sListeningStates =
+                PhoneStateListener.LISTEN_DATA_CONNECTION_STATE;
+        public int subscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+        public DataConnectionStateChangeListener(EventFacade ef, TelephonyManager tm) {
+            super();
+            mEventFacade = ef;
+            mTelephonyManager = tm;
+            subscriptionId = SubscriptionManager.getDefaultDataSubscriptionId();
+        }
+
+        public DataConnectionStateChangeListener(EventFacade ef, TelephonyManager tm, int subId) {
+            super(subId);
+            mEventFacade = ef;
+            mTelephonyManager = tm;
+            subscriptionId = subId;
+        }
+
+        @Override
+        public void onDataConnectionStateChanged(int state) {
+            mEventFacade.postEvent(
+                TelephonyConstants.EventDataConnectionStateChanged,
+                new TelephonyEvents.DataConnectionStateEvent(state,
+                    TelephonyUtils.getNetworkTypeString(
+                                 mTelephonyManager.getDataNetworkType()),
+                    subscriptionId));
+        }
+    }
+
+    public static class ServiceStateChangeListener extends PhoneStateListener {
+
+        private final EventFacade mEventFacade;
+        public static final int sListeningStates = PhoneStateListener.LISTEN_SERVICE_STATE;
+        public int subscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+        public ServiceStateChangeListener(EventFacade ef) {
+            super();
+            mEventFacade = ef;
+            subscriptionId = SubscriptionManager.getDefaultDataSubscriptionId();
+        }
+
+        public ServiceStateChangeListener(EventFacade ef, int subId) {
+            super(subId);
+            mEventFacade = ef;
+            subscriptionId = subId;
+        }
+
+        @Override
+        public void onServiceStateChanged(ServiceState serviceState) {
+            mEventFacade.postEvent(TelephonyConstants.EventServiceStateChanged,
+                new TelephonyEvents.ServiceStateEvent(
+                    serviceState, subscriptionId));
+        }
+
+    }
+
+    public static class CellInfoChangeListener
+            extends PhoneStateListener {
+
+        private final EventFacade mEventFacade;
+
+        public CellInfoChangeListener(EventFacade ef) {
+            super();
+            mEventFacade = ef;
+        }
+
+        public CellInfoChangeListener(EventFacade ef, int subId) {
+            super(subId);
+            mEventFacade = ef;
+        }
+
+        @Override
+        public void onCellInfoChanged(List<CellInfo> cellInfo) {
+            mEventFacade.postEvent(
+                TelephonyConstants.EventCellInfoChanged, cellInfo);
+        }
+    }
+
+    public static class VolteServiceStateChangeListener
+            extends PhoneStateListener {
+
+        private final EventFacade mEventFacade;
+
+        public VolteServiceStateChangeListener(EventFacade ef) {
+            super();
+            mEventFacade = ef;
+        }
+
+        public VolteServiceStateChangeListener(EventFacade ef, int subId) {
+            super(subId);
+            mEventFacade = ef;
+        }
+
+        @Override
+        public void onVoLteServiceStateChanged(VoLteServiceState volteInfo) {
+            mEventFacade.postEvent(
+                    TelephonyConstants.EventVolteServiceStateChanged,
+                    volteInfo);
+        }
+    }
+
+    public static class VoiceMailStateChangeListener extends PhoneStateListener {
+
+        private final EventFacade mEventFacade;
+
+        public static final int sListeningStates =
+                PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR;
+
+        public VoiceMailStateChangeListener(EventFacade ef) {
+            super();
+            mEventFacade = ef;
+        }
+
+        public VoiceMailStateChangeListener(EventFacade ef, int subId) {
+            super(subId);
+            mEventFacade = ef;
+        }
+
+        @Override
+        public void onMessageWaitingIndicatorChanged(boolean messageWaitingIndicator) {
+            mEventFacade.postEvent(
+                    TelephonyConstants.EventMessageWaitingIndicatorChanged,
+                    new TelephonyEvents.MessageWaitingIndicatorEvent(
+                        messageWaitingIndicator));
+        }
+    }
+
+
+    public static class SignalStrengthChangeListener extends PhoneStateListener {
+
+        private final EventFacade mEventFacade;
+        public SignalStrength mSignalStrengths;
+        public static final int sListeningStates = PhoneStateListener.LISTEN_SIGNAL_STRENGTHS;
+        public SignalStrengthChangeListener(EventFacade ef) {
+            super();
+            mEventFacade = ef;
+        }
+
+        public SignalStrengthChangeListener(EventFacade ef, int subId) {
+            super(subId);
+            mEventFacade = ef;
+        }
+
+        @Override
+        public void onSignalStrengthsChanged(SignalStrength signalStrength) {
+            mSignalStrengths = signalStrength;
+            mEventFacade.postEvent(
+                TelephonyConstants.EventSignalStrengthChanged, signalStrength);
+        }
+    }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyUtils.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyUtils.java
new file mode 100644
index 0000000..09e161f
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyUtils.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.telephony;
+import com.android.ims.ImsConfig;
+import com.android.internal.telephony.RILConstants;
+import com.googlecode.android_scripting.Log;
+import android.telecom.TelecomManager;
+import android.telephony.DataConnectionRealTimeInfo;
+import android.telephony.PreciseCallState;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.telephony.VoLteServiceState;
+
+/**
+ * Telephony utility functions
+ */
+public class TelephonyUtils {
+
+    public static String getWfcModeString(int mode) {
+       switch(mode) {
+           case ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED:
+               return TelephonyConstants.WFC_MODE_WIFI_PREFERRED;
+           case ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED:
+               return TelephonyConstants.WFC_MODE_CELLULAR_PREFERRED;
+           case ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY:
+               return TelephonyConstants.WFC_MODE_WIFI_ONLY;
+       }
+        Log.d("getWfcModeStringfromInt error. int: " + mode);
+        return TelephonyConstants.WFC_MODE_UNKNOWN;
+    }
+
+    public static String getTtyModeString(int mode) {
+        switch (mode) {
+            case TelecomManager.TTY_MODE_FULL:
+                return TelephonyConstants.TTY_MODE_FULL;
+            case TelecomManager.TTY_MODE_HCO:
+                return TelephonyConstants.TTY_MODE_HCO;
+            case TelecomManager.TTY_MODE_OFF:
+                return TelephonyConstants.TTY_MODE_OFF;
+            case TelecomManager.TTY_MODE_VCO:
+                return TelephonyConstants.TTY_MODE_VCO;
+        }
+        Log.d("getTtyModeString error. int: " + mode);
+        return null;
+    }
+
+    public static String getPhoneTypeString(int type) {
+        switch (type) {
+            case TelephonyManager.PHONE_TYPE_GSM:
+                return TelephonyConstants.PHONE_TYPE_GSM;
+            case TelephonyManager.PHONE_TYPE_NONE:
+                return TelephonyConstants.PHONE_TYPE_NONE;
+            case TelephonyManager.PHONE_TYPE_CDMA:
+                return TelephonyConstants.PHONE_TYPE_CDMA;
+            case TelephonyManager.PHONE_TYPE_SIP:
+                return TelephonyConstants.PHONE_TYPE_SIP;
+        }
+        Log.d("getPhoneTypeString error. int: " + type);
+        return null;
+    }
+
+    public static String getSimStateString(int state) {
+        switch (state) {
+            case TelephonyManager.SIM_STATE_UNKNOWN:
+                return TelephonyConstants.SIM_STATE_UNKNOWN;
+            case TelephonyManager.SIM_STATE_ABSENT:
+                return TelephonyConstants.SIM_STATE_ABSENT;
+            case TelephonyManager.SIM_STATE_PIN_REQUIRED:
+                return TelephonyConstants.SIM_STATE_PIN_REQUIRED;
+            case TelephonyManager.SIM_STATE_PUK_REQUIRED:
+                return TelephonyConstants.SIM_STATE_PUK_REQUIRED;
+            case TelephonyManager.SIM_STATE_NETWORK_LOCKED:
+                return TelephonyConstants.SIM_STATE_NETWORK_LOCKED;
+            case TelephonyManager.SIM_STATE_READY:
+                return TelephonyConstants.SIM_STATE_READY;
+            case TelephonyManager.SIM_STATE_NOT_READY:
+                return TelephonyConstants.SIM_STATE_NOT_READY;
+            case TelephonyManager.SIM_STATE_PERM_DISABLED:
+                return TelephonyConstants.SIM_STATE_PERM_DISABLED;
+            case TelephonyManager.SIM_STATE_CARD_IO_ERROR:
+                return TelephonyConstants.SIM_STATE_CARD_IO_ERROR;
+        }
+        Log.d("getSimStateString error. int: " + state);
+        return TelephonyConstants.SIM_STATE_UNKNOWN;
+    }
+
+    public static String formatIncomingNumber(String incomingNumber) {
+        String mIncomingNumber = null;
+        int len = 0;
+        if (incomingNumber != null) {
+            len = incomingNumber.length();
+        }
+        if (len > 0) {
+            /**
+             * Currently this incomingNumber modification is specific for
+             * US numbers.
+             */
+            if ((12 == len) && ('+' == incomingNumber.charAt(0))) {
+                mIncomingNumber = incomingNumber.substring(1);
+            } else if (10 == len) {
+                mIncomingNumber = '1' + incomingNumber;
+            } else {
+                mIncomingNumber = incomingNumber;
+            }
+        }
+        return mIncomingNumber;
+    }
+
+    public static String getTelephonyCallStateString(int callState) {
+        switch (callState) {
+            case TelephonyManager.CALL_STATE_IDLE:
+                return TelephonyConstants.TELEPHONY_STATE_IDLE;
+            case TelephonyManager.CALL_STATE_OFFHOOK:
+                return TelephonyConstants.TELEPHONY_STATE_OFFHOOK;
+            case TelephonyManager.CALL_STATE_RINGING:
+                return TelephonyConstants.TELEPHONY_STATE_RINGING;
+        }
+        Log.d("getTelephonyCallStateString error. int: " + callState);
+        return TelephonyConstants.TELEPHONY_STATE_UNKNOWN;
+    }
+
+    public static String getPreciseCallStateString(int state) {
+        switch (state) {
+            case PreciseCallState.PRECISE_CALL_STATE_ACTIVE:
+                return TelephonyConstants.PRECISE_CALL_STATE_ACTIVE;
+            case PreciseCallState.PRECISE_CALL_STATE_HOLDING:
+                return TelephonyConstants.PRECISE_CALL_STATE_HOLDING;
+            case PreciseCallState.PRECISE_CALL_STATE_DIALING:
+                return TelephonyConstants.PRECISE_CALL_STATE_DIALING;
+            case PreciseCallState.PRECISE_CALL_STATE_ALERTING:
+                return TelephonyConstants.PRECISE_CALL_STATE_ALERTING;
+            case PreciseCallState.PRECISE_CALL_STATE_INCOMING:
+                return TelephonyConstants.PRECISE_CALL_STATE_INCOMING;
+            case PreciseCallState.PRECISE_CALL_STATE_WAITING:
+                return TelephonyConstants.PRECISE_CALL_STATE_WAITING;
+            case PreciseCallState.PRECISE_CALL_STATE_DISCONNECTED:
+                return TelephonyConstants.PRECISE_CALL_STATE_DISCONNECTED;
+            case PreciseCallState.PRECISE_CALL_STATE_DISCONNECTING:
+                return TelephonyConstants.PRECISE_CALL_STATE_DISCONNECTING;
+            case PreciseCallState.PRECISE_CALL_STATE_IDLE:
+                return TelephonyConstants.PRECISE_CALL_STATE_IDLE;
+            case PreciseCallState.PRECISE_CALL_STATE_NOT_VALID:
+                return TelephonyConstants.PRECISE_CALL_STATE_INVALID;
+        }
+        Log.d("getPreciseCallStateString error. int: " + state);
+        return TelephonyConstants.PRECISE_CALL_STATE_UNKNOWN;
+    }
+
+    public static String getDcPowerStateString(int state) {
+        if (state == DataConnectionRealTimeInfo.DC_POWER_STATE_LOW) {
+            return TelephonyConstants.DC_POWER_STATE_LOW;
+        } else if (state == DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH) {
+            return TelephonyConstants.DC_POWER_STATE_HIGH;
+        } else if (state == DataConnectionRealTimeInfo.DC_POWER_STATE_MEDIUM) {
+            return TelephonyConstants.DC_POWER_STATE_MEDIUM;
+        } else {
+            return TelephonyConstants.DC_POWER_STATE_UNKNOWN;
+        }
+    }
+
+    public static String getDataConnectionStateString(int state) {
+        switch (state) {
+            case TelephonyManager.DATA_DISCONNECTED:
+                return TelephonyConstants.DATA_STATE_DISCONNECTED;
+            case TelephonyManager.DATA_CONNECTING:
+                return TelephonyConstants.DATA_STATE_CONNECTING;
+            case TelephonyManager.DATA_CONNECTED:
+                return TelephonyConstants.DATA_STATE_CONNECTED;
+            case TelephonyManager.DATA_SUSPENDED:
+                return TelephonyConstants.DATA_STATE_SUSPENDED;
+            case TelephonyManager.DATA_UNKNOWN:
+                return TelephonyConstants.DATA_STATE_UNKNOWN;
+        }
+        Log.d("getDataConnectionStateString error. int: " + state);
+        return TelephonyConstants.DATA_STATE_UNKNOWN;
+    }
+
+    public static int getNetworkModeIntfromString(String networkMode) {
+        switch (networkMode) {
+            case TelephonyConstants.NETWORK_MODE_WCDMA_PREF:
+                return RILConstants.NETWORK_MODE_WCDMA_PREF;
+            case TelephonyConstants.NETWORK_MODE_GSM_ONLY:
+                return RILConstants.NETWORK_MODE_GSM_ONLY;
+            case TelephonyConstants.NETWORK_MODE_WCDMA_ONLY:
+                return RILConstants.NETWORK_MODE_WCDMA_ONLY;
+            case TelephonyConstants.NETWORK_MODE_GSM_UMTS:
+                return RILConstants.NETWORK_MODE_GSM_UMTS;
+            case TelephonyConstants.NETWORK_MODE_CDMA:
+                return RILConstants.NETWORK_MODE_CDMA;
+            case TelephonyConstants.NETWORK_MODE_CDMA_NO_EVDO:
+                return RILConstants.NETWORK_MODE_CDMA_NO_EVDO;
+            case TelephonyConstants.NETWORK_MODE_EVDO_NO_CDMA:
+                return RILConstants.NETWORK_MODE_EVDO_NO_CDMA;
+            case TelephonyConstants.NETWORK_MODE_GLOBAL:
+                return RILConstants.NETWORK_MODE_GLOBAL;
+            case TelephonyConstants.NETWORK_MODE_LTE_CDMA_EVDO:
+                return RILConstants.NETWORK_MODE_LTE_CDMA_EVDO;
+            case TelephonyConstants.NETWORK_MODE_LTE_GSM_WCDMA:
+                return RILConstants.NETWORK_MODE_LTE_GSM_WCDMA;
+            case TelephonyConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA:
+                return RILConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA;
+            case TelephonyConstants.NETWORK_MODE_LTE_ONLY:
+                return RILConstants.NETWORK_MODE_LTE_ONLY;
+            case TelephonyConstants.NETWORK_MODE_LTE_WCDMA:
+                return RILConstants.NETWORK_MODE_LTE_WCDMA;
+            case TelephonyConstants.NETWORK_MODE_TDSCDMA_ONLY:
+                return RILConstants.NETWORK_MODE_TDSCDMA_ONLY;
+            case TelephonyConstants.NETWORK_MODE_TDSCDMA_WCDMA:
+                return RILConstants.NETWORK_MODE_TDSCDMA_WCDMA;
+            case TelephonyConstants.NETWORK_MODE_LTE_TDSCDMA:
+                return RILConstants.NETWORK_MODE_LTE_TDSCDMA;
+            case TelephonyConstants.NETWORK_MODE_TDSCDMA_GSM:
+                return RILConstants.NETWORK_MODE_TDSCDMA_GSM;
+            case TelephonyConstants.NETWORK_MODE_LTE_TDSCDMA_GSM:
+                return RILConstants.NETWORK_MODE_LTE_TDSCDMA_GSM;
+            case TelephonyConstants.NETWORK_MODE_TDSCDMA_GSM_WCDMA:
+                return RILConstants.NETWORK_MODE_TDSCDMA_GSM_WCDMA;
+            case TelephonyConstants.NETWORK_MODE_LTE_TDSCDMA_WCDMA:
+                return RILConstants.NETWORK_MODE_LTE_TDSCDMA_WCDMA;
+            case TelephonyConstants.NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA:
+                return RILConstants.NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA;
+            case TelephonyConstants.NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA:
+                return RILConstants.NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA;
+            case TelephonyConstants.NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA:
+                return RILConstants.NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA;
+        }
+        Log.d("getNetworkModeIntfromString error. String: " + networkMode);
+        return RILConstants.RIL_ERRNO_INVALID_RESPONSE;
+    }
+
+    public static String getNetworkModeStringfromInt(int networkMode) {
+        switch (networkMode) {
+            case RILConstants.NETWORK_MODE_WCDMA_PREF:
+                return TelephonyConstants.NETWORK_MODE_WCDMA_PREF;
+            case RILConstants.NETWORK_MODE_GSM_ONLY:
+                return TelephonyConstants.NETWORK_MODE_GSM_ONLY;
+            case RILConstants.NETWORK_MODE_WCDMA_ONLY:
+                return TelephonyConstants.NETWORK_MODE_WCDMA_ONLY;
+            case RILConstants.NETWORK_MODE_GSM_UMTS:
+                return TelephonyConstants.NETWORK_MODE_GSM_UMTS;
+            case RILConstants.NETWORK_MODE_CDMA:
+                return TelephonyConstants.NETWORK_MODE_CDMA;
+            case RILConstants.NETWORK_MODE_CDMA_NO_EVDO:
+                return TelephonyConstants.NETWORK_MODE_CDMA_NO_EVDO;
+            case RILConstants.NETWORK_MODE_EVDO_NO_CDMA:
+                return TelephonyConstants.NETWORK_MODE_EVDO_NO_CDMA;
+            case RILConstants.NETWORK_MODE_GLOBAL:
+                return TelephonyConstants.NETWORK_MODE_GLOBAL;
+            case RILConstants.NETWORK_MODE_LTE_CDMA_EVDO:
+                return TelephonyConstants.NETWORK_MODE_LTE_CDMA_EVDO;
+            case RILConstants.NETWORK_MODE_LTE_GSM_WCDMA:
+                return TelephonyConstants.NETWORK_MODE_LTE_GSM_WCDMA;
+            case RILConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA:
+                return TelephonyConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA;
+            case RILConstants.NETWORK_MODE_LTE_ONLY:
+                return TelephonyConstants.NETWORK_MODE_LTE_ONLY;
+            case RILConstants.NETWORK_MODE_LTE_WCDMA:
+                return TelephonyConstants.NETWORK_MODE_LTE_WCDMA;
+            case RILConstants.NETWORK_MODE_TDSCDMA_ONLY:
+                return TelephonyConstants.NETWORK_MODE_TDSCDMA_ONLY;
+            case RILConstants.NETWORK_MODE_TDSCDMA_WCDMA:
+                return TelephonyConstants.NETWORK_MODE_TDSCDMA_WCDMA;
+            case RILConstants.NETWORK_MODE_LTE_TDSCDMA:
+                return TelephonyConstants.NETWORK_MODE_LTE_TDSCDMA;
+            case RILConstants.NETWORK_MODE_TDSCDMA_GSM:
+                return TelephonyConstants.NETWORK_MODE_TDSCDMA_GSM;
+            case RILConstants.NETWORK_MODE_LTE_TDSCDMA_GSM:
+                return TelephonyConstants.NETWORK_MODE_LTE_TDSCDMA_GSM;
+            case RILConstants.NETWORK_MODE_TDSCDMA_GSM_WCDMA:
+                return TelephonyConstants.NETWORK_MODE_TDSCDMA_GSM_WCDMA;
+            case RILConstants.NETWORK_MODE_LTE_TDSCDMA_WCDMA:
+                return TelephonyConstants.NETWORK_MODE_LTE_TDSCDMA_WCDMA;
+            case RILConstants.NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA:
+                return TelephonyConstants.NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA;
+            case RILConstants.NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA:
+                return TelephonyConstants.NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA;
+            case RILConstants.NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA:
+                return TelephonyConstants.NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA;
+        }
+        Log.d("getNetworkModeStringfromInt error. Int: " + networkMode);
+        return TelephonyConstants.NETWORK_MODE_INVALID;
+    }
+
+    public static String getNetworkTypeString(int type) {
+        switch(type) {
+            case TelephonyManager.NETWORK_TYPE_GPRS:
+                return TelephonyConstants.RAT_GPRS;
+            case TelephonyManager.NETWORK_TYPE_EDGE:
+                return TelephonyConstants.RAT_EDGE;
+            case TelephonyManager.NETWORK_TYPE_UMTS:
+                return TelephonyConstants.RAT_UMTS;
+            case TelephonyManager.NETWORK_TYPE_HSDPA:
+                return TelephonyConstants.RAT_HSDPA;
+            case TelephonyManager.NETWORK_TYPE_HSUPA:
+                return TelephonyConstants.RAT_HSUPA;
+            case TelephonyManager.NETWORK_TYPE_HSPA:
+                return TelephonyConstants.RAT_HSPA;
+            case TelephonyManager.NETWORK_TYPE_CDMA:
+                return TelephonyConstants.RAT_CDMA;
+            case TelephonyManager.NETWORK_TYPE_1xRTT:
+                return TelephonyConstants.RAT_1XRTT;
+            case TelephonyManager.NETWORK_TYPE_EVDO_0:
+                return TelephonyConstants.RAT_EVDO_0;
+            case TelephonyManager.NETWORK_TYPE_EVDO_A:
+                return TelephonyConstants.RAT_EVDO_A;
+            case TelephonyManager.NETWORK_TYPE_EVDO_B:
+                return TelephonyConstants.RAT_EVDO_B;
+            case TelephonyManager.NETWORK_TYPE_EHRPD:
+                return TelephonyConstants.RAT_EHRPD;
+            case TelephonyManager.NETWORK_TYPE_LTE:
+                return TelephonyConstants.RAT_LTE;
+            case TelephonyManager.NETWORK_TYPE_HSPAP:
+                return TelephonyConstants.RAT_HSPAP;
+            case TelephonyManager.NETWORK_TYPE_GSM:
+                return TelephonyConstants.RAT_GSM;
+            case TelephonyManager. NETWORK_TYPE_TD_SCDMA:
+                return TelephonyConstants.RAT_TD_SCDMA;
+            case TelephonyManager.NETWORK_TYPE_IWLAN:
+                return TelephonyConstants.RAT_IWLAN;
+            case TelephonyManager.NETWORK_TYPE_IDEN:
+                return TelephonyConstants.RAT_IDEN;
+        }
+        return TelephonyConstants.RAT_UNKNOWN;
+    }
+
+    public static String getNetworkStateString(int state) {
+        switch(state) {
+            case ServiceState.STATE_EMERGENCY_ONLY:
+                return TelephonyConstants.SERVICE_STATE_EMERGENCY_ONLY;
+            case ServiceState.STATE_IN_SERVICE:
+                return TelephonyConstants.SERVICE_STATE_IN_SERVICE;
+            case ServiceState.STATE_OUT_OF_SERVICE:
+                return TelephonyConstants.SERVICE_STATE_OUT_OF_SERVICE;
+            case ServiceState.STATE_POWER_OFF:
+                return TelephonyConstants.SERVICE_STATE_POWER_OFF;
+            default:
+                return TelephonyConstants.SERVICE_STATE_UNKNOWN;
+        }
+   }
+
+    public static String getSrvccStateString(int srvccState) {
+        switch (srvccState) {
+            case VoLteServiceState.HANDOVER_STARTED:
+                return TelephonyConstants.VOLTE_SERVICE_STATE_HANDOVER_STARTED;
+            case VoLteServiceState.HANDOVER_COMPLETED:
+                return TelephonyConstants.VOLTE_SERVICE_STATE_HANDOVER_COMPLETED;
+            case VoLteServiceState.HANDOVER_FAILED:
+                return TelephonyConstants.VOLTE_SERVICE_STATE_HANDOVER_FAILED;
+            case VoLteServiceState.HANDOVER_CANCELED:
+                return TelephonyConstants.VOLTE_SERVICE_STATE_HANDOVER_CANCELED;
+            default:
+                Log.e(String.format("getSrvccStateString():"
+                        + "unknown state %d", srvccState));
+                return TelephonyConstants.VOLTE_SERVICE_STATE_HANDOVER_UNKNOWN;
+        }
+    };
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ui/AlertDialogTask.java b/Common/src/com/googlecode/android_scripting/facade/ui/AlertDialogTask.java
new file mode 100644
index 0000000..2a1e48e
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ui/AlertDialogTask.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.content.DialogInterface;
+import android.text.method.PasswordTransformationMethod;
+import android.widget.EditText;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+/**
+ * Wrapper class for alert dialog running in separate thread.
+ *
+ * @author MeanEYE.rcf (meaneye.rcf@gmail.com)
+ */
+class AlertDialogTask extends DialogTask {
+
+  private final String mTitle;
+  private final String mMessage;
+
+  private final List<String> mItems;
+  private final Set<Integer> mSelectedItems;
+  private final Map<String, Object> mResultMap;
+  private InputType mInputType;
+  private int mEditInputType = 0;
+
+  private String mPositiveButtonText;
+  private String mNegativeButtonText;
+  private String mNeutralButtonText;
+
+  private EditText mEditText;
+  private String mDefaultText;
+
+  private enum InputType {
+    DEFAULT, MENU, SINGLE_CHOICE, MULTI_CHOICE, PLAIN_TEXT, PASSWORD;
+  }
+
+  public AlertDialogTask(String title, String message) {
+    mTitle = title;
+    mMessage = message;
+    mInputType = InputType.DEFAULT;
+    mItems = new ArrayList<String>();
+    mSelectedItems = new TreeSet<Integer>();
+    mResultMap = new HashMap<String, Object>();
+  }
+
+  public void setPositiveButtonText(String text) {
+    mPositiveButtonText = text;
+  }
+
+  public void setNegativeButtonText(String text) {
+    mNegativeButtonText = text;
+  }
+
+  public void setNeutralButtonText(String text) {
+    mNeutralButtonText = text;
+  }
+
+  /**
+   * Set list items.
+   *
+   * @param items
+   */
+  public void setItems(JSONArray items) {
+    mItems.clear();
+    for (int i = 0; i < items.length(); i++) {
+      try {
+        mItems.add(items.getString(i));
+      } catch (JSONException e) {
+        throw new RuntimeException(e);
+      }
+    }
+    mInputType = InputType.MENU;
+  }
+
+  /**
+   * Set single choice items.
+   *
+   * @param items
+   *          a list of items as {@link String}s to display
+   * @param selected
+   *          the index of the item that is selected by default
+   */
+  public void setSingleChoiceItems(JSONArray items, int selected) {
+    setItems(items);
+    mSelectedItems.clear();
+    mSelectedItems.add(selected);
+    mInputType = InputType.SINGLE_CHOICE;
+  }
+
+  /**
+   * Set multi choice items.
+   *
+   * @param items
+   *          a list of items as {@link String}s to display
+   * @param selected
+   *          a list of indices for items that should be selected by default
+   * @throws JSONException
+   */
+  public void setMultiChoiceItems(JSONArray items, JSONArray selected) throws JSONException {
+    setItems(items);
+    mSelectedItems.clear();
+    if (selected != null) {
+      for (int i = 0; i < selected.length(); i++) {
+        mSelectedItems.add(selected.getInt(i));
+      }
+    }
+    mInputType = InputType.MULTI_CHOICE;
+  }
+
+  /**
+   * Returns the list of selected items.
+   */
+  public Set<Integer> getSelectedItems() {
+    return mSelectedItems;
+  }
+
+  public void setTextInput(String defaultText) {
+    mDefaultText = defaultText;
+    mInputType = InputType.PLAIN_TEXT;
+    setEditInputType("text");
+  }
+
+  public void setEditInputType(String editInputType) {
+    String[] list = editInputType.split("\\|");
+    Map<String, Integer> types = ViewInflater.getInputTypes();
+    mEditInputType = 0;
+    for (String flag : list) {
+      Integer v = types.get(flag.trim());
+      if (v != null) {
+        mEditInputType |= v;
+      }
+    }
+    if (mEditInputType == 0) {
+      mEditInputType = android.text.InputType.TYPE_CLASS_TEXT;
+    }
+  }
+
+  public void setPasswordInput() {
+    mInputType = InputType.PASSWORD;
+  }
+
+  @Override
+  public void onCreate() {
+    super.onCreate();
+    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+    if (mTitle != null) {
+      builder.setTitle(mTitle);
+    }
+    // Can't display both a message and items. We'll elect to show the items instead.
+    if (mMessage != null && mItems.isEmpty()) {
+      builder.setMessage(mMessage);
+    }
+    switch (mInputType) {
+    // Add single choice menu items to dialog.
+    case SINGLE_CHOICE:
+      builder.setSingleChoiceItems(getItemsAsCharSequenceArray(), mSelectedItems.iterator().next(),
+          new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int item) {
+              mSelectedItems.clear();
+              mSelectedItems.add(item);
+            }
+          });
+      break;
+    // Add multiple choice items to the dialog.
+    case MULTI_CHOICE:
+      boolean[] selectedItems = new boolean[mItems.size()];
+      for (int i : mSelectedItems) {
+        selectedItems[i] = true;
+      }
+      builder.setMultiChoiceItems(getItemsAsCharSequenceArray(), selectedItems,
+          new DialogInterface.OnMultiChoiceClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int item, boolean isChecked) {
+              if (isChecked) {
+                mSelectedItems.add(item);
+              } else {
+                mSelectedItems.remove(item);
+              }
+            }
+          });
+      break;
+    // Add standard, menu-like, items to dialog.
+    case MENU:
+      builder.setItems(getItemsAsCharSequenceArray(), new DialogInterface.OnClickListener() {
+        public void onClick(DialogInterface dialog, int item) {
+          Map<String, Integer> result = new HashMap<String, Integer>();
+          result.put("item", item);
+          dismissDialog();
+          setResult(result);
+        }
+      });
+      break;
+    case PLAIN_TEXT:
+      mEditText = new EditText(getActivity());
+      if (mDefaultText != null) {
+        mEditText.setText(mDefaultText);
+      }
+      mEditText.setInputType(mEditInputType);
+      builder.setView(mEditText);
+      break;
+    case PASSWORD:
+      mEditText = new EditText(getActivity());
+      mEditText.setInputType(android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
+      mEditText.setTransformationMethod(new PasswordTransformationMethod());
+      builder.setView(mEditText);
+      break;
+    default:
+      // No input type specified.
+    }
+    configureButtons(builder, getActivity());
+    addOnCancelListener(builder, getActivity());
+    mDialog = builder.show();
+    mShowLatch.countDown();
+  }
+
+  private CharSequence[] getItemsAsCharSequenceArray() {
+    return mItems.toArray(new CharSequence[mItems.size()]);
+  }
+
+  private Builder addOnCancelListener(final AlertDialog.Builder builder, final Activity activity) {
+    return builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+      @Override
+      public void onCancel(DialogInterface dialog) {
+        mResultMap.put("canceled", true);
+        setResult();
+      }
+    });
+  }
+
+  private void configureButtons(final AlertDialog.Builder builder, final Activity activity) {
+    DialogInterface.OnClickListener buttonListener = new DialogInterface.OnClickListener() {
+      @Override
+      public void onClick(DialogInterface dialog, int which) {
+        switch (which) {
+        case DialogInterface.BUTTON_POSITIVE:
+          mResultMap.put("which", "positive");
+          break;
+        case DialogInterface.BUTTON_NEGATIVE:
+          mResultMap.put("which", "negative");
+          break;
+        case DialogInterface.BUTTON_NEUTRAL:
+          mResultMap.put("which", "neutral");
+
+          break;
+        }
+        setResult();
+      }
+    };
+    if (mNegativeButtonText != null) {
+      builder.setNegativeButton(mNegativeButtonText, buttonListener);
+    }
+    if (mPositiveButtonText != null) {
+      builder.setPositiveButton(mPositiveButtonText, buttonListener);
+    }
+    if (mNeutralButtonText != null) {
+      builder.setNeutralButton(mNeutralButtonText, buttonListener);
+    }
+  }
+
+  private void setResult() {
+    dismissDialog();
+    if (mInputType == InputType.PLAIN_TEXT || mInputType == InputType.PASSWORD) {
+      mResultMap.put("value", mEditText.getText().toString());
+    }
+    setResult(mResultMap);
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ui/DatePickerDialogTask.java b/Common/src/com/googlecode/android_scripting/facade/ui/DatePickerDialogTask.java
new file mode 100644
index 0000000..e80bd25
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ui/DatePickerDialogTask.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.ui;
+
+import android.app.DatePickerDialog;
+import android.content.DialogInterface;
+import android.util.AndroidRuntimeException;
+import android.widget.DatePicker;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Wrapper class for date picker dialog running in separate thread.
+ *
+ * @author MeanEYE.rcf (meaneye.rcf@gmail.com)
+ */
+public class DatePickerDialogTask extends DialogTask {
+  public static int mYear;
+  public static int mMonth;
+  public static int mDay;
+
+  public DatePickerDialogTask(int year, int month, int day) {
+    mYear = year;
+    mMonth = month - 1;
+    mDay = day;
+  }
+
+  @Override
+  public void onCreate() {
+    super.onCreate();
+    mDialog = new DatePickerDialog(getActivity(), new DatePickerDialog.OnDateSetListener() {
+      @Override
+      public void onDateSet(DatePicker view, int year, int month, int day) {
+        JSONObject result = new JSONObject();
+        try {
+          result.put("which", "positive");
+          result.put("year", year);
+          result.put("month", month + 1);
+          result.put("day", day);
+          setResult(result);
+        } catch (JSONException e) {
+          throw new AndroidRuntimeException(e);
+        }
+      }
+    }, mYear, mMonth, mDay);
+    mDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+      @Override
+      public void onCancel(DialogInterface view) {
+        JSONObject result = new JSONObject();
+        try {
+          result.put("which", "neutral");
+          result.put("year", mYear);
+          result.put("month", mMonth + 1);
+          result.put("day", mDay);
+          setResult(result);
+        } catch (JSONException e) {
+          throw new AndroidRuntimeException(e);
+        }
+      }
+    });
+    mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+      @Override
+      public void onDismiss(DialogInterface dialog) {
+        JSONObject result = new JSONObject();
+        try {
+          result.put("which", "negative");
+          result.put("year", mYear);
+          result.put("month", mMonth + 1);
+          result.put("day", mDay);
+          setResult(result);
+        } catch (JSONException e) {
+          throw new AndroidRuntimeException(e);
+        }
+      }
+    });
+    mDialog.show();
+    mShowLatch.countDown();
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ui/DialogTask.java b/Common/src/com/googlecode/android_scripting/facade/ui/DialogTask.java
new file mode 100644
index 0000000..ba48fcb
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ui/DialogTask.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.ui;
+
+import android.app.Dialog;
+
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.future.FutureActivityTask;
+
+import java.util.concurrent.CountDownLatch;
+
+abstract class DialogTask extends FutureActivityTask<Object> {
+
+  protected Dialog mDialog;
+  private EventFacade mEventFacade;
+
+  public EventFacade getEventFacade() {
+    return mEventFacade;
+  }
+
+  public void setEventFacade(EventFacade mEventFacade) {
+    this.mEventFacade = mEventFacade;
+  }
+
+  @Override
+  protected void setResult(Object object) {
+    super.setResult(object);
+    EventFacade eventFacade = getEventFacade();
+    if (eventFacade != null) {
+      eventFacade.postEvent("dialog", object);
+    }
+  }
+
+  protected final CountDownLatch mShowLatch = new CountDownLatch(1);
+
+  /**
+   * Returns the wrapped {@link Dialog}.
+   */
+  public Dialog getDialog() {
+    return mDialog;
+  }
+
+  /**
+   * Dismiss the {@link Dialog} and close {@link Sl4aActivity}.
+   */
+  public void dismissDialog() {
+    if (mDialog != null) {
+      mDialog.dismiss();
+      finish();
+    }
+    mDialog = null;
+  }
+
+  /**
+   * Returns the {@link CountDownLatch} that is counted down when the dialog is shown.
+   */
+  public CountDownLatch getShowLatch() {
+    return mShowLatch;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ui/FullScreenTask.java b/Common/src/com/googlecode/android_scripting/facade/ui/FullScreenTask.java
new file mode 100644
index 0000000..a97d51d
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ui/FullScreenTask.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.ui;
+
+import android.R;
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.future.FutureActivityTask;
+
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+import org.json.JSONArray;
+import org.xmlpull.v1.XmlPullParser;
+
+public class FullScreenTask extends FutureActivityTask<Object> implements OnClickListener,
+    OnItemClickListener {
+  private EventFacade mEventFacade;
+  private UiFacade mUiFacade;
+  public View mView = null;
+  protected ViewInflater mInflater = new ViewInflater();
+  protected String mLayout;
+  protected final CountDownLatch mShowLatch = new CountDownLatch(1);
+  protected Handler mHandler = null;
+  private List<Integer> mOverrideKeys;
+  protected String mTitle;
+
+  public FullScreenTask(String layout, String title) {
+    super();
+    mLayout = layout;
+    if (title != null) {
+      mTitle = title;
+    } else {
+      mTitle = "SL4a";
+    }
+  }
+
+  @Override
+  public void onCreate() {
+    // super.onCreate();
+    if (mHandler == null) {
+      mHandler = new Handler();
+    }
+    mInflater.getErrors().clear();
+    try {
+      if (mView == null) {
+        StringReader sr = new StringReader(mLayout);
+        XmlPullParser xml = ViewInflater.getXml(sr);
+        mView = mInflater.inflate(getActivity(), xml);
+      }
+    } catch (Exception e) {
+      mInflater.getErrors().add(e.toString());
+      mView = defaultView();
+      mInflater.setIdList(R.id.class);
+    }
+    getActivity().setContentView(mView);
+    getActivity().setTitle(mTitle);
+    mInflater.setClickListener(mView, this, this);
+    mShowLatch.countDown();
+  }
+
+  @Override
+  public void onDestroy() {
+    mEventFacade.postEvent("screen", "destroy");
+    super.onDestroy();
+  }
+
+  /** default view in case of errors */
+  protected View defaultView() {
+    LinearLayout result = new LinearLayout(getActivity());
+    result.setOrientation(LinearLayout.VERTICAL);
+    TextView text = new TextView(getActivity());
+    text.setText("Sample Layout");
+    result.addView(text, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+    Button b = new Button(getActivity());
+    b.setText("OK");
+    result.addView(b, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+    return result;
+  }
+
+  public EventFacade getEventFacade() {
+    return mEventFacade;
+  }
+
+  public void setEventFacade(EventFacade eventFacade) {
+    mEventFacade = eventFacade;
+  }
+
+  public void setUiFacade(UiFacade uiFacade) {
+    mUiFacade = uiFacade;
+  }
+
+  public CountDownLatch getShowLatch() {
+    return mShowLatch;
+  }
+
+  public Map<String, Map<String, String>> getViewAsMap() {
+    return mInflater.getViewAsMap(mView);
+  }
+
+  private View getViewByName(String idName) {
+    View result = null;
+    int id = mInflater.getId(idName);
+    if (id != 0) {
+      result = mView.findViewById(id);
+    }
+    return result;
+  }
+
+  public Map<String, String> getViewDetail(String idName) {
+    Map<String, String> result = new HashMap<String, String>();
+    result.put("error", "id not found (" + idName + ")");
+    View v = getViewByName(idName);
+    if (v != null) {
+      result = mInflater.getViewInfo(v);
+    }
+    return result;
+  }
+
+  public String setViewProperty(String idName, String property, String value) {
+    View v = getViewByName(idName);
+    mInflater.getErrors().clear();
+    if (v != null) {
+      SetProperty p = new SetProperty(v, property, value);
+      mHandler.post(p);
+      try {
+        p.mLatch.await();
+      } catch (InterruptedException e) {
+        mInflater.getErrors().add(e.toString());
+      }
+    } else {
+      return "View " + idName + " not found.";
+    }
+    if (mInflater.getErrors().size() == 0) {
+      return "OK";
+    }
+    return mInflater.getErrors().get(0);
+  }
+
+  public String setList(String id, JSONArray items) {
+    View v = getViewByName(id);
+    mInflater.getErrors().clear();
+    if (v != null) {
+      SetList p = new SetList(v, items);
+      mHandler.post(p);
+      try {
+        p.mLatch.await();
+      } catch (InterruptedException e) {
+        mInflater.getErrors().add(e.toString());
+      }
+    } else {
+      return "View " + id + " not found.";
+    }
+    if (mInflater.getErrors().size() == 0) {
+      return "OK";
+    }
+    return mInflater.getErrors().get(0);
+  }
+
+  @Override
+  public void onClick(View view) {
+    mEventFacade.postEvent("click", mInflater.getViewInfo(view));
+  }
+
+  public void loadLayout(String layout) {
+    ViewInflater inflater = new ViewInflater();
+    View view;
+    StringReader sr = new StringReader(layout);
+    try {
+      XmlPullParser xml = ViewInflater.getXml(sr);
+      view = inflater.inflate(getActivity(), xml);
+      mView = view;
+      mInflater = inflater;
+      getActivity().setContentView(mView);
+      mInflater.setClickListener(mView, this, this);
+      mLayout = layout;
+      mView.invalidate();
+    } catch (Exception e) {
+      mInflater.getErrors().add(e.toString());
+    }
+  }
+
+  private class SetProperty implements Runnable {
+    View mView;
+    String mProperty;
+    String mValue;
+    CountDownLatch mLatch = new CountDownLatch(1);
+
+    SetProperty(View view, String property, String value) {
+      mView = view;
+      mProperty = property;
+      mValue = value;
+    }
+
+    @Override
+    public void run() {
+      // TODO Auto-generated method stub
+      mInflater.setProperty(mView, mProperty, mValue);
+      mView.invalidate();
+      mLatch.countDown();
+    }
+  }
+
+  private class SetList implements Runnable {
+    View mView;
+    JSONArray mItems;
+    CountDownLatch mLatch = new CountDownLatch(1);
+
+    SetList(View view, JSONArray items) {
+      mView = view;
+      mItems = items;
+      mLatch.countDown();
+    }
+
+    @Override
+    public void run() {
+      mInflater.setListAdapter(mView, mItems);
+      mView.invalidate();
+    }
+  }
+
+  private class SetLayout implements Runnable {
+    String mLayout;
+    CountDownLatch mLatch = new CountDownLatch(1);
+
+    SetLayout(String layout) {
+      mLayout = layout;
+    }
+
+    @Override
+    public void run() {
+      loadLayout(mLayout);
+      mLatch.countDown();
+    }
+  }
+
+  private class SetTitle implements Runnable {
+    String mSetTitle;
+    CountDownLatch mLatch = new CountDownLatch(1);
+
+    SetTitle(String title) {
+      mSetTitle = title;
+    }
+
+    @Override
+    public void run() {
+      mTitle = mSetTitle;
+      getActivity().setTitle(mSetTitle);
+      mLatch.countDown();
+    }
+  }
+
+  @Override
+  public boolean onKeyDown(int keyCode, KeyEvent event) {
+    Map<String, String> data = new HashMap<String, String>();
+    data.put("key", String.valueOf(keyCode));
+    data.put("action", String.valueOf(event.getAction()));
+    mEventFacade.postEvent("key", data);
+    boolean overrideKey =
+        (keyCode == KeyEvent.KEYCODE_BACK)
+            || (mOverrideKeys == null ? false : mOverrideKeys.contains(keyCode));
+    return overrideKey;
+  }
+
+  @Override
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    return mUiFacade.onPrepareOptionsMenu(menu);
+  }
+
+  @Override
+  public void onItemClick(AdapterView<?> aview, View aitem, int position, long id) {
+    Map<String, String> data = mInflater.getViewInfo(aview);
+    data.put("position", String.valueOf(position));
+    mEventFacade.postEvent("itemclick", data);
+  }
+
+  public void setOverrideKeys(List<Integer> overrideKeys) {
+    mOverrideKeys = overrideKeys;
+  }
+
+  // Used to hot-switch screens.
+  public void setLayout(String layout) {
+    SetLayout p = new SetLayout(layout);
+    mHandler.post(p);
+    try {
+      p.mLatch.await();
+    } catch (InterruptedException e) {
+      mInflater.getErrors().add(e.toString());
+    }
+  }
+
+  public void setTitle(String title) {
+    SetTitle p = new SetTitle(title);
+    mHandler.post(p);
+    try {
+      p.mLatch.await();
+    } catch (InterruptedException e) {
+      mInflater.getErrors().add(e.toString());
+    }
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ui/ProgressDialogTask.java b/Common/src/com/googlecode/android_scripting/facade/ui/ProgressDialogTask.java
new file mode 100644
index 0000000..2d9aabf
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ui/ProgressDialogTask.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.ui;
+
+import android.app.ProgressDialog;
+
+/**
+ * Wrapper class for progress dialog running in separate thread
+ *
+ * @author MeanEYE.rcf (meaneye.rcf@gmail.com)
+ */
+class ProgressDialogTask extends DialogTask {
+
+  private final int mStyle;
+  private final int mMax;
+  private final String mTitle;
+  private final String mMessage;
+  private final Boolean mCancelable;
+
+  public ProgressDialogTask(int style, int max, String title, String message, boolean cancelable) {
+    mStyle = style;
+    mMax = max;
+    mTitle = title;
+    mMessage = message;
+    mCancelable = cancelable;
+  }
+
+  @Override
+  public void onCreate() {
+    super.onCreate();
+    mDialog = new ProgressDialog(getActivity());
+    ((ProgressDialog) mDialog).setProgressStyle(mStyle);
+    ((ProgressDialog) mDialog).setMax(mMax);
+    mDialog.setCancelable(mCancelable);
+    mDialog.setTitle(mTitle);
+    ((ProgressDialog) mDialog).setMessage(mMessage);
+    mDialog.show();
+    mShowLatch.countDown();
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ui/SeekBarDialogTask.java b/Common/src/com/googlecode/android_scripting/facade/ui/SeekBarDialogTask.java
new file mode 100644
index 0000000..3930972
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ui/SeekBarDialogTask.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.content.DialogInterface;
+import android.util.AndroidRuntimeException;
+import android.widget.SeekBar;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.EventFacade;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Wrapper class for dialog box with seek bar.
+ *
+ * @author MeanEYE.rcf (meaneye.rcf@gmail.com)
+ */
+public class SeekBarDialogTask extends DialogTask {
+
+  private SeekBar mSeekBar;
+  private final int mProgress;
+  private final int mMax;
+  private final String mTitle;
+  private final String mMessage;
+  private String mPositiveButtonText;
+  private String mNegativeButtonText;
+
+  public SeekBarDialogTask(int progress, int max, String title, String message) {
+    mProgress = progress;
+    mMax = max;
+    mTitle = title;
+    mMessage = message;
+  }
+
+  public void setPositiveButtonText(String text) {
+    mPositiveButtonText = text;
+  }
+
+  public void setNegativeButtonText(String text) {
+    mNegativeButtonText = text;
+  }
+
+  @Override
+  public void onCreate() {
+    super.onCreate();
+    mSeekBar = new SeekBar(getActivity());
+    mSeekBar.setMax(mMax);
+    mSeekBar.setProgress(mProgress);
+    mSeekBar.setPadding(10, 0, 10, 3);
+    mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+
+      @Override
+      public void onStopTrackingTouch(SeekBar arg0) {
+      }
+
+      @Override
+      public void onStartTrackingTouch(SeekBar arg0) {
+      }
+
+      @Override
+      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+        EventFacade eventFacade = getEventFacade();
+        if (eventFacade != null) {
+          JSONObject result = new JSONObject();
+          try {
+            result.put("which", "seekbar");
+            result.put("progress", mSeekBar.getProgress());
+            result.put("fromuser", fromUser);
+            eventFacade.postEvent("dialog", result);
+          } catch (JSONException e) {
+            Log.e(e);
+          }
+        }
+      }
+    });
+
+    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+    if (mTitle != null) {
+      builder.setTitle(mTitle);
+    }
+    if (mMessage != null) {
+      builder.setMessage(mMessage);
+    }
+    builder.setView(mSeekBar);
+    configureButtons(builder, getActivity());
+    addOnCancelListener(builder, getActivity());
+    mDialog = builder.show();
+    mShowLatch.countDown();
+  }
+
+  private Builder addOnCancelListener(final AlertDialog.Builder builder, final Activity activity) {
+    return builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+      @Override
+      public void onCancel(DialogInterface dialog) {
+        JSONObject result = new JSONObject();
+        try {
+          result.put("canceled", true);
+          result.put("progress", mSeekBar.getProgress());
+        } catch (JSONException e) {
+          Log.e(e);
+        }
+        dismissDialog();
+        setResult(result);
+      }
+    });
+  }
+
+  private void configureButtons(final AlertDialog.Builder builder, final Activity activity) {
+    DialogInterface.OnClickListener buttonListener = new DialogInterface.OnClickListener() {
+      @Override
+      public void onClick(DialogInterface dialog, int which) {
+        JSONObject result = new JSONObject();
+        switch (which) {
+        case DialogInterface.BUTTON_POSITIVE:
+          try {
+            result.put("which", "positive");
+            result.put("progress", mSeekBar.getProgress());
+          } catch (JSONException e) {
+            throw new AndroidRuntimeException(e);
+          }
+          break;
+        case DialogInterface.BUTTON_NEGATIVE:
+          try {
+            result.put("which", "negative");
+            result.put("progress", mSeekBar.getProgress());
+          } catch (JSONException e) {
+            throw new AndroidRuntimeException(e);
+          }
+          break;
+        }
+        dismissDialog();
+        setResult(result);
+      }
+    };
+    if (mNegativeButtonText != null) {
+      builder.setNegativeButton(mNegativeButtonText, buttonListener);
+    }
+    if (mPositiveButtonText != null) {
+      builder.setPositiveButton(mPositiveButtonText, buttonListener);
+    }
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ui/TimePickerDialogTask.java b/Common/src/com/googlecode/android_scripting/facade/ui/TimePickerDialogTask.java
new file mode 100644
index 0000000..ad71cd3
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ui/TimePickerDialogTask.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.ui;
+
+import android.app.TimePickerDialog;
+import android.content.DialogInterface;
+import android.util.AndroidRuntimeException;
+import android.widget.TimePicker;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Wrapper class for time picker dialog running in separate thread.
+ *
+ * @author MeanEYE.rcf (meaneye.rcf@gmail.com)
+ */
+public class TimePickerDialogTask extends DialogTask {
+  private final int mHour;
+  private final int mMinute;
+  private final boolean mIs24Hour;
+
+  public TimePickerDialogTask(int hour, int minute, boolean is24hour) {
+    mHour = hour;
+    mMinute = minute;
+    mIs24Hour = is24hour;
+  }
+
+  @Override
+  public void onCreate() {
+    super.onCreate();
+    mDialog = new TimePickerDialog(getActivity(), new TimePickerDialog.OnTimeSetListener() {
+      @Override
+      public void onTimeSet(TimePicker view, int hour, int minute) {
+        JSONObject result = new JSONObject();
+        try {
+          result.put("which", "positive");
+          result.put("hour", hour);
+          result.put("minute", minute);
+          setResult(result);
+        } catch (JSONException e) {
+          throw new AndroidRuntimeException(e);
+        }
+      }
+    }, mHour, mMinute, mIs24Hour);
+    mDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+      @Override
+      public void onCancel(DialogInterface view) {
+        JSONObject result = new JSONObject();
+        try {
+          result.put("which", "neutral");
+          result.put("hour", mHour);
+          result.put("minute", mMinute);
+          setResult(result);
+        } catch (JSONException e) {
+          throw new AndroidRuntimeException(e);
+        }
+      }
+    });
+    mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+      @Override
+      public void onDismiss(DialogInterface dialog) {
+        JSONObject result = new JSONObject();
+        try {
+          result.put("which", "negative");
+          result.put("hour", mHour);
+          result.put("minute", mMinute);
+          setResult(result);
+        } catch (JSONException e) {
+          throw new AndroidRuntimeException(e);
+        }
+      }
+    });
+    mDialog.show();
+    mShowLatch.countDown();
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/ui/UiFacade.java b/Common/src/com/googlecode/android_scripting/facade/ui/UiFacade.java
new file mode 100644
index 0000000..ccd509e
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ui/UiFacade.java
@@ -0,0 +1,736 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.ui;
+
+import android.app.ProgressDialog;
+import android.app.Service;
+import android.util.AndroidRuntimeException;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.FileUtils;
+import com.googlecode.android_scripting.FutureActivityTaskExecutor;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.interpreter.html.HtmlActivityTask;
+import com.googlecode.android_scripting.interpreter.html.HtmlInterpreter;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+/**
+ * User Interface Facade. <br>
+ * <br>
+ * <b>Usage Notes</b><br>
+ * <br>
+ * The UI facade provides access to a selection of dialog boxes for general user interaction, and
+ * also hosts the {@link #webViewShow} call which allows interactive use of html pages.<br>
+ * The general use of the dialog functions is as follows:<br>
+ * <ol>
+ * <li>Create a dialog using one of the following calls:
+ * <ul>
+ * <li>{@link #dialogCreateInput}
+ * <li>{@link #dialogCreateAlert}
+ * <li>{@link #dialogCreateDatePicker}
+ * <li>{@link #dialogCreateHorizontalProgress}
+ * <li>{@link #dialogCreatePassword}
+ * <li>{@link #dialogCreateSeekBar}
+ * <li>{@link #dialogCreateSpinnerProgress}
+ * </ul>
+ * <li>Set additional features to your dialog
+ * <ul>
+ * <li>{@link #dialogSetItems} Set a list of items. Used like a menu.
+ * <li>{@link #dialogSetMultiChoiceItems} Set a multichoice list of items.
+ * <li>{@link #dialogSetSingleChoiceItems} Set a single choice list of items.
+ * <li>{@link #dialogSetPositiveButtonText}
+ * <li>{@link #dialogSetNeutralButtonText}
+ * <li>{@link #dialogSetNegativeButtonText}
+ * <li>{@link #dialogSetMaxProgress} Set max progress for your progress bar.
+ * </ul>
+ * <li>Display the dialog using {@link #dialogShow}
+ * <li>Update dialog information if needed
+ * <ul>
+ * <li>{@link #dialogSetCurrentProgress}
+ * </ul>
+ * <li>Get the results
+ * <ul>
+ * <li>Using {@link #dialogGetResponse}, which will wait until the user performs an action to close
+ * the dialog box, or
+ * <li>Use eventPoll to wait for a "dialog" event.
+ * <li>You can find out which list items were selected using {@link #dialogGetSelectedItems}, which
+ * returns an array of numeric indices to your list. For a single choice list, there will only ever
+ * be one of these.
+ * </ul>
+ * <li>Once done, use {@link #dialogDismiss} to remove the dialog.
+ * </ol>
+ * <br>
+ * You can also manipulate menu options. The menu options are available for both {@link #dialogShow}
+ * and {@link #fullShow}.
+ * <ul>
+ * <li>{@link #clearOptionsMenu}
+ * <li>{@link #addOptionsMenuItem}
+ * </ul>
+ * <br>
+ * <b>Some notes:</b><br>
+ * Not every dialogSet function is relevant to every dialog type, ie, dialogSetMaxProgress obviously
+ * only applies to dialogs created with a progress bar. Also, an Alert Dialog may have a message or
+ * items, not both. If you set both, items will take priority.<br>
+ * In addition to the above functions, {@link #dialogGetInput} and {@link #dialogGetPassword} are
+ * convenience functions that create, display and return the relevant dialogs in one call.<br>
+ * There is only ever one instance of a dialog. Any dialogCreate call will cause the existing dialog
+ * to be destroyed.
+ *
+ * @author MeanEYE.rcf (meaneye.rcf@gmail.com)
+ */
+public class UiFacade extends RpcReceiver {
+  // This value should not be used for menu groups outside this class.
+  private static final int MENU_GROUP_ID = Integer.MAX_VALUE;
+  private static final String blankLayout = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+          + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\""
+          + "android:id=\"@+id/background\" android:orientation=\"vertical\""
+          + "android:layout_width=\"match_parent\" android:layout_height=\"match_parent\""
+          + "android:background=\"#ff000000\"></LinearLayout>";
+
+  private final Service mService;
+  private final FutureActivityTaskExecutor mTaskQueue;
+  private DialogTask mDialogTask;
+  private FullScreenTask mFullScreenTask;
+
+  private final List<UiMenuItem> mContextMenuItems;
+  private final List<UiMenuItem> mOptionsMenuItems;
+  private final AtomicBoolean mMenuUpdated;
+
+  private final EventFacade mEventFacade;
+  private List<Integer> mOverrideKeys = Collections.synchronizedList(new ArrayList<Integer>());
+
+  private float mLastXPosition;
+
+  public UiFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mTaskQueue = ((BaseApplication) mService.getApplication()).getTaskExecutor();
+    mContextMenuItems = new CopyOnWriteArrayList<UiMenuItem>();
+    mOptionsMenuItems = new CopyOnWriteArrayList<UiMenuItem>();
+    mEventFacade = manager.getReceiver(EventFacade.class);
+    mMenuUpdated = new AtomicBoolean(false);
+  }
+
+  /**
+   * For inputType, see <a
+   * href="http://developer.android.com/reference/android/R.styleable.html#TextView_inputType"
+   * >InputTypes</a>. Some useful ones are text, number, and textUri. Multiple flags can be
+   * supplied, seperated by "|", ie: "textUri|textAutoComplete"
+   */
+  @Rpc(description = "Create a text input dialog.")
+  public void dialogCreateInput(
+      @RpcParameter(name = "title", description = "title of the input box") @RpcDefault("Value") final String title,
+      @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter value:") final String message,
+      @RpcParameter(name = "defaultText", description = "text to insert into the input box") @RpcOptional final String text,
+      @RpcParameter(name = "inputType", description = "type of input data, ie number or text") @RpcOptional final String inputType)
+      throws InterruptedException {
+    dialogDismiss();
+    mDialogTask = new AlertDialogTask(title, message);
+    ((AlertDialogTask) mDialogTask).setTextInput(text);
+    if (inputType != null) {
+      ((AlertDialogTask) mDialogTask).setEditInputType(inputType);
+    }
+  }
+
+  @Rpc(description = "Create a password input dialog.")
+  public void dialogCreatePassword(
+      @RpcParameter(name = "title", description = "title of the input box") @RpcDefault("Password") final String title,
+      @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter password:") final String message) {
+    dialogDismiss();
+    mDialogTask = new AlertDialogTask(title, message);
+    ((AlertDialogTask) mDialogTask).setPasswordInput();
+  }
+
+  /**
+   * The result is the user's input, or None (null) if cancel was hit. <br>
+   * Example (python)
+   *
+   * <pre>
+   * import android
+   * droid=android.Android()
+   *
+   * print droid.dialogGetInput("Title","Message","Default").result
+   * </pre>
+   *
+   */
+  @SuppressWarnings("unchecked")
+  @Rpc(description = "Queries the user for a text input.")
+  public String dialogGetInput(
+      @RpcParameter(name = "title", description = "title of the input box") @RpcDefault("Value") final String title,
+      @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter value:") final String message,
+      @RpcParameter(name = "defaultText", description = "text to insert into the input box") @RpcOptional final String text)
+      throws InterruptedException {
+    dialogCreateInput(title, message, text, "text");
+    dialogSetNegativeButtonText("Cancel");
+    dialogSetPositiveButtonText("Ok");
+    dialogShow();
+    Map<String, Object> response = (Map<String, Object>) dialogGetResponse();
+    if ("positive".equals(response.get("which"))) {
+      return (String) response.get("value");
+    } else {
+      return null;
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Rpc(description = "Queries the user for a password.")
+  public String dialogGetPassword(
+      @RpcParameter(name = "title", description = "title of the password box") @RpcDefault("Password") final String title,
+      @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter password:") final String message)
+      throws InterruptedException {
+    dialogCreatePassword(title, message);
+    dialogSetNegativeButtonText("Cancel");
+    dialogSetPositiveButtonText("Ok");
+    dialogShow();
+    Map<String, Object> response = (Map<String, Object>) dialogGetResponse();
+    if ("positive".equals(response.get("which"))) {
+      return (String) response.get("value");
+    } else {
+      return null;
+    }
+  }
+
+  @Rpc(description = "Create a spinner progress dialog.")
+  public void dialogCreateSpinnerProgress(@RpcParameter(name = "title") @RpcOptional String title,
+      @RpcParameter(name = "message") @RpcOptional String message,
+      @RpcParameter(name = "maximum progress") @RpcDefault("100") Integer max) {
+    dialogDismiss(); // Dismiss any existing dialog.
+    mDialogTask = new ProgressDialogTask(ProgressDialog.STYLE_SPINNER, max, title, message, true);
+  }
+
+  @Rpc(description = "Create a horizontal progress dialog.")
+  public void dialogCreateHorizontalProgress(
+      @RpcParameter(name = "title") @RpcOptional String title,
+      @RpcParameter(name = "message") @RpcOptional String message,
+      @RpcParameter(name = "maximum progress") @RpcDefault("100") Integer max) {
+    dialogDismiss(); // Dismiss any existing dialog.
+    mDialogTask =
+        new ProgressDialogTask(ProgressDialog.STYLE_HORIZONTAL, max, title, message, true);
+  }
+
+  /**
+   * <b>Example (python)</b>
+   *
+   * <pre>
+   *   import android
+   *   droid=android.Android()
+   *   droid.dialogCreateAlert("I like swords.","Do you like swords?")
+   *   droid.dialogSetPositiveButtonText("Yes")
+   *   droid.dialogSetNegativeButtonText("No")
+   *   droid.dialogShow()
+   *   response=droid.dialogGetResponse().result
+   *   droid.dialogDismiss()
+   *   if response.has_key("which"):
+   *     result=response["which"]
+   *     if result=="positive":
+   *       print "Yay! I like swords too!"
+   *     elif result=="negative":
+   *       print "Oh. How sad."
+   *   elif response.has_key("canceled"): # Yes, I know it's mispelled.
+   *     print "You can't even make up your mind?"
+   *   else:
+   *     print "Unknown response=",response
+   *
+   *   print "Done"
+   * </pre>
+   */
+  @Rpc(description = "Create alert dialog.")
+  public void dialogCreateAlert(@RpcParameter(name = "title") @RpcOptional String title,
+      @RpcParameter(name = "message") @RpcOptional String message) {
+    dialogDismiss(); // Dismiss any existing dialog.
+    mDialogTask = new AlertDialogTask(title, message);
+  }
+
+  /**
+   * Will produce "dialog" events on change, containing:
+   * <ul>
+   * <li>"progress" - Position chosen, between 0 and max
+   * <li>"which" = "seekbar"
+   * <li>"fromuser" = true/false change is from user input
+   * </ul>
+   * Response will contain a "progress" element.
+   */
+  @Rpc(description = "Create seek bar dialog.")
+  public void dialogCreateSeekBar(
+      @RpcParameter(name = "starting value") @RpcDefault("50") Integer progress,
+      @RpcParameter(name = "maximum value") @RpcDefault("100") Integer max,
+      @RpcParameter(name = "title") String title, @RpcParameter(name = "message") String message) {
+    dialogDismiss(); // Dismiss any existing dialog.
+    mDialogTask = new SeekBarDialogTask(progress, max, title, message);
+  }
+
+  @Rpc(description = "Create time picker dialog.")
+  public void dialogCreateTimePicker(
+      @RpcParameter(name = "hour") @RpcDefault("0") Integer hour,
+      @RpcParameter(name = "minute") @RpcDefault("0") Integer minute,
+      @RpcParameter(name = "is24hour", description = "Use 24 hour clock") @RpcDefault("false") Boolean is24hour) {
+    dialogDismiss(); // Dismiss any existing dialog.
+    mDialogTask = new TimePickerDialogTask(hour, minute, is24hour);
+  }
+
+  @Rpc(description = "Create date picker dialog.")
+  public void dialogCreateDatePicker(@RpcParameter(name = "year") @RpcDefault("1970") Integer year,
+      @RpcParameter(name = "month") @RpcDefault("1") Integer month,
+      @RpcParameter(name = "day") @RpcDefault("1") Integer day) {
+    dialogDismiss(); // Dismiss any existing dialog.
+    mDialogTask = new DatePickerDialogTask(year, month, day);
+  }
+
+  @Rpc(description = "Dismiss dialog.")
+  public void dialogDismiss() {
+    if (mDialogTask != null) {
+      mDialogTask.dismissDialog();
+      mDialogTask = null;
+    }
+  }
+
+  @Rpc(description = "Show dialog.")
+  public void dialogShow() throws InterruptedException {
+    if (mDialogTask != null && mDialogTask.getDialog() == null) {
+      mDialogTask.setEventFacade(mEventFacade);
+      mTaskQueue.execute(mDialogTask);
+      mDialogTask.getShowLatch().await();
+    } else {
+      throw new RuntimeException("No dialog to show.");
+    }
+  }
+
+  @Rpc(description = "Set progress dialog current value.")
+  public void dialogSetCurrentProgress(@RpcParameter(name = "current") Integer current) {
+    if (mDialogTask != null && mDialogTask instanceof ProgressDialogTask) {
+      ((ProgressDialog) mDialogTask.getDialog()).setProgress(current);
+    } else {
+      throw new RuntimeException("No valid dialog to assign value to.");
+    }
+  }
+
+  @Rpc(description = "Set progress dialog maximum value.")
+  public void dialogSetMaxProgress(@RpcParameter(name = "max") Integer max) {
+    if (mDialogTask != null && mDialogTask instanceof ProgressDialogTask) {
+      ((ProgressDialog) mDialogTask.getDialog()).setMax(max);
+    } else {
+      throw new RuntimeException("No valid dialog to set maximum value of.");
+    }
+  }
+
+  @Rpc(description = "Set alert dialog positive button text.")
+  public void dialogSetPositiveButtonText(@RpcParameter(name = "text") String text) {
+    if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) {
+      ((AlertDialogTask) mDialogTask).setPositiveButtonText(text);
+    } else if (mDialogTask != null && mDialogTask instanceof SeekBarDialogTask) {
+      ((SeekBarDialogTask) mDialogTask).setPositiveButtonText(text);
+    } else {
+      throw new AndroidRuntimeException("No dialog to add button to.");
+    }
+  }
+
+  @Rpc(description = "Set alert dialog button text.")
+  public void dialogSetNegativeButtonText(@RpcParameter(name = "text") String text) {
+    if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) {
+      ((AlertDialogTask) mDialogTask).setNegativeButtonText(text);
+    } else if (mDialogTask != null && mDialogTask instanceof SeekBarDialogTask) {
+      ((SeekBarDialogTask) mDialogTask).setNegativeButtonText(text);
+    } else {
+      throw new AndroidRuntimeException("No dialog to add button to.");
+    }
+  }
+
+  @Rpc(description = "Set alert dialog button text.")
+  public void dialogSetNeutralButtonText(@RpcParameter(name = "text") String text) {
+    if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) {
+      ((AlertDialogTask) mDialogTask).setNeutralButtonText(text);
+    } else {
+      throw new AndroidRuntimeException("No dialog to add button to.");
+    }
+  }
+
+  // TODO(damonkohler): Make RPC layer translate between JSONArray and List<Object>.
+  /**
+   * This effectively creates list of options. Clicking on an item will immediately return an "item"
+   * element, which is the index of the selected item.
+   */
+  @Rpc(description = "Set alert dialog list items.")
+  public void dialogSetItems(@RpcParameter(name = "items") JSONArray items) {
+    if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) {
+      ((AlertDialogTask) mDialogTask).setItems(items);
+    } else {
+      throw new AndroidRuntimeException("No dialog to add list to.");
+    }
+  }
+
+  /**
+   * This creates a list of radio buttons. You can select one item out of the list. A response will
+   * not be returned until the dialog is closed, either with the Cancel key or a button
+   * (positive/negative/neutral). Use {@link #dialogGetSelectedItems()} to find out what was
+   * selected.
+   */
+  @Rpc(description = "Set dialog single choice items and selected item.")
+  public void dialogSetSingleChoiceItems(
+      @RpcParameter(name = "items") JSONArray items,
+      @RpcParameter(name = "selected", description = "selected item index") @RpcDefault("0") Integer selected) {
+    if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) {
+      ((AlertDialogTask) mDialogTask).setSingleChoiceItems(items, selected);
+    } else {
+      throw new AndroidRuntimeException("No dialog to add list to.");
+    }
+  }
+
+  /**
+   * This creates a list of check boxes. You can select multiple items out of the list. A response
+   * will not be returned until the dialog is closed, either with the Cancel key or a button
+   * (positive/negative/neutral). Use {@link #dialogGetSelectedItems()} to find out what was
+   * selected.
+   */
+
+  @Rpc(description = "Set dialog multiple choice items and selection.")
+  public void dialogSetMultiChoiceItems(
+      @RpcParameter(name = "items") JSONArray items,
+      @RpcParameter(name = "selected", description = "list of selected items") @RpcOptional JSONArray selected)
+      throws JSONException {
+    if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) {
+      ((AlertDialogTask) mDialogTask).setMultiChoiceItems(items, selected);
+    } else {
+      throw new AndroidRuntimeException("No dialog to add list to.");
+    }
+  }
+
+  @Rpc(description = "Returns dialog response.")
+  public Object dialogGetResponse() {
+    try {
+      return mDialogTask.getResult();
+    } catch (Exception e) {
+      throw new AndroidRuntimeException(e);
+    }
+  }
+
+  @Rpc(description = "This method provides list of items user selected.", returns = "Selected items")
+  public Set<Integer> dialogGetSelectedItems() {
+    if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) {
+      return ((AlertDialogTask) mDialogTask).getSelectedItems();
+    } else {
+      throw new AndroidRuntimeException("No dialog to add list to.");
+    }
+  }
+
+  /**
+   * See <a href=http://code.google.com/p/android-scripting/wiki/UsingWebView>wiki page</a> for more
+   * detail.
+   */
+  @Rpc(description = "Display a WebView with the given URL.")
+  public void webViewShow(
+      @RpcParameter(name = "url") String url,
+      @RpcParameter(name = "wait", description = "block until the user exits the WebView") @RpcOptional Boolean wait)
+      throws IOException {
+    String jsonSrc = FileUtils.readFromAssetsFile(mService, HtmlInterpreter.JSON_FILE);
+    String AndroidJsSrc = FileUtils.readFromAssetsFile(mService, HtmlInterpreter.ANDROID_JS_FILE);
+    HtmlActivityTask task = new HtmlActivityTask(mManager, AndroidJsSrc, jsonSrc, url, false);
+    mTaskQueue.execute(task);
+    if (wait != null && wait) {
+      try {
+        task.getResult();
+      } catch (InterruptedException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  /**
+   * Context menus are used primarily with {@link #webViewShow}
+   */
+  @Rpc(description = "Adds a new item to context menu.")
+  public void addContextMenuItem(
+      @RpcParameter(name = "label", description = "label for this menu item") String label,
+      @RpcParameter(name = "event", description = "event that will be generated on menu item click") String event,
+      @RpcParameter(name = "eventData") @RpcOptional Object data) {
+    mContextMenuItems.add(new UiMenuItem(label, event, data, null));
+  }
+
+  /**
+   * <b>Example (python)</b>
+   *
+   * <pre>
+   * import android
+   * droid=android.Android()
+   *
+   * droid.addOptionsMenuItem("Silly","silly",None,"star_on")
+   * droid.addOptionsMenuItem("Sensible","sensible","I bet.","star_off")
+   * droid.addOptionsMenuItem("Off","off",None,"ic_menu_revert")
+   *
+   * print "Hit menu to see extra options."
+   * print "Will timeout in 10 seconds if you hit nothing."
+   *
+   * while True: # Wait for events from the menu.
+   *   response=droid.eventWait(10000).result
+   *   if response==None:
+   *     break
+   *   print response
+   *   if response["name"]=="off":
+   *     break
+   * print "And done."
+   *
+   * </pre>
+   */
+  @Rpc(description = "Adds a new item to options menu.")
+  public void addOptionsMenuItem(
+      @RpcParameter(name = "label", description = "label for this menu item") String label,
+      @RpcParameter(name = "event", description = "event that will be generated on menu item click") String event,
+      @RpcParameter(name = "eventData") @RpcOptional Object data,
+      @RpcParameter(name = "iconName", description = "Android system menu icon, see http://developer.android.com/reference/android/R.drawable.html") @RpcOptional String iconName) {
+    mOptionsMenuItems.add(new UiMenuItem(label, event, data, iconName));
+    mMenuUpdated.set(true);
+  }
+
+  @Rpc(description = "Removes all items previously added to context menu.")
+  public void clearContextMenu() {
+    mContextMenuItems.clear();
+  }
+
+  @Rpc(description = "Removes all items previously added to options menu.")
+  public void clearOptionsMenu() {
+    mOptionsMenuItems.clear();
+    mMenuUpdated.set(true);
+  }
+
+  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+    for (UiMenuItem item : mContextMenuItems) {
+      MenuItem menuItem = menu.add(item.mmTitle);
+      menuItem.setOnMenuItemClickListener(item.mmListener);
+    }
+  }
+
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    if (mMenuUpdated.getAndSet(false)) {
+      menu.removeGroup(MENU_GROUP_ID);
+      for (UiMenuItem item : mOptionsMenuItems) {
+        MenuItem menuItem = menu.add(MENU_GROUP_ID, Menu.NONE, Menu.NONE, item.mmTitle);
+        if (item.mmIcon != null) {
+          menuItem.setIcon(mService.getResources()
+              .getIdentifier(item.mmIcon, "drawable", "android"));
+        }
+        menuItem.setOnMenuItemClickListener(item.mmListener);
+      }
+      return true;
+    }
+    return true;
+  }
+
+  /**
+   * See <a href=http://code.google.com/p/android-scripting/wiki/FullScreenUI>wiki page</a> for more
+   * detail.
+   */
+  @Rpc(description = "Show Full Screen.")
+  public List<String> fullShow(
+      @RpcParameter(name = "layout", description = "String containing View layout") String layout,
+      @RpcParameter(name = "title", description = "Activity Title") @RpcOptional String title)
+      throws InterruptedException {
+    if (mFullScreenTask != null) {
+      // fullDismiss();
+      mFullScreenTask.setLayout(layout);
+      if (title != null) {
+        mFullScreenTask.setTitle(title);
+      }
+    } else {
+      mFullScreenTask = new FullScreenTask(layout, title);
+      mFullScreenTask.setEventFacade(mEventFacade);
+      mFullScreenTask.setUiFacade(this);
+      mFullScreenTask.setOverrideKeys(mOverrideKeys);
+      mTaskQueue.execute(mFullScreenTask);
+      mFullScreenTask.getShowLatch().await();
+    }
+    return mFullScreenTask.mInflater.getErrors();
+  }
+
+  @Rpc(description = "Dismiss Full Screen.")
+  public void fullDismiss() {
+    if (mFullScreenTask != null) {
+      mFullScreenTask.finish();
+      mFullScreenTask = null;
+    }
+  }
+
+  class MouseMotionListener implements View.OnGenericMotionListener {
+
+      @Override
+      public boolean onGenericMotion(View v, MotionEvent event) {
+          Log.d("Generic motion triggered.");
+          if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
+              mLastXPosition = event.getAxisValue(MotionEvent.AXIS_X);
+              Log.d("New mouse x coord: " + mLastXPosition);
+//              Bundle msg = new Bundle();
+//              msg.putFloat("value", mLastXPosition);
+//              mEventFacade.postEvent("MouseXPositionUpdate", msg);
+              return true;
+          }
+          return false;
+      }
+  }
+
+  @Rpc(description = "Get Fullscreen Properties")
+  public Map<String, Map<String, String>> fullQuery() {
+    if (mFullScreenTask == null) {
+      throw new RuntimeException("No screen displayed.");
+    }
+    return mFullScreenTask.getViewAsMap();
+  }
+
+  @Rpc(description = "Get fullscreen properties for a specific widget")
+  public Map<String, String> fullQueryDetail(
+      @RpcParameter(name = "id", description = "id of layout widget") String id) {
+    if (mFullScreenTask == null) {
+      throw new RuntimeException("No screen displayed.");
+    }
+    return mFullScreenTask.getViewDetail(id);
+  }
+
+  @Rpc(description = "Set fullscreen widget property")
+  public String fullSetProperty(
+      @RpcParameter(name = "id", description = "id of layout widget") String id,
+      @RpcParameter(name = "property", description = "name of property to set") String property,
+      @RpcParameter(name = "value", description = "value to set property to") String value) {
+    if (mFullScreenTask == null) {
+      throw new RuntimeException("No screen displayed.");
+    }
+    return mFullScreenTask.setViewProperty(id, property, value);
+  }
+
+  @Rpc(description = "Attach a list to a fullscreen widget")
+  public String fullSetList(
+      @RpcParameter(name = "id", description = "id of layout widget") String id,
+      @RpcParameter(name = "list", description = "List to set") JSONArray items) {
+    if (mFullScreenTask == null) {
+      throw new RuntimeException("No screen displayed.");
+    }
+    return mFullScreenTask.setList(id, items);
+  }
+
+  @Rpc(description = "Set the Full Screen Activity Title")
+  public void fullSetTitle(
+      @RpcParameter(name = "title", description = "Activity Title") String title) {
+    if (mFullScreenTask == null) {
+      throw new RuntimeException("No screen displayed.");
+    }
+    mFullScreenTask.setTitle(title);
+  }
+
+  /**
+   * This will override the default behaviour of keys while in the fullscreen mode. ie:
+   *
+   * <pre>
+   *   droid.fullKeyOverride([24,25],True)
+   * </pre>
+   *
+   * This will override the default behaviour of the volume keys (codes 24 and 25) so that they do
+   * not actually adjust the volume. <br>
+   * Returns a list of currently overridden keycodes.
+   */
+  @Rpc(description = "Override default key actions")
+  public JSONArray fullKeyOverride(
+      @RpcParameter(name = "keycodes", description = "List of keycodes to override") JSONArray keycodes,
+      @RpcParameter(name = "enable", description = "Turn overriding or off") @RpcDefault(value = "true") Boolean enable)
+      throws JSONException {
+    for (int i = 0; i < keycodes.length(); i++) {
+      int value = (int) keycodes.getLong(i);
+      if (value > 0) {
+        if (enable) {
+          if (!mOverrideKeys.contains(value)) {
+            mOverrideKeys.add(value);
+          }
+        } else {
+          int index = mOverrideKeys.indexOf(value);
+          if (index >= 0) {
+            mOverrideKeys.remove(index);
+          }
+        }
+      }
+    }
+    if (mFullScreenTask != null) {
+      mFullScreenTask.setOverrideKeys(mOverrideKeys);
+    }
+    return new JSONArray(mOverrideKeys);
+  }
+
+  @Rpc(description = "Start tracking mouse cursor x coordinate.")
+  public void startTrackingMouseXCoord() throws InterruptedException {
+    View.OnGenericMotionListener l = new MouseMotionListener();
+    fullShow(blankLayout, "Blank");
+    mFullScreenTask.mView.setOnGenericMotionListener(l);
+  }
+
+  @Rpc(description = "Stop tracking mouse cursor x coordinate.")
+  public void stopTrackingMouseXCoord() throws InterruptedException {
+    fullDismiss();
+  }
+
+  @Rpc(description = "Return the latest X position of mouse cursor.")
+  public float getLatestMouseXCoord() {
+      return mLastXPosition;
+  }
+
+@Override
+  public void shutdown() {
+    fullDismiss();
+    HtmlActivityTask.shutdown();
+  }
+
+  private class UiMenuItem {
+
+    private final String mmTitle;
+    private final String mmEvent;
+    private final Object mmEventData;
+    private final String mmIcon;
+    private final MenuItem.OnMenuItemClickListener mmListener;
+
+    public UiMenuItem(String title, String event, Object data, String icon) {
+      mmTitle = title;
+      mmEvent = event;
+      mmEventData = data;
+      mmIcon = icon;
+      mmListener = new MenuItem.OnMenuItemClickListener() {
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+          // TODO(damonkohler): Does mmEventData need to be cloned somehow?
+          mEventFacade.postEvent(mmEvent, mmEventData);
+          return true;
+        }
+      };
+    }
+  }
+}
\ No newline at end of file
diff --git a/Common/src/com/googlecode/android_scripting/facade/ui/ViewInflater.java b/Common/src/com/googlecode/android_scripting/facade/ui/ViewInflater.java
new file mode 100644
index 0000000..871c562
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/ui/ViewInflater.java
@@ -0,0 +1,1023 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.text.InputType;
+import android.text.method.DigitsKeyListener;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+import android.widget.RelativeLayout;
+import android.widget.Spinner;
+import android.widget.SpinnerAdapter;
+import android.widget.TableLayout;
+import android.widget.TextView;
+
+import com.googlecode.android_scripting.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.json.JSONArray;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+public class ViewInflater {
+  private static XmlPullParserFactory mFactory;
+  public static final String ANDROID = "http://schemas.android.com/apk/res/android";
+  public static final int BASESEQ = 0x7f0f0000;
+  private int mNextSeq = BASESEQ;
+  private final Map<String, Integer> mIdList = new HashMap<String, Integer>();
+  private final List<String> mErrors = new ArrayList<String>();
+  private Context mContext;
+  private DisplayMetrics mMetrics;
+  private static final Map<String, Integer> mInputTypes = new HashMap<String, Integer>();
+  public static final Map<String, String> mColorNames = new HashMap<String, String>();
+  public static final Map<String, Integer> mRelative = new HashMap<String, Integer>();
+  static {
+    mColorNames.put("aliceblue", "#f0f8ff");
+    mColorNames.put("antiquewhite", "#faebd7");
+    mColorNames.put("aqua", "#00ffff");
+    mColorNames.put("aquamarine", "#7fffd4");
+    mColorNames.put("azure", "#f0ffff");
+    mColorNames.put("beige", "#f5f5dc");
+    mColorNames.put("bisque", "#ffe4c4");
+    mColorNames.put("black", "#000000");
+    mColorNames.put("blanchedalmond", "#ffebcd");
+    mColorNames.put("blue", "#0000ff");
+    mColorNames.put("blueviolet", "#8a2be2");
+    mColorNames.put("brown", "#a52a2a");
+    mColorNames.put("burlywood", "#deb887");
+    mColorNames.put("cadetblue", "#5f9ea0");
+    mColorNames.put("chartreuse", "#7fff00");
+    mColorNames.put("chocolate", "#d2691e");
+    mColorNames.put("coral", "#ff7f50");
+    mColorNames.put("cornflowerblue", "#6495ed");
+    mColorNames.put("cornsilk", "#fff8dc");
+    mColorNames.put("crimson", "#dc143c");
+    mColorNames.put("cyan", "#00ffff");
+    mColorNames.put("darkblue", "#00008b");
+    mColorNames.put("darkcyan", "#008b8b");
+    mColorNames.put("darkgoldenrod", "#b8860b");
+    mColorNames.put("darkgray", "#a9a9a9");
+    mColorNames.put("darkgrey", "#a9a9a9");
+    mColorNames.put("darkgreen", "#006400");
+    mColorNames.put("darkkhaki", "#bdb76b");
+    mColorNames.put("darkmagenta", "#8b008b");
+    mColorNames.put("darkolivegreen", "#556b2f");
+    mColorNames.put("darkorange", "#ff8c00");
+    mColorNames.put("darkorchid", "#9932cc");
+    mColorNames.put("darkred", "#8b0000");
+    mColorNames.put("darksalmon", "#e9967a");
+    mColorNames.put("darkseagreen", "#8fbc8f");
+    mColorNames.put("darkslateblue", "#483d8b");
+    mColorNames.put("darkslategray", "#2f4f4f");
+    mColorNames.put("darkslategrey", "#2f4f4f");
+    mColorNames.put("darkturquoise", "#00ced1");
+    mColorNames.put("darkviolet", "#9400d3");
+    mColorNames.put("deeppink", "#ff1493");
+    mColorNames.put("deepskyblue", "#00bfff");
+    mColorNames.put("dimgray", "#696969");
+    mColorNames.put("dimgrey", "#696969");
+    mColorNames.put("dodgerblue", "#1e90ff");
+    mColorNames.put("firebrick", "#b22222");
+    mColorNames.put("floralwhite", "#fffaf0");
+    mColorNames.put("forestgreen", "#228b22");
+    mColorNames.put("fuchsia", "#ff00ff");
+    mColorNames.put("gainsboro", "#dcdcdc");
+    mColorNames.put("ghostwhite", "#f8f8ff");
+    mColorNames.put("gold", "#ffd700");
+    mColorNames.put("goldenrod", "#daa520");
+    mColorNames.put("gray", "#808080");
+    mColorNames.put("grey", "#808080");
+    mColorNames.put("green", "#008000");
+    mColorNames.put("greenyellow", "#adff2f");
+    mColorNames.put("honeydew", "#f0fff0");
+    mColorNames.put("hotpink", "#ff69b4");
+    mColorNames.put("indianred ", "#cd5c5c");
+    mColorNames.put("indigo ", "#4b0082");
+    mColorNames.put("ivory", "#fffff0");
+    mColorNames.put("khaki", "#f0e68c");
+    mColorNames.put("lavender", "#e6e6fa");
+    mColorNames.put("lavenderblush", "#fff0f5");
+    mColorNames.put("lawngreen", "#7cfc00");
+    mColorNames.put("lemonchiffon", "#fffacd");
+    mColorNames.put("lightblue", "#add8e6");
+    mColorNames.put("lightcoral", "#f08080");
+    mColorNames.put("lightcyan", "#e0ffff");
+    mColorNames.put("lightgoldenrodyellow", "#fafad2");
+    mColorNames.put("lightgray", "#d3d3d3");
+    mColorNames.put("lightgrey", "#d3d3d3");
+    mColorNames.put("lightgreen", "#90ee90");
+    mColorNames.put("lightpink", "#ffb6c1");
+    mColorNames.put("lightsalmon", "#ffa07a");
+    mColorNames.put("lightseagreen", "#20b2aa");
+    mColorNames.put("lightskyblue", "#87cefa");
+    mColorNames.put("lightslategray", "#778899");
+    mColorNames.put("lightslategrey", "#778899");
+    mColorNames.put("lightsteelblue", "#b0c4de");
+    mColorNames.put("lightyellow", "#ffffe0");
+    mColorNames.put("lime", "#00ff00");
+    mColorNames.put("limegreen", "#32cd32");
+    mColorNames.put("linen", "#faf0e6");
+    mColorNames.put("magenta", "#ff00ff");
+    mColorNames.put("maroon", "#800000");
+    mColorNames.put("mediumaquamarine", "#66cdaa");
+    mColorNames.put("mediumblue", "#0000cd");
+    mColorNames.put("mediumorchid", "#ba55d3");
+    mColorNames.put("mediumpurple", "#9370d8");
+    mColorNames.put("mediumseagreen", "#3cb371");
+    mColorNames.put("mediumslateblue", "#7b68ee");
+    mColorNames.put("mediumspringgreen", "#00fa9a");
+    mColorNames.put("mediumturquoise", "#48d1cc");
+    mColorNames.put("mediumvioletred", "#c71585");
+    mColorNames.put("midnightblue", "#191970");
+    mColorNames.put("mintcream", "#f5fffa");
+    mColorNames.put("mistyrose", "#ffe4e1");
+    mColorNames.put("moccasin", "#ffe4b5");
+    mColorNames.put("navajowhite", "#ffdead");
+    mColorNames.put("navy", "#000080");
+    mColorNames.put("oldlace", "#fdf5e6");
+    mColorNames.put("olive", "#808000");
+    mColorNames.put("olivedrab", "#6b8e23");
+    mColorNames.put("orange", "#ffa500");
+    mColorNames.put("orangered", "#ff4500");
+    mColorNames.put("orchid", "#da70d6");
+    mColorNames.put("palegoldenrod", "#eee8aa");
+    mColorNames.put("palegreen", "#98fb98");
+    mColorNames.put("paleturquoise", "#afeeee");
+    mColorNames.put("palevioletred", "#d87093");
+    mColorNames.put("papayawhip", "#ffefd5");
+    mColorNames.put("peachpuff", "#ffdab9");
+    mColorNames.put("peru", "#cd853f");
+    mColorNames.put("pink", "#ffc0cb");
+    mColorNames.put("plum", "#dda0dd");
+    mColorNames.put("powderblue", "#b0e0e6");
+    mColorNames.put("purple", "#800080");
+    mColorNames.put("red", "#ff0000");
+    mColorNames.put("rosybrown", "#bc8f8f");
+    mColorNames.put("royalblue", "#4169e1");
+    mColorNames.put("saddlebrown", "#8b4513");
+    mColorNames.put("salmon", "#fa8072");
+    mColorNames.put("sandybrown", "#f4a460");
+    mColorNames.put("seagreen", "#2e8b57");
+    mColorNames.put("seashell", "#fff5ee");
+    mColorNames.put("sienna", "#a0522d");
+    mColorNames.put("silver", "#c0c0c0");
+    mColorNames.put("skyblue", "#87ceeb");
+    mColorNames.put("slateblue", "#6a5acd");
+    mColorNames.put("slategray", "#708090");
+    mColorNames.put("slategrey", "#708090");
+    mColorNames.put("snow", "#fffafa");
+    mColorNames.put("springgreen", "#00ff7f");
+    mColorNames.put("steelblue", "#4682b4");
+    mColorNames.put("tan", "#d2b48c");
+    mColorNames.put("teal", "#008080");
+    mColorNames.put("thistle", "#d8bfd8");
+    mColorNames.put("tomato", "#ff6347");
+    mColorNames.put("turquoise", "#40e0d0");
+    mColorNames.put("violet", "#ee82ee");
+    mColorNames.put("wheat", "#f5deb3");
+    mColorNames.put("white", "#ffffff");
+    mColorNames.put("whitesmoke", "#f5f5f5");
+    mColorNames.put("yellow", "#ffff00");
+    mColorNames.put("yellowgreen", "#9acd32");
+
+    mRelative.put("above", RelativeLayout.ABOVE);
+    mRelative.put("alignBaseline", RelativeLayout.ALIGN_BASELINE);
+    mRelative.put("alignBottom", RelativeLayout.ALIGN_BOTTOM);
+    mRelative.put("alignLeft", RelativeLayout.ALIGN_LEFT);
+    mRelative.put("alignParentBottom", RelativeLayout.ALIGN_PARENT_BOTTOM);
+    mRelative.put("alignParentLeft", RelativeLayout.ALIGN_PARENT_LEFT);
+    mRelative.put("alignParentRight", RelativeLayout.ALIGN_PARENT_RIGHT);
+    mRelative.put("alignParentTop", RelativeLayout.ALIGN_PARENT_TOP);
+    mRelative.put("alignRight", RelativeLayout.ALIGN_PARENT_RIGHT);
+    mRelative.put("alignTop", RelativeLayout.ALIGN_TOP);
+    // mRelative.put("alignWithParentIfMissing",RelativeLayout.); // No idea what this translates
+    // to.
+    mRelative.put("below", RelativeLayout.BELOW);
+    mRelative.put("centerHorizontal", RelativeLayout.CENTER_HORIZONTAL);
+    mRelative.put("centerInParent", RelativeLayout.CENTER_IN_PARENT);
+    mRelative.put("centerVertical", RelativeLayout.CENTER_VERTICAL);
+    mRelative.put("toLeftOf", RelativeLayout.LEFT_OF);
+    mRelative.put("toRightOf", RelativeLayout.RIGHT_OF);
+  }
+
+  public static XmlPullParserFactory getFactory() throws XmlPullParserException {
+    if (mFactory == null) {
+      mFactory = XmlPullParserFactory.newInstance();
+      mFactory.setNamespaceAware(true);
+    }
+    return mFactory;
+  }
+
+  public static XmlPullParser getXml() throws XmlPullParserException {
+    return getFactory().newPullParser();
+  }
+
+  public static XmlPullParser getXml(InputStream is) throws XmlPullParserException {
+    XmlPullParser xml = getXml();
+    xml.setInput(is, null);
+    return xml;
+  }
+
+  public static XmlPullParser getXml(Reader ir) throws XmlPullParserException {
+    XmlPullParser xml = getXml();
+    xml.setInput(ir);
+    return xml;
+  }
+
+  public View inflate(Activity context, XmlPullParser xml) throws XmlPullParserException,
+      IOException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
+    int event;
+    mContext = context;
+    mErrors.clear();
+    mMetrics = new DisplayMetrics();
+    context.getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
+    do {
+      event = xml.next();
+      if (event == XmlPullParser.END_DOCUMENT) {
+        return null;
+      }
+    } while (event != XmlPullParser.START_TAG);
+    View view = inflateView(context, xml, null);
+    return view;
+  }
+
+  private void addln(Object msg) {
+    Log.d(msg.toString());
+  }
+
+  @SuppressWarnings("rawtypes")
+  public void setClickListener(View v, android.view.View.OnClickListener listener,
+      OnItemClickListener itemListener) {
+    if (v.isClickable()) {
+
+      if (v instanceof AdapterView) {
+        try {
+          ((AdapterView) v).setOnItemClickListener(itemListener);
+        } catch (RuntimeException e) {
+          // Ignore this, not all controls support OnItemClickListener
+        }
+      }
+      try {
+        v.setOnClickListener(listener);
+      } catch (RuntimeException e) {
+        // And not all controls support OnClickListener.
+      }
+    }
+    if (v instanceof ViewGroup) {
+      ViewGroup vg = (ViewGroup) v;
+      for (int i = 0; i < vg.getChildCount(); i++) {
+        setClickListener(vg.getChildAt(i), listener, itemListener);
+      }
+    }
+  }
+
+  private View inflateView(Context context, XmlPullParser xml, ViewGroup root)
+      throws IllegalArgumentException, IllegalAccessException, InvocationTargetException,
+      XmlPullParserException, IOException {
+    View view = buildView(context, xml, root);
+    if (view == null) {
+      return view;
+    }
+    int event;
+    while ((event = xml.next()) != XmlPullParser.END_DOCUMENT) {
+      switch (event) {
+      case XmlPullParser.START_TAG:
+        if (view == null || view instanceof ViewGroup) {
+          inflateView(context, xml, (ViewGroup) view);
+        } else {
+          skipTag(xml); // Not really a view, probably, skip it.
+        }
+        break;
+      case XmlPullParser.END_TAG:
+        return view;
+      }
+    }
+    return view;
+  }
+
+  private void skipTag(XmlPullParser xml) throws XmlPullParserException, IOException {
+    int depth = xml.getDepth();
+    int event;
+    while ((event = xml.next()) != XmlPullParser.END_DOCUMENT) {
+      if (event == XmlPullParser.END_TAG && xml.getDepth() <= depth) {
+        break;
+      }
+    }
+  }
+
+  private View buildView(Context context, XmlPullParser xml, ViewGroup root)
+      throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
+    View view = viewClass(context, xml.getName());
+    if (view != null) {
+      getLayoutParams(view, root); // Make quite sure every view has a layout param.
+      for (int i = 0; i < xml.getAttributeCount(); i++) {
+        String ns = xml.getAttributeNamespace(i);
+        String attr = xml.getAttributeName(i);
+        if (ANDROID.equals(ns)) {
+          setProperty(view, root, attr, xml.getAttributeValue(i));
+        }
+      }
+      if (root != null) {
+        root.addView(view);
+      }
+    }
+
+    return view;
+  }
+
+  private int getLayoutValue(String value) {
+    if (value == null) {
+      return 0;
+    }
+    if (value.equals("match_parent")) {
+      return LayoutParams.MATCH_PARENT;
+    }
+    if (value.equals("wrap_content")) {
+      return LayoutParams.WRAP_CONTENT;
+    }
+    if (value.equals("fill_parent")) {
+      return LayoutParams.MATCH_PARENT;
+    }
+    return (int) getFontSize(value);
+  }
+
+  private float getFontSize(String value) {
+    int i;
+    float size;
+    String unit = "px";
+    for (i = 0; i < value.length(); i++) {
+      char c = value.charAt(i);
+      if (!(Character.isDigit(c) || c == '.')) {
+        break;
+      }
+    }
+    size = Float.parseFloat(value.substring(0, i));
+    if (i < value.length()) {
+      unit = value.substring(i).trim();
+    }
+    if (unit.equals("px")) {
+      return size;
+    }
+    if (unit.equals("sp")) {
+      return mMetrics.scaledDensity * size;
+    }
+    if (unit.equals("dp") || unit.equals("dip")) {
+      return mMetrics.density * size;
+    }
+    float inches = mMetrics.ydpi * size;
+    if (unit.equals("in")) {
+      return inches;
+    }
+    if (unit.equals("pt")) {
+      return inches / 72;
+    }
+    if (unit.equals("mm")) {
+      return (float) (inches / 2.54);
+    }
+    return 0;
+  }
+
+  private int calcId(String value) {
+    if (value == null) {
+      return 0;
+    }
+    if (value.startsWith("@+id/")) {
+      return tryGetId(value.substring(5));
+    }
+    if (value.startsWith("@id/")) {
+      return tryGetId(value.substring(4));
+    }
+    try {
+      return Integer.parseInt(value);
+    } catch (NumberFormatException e) {
+      return 0;
+    }
+  }
+
+  private int tryGetId(String value) {
+    Integer id = mIdList.get(value);
+    if (id == null) {
+      id = new Integer(mNextSeq++);
+      mIdList.put(value, id);
+    }
+    return id;
+  }
+
+  private LayoutParams getLayoutParams(View view, ViewGroup root) {
+    LayoutParams result = view.getLayoutParams();
+    if (result == null) {
+      result = createLayoutParams(root);
+      view.setLayoutParams(result);
+    }
+    return result;
+  }
+
+  private LayoutParams createLayoutParams(ViewGroup root) {
+    LayoutParams result = null;
+    if (root != null) {
+      try {
+        String lookfor = root.getClass().getName() + "$LayoutParams";
+        addln(lookfor);
+        Class<? extends LayoutParams> clazz = Class.forName(lookfor).asSubclass(LayoutParams.class);
+        if (clazz != null) {
+          Constructor<? extends LayoutParams> ct = clazz.getConstructor(int.class, int.class);
+          result = ct.newInstance(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+        }
+      } catch (Exception e) {
+        result = null;
+      }
+    }
+    if (result == null) {
+      result = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+    }
+    return result;
+  }
+
+  public void setProperty(View view, String attr, String value) {
+    try {
+      setProperty(view, (ViewGroup) view.getParent(), attr, value);
+    } catch (Exception e) {
+      mErrors.add(e.toString());
+    }
+  }
+
+  private void setProperty(View view, ViewGroup root, String attr, String value)
+      throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
+    addln(attr + ":" + value);
+    if (attr.startsWith("layout_")) {
+      setLayoutProperty(view, root, attr, value);
+    } else if (attr.equals("id")) {
+      view.setId(calcId(value));
+    } else if (attr.equals("gravity")) {
+      setInteger(view, attr, getInteger(Gravity.class, value));
+    } else if (attr.equals("width") || attr.equals("height")) {
+      setInteger(view, attr, (int) getFontSize(value));
+    } else if (attr.equals("inputType")) {
+      setInteger(view, attr, getInteger(InputType.class, value));
+    } else if (attr.equals("background")) {
+      setBackground(view, value);
+    } else if (attr.equals("digits") && view instanceof TextView) {
+      ((TextView) view).setKeyListener(DigitsKeyListener.getInstance(value));
+    } else if (attr.startsWith("nextFocus")) {
+      setInteger(view, attr + "Id", calcId(value));
+    } else if (attr.equals("padding")) {
+      int size = (int) getFontSize(value);
+      view.setPadding(size, size, size, size);
+    } else if (attr.equals("stretchColumns")) {
+      setStretchColumns(view, value);
+    } else if (attr.equals("textSize")) {
+      setFloat(view, attr, getFontSize(value));
+    } else if (attr.equals("textColor")) {
+      setInteger(view, attr, getColor(value));
+    } else if (attr.equals("textHighlightColor")) {
+      setInteger(view, "HighlightColor", getColor(value));
+    } else if (attr.equals("textColorHint")) {
+      setInteger(view, "LinkTextColor", getColor(value));
+    } else if (attr.equals("textStyle")) {
+      TextView textview = (TextView) view;
+      int style = getInteger(Typeface.class, value);
+      if (style == 0) {
+        textview.setTypeface(Typeface.DEFAULT);
+      } else {
+        textview.setTypeface(textview.getTypeface(), style);
+      }
+    } else if (attr.equals("typeface")) {
+      TextView textview = (TextView) view;
+      Typeface typeface = textview.getTypeface();
+      int style = typeface == null ? 0 : typeface.getStyle();
+      textview.setTypeface(Typeface.create(value, style));
+    } else if (attr.equals("src")) {
+      setImage(view, value);
+    } else {
+      setDynamicProperty(view, attr, value);
+    }
+  }
+
+  private void setStretchColumns(View view, String value) {
+    TableLayout table = (TableLayout) view;
+    String[] values = value.split(",");
+    for (String column : values) {
+      table.setColumnStretchable(Integer.parseInt(column), true);
+    }
+  }
+
+  private void setLayoutProperty(View view, ViewGroup root, String attr, String value) {
+    LayoutParams layout = getLayoutParams(view, root);
+    String layoutAttr = attr.substring(7);
+    if (layoutAttr.equals("width")) {
+      layout.width = getLayoutValue(value);
+    } else if (layoutAttr.equals("height")) {
+      layout.height = getLayoutValue(value);
+    } else if (layoutAttr.equals("gravity")) {
+      setIntegerField(layout, "gravity", getInteger(Gravity.class, value));
+    } else {
+      if (layoutAttr.startsWith("margin") && layout instanceof MarginLayoutParams) {
+        int size = (int) getFontSize(value);
+        MarginLayoutParams margins = (MarginLayoutParams) layout;
+        if (layoutAttr.equals("marginBottom")) {
+          margins.bottomMargin = size;
+        } else if (layoutAttr.equals("marginTop")) {
+          margins.topMargin = size;
+        } else if (layoutAttr.equals("marginLeft")) {
+          margins.leftMargin = size;
+        } else if (layoutAttr.equals("marginRight")) {
+          margins.rightMargin = size;
+        }
+      } else if (layout instanceof RelativeLayout.LayoutParams) {
+        int anchor = calcId(value);
+        if (anchor == 0) {
+          anchor = getInteger(RelativeLayout.class, value);
+        }
+        int rule = mRelative.get(layoutAttr);
+        ((RelativeLayout.LayoutParams) layout).addRule(rule, anchor);
+      } else {
+        setIntegerField(layout, layoutAttr, getInteger(layout.getClass(), value));
+      }
+    }
+  }
+
+  private void setBackground(View view, String value) {
+    if (value.startsWith("#")) {
+      view.setBackgroundColor(getColor(value));
+    } else if (value.startsWith("@")) {
+      setInteger(view, "backgroundResource", getInteger(view, value));
+    } else {
+      view.setBackground(getDrawable(value));
+    }
+  }
+
+  private Drawable getDrawable(String value) {
+    try {
+      Uri uri = Uri.parse(value);
+      if ("file".equals(uri.getScheme())) {
+        BitmapDrawable bd = new BitmapDrawable(mContext.getResources(), uri.getPath());
+        return bd;
+      }
+    } catch (Exception e) {
+      mErrors.add("failed to load drawable " + value);
+    }
+    return null;
+  }
+
+  private void setImage(View view, String value) {
+    if (value.startsWith("@")) {
+      setInteger(view, "imageResource", getInteger(view, value));
+    } else {
+      try {
+        Uri uri = Uri.parse(value);
+        if ("file".equals(uri.getScheme())) {
+          Bitmap bm = BitmapFactory.decodeFile(uri.getPath());
+          Method method = view.getClass().getMethod("setImageBitmap", Bitmap.class);
+          method.invoke(view, bm);
+        } else {
+          mErrors.add("Only 'file' currently supported for images");
+        }
+      } catch (Exception e) {
+        mErrors.add("failed to set image " + value);
+      }
+    }
+  }
+
+  private void setIntegerField(Object target, String fieldName, int value) {
+    try {
+      Field f = target.getClass().getField(fieldName);
+      f.setInt(target, value);
+    } catch (Exception e) {
+      mErrors.add("set field)" + fieldName + " failed. " + e.toString());
+    }
+  }
+
+  /** Expand single digit color to 2 digits. */
+  private int expandColor(String colorValue) {
+    return Integer.parseInt(colorValue + colorValue, 16);
+  }
+
+  private int getColor(String value) {
+    int a = 0xff, r = 0, g = 0, b = 0;
+    if (value.startsWith("#")) {
+      try {
+        value = value.substring(1);
+        if (value.length() == 4) {
+          a = expandColor(value.substring(0, 1));
+          value = value.substring(1);
+        }
+        if (value.length() == 3) {
+          r = expandColor(value.substring(0, 1));
+          g = expandColor(value.substring(1, 2));
+          b = expandColor(value.substring(2, 3));
+        } else {
+          if (value.length() == 8) {
+            a = Integer.parseInt(value.substring(0, 2), 16);
+            value = value.substring(2);
+          }
+          if (value.length() == 6) {
+            r = Integer.parseInt(value.substring(0, 2), 16);
+            g = Integer.parseInt(value.substring(2, 4), 16);
+            b = Integer.parseInt(value.substring(4, 6), 16);
+          }
+        }
+        long result = (a << 24) | (r << 16) | (g << 8) | b;
+        return (int) result;
+      } catch (Exception e) {
+      }
+    } else if (mColorNames.containsKey(value.toLowerCase())) {
+      return getColor(mColorNames.get(value.toLowerCase()));
+    }
+    mErrors.add("Unknown color " + value);
+    return 0;
+  }
+
+  private int getInputType(String value) {
+    int result = 0;
+    Integer v = getInputTypes().get(value);
+    if (v == null) {
+      mErrors.add("Unkown input type " + value);
+    } else {
+      result = v;
+    }
+    return result;
+  }
+
+  private void setInteger(View view, String attr, int value) {
+    String name = "set" + PCase(attr);
+    Method m;
+    try {
+      if ((m = tryMethod(view, name, Context.class, int.class)) != null) {
+        m.invoke(view, mContext, value);
+      } else if ((m = tryMethod(view, name, int.class)) != null) {
+        m.invoke(view, value);
+      }
+    } catch (Exception e) {
+      addln(name + ":" + value + ":" + e.toString());
+    }
+
+  }
+
+  private void setFloat(View view, String attr, float value) {
+    String name = "set" + PCase(attr);
+    Method m;
+    try {
+      if ((m = tryMethod(view, name, Context.class, float.class)) != null) {
+        m.invoke(view, mContext, value);
+      } else if ((m = tryMethod(view, name, float.class)) != null) {
+        m.invoke(view, value);
+      }
+    } catch (Exception e) {
+      addln(name + ":" + value + ":" + e.toString());
+    }
+
+  }
+
+  private void setDynamicProperty(View view, String attr, String value)
+      throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
+    String name = "set" + PCase(attr);
+    try {
+      Method m = tryMethod(view, name, CharSequence.class);
+      if (m != null) {
+        m.invoke(view, value);
+      } else if ((m = tryMethod(view, name, Context.class, int.class)) != null) {
+        m.invoke(view, mContext, getInteger(view, value));
+      } else if ((m = tryMethod(view, name, int.class)) != null) {
+        m.invoke(view, getInteger(view, value));
+      } else if ((m = tryMethod(view, name, float.class)) != null) {
+        m.invoke(view, Float.parseFloat(value));
+      } else if ((m = tryMethod(view, name, boolean.class)) != null) {
+        m.invoke(view, Boolean.parseBoolean(value));
+      } else if ((m = tryMethod(view, name, Object.class)) != null) {
+        m.invoke(view, value);
+      } else {
+        mErrors.add(view.getClass().getSimpleName() + ":" + attr + " Property not found.");
+      }
+    } catch (Exception e) {
+      addln(name + ":" + value + ":" + e.toString());
+      mErrors.add(name + ":" + value + ":" + e.toString());
+    }
+  }
+
+  private String PCase(String s) {
+    if (s == null) {
+      return null;
+    }
+    if (s.length() > 0) {
+      return s.substring(0, 1).toUpperCase() + s.substring(1);
+    }
+    return "";
+  }
+
+  private Method tryMethod(Object o, String name, Class<?>... parameters) {
+    Method result;
+    try {
+      result = o.getClass().getMethod(name, parameters);
+    } catch (Exception e) {
+      result = null;
+    }
+    return result;
+  }
+
+  public String camelCase(String s) {
+    if (s == null) {
+      return "";
+    } else if (s.length() < 2) {
+      return s.toUpperCase();
+    } else {
+      return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
+    }
+  }
+
+  private Integer getInteger(Class<?> clazz, String value) {
+    Integer result = null;
+    if (value.contains("|")) {
+      int work = 0;
+      for (String s : value.split("\\|")) {
+        work |= getInteger(clazz, s);
+      }
+      result = work;
+    } else {
+      if (value.startsWith("?")) {
+        result = parseTheme(value);
+      } else if (value.startsWith("@")) {
+        result = parseTheme(value);
+      } else if (value.startsWith("0x")) {
+        try {
+          result = (int) Long.parseLong(value.substring(2), 16);
+        } catch (NumberFormatException e) {
+          result = 0;
+        }
+      } else {
+        try {
+          result = Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+          if (clazz == InputType.class) {
+            return getInputType(value);
+          }
+          try {
+            Field f = clazz.getField(value.toUpperCase());
+            result = f.getInt(null);
+          } catch (Exception ex) {
+            mErrors.add("Unknown value: " + value);
+            result = 0;
+          }
+        }
+      }
+    }
+    return result;
+  }
+
+  private Integer getInteger(View view, String value) {
+    return getInteger(view.getClass(), value);
+  }
+
+  private Integer parseTheme(String value) {
+    int result;
+    try {
+      String query = "";
+      int i;
+      value = value.substring(1); // skip past "?"
+      i = value.indexOf(":");
+      if (i >= 0) {
+        query = value.substring(0, i) + ".";
+        value = value.substring(i + 1);
+      }
+      query += "R";
+      i = value.indexOf("/");
+      if (i >= 0) {
+        query += "$" + value.substring(0, i);
+        value = value.substring(i + 1);
+      }
+      Class<?> clazz = Class.forName(query);
+      Field f = clazz.getField(value);
+      result = f.getInt(null);
+    } catch (Exception e) {
+      result = 0;
+    }
+    return result;
+  }
+
+  private View viewClass(Context context, String name) {
+    View result = null;
+    result = viewClassTry(context, "android.view." + name);
+    if (result == null) {
+      result = viewClassTry(context, "android.widget." + name);
+    }
+    if (result == null) {
+      result = viewClassTry(context, name);
+    }
+    return result;
+  }
+
+  private View viewClassTry(Context context, String name) {
+    View result = null;
+    try {
+      Class<? extends View> viewclass = Class.forName(name).asSubclass(View.class);
+      if (viewclass != null) {
+        Constructor<? extends View> ct = viewclass.getConstructor(Context.class);
+        result = ct.newInstance(context);
+      }
+    } catch (Exception e) {
+    }
+    return result;
+
+  }
+
+  public Map<String, Integer> getIdList() {
+    return mIdList;
+  }
+
+  public List<String> getErrors() {
+    return mErrors;
+  }
+
+  public String getIdName(int id) {
+    for (String key : mIdList.keySet()) {
+      if (mIdList.get(key) == id) {
+        return key;
+      }
+    }
+    return null;
+  }
+
+  public int getId(String name) {
+    return mIdList.get(name);
+  }
+
+  public Map<String, Map<String, String>> getViewAsMap(View v) {
+    Map<String, Map<String, String>> result = new HashMap<String, Map<String, String>>();
+    for (Entry<String, Integer> entry : mIdList.entrySet()) {
+      View tmp = v.findViewById(entry.getValue());
+      if (tmp != null) {
+        result.put(entry.getKey(), getViewInfo(tmp));
+      }
+    }
+    return result;
+  }
+
+  public Map<String, String> getViewInfo(View v) {
+    Map<String, String> result = new HashMap<String, String>();
+    if (v.getId() != 0) {
+      result.put("id", getIdName(v.getId()));
+    }
+    result.put("type", v.getClass().getSimpleName());
+    addProperty(v, "text", result);
+    addProperty(v, "visibility", result);
+    addProperty(v, "checked", result);
+    addProperty(v, "tag", result);
+    addProperty(v, "selectedItemPosition", result);
+    addProperty(v, "progress", result);
+    return result;
+  }
+
+  private void addProperty(View v, String attr, Map<String, String> dest) {
+    String result = getProperty(v, attr);
+    if (result != null) {
+      dest.put(attr, result);
+    }
+  }
+
+  private String getProperty(View v, String attr) {
+    String name = PCase(attr);
+    Method m = tryMethod(v, "get" + name);
+    if (m == null) {
+      m = tryMethod(v, "is" + name);
+    }
+    String result = null;
+    if (m != null) {
+      try {
+        Object o = m.invoke(v);
+        if (o != null) {
+          result = o.toString();
+        }
+      } catch (Exception e) {
+        result = null;
+      }
+    }
+    return result;
+  }
+
+  public static Map<String, Integer> getInputTypes() {
+    if (mInputTypes.size() == 0) {
+      mInputTypes.put("none", 0x00000000);
+      mInputTypes.put("text", 0x00000001);
+      mInputTypes.put("textCapCharacters", 0x00001001);
+      mInputTypes.put("textCapWords", 0x00002001);
+      mInputTypes.put("textCapSentences", 0x00004001);
+      mInputTypes.put("textAutoCorrect", 0x00008001);
+      mInputTypes.put("textAutoComplete", 0x00010001);
+      mInputTypes.put("textMultiLine", 0x00020001);
+      mInputTypes.put("textImeMultiLine", 0x00040001);
+      mInputTypes.put("textNoSuggestions", 0x00080001);
+      mInputTypes.put("textUri", 0x00000011);
+      mInputTypes.put("textEmailAddress", 0x00000021);
+      mInputTypes.put("textEmailSubject", 0x00000031);
+      mInputTypes.put("textShortMessage", 0x00000041);
+      mInputTypes.put("textLongMessage", 0x00000051);
+      mInputTypes.put("textPersonName", 0x00000061);
+      mInputTypes.put("textPostalAddress", 0x00000071);
+      mInputTypes.put("textPassword", 0x00000081);
+      mInputTypes.put("textVisiblePassword", 0x00000091);
+      mInputTypes.put("textWebEditText", 0x000000a1);
+      mInputTypes.put("textFilter", 0x000000b1);
+      mInputTypes.put("textPhonetic", 0x000000c1);
+      mInputTypes.put("textWebEmailAddress", 0x000000d1);
+      mInputTypes.put("textWebPassword", 0x000000e1);
+      mInputTypes.put("number", 0x00000002);
+      mInputTypes.put("numberSigned", 0x00001002);
+      mInputTypes.put("numberDecimal", 0x00002002);
+      mInputTypes.put("numberPassword", 0x00000012);
+      mInputTypes.put("phone", 0x00000003);
+      mInputTypes.put("datetime", 0x00000004);
+      mInputTypes.put("date", 0x00000014);
+      mInputTypes.put("time", 0x00000024);
+    }
+    return mInputTypes;
+  }
+
+  /** Query class (typically R.id) to extract id names */
+  public void setIdList(Class<?> idClass) {
+    mIdList.clear();
+    for (Field f : idClass.getDeclaredFields()) {
+      try {
+        String name = f.getName();
+        int value = f.getInt(null);
+        mIdList.put(name, value);
+      } catch (Exception e) {
+        // Ignore
+      }
+    }
+  }
+
+  public void setListAdapter(View view, JSONArray items) {
+    List<String> list = new ArrayList<String>();
+    try {
+      for (int i = 0; i < items.length(); i++) {
+        list.add(items.get(i).toString());
+      }
+      ArrayAdapter<String> adapter;
+      if (view instanceof Spinner) {
+        adapter =
+            new ArrayAdapter<String>(mContext, android.R.layout.simple_spinner_item,
+                android.R.id.text1, list);
+      } else {
+        adapter =
+            new ArrayAdapter<String>(mContext, android.R.layout.simple_list_item_1,
+                android.R.id.text1, list);
+      }
+      adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+      Method m = tryMethod(view, "setAdapter", SpinnerAdapter.class);
+      if (m == null) {
+        m = view.getClass().getMethod("setAdapter", ListAdapter.class);
+      }
+      m.invoke(view, adapter);
+    } catch (Exception e) {
+      mErrors.add("failed to load list " + e.getMessage());
+    }
+  }
+
+  public void clearAll() {
+    getErrors().clear();
+    mIdList.clear();
+    mNextSeq = BASESEQ;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/wifi/HttpFacade.java b/Common/src/com/googlecode/android_scripting/facade/wifi/HttpFacade.java
new file mode 100644
index 0000000..75bf888
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/wifi/HttpFacade.java
@@ -0,0 +1,216 @@
+
+package com.googlecode.android_scripting.facade.wifi;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+
+/**
+ * Basic http operations.
+ */
+public class HttpFacade extends RpcReceiver {
+
+    private ServerSocket mServerSocket = null;
+    private int mServerTimeout = -1;
+    private HashMap<Integer, Socket> mSockets = null;
+    private int socketCnt = 0;
+
+    public HttpFacade(FacadeManager manager) throws IOException {
+        super(manager);
+        mSockets = new HashMap<Integer, Socket>();
+    }
+
+    private void inputStreamToOutputStream(InputStream in, OutputStream out) throws IOException {
+        if (in == null) {
+            Log.e("InputStream is null.");
+            return;
+        }
+        if (out == null) {
+            Log.e("OutputStream is null.");
+            return;
+        }
+        try {
+            int read = 0;
+            byte[] bytes = new byte[1024];
+            while ((read = in.read(bytes)) != -1) {
+                out.write(bytes, 0, read);
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            in.close();
+            out.close();
+        }
+
+    }
+
+    private String inputStreamToString(InputStream in) throws IOException {
+        BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
+        StringBuilder sb = new StringBuilder();
+        String str = null;
+        while ((str = r.readLine()) != null) {
+            sb.append(str);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Send an http request and get the response.
+     *
+     * @param url The url to send request to.
+     * @return The HttpURLConnection object.
+     */
+    private HttpURLConnection httpRequest(String url) throws IOException {
+        URL targetURL = new URL(url);
+        HttpURLConnection urlConnection = null;
+        try {
+            urlConnection = (HttpURLConnection) targetURL.openConnection();
+            urlConnection.connect();
+            int respCode = urlConnection.getResponseCode();
+            String respMsg = urlConnection.getResponseMessage();
+            Log.d("Got response code: " + respCode + " and response msg: " + respMsg);
+        } catch (IOException e) {
+            Log.e("Failed to open a connection to " + url);
+            Log.e(e.toString());
+        } finally {
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+        }
+        return urlConnection;
+    }
+
+    @Rpc(description = "Start waiting for a connection request on a specified port.",
+            returns = "The index of the connection.")
+    public Integer httpAcceptConnection(Integer port) throws IOException {
+        mServerSocket = new ServerSocket(port);
+        if (mServerTimeout > 0) {
+            mServerSocket.setSoTimeout(mServerTimeout);
+        }
+        Socket sock = mServerSocket.accept();
+        socketCnt += 1;
+        mSockets.put(socketCnt, sock);
+        return socketCnt;
+    }
+
+    @Rpc(description = "Download a file from specified url.")
+    public void httpDownloadFile(String url) throws IOException {
+        HttpURLConnection urlConnection = httpRequest(url);
+        String filename = null;
+        String contentDisposition = urlConnection.getHeaderField("Content-Disposition");
+        // Try to figure out the name of the file being downloaded.
+        // If the server returned a filename, use it.
+        if (contentDisposition != null) {
+            int idx = contentDisposition.toLowerCase().indexOf("filename");
+            if (idx != -1) {
+                filename = contentDisposition.substring(idx + 9);
+                Log.d("Using name returned by server: " + filename);
+            }
+        }
+        // If the server did not provide a filename to us, use the last part of url.
+        if (filename == null) {
+            int lastIdx = url.lastIndexOf('/');
+            filename = url.substring(lastIdx + 1);
+            Log.d("Using name from url: " + filename);
+        }
+        InputStream in = new BufferedInputStream(urlConnection.getInputStream());
+        String outPath = "/sdcard/Download/" + filename;
+        OutputStream output = new FileOutputStream(new File(outPath));
+        inputStreamToOutputStream(in, output);
+        Log.d("Downloaded file at " + outPath);
+    }
+
+    @Rpc(description = "Make an http request and return the response message.")
+    public HttpURLConnection httpPing(@RpcParameter(name = "url") String url) throws IOException {
+        try {
+            HttpURLConnection urlConnection = null;
+            urlConnection = httpRequest(url);
+            return urlConnection;
+        } catch (UnknownHostException e) {
+            return null;
+        }
+    }
+
+    @Rpc(description = "Make an http request and return the response content as a string.")
+    public String httpRequestString(@RpcParameter(name = "url") String url) throws IOException {
+        HttpURLConnection urlConnection = httpRequest(url);
+        InputStream in = new BufferedInputStream(urlConnection.getInputStream());
+        String result = inputStreamToString(in);
+        Log.d("Fetched: " + result);
+        return result;
+    }
+
+    @Rpc(description = "Set how many milliseconds to wait for an incoming connection.")
+    public void httpSetServerTimeout(@RpcParameter(name = "timeout") Integer timeout)
+            throws SocketException {
+        mServerSocket.setSoTimeout(timeout);
+        mServerTimeout = timeout;
+    }
+
+    @Rpc(description = "Ping to host(URL or IP), return success (true) or fail (false).")
+    // The optional timeout parameter is in unit of second.
+    public Boolean pingHost(@RpcParameter(name = "host") String hostString,
+            @RpcParameter(name = "timeout") @RpcOptional Integer timeout) {
+        try {
+            String host;
+            try {
+                URL url = new URL(hostString);
+                host = url.getHost();
+            } catch (java.net.MalformedURLException e) {
+                Log.d("hostString is not URL, it may be IP address.");
+                host = hostString;
+            }
+
+            Log.d("Host:" + host);
+            String pingCmdString = "ping -c 1 ";
+            if (timeout != null) {
+                pingCmdString = pingCmdString + "-W " + timeout + " ";
+            }
+            pingCmdString = pingCmdString + host;
+            Log.d("Execute command: " + pingCmdString);
+            Process p1 = java.lang.Runtime.getRuntime().exec(pingCmdString);
+            int returnVal = p1.waitFor();
+            boolean reachable = (returnVal == 0);
+            Log.d("Ping return Value:" + returnVal);
+            return reachable;
+        } catch (Exception e){
+            e.printStackTrace();
+            return false;
+        }
+        /*TODO see b/18899134 for more information.
+        */
+    }
+
+    @Override
+    public void shutdown() {
+        for (int key : mSockets.keySet()) {
+            Socket sock = mSockets.get(key);
+            try {
+                sock.close();
+            } catch (IOException e) {
+                Log.e("Failed to close socket " + key + " on port " + sock.getLocalPort());
+                e.printStackTrace();
+            }
+        }
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiManagerFacade.java
new file mode 100755
index 0000000..c0efe0f
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiManagerFacade.java
@@ -0,0 +1,887 @@
+
+package com.googlecode.android_scripting.facade.wifi;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectOutput;
+import java.io.ObjectOutputStream;
+import java.net.ConnectException;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.DhcpInfo;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiActivityEnergyInfo;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiConfiguration.AuthAlgorithm;
+import android.net.wifi.WifiConfiguration.KeyMgmt;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.WifiLock;
+import android.net.wifi.WpsInfo;
+import android.os.Bundle;
+import android.provider.Settings.Global;
+import android.provider.Settings.SettingNotFoundException;
+import android.util.Base64;
+
+/**
+ * WifiManager functions.
+ */
+// TODO: make methods handle various wifi states properly
+// e.g. wifi connection result will be null when flight mode is on
+public class WifiManagerFacade extends RpcReceiver {
+    private final static String mEventType = "WifiManager";
+    private final Service mService;
+    private final WifiManager mWifi;
+    private final EventFacade mEventFacade;
+
+    private final IntentFilter mScanFilter;
+    private final IntentFilter mStateChangeFilter;
+    private final IntentFilter mTetherFilter;
+    private final WifiScanReceiver mScanResultsAvailableReceiver;
+    private final WifiStateChangeReceiver mStateChangeReceiver;
+    private boolean mTrackingWifiStateChange;
+
+    private final BroadcastReceiver mTetherStateReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (WifiManager.WIFI_AP_STATE_CHANGED_ACTION.equals(action)) {
+                Log.d("Wifi AP state changed.");
+                int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_AP_STATE,
+                        WifiManager.WIFI_AP_STATE_FAILED);
+                if (state == WifiManager.WIFI_AP_STATE_ENABLED) {
+                    mEventFacade.postEvent("WifiManagerApEnabled", null);
+                } else if (state == WifiManager.WIFI_AP_STATE_DISABLED) {
+                    mEventFacade.postEvent("WifiManagerApDisabled", null);
+                }
+            } else if (ConnectivityManager.ACTION_TETHER_STATE_CHANGED.equals(action)) {
+                Log.d("Tether state changed.");
+                ArrayList<String> available = intent.getStringArrayListExtra(
+                        ConnectivityManager.EXTRA_AVAILABLE_TETHER);
+                ArrayList<String> active = intent.getStringArrayListExtra(
+                        ConnectivityManager.EXTRA_ACTIVE_TETHER);
+                ArrayList<String> errored = intent.getStringArrayListExtra(
+                        ConnectivityManager.EXTRA_ERRORED_TETHER);
+                Bundle msg = new Bundle();
+                msg.putStringArrayList("AVAILABLE_TETHER", available);
+                msg.putStringArrayList("ACTIVE_TETHER", active);
+                msg.putStringArrayList("ERRORED_TETHER", errored);
+                mEventFacade.postEvent("TetherStateChanged", msg);
+            }
+        }
+    };
+
+    private WifiLock mLock = null;
+    private boolean mIsConnected = false;
+
+    public WifiManagerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mWifi = (WifiManager) mService.getSystemService(Context.WIFI_SERVICE);
+        mEventFacade = manager.getReceiver(EventFacade.class);
+
+        mScanFilter = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+        mStateChangeFilter = new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION);
+        mStateChangeFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
+        mStateChangeFilter.addAction(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION);
+        mStateChangeFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1);
+
+        mTetherFilter = new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+        mTetherFilter.addAction(ConnectivityManager.ACTION_TETHER_STATE_CHANGED);
+
+        mScanResultsAvailableReceiver = new WifiScanReceiver(mEventFacade);
+        mStateChangeReceiver = new WifiStateChangeReceiver();
+        mTrackingWifiStateChange = false;
+    }
+
+    private void makeLock(int wifiMode) {
+        if (mLock == null) {
+            mLock = mWifi.createWifiLock(wifiMode, "sl4a");
+            mLock.acquire();
+        }
+    }
+
+    /**
+     * Handle Broadcast receiver for Scan Result
+     *
+     * @parm eventFacade Object of EventFacade
+     */
+    class WifiScanReceiver extends BroadcastReceiver {
+        private final EventFacade mEventFacade;
+
+        WifiScanReceiver(EventFacade eventFacade) {
+            mEventFacade = eventFacade;
+        }
+
+        @Override
+        public void onReceive(Context c, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
+                Bundle mResults = new Bundle();
+                Log.d("Wifi connection scan finished, results available.");
+                mResults.putLong("Timestamp", System.currentTimeMillis() / 1000);
+                mEventFacade.postEvent(mEventType + "ScanResultsAvailable", mResults);
+                mService.unregisterReceiver(mScanResultsAvailableReceiver);
+            }
+        }
+    }
+
+    class WifiActionListener implements WifiManager.ActionListener {
+        private final EventFacade mEventFacade;
+        private final String TAG;
+
+        public WifiActionListener(EventFacade eventFacade, String tag) {
+            mEventFacade = eventFacade;
+            this.TAG = tag;
+        }
+
+        @Override
+        public void onSuccess() {
+            Log.d("WifiActionListener  onSuccess called for " + mEventType + TAG + "OnSuccess");
+            mEventFacade.postEvent(mEventType + TAG + "OnSuccess", null);
+        }
+
+        @Override
+        public void onFailure(int reason) {
+            Log.d("WifiActionListener  onFailure called for" + mEventType);
+            Bundle msg = new Bundle();
+            msg.putInt("reason", reason);
+            mEventFacade.postEvent(mEventType + TAG + "OnFailure", msg);
+        }
+    }
+
+    public class WifiStateChangeReceiver extends BroadcastReceiver {
+        String mCachedWifiInfo = "";
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
+                Log.d("Wifi network state changed.");
+                NetworkInfo nInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
+                WifiInfo wInfo = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
+                Log.d("NetworkInfo " + nInfo);
+                Log.d("WifiInfo " + wInfo);
+                // If network info is of type wifi, send wifi events.
+                if (nInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+                    if (wInfo != null && nInfo.getDetailedState().equals(DetailedState.CONNECTED)) {
+                        String bssid = wInfo.getBSSID();
+                        if (bssid != null && !mCachedWifiInfo.equals(wInfo.toString())) {
+                            Log.d("WifiNetworkConnected");
+                            mEventFacade.postEvent("WifiNetworkConnected", wInfo);
+                        }
+                        mCachedWifiInfo = wInfo.toString();
+                    } else {
+                        if (nInfo.getDetailedState().equals(DetailedState.DISCONNECTED)) {
+                            if (!mCachedWifiInfo.equals("")) {
+                                mCachedWifiInfo = "";
+                                mEventFacade.postEvent("WifiNetworkDisconnected", null);
+                            }
+                        }
+                    }
+                }
+            } else if (action.equals(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION)) {
+                Log.d("Supplicant connection state changed.");
+                mIsConnected = intent
+                        .getBooleanExtra(WifiManager.EXTRA_SUPPLICANT_CONNECTED, false);
+                Bundle msg = new Bundle();
+                msg.putBoolean("Connected", mIsConnected);
+                mEventFacade.postEvent("SupplicantConnectionChanged", msg);
+            }
+        }
+    }
+
+    public class WifiWpsCallback extends WifiManager.WpsCallback {
+        private static final String tag = "WifiWps";
+
+        @Override
+        public void onStarted(String pin) {
+            Bundle msg = new Bundle();
+            msg.putString("pin", pin);
+            mEventFacade.postEvent(tag + "OnStarted", msg);
+        }
+
+        @Override
+        public void onSucceeded() {
+            Log.d("Wps op succeeded");
+            mEventFacade.postEvent(tag + "OnSucceeded", null);
+        }
+
+        @Override
+        public void onFailed(int reason) {
+            Bundle msg = new Bundle();
+            msg.putInt("reason", reason);
+            mEventFacade.postEvent(tag + "OnFailed", msg);
+        }
+    }
+
+    private void applyingkeyMgmt(WifiConfiguration config, ScanResult result) {
+        if (result.capabilities.contains("WEP")) {
+            config.allowedKeyManagement.set(KeyMgmt.NONE);
+            config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN);
+            config.allowedAuthAlgorithms.set(AuthAlgorithm.SHARED);
+        } else if (result.capabilities.contains("PSK")) {
+            config.allowedKeyManagement.set(KeyMgmt.WPA_PSK);
+        } else if (result.capabilities.contains("EAP")) {
+            // this is probably wrong, as we don't have a way to enter the enterprise config
+            config.allowedKeyManagement.set(KeyMgmt.WPA_EAP);
+            config.allowedKeyManagement.set(KeyMgmt.IEEE8021X);
+        } else {
+            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+        }
+    }
+
+    private WifiConfiguration genWifiConfig(JSONObject j) throws JSONException {
+        if (j == null) {
+            return null;
+        }
+        WifiConfiguration config = new WifiConfiguration();
+        if (j.has("SSID")) {
+            config.SSID = "\"" + j.getString("SSID") + "\"";
+        } else if (j.has("ssid")) {
+            config.SSID = "\"" + j.getString("ssid") + "\"";
+        }
+        if (j.has("password")) {
+            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
+            config.preSharedKey = "\"" + j.getString("password") + "\"";
+        } else {
+            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+        }
+        if (j.has("BSSID")) {
+            config.BSSID = j.getString("BSSID");
+        }
+        if (j.has("hiddenSSID")) {
+            config.hiddenSSID = j.getBoolean("hiddenSSID");
+        }
+        if (j.has("priority")) {
+            config.priority = j.getInt("priority");
+        }
+        if (j.has("apBand")) {
+            config.apBand = j.getInt("apBand");
+        }
+        if (j.has("preSharedKey")) {
+            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
+            config.preSharedKey = j.getString("preSharedKey");
+        }
+        if (j.has("wepKeys")) {
+            // Looks like we only support static WEP.
+            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+            config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN);
+            config.allowedAuthAlgorithms.set(AuthAlgorithm.SHARED);
+            JSONArray keys = j.getJSONArray("wepKeys");
+            String[] wepKeys = new String[keys.length()];
+            for (int i = 0; i < keys.length(); i++) {
+                wepKeys[i] = keys.getString(i);
+            }
+            config.wepKeys = wepKeys;
+        }
+        if (j.has("wepTxKeyIndex")) {
+            config.wepTxKeyIndex = j.getInt("wepTxKeyIndex");
+        }
+        return config;
+    }
+
+    private WifiConfiguration genWifiEnterpriseConfig(JSONObject j) throws JSONException,
+            GeneralSecurityException {
+        if (j == null) {
+            return null;
+        }
+        WifiConfiguration config = new WifiConfiguration();
+        config.allowedKeyManagement.set(KeyMgmt.WPA_EAP);
+        config.allowedKeyManagement.set(KeyMgmt.IEEE8021X);
+        if (j.has("SSID")) {
+            config.SSID = j.getString("SSID");
+        }
+        if (j.has("FQDN")) {
+            config.FQDN = j.getString("FQDN");
+        }
+        if (j.has("providerFriendlyName")) {
+            config.providerFriendlyName = j.getString("providerFriendlyName");
+        }
+        if (j.has("roamingConsortiumIds")) {
+            JSONArray ids = j.getJSONArray("roamingConsortiumIds");
+            long[] rIds = new long[ids.length()];
+            for (int i = 0; i < ids.length(); i++) {
+                rIds[i] = ids.getLong(i);
+            }
+            config.roamingConsortiumIds = rIds;
+        }
+        WifiEnterpriseConfig eConfig = new WifiEnterpriseConfig();
+        if (j.has(WifiEnterpriseConfig.EAP_KEY)) {
+            int eap = j.getInt(WifiEnterpriseConfig.EAP_KEY);
+            eConfig.setEapMethod(eap);
+        }
+        if (j.has(WifiEnterpriseConfig.PHASE2_KEY)) {
+            int p2Method = j.getInt(WifiEnterpriseConfig.PHASE2_KEY);
+            eConfig.setPhase2Method(p2Method);
+        }
+        if (j.has(WifiEnterpriseConfig.CA_CERT_KEY)) {
+            String certStr = j.getString(WifiEnterpriseConfig.CA_CERT_KEY);
+            Log.v("CA Cert String is " + certStr);
+            eConfig.setCaCertificate(strToX509Cert(certStr));
+        }
+        if (j.has(WifiEnterpriseConfig.CLIENT_CERT_KEY)
+                && j.has(WifiEnterpriseConfig.PRIVATE_KEY_ID_KEY)) {
+            String certStr = j.getString(WifiEnterpriseConfig.CLIENT_CERT_KEY);
+            String keyStr = j.getString(WifiEnterpriseConfig.PRIVATE_KEY_ID_KEY);
+            Log.v("Client Cert String is " + certStr);
+            Log.v("Client Key String is " + keyStr);
+            X509Certificate cert = strToX509Cert(certStr);
+            PrivateKey privKey = strToPrivateKey(keyStr);
+            Log.v("Cert is " + cert);
+            Log.v("Private Key is " + privKey);
+            eConfig.setClientKeyEntry(privKey, cert);
+        }
+        if (j.has(WifiEnterpriseConfig.IDENTITY_KEY)) {
+            String identity = j.getString(WifiEnterpriseConfig.IDENTITY_KEY);
+            Log.v("Setting identity to " + identity);
+            eConfig.setIdentity(identity);
+        }
+        if (j.has(WifiEnterpriseConfig.PASSWORD_KEY)) {
+            String pwd = j.getString(WifiEnterpriseConfig.PASSWORD_KEY);
+            Log.v("Setting password to " + pwd);
+            eConfig.setPassword(pwd);
+        }
+        if (j.has(WifiEnterpriseConfig.ALTSUBJECT_MATCH_KEY)) {
+            String altSub = j.getString(WifiEnterpriseConfig.ALTSUBJECT_MATCH_KEY);
+            Log.v("Setting Alt Subject to " + altSub);
+            eConfig.setAltSubjectMatch(altSub);
+        }
+        if (j.has(WifiEnterpriseConfig.DOM_SUFFIX_MATCH_KEY)) {
+            String domSuffix = j.getString(WifiEnterpriseConfig.DOM_SUFFIX_MATCH_KEY);
+            Log.v("Setting Domain Suffix Match to " + domSuffix);
+            eConfig.setDomainSuffixMatch(domSuffix);
+        }
+        if (j.has(WifiEnterpriseConfig.REALM_KEY)) {
+            String realm = j.getString(WifiEnterpriseConfig.REALM_KEY);
+            Log.v("Setting Domain Suffix Match to " + realm);
+            eConfig.setRealm(realm);
+        }
+        config.enterpriseConfig = eConfig;
+        return config;
+    }
+
+    private boolean matchScanResult(ScanResult result, String id) {
+        if (result.BSSID.equals(id) || result.SSID.equals(id)) {
+            return true;
+        }
+        return false;
+    }
+
+    private WpsInfo parseWpsInfo(String infoStr) throws JSONException {
+        if (infoStr == null) {
+            return null;
+        }
+        JSONObject j = new JSONObject(infoStr);
+        WpsInfo info = new WpsInfo();
+        if (j.has("setup")) {
+            info.setup = j.getInt("setup");
+        }
+        if (j.has("BSSID")) {
+            info.BSSID = j.getString("BSSID");
+        }
+        if (j.has("pin")) {
+            info.pin = j.getString("pin");
+        }
+        return info;
+    }
+
+    private byte[] base64StrToBytes(String input) {
+        return Base64.decode(input, Base64.DEFAULT);
+    }
+
+    private X509Certificate strToX509Cert(String certStr) throws CertificateException {
+        byte[] certBytes = base64StrToBytes(certStr);
+        InputStream certStream = new ByteArrayInputStream(certBytes);
+        CertificateFactory cf = CertificateFactory.getInstance("X509");
+        return (X509Certificate) cf.generateCertificate(certStream);
+    }
+
+    private PrivateKey strToPrivateKey(String key) throws NoSuchAlgorithmException,
+            InvalidKeySpecException {
+        byte[] keyBytes = base64StrToBytes(key);
+        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
+        KeyFactory fact = KeyFactory.getInstance("RSA");
+        PrivateKey priv = fact.generatePrivate(keySpec);
+        return priv;
+    }
+
+    private PublicKey strToPublicKey(String key) throws NoSuchAlgorithmException,
+            InvalidKeySpecException {
+        byte[] keyBytes = base64StrToBytes(key);
+        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
+        KeyFactory fact = KeyFactory.getInstance("RSA");
+        PublicKey pub = fact.generatePublic(keySpec);
+        return pub;
+    }
+
+    private WifiConfiguration wifiConfigurationFromScanResult(ScanResult result) {
+        if (result == null)
+            return null;
+        WifiConfiguration config = new WifiConfiguration();
+        config.SSID = "\"" + result.SSID + "\"";
+        applyingkeyMgmt(config, result);
+        config.BSSID = result.BSSID;
+        return config;
+    }
+
+    @Rpc(description = "test.")
+    public String wifiTest(String certString) throws CertificateException, IOException {
+        // TODO(angli): Make this work. Convert a X509Certificate back to a string.
+        X509Certificate caCert = strToX509Cert(certString);
+        caCert.getEncoded();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ObjectOutput out = new ObjectOutputStream(bos);
+        out.writeObject(caCert);
+        byte[] data = bos.toByteArray();
+        bos.close();
+        return Base64.encodeToString(data, Base64.DEFAULT);
+    }
+
+    @Rpc(description = "Add a network.")
+    public Integer wifiAddNetwork(@RpcParameter(name = "wifiConfig") JSONObject wifiConfig)
+            throws JSONException {
+        return mWifi.addNetwork(genWifiConfig(wifiConfig));
+    }
+
+    @Rpc(description = "Builds a WifiConfiguration from Hotspot 2.0 MIME file.")
+    public WifiConfiguration wifiBuildConfig(
+            @RpcParameter(name = "uriString") String uriString,
+            @RpcParameter(name = "mimeType") String mimeType,
+            String dataString)
+                    throws JSONException {
+        byte[] data = base64StrToBytes(dataString);
+        return mWifi.buildWifiConfig(uriString, mimeType, data);
+    }
+
+    @Rpc(description = "Cancel Wi-fi Protected Setup.")
+    public void wifiCancelWps() throws JSONException {
+        WifiWpsCallback listener = new WifiWpsCallback();
+        mWifi.cancelWps(listener);
+    }
+
+    @Rpc(description = "Checks Wifi state.", returns = "True if Wifi is enabled.")
+    public Boolean wifiCheckState() {
+        return mWifi.getWifiState() == WifiManager.WIFI_STATE_ENABLED;
+    }
+
+    /**
+     * Connects to a WPA protected wifi network
+     *
+     * @param wifiSSID SSID of the wifi network
+     * @param wifiPassword password for the wifi network
+     * @return true on success
+     * @throws ConnectException
+     * @throws JSONException
+     */
+    @Rpc(description = "Connects a wifi network by ssid", returns = "True if the operation succeeded.")
+    public Boolean wifiConnect(@RpcParameter(name = "config") JSONObject config)
+            throws ConnectException, JSONException {
+        WifiConfiguration wifiConfig = genWifiConfig(config);
+        int nId = mWifi.addNetwork(wifiConfig);
+        if (nId < 0) {
+            Log.e("Got negative network Id.");
+            return false;
+        }
+        mWifi.disconnect();
+        mWifi.enableNetwork(nId, true);
+        return mWifi.reconnect();
+    }
+
+    @Rpc(description = "Disconnects from the currently active access point.", returns = "True if the operation succeeded.")
+    public Boolean wifiDisconnect() {
+        return mWifi.disconnect();
+    }
+
+    @Rpc(description = "Enable/disable autojoin scan and switch network when connected.")
+    public Boolean wifiEnableAutoJoinWhenAssociated(@RpcParameter(name = "enable") Boolean enable) {
+        return mWifi.enableAutoJoinWhenAssociated(enable);
+    }
+
+    @Rpc(description = "Enable a configured network. Initiate a connection if disableOthers is true", returns = "True if the operation succeeded.")
+    public Boolean wifiEnableNetwork(@RpcParameter(name = "netId") Integer netId,
+            @RpcParameter(name = "disableOthers") Boolean disableOthers) {
+        return mWifi.enableNetwork(netId, disableOthers);
+    }
+
+    @Rpc(description = "Enable WiFi verbose logging.")
+    public void wifiEnableVerboseLogging(@RpcParameter(name = "level") Integer level) {
+        mWifi.enableVerboseLogging(level);
+    }
+
+    @Rpc(description = "Connect to a wifi network that uses Enterprise authentication methods.")
+    public void wifiEnterpriseConnect(@RpcParameter(name = "config") JSONObject config)
+            throws JSONException, GeneralSecurityException {
+        // Create Certificate
+        WifiActionListener listener = new WifiActionListener(mEventFacade, "EnterpriseConnect");
+        WifiConfiguration wifiConfig = genWifiEnterpriseConfig(config);
+        if (wifiConfig.isPasspoint()) {
+            Log.d("Got a passpoint config, add it and save config.");
+            mWifi.addNetwork(wifiConfig);
+            mWifi.saveConfiguration();
+        } else {
+            Log.d("Got a non-passpoint enterprise config, connect directly.");
+            mWifi.connect(wifiConfig, listener);
+        }
+    }
+
+    @Rpc(description = "Resets all WifiManager settings.")
+    public void wifiFactoryReset() {
+        mWifi.factoryReset();
+    }
+
+    /**
+     * Forget a wifi network with priority
+     *
+     * @param networkID Id of wifi network
+     */
+    @Rpc(description = "Forget a wifi network with priority")
+    public void wifiForgetNetwork(@RpcParameter(name = "wifiSSID") Integer newtorkId) {
+        WifiActionListener listener = new WifiActionListener(mEventFacade, "ForgetNetwork");
+        mWifi.forget(newtorkId, listener);
+    }
+
+    @Rpc(description = "Gets the Wi-Fi AP Configuration.")
+    public WifiConfiguration wifiGetApConfiguration() {
+        return mWifi.getWifiApConfiguration();
+    }
+
+    @Rpc(description = "Returns the file in which IP and proxy configuration data is stored.")
+    public String wifiGetConfigFile() {
+        return mWifi.getConfigFile();
+    }
+
+    @Rpc(description = "Return a list of all the configured wifi networks.")
+    public List<WifiConfiguration> wifiGetConfiguredNetworks() {
+        return mWifi.getConfiguredNetworks();
+    }
+
+    @Rpc(description = "Returns information about the currently active access point.")
+    public WifiInfo wifiGetConnectionInfo() {
+        return mWifi.getConnectionInfo();
+    }
+
+    @Rpc(description = "Returns wifi activity and energy usage info.")
+    public WifiActivityEnergyInfo wifiGetControllerActivityEnergyInfo() {
+        return mWifi.getControllerActivityEnergyInfo(0);
+    }
+
+    @Rpc(description = "Get the country code used by WiFi.")
+    public String wifiGetCountryCode() {
+        return mWifi.getCountryCode();
+    }
+
+    @Rpc(description = "Get the current network.")
+    public Network wifiGetCurrentNetwork() {
+        return mWifi.getCurrentNetwork();
+    }
+
+    @Rpc(description = "Get the info from last successful DHCP request.")
+    public DhcpInfo wifiGetDhcpInfo() {
+        return mWifi.getDhcpInfo();
+    }
+
+    @Rpc(description = "Get setting for Framework layer autojoin enable status.")
+    public Boolean wifiGetEnableAutoJoinWhenAssociated() {
+        return mWifi.getEnableAutoJoinWhenAssociated();
+    }
+
+    @Rpc(description = "Returns 1 if autojoin offload thru Wifi HAL layer is enabled, 0 otherwise.")
+    public Integer wifiGetHalBasedAutojoinOffload() {
+        return mWifi.getHalBasedAutojoinOffload();
+    }
+
+    @Rpc(description = "Get privileged configured networks.")
+    public List<WifiConfiguration> wifiGetPrivilegedConfiguredNetworks() {
+        return mWifi.getPrivilegedConfiguredNetworks();
+    }
+
+    @Rpc(description = "Returns the list of access points found during the most recent Wifi scan.")
+    public List<ScanResult> wifiGetScanResults() {
+        return mWifi.getScanResults();
+    }
+
+    @Rpc(description = "Get the current level of WiFi verbose logging.")
+    public Integer wifiGetVerboseLoggingLevel() {
+        return mWifi.getVerboseLoggingLevel();
+    }
+
+    @Rpc(description = "true if this adapter supports 5 GHz band.")
+    public Boolean wifiIs5GHzBandSupported() {
+        return mWifi.is5GHzBandSupported();
+    }
+
+    @Rpc(description = "true if this adapter supports multiple simultaneous connections.")
+    public Boolean wifiIsAdditionalStaSupported() {
+        return mWifi.isAdditionalStaSupported();
+    }
+
+    @Rpc(description = "Return whether Wi-Fi AP is enabled or disabled.")
+    public Boolean wifiIsApEnabled() {
+        return mWifi.isWifiApEnabled();
+    }
+
+    @Rpc(description = "Check if Device-to-AP RTT is supported.")
+    public Boolean wifiIsDeviceToApRttSupported() {
+        return mWifi.isDeviceToApRttSupported();
+    }
+
+    @Rpc(description = "Check if Device-to-device RTT is supported.")
+    public Boolean wifiIsDeviceToDeviceRttSupported() {
+        return mWifi.isDeviceToDeviceRttSupported();
+    }
+
+    @Rpc(description = "Check if the chipset supports dual frequency band (2.4 GHz and 5 GHz).")
+    public Boolean wifiIsDualBandSupported() {
+        return mWifi.isDualBandSupported();
+    }
+
+    @Rpc(description = "Check if this adapter supports advanced power/performance counters.")
+    public Boolean wifiIsEnhancedPowerReportingSupported() {
+        return mWifi.isEnhancedPowerReportingSupported();
+    }
+
+    @Rpc(description = "Check if multicast is enabled.")
+    public Boolean wifiIsMulticastEnabled() {
+        return mWifi.isMulticastEnabled();
+    }
+
+    @Rpc(description = "true if this adapter supports Neighbour Awareness Network APIs.")
+    public Boolean wifiIsNanSupported() {
+        return mWifi.isNanSupported();
+    }
+
+    @Rpc(description = "true if this adapter supports Off Channel Tunnel Directed Link Setup.")
+    public Boolean wifiIsOffChannelTdlsSupported() {
+        return mWifi.isOffChannelTdlsSupported();
+    }
+
+    @Rpc(description = "true if this adapter supports WifiP2pManager (Wi-Fi Direct).")
+    public Boolean wifiIsP2pSupported() {
+        return mWifi.isP2pSupported();
+    }
+
+    @Rpc(description = "true if this adapter supports passpoint.")
+    public Boolean wifiIsPasspointSupported() {
+        return mWifi.isPasspointSupported();
+    }
+
+    @Rpc(description = "true if this adapter supports portable Wi-Fi hotspot.")
+    public Boolean wifiIsPortableHotspotSupported() {
+        return mWifi.isPortableHotspotSupported();
+    }
+
+    @Rpc(description = "true if this adapter supports offloaded connectivity scan.")
+    public Boolean wifiIsPreferredNetworkOffloadSupported() {
+        return mWifi.isPreferredNetworkOffloadSupported();
+    }
+
+    @Rpc(description = "Check if wifi scanner is supported on this device.")
+    public Boolean wifiIsScannerSupported() {
+        return mWifi.isWifiScannerSupported();
+    }
+
+    @Rpc(description = "Check if tdls is supported on this device.")
+    public Boolean wifiIsTdlsSupported() {
+        return mWifi.isTdlsSupported();
+    }
+
+    @Rpc(description = "Acquires a full Wifi lock.")
+    public void wifiLockAcquireFull() {
+        makeLock(WifiManager.WIFI_MODE_FULL);
+    }
+
+    @Rpc(description = "Acquires a scan only Wifi lock.")
+    public void wifiLockAcquireScanOnly() {
+        makeLock(WifiManager.WIFI_MODE_SCAN_ONLY);
+    }
+
+    @Rpc(description = "Releases a previously acquired Wifi lock.")
+    public void wifiLockRelease() {
+        if (mLock != null) {
+            mLock.release();
+            mLock = null;
+        }
+    }
+
+    /**
+     * Connects to a wifi network with priority
+     *
+     * @param wifiSSID SSID of the wifi network
+     * @param wifiPassword password for the wifi network
+     * @throws JSONException
+     */
+    @Rpc(description = "Connects a wifi network as priority by pasing ssid")
+    public void wifiPriorityConnect(@RpcParameter(name = "config") JSONObject config)
+            throws JSONException {
+        WifiConfiguration wifiConfig = genWifiConfig(config);
+        WifiActionListener listener = new WifiActionListener(mEventFacade, "PriorityConnect");
+        mWifi.connect(wifiConfig, listener);
+    }
+
+    @Rpc(description = "Reassociates with the currently active access point.", returns = "True if the operation succeeded.")
+    public Boolean wifiReassociate() {
+        return mWifi.reassociate();
+    }
+
+    @Rpc(description = "Reconnects to the currently active access point.", returns = "True if the operation succeeded.")
+    public Boolean wifiReconnect() {
+        return mWifi.reconnect();
+    }
+
+    @Rpc(description = "Remove a configured network.", returns = "True if the operation succeeded.")
+    public Boolean wifiRemoveNetwork(@RpcParameter(name = "netId") Integer netId) {
+        return mWifi.removeNetwork(netId);
+    }
+
+    @Rpc(description = "Start/stop wifi soft AP.")
+    public Boolean wifiSetApEnabled(
+            @RpcParameter(name = "enable") Boolean enable,
+            @RpcParameter(name = "configJson") JSONObject configJson) throws JSONException {
+        int wifiState = mWifi.getWifiState();
+        if (enable) {
+            if ((wifiState == WifiManager.WIFI_STATE_ENABLING) ||
+                    (wifiState == WifiManager.WIFI_STATE_ENABLED)) {
+                mWifi.setWifiEnabled(false);
+            }
+            WifiConfiguration config = genWifiConfig(configJson);
+            // Need to strip of extra quotation marks for SSID and password.
+            String ssid = config.SSID;
+            if (ssid != null) {
+                config.SSID = ssid.substring(1, ssid.length() - 1);
+            }
+            String pwd = config.preSharedKey;
+            if (pwd != null) {
+                config.preSharedKey = pwd.substring(1, pwd.length() - 1);
+            }
+            return mWifi.setWifiApEnabled(config, enable);
+        } else {
+            return mWifi.setWifiApEnabled(null, false);
+        }
+    }
+
+    @Rpc(description = "Set the country code used by WiFi.")
+    public void wifiSetCountryCode(
+            @RpcParameter(name = "country") String country,
+            @RpcParameter(name = "persist") Boolean persist) {
+        mWifi.setCountryCode(country, persist);
+    }
+
+    @Rpc(description = "Enable/disable autojoin offload through Wifi HAL layer.")
+    public void wifiSetHalBasedAutojoinOffload(
+            @RpcParameter(name = "enable") Integer enable) {
+        mWifi.setHalBasedAutojoinOffload(enable);
+    }
+
+    @Rpc(description = "Enable/disable tdls with a mac address.")
+    public void wifiSetTdlsEnabledWithMacAddress(
+            @RpcParameter(name = "remoteMacAddress") String remoteMacAddress,
+            @RpcParameter(name = "enable") Boolean enable) {
+        mWifi.setTdlsEnabledWithMacAddress(remoteMacAddress, enable);
+    }
+
+    @Rpc(description = "Starts a scan for Wifi access points.", returns = "True if the scan was initiated successfully.")
+    public Boolean wifiStartScan() {
+        mService.registerReceiver(mScanResultsAvailableReceiver, mScanFilter);
+        return mWifi.startScan();
+    }
+
+    @Rpc(description = "Start Wi-fi Protected Setup.")
+    public void wifiStartWps(
+            @RpcParameter(name = "config", description = "A json string with fields \"setup\", \"BSSID\", and \"pin\"") String config)
+                    throws JSONException {
+        WpsInfo info = parseWpsInfo(config);
+        WifiWpsCallback listener = new WifiWpsCallback();
+        Log.d("Starting wps with: " + info);
+        mWifi.startWps(info, listener);
+    }
+
+    @Rpc(description = "Start listening for wifi state change related broadcasts.")
+    public void wifiStartTrackingStateChange() {
+        mService.registerReceiver(mStateChangeReceiver, mStateChangeFilter);
+        mService.registerReceiver(mTetherStateReceiver, mTetherFilter);
+        mTrackingWifiStateChange = true;
+    }
+
+    @Rpc(description = "Stop listening for wifi state change related broadcasts.")
+    public void wifiStopTrackingStateChange() {
+        if (mTrackingWifiStateChange == true) {
+            mService.unregisterReceiver(mTetherStateReceiver);
+            mService.unregisterReceiver(mStateChangeReceiver);
+            mTrackingWifiStateChange = false;
+        }
+    }
+
+    @Rpc(description = "Toggle Wifi on and off.", returns = "True if Wifi is enabled.")
+    public Boolean wifiToggleState(@RpcParameter(name = "enabled") @RpcOptional Boolean enabled) {
+        if (enabled == null) {
+            enabled = !wifiCheckState();
+        }
+        mWifi.setWifiEnabled(enabled);
+        return enabled;
+    }
+
+    @Rpc(description = "Toggle Wifi scan always available on and off.", returns = "True if Wifi scan is always available.")
+    public Boolean wifiToggleScanAlwaysAvailable(
+            @RpcParameter(name = "enabled") @RpcOptional Boolean enabled)
+                    throws SettingNotFoundException {
+        ContentResolver cr = mService.getContentResolver();
+        int isSet = 0;
+        if (enabled == null) {
+            isSet = Global.getInt(cr, Global.WIFI_SCAN_ALWAYS_AVAILABLE);
+            isSet ^= 1;
+        } else if (enabled == true) {
+            isSet = 1;
+        }
+        Global.putInt(cr, Global.WIFI_SCAN_ALWAYS_AVAILABLE, isSet);
+        if (isSet == 1) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void shutdown() {
+        wifiLockRelease();
+        if (mTrackingWifiStateChange == true) {
+            wifiStopTrackingStateChange();
+        }
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiNanManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiNanManagerFacade.java
new file mode 100644
index 0000000..5863e8d
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiNanManagerFacade.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.wifi;
+
+import android.app.Service;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.wifi.nan.ConfigRequest;
+import android.net.wifi.nan.PublishData;
+import android.net.wifi.nan.PublishSettings;
+import android.net.wifi.nan.SubscribeData;
+import android.net.wifi.nan.SubscribeSettings;
+import android.net.wifi.nan.TlvBufferUtils;
+import android.net.wifi.nan.WifiNanEventListener;
+import android.net.wifi.nan.WifiNanManager;
+import android.net.wifi.nan.WifiNanSession;
+import android.net.wifi.nan.WifiNanSessionListener;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.RemoteException;
+
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * WifiNanManager functions.
+ */
+public class WifiNanManagerFacade extends RpcReceiver {
+    private final Service mService;
+    private final EventFacade mEventFacade;
+
+    private WifiNanManager mMgr;
+    private WifiNanSession mSession;
+    private HandlerThread mNanFacadeThread;
+    private ConnectivityManager mConnMgr;
+
+    private static TlvBufferUtils.TlvConstructor getFilterData(JSONObject j) throws JSONException {
+        if (j == null) {
+            return null;
+        }
+
+        TlvBufferUtils.TlvConstructor constructor = new TlvBufferUtils.TlvConstructor(0, 1);
+        constructor.allocate(255);
+
+        if (j.has("int0")) {
+            constructor.putShort(0, (short) j.getInt("int0"));
+        }
+
+        if (j.has("int1")) {
+            constructor.putShort(0, (short) j.getInt("int1"));
+        }
+
+        if (j.has("data0")) {
+            constructor.putString(0, j.getString("data0"));
+        }
+
+        if (j.has("data1")) {
+            constructor.putString(0, j.getString("data1"));
+        }
+
+        return constructor;
+    }
+
+    private static ConfigRequest getConfigRequest(JSONObject j) throws JSONException {
+        if (j == null) {
+            return null;
+        }
+
+        ConfigRequest.Builder builder = new ConfigRequest.Builder();
+
+        if (j.has("Support5gBand")) {
+            builder.setSupport5gBand(j.getBoolean("Support5gBand"));
+        }
+        if (j.has("MasterPreference")) {
+            builder.setMasterPreference(j.getInt("MasterPreference"));
+        }
+        if (j.has("ClusterLow")) {
+            builder.setClusterLow(j.getInt("ClusterLow"));
+        }
+        if (j.has("ClusterHigh")) {
+            builder.setClusterHigh(j.getInt("ClusterHigh"));
+        }
+
+        return builder.build();
+    }
+
+    private static PublishData getPublishData(JSONObject j) throws JSONException {
+        if (j == null) {
+            return null;
+        }
+
+        PublishData.Builder builder = new PublishData.Builder();
+
+        if (j.has("ServiceName")) {
+            builder.setServiceName(j.getString("ServiceName"));
+        }
+
+        if (j.has("ServiceSpecificInfo")) {
+            String ssi = j.getString("ServiceSpecificInfo");
+            builder.setServiceSpecificInfo(ssi.getBytes(), ssi.length());
+        }
+
+        if (j.has("TxFilter")) {
+            TlvBufferUtils.TlvConstructor constructor = getFilterData(j.getJSONObject("TxFilter"));
+            builder.setTxFilter(constructor.getArray(), constructor.getActualLength());
+        }
+
+        if (j.has("RxFilter")) {
+            TlvBufferUtils.TlvConstructor constructor = getFilterData(j.getJSONObject("RxFilter"));
+            builder.setRxFilter(constructor.getArray(), constructor.getActualLength());
+        }
+
+        return builder.build();
+    }
+
+    private static PublishSettings getPublishSettings(JSONObject j) throws JSONException {
+        if (j == null) {
+            return null;
+        }
+
+        PublishSettings.Builder builder = new PublishSettings.Builder();
+
+        if (j.has("PublishType")) {
+            builder.setPublishType(j.getInt("PublishType"));
+        }
+        if (j.has("PublishCount")) {
+            builder.setPublishCount(j.getInt("PublishCount"));
+        }
+        if (j.has("TtlSec")) {
+            builder.setTtlSec(j.getInt("TtlSec"));
+        }
+
+        return builder.build();
+    }
+
+    private static SubscribeData getSubscribeData(JSONObject j) throws JSONException {
+        if (j == null) {
+            return null;
+        }
+
+        SubscribeData.Builder builder = new SubscribeData.Builder();
+
+        if (j.has("ServiceName")) {
+            builder.setServiceName(j.getString("ServiceName"));
+        }
+
+        if (j.has("ServiceSpecificInfo")) {
+            String ssi = j.getString("ServiceSpecificInfo");
+            builder.setServiceSpecificInfo(ssi);
+        }
+
+        if (j.has("TxFilter")) {
+            TlvBufferUtils.TlvConstructor constructor = getFilterData(j.getJSONObject("TxFilter"));
+            builder.setTxFilter(constructor.getArray(), constructor.getActualLength());
+        }
+
+        if (j.has("RxFilter")) {
+            TlvBufferUtils.TlvConstructor constructor = getFilterData(j.getJSONObject("RxFilter"));
+            builder.setRxFilter(constructor.getArray(), constructor.getActualLength());
+        }
+
+        return builder.build();
+    }
+
+    private static SubscribeSettings getSubscribeSettings(JSONObject j) throws JSONException {
+        if (j == null) {
+            return null;
+        }
+
+        SubscribeSettings.Builder builder = new SubscribeSettings.Builder();
+
+        if (j.has("SubscribeType")) {
+            builder.setSubscribeType(j.getInt("SubscribeType"));
+        }
+        if (j.has("SubscribeCount")) {
+            builder.setSubscribeCount(j.getInt("SubscribeCount"));
+        }
+        if (j.has("TtlSec")) {
+            builder.setTtlSec(j.getInt("TtlSec"));
+        }
+
+        return builder.build();
+    }
+
+    public WifiNanManagerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+
+        mNanFacadeThread = new HandlerThread("nanFacadeThread");
+        mNanFacadeThread.start();
+
+        mMgr = (WifiNanManager) mService.getSystemService(Context.WIFI_NAN_SERVICE);
+        mMgr.connect(new NanEventListenerPostsEvents(mNanFacadeThread.getLooper()),
+                WifiNanEventListener.LISTEN_CONFIG_COMPLETED
+                        | WifiNanEventListener.LISTEN_CONFIG_FAILED
+                        | WifiNanEventListener.LISTEN_NAN_DOWN
+                        | WifiNanEventListener.LISTEN_IDENTITY_CHANGED);
+
+        mConnMgr = (ConnectivityManager) mService.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+        mEventFacade = manager.getReceiver(EventFacade.class);
+    }
+
+    @Override
+    public void shutdown() {
+    }
+
+    @Rpc(description = "Start NAN.")
+    public void wifiNanEnable(@RpcParameter(name = "nanConfig") JSONObject nanConfig)
+            throws RemoteException, JSONException {
+        mMgr.requestConfig(getConfigRequest(nanConfig));
+    }
+
+    @Rpc(description = "Stop NAN.")
+    public void wifiNanDisable() throws RemoteException, JSONException {
+        mMgr.disconnect();
+    }
+
+    @Rpc(description = "Publish.")
+    public void wifiNanPublish(@RpcParameter(name = "publishData") JSONObject publishData,
+            @RpcParameter(name = "publishSettings") JSONObject publishSettings,
+            @RpcParameter(name = "listenerId") Integer listenerId)
+                    throws RemoteException, JSONException {
+        mSession = mMgr.publish(getPublishData(publishData), getPublishSettings(publishSettings),
+                new NanSessionListenerPostsEvents(mNanFacadeThread.getLooper(), listenerId),
+                WifiNanSessionListener.LISTEN_PUBLISH_FAIL
+                        | WifiNanSessionListener.LISTEN_PUBLISH_TERMINATED
+                        | WifiNanSessionListener.LISTEN_SUBSCRIBE_FAIL
+                        | WifiNanSessionListener.LISTEN_SUBSCRIBE_TERMINATED
+                        | WifiNanSessionListener.LISTEN_MATCH
+                        | WifiNanSessionListener.LISTEN_MESSAGE_SEND_SUCCESS
+                        | WifiNanSessionListener.LISTEN_MESSAGE_SEND_FAIL
+                        | WifiNanSessionListener.LISTEN_MESSAGE_RECEIVED);
+    }
+
+    @Rpc(description = "Subscribe.")
+    public void wifiNanSubscribe(@RpcParameter(name = "subscribeData") JSONObject subscribeData,
+            @RpcParameter(name = "subscribeSettings") JSONObject subscribeSettings,
+            @RpcParameter(name = "listenerId") Integer listenerId)
+                    throws RemoteException, JSONException {
+
+        mSession = mMgr.subscribe(getSubscribeData(subscribeData),
+                getSubscribeSettings(subscribeSettings),
+                new NanSessionListenerPostsEvents(mNanFacadeThread.getLooper(), listenerId),
+                WifiNanSessionListener.LISTEN_PUBLISH_FAIL
+                        | WifiNanSessionListener.LISTEN_PUBLISH_TERMINATED
+                        | WifiNanSessionListener.LISTEN_SUBSCRIBE_FAIL
+                        | WifiNanSessionListener.LISTEN_SUBSCRIBE_TERMINATED
+                        | WifiNanSessionListener.LISTEN_MATCH
+                        | WifiNanSessionListener.LISTEN_MESSAGE_SEND_SUCCESS
+                        | WifiNanSessionListener.LISTEN_MESSAGE_SEND_FAIL
+                        | WifiNanSessionListener.LISTEN_MESSAGE_RECEIVED);
+    }
+
+    @Rpc(description = "Send peer-to-peer NAN message")
+    public void wifiNanSendMessage(
+            @RpcParameter(name = "peerId", description = "The ID of the peer being communicated "
+                    + "with. Obtained from a previous message or match session.") Integer peerId,
+            @RpcParameter(name = "message") String message,
+            @RpcParameter(name = "messageId", description = "Arbitrary handle used for "
+                    + "identification of the message in the message status callbacks")
+            Integer messageId)
+                    throws RemoteException {
+        mSession.sendMessage(peerId, message.getBytes(), message.length(), messageId);
+    }
+
+    private class NanEventListenerPostsEvents extends WifiNanEventListener {
+        public NanEventListenerPostsEvents(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void onConfigCompleted(ConfigRequest configRequest) {
+            Bundle mResults = new Bundle();
+            mResults.putParcelable("configRequest", configRequest);
+            mEventFacade.postEvent("WifiNanOnConfigCompleted", mResults);
+        }
+
+        @Override
+        public void onConfigFailed(ConfigRequest failedConfig, int reason) {
+            Bundle mResults = new Bundle();
+            mResults.putParcelable("failedConfig", failedConfig);
+            mResults.putInt("reason", reason);
+            mEventFacade.postEvent("WifiNanOnConfigFailed", mResults);
+        }
+
+        @Override
+        public void onNanDown(int reason) {
+            Bundle mResults = new Bundle();
+            mResults.putInt("reason", reason);
+            mEventFacade.postEvent("WifiNanOnNanDown", mResults);
+        }
+
+        @Override
+        public void onIdentityChanged() {
+            Bundle mResults = new Bundle();
+            mEventFacade.postEvent("WifiNanOnIdentityChanged", mResults);
+        }
+    }
+
+    private class NanSessionListenerPostsEvents extends WifiNanSessionListener {
+        private int mListenerId;
+
+        public NanSessionListenerPostsEvents(Looper looper, int listenerId) {
+            super(looper);
+            mListenerId = listenerId;
+        }
+
+        @Override
+        public void onPublishFail(int reason) {
+            Bundle mResults = new Bundle();
+            mResults.putInt("listenerId", mListenerId);
+            mResults.putInt("reason", reason);
+            mEventFacade.postEvent("WifiNanSessionOnPublishFail", mResults);
+        }
+
+        @Override
+        public void onPublishTerminated(int reason) {
+            Bundle mResults = new Bundle();
+            mResults.putInt("listenerId", mListenerId);
+            mResults.putInt("reason", reason);
+            mEventFacade.postEvent("WifiNanSessionOnPublishTerminated", mResults);
+        }
+
+        @Override
+        public void onSubscribeFail(int reason) {
+            Bundle mResults = new Bundle();
+            mResults.putInt("listenerId", mListenerId);
+            mResults.putInt("reason", reason);
+            mEventFacade.postEvent("WifiNanSessionOnSubscribeFail", mResults);
+        }
+
+        @Override
+        public void onSubscribeTerminated(int reason) {
+            Bundle mResults = new Bundle();
+            mResults.putInt("listenerId", mListenerId);
+            mResults.putInt("reason", reason);
+            mEventFacade.postEvent("WifiNanSessionOnSubscribeTerminated", mResults);
+        }
+
+        @Override
+        public void onMatch(int peerId, byte[] serviceSpecificInfo,
+                int serviceSpecificInfoLength, byte[] matchFilter, int matchFilterLength) {
+            Bundle mResults = new Bundle();
+            mResults.putInt("listenerId", mListenerId);
+            mResults.putInt("peerId", peerId);
+            mResults.putInt("serviceSpecificInfoLength", serviceSpecificInfoLength);
+            mResults.putByteArray("serviceSpecificInfo", serviceSpecificInfo); // TODO: base64
+            mResults.putInt("matchFilterLength", matchFilterLength);
+            mResults.putByteArray("matchFilter", matchFilter); // TODO: base64
+            mEventFacade.postEvent("WifiNanSessionOnMatch", mResults);
+        }
+
+        @Override
+        public void onMessageSendSuccess(int messageId) {
+            Bundle mResults = new Bundle();
+            mResults.putInt("listenerId", mListenerId);
+            mResults.putInt("messageId", messageId);
+            mEventFacade.postEvent("WifiNanSessionOnMessageSendSuccess", mResults);
+        }
+
+        @Override
+        public void onMessageSendFail(int messageId, int reason) {
+            Bundle mResults = new Bundle();
+            mResults.putInt("listenerId", mListenerId);
+            mResults.putInt("messageId", messageId);
+            mResults.putInt("reason", reason);
+            mEventFacade.postEvent("WifiNanSessionOnMessageSendFail", mResults);
+        }
+
+        @Override
+        public void onMessageReceived(int peerId, byte[] message, int messageLength) {
+            Bundle mResults = new Bundle();
+            mResults.putInt("listenerId", mListenerId);
+            mResults.putInt("peerId", peerId);
+            mResults.putInt("messageLength", messageLength);
+            mResults.putByteArray("message", message); // TODO: base64
+            mResults.putString("messageAsString", new String(message, 0, messageLength));
+            mEventFacade.postEvent("WifiNanSessionOnMessageReceived", mResults);
+        }
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiP2pManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiP2pManagerFacade.java
new file mode 100644
index 0000000..d0514b9
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiP2pManagerFacade.java
@@ -0,0 +1,506 @@
+
+package com.googlecode.android_scripting.facade.wifi;
+
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.NetworkInfo;
+import android.net.wifi.WpsInfo;
+import android.net.wifi.p2p.WifiP2pConfig;
+import android.net.wifi.p2p.WifiP2pDevice;
+import android.net.wifi.p2p.WifiP2pDeviceList;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pGroupList;
+import android.net.wifi.p2p.WifiP2pInfo;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.net.wifi.p2p.nsd.WifiP2pDnsSdServiceInfo;
+import android.net.wifi.p2p.nsd.WifiP2pServiceInfo;
+import android.net.wifi.p2p.nsd.WifiP2pServiceRequest;
+import android.net.wifi.p2p.nsd.WifiP2pUpnpServiceInfo;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+
+import com.android.internal.util.Protocol;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * WifiP2pManager functions.
+ */
+public class WifiP2pManagerFacade extends RpcReceiver {
+
+    class WifiP2pActionListener implements WifiP2pManager.ActionListener {
+        private final EventFacade mEventFacade;
+        private final String mEventType;
+        private final String TAG;
+
+        public WifiP2pActionListener(EventFacade eventFacade, String tag) {
+            mEventType = "WifiP2p";
+            mEventFacade = eventFacade;
+            TAG = tag;
+        }
+
+        @Override
+        public void onSuccess() {
+            mEventFacade.postEvent(mEventType + TAG + "OnSuccess", null);
+        }
+
+        @Override
+        public void onFailure(int reason) {
+            Log.d("WifiActionListener  " + mEventType);
+            Bundle msg = new Bundle();
+            if (reason == WifiP2pManager.P2P_UNSUPPORTED) {
+                msg.putString("reason", "P2P_UNSUPPORTED");
+            } else if (reason == WifiP2pManager.ERROR) {
+                msg.putString("reason", "ERROR");
+            } else if (reason == WifiP2pManager.BUSY) {
+                msg.putString("reason", "BUSY");
+            } else if (reason == WifiP2pManager.NO_SERVICE_REQUESTS) {
+                msg.putString("reason", "NO_SERVICE_REQUESTS");
+            } else {
+                msg.putInt("reason", reason);
+            }
+            mEventFacade.postEvent(mEventType + TAG + "OnFailure", msg);
+        }
+    }
+
+    class WifiP2pConnectionInfoListener implements WifiP2pManager.ConnectionInfoListener {
+        private final EventFacade mEventFacade;
+        private final String mEventType;
+
+        public WifiP2pConnectionInfoListener(EventFacade eventFacade) {
+            mEventType = "WifiP2p";
+            mEventFacade = eventFacade;
+        }
+
+        @Override
+        public void onConnectionInfoAvailable(WifiP2pInfo info) {
+            Bundle msg = new Bundle();
+            msg.putBoolean("groupFormed", info.groupFormed);
+            msg.putBoolean("isGroupOwner", info.isGroupOwner);
+            InetAddress addr = info.groupOwnerAddress;
+            String hostName = null;
+            String hostAddress = null;
+            if (addr != null) {
+                hostName = addr.getHostName();
+                hostAddress = addr.getHostAddress();
+            }
+            msg.putString("groupOwnerHostName", hostName);
+            msg.putString("groupOwnerHostAddress", hostAddress);
+            mEventFacade.postEvent(mEventType + "OnConnectionInfoAvailable", msg);
+        }
+    }
+
+    class WifiP2pDnsSdServiceResponseListener implements
+            WifiP2pManager.DnsSdServiceResponseListener {
+        private final EventFacade mEventFacade;
+        private final String mEventType;
+
+        public WifiP2pDnsSdServiceResponseListener(EventFacade eventFacade) {
+            mEventType = "WifiP2p";
+            mEventFacade = eventFacade;
+        }
+
+        @Override
+        public void onDnsSdServiceAvailable(String instanceName, String registrationType,
+                WifiP2pDevice srcDevice) {
+            Bundle msg = new Bundle();
+            msg.putString("InstanceName", instanceName);
+            msg.putString("RegistrationType", registrationType);
+            msg.putString("SourceDeviceName", srcDevice.deviceName);
+            msg.putString("SourceDeviceAddress", srcDevice.deviceAddress);
+            mEventFacade.postEvent(mEventType + "OnDnsSdServiceAvailable", msg);
+        }
+    }
+
+    class WifiP2pDnsSdTxtRecordListener implements WifiP2pManager.DnsSdTxtRecordListener {
+        private final EventFacade mEventFacade;
+        private final String mEventType;
+
+        public WifiP2pDnsSdTxtRecordListener(EventFacade eventFacade) {
+            mEventType = "WifiP2p";
+            mEventFacade = eventFacade;
+        }
+
+        @Override
+        public void onDnsSdTxtRecordAvailable(String fullDomainName,
+                Map<String, String> txtRecordMap, WifiP2pDevice srcDevice) {
+            Bundle msg = new Bundle();
+            msg.putString("FullDomainName", fullDomainName);
+            Bundle txtMap = new Bundle();
+            for (String key : txtRecordMap.keySet()) {
+                txtMap.putString(key, txtRecordMap.get(key));
+            }
+            msg.putBundle("TxtRecordMap", txtMap);
+            msg.putString("SourceDeviceName", srcDevice.deviceName);
+            msg.putString("SourceDeviceAddress", srcDevice.deviceAddress);
+            mEventFacade.postEvent(mEventType + "OnDnsSdTxtRecordAvailable", msg);
+        }
+
+    }
+
+    class WifiP2pGroupInfoListener implements WifiP2pManager.GroupInfoListener {
+        private final EventFacade mEventFacade;
+        private final String mEventType;
+
+        public WifiP2pGroupInfoListener(EventFacade eventFacade) {
+            mEventType = "WifiP2p";
+            mEventFacade = eventFacade;
+        }
+
+        @Override
+        public void onGroupInfoAvailable(WifiP2pGroup group) {
+            mEventFacade.postEvent(mEventType + "OnGroupInfoAvailable", parseGroupInfo(group));
+        }
+    }
+
+    class WifiP2pPeerListListener implements WifiP2pManager.PeerListListener {
+        private final EventFacade mEventFacade;
+
+        public WifiP2pPeerListListener(EventFacade eventFacade) {
+            mEventFacade = eventFacade;
+        }
+
+        @Override
+        public void onPeersAvailable(WifiP2pDeviceList newPeers) {
+            Collection<WifiP2pDevice> devices = newPeers.getDeviceList();
+            Log.d(devices.toString());
+            if (devices.size() > 0) {
+                mP2pPeers.clear();
+                mP2pPeers.addAll(devices);
+                Bundle msg = new Bundle();
+                msg.putParcelableList("Peers", mP2pPeers);
+                mEventFacade.postEvent(mEventType + "OnPeersAvailable", msg);
+            }
+        }
+    }
+
+    class WifiP2pPersistentGroupInfoListener implements WifiP2pManager.PersistentGroupInfoListener {
+        private final EventFacade mEventFacade;
+        private final String mEventType;
+
+        public WifiP2pPersistentGroupInfoListener(EventFacade eventFacade) {
+            mEventType = "WifiP2p";
+            mEventFacade = eventFacade;
+        }
+
+        @Override
+        public void onPersistentGroupInfoAvailable(WifiP2pGroupList groups) {
+            ArrayList<Bundle> gs = new ArrayList<Bundle>();
+            for (WifiP2pGroup g : groups.getGroupList()) {
+                gs.add(parseGroupInfo(g));
+            }
+            mEventFacade.postEvent(mEventType + "OnPersistentGroupInfoAvailable", gs);
+        }
+
+    }
+
+    class WifiP2pStateChangedReceiver extends BroadcastReceiver {
+        private final EventFacade mEventFacade;
+        private final Bundle mResults;
+
+        WifiP2pStateChangedReceiver(EventFacade eventFacade) {
+            mEventFacade = eventFacade;
+            mResults = new Bundle();
+        }
+
+        @Override
+        public void onReceive(Context c, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)) {
+                Log.d("Wifi P2p State Changed.");
+                int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, 0);
+                if (state == WifiP2pManager.WIFI_P2P_STATE_DISABLED) {
+                    Log.d("Disabled");
+                    isP2pEnabled = false;
+                } else if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
+                    Log.d("Enabled");
+                    isP2pEnabled = true;
+                }
+            } else if (action.equals(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)) {
+                Log.d("Wifi P2p Peers Changed. Requesting peers.");
+                WifiP2pDeviceList peers = intent
+                        .getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST);
+                Log.d(peers.toString());
+                wifiP2pRequestPeers();
+            } else if (action.equals(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) {
+                Log.d("Wifi P2p Connection Changed.");
+                WifiP2pInfo p2pInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO);
+                NetworkInfo networkInfo = intent
+                        .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
+                WifiP2pGroup group = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP);
+                if (networkInfo.isConnected()) {
+                    Log.d("Wifi P2p Connected.");
+                    mResults.putParcelable("P2pInfo", p2pInfo);
+                    mResults.putParcelable("Group", group);
+                    mEventFacade.postEvent(mEventType + "Connected", mResults);
+                    mResults.clear();
+                } else {
+                    mEventFacade.postEvent(mEventType + "Disconnected", null);
+                }
+            } else if (action.equals(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)) {
+                Log.d("Wifi P2p This Device Changed.");
+                WifiP2pDevice device = intent
+                        .getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE);
+                mResults.putParcelable("Device", device);
+                mEventFacade.postEvent(mEventType + "ThisDeviceChanged", mResults);
+                mResults.clear();
+            } else if (action.equals(WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION)) {
+                Log.d("Wifi P2p Discovery Changed.");
+                int state = intent.getIntExtra(WifiP2pManager.EXTRA_DISCOVERY_STATE, 0);
+                if (state == WifiP2pManager.WIFI_P2P_DISCOVERY_STARTED) {
+                    Log.d("discovery started.");
+                } else if (state == WifiP2pManager.WIFI_P2P_DISCOVERY_STOPPED) {
+                    Log.d("discovery stoped.");
+                }
+            }
+        }
+    }
+
+    private final static String mEventType = "WifiP2p";
+
+    private WifiP2pManager.Channel mChannel;
+    private final EventFacade mEventFacade;
+    private final WifiP2pManager mP2p;
+    private final WifiP2pStateChangedReceiver mP2pStateChangedReceiver;
+    private final Service mService;
+    private final IntentFilter mStateChangeFilter;
+    private final Map<Integer, WifiP2pServiceRequest> mServiceRequests;
+
+    private boolean isP2pEnabled;
+    private int mServiceRequestCnt = 0;
+    private WifiP2pServiceInfo mServiceInfo = null;
+    private List<WifiP2pDevice> mP2pPeers = new ArrayList<WifiP2pDevice>();
+
+    public WifiP2pManagerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mP2p = (WifiP2pManager) mService.getSystemService(Context.WIFI_P2P_SERVICE);
+        mEventFacade = manager.getReceiver(EventFacade.class);
+
+        mStateChangeFilter = new IntentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
+        mStateChangeFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
+        mStateChangeFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
+        mStateChangeFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
+        mStateChangeFilter.setPriority(999);
+
+        mP2pStateChangedReceiver = new WifiP2pStateChangedReceiver(mEventFacade);
+        mServiceRequests = new HashMap<Integer, WifiP2pServiceRequest>();
+    }
+
+    public Bundle parseGroupInfo(WifiP2pGroup group) {
+        Bundle msg = new Bundle();
+        msg.putString("Interface", group.getInterface());
+        msg.putString("NetworkName", group.getNetworkName());
+        msg.putString("Passphrase", group.getPassphrase());
+        msg.putInt("NetworkId", group.getNetworkId());
+        msg.putString("OwnerName", group.getOwner().deviceName);
+        msg.putString("OwnerAddress", group.getOwner().deviceAddress);
+        return msg;
+    }
+
+    @Override
+    public void shutdown() {
+        mService.unregisterReceiver(mP2pStateChangedReceiver);
+    }
+
+    @Rpc(description = "Accept p2p connection invitation.")
+    public void wifiP2pAcceptConnection() throws RemoteException {
+        Log.d("Accepting p2p connection.");
+        Messenger m = mP2p.getP2pStateMachineMessenger();
+        int user_accept = Protocol.BASE_WIFI_P2P_SERVICE + 2;
+        Message msg = Message.obtain();
+        msg.what = user_accept;
+        m.send(msg);
+    }
+
+    @Rpc(description = "Reject p2p connection invitation.")
+    public void wifiP2pRejectConnection() throws RemoteException {
+        Log.d("Rejecting p2p connection.");
+        Messenger m = mP2p.getP2pStateMachineMessenger();
+        int user_accept = Protocol.BASE_WIFI_P2P_SERVICE + 3;
+        Message msg = Message.obtain();
+        msg.what = user_accept;
+        m.send(msg);
+    }
+
+    @Rpc(description = "Register a local service for service discovery. One of the \"CreateXxxServiceInfo functions needs to be called first.\"")
+    public void wifiP2pAddLocalService() {
+        mP2p.addLocalService(mChannel, mServiceInfo,
+                new WifiP2pActionListener(mEventFacade, "AddLocalService"));
+    }
+
+    @Rpc(description = "Add a service discovery request.")
+    public Integer wifiP2pAddServiceRequest(
+            @RpcParameter(name = "protocolType") Integer protocolType) {
+        WifiP2pServiceRequest request = WifiP2pServiceRequest.newInstance(protocolType);
+        mServiceRequestCnt += 1;
+        mServiceRequests.put(mServiceRequestCnt, request);
+        mP2p.addServiceRequest(mChannel, request, new WifiP2pActionListener(mEventFacade,
+                "AddServiceRequest"));
+        return mServiceRequestCnt;
+    }
+
+    @Rpc(description = "Cancel any ongoing connect negotiation.")
+    public void wifiP2pCancelConnect() {
+        mP2p.cancelConnect(mChannel, new WifiP2pActionListener(mEventFacade, "CancelConnect"));
+    }
+
+    @Rpc(description = "Clear all registered local services of service discovery.")
+    public void wifiP2pClearLocalServices() {
+        mP2p.clearLocalServices(mChannel,
+                new WifiP2pActionListener(mEventFacade, "ClearLocalServices"));
+    }
+
+    @Rpc(description = "Clear all registered service discovery requests.")
+    public void wifiP2pClearServiceRequests() {
+        mP2p.clearServiceRequests(mChannel,
+                new WifiP2pActionListener(mEventFacade, "ClearServiceRequests"));
+    }
+
+    @Rpc(description = "Connects to a discovered wifi p2p device.")
+    public void wifiP2pConnect(@RpcParameter(name = "deviceId") String deviceId) {
+        for (WifiP2pDevice d : mP2pPeers) {
+            if (wifiP2pDeviceMatches(d, deviceId)) {
+                WifiP2pConfig config = new WifiP2pConfig();
+                config.deviceAddress = d.deviceAddress;
+                config.wps.setup = WpsInfo.PBC;
+                mP2p.connect(mChannel, config,
+                        new WifiP2pActionListener(mEventFacade, "Connect"));
+            }
+        }
+    }
+
+    @Rpc(description = "Create a Bonjour service info object to be used for wifiP2pAddLocalService.")
+    public void wifiP2pCreateBonjourServiceInfo(
+            @RpcParameter(name = "instanceName") String instanceName,
+            @RpcParameter(name = "serviceType") String serviceType,
+            @RpcParameter(name = "txtMap") JSONObject txtMap) throws JSONException {
+        Map<String, String> map = new HashMap<String, String>();
+        for (String key : txtMap.keySet()) {
+            map.put(key, txtMap.getString(key));
+        }
+        mServiceInfo = WifiP2pDnsSdServiceInfo.newInstance(instanceName, serviceType, map);
+    }
+
+    @Rpc(description = "Create a wifi p2p group.")
+    public void wifiP2pCreateGroup() {
+        mP2p.createGroup(mChannel, new WifiP2pActionListener(mEventFacade, "CreatGroup"));
+    }
+
+    @Rpc(description = "Create a Upnp service info object to be used for wifiP2pAddLocalService.")
+    public void wifiP2pCreateUpnpServiceInfo(
+            @RpcParameter(name = "uuid") String uuid,
+            @RpcParameter(name = "device") String device,
+            @RpcParameter(name = "services") List<String> services) {
+        mServiceInfo = WifiP2pUpnpServiceInfo.newInstance(uuid, device, services);
+    }
+
+    @Rpc(description = "Delete a stored persistent group from the system settings.")
+    public void wifiP2pDeletePersistentGroup(@RpcParameter(name = "netId") Integer netId) {
+        mP2p.deletePersistentGroup(mChannel, netId,
+                new WifiP2pActionListener(mEventFacade, "DeletePersistentGroup"));
+    }
+
+    private boolean wifiP2pDeviceMatches(WifiP2pDevice d, String deviceId) {
+        return d.deviceName.equals(deviceId) || d.deviceAddress.equals(deviceId);
+    }
+
+    @Rpc(description = "Start peers discovery for wifi p2p.")
+    public void wifiP2pDiscoverPeers() {
+        mP2p.discoverPeers(mChannel, new WifiP2pActionListener(mEventFacade, "DiscoverPeers"));
+    }
+
+    @Rpc(description = "Initiate service discovery.")
+    public void wifiP2pDiscoverServices() {
+        mP2p.discoverServices(mChannel,
+                new WifiP2pActionListener(mEventFacade, "DiscoverServices"));
+    }
+
+    @Rpc(description = "Initialize wifi p2p. Must be called before any other p2p functions.")
+    public void wifiP2pInitialize() {
+        mService.registerReceiver(mP2pStateChangedReceiver, mStateChangeFilter);
+        mChannel = mP2p.initialize(mService, mService.getMainLooper(), null);
+    }
+
+    @Rpc(description = "Returns true if wifi p2p is enabled, false otherwise.")
+    public Boolean wifiP2pIsEnabled() {
+        return isP2pEnabled;
+    }
+
+    @Rpc(description = "Remove the current p2p group.")
+    public void wifiP2pRemoveGroup() {
+        mP2p.removeGroup(mChannel, new WifiP2pActionListener(mEventFacade, "RemoveGroup"));
+    }
+
+    @Rpc(description = "Remove a registered local service added with wifiP2pAddLocalService.")
+    public void wifiP2pRemoveLocalService() {
+        mP2p.removeLocalService(mChannel, mServiceInfo,
+                new WifiP2pActionListener(mEventFacade, "RemoveLocalService"));
+    }
+
+    @Rpc(description = "Remove a service discovery request.")
+    public void wifiP2pRemoveServiceRequest(@RpcParameter(name = "index") Integer index) {
+        mP2p.removeServiceRequest(mChannel, mServiceRequests.remove(index),
+                new WifiP2pActionListener(mEventFacade, "RemoveServiceRequest"));
+    }
+
+    @Rpc(description = "Request device connection info.")
+    public void wifiP2pRequestConnectionInfo() {
+        mP2p.requestConnectionInfo(mChannel, new WifiP2pConnectionInfoListener(mEventFacade));
+    }
+
+    @Rpc(description = "Create a wifi p2p group.")
+    public void wifiP2pRequestGroupInfo() {
+        mP2p.requestGroupInfo(mChannel, new WifiP2pGroupInfoListener(mEventFacade));
+    }
+
+    @Rpc(description = "Request peers that are discovered for wifi p2p.")
+    public void wifiP2pRequestPeers() {
+        mP2p.requestPeers(mChannel, new WifiP2pPeerListListener(mEventFacade));
+    }
+
+    @Rpc(description = "Request a list of all the persistent p2p groups stored in system.")
+    public void wifiP2pRequestPersistentGroupInfo() {
+        mP2p.requestPersistentGroupInfo(mChannel,
+                new WifiP2pPersistentGroupInfoListener(mEventFacade));
+    }
+
+    @Rpc(description = "Set p2p device name.")
+    public void wifiP2pSetDeviceName(@RpcParameter(name = "devName") String devName) {
+        mP2p.setDeviceName(mChannel, devName,
+                new WifiP2pActionListener(mEventFacade, "SetDeviceName"));
+    }
+
+    @Rpc(description = "Register a callback to be invoked on receiving Bonjour service discovery response.")
+    public void wifiP2pSetDnsSdResponseListeners() {
+        mP2p.setDnsSdResponseListeners(mChannel,
+                new WifiP2pDnsSdServiceResponseListener(mEventFacade),
+                new WifiP2pDnsSdTxtRecordListener(mEventFacade));
+    }
+
+    @Rpc(description = "Stop an ongoing peer discovery.")
+    public void wifiP2pStopPeerDiscovery() {
+        mP2p.stopPeerDiscovery(mChannel,
+                new WifiP2pActionListener(mEventFacade, "StopPeerDiscovery"));
+    }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiRttManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiRttManagerFacade.java
new file mode 100644
index 0000000..b4bf1e1
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiRttManagerFacade.java
@@ -0,0 +1,210 @@
+
+package com.googlecode.android_scripting.facade.wifi;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.app.Service;
+import android.content.Context;
+import android.net.wifi.RttManager;
+import android.net.wifi.RttManager.RttCapabilities;
+import android.net.wifi.RttManager.RttListener;
+import android.net.wifi.RttManager.RttParams;
+import android.net.wifi.RttManager.RttResult;
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * WifiRttManager functions.
+ */
+public class WifiRttManagerFacade extends RpcReceiver {
+    private final Service mService;
+    private final RttManager mRtt;
+    private final EventFacade mEventFacade;
+    private final Map<Integer, RttListener> mRangingListeners;
+
+    public WifiRttManagerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mRtt = (RttManager) mService.getSystemService(Context.WIFI_RTT_SERVICE);
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        mRangingListeners = new Hashtable<Integer, RttListener>();
+    }
+
+    public static class RangingListener implements RttListener {
+        private static final String TAG = "WifiRttRanging";
+        private static int sCount = 0;
+        private final EventFacade mEventFacade;
+        public final int mId;
+
+        public RangingListener(EventFacade eventFacade) {
+            sCount += 1;
+            mId = sCount;
+            mEventFacade = eventFacade;
+        }
+
+        private Bundle packRttResult(RttResult result) {
+            Bundle rttResult = new Bundle();
+            rttResult.putString("BSSID", result.bssid);
+            rttResult.putInt("txRate", result.txRate);
+            rttResult.putInt("rxRate", result.rxRate);
+            rttResult.putInt("distance", result.distance);
+            rttResult.putInt("distanceStandardDeviation",
+                    result.distanceStandardDeviation);
+            rttResult.putInt("distanceSpread", result.distanceSpread);
+            rttResult.putInt("burstDuration", result.burstDuration);
+            rttResult.putLong("rtt", result.rtt);
+            rttResult.putLong("rttStandardDeviation",
+                    result.rttStandardDeviation);
+            rttResult.putLong("rttSpread", result.rttSpread);
+            rttResult.putLong("ts", result.ts);
+            rttResult.putInt("rssi", result.rssi);
+            rttResult.putInt("rssiSpread", result.rssiSpread);
+            rttResult.putInt("retryAfterDuration", result.retryAfterDuration);
+            rttResult.putInt("measurementType", result.measurementType);
+            rttResult.putInt("status", result.status);
+            rttResult.putInt("frameNumberPerBurstPeer",
+                    result.frameNumberPerBurstPeer);
+            rttResult.putInt("successMeasurementFrameNumber",
+                    result.successMeasurementFrameNumber);
+            rttResult.putInt("measurementFrameNumber",
+                    result.measurementFrameNumber);
+            rttResult.putInt("burstNumber", result.burstNumber);
+            rttResult.putInt("status", result.status);
+            return rttResult;
+        }
+
+        @Override
+        public void onSuccess(RttResult[] results) {
+            if (results == null) {
+                mEventFacade
+                        .postEvent(RangingListener.TAG + mId + "onSuccess", null);
+                return;
+            }
+            Bundle msg = new Bundle();
+            Parcelable[] resultBundles = new Parcelable[results.length];
+            for (int i = 0; i < results.length; i++) {
+                resultBundles[i] = packRttResult(results[i]);
+            }
+            msg.putParcelableArray("Results", resultBundles);
+            mEventFacade
+                    .postEvent(RangingListener.TAG + mId + "onSuccess", msg);
+        }
+
+        @Override
+        public void onFailure(int reason, String description) {
+            Bundle msg = new Bundle();
+            msg.putInt("Reason", reason);
+            msg.putString("Description", description);
+            mEventFacade
+                    .postEvent(RangingListener.TAG + mId + "onFailure", msg);
+        }
+
+        @Override
+        public void onAborted() {
+            mEventFacade.postEvent(RangingListener.TAG + mId + "onAborted",
+                    new Bundle());
+        }
+    }
+
+    @Rpc(description = "Get wifi Rtt capabilities.")
+    public RttCapabilities wifiRttGetCapabilities() {
+        return mRtt.getRttCapabilities();
+    }
+
+    private RttParams parseRttParam(JSONObject j) throws JSONException {
+        RttParams result = new RttParams();
+        if (j.has("deviceType")) {
+            result.deviceType = j.getInt("deviceType");
+        }
+        if (j.has("requestType")) {
+            result.requestType = j.getInt("requestType");
+        }
+        if (j.has("bssid")) {
+            result.bssid = j.getString("bssid");
+        }
+        if (j.has("frequency")) {
+            result.frequency = j.getInt("frequency");
+        }
+        if (j.has("channelWidth")) {
+            result.channelWidth = j.getInt("channelWidth");
+        }
+        if (j.has("centerFreq0")) {
+            result.centerFreq0 = j.getInt("centerFreq0");
+        }
+        if (j.has("centerFreq1")) {
+            result.centerFreq1 = j.getInt("centerFreq1");
+        }
+        if (j.has("numberBurst")) {
+            result.numberBurst = j.getInt("numberBurst");
+        }
+        if (j.has("burstTimeout")) {
+            result.burstTimeout = j.getInt("burstTimeout");
+        }
+        if (j.has("interval")) {
+            result.interval = j.getInt("interval");
+        }
+        if (j.has("numSamplesPerBurst")) {
+            result.numSamplesPerBurst = j.getInt("numSamplesPerBurst");
+        }
+        if (j.has("numRetriesPerMeasurementFrame")) {
+            result.numRetriesPerMeasurementFrame = j
+                    .getInt("numRetriesPerMeasurementFrame");
+        }
+        if (j.has("numRetriesPerFTMR")) {
+            result.numRetriesPerFTMR = j.getInt("numRetriesPerFTMR");
+        }
+        if (j.has("LCIRequest")) {
+            result.LCIRequest = j.getBoolean("LCIRequest");
+        }
+        if (j.has("LCRRequest")) {
+            result.LCRRequest = j.getBoolean("LCRRequest");
+        }
+        if (j.has("preamble")) {
+            result.preamble = j.getInt("preamble");
+        }
+        if (j.has("bandwidth")) {
+            result.bandwidth = j.getInt("bandwidth");
+        }
+        return result;
+    }
+
+    @Rpc(description = "Start ranging.", returns = "Id of the listener associated with the started ranging.")
+    public Integer wifiRttStartRanging(
+            @RpcParameter(name = "params") JSONArray params)
+            throws JSONException {
+        RttParams[] rParams = new RttParams[params.length()];
+        for (int i = 0; i < params.length(); i++) {
+            rParams[i] = parseRttParam(params.getJSONObject(i));
+        }
+        RangingListener listener = new RangingListener(mEventFacade);
+        mRangingListeners.put(listener.mId, listener);
+        mRtt.startRanging(rParams, listener);
+        return listener.mId;
+    }
+
+    @Rpc(description = "Stop ranging.")
+    public void wifiRttStopRanging(@RpcParameter(name = "index") Integer index) {
+        mRtt.stopRanging(mRangingListeners.remove(index));
+    }
+
+    @Override
+    public void shutdown() {
+        ArrayList<Integer> keys = new ArrayList<Integer>(
+                mRangingListeners.keySet());
+        for (int k : keys) {
+            wifiRttStopRanging(k);
+        }
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/wifi/WifiScannerFacade.java b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiScannerFacade.java
new file mode 100644
index 0000000..f718b90
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/wifi/WifiScannerFacade.java
@@ -0,0 +1,649 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade.wifi;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.MainThread;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+import android.app.Service;
+import android.content.Context;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiScanner;
+import android.net.wifi.WifiScanner.BssidInfo;
+import android.net.wifi.WifiScanner.ChannelSpec;
+import android.net.wifi.WifiScanner.ScanData;
+import android.net.wifi.WifiScanner.ScanSettings;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.Settings.Global;
+import android.provider.Settings.SettingNotFoundException;
+
+/**
+ * WifiScanner functions.
+ */
+public class WifiScannerFacade extends RpcReceiver {
+    private final Service mService;
+    private final EventFacade mEventFacade;
+    private final WifiScanner mScan;
+    // These counters are just for indexing;
+    // they do not represent the total number of listeners
+    private static int WifiScanListenerCnt;
+    private static int WifiChangeListenerCnt;
+    private static int WifiBssidListenerCnt;
+    private final ConcurrentHashMap<Integer, WifiScanListener> scanListeners;
+    private final ConcurrentHashMap<Integer, WifiScanListener> scanBackgroundListeners;
+    private final ConcurrentHashMap<Integer, ChangeListener> trackChangeListeners;
+    private final ConcurrentHashMap<Integer, WifiBssidListener> trackBssidListeners;
+    private static ConcurrentHashMap<Integer, ScanResult[]> wifiScannerResultList;
+    private static ConcurrentHashMap<Integer, ScanData[]> wifiScannerDataList;
+
+    public WifiScannerFacade(FacadeManager manager) {
+        super(manager);
+        mService = manager.getService();
+        mScan = (WifiScanner) mService.getSystemService(Context.WIFI_SCANNING_SERVICE);
+        mEventFacade = manager.getReceiver(EventFacade.class);
+        scanListeners = new ConcurrentHashMap<Integer, WifiScanListener>();
+        scanBackgroundListeners = new ConcurrentHashMap<Integer, WifiScanListener>();
+        trackChangeListeners = new ConcurrentHashMap<Integer, ChangeListener>();
+        trackBssidListeners = new ConcurrentHashMap<Integer, WifiBssidListener>();
+        wifiScannerResultList = new ConcurrentHashMap<Integer, ScanResult[]>();
+        wifiScannerDataList = new ConcurrentHashMap<Integer, ScanData[]>();
+    }
+
+    public static List<ScanResult> getWifiScanResult(Integer listenerIndex) {
+        ScanResult[] sr = wifiScannerResultList.get(listenerIndex);
+        return Arrays.asList(sr);
+    }
+
+    private class WifiActionListener implements WifiScanner.ActionListener {
+        private final Bundle mResults;
+        public int mIndex;
+        protected String mEventType;
+        private long startScanElapsedRealTime;
+
+        public WifiActionListener(String type, int idx, Bundle resultBundle, long startScanERT) {
+            this.mIndex = idx;
+            this.mEventType = type;
+            this.mResults = resultBundle;
+            this.startScanElapsedRealTime = startScanERT;
+        }
+
+        @Override
+        public void onSuccess() {
+            Log.d("onSuccess " + mEventType + " " + mIndex);
+            mResults.putString("Type", "onSuccess");
+            mResults.putInt("Index", mIndex);
+            mResults.putLong("ScanElapsedRealtime", startScanElapsedRealTime);
+            mEventFacade.postEvent(mEventType + mIndex + "onSuccess", mResults.clone());
+            mResults.clear();
+        }
+
+        @Override
+        public void onFailure(int reason, String description) {
+            Log.d("onFailure " + mEventType + " " + mIndex);
+            mResults.putString("Type", "onFailure");
+            mResults.putInt("Index", mIndex);
+            mResults.putInt("Reason", reason);
+            mResults.putString("Description", description);
+            mEventFacade.postEvent(mEventType + mIndex + "onFailure", mResults.clone());
+            mResults.clear();
+        }
+
+        public void reportResult(ScanResult[] results, String type) {
+            Log.d("reportResult " + mEventType + " " + mIndex);
+            mResults.putInt("Index", mIndex);
+            mResults.putLong("ResultElapsedRealtime", SystemClock.elapsedRealtime());
+            mResults.putString("Type", type);
+            mResults.putParcelableArray("Results", results);
+            mEventFacade.postEvent(mEventType + mIndex + type, mResults.clone());
+            mResults.clear();
+        }
+    }
+
+    /**
+     * Constructs a wifiScanListener obj and returns it
+     *
+     * @return WifiScanListener
+     */
+    private WifiScanListener genWifiScanListener() {
+        WifiScanListener mWifiScannerListener = MainThread.run(mService,
+                new Callable<WifiScanListener>() {
+                    @Override
+                    public WifiScanListener call() throws Exception {
+                        return new WifiScanListener();
+                    }
+                });
+        scanListeners.put(mWifiScannerListener.mIndex, mWifiScannerListener);
+        return mWifiScannerListener;
+    }
+
+    /**
+     * Constructs a wifiScanListener obj for background scan and returns it
+     *
+     * @return WifiScanListener
+     */
+    private WifiScanListener genBackgroundWifiScanListener() {
+        WifiScanListener mWifiScannerListener = MainThread.run(mService,
+                new Callable<WifiScanListener>() {
+                    @Override
+                    public WifiScanListener call() throws Exception {
+                        return new WifiScanListener();
+                    }
+                });
+        scanBackgroundListeners.put(mWifiScannerListener.mIndex, mWifiScannerListener);
+        return mWifiScannerListener;
+    }
+
+    private class WifiScanListener implements WifiScanner.ScanListener {
+        private static final String mEventType = "WifiScannerScan";
+        protected final Bundle mScanResults;
+        protected final Bundle mScanData;
+        private final WifiActionListener mWAL;
+        public int mIndex;
+
+        public WifiScanListener() {
+            mScanResults = new Bundle();
+            mScanData = new Bundle();
+            WifiScanListenerCnt += 1;
+            mIndex = WifiScanListenerCnt;
+            mWAL = new WifiActionListener(mEventType, mIndex, mScanResults,
+                    SystemClock.elapsedRealtime());
+        }
+
+        @Override
+        public void onSuccess() {
+            mWAL.onSuccess();
+        }
+
+        @Override
+        public void onFailure(int reason, String description) {
+            scanListeners.remove(mIndex);
+            mWAL.onFailure(reason, description);
+        }
+
+        @Override
+        public void onPeriodChanged(int periodInMs) {
+            Log.d("onPeriodChanged " + mEventType + " " + mIndex);
+            mScanResults.putString("Type", "onPeriodChanged");
+            mScanResults.putInt("NewPeriod", periodInMs);
+            mEventFacade.postEvent(mEventType + mIndex, mScanResults.clone());
+            mScanResults.clear();
+        }
+
+        @Override
+        public void onFullResult(ScanResult fullScanResult) {
+            Log.d("onFullResult WifiScanListener " + mIndex);
+            mWAL.reportResult(new ScanResult[] {
+                    fullScanResult
+            }, "onFullResult");
+        }
+
+        public void onResults(ScanData[] results) {
+            Log.d("onResult WifiScanListener " + mIndex);
+            wifiScannerDataList.put(mIndex, results);
+            mScanData.putInt("Index", mIndex);
+            mScanData.putLong("ResultElapsedRealtime", SystemClock.elapsedRealtime());
+            mScanData.putString("Type", "onResults");
+            mScanData.putParcelableArray("Results", results);
+            mEventFacade.postEvent(mEventType + mIndex + "onResults", mScanData.clone());
+            mScanData.clear();
+        }
+    }
+
+    /**
+     * Constructs a ChangeListener obj and returns it
+     *
+     * @return ChangeListener
+     */
+    private ChangeListener genWifiChangeListener() {
+        ChangeListener mWifiChangeListener = MainThread.run(mService,
+                new Callable<ChangeListener>() {
+                    @Override
+                    public ChangeListener call() throws Exception {
+                        return new ChangeListener();
+                    }
+                });
+        trackChangeListeners.put(mWifiChangeListener.mIndex, mWifiChangeListener);
+        return mWifiChangeListener;
+    }
+
+    private class ChangeListener implements WifiScanner.WifiChangeListener {
+        private static final String mEventType = "WifiScannerChange";
+        protected final Bundle mResults;
+        private final WifiActionListener mWAL;
+        public int mIndex;
+
+        public ChangeListener() {
+            mResults = new Bundle();
+            WifiChangeListenerCnt += 1;
+            mIndex = WifiChangeListenerCnt;
+            mWAL = new WifiActionListener(mEventType, mIndex, mResults,
+                    SystemClock.elapsedRealtime());
+        }
+
+        @Override
+        public void onSuccess() {
+            mWAL.onSuccess();
+        }
+
+        @Override
+        public void onFailure(int reason, String description) {
+            trackChangeListeners.remove(mIndex);
+            mWAL.onFailure(reason, description);
+        }
+
+        /**
+         * indicates that changes were detected in wifi environment
+         *
+         * @param results indicate the access points that exhibited change
+         */
+        @Override
+        public void onChanging(ScanResult[] results) { /* changes are found */
+            mWAL.reportResult(results, "onChanging");
+        }
+
+        /**
+         * indicates that no wifi changes are being detected for a while
+         *
+         * @param results indicate the access points that are bing monitored for change
+         */
+        @Override
+        public void onQuiescence(ScanResult[] results) { /* changes settled down */
+            mWAL.reportResult(results, "onQuiescence");
+        }
+    }
+
+    private WifiBssidListener genWifiBssidListener() {
+        WifiBssidListener mWifiBssidListener = MainThread.run(mService,
+                new Callable<WifiBssidListener>() {
+                    @Override
+                    public WifiBssidListener call() throws Exception {
+                        return new WifiBssidListener();
+                    }
+                });
+        trackBssidListeners.put(mWifiBssidListener.mIndex, mWifiBssidListener);
+        return mWifiBssidListener;
+    }
+
+    private class WifiBssidListener implements WifiScanner.BssidListener {
+        private static final String mEventType = "WifiScannerBssid";
+        protected final Bundle mResults;
+        private final WifiActionListener mWAL;
+        public int mIndex;
+
+        public WifiBssidListener() {
+            mResults = new Bundle();
+            WifiBssidListenerCnt += 1;
+            mIndex = WifiBssidListenerCnt;
+            mWAL = new WifiActionListener(mEventType, mIndex, mResults,
+                    SystemClock.elapsedRealtime());
+        }
+
+        @Override
+        public void onSuccess() {
+            mWAL.onSuccess();
+        }
+
+        @Override
+        public void onFailure(int reason, String description) {
+            trackBssidListeners.remove(mIndex);
+            mWAL.onFailure(reason, description);
+        }
+
+        @Override
+        public void onFound(ScanResult[] results) {
+            mWAL.reportResult(results, "onFound");
+        }
+
+        @Override
+        public void onLost(ScanResult[] results) {
+            mWAL.reportResult(results, "onLost");
+        }
+    }
+
+    private ScanSettings parseScanSettings(JSONObject j) throws JSONException {
+        if (j == null) {
+            return null;
+        }
+        ScanSettings result = new ScanSettings();
+        if (j.has("band")) {
+            result.band = j.optInt("band");
+        }
+        if (j.has("channels")) {
+            JSONArray chs = j.getJSONArray("channels");
+            ChannelSpec[] channels = new ChannelSpec[chs.length()];
+            for (int i = 0; i < channels.length; i++) {
+                channels[i] = new ChannelSpec(chs.getInt(i));
+            }
+            result.channels = channels;
+        }
+        if (j.has("maxScansToCache")) {
+            result.maxScansToCache = j.getInt("maxScansToCache");
+        }
+        /* periodInMs and reportEvents are required */
+        result.periodInMs = j.getInt("periodInMs");
+        if (j.has("maxPeriodInMs")) {
+            result.maxPeriodInMs = j.getInt("maxPeriodInMs");
+        }
+        if (j.has("stepCount")) {
+            result.stepCount = j.getInt("stepCount");
+        }
+        result.reportEvents = j.getInt("reportEvents");
+        if (j.has("numBssidsPerScan")) {
+            result.numBssidsPerScan = j.getInt("numBssidsPerScan");
+        }
+        return result;
+    }
+
+    private BssidInfo[] parseBssidInfo(JSONArray jBssids) throws JSONException {
+        BssidInfo[] bssids = new BssidInfo[jBssids.length()];
+        for (int i = 0; i < bssids.length; i++) {
+            JSONObject bi = (JSONObject) jBssids.get(i);
+            BssidInfo bssidInfo = new BssidInfo();
+            bssidInfo.bssid = bi.getString("BSSID");
+            bssidInfo.high = bi.getInt("high");
+            bssidInfo.low = bi.getInt("low");
+            if (bi.has("frequencyHint")) {
+                bssidInfo.frequencyHint = bi.getInt("frequencyHint");
+            }
+            bssids[i] = bssidInfo;
+        }
+        return bssids;
+    }
+
+    /**
+     * Starts periodic WifiScanner scan
+     *
+     * @param scanSettings
+     * @return the id of the scan listener associated with this scan
+     * @throws JSONException
+     */
+    @Rpc(description = "Starts a WifiScanner Background scan")
+    public Integer wifiScannerStartBackgroundScan(
+            @RpcParameter(name = "scanSettings") JSONObject scanSettings)
+                    throws JSONException {
+        ScanSettings ss = parseScanSettings(scanSettings);
+        Log.d("startWifiScannerScan with " + ss.channels);
+        WifiScanListener listener = genBackgroundWifiScanListener();
+        mScan.startBackgroundScan(ss, listener);
+        return listener.mIndex;
+    }
+
+    /**
+     * Get currently available scan results on appropriate listeners
+     *
+     * @return true if all scan results were reported correctly
+     * @throws JSONException
+     */
+    @Rpc(description = "Get currently available scan results on appropriate listeners")
+    public Boolean wifiScannerGetScanResults() throws JSONException {
+        mScan.getScanResults();
+        return true;
+    }
+
+    /**
+     * Stops a WifiScanner scan
+     *
+     * @param listenerIndex the id of the scan listener whose scan to stop
+     * @throws Exception
+     */
+    @Rpc(description = "Stops an ongoing  WifiScanner Background scan")
+    public void wifiScannerStopBackgroundScan(
+            @RpcParameter(name = "listener") Integer listenerIndex)
+                    throws Exception {
+        if (!scanBackgroundListeners.containsKey(listenerIndex)) {
+            throw new Exception("Background scan session " + listenerIndex + " does not exist");
+        }
+        WifiScanListener listener = scanBackgroundListeners.get(listenerIndex);
+        Log.d("stopWifiScannerScan listener " + listener.mIndex);
+        mScan.stopBackgroundScan(listener);
+        wifiScannerResultList.remove(listenerIndex);
+        scanBackgroundListeners.remove(listenerIndex);
+    }
+
+    /**
+     * Starts periodic WifiScanner scan
+     *
+     * @param scanSettings
+     * @return the id of the scan listener associated with this scan
+     * @throws JSONException
+     */
+    @Rpc(description = "Starts a WifiScanner single scan")
+    public Integer wifiScannerStartScan(
+            @RpcParameter(name = "scanSettings") JSONObject scanSettings)
+                    throws JSONException {
+        ScanSettings ss = parseScanSettings(scanSettings);
+        Log.d("startWifiScannerScan with " + ss.channels);
+        WifiScanListener listener = genWifiScanListener();
+        mScan.startScan(ss, listener);
+        return listener.mIndex;
+    }
+
+    /**
+     * Stops a WifiScanner scan
+     *
+     * @param listenerIndex the id of the scan listener whose scan to stop
+     * @throws Exception
+     */
+    @Rpc(description = "Stops an ongoing  WifiScanner Single scan")
+    public void wifiScannerStopScan(@RpcParameter(name = "listener") Integer listenerIndex)
+            throws Exception {
+        if (!scanListeners.containsKey(listenerIndex)) {
+            throw new Exception("Single scan session " + listenerIndex + " does not exist");
+        }
+        WifiScanListener listener = scanListeners.get(listenerIndex);
+        Log.d("stopWifiScannerScan listener " + listener.mIndex);
+        mScan.stopScan(listener);
+        wifiScannerResultList.remove(listener.mIndex);
+        scanListeners.remove(listenerIndex);
+    }
+
+    /** RPC Methods */
+    @Rpc(description = "Returns the channels covered by the specified band number.")
+    public List<Integer> wifiScannerGetAvailableChannels(
+            @RpcParameter(name = "band") Integer band) {
+        return mScan.getAvailableChannels(band);
+    }
+
+    /**
+     * Starts tracking wifi changes
+     *
+     * @return the id of the change listener associated with this track
+     * @throws Exception
+     */
+    @Rpc(description = "Starts tracking wifi changes")
+    public Integer wifiScannerStartTrackingChange() throws Exception {
+        ChangeListener listener = genWifiChangeListener();
+        mScan.startTrackingWifiChange(listener);
+        return listener.mIndex;
+    }
+
+    /**
+     * Stops tracking wifi changes
+     *
+     * @param listenerIndex the id of the change listener whose track to stop
+     * @throws Exception
+     */
+    @Rpc(description = "Stops tracking wifi changes")
+    public void wifiScannerStopTrackingChange(
+            @RpcParameter(name = "listener") Integer listenerIndex) throws Exception {
+        if (!trackChangeListeners.containsKey(listenerIndex)) {
+            throw new Exception("Wifi change tracking session " + listenerIndex
+                    + " does not exist");
+        }
+        ChangeListener listener = trackChangeListeners.get(listenerIndex);
+        mScan.stopTrackingWifiChange(listener);
+        trackChangeListeners.remove(listenerIndex);
+    }
+
+    /**
+     * Starts tracking changes of the specified bssids.
+     *
+     * @param bssidInfos An array of json strings, each representing a BssidInfo object.
+     * @param apLostThreshold
+     * @return The index of the listener used to start the tracking.
+     * @throws JSONException
+     */
+    @Rpc(description = "Starts tracking changes of the specified bssids.")
+    public Integer wifiScannerStartTrackingBssids(
+            @RpcParameter(name = "bssidInfos") JSONArray bssidInfos,
+            @RpcParameter(name = "apLostThreshold") Integer apLostThreshold) throws JSONException {
+        BssidInfo[] bssids = parseBssidInfo(bssidInfos);
+        WifiBssidListener listener = genWifiBssidListener();
+        mScan.startTrackingBssids(bssids, apLostThreshold, listener);
+        return listener.mIndex;
+    }
+
+    /**
+     * Stops tracking the list of APs associated with the input listener
+     *
+     * @param listenerIndex the id of the bssid listener whose track to stop
+     * @throws Exception
+     */
+    @Rpc(description = "Stops tracking changes in the APs on the list")
+    public void wifiScannerStopTrackingBssids(
+            @RpcParameter(name = "listener") Integer listenerIndex) throws Exception {
+        if (!trackBssidListeners.containsKey(listenerIndex)) {
+            throw new Exception("Bssid tracking session " + listenerIndex + " does not exist");
+        }
+        WifiBssidListener listener = trackBssidListeners.get(listenerIndex);
+        mScan.stopTrackingBssids(listener);
+        trackBssidListeners.remove(listenerIndex);
+    }
+
+    @Rpc(description = "Toggle the 'WiFi scan always available' option. If an input is given, the "
+            + "option is set to what the input boolean indicates.")
+    public void wifiScannerToggleAlwaysAvailable(
+            @RpcParameter(name = "alwaysAvailable") @RpcOptional Boolean alwaysAvailable)
+                    throws SettingNotFoundException {
+        int new_state = 0;
+        if (alwaysAvailable == null) {
+            int current_state = Global.getInt(mService.getContentResolver(),
+                    Global.WIFI_SCAN_ALWAYS_AVAILABLE);
+            new_state = current_state ^ 0x1;
+        } else {
+            new_state = alwaysAvailable ? 1 : 0;
+        }
+        Global.putInt(mService.getContentResolver(), Global.WIFI_SCAN_ALWAYS_AVAILABLE, new_state);
+    }
+
+    @Rpc(description = "Returns true if WiFi scan is always available, false otherwise.")
+    public Boolean wifiScannerIsAlwaysAvailable() throws SettingNotFoundException {
+        int current_state = Global.getInt(mService.getContentResolver(),
+                Global.WIFI_SCAN_ALWAYS_AVAILABLE);
+        if (current_state == 1) {
+            return true;
+        }
+        return false;
+    }
+
+    @Rpc(description = "Returns a list of mIndexes of existing listeners")
+    public Set<Integer> wifiGetCurrentScanIndexes() {
+        return scanListeners.keySet();
+    }
+
+    /**
+     * Starts tracking wifi changes
+     *
+     * @return the id of the change listener associated with this track
+     * @throws Exception
+     */
+    @Rpc(description = "Starts tracking wifi changes with track settings")
+    public Integer wifiScannerStartTrackingChangeWithSetting(
+            @RpcParameter(name = "trackSettings") JSONArray bssidSettings,
+            @RpcParameter(name = "rssiSS") Integer rssiSS,
+            @RpcParameter(name = "lostApSS") Integer lostApSS,
+            @RpcParameter(name = "unchangedSS") Integer unchangedSS,
+            @RpcParameter(name = "minApsBreachingThreshold") Integer minApsBreachingThreshold,
+            @RpcParameter(name = "periodInMs") Integer periodInMs) throws Exception {
+        Log.d("starting change track with track settings");
+        BssidInfo[] bssids = parseBssidInfo(bssidSettings);
+        mScan.configureWifiChange(rssiSS, lostApSS, unchangedSS, minApsBreachingThreshold,
+              periodInMs, bssids);
+        ChangeListener listener = genWifiChangeListener();
+        mScan.startTrackingWifiChange(listener);
+        return listener.mIndex;
+    }
+
+    /**
+     * Shuts down all activities associated with WifiScanner
+     */
+    @Rpc(description = "Shuts down all WifiScanner activities and remove listeners.")
+    public void wifiScannerShutdown() {
+        this.shutdown();
+    }
+
+    /**
+     * Stops all activity
+     */
+    @Override
+    public void shutdown() {
+        try {
+            if (!scanListeners.isEmpty()) {
+                Iterator<ConcurrentHashMap.Entry<Integer, WifiScanListener>> iter = scanListeners
+                        .entrySet().iterator();
+                while (iter.hasNext()) {
+                    ConcurrentHashMap.Entry<Integer, WifiScanListener> entry = iter.next();
+                    this.wifiScannerStopScan(entry.getKey());
+                }
+            }
+            if (!scanBackgroundListeners.isEmpty()) {
+                Iterator<ConcurrentHashMap.Entry<Integer, WifiScanListener>> iter = scanBackgroundListeners
+                        .entrySet().iterator();
+                while (iter.hasNext()) {
+                    ConcurrentHashMap.Entry<Integer, WifiScanListener> entry = iter.next();
+                    this.wifiScannerStopBackgroundScan(entry.getKey());
+                }
+            }
+            if (!trackChangeListeners.isEmpty()) {
+                Iterator<ConcurrentHashMap.Entry<Integer, ChangeListener>> iter = trackChangeListeners
+                        .entrySet().iterator();
+                while (iter.hasNext()) {
+                    ConcurrentHashMap.Entry<Integer, ChangeListener> entry = iter.next();
+                    this.wifiScannerStopTrackingChange(entry.getKey());
+                }
+            }
+            if (!trackBssidListeners.isEmpty()) {
+                Iterator<ConcurrentHashMap.Entry<Integer, WifiBssidListener>> iter = trackBssidListeners
+                        .entrySet().iterator();
+                while (iter.hasNext()) {
+                    ConcurrentHashMap.Entry<Integer, WifiBssidListener> entry = iter.next();
+                    this.wifiScannerStopTrackingBssids(entry.getKey());
+                }
+            }
+        } catch (Exception e) {
+            Log.e("Shutdown failed: " + e.toString());
+        }
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/future/FutureActivityTask.java b/Common/src/com/googlecode/android_scripting/future/FutureActivityTask.java
new file mode 100644
index 0000000..b474736
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/future/FutureActivityTask.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.future;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.View;
+import android.view.Window;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Encapsulates an {@link Activity} and a {@link FutureObject}.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public abstract class FutureActivityTask<T> {
+
+  private final FutureResult<T> mResult = new FutureResult<T>();
+  private Activity mActivity;
+
+  public void setActivity(Activity activity) {
+    mActivity = activity;
+  }
+
+  public Activity getActivity() {
+    return mActivity;
+  }
+
+  public void onCreate() {
+    mActivity.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+  }
+
+  public void onStart() {
+  }
+
+  public void onResume() {
+  }
+
+  public void onPause() {
+  }
+
+  public void onStop() {
+  }
+
+  public void onDestroy() {
+  }
+
+  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+  }
+
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    return false;
+  }
+
+  public void onActivityResult(int requestCode, int resultCode, Intent data) {
+  }
+
+  protected void setResult(T result) {
+    mResult.set(result);
+  }
+
+  public T getResult() throws InterruptedException {
+    return mResult.get();
+  }
+
+  public T getResult(long timeout, TimeUnit unit) throws InterruptedException {
+    return mResult.get(timeout, unit);
+  }
+
+  public void finish() {
+    mActivity.finish();
+  }
+
+  public void startActivity(Intent intent) {
+    mActivity.startActivity(intent);
+  }
+
+  public void startActivityForResult(Intent intent, int requestCode) {
+    mActivity.startActivityForResult(intent, requestCode);
+  }
+
+  public boolean onKeyDown(int keyCode, KeyEvent event) {
+    // Placeholder.
+    return false;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/interpreter/InProcessInterpreter.java b/Common/src/com/googlecode/android_scripting/interpreter/InProcessInterpreter.java
new file mode 100644
index 0000000..48de556
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/interpreter/InProcessInterpreter.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+import java.io.FileDescriptor;
+
+public interface InProcessInterpreter {
+  public FileDescriptor getStdOut();
+
+  public FileDescriptor getStdIn();
+
+  public boolean runInteractive();
+
+  public void runScript(String filename);
+}
diff --git a/Common/src/com/googlecode/android_scripting/interpreter/Interpreter.java b/Common/src/com/googlecode/android_scripting/interpreter/Interpreter.java
new file mode 100644
index 0000000..7a69032
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/interpreter/Interpreter.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+import com.googlecode.android_scripting.language.Language;
+import com.googlecode.android_scripting.language.SupportedLanguages;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Combines all the execution-related specs of a particular interpreter installed in the system.
+ * This class is instantiated through a map received from a concrete InterpreterProfider.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public class Interpreter implements InterpreterPropertyNames {
+
+  private String mExtension;
+  private String mName;
+  private String mNiceName;
+  private String mInteractiveCommand;
+  private String mScriptExecutionCommand;
+  private File mBinary;
+  private boolean mHasInteractiveMode;
+  private final List<String> mArguments;
+  private final Map<String, String> mEnvironment;
+  private Language mLanguage;
+
+  public Interpreter() {
+    mArguments = new ArrayList<String>();
+    mEnvironment = new HashMap<String, String>();
+  }
+
+  public static Interpreter buildFromMaps(Map<String, String> data,
+      Map<String, String> environment_variables, Map<String, String> arguments) {
+    String extension = data.get(EXTENSION);
+    String name = data.get(NAME);
+    String niceName = data.get(NICE_NAME);
+    String binary = data.get(BINARY);
+    String interactiveCommand = data.get(INTERACTIVE_COMMAND);
+    String scriptCommand = data.get(SCRIPT_COMMAND);
+    Boolean hasInteractiveMode;
+    if (data.containsKey(HAS_INTERACTIVE_MODE)) {
+      hasInteractiveMode = Boolean.parseBoolean(data.get(HAS_INTERACTIVE_MODE));
+    } else {
+      // Default to true so that older interpreter APKs that don't have this value define still
+      // work.
+      hasInteractiveMode = true;
+    }
+    Interpreter interpreter = new Interpreter();
+    interpreter.setName(name);
+    interpreter.setNiceName(niceName);
+    interpreter.setExtension(extension);
+    interpreter.setBinary(new File(binary));
+    interpreter.setInteractiveCommand(interactiveCommand);
+    interpreter.setScriptCommand(scriptCommand);
+    interpreter.setHasInteractiveMode(hasInteractiveMode);
+    interpreter.setLanguage(SupportedLanguages.getLanguageByExtension(extension));
+    interpreter.putAllEnvironmentVariables(environment_variables);
+    interpreter.addAllArguments(arguments.values());
+    return interpreter;
+  }
+
+  // TODO(damonkohler): This should take a List<String> since order is important.
+  private void addAllArguments(Collection<String> arguments) {
+    mArguments.addAll(arguments);
+  }
+
+  List<String> getArguments() {
+    return mArguments;
+  }
+
+  private void putAllEnvironmentVariables(Map<String, String> environmentVariables) {
+    mEnvironment.putAll(environmentVariables);
+  }
+
+  public Map<String, String> getEnvironmentVariables() {
+    return mEnvironment;
+  }
+
+  protected void setScriptCommand(String executeParameters) {
+    mScriptExecutionCommand = executeParameters;
+  }
+
+  public String getScriptCommand() {
+    return mScriptExecutionCommand;
+  }
+
+  protected void setInteractiveCommand(String interactiveCommand) {
+    mInteractiveCommand = interactiveCommand;
+  }
+
+  public String getInteractiveCommand() {
+    return mInteractiveCommand;
+  }
+
+  protected void setBinary(File binary) {
+    if (!binary.exists()) {
+      throw new RuntimeException("Binary " + binary + " does not exist!");
+    }
+    mBinary = binary;
+  }
+
+  public File getBinary() {
+    return mBinary;
+  }
+
+  protected void setExtension(String extension) {
+    mExtension = extension;
+  }
+
+  protected void setHasInteractiveMode(boolean hasInteractiveMode) {
+    mHasInteractiveMode = hasInteractiveMode;
+  }
+
+  public boolean hasInteractiveMode() {
+    return mHasInteractiveMode;
+  }
+
+  public String getExtension() {
+    return mExtension;
+  }
+
+  protected void setName(String name) {
+    mName = name;
+  }
+
+  public String getName() {
+    return mName;
+  }
+
+  protected void setNiceName(String niceName) {
+    mNiceName = niceName;
+  }
+
+  public String getNiceName() {
+    return mNiceName;
+  }
+
+  public String getContentTemplate() {
+    return mLanguage.getContentTemplate();
+  }
+
+  protected void setLanguage(Language language) {
+    mLanguage = language;
+  }
+
+  public Language getLanguage() {
+    return mLanguage;
+  }
+
+  public String getRpcText(String content, MethodDescriptor rpc, String[] values) {
+    return mLanguage.getRpcText(content, rpc, values);
+  }
+
+  public boolean isInstalled() {
+    return mBinary.exists();
+  }
+
+  public boolean isUninstallable() {
+    return true;
+  }
+}
\ No newline at end of file
diff --git a/Common/src/com/googlecode/android_scripting/interpreter/InterpreterConfiguration.java b/Common/src/com/googlecode/android_scripting/interpreter/InterpreterConfiguration.java
new file mode 100644
index 0000000..02bd654
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/interpreter/InterpreterConfiguration.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.Cursor;
+import android.net.Uri;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.SingleThreadExecutor;
+import com.googlecode.android_scripting.interpreter.html.HtmlInterpreter;
+import com.googlecode.android_scripting.interpreter.shell.ShellInterpreter;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Manages and provides access to the set of available interpreters.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class InterpreterConfiguration {
+
+  private final InterpreterListener mListener;
+  private final Set<Interpreter> mInterpreterSet;
+  private final Set<ConfigurationObserver> mObserverSet;
+  private final Context mContext;
+  private volatile boolean mIsDiscoveryComplete = false;
+
+  public interface ConfigurationObserver {
+    public void onConfigurationChanged();
+  }
+
+  private class InterpreterListener extends BroadcastReceiver {
+    private final PackageManager mmPackageManager;
+    private final ContentResolver mmResolver;
+    private final ExecutorService mmExecutor;
+    private final Map<String, Interpreter> mmDiscoveredInterpreters;
+
+    private InterpreterListener(Context context) {
+      mmPackageManager = context.getPackageManager();
+      mmResolver = context.getContentResolver();
+      mmExecutor = new SingleThreadExecutor();
+      mmDiscoveredInterpreters = new HashMap<String, Interpreter>();
+    }
+
+    private void discoverForType(final String mime) {
+      mmExecutor.execute(new Runnable() {
+        @Override
+        public void run() {
+          Intent intent = new Intent(InterpreterConstants.ACTION_DISCOVER_INTERPRETERS);
+          intent.addCategory(Intent.CATEGORY_LAUNCHER);
+          intent.setType(mime);
+          List<ResolveInfo> resolveInfos = mmPackageManager.queryIntentActivities(intent, 0);
+          for (ResolveInfo info : resolveInfos) {
+            addInterpreter(info.activityInfo.packageName);
+          }
+          mIsDiscoveryComplete = true;
+          notifyConfigurationObservers();
+        }
+      });
+    }
+
+    private void discoverAll() {
+      mmExecutor.execute(new Runnable() {
+        @Override
+        public void run() {
+          Intent intent = new Intent(InterpreterConstants.ACTION_DISCOVER_INTERPRETERS);
+          intent.addCategory(Intent.CATEGORY_LAUNCHER);
+          intent.setType(InterpreterConstants.MIME + "*");
+          List<ResolveInfo> resolveInfos = mmPackageManager.queryIntentActivities(intent, 0);
+          for (ResolveInfo info : resolveInfos) {
+            addInterpreter(info.activityInfo.packageName);
+          }
+          mIsDiscoveryComplete = true;
+          notifyConfigurationObservers();
+        }
+      });
+    }
+
+    private void notifyConfigurationObservers() {
+      for (ConfigurationObserver observer : mObserverSet) {
+        observer.onConfigurationChanged();
+      }
+    }
+
+    private void addInterpreter(final String packageName) {
+      if (mmDiscoveredInterpreters.containsKey(packageName)) {
+        return;
+      }
+      Interpreter discoveredInterpreter = buildInterpreter(packageName);
+      if (discoveredInterpreter == null) {
+        return;
+      }
+      mmDiscoveredInterpreters.put(packageName, discoveredInterpreter);
+      mInterpreterSet.add(discoveredInterpreter);
+      Log.v("Interpreter discovered: " + packageName + "\nBinary: "
+          + discoveredInterpreter.getBinary());
+    }
+
+    private void remove(final String packageName) {
+      if (!mmDiscoveredInterpreters.containsKey(packageName)) {
+        return;
+      }
+      mmExecutor.execute(new Runnable() {
+        @Override
+        public void run() {
+          Interpreter interpreter = mmDiscoveredInterpreters.get(packageName);
+          if (interpreter == null) {
+            Log.v("Interpreter for " + packageName + " not installed.");
+            return;
+          }
+          mInterpreterSet.remove(interpreter);
+          mmDiscoveredInterpreters.remove(packageName);
+          notifyConfigurationObservers();
+        }
+      });
+    }
+
+    // We require that there's only one interpreter provider per APK.
+    private Interpreter buildInterpreter(String packageName) {
+      PackageInfo packInfo;
+      try {
+        packInfo = mmPackageManager.getPackageInfo(packageName, PackageManager.GET_PROVIDERS);
+      } catch (NameNotFoundException e) {
+        throw new RuntimeException("Package '" + packageName + "' not found.");
+      }
+      ProviderInfo provider = packInfo.providers[0];
+
+      Map<String, String> interpreterMap =
+          getMap(provider, InterpreterConstants.PROVIDER_PROPERTIES);
+      if (interpreterMap == null) {
+        Log.e("Null interpreter map for: " + packageName);
+        return null;
+      }
+      Map<String, String> environmentMap =
+          getMap(provider, InterpreterConstants.PROVIDER_ENVIRONMENT_VARIABLES);
+      if (environmentMap == null) {
+        throw new RuntimeException("Null environment map for: " + packageName);
+      }
+      Map<String, String> argumentsMap = getMap(provider, InterpreterConstants.PROVIDER_ARGUMENTS);
+      if (argumentsMap == null) {
+        throw new RuntimeException("Null arguments map for: " + packageName);
+      }
+      return Interpreter.buildFromMaps(interpreterMap, environmentMap, argumentsMap);
+    }
+
+    private Map<String, String> getMap(ProviderInfo provider, String name) {
+      Uri uri = Uri.parse("content://" + provider.authority + "/" + name);
+      Cursor cursor = mmResolver.query(uri, null, null, null, null);
+      if (cursor == null) {
+        return null;
+      }
+      cursor.moveToFirst();
+      // Use LinkedHashMap so that order is maintained (important for position CLI arguments).
+      Map<String, String> map = new LinkedHashMap<String, String>();
+      for (int i = 0; i < cursor.getColumnCount(); i++) {
+        map.put(cursor.getColumnName(i), cursor.getString(i));
+      }
+      return map;
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+      final String action = intent.getAction();
+      final String packageName = intent.getData().getSchemeSpecificPart();
+      if (action.equals(InterpreterConstants.ACTION_INTERPRETER_ADDED)) {
+        mmExecutor.execute(new Runnable() {
+          @Override
+          public void run() {
+            addInterpreter(packageName);
+            notifyConfigurationObservers();
+          }
+        });
+      } else if (action.equals(InterpreterConstants.ACTION_INTERPRETER_REMOVED)
+          || action.equals(Intent.ACTION_PACKAGE_REMOVED)
+          || action.equals(Intent.ACTION_PACKAGE_REPLACED)
+          || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) {
+        remove(packageName);
+      }
+    }
+
+  }
+
+  public InterpreterConfiguration(Context context) {
+    mContext = context;
+    mInterpreterSet = new CopyOnWriteArraySet<Interpreter>();
+    mInterpreterSet.add(new ShellInterpreter());
+    try {
+      mInterpreterSet.add(new HtmlInterpreter(mContext));
+    } catch (IOException e) {
+      Log.e("Failed to instantiate HtmlInterpreter.", e);
+    }
+    mObserverSet = new CopyOnWriteArraySet<ConfigurationObserver>();
+    IntentFilter filter = new IntentFilter();
+    filter.addAction(InterpreterConstants.ACTION_INTERPRETER_ADDED);
+    filter.addAction(InterpreterConstants.ACTION_INTERPRETER_REMOVED);
+    filter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
+    filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+    filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+    filter.addDataScheme("package");
+    mListener = new InterpreterListener(mContext);
+    mContext.registerReceiver(mListener, filter);
+  }
+
+  public void startDiscovering() {
+    mListener.discoverAll();
+  }
+
+  public void startDiscovering(String mime) {
+    mListener.discoverForType(mime);
+  }
+
+  public boolean isDiscoveryComplete() {
+    return mIsDiscoveryComplete;
+  }
+
+  public void registerObserver(ConfigurationObserver observer) {
+    if (observer != null) {
+      mObserverSet.add(observer);
+    }
+  }
+
+  public void unregisterObserver(ConfigurationObserver observer) {
+    if (observer != null) {
+      mObserverSet.remove(observer);
+    }
+  }
+
+  /**
+   * Returns the list of all known interpreters.
+   */
+  public List<? extends Interpreter> getSupportedInterpreters() {
+    return new ArrayList<Interpreter>(mInterpreterSet);
+  }
+
+  /**
+   * Returns the list of all installed interpreters.
+   */
+  public List<Interpreter> getInstalledInterpreters() {
+    List<Interpreter> interpreters = new ArrayList<Interpreter>();
+    for (Interpreter i : mInterpreterSet) {
+      if (i.isInstalled()) {
+        interpreters.add(i);
+      }
+    }
+    return interpreters;
+  }
+
+  /**
+   * Returns the list of interpreters that support interactive mode execution.
+   */
+  public List<Interpreter> getInteractiveInterpreters() {
+    List<Interpreter> interpreters = new ArrayList<Interpreter>();
+    for (Interpreter i : mInterpreterSet) {
+      if (i.isInstalled() && i.hasInteractiveMode()) {
+        interpreters.add(i);
+      }
+    }
+    return interpreters;
+  }
+
+  /**
+   * Returns the interpreter matching the provided name or null if no interpreter was found.
+   */
+  public Interpreter getInterpreterByName(String interpreterName) {
+    for (Interpreter i : mInterpreterSet) {
+      if (i.getName().equals(interpreterName)) {
+        return i;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns the correct interpreter for the provided script name based on the script's extension or
+   * null if no interpreter was found.
+   */
+  public Interpreter getInterpreterForScript(String scriptName) {
+    int dotIndex = scriptName.lastIndexOf('.');
+    if (dotIndex == -1) {
+      return null;
+    }
+    String ext = scriptName.substring(dotIndex);
+    for (Interpreter i : mInterpreterSet) {
+      if (i.getExtension().equals(ext)) {
+        return i;
+      }
+    }
+    return null;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/interpreter/html/HtmlActivityTask.java b/Common/src/com/googlecode/android_scripting/interpreter/html/HtmlActivityTask.java
new file mode 100644
index 0000000..fa5d768
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/interpreter/html/HtmlActivityTask.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter.html;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Menu;
+import android.view.View;
+import android.view.Window;
+import android.webkit.JsPromptResult;
+import android.webkit.JsResult;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import com.googlecode.android_scripting.FileUtils;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.SingleThreadExecutor;
+import com.googlecode.android_scripting.event.Event;
+import com.googlecode.android_scripting.event.EventObserver;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.ui.UiFacade;
+import com.googlecode.android_scripting.future.FutureActivityTask;
+import com.googlecode.android_scripting.interpreter.InterpreterConstants;
+import com.googlecode.android_scripting.jsonrpc.JsonBuilder;
+import com.googlecode.android_scripting.jsonrpc.JsonRpcResult;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+import com.googlecode.android_scripting.rpc.RpcError;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public class HtmlActivityTask extends FutureActivityTask<Void> {
+
+  private static final String HTTP = "http";
+  private static final String ANDROID_PROTOTYPE_JS =
+      "Android.prototype.%1$s = function(var_args) { "
+          + "return this._call(\"%1$s\", Array.prototype.slice.call(arguments)); };";
+
+  private static final String PREFIX = "file://";
+  private static final String BASE_URL = PREFIX + InterpreterConstants.SCRIPTS_ROOT;
+
+  private final RpcReceiverManager mReceiverManager;
+  private final String mJsonSource;
+  private final String mAndroidJsSource;
+  private final String mAPIWrapperSource;
+  private final String mUrl;
+  private final JavaScriptWrapper mWrapper;
+  private final HtmlEventObserver mObserver;
+  private final UiFacade mUiFacade;
+  private ChromeClient mChromeClient;
+  private WebView mView;
+  private MyWebViewClient mWebViewClient;
+  private static HtmlActivityTask reference;
+  private boolean mDestroyManager;
+
+  public HtmlActivityTask(RpcReceiverManager manager, String androidJsSource, String jsonSource,
+      String url, boolean destroyManager) {
+    reference = this;
+    mReceiverManager = manager;
+    mJsonSource = jsonSource;
+    mAndroidJsSource = androidJsSource;
+    mAPIWrapperSource = generateAPIWrapper();
+    mWrapper = new JavaScriptWrapper();
+    mObserver = new HtmlEventObserver();
+    mReceiverManager.getReceiver(EventFacade.class).addGlobalEventObserver(mObserver);
+    mUiFacade = mReceiverManager.getReceiver(UiFacade.class);
+    mUrl = url;
+    mDestroyManager = destroyManager;
+  }
+
+  public RpcReceiverManager getRpcReceiverManager() {
+    return mReceiverManager;
+  }
+
+  /*
+   * New WebviewClient
+   */
+  private class MyWebViewClient extends WebViewClient {
+    @Override
+    public boolean shouldOverrideUrlLoading(WebView view, String url) {
+      /*
+       * if (Uri.parse(url).getHost().equals("www.example.com")) {
+       * // This is my web site, so do not
+       * override; let my WebView load the page return false; }
+       * // Otherwise, the link is not for a
+       * page on my site, so launch another Activity that handles URLs Intent intent = new
+       * Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent);
+       */
+      if (!HTTP.equals(Uri.parse(url).getScheme())) {
+        String source = null;
+        try {
+          source = FileUtils.readToString(new File(Uri.parse(url).getPath()));
+        } catch (IOException e) {
+          throw new RuntimeException(e);
+        }
+        source =
+            "<script>" + mJsonSource + "</script>" + "<script>" + mAndroidJsSource + "</script>"
+                + "<script>" + mAPIWrapperSource + "</script>" + source;
+        mView.loadDataWithBaseURL(BASE_URL, source, "text/html", "utf-8", null);
+      } else {
+        mView.loadUrl(url);
+      }
+      return true;
+    }
+  }
+
+  @Override
+  public void onCreate() {
+    mView = new WebView(getActivity());
+    mView.setId(1);
+    mView.getSettings().setJavaScriptEnabled(true);
+    mView.addJavascriptInterface(mWrapper, "_rpc_wrapper");
+    mView.addJavascriptInterface(new Object() {
+
+      @SuppressWarnings("unused")
+      public void register(String event, int id) {
+        mObserver.register(event, id);
+      }
+    }, "_callback_wrapper");
+
+    getActivity().setContentView(mView);
+    mView.setOnCreateContextMenuListener(getActivity());
+    mChromeClient = new ChromeClient(getActivity());
+    mWebViewClient = new MyWebViewClient();
+    mView.setWebChromeClient(mChromeClient);
+    mView.setWebViewClient(mWebViewClient);
+    mView.loadUrl("javascript:" + mJsonSource);
+    mView.loadUrl("javascript:" + mAndroidJsSource);
+    mView.loadUrl("javascript:" + mAPIWrapperSource);
+    load();
+  }
+
+  private void load() {
+    if (!HTTP.equals(Uri.parse(mUrl).getScheme())) {
+      String source = null;
+      try {
+        source = FileUtils.readToString(new File(Uri.parse(mUrl).getPath()));
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+      mView.loadDataWithBaseURL(BASE_URL, source, "text/html", "utf-8", null);
+    } else {
+      mView.loadUrl(mUrl);
+    }
+  }
+
+  @Override
+  public void onDestroy() {
+    mReceiverManager.getReceiver(EventFacade.class).removeEventObserver(mObserver);
+    if (mDestroyManager) {
+      mReceiverManager.shutdown();
+    }
+    mView.destroy();
+    mView = null;
+    reference = null;
+    setResult(null);
+  }
+
+  public static void shutdown() {
+    if (HtmlActivityTask.reference != null) {
+      HtmlActivityTask.reference.finish();
+    }
+  }
+
+  @Override
+  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+    mUiFacade.onCreateContextMenu(menu, v, menuInfo);
+  }
+
+  @Override
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    return mUiFacade.onPrepareOptionsMenu(menu);
+  }
+
+  private String generateAPIWrapper() {
+    StringBuilder wrapper = new StringBuilder();
+    for (Class<? extends RpcReceiver> clazz : mReceiverManager.getRpcReceiverClasses()) {
+      for (MethodDescriptor rpc : MethodDescriptor.collectFrom(clazz)) {
+        wrapper.append(String.format(ANDROID_PROTOTYPE_JS, rpc.getName()));
+      }
+    }
+    return wrapper.toString();
+  }
+
+  private class JavaScriptWrapper {
+    @SuppressWarnings("unused")
+    public String call(String data) throws JSONException {
+      Log.v("Received: " + data);
+      JSONObject request = new JSONObject(data);
+      int id = request.getInt("id");
+      String method = request.getString("method");
+      JSONArray params = request.getJSONArray("params");
+      MethodDescriptor rpc = mReceiverManager.getMethodDescriptor(method);
+      if (rpc == null) {
+        return JsonRpcResult.error(id, new RpcError("Unknown RPC.")).toString();
+      }
+      try {
+        return JsonRpcResult.result(id, rpc.invoke(mReceiverManager, params)).toString();
+      } catch (Throwable t) {
+        Log.e("Invocation error.", t);
+        return JsonRpcResult.error(id, t).toString();
+      }
+    }
+
+    @SuppressWarnings("unused")
+    public void dismiss() {
+      Activity parent = getActivity();
+      parent.finish();
+    }
+  }
+
+  private class HtmlEventObserver implements EventObserver {
+    private Map<String, Set<Integer>> mEventMap = new HashMap<String, Set<Integer>>();
+
+    public void register(String eventName, Integer id) {
+      if (mEventMap.containsKey(eventName)) {
+        mEventMap.get(eventName).add(id);
+      } else {
+        Set<Integer> idSet = new HashSet<Integer>();
+        idSet.add(id);
+        mEventMap.put(eventName, idSet);
+      }
+    }
+
+    @Override
+    public void onEventReceived(Event event) {
+      final JSONObject json = new JSONObject();
+      try {
+        json.put("data", JsonBuilder.build(event.getData()));
+      } catch (JSONException e) {
+        Log.e(e);
+      }
+      if (mEventMap.containsKey(event.getName())) {
+        for (final Integer id : mEventMap.get(event.getName())) {
+          getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+              mView.loadUrl(String.format("javascript:droid._callback(%d, %s);", id, json));
+            }
+          });
+        }
+      }
+    }
+
+    @SuppressWarnings("unused")
+    public void dismiss() {
+      Activity parent = getActivity();
+      parent.finish();
+    }
+  }
+
+  private class ChromeClient extends WebChromeClient {
+    private final static String JS_TITLE = "JavaScript Dialog";
+
+    private final Activity mActivity;
+    private final Resources mResources;
+    private final ExecutorService mmExecutor;
+
+    public ChromeClient(Activity activity) {
+      mActivity = activity;
+      mResources = mActivity.getResources();
+      mmExecutor = new SingleThreadExecutor();
+    }
+
+    @Override
+    public void onReceivedTitle(WebView view, String title) {
+      mActivity.setTitle(title);
+    }
+
+    @Override
+    public void onReceivedIcon(WebView view, Bitmap icon) {
+      mActivity.getWindow().requestFeature(Window.FEATURE_RIGHT_ICON);
+      mActivity.getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON,
+                                               new BitmapDrawable(mActivity.getResources(), icon));
+    }
+
+    @Override
+    public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
+      final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
+      uiFacade.dialogCreateAlert(JS_TITLE, message);
+      uiFacade.dialogSetPositiveButtonText(mResources.getString(android.R.string.ok));
+
+      mmExecutor.execute(new Runnable() {
+
+        @Override
+        public void run() {
+          try {
+            uiFacade.dialogShow();
+          } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+          }
+          uiFacade.dialogGetResponse();
+          result.confirm();
+        }
+      });
+      return true;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
+      final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
+      uiFacade.dialogCreateAlert(JS_TITLE, message);
+      uiFacade.dialogSetPositiveButtonText(mResources.getString(android.R.string.ok));
+      uiFacade.dialogSetNegativeButtonText(mResources.getString(android.R.string.cancel));
+
+      mmExecutor.execute(new Runnable() {
+
+        @Override
+        public void run() {
+          try {
+            uiFacade.dialogShow();
+          } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+          }
+          Map<String, Object> mResultMap = (Map<String, Object>) uiFacade.dialogGetResponse();
+          if ("positive".equals(mResultMap.get("which"))) {
+            result.confirm();
+          } else {
+            result.cancel();
+          }
+        }
+      });
+
+      return true;
+    }
+
+    @Override
+    public boolean onJsPrompt(WebView view, String url, final String message,
+        final String defaultValue, final JsPromptResult result) {
+      final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
+      mmExecutor.execute(new Runnable() {
+        @Override
+        public void run() {
+          String value = null;
+          try {
+            value = uiFacade.dialogGetInput(JS_TITLE, message, defaultValue);
+          } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+          }
+          if (value != null) {
+            result.confirm(value);
+          } else {
+            result.cancel();
+          }
+        }
+      });
+      return true;
+    }
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/interpreter/html/HtmlInterpreter.java b/Common/src/com/googlecode/android_scripting/interpreter/html/HtmlInterpreter.java
new file mode 100644
index 0000000..3f86ff0
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/interpreter/html/HtmlInterpreter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter.html;
+
+import android.content.Context;
+
+import com.googlecode.android_scripting.FileUtils;
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.language.HtmlLanguage;
+
+import java.io.IOException;
+
+public class HtmlInterpreter extends Interpreter {
+
+  public static final String HTML = "html";
+  public static final String HTML_EXTENSION = ".html";
+
+  public static final String JSON_FILE = "json2.js";
+  public static final String ANDROID_JS_FILE = "android.js";
+  public static final String HTML_NICE_NAME = "HTML and JavaScript";
+
+  private final String mJson;
+  private final String mAndroidJs;
+
+  public HtmlInterpreter(Context context) throws IOException {
+    setExtension(HTML_EXTENSION);
+    setName(HTML);
+    setNiceName(HTML_NICE_NAME);
+    setInteractiveCommand("");
+    setScriptCommand("%s");
+    setLanguage(new HtmlLanguage());
+    setHasInteractiveMode(false);
+    mJson = FileUtils.readFromAssetsFile(context, JSON_FILE);
+    mAndroidJs = FileUtils.readFromAssetsFile(context, ANDROID_JS_FILE);
+  }
+
+  public boolean hasInterpreterArchive() {
+    return false;
+  }
+
+  public boolean hasExtrasArchive() {
+    return false;
+  }
+
+  public boolean hasScriptsArchive() {
+    return false;
+  }
+
+  public int getVersion() {
+    return 0;
+  }
+
+  @Override
+  public boolean isUninstallable() {
+    return false;
+  }
+
+  @Override
+  public boolean isInstalled() {
+    return true;
+  }
+
+  public String getJsonSource() {
+    return mJson;
+  }
+
+  public String getAndroidJsSource() {
+    return mAndroidJs;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/interpreter/shell/ShellInterpreter.java b/Common/src/com/googlecode/android_scripting/interpreter/shell/ShellInterpreter.java
new file mode 100644
index 0000000..a1f11ff
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/interpreter/shell/ShellInterpreter.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter.shell;
+
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.language.ShellLanguage;
+
+import java.io.File;
+
+/**
+ * Represents the shell.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class ShellInterpreter extends Interpreter {
+  private final static String SHELL_BIN = "/system/bin/sh";
+
+  public ShellInterpreter() {
+    setExtension(".sh");
+    setName("sh");
+    setNiceName("Shell");
+    setBinary(new File(SHELL_BIN));
+    setInteractiveCommand("");
+    setScriptCommand("%s");
+    setLanguage(new ShellLanguage());
+    setHasInteractiveMode(true);
+  }
+
+  public boolean hasInterpreterArchive() {
+    return false;
+  }
+
+  public boolean hasExtrasArchive() {
+    return false;
+  }
+
+  public boolean hasScriptsArchive() {
+    return false;
+  }
+
+  public int getVersion() {
+    return 0;
+  }
+
+  @Override
+  public boolean isUninstallable() {
+    return false;
+  }
+
+  @Override
+  public boolean isInstalled() {
+    return true;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/jsonrpc/JsonBuilder.java b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonBuilder.java
new file mode 100644
index 0000000..24428a4
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonBuilder.java
@@ -0,0 +1,1188 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.jsonrpc;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.security.PrivateKey;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.commons.codec.binary.Base64Codec;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.le.AdvertiseSettings;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Point;
+import android.location.Address;
+import android.location.Location;
+import android.net.DhcpInfo;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.wifi.RttManager.RttCapabilities;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiActivityEnergyInfo;
+import android.net.wifi.WifiChannel;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiScanner.ScanData;
+import android.net.wifi.p2p.WifiP2pDevice;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pInfo;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import android.telephony.CellIdentityCdma;
+import android.telephony.CellIdentityGsm;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellIdentityWcdma;
+import android.telephony.CellInfoCdma;
+import android.telephony.CellInfoGsm;
+import android.telephony.CellInfoLte;
+import android.telephony.CellInfoWcdma;
+import android.telephony.CellLocation;
+import android.telephony.CellSignalStrengthCdma;
+import android.telephony.CellSignalStrengthGsm;
+import android.telephony.CellSignalStrengthLte;
+import android.telephony.CellSignalStrengthWcdma;
+import android.telephony.ModemActivityInfo;
+import android.telephony.NeighboringCellInfo;
+import android.telephony.SmsMessage;
+import android.telephony.SignalStrength;
+import android.telephony.SubscriptionInfo;
+import android.telephony.gsm.GsmCellLocation;
+import android.telephony.VoLteServiceState;
+import android.util.Base64;
+import android.util.DisplayMetrics;
+import android.util.SparseArray;
+
+import com.googlecode.android_scripting.ConvertUtils;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.event.Event;
+//FIXME: Refactor classes, constants and conversions out of here
+import com.googlecode.android_scripting.facade.telephony.InCallServiceImpl;
+import com.googlecode.android_scripting.facade.telephony.TelephonyUtils;
+import com.googlecode.android_scripting.facade.telephony.TelephonyConstants;
+
+public class JsonBuilder {
+
+    @SuppressWarnings("unchecked")
+    public static Object build(Object data) throws JSONException {
+        if (data == null) {
+            return JSONObject.NULL;
+        }
+        if (data instanceof Integer) {
+            return data;
+        }
+        if (data instanceof Float) {
+            return data;
+        }
+        if (data instanceof Double) {
+            return data;
+        }
+        if (data instanceof Long) {
+            return data;
+        }
+        if (data instanceof String) {
+            return data;
+        }
+        if (data instanceof Boolean) {
+            return data;
+        }
+        if (data instanceof JsonSerializable) {
+            return ((JsonSerializable)data).toJSON();
+        }
+        if (data instanceof JSONObject) {
+            return data;
+        }
+        if (data instanceof JSONArray) {
+            return data;
+        }
+        if (data instanceof Set<?>) {
+            List<Object> items = new ArrayList<Object>((Set<?>) data);
+            return buildJsonList(items);
+        }
+        if (data instanceof Collection<?>) {
+            List<Object> items = new ArrayList<Object>((Collection<?>) data);
+            return buildJsonList(items);
+        }
+        if (data instanceof List<?>) {
+            return buildJsonList((List<?>) data);
+        }
+        if (data instanceof Address) {
+            return buildJsonAddress((Address) data);
+        }
+        if (data instanceof CallAudioState) {
+            return buildJsonAudioState((CallAudioState) data);
+        }
+        if (data instanceof Location) {
+            return buildJsonLocation((Location) data);
+        }
+        if (data instanceof Bundle) {
+            return buildJsonBundle((Bundle) data);
+        }
+        if (data instanceof Intent) {
+            return buildJsonIntent((Intent) data);
+        }
+        if (data instanceof Event) {
+            return buildJsonEvent((Event) data);
+        }
+        if (data instanceof Map<?, ?>) {
+            // TODO(damonkohler): I would like to make this a checked cast if
+            // possible.
+            return buildJsonMap((Map<String, ?>) data);
+        }
+        if (data instanceof ParcelUuid) {
+            return data.toString();
+        }
+        if (data instanceof ScanResult) {
+            return buildJsonScanResult((ScanResult) data);
+        }
+        if (data instanceof ScanData) {
+            return buildJsonScanData((ScanData) data);
+        }
+        if (data instanceof android.bluetooth.le.ScanResult) {
+            return buildJsonBleScanResult((android.bluetooth.le.ScanResult) data);
+        }
+        if (data instanceof AdvertiseSettings) {
+            return buildJsonBleAdvertiseSettings((AdvertiseSettings) data);
+        }
+        if (data instanceof BluetoothGattService) {
+            return buildJsonBluetoothGattService((BluetoothGattService) data);
+        }
+        if (data instanceof BluetoothGattCharacteristic) {
+            return buildJsonBluetoothGattCharacteristic((BluetoothGattCharacteristic) data);
+        }
+        if (data instanceof BluetoothGattDescriptor) {
+            return buildJsonBluetoothGattDescriptor((BluetoothGattDescriptor) data);
+        }
+        if (data instanceof BluetoothDevice) {
+            return buildJsonBluetoothDevice((BluetoothDevice) data);
+        }
+        if (data instanceof CellLocation) {
+            return buildJsonCellLocation((CellLocation) data);
+        }
+        if (data instanceof WifiInfo) {
+            return buildJsonWifiInfo((WifiInfo) data);
+        }
+        if (data instanceof NeighboringCellInfo) {
+            return buildNeighboringCellInfo((NeighboringCellInfo) data);
+        }
+        if (data instanceof Network) {
+            return buildNetwork((Network) data);
+        }
+        if (data instanceof NetworkInfo) {
+            return buildNetworkInfo((NetworkInfo) data);
+        }
+        if (data instanceof HttpURLConnection) {
+            return buildHttpURLConnection((HttpURLConnection) data);
+        }
+        if (data instanceof InetSocketAddress) {
+            return buildInetSocketAddress((InetSocketAddress) data);
+        }
+        if (data instanceof InetAddress) {
+            return buildInetAddress((InetAddress) data);
+        }
+        if (data instanceof URL) {
+            return buildURL((URL) data);
+        }
+        if (data instanceof Point) {
+            return buildPoint((Point) data);
+        }
+        if (data instanceof SmsMessage) {
+            return buildSmsMessage((SmsMessage) data);
+        }
+        if (data instanceof PhoneAccount) {
+            return buildPhoneAccount((PhoneAccount) data);
+        }
+        if (data instanceof PhoneAccountHandle) {
+            return buildPhoneAccountHandle((PhoneAccountHandle) data);
+        }
+        if (data instanceof SubscriptionInfo) {
+            return buildSubscriptionInfoRecord((SubscriptionInfo) data);
+        }
+        if (data instanceof DhcpInfo) {
+            return buildDhcpInfo((DhcpInfo) data);
+        }
+        if (data instanceof DisplayMetrics) {
+            return buildDisplayMetrics((DisplayMetrics) data);
+        }
+        if (data instanceof RttCapabilities) {
+            return buildRttCapabilities((RttCapabilities) data);
+        }
+        if (data instanceof WifiActivityEnergyInfo) {
+            return buildWifiActivityEnergyInfo((WifiActivityEnergyInfo) data);
+        }
+        if (data instanceof WifiChannel) {
+            return buildWifiChannel((WifiChannel) data);
+        }
+        if (data instanceof WifiConfiguration) {
+            return buildWifiConfiguration((WifiConfiguration) data);
+        }
+        if (data instanceof WifiP2pDevice) {
+            return buildWifiP2pDevice((WifiP2pDevice) data);
+        }
+        if (data instanceof WifiP2pInfo) {
+            return buildWifiP2pInfo((WifiP2pInfo) data);
+        }
+        if (data instanceof WifiP2pGroup) {
+            return buildWifiP2pGroup((WifiP2pGroup) data);
+        }
+        if (data instanceof byte[]) {
+            return Base64Codec.encodeBase64((byte[]) data);
+        }
+        if (data instanceof Object[]) {
+            return buildJSONArray((Object[]) data);
+        }
+        if (data instanceof CellInfoLte) {
+            return buildCellInfoLte((CellInfoLte) data);
+        }
+        if (data instanceof CellInfoWcdma) {
+            return buildCellInfoWcdma((CellInfoWcdma) data);
+        }
+        if (data instanceof CellInfoGsm) {
+            return buildCellInfoGsm((CellInfoGsm) data);
+        }
+        if (data instanceof CellInfoCdma) {
+            return buildCellInfoCdma((CellInfoCdma) data);
+        }
+        if (data instanceof Call) {
+            return buildCall((Call) data);
+        }
+        if (data instanceof Call.Details) {
+            return buildCallDetails((Call.Details) data);
+        }
+        if (data instanceof InCallServiceImpl.CallEvent<?>) {
+            return buildCallEvent((InCallServiceImpl.CallEvent<?>) data);
+        }
+        if (data instanceof VideoProfile) {
+            return buildVideoProfile((VideoProfile) data);
+        }
+        if (data instanceof CameraCapabilities) {
+            return buildCameraCapabilities((CameraCapabilities) data);
+        }
+        if (data instanceof VoLteServiceState) {
+            return buildVoLteServiceStateEvent((VoLteServiceState) data);
+        }
+        if (data instanceof ModemActivityInfo) {
+            return buildModemActivityInfo((ModemActivityInfo) data);
+        }
+        if (data instanceof SignalStrength) {
+            return buildSignalStrength((SignalStrength) data);
+        }
+
+
+        return data.toString();
+        // throw new JSONException("Failed to build JSON result. " +
+        // data.getClass().getName());
+    }
+
+    private static JSONObject buildJsonAudioState(CallAudioState data)
+            throws JSONException {
+        JSONObject state = new JSONObject();
+        state.put("isMuted", data.isMuted());
+        state.put("AudioRoute", InCallServiceImpl.getAudioRouteString(data.getRoute()));
+        return state;
+    }
+
+    private static Object buildDisplayMetrics(DisplayMetrics data)
+            throws JSONException {
+        JSONObject dm = new JSONObject();
+        dm.put("widthPixels", data.widthPixels);
+        dm.put("heightPixels", data.heightPixels);
+        dm.put("noncompatHeightPixels", data.noncompatHeightPixels);
+        dm.put("noncompatWidthPixels", data.noncompatWidthPixels);
+        return dm;
+    }
+
+    private static Object buildInetAddress(InetAddress data) {
+        JSONArray address = new JSONArray();
+        address.put(data.getHostName());
+        address.put(data.getHostAddress());
+        return address;
+    }
+
+    private static Object buildInetSocketAddress(InetSocketAddress data) {
+        JSONArray address = new JSONArray();
+        address.put(data.getHostName());
+        address.put(data.getPort());
+        return address;
+    }
+
+    private static JSONObject buildJsonAddress(Address address)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("admin_area", address.getAdminArea());
+        result.put("country_code", address.getCountryCode());
+        result.put("country_name", address.getCountryName());
+        result.put("feature_name", address.getFeatureName());
+        result.put("phone", address.getPhone());
+        result.put("locality", address.getLocality());
+        result.put("postal_code", address.getPostalCode());
+        result.put("sub_admin_area", address.getSubAdminArea());
+        result.put("thoroughfare", address.getThoroughfare());
+        result.put("url", address.getUrl());
+        return result;
+    }
+
+    private static JSONArray buildJSONArray(Object[] data) throws JSONException {
+        JSONArray result = new JSONArray();
+        for (Object o : data) {
+            result.put(build(o));
+        }
+        return result;
+    }
+
+    private static JSONObject buildJsonBleAdvertiseSettings(
+            AdvertiseSettings advertiseSettings) throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("mode", advertiseSettings.getMode());
+        result.put("txPowerLevel", advertiseSettings.getTxPowerLevel());
+        result.put("isConnectable", advertiseSettings.isConnectable());
+        return result;
+    }
+
+    private static JSONObject buildJsonBleScanResult(
+            android.bluetooth.le.ScanResult scanResult) throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("rssi", scanResult.getRssi());
+        result.put("timestampNanos", scanResult.getTimestampNanos());
+        result.put("deviceName", scanResult.getScanRecord().getDeviceName());
+        result.put("txPowerLevel", scanResult.getScanRecord().getTxPowerLevel());
+        result.put("advertiseFlags", scanResult.getScanRecord()
+                .getAdvertiseFlags());
+        ArrayList<String> manufacturerDataList = new ArrayList<String>();
+        ArrayList<Integer> idList = new ArrayList<Integer>();
+        if (scanResult.getScanRecord().getManufacturerSpecificData() != null) {
+            SparseArray<byte[]> manufacturerSpecificData = scanResult
+                    .getScanRecord().getManufacturerSpecificData();
+            for (int i = 0; i < manufacturerSpecificData.size(); i++) {
+                manufacturerDataList.add(ConvertUtils
+                        .convertByteArrayToString(manufacturerSpecificData
+                                .valueAt(i)));
+                idList.add(manufacturerSpecificData.keyAt(i));
+            }
+        }
+        result.put("manufacturerSpecificDataList", manufacturerDataList);
+        result.put("manufacturereIdList", idList);
+        ArrayList<String> serviceUuidList = new ArrayList<String>();
+        ArrayList<String> serviceDataList = new ArrayList<String>();
+        if (scanResult.getScanRecord().getServiceData() != null) {
+            Map<ParcelUuid, byte[]> serviceDataMap = scanResult.getScanRecord()
+                    .getServiceData();
+            for (ParcelUuid serviceUuid : serviceDataMap.keySet()) {
+                serviceUuidList.add(serviceUuid.toString());
+                serviceDataList.add(ConvertUtils
+                        .convertByteArrayToString(serviceDataMap
+                                .get(serviceUuid)));
+            }
+        }
+        result.put("serviceUuidList", serviceUuidList);
+        result.put("serviceDataList", serviceDataList);
+        List<ParcelUuid> serviceUuids = scanResult.getScanRecord()
+                .getServiceUuids();
+        String serviceUuidsString = "";
+        if (serviceUuids != null && serviceUuids.size() > 0) {
+            for (ParcelUuid uuid : serviceUuids) {
+                serviceUuidsString = serviceUuidsString + "," + uuid;
+            }
+        }
+        result.put("serviceUuids", serviceUuidsString);
+        result.put("scanRecord",
+                build(ConvertUtils.convertByteArrayToString(scanResult
+                        .getScanRecord().getBytes())));
+        result.put("deviceInfo", build(scanResult.getDevice()));
+        return result;
+    }
+
+    private static JSONObject buildJsonBluetoothDevice(BluetoothDevice data)
+            throws JSONException {
+        JSONObject deviceInfo = new JSONObject();
+        deviceInfo.put("address", data.getAddress());
+        deviceInfo.put("state", data.getBondState());
+        deviceInfo.put("name", data.getName());
+        deviceInfo.put("type", data.getType());
+        return deviceInfo;
+    }
+
+    private static Object buildJsonBluetoothGattCharacteristic(
+            BluetoothGattCharacteristic data) throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("instanceId", data.getInstanceId());
+        result.put("permissions", data.getPermissions());
+        result.put("properties", data.getProperties());
+        result.put("writeType", data.getWriteType());
+        result.put("descriptorsList", build(data.getDescriptors()));
+        result.put("uuid", data.getUuid().toString());
+        result.put("value", build(data.getValue()));
+
+        return result;
+    }
+
+    private static Object buildJsonBluetoothGattDescriptor(
+            BluetoothGattDescriptor data) throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("instanceId", data.getInstanceId());
+        result.put("permissions", data.getPermissions());
+        result.put("characteristic", data.getCharacteristic());
+        result.put("uuid", data.getUuid().toString());
+        result.put("value", build(data.getValue()));
+        return result;
+    }
+
+    private static Object buildJsonBluetoothGattService(
+            BluetoothGattService data) throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("instanceId", data.getInstanceId());
+        result.put("type", data.getType());
+        result.put("gattCharacteristicList", build(data.getCharacteristics()));
+        result.put("includedServices", build(data.getIncludedServices()));
+        result.put("uuid", data.getUuid().toString());
+        return result;
+    }
+
+    private static JSONObject buildJsonBundle(Bundle bundle)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        for (String key : bundle.keySet()) {
+            result.put(key, build(bundle.get(key)));
+        }
+        return result;
+    }
+
+    private static JSONObject buildJsonCellLocation(CellLocation cellLocation)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        if (cellLocation instanceof GsmCellLocation) {
+            GsmCellLocation location = (GsmCellLocation) cellLocation;
+            result.put("lac", location.getLac());
+            result.put("cid", location.getCid());
+        }
+        // TODO(damonkohler): Add support for CdmaCellLocation. Not supported
+        // until API level 5.
+        return result;
+    }
+
+    private static JSONObject buildDhcpInfo(DhcpInfo data) throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("ipAddress", data.ipAddress);
+        result.put("dns1", data.dns1);
+        result.put("dns2", data.dns2);
+        result.put("gateway", data.gateway);
+        result.put("serverAddress", data.serverAddress);
+        result.put("leaseDuration", data.leaseDuration);
+        return result;
+    }
+
+    private static JSONObject buildJsonEvent(Event event) throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("name", event.getName());
+        result.put("data", build(event.getData()));
+        result.put("time", event.getCreationTime());
+        return result;
+    }
+
+    private static JSONObject buildJsonIntent(Intent data) throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("data", data.getDataString());
+        result.put("type", data.getType());
+        result.put("extras", build(data.getExtras()));
+        result.put("categories", build(data.getCategories()));
+        result.put("action", data.getAction());
+        ComponentName component = data.getComponent();
+        if (component != null) {
+            result.put("packagename", component.getPackageName());
+            result.put("classname", component.getClassName());
+        }
+        result.put("flags", data.getFlags());
+        return result;
+    }
+
+    private static <T> JSONArray buildJsonList(final List<T> list)
+            throws JSONException {
+        JSONArray result = new JSONArray();
+        for (T item : list) {
+            result.put(build(item));
+        }
+        return result;
+    }
+
+    private static JSONObject buildJsonLocation(Location location)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("altitude", location.getAltitude());
+        result.put("latitude", location.getLatitude());
+        result.put("longitude", location.getLongitude());
+        result.put("time", location.getTime());
+        result.put("accuracy", location.getAccuracy());
+        result.put("speed", location.getSpeed());
+        result.put("provider", location.getProvider());
+        result.put("bearing", location.getBearing());
+        return result;
+    }
+
+    private static JSONObject buildJsonMap(Map<String, ?> map)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        for (Entry<String, ?> entry : map.entrySet()) {
+            String key = entry.getKey();
+            if (key == null) {
+                key = "";
+            }
+            result.put(key, build(entry.getValue()));
+        }
+        return result;
+    }
+
+    private static JSONObject buildJsonScanResult(ScanResult scanResult)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("BSSID", scanResult.BSSID);
+        result.put("SSID", scanResult.SSID);
+        result.put("frequency", scanResult.frequency);
+        result.put("level", scanResult.level);
+        result.put("capabilities", scanResult.capabilities);
+        result.put("timestamp", scanResult.timestamp);
+        result.put("autoJoinStatus", scanResult.autoJoinStatus);
+        result.put("blackListTimestamp", scanResult.blackListTimestamp);
+        result.put("centerFreq0", scanResult.centerFreq0);
+        result.put("centerFreq1", scanResult.centerFreq1);
+        result.put("channelWidth", scanResult.channelWidth);
+        result.put("distanceCm", scanResult.distanceCm);
+        result.put("distanceSdCm", scanResult.distanceSdCm);
+        result.put("is80211McRTTResponder", scanResult.is80211mcResponder());
+        result.put("isAutoJoinCandidate", scanResult.isAutoJoinCandidate);
+        result.put("numConnection", scanResult.numConnection);
+        result.put("passpointNetwork", scanResult.isPasspointNetwork());
+        result.put("numIpConfigFailures", scanResult.numIpConfigFailures);
+        result.put("numUsage", scanResult.numUsage);
+        result.put("seen", scanResult.seen);
+        result.put("untrusted", scanResult.untrusted);
+        result.put("operatorFriendlyName", scanResult.operatorFriendlyName);
+        result.put("venueName", scanResult.venueName);
+        if (scanResult.informationElements != null) {
+            JSONArray infoEles = new JSONArray();
+            for (ScanResult.InformationElement ie : scanResult.informationElements) {
+                JSONObject infoEle = new JSONObject();
+                infoEle.put("id", ie.id);
+                infoEle.put("bytes", Base64Codec.encodeBase64(ie.bytes).toString());
+                infoEles.put(infoEle);
+            }
+            result.put("InfomationElements", infoEles);
+        } else {
+            result.put("InfomationElements", null);
+        }
+        return result;
+    }
+
+    private static JSONObject buildJsonScanData(ScanData scanData)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("Id", scanData.getId());
+        result.put("Flags", scanData.getFlags());
+        JSONArray scanResults = new JSONArray();
+        for (ScanResult sr : scanData.getResults()) {
+            scanResults.put(buildJsonScanResult(sr));
+        }
+        result.put("ScanResults", scanResults);
+        return result;
+    }
+
+    private static JSONObject buildJsonWifiInfo(WifiInfo data)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("hidden_ssid", data.getHiddenSSID());
+        result.put("ip_address", data.getIpAddress());
+        result.put("link_speed", data.getLinkSpeed());
+        result.put("network_id", data.getNetworkId());
+        result.put("rssi", data.getRssi());
+        result.put("BSSID", data.getBSSID());
+        result.put("mac_address", data.getMacAddress());
+        // Trim the double quotes if exist
+        String ssid = data.getSSID();
+        if (ssid.charAt(0) == '"'
+                && ssid.charAt(ssid.length() - 1) == '"') {
+            result.put("SSID", ssid.substring(1, ssid.length() - 1));
+        } else {
+            result.put("SSID", ssid);
+        }
+        String supplicantState = "";
+        switch (data.getSupplicantState()) {
+            case ASSOCIATED:
+                supplicantState = "associated";
+                break;
+            case ASSOCIATING:
+                supplicantState = "associating";
+                break;
+            case COMPLETED:
+                supplicantState = "completed";
+                break;
+            case DISCONNECTED:
+                supplicantState = "disconnected";
+                break;
+            case DORMANT:
+                supplicantState = "dormant";
+                break;
+            case FOUR_WAY_HANDSHAKE:
+                supplicantState = "four_way_handshake";
+                break;
+            case GROUP_HANDSHAKE:
+                supplicantState = "group_handshake";
+                break;
+            case INACTIVE:
+                supplicantState = "inactive";
+                break;
+            case INVALID:
+                supplicantState = "invalid";
+                break;
+            case SCANNING:
+                supplicantState = "scanning";
+                break;
+            case UNINITIALIZED:
+                supplicantState = "uninitialized";
+                break;
+            default:
+                supplicantState = null;
+        }
+        result.put("supplicant_state", build(supplicantState));
+        result.put("is_5ghz", data.is5GHz());
+        result.put("is_24ghz", data.is24GHz());
+        return result;
+    }
+
+    private static JSONObject buildNeighboringCellInfo(NeighboringCellInfo data)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("cid", data.getCid());
+        result.put("rssi", data.getRssi());
+        result.put("lac", data.getLac());
+        result.put("psc", data.getPsc());
+        String networkType =
+                TelephonyUtils.getNetworkTypeString(data.getNetworkType());
+        result.put("network_type", build(networkType));
+        return result;
+    }
+
+    private static JSONObject buildCellInfoLte(CellInfoLte data)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("rat", "lte");
+        result.put("registered", data.isRegistered());
+        CellIdentityLte cellidentity =
+                ((CellInfoLte) data).getCellIdentity();
+        CellSignalStrengthLte signalstrength =
+                ((CellInfoLte) data).getCellSignalStrength();
+        result.put("mcc", cellidentity.getMcc());
+        result.put("mnc", cellidentity.getMnc());
+        result.put("cid", cellidentity.getCi());
+        result.put("pcid", cellidentity.getPci());
+        result.put("tac", cellidentity.getTac());
+        result.put("rsrp", signalstrength.getDbm());
+        result.put("asulevel", signalstrength.getAsuLevel());
+        result.put("timing_advance", signalstrength.getTimingAdvance());
+        return result;
+    }
+
+    private static JSONObject buildCellInfoGsm(CellInfoGsm data)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("rat", "gsm");
+        result.put("registered", data.isRegistered());
+        CellIdentityGsm cellidentity =
+                ((CellInfoGsm) data).getCellIdentity();
+        CellSignalStrengthGsm signalstrength =
+                ((CellInfoGsm) data).getCellSignalStrength();
+        result.put("mcc", cellidentity.getMcc());
+        result.put("mnc", cellidentity.getMnc());
+        result.put("cid", cellidentity.getCid());
+        result.put("lac", cellidentity.getLac());
+        result.put("signal_strength", signalstrength.getDbm());
+        result.put("asulevel", signalstrength.getAsuLevel());
+        return result;
+    }
+
+    private static JSONObject buildCellInfoWcdma(CellInfoWcdma data)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("rat", "wcdma");
+        result.put("registered", data.isRegistered());
+        CellIdentityWcdma cellidentity =
+                ((CellInfoWcdma) data).getCellIdentity();
+        CellSignalStrengthWcdma signalstrength =
+                ((CellInfoWcdma) data).getCellSignalStrength();
+        result.put("mcc", cellidentity.getMcc());
+        result.put("mnc", cellidentity.getMnc());
+        result.put("cid", cellidentity.getCid());
+        result.put("lac", cellidentity.getLac());
+        result.put("psc", cellidentity.getPsc());
+        result.put("signal_strength", signalstrength.getDbm());
+        result.put("asulevel", signalstrength.getAsuLevel());
+        return result;
+    }
+
+    private static JSONObject buildCellInfoCdma(CellInfoCdma data)
+            throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("rat", "cdma");
+        result.put("registered", data.isRegistered());
+        CellIdentityCdma cellidentity =
+                ((CellInfoCdma) data).getCellIdentity();
+        CellSignalStrengthCdma signalstrength =
+                ((CellInfoCdma) data).getCellSignalStrength();
+        result.put("network_id", cellidentity.getNetworkId());
+        result.put("system_id", cellidentity.getSystemId());
+        result.put("basestation_id", cellidentity.getBasestationId());
+        result.put("longitude", cellidentity.getLongitude());
+        result.put("latitude", cellidentity.getLatitude());
+        result.put("cdma_dbm", signalstrength.getCdmaDbm());
+        result.put("cdma_ecio", signalstrength.getCdmaEcio());
+        result.put("evdo_dbm", signalstrength.getEvdoDbm());
+        result.put("evdo_ecio", signalstrength.getEvdoEcio());
+        result.put("evdo_snr", signalstrength.getEvdoSnr());
+        return result;
+    }
+
+    private static Object buildHttpURLConnection(HttpURLConnection data)
+            throws JSONException {
+        JSONObject con = new JSONObject();
+        try {
+            con.put("ResponseCode", data.getResponseCode());
+            con.put("ResponseMessage", data.getResponseMessage());
+        } catch (IOException e) {
+            e.printStackTrace();
+            return con;
+        }
+        con.put("ContentLength", data.getContentLength());
+        con.put("ContentEncoding", data.getContentEncoding());
+        con.put("ContentType", data.getContentType());
+        con.put("Date", data.getDate());
+        con.put("ReadTimeout", data.getReadTimeout());
+        con.put("HeaderFields", buildJsonMap(data.getHeaderFields()));
+        con.put("URL", buildURL(data.getURL()));
+        return con;
+    }
+
+    private static Object buildNetwork(Network data) throws JSONException {
+        JSONObject nw = new JSONObject();
+        nw.put("netId", data.netId);
+        return nw;
+    }
+
+    private static Object buildNetworkInfo(NetworkInfo data)
+            throws JSONException {
+        JSONObject info = new JSONObject();
+        info.put("isAvailable", data.isAvailable());
+        info.put("isConnected", data.isConnected());
+        info.put("isFailover", data.isFailover());
+        info.put("isRoaming", data.isRoaming());
+        info.put("ExtraInfo", data.getExtraInfo());
+        info.put("FailedReason", data.getReason());
+        info.put("TypeName", data.getTypeName());
+        info.put("SubtypeName", data.getSubtypeName());
+        info.put("State", data.getState().name().toString());
+        return info;
+    }
+
+    private static Object buildURL(URL data) throws JSONException {
+        JSONObject url = new JSONObject();
+        url.put("Authority", data.getAuthority());
+        url.put("Host", data.getHost());
+        url.put("Path", data.getPath());
+        url.put("Port", data.getPort());
+        url.put("Protocol", data.getProtocol());
+        return url;
+    }
+
+    private static JSONObject buildPhoneAccount(PhoneAccount data)
+            throws JSONException {
+        JSONObject acct = new JSONObject();
+        acct.put("Address", data.getAddress().toSafeString());
+        acct.put("SubscriptionAddress", data.getSubscriptionAddress()
+                .toSafeString());
+        acct.put("Label", ((data.getLabel() != null) ? data.getLabel().toString() : ""));
+        acct.put("ShortDescription", ((data.getShortDescription() != null) ? data
+                .getShortDescription().toString() : ""));
+        return acct;
+    }
+
+    private static Object buildPhoneAccountHandle(PhoneAccountHandle data)
+            throws JSONException {
+        JSONObject msg = new JSONObject();
+        msg.put("id", data.getId());
+        msg.put("ComponentName", data.getComponentName().flattenToString());
+        return msg;
+    }
+
+    private static Object buildSubscriptionInfoRecord(SubscriptionInfo data)
+            throws JSONException {
+        JSONObject msg = new JSONObject();
+        msg.put("subscriptionId", data.getSubscriptionId());
+        msg.put("iccId", data.getIccId());
+        msg.put("simSlotIndex", data.getSimSlotIndex());
+        msg.put("displayName", data.getDisplayName());
+        msg.put("nameSource", data.getNameSource());
+        msg.put("iconTint", data.getIconTint());
+        msg.put("number", data.getNumber());
+        msg.put("dataRoaming", data.getDataRoaming());
+        msg.put("mcc", data.getMcc());
+        msg.put("mnc", data.getMnc());
+        return msg;
+    }
+
+    private static Object buildPoint(Point data) throws JSONException {
+        JSONObject point = new JSONObject();
+        point.put("x", data.x);
+        point.put("y", data.y);
+        return point;
+    }
+
+    private static Object buildRttCapabilities(RttCapabilities data)
+            throws JSONException {
+        JSONObject cap = new JSONObject();
+        cap.put("bwSupported", data.bwSupported);
+        cap.put("lciSupported", data.lciSupported);
+        cap.put("lcrSupported", data.lcrSupported);
+        cap.put("oneSidedRttSupported", data.oneSidedRttSupported);
+        cap.put("preambleSupported", data.preambleSupported);
+        cap.put("twoSided11McRttSupported", data.twoSided11McRttSupported);
+        return cap;
+    }
+
+    private static Object buildSmsMessage(SmsMessage data) throws JSONException {
+        JSONObject msg = new JSONObject();
+        msg.put("originatingAddress", data.getOriginatingAddress());
+        msg.put("messageBody", data.getMessageBody());
+        return msg;
+    }
+
+    private static JSONObject buildWifiActivityEnergyInfo(
+            WifiActivityEnergyInfo data) throws JSONException {
+        JSONObject result = new JSONObject();
+        result.put("ControllerEnergyUsed", data.getControllerEnergyUsed());
+        result.put("ControllerIdleTimeMillis",
+                data.getControllerIdleTimeMillis());
+        result.put("ControllerRxTimeMillis", data.getControllerRxTimeMillis());
+        result.put("ControllerTxTimeMillis", data.getControllerTxTimeMillis());
+        result.put("StackState", data.getStackState());
+        result.put("TimeStamp", data.getTimeStamp());
+        return result;
+    }
+
+    private static Object buildWifiChannel(WifiChannel data) throws JSONException {
+        JSONObject channel = new JSONObject();
+        channel.put("channelNum", data.channelNum);
+        channel.put("freqMHz", data.freqMHz);
+        channel.put("isDFS", data.isDFS);
+        channel.put("isValid", data.isValid());
+        return channel;
+    }
+
+    private static Object buildWifiConfiguration(WifiConfiguration data)
+            throws JSONException {
+        JSONObject config = new JSONObject();
+        config.put("networkId", data.networkId);
+        // Trim the double quotes if exist
+        if (data.SSID.charAt(0) == '"'
+                && data.SSID.charAt(data.SSID.length() - 1) == '"') {
+            config.put("SSID", data.SSID.substring(1, data.SSID.length() - 1));
+        } else {
+            config.put("SSID", data.SSID);
+        }
+        config.put("BSSID", data.BSSID);
+        config.put("priority", data.priority);
+        config.put("hiddenSSID", data.hiddenSSID);
+        config.put("FQDN", data.FQDN);
+        config.put("providerFriendlyName", data.providerFriendlyName);
+        config.put("isPasspoint", data.isPasspoint());
+        config.put("hiddenSSID", data.hiddenSSID);
+        if (data.status == WifiConfiguration.Status.CURRENT) {
+            config.put("status", "CURRENT");
+        } else if (data.status == WifiConfiguration.Status.DISABLED) {
+            config.put("status", "DISABLED");
+        } else if (data.status == WifiConfiguration.Status.ENABLED) {
+            config.put("status", "ENABLED");
+        } else {
+            config.put("status", "UNKNOWN");
+        }
+        // config.put("enterpriseConfig", buildWifiEnterpriseConfig(data.enterpriseConfig));
+        return config;
+    }
+
+    private static Object buildWifiEnterpriseConfig(WifiEnterpriseConfig data)
+            throws JSONException, CertificateEncodingException {
+        JSONObject config = new JSONObject();
+        config.put(WifiEnterpriseConfig.PLMN_KEY, data.getPlmn());
+        config.put(WifiEnterpriseConfig.REALM_KEY, data.getRealm());
+        config.put(WifiEnterpriseConfig.EAP_KEY, data.getEapMethod());
+        config.put(WifiEnterpriseConfig.PHASE2_KEY, data.getPhase2Method());
+        config.put(WifiEnterpriseConfig.ALTSUBJECT_MATCH_KEY, data.getAltSubjectMatch());
+        X509Certificate caCert = data.getCaCertificate();
+        String caCertString = Base64.encodeToString(caCert.getEncoded(), Base64.DEFAULT);
+        config.put(WifiEnterpriseConfig.CA_CERT_KEY, caCertString);
+        X509Certificate clientCert = data.getClientCertificate();
+        String clientCertString = Base64.encodeToString(clientCert.getEncoded(), Base64.DEFAULT);
+        config.put(WifiEnterpriseConfig.CLIENT_CERT_KEY, clientCertString);
+        PrivateKey pk = data.getClientPrivateKey();
+        String privateKeyString = Base64.encodeToString(pk.getEncoded(), Base64.DEFAULT);
+        config.put(WifiEnterpriseConfig.PRIVATE_KEY_ID_KEY, privateKeyString);
+        config.put(WifiEnterpriseConfig.PASSWORD_KEY, data.getPassword());
+        return config;
+    }
+
+    private static JSONObject buildWifiP2pDevice(WifiP2pDevice data)
+            throws JSONException {
+        JSONObject deviceInfo = new JSONObject();
+        deviceInfo.put("Name", data.deviceName);
+        deviceInfo.put("Address", data.deviceAddress);
+        return deviceInfo;
+    }
+
+    private static JSONObject buildWifiP2pGroup(WifiP2pGroup data)
+            throws JSONException {
+        JSONObject group = new JSONObject();
+        Log.d("build p2p group.");
+        group.put("ClientList", build(data.getClientList()));
+        group.put("Interface", data.getInterface());
+        group.put("Networkname", data.getNetworkName());
+        group.put("Owner", data.getOwner());
+        group.put("Passphrase", data.getPassphrase());
+        group.put("NetworkId", data.getNetworkId());
+        return group;
+    }
+
+    private static JSONObject buildWifiP2pInfo(WifiP2pInfo data)
+            throws JSONException {
+        JSONObject info = new JSONObject();
+        Log.d("build p2p info.");
+        info.put("groupFormed", data.groupFormed);
+        info.put("isGroupOwner", data.isGroupOwner);
+        info.put("groupOwnerAddress", data.groupOwnerAddress);
+        return info;
+    }
+
+    private static <T> JSONObject buildCallEvent(InCallServiceImpl.CallEvent<T> callEvent)
+            throws JSONException {
+        JSONObject jsonEvent = new JSONObject();
+        jsonEvent.put("CallId", callEvent.getCallId());
+        jsonEvent.put("Event", build(callEvent.getEvent()));
+        return jsonEvent;
+    }
+
+    private static JSONObject buildUri(Uri uri) throws JSONException {
+        return new JSONObject().put("Uri", build((uri != null) ? uri.toString() : ""));
+    }
+
+    private static JSONObject buildCallDetails(Call.Details details) throws JSONException {
+
+        JSONObject callDetails = new JSONObject();
+
+        callDetails.put("Handle", buildUri(details.getHandle()));
+        callDetails.put("HandlePresentation",
+                build(InCallServiceImpl.getCallPresentationInfoString(
+                        details.getHandlePresentation())));
+        callDetails.put("CallerDisplayName", build(details.getCallerDisplayName()));
+
+        // TODO AccountHandle
+        // callDetails.put("AccountHandle", build(""));
+
+        callDetails.put("Capabilities",
+                build(InCallServiceImpl.getCallCapabilitiesString(details.getCallCapabilities())));
+
+        callDetails.put("Properties",
+                build(InCallServiceImpl.getCallPropertiesString(details.getCallProperties())));
+
+        // TODO Parse fields in Disconnect Cause
+        callDetails.put("DisconnectCause", build((details.getDisconnectCause() != null) ? details
+                .getDisconnectCause().toString() : ""));
+        callDetails.put("ConnectTimeMillis", build(details.getConnectTimeMillis()));
+
+        // TODO: GatewayInfo
+        // callDetails.put("GatewayInfo", build(""));
+
+        callDetails.put("VideoState",
+                build(InCallServiceImpl.getVideoCallStateString(details.getVideoState())));
+
+        // TODO: StatusHints
+        // callDetails.put("StatusHints", build(""));
+
+        callDetails.put("Extras", build(details.getExtras()));
+
+        return callDetails;
+    }
+
+    private static JSONObject buildCall(Call call) throws JSONException {
+
+        JSONObject callInfo = new JSONObject();
+
+        callInfo.put("Parent", build(InCallServiceImpl.getCallId(call)));
+
+        // TODO:Make a function out of this for consistency
+        ArrayList<String> children = new ArrayList<String>();
+        for (Call child : call.getChildren()) {
+            children.add(InCallServiceImpl.getCallId(child));
+        }
+        callInfo.put("Children", build(children));
+
+        // TODO:Make a function out of this for consistency
+        ArrayList<String> conferenceables = new ArrayList<String>();
+        for (Call conferenceable : call.getChildren()) {
+            children.add(InCallServiceImpl.getCallId(conferenceable));
+        }
+        callInfo.put("ConferenceableCalls", build(conferenceables));
+
+        callInfo.put("State", build(InCallServiceImpl.getCallStateString(call.getState())));
+        callInfo.put("CannedTextResponses", build(call.getCannedTextResponses()));
+        callInfo.put("VideoCall", InCallServiceImpl.getVideoCallId(call.getVideoCall()));
+        callInfo.put("Details", build(call.getDetails()));
+
+        return callInfo;
+    }
+
+    private static JSONObject buildVideoProfile(VideoProfile videoProfile) throws JSONException {
+        JSONObject profile = new JSONObject();
+
+        profile.put("VideoState",
+                InCallServiceImpl.getVideoCallStateString(videoProfile.getVideoState()));
+        profile.put("VideoQuality",
+                InCallServiceImpl.getVideoCallQualityString(videoProfile.getQuality()));
+
+        return profile;
+    }
+
+    private static JSONObject buildCameraCapabilities(CameraCapabilities cameraCapabilities)
+            throws JSONException {
+        JSONObject capabilities = new JSONObject();
+
+        capabilities.put("Height", build(cameraCapabilities.getHeight()));
+        capabilities.put("Width", build(cameraCapabilities.getWidth()));
+        capabilities.put("ZoomSupported", build(cameraCapabilities.isZoomSupported()));
+        capabilities.put("MaxZoom", build(cameraCapabilities.getMaxZoom()));
+
+        return capabilities;
+    }
+
+    private static JSONObject buildVoLteServiceStateEvent(
+        VoLteServiceState volteInfo)
+            throws JSONException {
+        JSONObject info = new JSONObject();
+        info.put(TelephonyConstants.VoLteServiceStateContainer.SRVCC_STATE,
+            TelephonyUtils.getSrvccStateString(volteInfo.getSrvccState()));
+        return info;
+    }
+
+    private static JSONObject buildModemActivityInfo(ModemActivityInfo modemInfo)
+            throws JSONException {
+        JSONObject info = new JSONObject();
+
+        info.put("Timestamp", modemInfo.getTimestamp());
+        info.put("SleepTimeMs", modemInfo.getSleepTimeMillis());
+        info.put("IdleTimeMs", modemInfo.getIdleTimeMillis());
+        //convert from int[] to List<Integer> for proper JSON translation
+        int[] txTimes = modemInfo.getTxTimeMillis();
+        List<Integer> tmp = new ArrayList<Integer>(txTimes.length);
+        for(int val : txTimes) {
+            tmp.add(val);
+        }
+        info.put("TxTimeMs", build(tmp));
+        info.put("RxTimeMs", modemInfo.getRxTimeMillis());
+        info.put("EnergyUsedMw", modemInfo.getEnergyUsed());
+        return info;
+    }
+    private static JSONObject buildSignalStrength(SignalStrength signalStrength)
+            throws JSONException {
+        JSONObject info = new JSONObject();
+        info.put(TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_GSM,
+            signalStrength.getGsmSignalStrength());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_GSM_DBM,
+            signalStrength.getGsmDbm());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_GSM_LEVEL,
+            signalStrength.getGsmLevel());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_GSM_ASU_LEVEL,
+            signalStrength.getGsmAsuLevel());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_GSM_BIT_ERROR_RATE,
+            signalStrength.getGsmBitErrorRate());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_CDMA_DBM,
+            signalStrength.getCdmaDbm());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_CDMA_LEVEL,
+            signalStrength.getCdmaLevel());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_CDMA_ASU_LEVEL,
+            signalStrength.getCdmaAsuLevel());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_CDMA_ECIO,
+            signalStrength.getCdmaEcio());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_EVDO_DBM,
+            signalStrength.getEvdoDbm());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_EVDO_ECIO,
+            signalStrength.getEvdoEcio());
+        info.put(TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_LTE,
+            signalStrength.getLteSignalStrength());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_LTE_DBM,
+            signalStrength.getLteDbm());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_LTE_LEVEL,
+            signalStrength.getLteLevel());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_LTE_ASU_LEVEL,
+            signalStrength.getLteAsuLevel());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_LEVEL,
+            signalStrength.getLevel());
+        info.put(
+            TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_ASU_LEVEL,
+            signalStrength.getAsuLevel());
+        info.put(TelephonyConstants.SignalStrengthContainer.SIGNAL_STRENGTH_DBM,
+            signalStrength.getDbm());
+        return info;
+    }
+
+    private JsonBuilder() {
+        // This is a utility class.
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/jsonrpc/JsonDeserializable.java b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonDeserializable.java
new file mode 100644
index 0000000..a577fdc
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonDeserializable.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.jsonrpc;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public interface JsonDeserializable {
+    public JSONObject fromJSON() throws JSONException;
+}
diff --git a/Common/src/com/googlecode/android_scripting/jsonrpc/JsonRpcResult.java b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonRpcResult.java
new file mode 100644
index 0000000..b9f3d67
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonRpcResult.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.jsonrpc;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents a JSON RPC result.
+ *
+ * @see http://json-rpc.org/wiki/specification
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class JsonRpcResult {
+
+  private JsonRpcResult() {
+    // Utility class.
+  }
+
+  public static JSONObject empty(int id) throws JSONException {
+    JSONObject json = new JSONObject();
+    json.put("id", id);
+    json.put("result", JSONObject.NULL);
+    json.put("error", JSONObject.NULL);
+    return json;
+  }
+
+  public static JSONObject result(int id, Object data) throws JSONException {
+    JSONObject json = new JSONObject();
+    json.put("id", id);
+    json.put("result", JsonBuilder.build(data));
+    json.put("error", JSONObject.NULL);
+    return json;
+  }
+
+  public static JSONObject error(int id, Throwable t) throws JSONException {
+    JSONObject json = new JSONObject();
+    json.put("id", id);
+    json.put("result", JSONObject.NULL);
+    json.put("error", t.toString());
+    return json;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/jsonrpc/JsonRpcServer.java b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonRpcServer.java
new file mode 100644
index 0000000..5bdd99a
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonRpcServer.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.jsonrpc;
+
+import java.io.BufferedReader;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.util.Map;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.SimpleServer;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+import com.googlecode.android_scripting.rpc.RpcError;
+
+/**
+ * A JSON RPC server that forwards RPC calls to a specified receiver object.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class JsonRpcServer extends SimpleServer {
+
+    private static final String CMD_CLOSE_SESSION = "closeSl4aSession";
+
+    private final RpcReceiverManagerFactory mRpcReceiverManagerFactory;
+
+    // private final String mHandshake;
+
+    /**
+     * Construct a {@link JsonRpcServer} connected to the provided {@link RpcReceiverManager}.
+     *
+     * @param managerFactory the {@link RpcReceiverManager} to register with the server
+     * @param handshake the secret handshake required for authorization to use this server
+     */
+    public JsonRpcServer(RpcReceiverManagerFactory managerFactory, String handshake) {
+        // mHandshake = handshake;
+        mRpcReceiverManagerFactory = managerFactory;
+    }
+
+    @Override
+    public void shutdown() {
+        super.shutdown();
+        // Notify all RPC receiving objects. They may have to clean up some of their state.
+        for (RpcReceiverManager manager : mRpcReceiverManagerFactory.getRpcReceiverManagers()
+                .values()) {
+            manager.shutdown();
+        }
+    }
+
+    @Override
+    protected void handleRPCConnection(Socket sock, Integer UID, BufferedReader reader,
+            PrintWriter writer) throws Exception {
+        RpcReceiverManager receiverManager = null;
+        Map<Integer, RpcReceiverManager> mgrs = mRpcReceiverManagerFactory.getRpcReceiverManagers();
+        synchronized (mgrs) {
+            Log.d("UID " + UID);
+            Log.d("manager map keys: "
+                    + mRpcReceiverManagerFactory.getRpcReceiverManagers().keySet());
+            if (mgrs.containsKey(UID)) {
+                Log.d("Look up existing session");
+                receiverManager = mgrs.get(UID);
+            } else {
+                Log.d("Create a new session");
+                receiverManager = mRpcReceiverManagerFactory.create(UID);
+            }
+        }
+        // boolean passedAuthentication = false;
+        String data;
+        while ((data = reader.readLine()) != null) {
+            Log.v("Session " + UID + " Received: " + data);
+            JSONObject request = new JSONObject(data);
+            int id = request.getInt("id");
+            String method = request.getString("method");
+            JSONArray params = request.getJSONArray("params");
+
+            MethodDescriptor rpc = receiverManager.getMethodDescriptor(method);
+            if (rpc == null) {
+                send(writer, JsonRpcResult.error(id, new RpcError("Unknown RPC: " + method)), UID);
+                continue;
+            }
+            try {
+                send(writer, JsonRpcResult.result(id, rpc.invoke(receiverManager, params)), UID);
+            } catch (Throwable t) {
+                Log.e("Invocation error.", t);
+                send(writer, JsonRpcResult.error(id, t), UID);
+            }
+            if (method.equals(CMD_CLOSE_SESSION)) {
+                Log.d("Got shutdown signal");
+                synchronized (writer) {
+                    receiverManager.shutdown();
+                    reader.close();
+                    writer.close();
+                    sock.close();
+                    shutdown();
+                    mgrs.remove(UID);
+                }
+                return;
+            }
+        }
+    }
+
+    private void send(PrintWriter writer, JSONObject result, int UID) {
+        writer.write(result + "\n");
+        writer.flush();
+        Log.v("Session " + UID + " Sent: " + result);
+    }
+
+    @Override
+    protected void handleConnection(Socket socket) throws Exception {
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/jsonrpc/JsonSerializable.java b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonSerializable.java
new file mode 100644
index 0000000..5f05d40
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/jsonrpc/JsonSerializable.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.jsonrpc;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public interface JsonSerializable {
+    public JSONObject toJSON() throws JSONException;
+}
diff --git a/Common/src/com/googlecode/android_scripting/jsonrpc/RpcReceiver.java b/Common/src/com/googlecode/android_scripting/jsonrpc/RpcReceiver.java
new file mode 100644
index 0000000..e885422
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/jsonrpc/RpcReceiver.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.jsonrpc;
+
+public abstract class RpcReceiver {
+
+  protected final RpcReceiverManager mManager;
+
+  public RpcReceiver(RpcReceiverManager manager) {
+    // To make reflection easier, we ensures that all the subclasses agree on this common
+    // constructor.
+    mManager = manager;
+  }
+
+  /** Invoked when the receiver is shut down. */
+  public abstract void shutdown();
+}
diff --git a/Common/src/com/googlecode/android_scripting/jsonrpc/RpcReceiverManager.java b/Common/src/com/googlecode/android_scripting/jsonrpc/RpcReceiverManager.java
new file mode 100644
index 0000000..3f6b105
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/jsonrpc/RpcReceiverManager.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.jsonrpc;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+
+public abstract class RpcReceiverManager {
+
+    private final Map<Class<? extends RpcReceiver>, RpcReceiver> mReceivers;
+
+    /**
+     * A map of strings to known RPCs.
+     */
+    private final Map<String, MethodDescriptor> mKnownRpcs = new HashMap<String, MethodDescriptor>();
+
+    public RpcReceiverManager(Collection<Class<? extends RpcReceiver>> classList) {
+        mReceivers = new HashMap<Class<? extends RpcReceiver>, RpcReceiver>();
+        for (Class<? extends RpcReceiver> receiverClass : classList) {
+            mReceivers.put(receiverClass, null);
+            Collection<MethodDescriptor> methodList = MethodDescriptor.collectFrom(receiverClass);
+            for (MethodDescriptor m : methodList) {
+                if (mKnownRpcs.containsKey(m.getName())) {
+                    // We already know an RPC of the same name. We don't catch this anywhere because
+                    // this is a programming error.
+                    throw new RuntimeException("An RPC with the name " + m.getName()
+                            + " is already known.");
+                }
+                mKnownRpcs.put(m.getName(), m);
+            }
+        }
+    }
+
+    public Collection<Class<? extends RpcReceiver>> getRpcReceiverClasses() {
+        return mReceivers.keySet();
+    }
+
+    private RpcReceiver get(Class<? extends RpcReceiver> clazz) {
+        RpcReceiver object = mReceivers.get(clazz);
+        if (object != null) {
+            return object;
+        }
+
+        Constructor<? extends RpcReceiver> constructor;
+        try {
+            constructor = clazz.getConstructor(FacadeManager.class);
+            object = constructor.newInstance(this);
+            mReceivers.put(clazz, object);
+        } catch (Exception e) {
+            Log.e(e);
+        }
+
+        return object;
+    }
+
+    public <T extends RpcReceiver> T getReceiver(Class<T> clazz) {
+        RpcReceiver receiver = get(clazz);
+        return clazz.cast(receiver);
+    }
+
+    public MethodDescriptor getMethodDescriptor(String methodName) {
+        return mKnownRpcs.get(methodName);
+    }
+
+    public Object invoke(Class<? extends RpcReceiver> clazz, Method method, Object[] args)
+            throws Exception {
+        RpcReceiver object = get(clazz);
+        return method.invoke(object, args);
+    }
+
+    public void shutdown() {
+        for (RpcReceiver receiver : mReceivers.values()) {
+            try {
+                if (receiver != null) {
+                    receiver.shutdown();
+                }
+            } catch (Exception e) {
+                Log.e("Failed to shut down an RpcReceiver", e);
+            }
+        }
+    }
+}
diff --git a/Common/src/com/googlecode/android_scripting/jsonrpc/RpcReceiverManagerFactory.java b/Common/src/com/googlecode/android_scripting/jsonrpc/RpcReceiverManagerFactory.java
new file mode 100644
index 0000000..defec5f
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/jsonrpc/RpcReceiverManagerFactory.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.jsonrpc;
+
+import java.util.Map;
+
+public interface RpcReceiverManagerFactory {
+  public RpcReceiverManager create(Integer UID);
+
+  public Map<Integer, RpcReceiverManager> getRpcReceiverManagers();
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/BeanShellLanguage.java b/Common/src/com/googlecode/android_scripting/language/BeanShellLanguage.java
new file mode 100644
index 0000000..b92d502
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/BeanShellLanguage.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+import com.googlecode.android_scripting.rpc.ParameterDescriptor;
+
+/**
+ * Represents the BeanShell programming language.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class BeanShellLanguage extends Language {
+
+  @Override
+  protected String getImportStatement() {
+    // FIXME(igor.v.karp): this is interpreter specific
+    return "source(\"/sdcard/com.googlecode.bshforandroid/extras/bsh/android.bsh\");\n";
+  }
+
+  @Override
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return rpcReceiver + " = Android();\n";
+  }
+
+  @Override
+  protected String getMethodCallText(String receiver, String method,
+      ParameterDescriptor[] parameters) {
+    StringBuilder result =
+        new StringBuilder().append(getApplyReceiverText(receiver)).append(getApplyOperatorText())
+            .append(method);
+    if (parameters.length > 0) {
+      result.append(getLeftParametersText());
+    } else {
+      result.append(getQuote());
+    }
+    String separator = "";
+    for (ParameterDescriptor parameter : parameters) {
+      result.append(separator).append(getValueText(parameter));
+      separator = getParameterSeparator();
+    }
+    result.append(getRightParametersText());
+
+    return result.toString();
+  }
+
+  @Override
+  protected String getApplyOperatorText() {
+    return ".call(\"";
+  }
+
+  @Override
+  protected String getLeftParametersText() {
+    return "\", ";
+  }
+
+  @Override
+  protected String getRightParametersText() {
+    return ")";
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/HtmlLanguage.java b/Common/src/com/googlecode/android_scripting/language/HtmlLanguage.java
new file mode 100644
index 0000000..e7abf33
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/HtmlLanguage.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+import com.googlecode.android_scripting.rpc.ParameterDescriptor;
+
+public class HtmlLanguage extends Language {
+
+  /** Returns the Android package import statement. */
+  @Override
+  protected String getImportStatement() {
+    return "<html>\n<head>\n<script>";
+  }
+
+  @Override
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return String.format("var %s = new Android();\n</script>\n</head>\n<body>\n\n</body>\n</html>",
+        rpcReceiver);
+  }
+
+  @Override
+  protected String getMethodCallText(String receiver, String method,
+      ParameterDescriptor[] parameters) {
+    StringBuilder result =
+        new StringBuilder().append(getApplyReceiverText(receiver)).append(getApplyOperatorText())
+            .append(method);
+    if (parameters.length > 0) {
+      result.append(getLeftParametersText());
+    } else {
+      result.append(getQuote());
+    }
+    String separator = "";
+    for (ParameterDescriptor parameter : parameters) {
+      result.append(separator).append(getValueText(parameter));
+      separator = getParameterSeparator();
+    }
+    result.append(getRightParametersText());
+
+    return result.toString();
+  }
+
+  @Override
+  protected String getApplyOperatorText() {
+    return ".call('";
+  }
+
+  @Override
+  protected String getLeftParametersText() {
+    return "', ";
+  }
+
+  @Override
+  protected String getRightParametersText() {
+    return ")";
+  }
+
+  @Override
+  protected String getQuote() {
+    return "'";
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/JavaScriptLanguage.java b/Common/src/com/googlecode/android_scripting/language/JavaScriptLanguage.java
new file mode 100644
index 0000000..7e88d3c
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/JavaScriptLanguage.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+/**
+ * Represents the JavaScript programming language.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class JavaScriptLanguage extends Language {
+
+  @Override
+  protected String getImportStatement() {
+    // FIXME(igor.v.karp): this is interpreter specific
+    return "load(\"/sdcard/com.googlecode.rhinoforandroid/extras/rhino/android.js\");\n";
+  }
+
+  @Override
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return "var " + rpcReceiver + " = Android();\n";
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/Language.java b/Common/src/com/googlecode/android_scripting/language/Language.java
new file mode 100644
index 0000000..0a8d487
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/Language.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+import com.googlecode.android_scripting.rpc.ParameterDescriptor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents the programming language supported by the SL4A.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class Language {
+
+  private final static Map<Character, String> AUTO_CLOSE_MAP = buildAutoCloseMap('[', "[]", '{',
+      "{}", '(', "()", '\'', "''", '"', "\"\"");
+
+  /** Returns the initial template for newly created script. */
+  public String getContentTemplate() {
+    StringBuilder content = new StringBuilder(getImportStatement());
+    if (content.length() != 0) {
+      content.append('\n');
+    }
+    content.append(getRpcReceiverDeclaration(getDefaultRpcReceiver()));
+    return content.toString();
+  }
+
+  /** Returns the Android package import statement. */
+  protected String getImportStatement() {
+    return "";
+  }
+
+  /** Returns the RPC receiver declaration. */
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return "";
+  }
+
+  /** Returns the default RPC receiver name. */
+  protected String getDefaultRpcReceiver() {
+    return "droid";
+  }
+
+  /**
+   * Returns the string containing opening and closing tokens if the input is an opening token.
+   * Returns {@code null} otherwise.
+   */
+  public String autoClose(char token) {
+    return AUTO_CLOSE_MAP.get(token);
+  }
+
+  /** Returns the RPC call text with given parameter values. */
+  public final String getRpcText(String content, MethodDescriptor rpc, String[] values) {
+    return getMethodCallText(getRpcReceiverName(content), rpc.getName(),
+        rpc.getParameterValues(values));
+  }
+
+  /** Returns the RPC receiver found in the given script. */
+  protected String getRpcReceiverName(String content) {
+    return getDefaultRpcReceiver();
+  }
+
+  /** Returns the method call text in the language. */
+  protected String getMethodCallText(String receiver, String method,
+      ParameterDescriptor[] parameters) {
+    StringBuilder result =
+        new StringBuilder().append(getApplyReceiverText(receiver)).append(getApplyOperatorText())
+            .append(method).append(getLeftParametersText());
+    String separator = "";
+    for (ParameterDescriptor parameter : parameters) {
+      result.append(separator).append(getValueText(parameter));
+      separator = getParameterSeparator();
+    }
+    result.append(getRightParametersText());
+
+    return result.toString();
+  }
+
+  /** Returns the apply receiver text. */
+  protected String getApplyReceiverText(String receiver) {
+    return receiver;
+  }
+
+  /** Returns the apply operator text. */
+  protected String getApplyOperatorText() {
+    return ".";
+  }
+
+  /** Returns the text to the left of the parameters. */
+  protected String getLeftParametersText() {
+    return "(";
+  }
+
+  /** Returns the text to the right of the parameters. */
+  protected String getRightParametersText() {
+    return ")";
+  }
+
+  /** Returns the parameter separator text. */
+  protected String getParameterSeparator() {
+    return ", ";
+  }
+
+  /** Returns the text of the quotation. */
+  protected String getQuote() {
+    return "\"";
+  }
+
+  /** Returns the text of the {@code null} value. */
+  protected String getNull() {
+    return "null";
+  }
+
+  /** Returns the text of the {{@code true} value. */
+  protected String getTrue() {
+    return "true";
+  }
+
+  /** Returns the text of the false value. */
+  protected String getFalse() {
+    return "false";
+  }
+
+  /** Returns the parameter value suitable for code generation. */
+  protected String getValueText(ParameterDescriptor parameter) {
+    if (parameter.getValue() == null) {
+      return getNullValueText();
+    } else if (parameter.getType().equals(String.class)) {
+      return getStringValueText(parameter.getValue());
+    } else if (parameter.getType().equals(Boolean.class)) {
+      return getBooleanValueText(parameter.getValue());
+    } else {
+      return parameter.getValue();
+    }
+  }
+
+  /** Returns the null value suitable for code generation. */
+  private String getNullValueText() {
+    return getNull();
+  }
+
+  /** Returns the string parameter value suitable for code generation. */
+  protected String getStringValueText(String value) {
+    // TODO(igorkarp): do not quote expressions once they could be detected.
+    return getQuote() + value + getQuote();
+  }
+
+  /** Returns the boolean parameter value suitable for code generation. */
+  protected String getBooleanValueText(String value) {
+    if (value.equals(Boolean.TRUE.toString())) {
+      return getTrue();
+    } else if (value.equals(Boolean.FALSE.toString())) {
+      return getFalse();
+    } else {
+      // If it is neither true nor false it is must be an expression.
+      return value;
+    }
+  }
+
+  private static Map<Character, String> buildAutoCloseMap(char c1, String s1, char c2, String s2,
+      char c3, String s3, char c4, String s4, char c5, String s5) {
+    Map<Character, String> map = new HashMap<Character, String>(5);
+    map.put(c1, s1);
+    map.put(c2, s2);
+    map.put(c3, s3);
+    map.put(c4, s4);
+    map.put(c5, s5);
+    return map;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/LuaLanguage.java b/Common/src/com/googlecode/android_scripting/language/LuaLanguage.java
new file mode 100644
index 0000000..a279434
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/LuaLanguage.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+/**
+ * Represents the Lua programming language.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class LuaLanguage extends Language {
+
+  @Override
+  protected String getImportStatement() {
+    return "require \"android\"\n";
+  }
+
+  @Override
+  protected String getDefaultRpcReceiver() {
+    return "android";
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/PerlLanguage.java b/Common/src/com/googlecode/android_scripting/language/PerlLanguage.java
new file mode 100644
index 0000000..c5cca0b
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/PerlLanguage.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+/**
+ * Represents the Perl programming language.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class PerlLanguage extends Language {
+
+  @Override
+  protected String getImportStatement() {
+    return "use Android;\n";
+  }
+
+  @Override
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return "my " + rpcReceiver + " = Android->new();\n";
+  }
+
+  @Override
+  protected String getDefaultRpcReceiver() {
+    return "$droid";
+  }
+
+  @Override
+  protected String getApplyOperatorText() {
+    return "->";
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/PhpLanguage.java b/Common/src/com/googlecode/android_scripting/language/PhpLanguage.java
new file mode 100644
index 0000000..a141a3c
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/PhpLanguage.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+/**
+ * Represents the PHP programming language.
+ *
+ * @author ivan@irontec.com (Ivan Mosquera Paulo)
+ */
+public class PhpLanguage extends Language {
+
+  @Override
+  protected String getImportStatement() {
+    return "<?php\n\nrequire_once(\"Android.php\");";
+
+  }
+
+  @Override
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return rpcReceiver + " = new Android();\n";
+  }
+
+  @Override
+  protected String getDefaultRpcReceiver() {
+    return "$droid";
+  }
+
+  @Override
+  protected String getApplyOperatorText() {
+    return "->";
+  }
+
+  @Override
+  protected String getQuote() {
+    return "'";
+  }
+
+}
\ No newline at end of file
diff --git a/Common/src/com/googlecode/android_scripting/language/PythonLanguage.java b/Common/src/com/googlecode/android_scripting/language/PythonLanguage.java
new file mode 100644
index 0000000..beb59ae
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/PythonLanguage.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+/**
+ * Represents the Python programming language.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class PythonLanguage extends Language {
+
+  @Override
+  protected String getImportStatement() {
+    return "import android\n";
+  }
+
+  @Override
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return rpcReceiver + " = android.Android()\n";
+  }
+
+  @Override
+  protected String getQuote() {
+    return "'";
+  }
+
+  @Override
+  protected String getNull() {
+    return "None";
+  }
+
+  @Override
+  protected String getTrue() {
+    return "True";
+  }
+
+  @Override
+  protected String getFalse() {
+    return "False";
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/RubyLanguage.java b/Common/src/com/googlecode/android_scripting/language/RubyLanguage.java
new file mode 100644
index 0000000..d6cd189
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/RubyLanguage.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+/**
+ * Represents the Ruby programming language.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class RubyLanguage extends Language {
+
+  @Override
+  protected String getImportStatement() {
+    return "require \"android\";\n";
+  }
+
+  @Override
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return rpcReceiver + " = Droid.new\n";
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/ShellLanguage.java b/Common/src/com/googlecode/android_scripting/language/ShellLanguage.java
new file mode 100644
index 0000000..d7bc5a2
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/ShellLanguage.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+/**
+ * Represents the Shell programming language.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class ShellLanguage extends Language {
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/SleepLanguage.java b/Common/src/com/googlecode/android_scripting/language/SleepLanguage.java
new file mode 100644
index 0000000..1b623ab
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/SleepLanguage.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+/**
+ * Represents the Sleep programming language.
+ *
+ * @author tomcatalbino@gmail.com
+ */
+public class SleepLanguage extends Language {
+
+  @Override
+  protected String getImportStatement() {
+    return "import com.googlecode.rpc.*;\n";
+  }
+
+  @Override
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return rpcReceiver + " = [new Android];\n";
+  }
+
+  @Override
+  protected String getDefaultRpcReceiver() {
+    return "$droid";
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/SquirrelLanguage.java b/Common/src/com/googlecode/android_scripting/language/SquirrelLanguage.java
new file mode 100644
index 0000000..f9b25bf
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/SquirrelLanguage.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+/**
+ * Represents the Squirrel programming language, by Alberto Demichelis
+ * this file adapted by Andy Tai, atai@atai.org
+ * based on the Python version by
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class SquirrelLanguage extends Language {
+
+  @Override
+  protected String getImportStatement() {
+    /* initialization code */
+    return "";
+  }
+
+  @Override
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return rpcReceiver + " <- Android();\n";
+  }
+
+  @Override
+  protected String getQuote() {
+    return "\"";
+  }
+
+  @Override
+  protected String getNull() {
+    return "null";
+  }
+
+  @Override
+  protected String getTrue() {
+    return "true";
+  }
+
+  @Override
+  protected String getFalse() {
+    return "false";
+  }
+}
+
diff --git a/Common/src/com/googlecode/android_scripting/language/SupportedLanguages.java b/Common/src/com/googlecode/android_scripting/language/SupportedLanguages.java
new file mode 100644
index 0000000..e496378
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/SupportedLanguages.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+import com.googlecode.android_scripting.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SupportedLanguages {
+
+  private static enum KnownLanguage {
+    // SHELL(".sh", ShellLanguage.class), // We don't really support Shell language
+    HTML(".html", HtmlLanguage.class), BEANSHELL(".bsh", BeanShellLanguage.class), JAVASCRIPT(
+        ".js", JavaScriptLanguage.class), LUA(".lua", LuaLanguage.class), PERL(".pl",
+        PerlLanguage.class), PYTHON(".py", PythonLanguage.class), RUBY(".rb", RubyLanguage.class),
+    TCL(".tcl", TclLanguage.class), PHP(".php", PhpLanguage.class), SLEEP(".sl",
+        SleepLanguage.class), SQUIRREL(".nut", SquirrelLanguage.class);
+
+    private final String mmExtension;
+    private final Class<? extends Language> mmClass;
+
+    private KnownLanguage(String ext, Class<? extends Language> clazz) {
+      mmExtension = ext;
+      mmClass = clazz;
+    }
+
+    private String getExtension() {
+      return mmExtension;
+    }
+
+    private Class<? extends Language> getLanguageClass() {
+      return mmClass;
+    }
+  }
+
+  private static Map<String, Class<? extends Language>> sSupportedLanguages;
+
+  static {
+    sSupportedLanguages = new HashMap<String, Class<? extends Language>>();
+    for (KnownLanguage language : KnownLanguage.values()) {
+      sSupportedLanguages.put(language.getExtension(), language.getLanguageClass());
+    }
+  }
+
+  public static Language getLanguageByExtension(String extension) {
+    extension = extension.toLowerCase();
+    if (!extension.startsWith(".")) {
+      throw new RuntimeException("Extension does not start with a dot: " + extension);
+    }
+    Language lang = null;
+
+    Class<? extends Language> clazz = sSupportedLanguages.get(extension);
+    if (clazz == null) {
+      clazz = Language.class; // revert to default language.
+    }
+    if (clazz != null) {
+      try {
+        lang = clazz.newInstance();
+      } catch (IllegalAccessException e) {
+        Log.e(e);
+      } catch (InstantiationException e) {
+        Log.e(e);
+      }
+    }
+    return lang;
+  }
+
+  public static boolean checkLanguageSupported(String name) {
+    String extension = name.toLowerCase();
+    int index = extension.lastIndexOf('.');
+    if (index < 0) {
+      extension = "." + extension;
+    } else if (index > 0) {
+      extension = extension.substring(index);
+    }
+    return sSupportedLanguages.containsKey(extension);
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/language/TclLanguage.java b/Common/src/com/googlecode/android_scripting/language/TclLanguage.java
new file mode 100644
index 0000000..52b8532
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/language/TclLanguage.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.language;
+
+/**
+ * Represents the Tcl programming language.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class TclLanguage extends Language {
+
+  @Override
+  protected String getImportStatement() {
+    return "package require android\n";
+  }
+
+  @Override
+  protected String getRpcReceiverDeclaration(String rpcReceiver) {
+    return "set " + rpcReceiver + " [android new]\n";
+  }
+
+  @Override
+  protected String getApplyReceiverText(String receiver) {
+    return "$" + receiver;
+  }
+
+  @Override
+  protected String getApplyOperatorText() {
+    return " ";
+  }
+
+  @Override
+  protected String getLeftParametersText() {
+    return " ";
+  }
+
+  @Override
+  protected String getRightParametersText() {
+    return "";
+  }
+
+  @Override
+  protected String getParameterSeparator() {
+    return " ";
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/Converter.java b/Common/src/com/googlecode/android_scripting/rpc/Converter.java
new file mode 100644
index 0000000..274d838
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/Converter.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+/**
+ * A converter can take a String and turn it into an instance of type T (the type parameter to the
+ * converter).
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public interface Converter<T> {
+
+  /** Convert a string into type T. */
+  T convert(String value);
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/MethodDescriptor.java b/Common/src/com/googlecode/android_scripting/rpc/MethodDescriptor.java
new file mode 100644
index 0000000..5c31c34
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/MethodDescriptor.java
@@ -0,0 +1,593 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import com.googlecode.android_scripting.facade.AndroidFacade;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
+import com.googlecode.android_scripting.util.VisibleForTesting;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * An adapter that wraps {@code Method}.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public final class MethodDescriptor {
+  private static final Map<Class<?>, Converter<?>> sConverters = populateConverters();
+
+  private final Method mMethod;
+  private final Class<? extends RpcReceiver> mClass;
+
+  public MethodDescriptor(Class<? extends RpcReceiver> clazz, Method method) {
+    mClass = clazz;
+    mMethod = method;
+  }
+
+  @Override
+  public String toString() {
+    return mMethod.getDeclaringClass().getCanonicalName() + "." + mMethod.getName();
+  }
+
+  /** Collects all methods with {@code RPC} annotation from given class. */
+  public static Collection<MethodDescriptor> collectFrom(Class<? extends RpcReceiver> clazz) {
+    List<MethodDescriptor> descriptors = new ArrayList<MethodDescriptor>();
+    for (Method method : clazz.getMethods()) {
+      if (method.isAnnotationPresent(Rpc.class)) {
+        descriptors.add(new MethodDescriptor(clazz, method));
+      }
+    }
+    return descriptors;
+  }
+
+  /**
+   * Invokes the call that belongs to this object with the given parameters. Wraps the response
+   * (possibly an exception) in a JSONObject.
+   *
+   * @param parameters
+   *          {@code JSONArray} containing the parameters
+   * @return result
+   * @throws Throwable
+   */
+  public Object invoke(RpcReceiverManager manager, final JSONArray parameters) throws Throwable {
+
+    final Type[] parameterTypes = getGenericParameterTypes();
+    final Object[] args = new Object[parameterTypes.length];
+    final Annotation annotations[][] = getParameterAnnotations();
+
+    if (parameters.length() > args.length) {
+      throw new RpcError("Too many parameters specified.");
+    }
+
+    for (int i = 0; i < args.length; i++) {
+      final Type parameterType = parameterTypes[i];
+      if (i < parameters.length()) {
+        args[i] = convertParameter(parameters, i, parameterType);
+      } else if (MethodDescriptor.hasDefaultValue(annotations[i])) {
+        args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]);
+      } else {
+        throw new RpcError("Argument " + (i + 1) + " is not present");
+      }
+    }
+
+    return invoke(manager, args);
+  }
+
+  /**
+   * Invokes the call that belongs to this object with the given parameters. Wraps the response
+   * (possibly an exception) in a JSONObject.
+   *
+   * @param parameters {@code Bundle} containing the parameters
+   * @return result
+   * @throws Throwable
+   */
+  public Object invoke(RpcReceiverManager manager, final Bundle parameters) throws Throwable {
+    final Annotation annotations[][] = getParameterAnnotations();
+    final Class<?>[] parameterTypes = getMethod().getParameterTypes();
+    final Object[] args = new Object[parameterTypes.length];
+
+    for (int i = 0; i < parameterTypes.length; i++) {
+      Class<?> parameterType = parameterTypes[i];
+      String parameterName = getName(annotations[i]);
+      if (i < parameterTypes.length) {
+        args[i] = convertParameter(parameters, parameterType, parameterName);
+      } else if (MethodDescriptor.hasDefaultValue(annotations[i])) {
+        args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]);
+      } else {
+        throw new RpcError("Argument " + (i + 1) + " is not present");
+      }
+    }
+    return invoke(manager, args);
+  }
+
+  private Object invoke(RpcReceiverManager manager, Object[] args) throws Throwable{
+    Object result = null;
+    try {
+      result = manager.invoke(mClass, mMethod, args);
+    } catch (Throwable t) {
+      throw t.getCause();
+    }
+    return result;
+  }
+
+  /**
+   * Converts a parameter from JSON into a Java Object.
+   *
+   * @return TODO
+   */
+  // TODO(damonkohler): This signature is a bit weird (auto-refactored). The obvious alternative
+  // would be to work on one supplied parameter and return the converted parameter. However, that's
+  // problematic because you lose the ability to call the getXXX methods on the JSON array.
+  @VisibleForTesting
+  static Object convertParameter(final JSONArray parameters, int index, Type type)
+      throws JSONException, RpcError {
+    try {
+      // Log.d("sl4a", parameters.toString());
+      // Log.d("sl4a", type.toString());
+      // We must handle null and numbers explicitly because we cannot magically cast them. We
+      // also need to convert implicitly from numbers to bools.
+      if (parameters.isNull(index)) {
+        return null;
+      } else if (type == Boolean.class) {
+        try {
+          return parameters.getBoolean(index);
+        } catch (JSONException e) {
+          return new Boolean(parameters.getInt(index) != 0);
+        }
+      } else if (type == Long.class) {
+        return parameters.getLong(index);
+      } else if (type == Double.class) {
+        return parameters.getDouble(index);
+      } else if (type == Integer.class) {
+        return parameters.getInt(index);
+      } else if (type == Intent.class) {
+        return buildIntent(parameters.getJSONObject(index));
+      } else if (type == Integer[].class) {
+        JSONArray list = parameters.getJSONArray(index);
+        Integer[] result = new Integer[list.length()];
+        for (int i = 0; i < list.length(); i++) {
+          result[i] = list.getInt(i);
+        }
+        return result;
+      } else if (type == String[].class) {
+        JSONArray list = parameters.getJSONArray(index);
+        String[] result = new String[list.length()];
+        for (int i = 0; i < list.length(); i++) {
+          result[i] = list.getString(i);
+        }
+        return result;
+      } else if (type == JSONObject.class) {
+          return parameters.getJSONObject(index);
+      } else {
+        // Magically cast the parameter to the right Java type.
+        return ((Class<?>) type).cast(parameters.get(index));
+      }
+    } catch (ClassCastException e) {
+      throw new RpcError("Argument " + (index + 1) + " should be of type "
+          + ((Class<?>) type).getSimpleName() + ".");
+    }
+  }
+
+  private Object convertParameter(Bundle bundle, Class<?> type, String name) {
+    Object param = null;
+    if (type.isAssignableFrom(Boolean.class)) {
+      param = bundle.getBoolean(name, false);
+    }
+    if (type.isAssignableFrom(Boolean[].class)) {
+      param = bundle.getBooleanArray(name);
+    }
+    if (type.isAssignableFrom(String.class)) {
+      param = bundle.getString(name);
+    }
+    if (type.isAssignableFrom(String[].class)) {
+      param = bundle.getStringArray(name);
+    }
+    if (type.isAssignableFrom(Integer.class)) {
+      param = bundle.getInt(name, 0);
+    }
+    if (type.isAssignableFrom(Integer[].class)) {
+      param = bundle.getIntArray(name);
+    }
+    if (type.isAssignableFrom(Bundle.class)) {
+      param = bundle.getBundle(name);
+    }
+    if (type.isAssignableFrom(Parcelable.class)) {
+      param = bundle.getParcelable(name);
+    }
+    if (type.isAssignableFrom(Parcelable[].class)) {
+      param = bundle.getParcelableArray(name);
+    }
+    if (type.isAssignableFrom(Intent.class)) {
+      param = bundle.getParcelable(name);
+    }
+    return param;
+  }
+
+  public static Object buildIntent(JSONObject jsonObject) throws JSONException {
+    Intent intent = new Intent();
+    if (jsonObject.has("action")) {
+      intent.setAction(jsonObject.getString("action"));
+    }
+    if (jsonObject.has("data") && jsonObject.has("type")) {
+      intent.setDataAndType(Uri.parse(jsonObject.optString("data", null)),
+          jsonObject.optString("type", null));
+    } else if (jsonObject.has("data")) {
+      intent.setData(Uri.parse(jsonObject.optString("data", null)));
+    } else if (jsonObject.has("type")) {
+      intent.setType(jsonObject.optString("type", null));
+    }
+    if (jsonObject.has("packagename") && jsonObject.has("classname")) {
+      intent.setClassName(jsonObject.getString("packagename"), jsonObject.getString("classname"));
+    }
+    if (jsonObject.has("flags")) {
+      intent.setFlags(jsonObject.getInt("flags"));
+    }
+    if (!jsonObject.isNull("extras")) {
+      AndroidFacade.putExtrasFromJsonObject(jsonObject.getJSONObject("extras"), intent);
+    }
+    if (!jsonObject.isNull("categories")) {
+      JSONArray categories = jsonObject.getJSONArray("categories");
+      for (int i = 0; i < categories.length(); i++) {
+        intent.addCategory(categories.getString(i));
+      }
+    }
+    return intent;
+  }
+
+  public Method getMethod() {
+    return mMethod;
+  }
+
+  public Class<? extends RpcReceiver> getDeclaringClass() {
+    return mClass;
+  }
+
+  public String getName() {
+    if (mMethod.isAnnotationPresent(RpcName.class)) {
+      return mMethod.getAnnotation(RpcName.class).name();
+    }
+    return mMethod.getName();
+  }
+
+  public Type[] getGenericParameterTypes() {
+    return mMethod.getGenericParameterTypes();
+  }
+
+  public Annotation[][] getParameterAnnotations() {
+    return mMethod.getParameterAnnotations();
+  }
+
+  /**
+   * Returns a human-readable help text for this RPC, based on annotations in the source code.
+   *
+   * @return derived help string
+   */
+  public String getHelp() {
+    StringBuilder helpBuilder = new StringBuilder();
+    Rpc rpcAnnotation = mMethod.getAnnotation(Rpc.class);
+
+    helpBuilder.append(mMethod.getName());
+    helpBuilder.append("(");
+    final Class<?>[] parameterTypes = mMethod.getParameterTypes();
+    final Type[] genericParameterTypes = mMethod.getGenericParameterTypes();
+    final Annotation[][] annotations = mMethod.getParameterAnnotations();
+    for (int i = 0; i < parameterTypes.length; i++) {
+      if (i == 0) {
+        helpBuilder.append("\n  ");
+      } else {
+        helpBuilder.append(",\n  ");
+      }
+
+      helpBuilder.append(getHelpForParameter(genericParameterTypes[i], annotations[i]));
+    }
+    helpBuilder.append(")\n\n");
+    helpBuilder.append(rpcAnnotation.description());
+    if (!rpcAnnotation.returns().equals("")) {
+      helpBuilder.append("\n");
+      helpBuilder.append("\nReturns:\n  ");
+      helpBuilder.append(rpcAnnotation.returns());
+    }
+
+    if (mMethod.isAnnotationPresent(RpcStartEvent.class)) {
+      String eventName = mMethod.getAnnotation(RpcStartEvent.class).value();
+      helpBuilder.append(String.format("\n\nGenerates \"%s\" events.", eventName));
+    }
+
+    if (mMethod.isAnnotationPresent(RpcDeprecated.class)) {
+      String replacedBy = mMethod.getAnnotation(RpcDeprecated.class).value();
+      String release = mMethod.getAnnotation(RpcDeprecated.class).release();
+      helpBuilder.append(String.format("\n\nDeprecated in %s! Please use %s instead.", release,
+          replacedBy));
+    }
+
+    return helpBuilder.toString();
+  }
+
+  /**
+   * Returns the help string for one particular parameter. This respects optional parameters.
+   *
+   * @param parameterType
+   *          (generic) type of the parameter
+   * @param annotations
+   *          annotations of the parameter, may be null
+   * @return string describing the parameter based on source code annotations
+   */
+  private static String getHelpForParameter(Type parameterType, Annotation[] annotations) {
+    StringBuilder result = new StringBuilder();
+
+    appendTypeName(result, parameterType);
+    result.append(" ");
+    result.append(getName(annotations));
+    if (hasDefaultValue(annotations)) {
+      result.append("[optional");
+      if (hasExplicitDefaultValue(annotations)) {
+        result.append(", default " + getDefaultValue(parameterType, annotations));
+      }
+      result.append("]");
+    }
+
+    String description = getDescription(annotations);
+    if (description.length() > 0) {
+      result.append(": ");
+      result.append(description);
+    }
+
+    return result.toString();
+  }
+
+  /**
+   * Appends the name of the given type to the {@link StringBuilder}.
+   *
+   * @param builder
+   *          string builder to append to
+   * @param type
+   *          type whose name to append
+   */
+  private static void appendTypeName(final StringBuilder builder, final Type type) {
+    if (type instanceof Class<?>) {
+      builder.append(((Class<?>) type).getSimpleName());
+    } else {
+      ParameterizedType parametrizedType = (ParameterizedType) type;
+      builder.append(((Class<?>) parametrizedType.getRawType()).getSimpleName());
+      builder.append("<");
+
+      Type[] arguments = parametrizedType.getActualTypeArguments();
+      for (int i = 0; i < arguments.length; i++) {
+        if (i > 0) {
+          builder.append(", ");
+        }
+        appendTypeName(builder, arguments[i]);
+      }
+      builder.append(">");
+    }
+  }
+
+  /**
+   * Returns parameter descriptors suitable for the RPC call text representation.
+   *
+   * <p>
+   * Uses parameter value, default value or name, whatever is available first.
+   *
+   * @return an array of parameter descriptors
+   */
+  public ParameterDescriptor[] getParameterValues(String[] values) {
+    Type[] parameterTypes = mMethod.getGenericParameterTypes();
+    Annotation[][] parametersAnnotations = mMethod.getParameterAnnotations();
+    ParameterDescriptor[] parameters = new ParameterDescriptor[parametersAnnotations.length];
+    for (int index = 0; index < parameters.length; index++) {
+      String value;
+      if (index < values.length) {
+        value = values[index];
+      } else if (hasDefaultValue(parametersAnnotations[index])) {
+        Object defaultValue = getDefaultValue(parameterTypes[index], parametersAnnotations[index]);
+        if (defaultValue == null) {
+          value = null;
+        } else {
+          value = String.valueOf(defaultValue);
+        }
+      } else {
+        value = getName(parametersAnnotations[index]);
+      }
+      parameters[index] = new ParameterDescriptor(value, parameterTypes[index]);
+    }
+    return parameters;
+  }
+
+  /**
+   * Returns parameter hints.
+   *
+   * @return an array of parameter hints
+   */
+  public String[] getParameterHints() {
+    Annotation[][] parametersAnnotations = mMethod.getParameterAnnotations();
+    String[] hints = new String[parametersAnnotations.length];
+    for (int index = 0; index < hints.length; index++) {
+      String name = getName(parametersAnnotations[index]);
+      String description = getDescription(parametersAnnotations[index]);
+      String hint = "No paramenter description.";
+      if (!name.equals("") && !description.equals("")) {
+        hint = name + ": " + description;
+      } else if (!name.equals("")) {
+        hint = name;
+      } else if (!description.equals("")) {
+        hint = description;
+      }
+      hints[index] = hint;
+    }
+    return hints;
+  }
+
+  /**
+   * Extracts the formal parameter name from an annotation.
+   *
+   * @param annotations
+   *          the annotations of the parameter
+   * @return the formal name of the parameter
+   */
+  private static String getName(Annotation[] annotations) {
+    for (Annotation a : annotations) {
+      if (a instanceof RpcParameter) {
+        return ((RpcParameter) a).name();
+      }
+    }
+    throw new IllegalStateException("No parameter name");
+  }
+
+  /**
+   * Extracts the parameter description from its annotations.
+   *
+   * @param annotations
+   *          the annotations of the parameter
+   * @return the description of the parameter
+   */
+  private static String getDescription(Annotation[] annotations) {
+    for (Annotation a : annotations) {
+      if (a instanceof RpcParameter) {
+        return ((RpcParameter) a).description();
+      }
+    }
+    throw new IllegalStateException("No parameter description");
+  }
+
+  /**
+   * Returns the default value for a specific parameter.
+   *
+   * @param parameterType
+   *          parameterType
+   * @param annotations
+   *          annotations of the parameter
+   */
+  public static Object getDefaultValue(Type parameterType, Annotation[] annotations) {
+    for (Annotation a : annotations) {
+      if (a instanceof RpcDefault) {
+        RpcDefault defaultAnnotation = (RpcDefault) a;
+        Converter<?> converter = converterFor(parameterType, defaultAnnotation.converter());
+        return converter.convert(defaultAnnotation.value());
+      } else if (a instanceof RpcOptional) {
+        return null;
+      }
+    }
+    throw new IllegalStateException("No default value for " + parameterType);
+  }
+
+  @SuppressWarnings("rawtypes")
+  private static Converter<?> converterFor(Type parameterType,
+      Class<? extends Converter> converterClass) {
+    if (converterClass == Converter.class) {
+      Converter<?> converter = sConverters.get(parameterType);
+      if (converter == null) {
+        throw new IllegalArgumentException("No predefined converter found for " + parameterType);
+      }
+      return converter;
+    }
+    try {
+      Constructor<?> constructor = converterClass.getConstructor(new Class<?>[0]);
+      return (Converter<?>) constructor.newInstance(new Object[0]);
+    } catch (Exception e) {
+      throw new IllegalArgumentException("Cannot create converter from "
+          + converterClass.getCanonicalName());
+    }
+  }
+
+  /**
+   * Determines whether or not this parameter has default value.
+   *
+   * @param annotations
+   *          annotations of the parameter
+   */
+  public static boolean hasDefaultValue(Annotation[] annotations) {
+    for (Annotation a : annotations) {
+      if (a instanceof RpcDefault || a instanceof RpcOptional) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns whether the default value is specified for a specific parameter.
+   *
+   * @param annotations
+   *          annotations of the parameter
+   */
+  @VisibleForTesting
+  static boolean hasExplicitDefaultValue(Annotation[] annotations) {
+    for (Annotation a : annotations) {
+      if (a instanceof RpcDefault) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /** Returns the converters for {@code String}, {@code Integer} and {@code Boolean}. */
+  private static Map<Class<?>, Converter<?>> populateConverters() {
+    Map<Class<?>, Converter<?>> converters = new HashMap<Class<?>, Converter<?>>();
+    converters.put(String.class, new Converter<String>() {
+      @Override
+      public String convert(String value) {
+        return value;
+      }
+    });
+    converters.put(Integer.class, new Converter<Integer>() {
+      @Override
+      public Integer convert(String input) {
+        try {
+          return Integer.decode(input);
+        } catch (NumberFormatException e) {
+          throw new IllegalArgumentException("'" + input + "' is not an integer");
+        }
+      }
+    });
+    converters.put(Boolean.class, new Converter<Boolean>() {
+      @Override
+      public Boolean convert(String input) {
+        if (input == null) {
+          return null;
+        }
+        input = input.toLowerCase();
+        if (input.equals("true")) {
+          return Boolean.TRUE;
+        }
+        if (input.equals("false")) {
+          return Boolean.FALSE;
+        }
+        throw new IllegalArgumentException("'" + input + "' is not a boolean");
+      }
+    });
+    return converters;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/ParameterDescriptor.java b/Common/src/com/googlecode/android_scripting/rpc/ParameterDescriptor.java
new file mode 100644
index 0000000..021373c
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/ParameterDescriptor.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import java.lang.reflect.Type;
+
+/**
+ * RPC parameter description.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public final class ParameterDescriptor {
+  private final String value;
+  private final Type type;
+
+  public ParameterDescriptor(String value, Type type) {
+    this.value = value;
+    this.type = type;
+  }
+
+  public String getValue() {
+    return value;
+  }
+
+  public Type getType() {
+    return type;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/Rpc.java b/Common/src/com/googlecode/android_scripting/rpc/Rpc.java
new file mode 100644
index 0000000..8fc932e
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/Rpc.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The {@link Rpc} annotation is used to annotate server-side implementations of RPCs. It describes
+ * meta-information (currently a brief documentation of the function), and marks a function as the
+ * implementation of an RPC.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface Rpc {
+  /**
+   * Returns brief description of the function. Should be limited to one or two sentences.
+   */
+  String description();
+
+  /**
+   * Gives a brief description of the functions return value (and the underlying data structure).
+   */
+  String returns() default "";
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/RpcDefault.java b/Common/src/com/googlecode/android_scripting/rpc/RpcDefault.java
new file mode 100644
index 0000000..b27bccc
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/RpcDefault.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation to mark an RPC parameter that have a default value.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+@Documented
+public @interface RpcDefault {
+  /** The default value of the RPC parameter. */
+  public String value();
+
+  @SuppressWarnings("rawtypes")
+  public Class<? extends Converter> converter() default Converter.class;
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/RpcDeprecated.java b/Common/src/com/googlecode/android_scripting/rpc/RpcDeprecated.java
new file mode 100644
index 0000000..c717e5d
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/RpcDeprecated.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation to mark RPC method as deprecated.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface RpcDeprecated {
+  /** The method that replaced this one. */
+  public String value();
+
+  /** Release of SL4A when deprecation occurred. */
+  public String release() default "r4";
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/RpcError.java b/Common/src/com/googlecode/android_scripting/rpc/RpcError.java
new file mode 100644
index 0000000..c4591c7
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/RpcError.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+@SuppressWarnings("serial")
+public class RpcError extends Exception {
+
+  public RpcError(String message) {
+    super(message);
+  }
+
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/RpcMinSdk.java b/Common/src/com/googlecode/android_scripting/rpc/RpcMinSdk.java
new file mode 100644
index 0000000..00b49ec
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/RpcMinSdk.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Use this annotation to specify minimum SDK level (if higher than 3).
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RpcMinSdk {
+  /** Minimum SDK Level. */
+  public int value();
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/RpcName.java b/Common/src/com/googlecode/android_scripting/rpc/RpcName.java
new file mode 100644
index 0000000..b0992e6
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/RpcName.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation to mark an RPC parameter that have a default value.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface RpcName {
+  /** The default value of the RPC parameter. */
+  public String name();
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/RpcOptional.java b/Common/src/com/googlecode/android_scripting/rpc/RpcOptional.java
new file mode 100644
index 0000000..53ac650
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/RpcOptional.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation to mark RPC parameter as optional.
+ *
+ * <p>
+ * The parameter marked as optional has no explicit default value. {@code null} is used as default
+ * value.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+@Documented
+public @interface RpcOptional {
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/RpcParameter.java b/Common/src/com/googlecode/android_scripting/rpc/RpcParameter.java
new file mode 100644
index 0000000..eb84879
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/RpcParameter.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that is used to document the parameters of an RPC.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+@Documented
+public @interface RpcParameter {
+  /**
+   * The name of the formal parameter. This should be in agreement with the java code.
+   */
+  public String name();
+
+  /**
+   * Description of the RPC. This should be a short descriptive statement without a full stop, such
+   * as 'disables the WiFi mode'.
+   */
+  public String description() default "";
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/RpcStartEvent.java b/Common/src/com/googlecode/android_scripting/rpc/RpcStartEvent.java
new file mode 100644
index 0000000..68a5af3
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/RpcStartEvent.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation to mark an RPC as one that starts generating events.
+ *
+ * @author damonkohler@gmail.com (Damon Kohler)
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface RpcStartEvent {
+  /** The name of the event that is generated. */
+  public String value();
+}
diff --git a/Common/src/com/googlecode/android_scripting/rpc/RpcStopEvent.java b/Common/src/com/googlecode/android_scripting/rpc/RpcStopEvent.java
new file mode 100644
index 0000000..fce25f9
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/rpc/RpcStopEvent.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.rpc;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation to mark an RPC as one that stops generating events.
+ *
+ * @author damonkohler@gmail.com (Damon Kohler)
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface RpcStopEvent {
+  /** The name of the event that stops being generated. */
+  public String value();
+}
diff --git a/Common/src/com/googlecode/android_scripting/trigger/ScriptTrigger.java b/Common/src/com/googlecode/android_scripting/trigger/ScriptTrigger.java
new file mode 100644
index 0000000..76640fe
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/trigger/ScriptTrigger.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.trigger;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.googlecode.android_scripting.IntentBuilders;
+import com.googlecode.android_scripting.event.Event;
+
+import java.io.File;
+
+/**
+ * A trigger implementation that launches a given script when the event occurs.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ */
+public class ScriptTrigger implements Trigger {
+  private static final long serialVersionUID = 1804599219214041409L;
+  private final File mScript;
+  private final String mEventName;
+
+  public ScriptTrigger(String eventName, File script) {
+    mEventName = eventName;
+    mScript = script;
+  }
+
+  @Override
+  public void handleEvent(Event event, Context context) {
+    Intent intent = IntentBuilders.buildStartInBackgroundIntent(mScript);
+    // This is required since the script is being started from the TriggerService.
+    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    context.startActivity(intent);
+  }
+
+  @Override
+  public String getEventName() {
+    return mEventName;
+  }
+
+  public File getScript() {
+    return mScript;
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/trigger/Trigger.java b/Common/src/com/googlecode/android_scripting/trigger/Trigger.java
new file mode 100644
index 0000000..e06a973
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/trigger/Trigger.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.trigger;
+
+import android.content.Context;
+
+import com.googlecode.android_scripting.event.Event;
+
+import java.io.Serializable;
+
+/**
+ * Interface implemented by objects listening to events on the event queue inside of the
+ * {@link SerivceManager}.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ */
+public interface Trigger extends Serializable {
+  /**
+   * Handles an event from the event queue.
+   *
+   * @param event
+   *          Event to handle
+   * @param context
+   *          TODO
+   */
+  void handleEvent(Event event, Context context);
+
+  /**
+   * Returns the event name that this {@link Trigger} is interested in.
+   */
+  // TODO(damonkohler): This could be removed by maintaining a reverse mapping from Trigger to event
+  // name in the TriggerRespository.
+  String getEventName();
+}
diff --git a/Common/src/com/googlecode/android_scripting/trigger/TriggerRepository.java b/Common/src/com/googlecode/android_scripting/trigger/TriggerRepository.java
new file mode 100644
index 0000000..4e42bfc
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/trigger/TriggerRepository.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.trigger;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.googlecode.android_scripting.IntentBuilders;
+import com.googlecode.android_scripting.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Map.Entry;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.commons.codec.binary.Base64Codec;
+
+/**
+ * A repository maintaining all currently scheduled triggers. This includes, for example, alarms or
+ * observers of arriving text messages etc. This class is responsible for serializing the list of
+ * triggers to the shared preferences store, and retrieving it from there.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class TriggerRepository {
+  /**
+   * The list of triggers is serialized to the shared preferences entry with this name.
+   */
+  private static final String TRIGGERS_PREF_KEY = "TRIGGERS";
+
+  private final SharedPreferences mPreferences;
+  private final Context mContext;
+
+  /**
+   * An interface for objects that are notified when a trigger is added to the repository.
+   */
+  public interface TriggerRepositoryObserver {
+    /**
+     * Invoked just before the trigger is added to the repository.
+     *
+     * @param trigger
+     *          The trigger about to be added to the repository.
+     */
+    void onPut(Trigger trigger);
+
+    /**
+     * Invoked just after the trigger has been removed from the repository.
+     *
+     * @param trigger
+     *          The trigger that has just been removed from the repository.
+     */
+    void onRemove(Trigger trigger);
+  }
+
+  private final Multimap<String, Trigger> mTriggers;
+  private final CopyOnWriteArrayList<TriggerRepositoryObserver> mTriggerObservers =
+      new CopyOnWriteArrayList<TriggerRepositoryObserver>();
+
+  public TriggerRepository(Context context) {
+    mContext = context;
+    mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+    String triggers = mPreferences.getString(TRIGGERS_PREF_KEY, null);
+    mTriggers = deserializeTriggersFromString(triggers);
+  }
+
+  /** Returns a list of all triggers. The list is unmodifiable. */
+  public synchronized Multimap<String, Trigger> getAllTriggers() {
+    return Multimaps.unmodifiableMultimap(mTriggers);
+  }
+
+  /**
+   * Adds a new trigger to the repository.
+   *
+   * @param trigger
+   *          the {@link Trigger} to add
+   */
+  public synchronized void put(Trigger trigger) {
+    notifyOnAdd(trigger);
+    mTriggers.put(trigger.getEventName(), trigger);
+    storeTriggers();
+    ensureTriggerServiceRunning();
+  }
+
+  /** Removes a specific {@link Trigger}. */
+  public synchronized void remove(final Trigger trigger) {
+    mTriggers.get(trigger.getEventName()).remove(trigger);
+    storeTriggers();
+    notifyOnRemove(trigger);
+  }
+
+  /** Ensures that the {@link TriggerService} is running */
+  private void ensureTriggerServiceRunning() {
+    Intent startTriggerServiceIntent = IntentBuilders.buildTriggerServiceIntent();
+    mContext.startService(startTriggerServiceIntent);
+  }
+
+  /** Notify all {@link TriggerRepositoryObserver}s that a {@link Trigger} was added. */
+  private void notifyOnAdd(Trigger trigger) {
+    for (TriggerRepositoryObserver observer : mTriggerObservers) {
+      observer.onPut(trigger);
+    }
+  }
+
+  /** Notify all {@link TriggerRepositoryObserver}s that a {@link Trigger} was removed. */
+  private void notifyOnRemove(Trigger trigger) {
+    for (TriggerRepositoryObserver observer : mTriggerObservers) {
+      observer.onRemove(trigger);
+    }
+  }
+
+  /** Writes the list of triggers to the shared preferences. */
+  private synchronized void storeTriggers() {
+    SharedPreferences.Editor editor = mPreferences.edit();
+    final String triggerValue = serializeTriggersToString(mTriggers);
+    if (triggerValue != null) {
+      editor.putString(TRIGGERS_PREF_KEY, triggerValue);
+    }
+    editor.commit();
+  }
+
+  /** Deserializes the {@link Multimap} of {@link Trigger}s from a base 64 encoded string. */
+  @SuppressWarnings("unchecked")
+  private Multimap<String, Trigger> deserializeTriggersFromString(String triggers) {
+    if (triggers == null) {
+      return ArrayListMultimap.<String, Trigger> create();
+    }
+    try {
+      final ByteArrayInputStream inputStream =
+          new ByteArrayInputStream(Base64Codec.decodeBase64(triggers.getBytes()));
+      final ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
+      return (Multimap<String, Trigger>) objectInputStream.readObject();
+    } catch (Exception e) {
+      Log.e(e);
+    }
+    return ArrayListMultimap.<String, Trigger> create();
+  }
+
+  /** Serializes the list of triggers to a Base64 encoded string. */
+  private String serializeTriggersToString(Multimap<String, Trigger> triggers) {
+    try {
+      final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+      final ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
+      objectOutputStream.writeObject(triggers);
+      return new String(Base64Codec.encodeBase64(outputStream.toByteArray()));
+    } catch (IOException e) {
+      Log.e(e);
+      return null;
+    }
+  }
+
+  /** Returns {@code true} iff the list of triggers is empty. */
+  public synchronized boolean isEmpty() {
+    return mTriggers.isEmpty();
+  }
+
+  /** Adds a {@link TriggerRepositoryObserver}. */
+  public void addObserver(TriggerRepositoryObserver observer) {
+    mTriggerObservers.add(observer);
+  }
+
+  /**
+   * Adds the given {@link TriggerRepositoryObserver} and invokes
+   * {@link TriggerRepositoryObserver#onPut} for all existing triggers.
+   *
+   * @param observer
+   *          The observer to add.
+   */
+  public synchronized void bootstrapObserver(TriggerRepositoryObserver observer) {
+    addObserver(observer);
+    for (Entry<String, Trigger> trigger : mTriggers.entries()) {
+      observer.onPut(trigger.getValue());
+    }
+  }
+
+  /**
+   * Removes a {@link TriggerRepositoryObserver}.
+   */
+  public void removeObserver(TriggerRepositoryObserver observer) {
+    mTriggerObservers.remove(observer);
+  }
+}
\ No newline at end of file
diff --git a/Common/src/com/googlecode/android_scripting/util/VisibleForTesting.java b/Common/src/com/googlecode/android_scripting/util/VisibleForTesting.java
new file mode 100644
index 0000000..cc8f92a
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/util/VisibleForTesting.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.util;
+
+/**
+ * An annotation that indicates that the visibility of a type or member has been relaxed from
+ * private to package to make the code testable.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+// TODO(igor.v.karp): Consider replacing this annotation by one from Guava or GCL
+public @interface VisibleForTesting {
+}
diff --git a/Common/src/com/googlecode/android_scripting/webcam/JpegProvider.java b/Common/src/com/googlecode/android_scripting/webcam/JpegProvider.java
new file mode 100644
index 0000000..d258882
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/webcam/JpegProvider.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.webcam;
+
+interface JpegProvider {
+  public byte[] getJpeg();
+}
\ No newline at end of file
diff --git a/Common/src/com/googlecode/android_scripting/webcam/MjpegServer.java b/Common/src/com/googlecode/android_scripting/webcam/MjpegServer.java
new file mode 100644
index 0000000..c2b3ac5
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/webcam/MjpegServer.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.webcam;
+
+import java.io.BufferedReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.Socket;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.SimpleServer;
+
+class MjpegServer extends SimpleServer {
+
+  private final JpegProvider mProvider;
+
+  public MjpegServer(JpegProvider provider) {
+    mProvider = provider;
+  }
+
+  @Override
+  protected void handleConnection(Socket socket) throws Exception {
+    Log.d("handle Mjpeg connection");
+    byte[] data = mProvider.getJpeg();
+    if (data == null) {
+      return;
+    }
+    OutputStream outputStream = socket.getOutputStream();
+    outputStream.write((
+        "HTTP/1.0 200 OK\r\n" +
+        "Server: SL4A\r\n" +
+        "Connection: close\r\n" +
+        "Max-Age: 0\r\n" +
+        "Expires: 0\r\n" +
+        "Cache-Control: no-cache, private\r\n" +
+        "Pragma: no-cache\r\n" +
+        "Content-Type: multipart/x-mixed-replace; boundary=--BoundaryString\r\n\r\n").getBytes());
+    while (true) {
+      data = mProvider.getJpeg();
+      if (data == null) {
+        return;
+      }
+      outputStream.write("--BoundaryString\r\n".getBytes());
+      outputStream.write("Content-type: image/jpg\r\n".getBytes());
+      outputStream.write(("Content-Length: " + data.length + "\r\n\r\n").getBytes());
+      outputStream.write(data);
+      outputStream.write("\r\n\r\n".getBytes());
+      outputStream.flush();
+    }
+  }
+
+  @Override
+  protected void handleRPCConnection(Socket sock, Integer UID, BufferedReader reader, PrintWriter writer)
+      throws Exception {
+  }
+}
diff --git a/Common/src/com/googlecode/android_scripting/webcam/WebCamFacade.java b/Common/src/com/googlecode/android_scripting/webcam/WebCamFacade.java
new file mode 100644
index 0000000..b984828
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/webcam/WebCamFacade.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.webcam;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+import android.app.Service;
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.graphics.YuvImage;
+import android.hardware.Camera;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.PreviewCallback;
+import android.hardware.Camera.Size;
+import android.util.Base64;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.WindowManager;
+import android.view.SurfaceHolder.Callback;
+
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.FutureActivityTaskExecutor;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.SingleThreadExecutor;
+import com.googlecode.android_scripting.SimpleServer.SimpleServerObserver;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.future.FutureActivityTask;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDefault;
+import com.googlecode.android_scripting.rpc.RpcOptional;
+import com.googlecode.android_scripting.rpc.RpcParameter;
+
+/**
+ * Manages access to camera streaming.
+ * <br>
+ * <h3>Usage Notes</h3>
+ * <br><b>webCamStart</b> and <b>webCamStop</b> are used to start and stop an Mpeg stream on a given port. <b>webcamAdjustQuality</b> is used to ajust the quality of the streaming video.
+ * <br><b>cameraStartPreview</b> is used to get access to the camera preview screen. It will generate "preview" events as images become available.
+ * <br>The preview has two modes: data or file. If you pass a non-blank, writable file path to the <b>cameraStartPreview</b> it will store jpg images in that folder.
+ * It is up to the caller to clean up these files after the fact. If no file element is provided,
+ * the event will include the image data as a base64 encoded string.
+ * <h3>Event details</h3>
+ * <br>The data element of the preview event will be a map, with the following elements defined.
+ * <ul>
+ * <li><b>format</b> - currently always "jpeg"
+ * <li><b>width</b> - image width (in pixels)
+ * <li><b>height</b> - image height (in pixels)
+ * <li><b>quality</b> - JPEG quality. Number from 1-100
+ * <li><b>filename</b> - Name of file where image was saved. Only relevant if filepath defined.
+ * <li><b>error</b> - included if there was an IOException saving file, ie, disk full or path write protected.
+ * <li><b>encoding</b> - Data encoding. If filepath defined, will be "file" otherwise "base64"
+ * <li><b>data</b> - Base64 encoded image data.
+ * </ul>
+ *<br>Note that "filename", "error" and "data" are mutual exclusive.
+ *<br>
+ *<br>The webcam and preview modes use the same resources, so you can't use them both at the same time. Stop one mode before starting the other.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com) (probably)
+ * @author Robbie Matthews (rjmatthews62@gmail.com)
+ *
+ */
+public class WebCamFacade extends RpcReceiver {
+
+  private final Service mService;
+  private final Executor mJpegCompressionExecutor = new SingleThreadExecutor();
+  private final ByteArrayOutputStream mJpegCompressionBuffer = new ByteArrayOutputStream();
+
+  private volatile byte[] mJpegData;
+
+  private CountDownLatch mJpegDataReady;
+  private boolean mStreaming;
+  private int mPreviewHeight;
+  private int mPreviewWidth;
+  private int mJpegQuality;
+
+  private MjpegServer mJpegServer;
+  private FutureActivityTask<SurfaceHolder> mPreviewTask;
+  private Camera mCamera;
+  private Parameters mParameters;
+  private final EventFacade mEventFacade;
+  private boolean mPreview;
+  private File mDest;
+
+  private final PreviewCallback mPreviewCallback = new PreviewCallback() {
+    @Override
+    public void onPreviewFrame(final byte[] data, final Camera camera) {
+      mJpegCompressionExecutor.execute(new Runnable() {
+        @Override
+        public void run() {
+          mJpegData = compressYuvToJpeg(data);
+          mJpegDataReady.countDown();
+          if (mStreaming) {
+            camera.setOneShotPreviewCallback(mPreviewCallback);
+          }
+        }
+      });
+    }
+  };
+
+  private final PreviewCallback mPreviewEvent = new PreviewCallback() {
+    @Override
+    public void onPreviewFrame(final byte[] data, final Camera camera) {
+      mJpegCompressionExecutor.execute(new Runnable() {
+        @Override
+        public void run() {
+          mJpegData = compressYuvToJpeg(data);
+          Map<String,Object> map = new HashMap<String, Object>();
+          map.put("format", "jpeg");
+          map.put("width", mPreviewWidth);
+          map.put("height", mPreviewHeight);
+          map.put("quality", mJpegQuality);
+          if (mDest!=null) {
+            try {
+              File dest=File.createTempFile("prv",".jpg",mDest);
+              OutputStream output = new FileOutputStream(dest);
+              output.write(mJpegData);
+              output.close();
+              map.put("encoding","file");
+              map.put("filename",dest.toString());
+            } catch (IOException e) {
+              map.put("error", e.toString());
+            }
+          }
+          else {
+            map.put("encoding","Base64");
+            map.put("data", Base64.encodeToString(mJpegData, Base64.DEFAULT));
+          }
+          mEventFacade.postEvent("preview", map);
+          if (mPreview) {
+            camera.setOneShotPreviewCallback(mPreviewEvent);
+          }
+        }
+      });
+    }
+  };
+
+  public WebCamFacade(FacadeManager manager) {
+    super(manager);
+    mService = manager.getService();
+    mJpegDataReady = new CountDownLatch(1);
+    mEventFacade = manager.getReceiver(EventFacade.class);
+  }
+
+  private byte[] compressYuvToJpeg(final byte[] yuvData) {
+    mJpegCompressionBuffer.reset();
+    YuvImage yuvImage =
+        new YuvImage(yuvData, ImageFormat.NV21, mPreviewWidth, mPreviewHeight, null);
+    yuvImage.compressToJpeg(new Rect(0, 0, mPreviewWidth, mPreviewHeight), mJpegQuality,
+        mJpegCompressionBuffer);
+    return mJpegCompressionBuffer.toByteArray();
+  }
+
+  @Rpc(description = "Starts an MJPEG stream and returns a Tuple of address and port for the stream.")
+  public InetSocketAddress webcamStart(
+      @RpcParameter(name = "resolutionLevel", description = "increasing this number provides higher resolution") @RpcDefault("0") Integer resolutionLevel,
+      @RpcParameter(name = "jpegQuality", description = "a number from 0-100") @RpcDefault("20") Integer jpegQuality,
+      @RpcParameter(name = "port", description = "If port is specified, the webcam service will bind to port, otherwise it will pick any available port.") @RpcDefault("0") Integer port)
+      throws Exception {
+    try {
+      openCamera(resolutionLevel, jpegQuality);
+      return startServer(port);
+    } catch (Exception e) {
+      webcamStop();
+      throw e;
+    }
+  }
+
+  private InetSocketAddress startServer(Integer port) {
+    mJpegServer = new MjpegServer(new JpegProvider() {
+      @Override
+      public byte[] getJpeg() {
+        try {
+          mJpegDataReady.await();
+        } catch (InterruptedException e) {
+          Log.e(e);
+        }
+        return mJpegData;
+      }
+    });
+    mJpegServer.addObserver(new SimpleServerObserver() {
+      @Override
+      public void onDisconnect() {
+        if (mJpegServer.getNumberOfConnections() == 0 && mStreaming) {
+          stopStream();
+        }
+      }
+
+      @Override
+      public void onConnect() {
+        if (!mStreaming) {
+          startStream();
+        }
+      }
+    });
+    return mJpegServer.startPublic(port);
+  }
+
+  private void stopServer() {
+    if (mJpegServer != null) {
+      mJpegServer.shutdown();
+      mJpegServer = null;
+    }
+  }
+
+  @Rpc(description = "Adjusts the quality of the webcam stream while it is running.")
+  public void webcamAdjustQuality(
+      @RpcParameter(name = "resolutionLevel", description = "increasing this number provides higher resolution") @RpcDefault("0") Integer resolutionLevel,
+      @RpcParameter(name = "jpegQuality", description = "a number from 0-100") @RpcDefault("20") Integer jpegQuality)
+      throws Exception {
+    if (mStreaming == false) {
+      throw new IllegalStateException("Webcam not streaming.");
+    }
+    stopStream();
+    releaseCamera();
+    openCamera(resolutionLevel, jpegQuality);
+    startStream();
+  }
+
+  private void openCamera(Integer resolutionLevel, Integer jpegQuality) throws IOException,
+      InterruptedException {
+    mCamera = Camera.open();
+    mParameters = mCamera.getParameters();
+    mParameters.setPictureFormat(ImageFormat.JPEG);
+    mParameters.setPreviewFormat(ImageFormat.JPEG);
+    List<Size> supportedPreviewSizes = mParameters.getSupportedPreviewSizes();
+    Collections.sort(supportedPreviewSizes, new Comparator<Size>() {
+      @Override
+      public int compare(Size o1, Size o2) {
+        return o1.width - o2.width;
+      }
+    });
+    Size previewSize =
+        supportedPreviewSizes.get(Math.min(resolutionLevel, supportedPreviewSizes.size() - 1));
+    mPreviewHeight = previewSize.height;
+    mPreviewWidth = previewSize.width;
+    mParameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
+    mJpegQuality = Math.min(Math.max(jpegQuality, 0), 100);
+    mCamera.setParameters(mParameters);
+    // TODO(damonkohler): Rotate image based on orientation.
+    mPreviewTask = createPreviewTask();
+    mCamera.startPreview();
+  }
+
+  private void startStream() {
+    mStreaming = true;
+    mCamera.setOneShotPreviewCallback(mPreviewCallback);
+  }
+
+  private void stopStream() {
+    mJpegDataReady = new CountDownLatch(1);
+    mStreaming = false;
+    if (mPreviewTask != null) {
+      mPreviewTask.finish();
+      mPreviewTask = null;
+    }
+  }
+
+  private void releaseCamera() {
+    if (mCamera != null) {
+      mCamera.release();
+      mCamera = null;
+    }
+    mParameters = null;
+  }
+
+  @Rpc(description = "Stops the webcam stream.")
+  public void webcamStop() {
+    stopServer();
+    stopStream();
+    releaseCamera();
+  }
+
+  private FutureActivityTask<SurfaceHolder> createPreviewTask() throws IOException,
+      InterruptedException {
+    FutureActivityTask<SurfaceHolder> task = new FutureActivityTask<SurfaceHolder>() {
+      @Override
+      public void onCreate() {
+        super.onCreate();
+        final SurfaceView view = new SurfaceView(getActivity());
+        getActivity().setContentView(view);
+        getActivity().getWindow().setSoftInputMode(
+            WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+        //view.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+        view.getHolder().addCallback(new Callback() {
+          @Override
+          public void surfaceDestroyed(SurfaceHolder holder) {
+          }
+
+          @Override
+          public void surfaceCreated(SurfaceHolder holder) {
+            setResult(view.getHolder());
+          }
+
+          @Override
+          public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+          }
+        });
+      }
+    };
+    FutureActivityTaskExecutor taskExecutor =
+        ((BaseApplication) mService.getApplication()).getTaskExecutor();
+    taskExecutor.execute(task);
+    mCamera.setPreviewDisplay(task.getResult());
+    return task;
+  }
+
+  @Rpc(description = "Start Preview Mode. Throws 'preview' events.",returns="True if successful")
+  public boolean cameraStartPreview(
+          @RpcParameter(name = "resolutionLevel", description = "increasing this number provides higher resolution") @RpcDefault("0") Integer resolutionLevel,
+          @RpcParameter(name = "jpegQuality", description = "a number from 0-100") @RpcDefault("20") Integer jpegQuality,
+          @RpcParameter(name = "filepath", description = "Path to store jpeg files.") @RpcOptional String filepath)
+      throws InterruptedException {
+    mDest=null;
+    if (filepath!=null && (filepath.length()>0)) {
+      mDest = new File(filepath);
+      if (!mDest.exists()) mDest.mkdirs();
+      if (!(mDest.isDirectory() && mDest.canWrite())) {
+        return false;
+      }
+    }
+
+    try {
+      openCamera(resolutionLevel, jpegQuality);
+    } catch (IOException e) {
+      Log.e(e);
+      return false;
+    }
+    startPreview();
+    return true;
+  }
+
+  @Rpc(description = "Stop the preview mode.")
+  public void cameraStopPreview() {
+    stopPreview();
+  }
+
+  private void startPreview() {
+    mPreview = true;
+    mCamera.setOneShotPreviewCallback(mPreviewEvent);
+  }
+
+  private void stopPreview() {
+    mPreview = false;
+    if (mPreviewTask!=null)
+    {
+      mPreviewTask.finish();
+      mPreviewTask=null;
+    }
+    releaseCamera();
+  }
+
+  @Override
+  public void shutdown() {
+    mPreview=false;
+    webcamStop();
+  }
+}
diff --git a/Common/src/org/apache/commons/codec/BinaryDecoder.java b/Common/src/org/apache/commons/codec/BinaryDecoder.java
new file mode 100644
index 0000000..07d93fa
--- /dev/null
+++ b/Common/src/org/apache/commons/codec/BinaryDecoder.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.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.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 org.apache.commons.codec;
+
+/**
+ * Defines common decoding methods for byte array decoders.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: BinaryDecoder.java 651573 2008-04-25 11:11:21Z niallp $
+ */
+public interface BinaryDecoder extends Decoder {
+
+    /**
+     * Decodes a byte array and returns the results as a byte array.
+     *
+     * @param pArray A byte array which has been encoded with the
+     *      appropriate encoder
+     *
+     * @return a byte array that contains decoded content
+     *
+     * @throws DecoderException A decoder exception is thrown
+     *          if a Decoder encounters a failure condition during
+     *          the decode process.
+     */
+    byte[] decode(byte[] pArray) throws DecoderException;
+}
+
diff --git a/Common/src/org/apache/commons/codec/BinaryEncoder.java b/Common/src/org/apache/commons/codec/BinaryEncoder.java
new file mode 100644
index 0000000..40bd797
--- /dev/null
+++ b/Common/src/org/apache/commons/codec/BinaryEncoder.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.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.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 org.apache.commons.codec;
+
+/**
+ * Defines common encoding methods for byte array encoders.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: BinaryEncoder.java 651573 2008-04-25 11:11:21Z niallp $
+ */
+public interface BinaryEncoder extends Encoder {
+
+    /**
+     * Encodes a byte array and return the encoded data
+     * as a byte array.
+     *
+     * @param pArray Data to be encoded
+     *
+     * @return A byte array containing the encoded data
+     *
+     * @throws EncoderException thrown if the Encoder
+     *      encounters a failure condition during the
+     *      encoding process.
+     */
+    byte[] encode(byte[] pArray) throws EncoderException;
+}
+
diff --git a/Common/src/org/apache/commons/codec/CharEncoding.java b/Common/src/org/apache/commons/codec/CharEncoding.java
new file mode 100644
index 0000000..01af749
--- /dev/null
+++ b/Common/src/org/apache/commons/codec/CharEncoding.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.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.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 org.apache.commons.codec;
+
+/**
+ * Character encoding names required of every implementation of the Java platform.
+ *
+ * From the Java documentation <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard
+ * charsets</a>:
+ * <p>
+ * <cite>Every implementation of the Java platform is required to support the following character encodings. Consult the
+ * release documentation for your implementation to see if any other encodings are supported. Consult the release
+ * documentation for your implementation to see if any other encodings are supported. </cite>
+ * </p>
+ *
+ * <ul>
+ * <li><code>US-ASCII</code><br/>
+ * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode character set.</li>
+ * <li><code>ISO-8859-1</code><br/>
+ * ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.</li>
+ * <li><code>UTF-8</code><br/>
+ * Eight-bit Unicode Transformation Format.</li>
+ * <li><code>UTF-16BE</code><br/>
+ * Sixteen-bit Unicode Transformation Format, big-endian byte order.</li>
+ * <li><code>UTF-16LE</code><br/>
+ * Sixteen-bit Unicode Transformation Format, little-endian byte order.</li>
+ * <li><code>UTF-16</code><br/>
+ * Sixteen-bit Unicode Transformation Format, byte order specified by a mandatory initial byte-order mark (either order
+ * accepted on input, big-endian used on output.)</li>
+ * </ul>
+ *
+ * This perhaps would best belong in the [lang] project. Even if a similar interface is defined in [lang], it is not
+ * forseen that [codec] would be made to depend on [lang].
+ *
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ * @author Apache Software Foundation
+ * @since 1.4
+ * @version $Id: CharEncoding.java 797857 2009-07-25 23:43:33Z ggregory $
+ */
+public class CharEncoding {
+    /**
+     * CharEncodingISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1. </p>
+     * <p>
+     * Every implementation of the Java platform is required to support this character encoding.
+     * </p>
+     *
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     */
+    public static final String ISO_8859_1 = "ISO-8859-1";
+
+    /**
+     * <p>
+     * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set.
+     * </p>
+     * <p>
+     * Every implementation of the Java platform is required to support this character encoding.
+     * </p>
+     *
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     */
+    public static final String US_ASCII = "US-ASCII";
+
+    /**
+     * <p>
+     * Sixteen-bit Unicode Transformation Format, The byte order specified by a mandatory initial byte-order mark
+     * (either order accepted on input, big-endian used on output)
+     * </p>
+     * <p>
+     * Every implementation of the Java platform is required to support this character encoding.
+     * </p>
+     *
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     */
+    public static final String UTF_16 = "UTF-16";
+
+    /**
+     * <p>
+     * Sixteen-bit Unicode Transformation Format, big-endian byte order.
+     * </p>
+     * <p>
+     * Every implementation of the Java platform is required to support this character encoding.
+     * </p>
+     *
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     */
+    public static final String UTF_16BE = "UTF-16BE";
+
+    /**
+     * <p>
+     * Sixteen-bit Unicode Transformation Format, little-endian byte order.
+     * </p>
+     * <p>
+     * Every implementation of the Java platform is required to support this character encoding.
+     * </p>
+     *
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     */
+    public static final String UTF_16LE = "UTF-16LE";
+
+    /**
+     * <p>
+     * Eight-bit Unicode Transformation Format.
+     * </p>
+     * <p>
+     * Every implementation of the Java platform is required to support this character encoding.
+     * </p>
+     *
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     */
+    public static final String UTF_8 = "UTF-8";
+}
\ No newline at end of file
diff --git a/Common/src/org/apache/commons/codec/Decoder.java b/Common/src/org/apache/commons/codec/Decoder.java
new file mode 100644
index 0000000..c4de37a
--- /dev/null
+++ b/Common/src/org/apache/commons/codec/Decoder.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.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.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 org.apache.commons.codec;
+
+/**
+ * <p>Provides the highest level of abstraction for Decoders.
+ * This is the sister interface of {@link Encoder}.  All
+ * Decoders implement this common generic interface.</p>
+ *
+ * <p>Allows a user to pass a generic Object to any Decoder
+ * implementation in the codec package.</p>
+ *
+ * <p>One of the two interfaces at the center of the codec package.</p>
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Decoder.java 797690 2009-07-24 23:28:35Z ggregory $
+ */
+public interface Decoder {
+
+    /**
+     * Decodes an "encoded" Object and returns a "decoded"
+     * Object.  Note that the implementation of this
+     * interface will try to cast the Object parameter
+     * to the specific type expected by a particular Decoder
+     * implementation.  If a {@link ClassCastException} occurs
+     * this decode method will throw a DecoderException.
+     *
+     * @param pObject an object to "decode"
+     *
+     * @return a 'decoded" object
+     *
+     * @throws DecoderException a decoder exception can
+     * be thrown for any number of reasons.  Some good
+     * candidates are that the parameter passed to this
+     * method is null, a param cannot be cast to the
+     * appropriate type for a specific encoder.
+     */
+    Object decode(Object pObject) throws DecoderException;
+}
+
diff --git a/Common/src/org/apache/commons/codec/DecoderException.java b/Common/src/org/apache/commons/codec/DecoderException.java
new file mode 100644
index 0000000..c928086
--- /dev/null
+++ b/Common/src/org/apache/commons/codec/DecoderException.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.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.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 org.apache.commons.codec;
+
+/**
+ * Thrown when a Decoder has encountered a failure condition during a decode.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: DecoderException.java 797804 2009-07-25 17:27:04Z ggregory $
+ */
+public class DecoderException extends Exception {
+
+    /**
+     * Declares the Serial Version Uid.
+     *
+     * @see <a href="http://c2.com/cgi/wiki?AlwaysDeclareSerialVersionUid">Always Declare Serial Version Uid</a>
+     */
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructs a new exception with <code>null</code> as its detail message. The cause is not initialized, and may
+     * subsequently be initialized by a call to {@link #initCause}.
+     *
+     * @since 1.4
+     */
+    public DecoderException() {
+        super();
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently
+     * be initialized by a call to {@link #initCause}.
+     *
+     * @param message
+     *            The detail message which is saved for later retrieval by the {@link #getMessage()} method.
+     */
+    public DecoderException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructsa new exception with the specified detail message and cause.
+     *
+     * <p>
+     * Note that the detail message associated with <code>cause</code> is not automatically incorporated into this
+     * exception's detail message.
+     * </p>
+     *
+     * @param message
+     *            The detail message which is saved for later retrieval by the {@link #getMessage()} method.
+     * @param cause
+     *            The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>
+     *            value is permitted, and indicates that the cause is nonexistent or unknown.
+     * @since 1.4
+     */
+    public DecoderException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Constructs a new exception with the specified cause and a detail message of <code>(cause==null ?
+     * null : cause.toString())</code> (which typically contains the class and detail message of <code>cause</code>).
+     * This constructor is useful for exceptions that are little more than wrappers for other throwables.
+     *
+     * @param cause
+     *            The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>
+     *            value is permitted, and indicates that the cause is nonexistent or unknown.
+     * @since 1.4
+     */
+    public DecoderException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/Common/src/org/apache/commons/codec/Encoder.java b/Common/src/org/apache/commons/codec/Encoder.java
new file mode 100644
index 0000000..a137e39
--- /dev/null
+++ b/Common/src/org/apache/commons/codec/Encoder.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.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.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 org.apache.commons.codec;
+
+/**
+ * <p>Provides the highest level of abstraction for Encoders.
+ * This is the sister interface of {@link Decoder}.  Every implementation of
+ * Encoder provides this common generic interface whic allows a user to pass a
+ * generic Object to any Encoder implementation in the codec package.</p>
+ *
+ * @author Apache Software Foundation
+ * @version $Id: Encoder.java 634915 2008-03-08 09:30:25Z bayard $
+ */
+public interface Encoder {
+
+    /**
+     * Encodes an "Object" and returns the encoded content
+     * as an Object.  The Objects here may just be <code>byte[]</code>
+     * or <code>String</code>s depending on the implementation used.
+     *
+     * @param pObject An object ot encode
+     *
+     * @return An "encoded" Object
+     *
+     * @throws EncoderException an encoder exception is
+     *  thrown if the encoder experiences a failure
+     *  condition during the encoding process.
+     */
+    Object encode(Object pObject) throws EncoderException;
+}
+
diff --git a/Common/src/org/apache/commons/codec/EncoderException.java b/Common/src/org/apache/commons/codec/EncoderException.java
new file mode 100644
index 0000000..975bb77
--- /dev/null
+++ b/Common/src/org/apache/commons/codec/EncoderException.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.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.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 org.apache.commons.codec;
+
+/**
+ * Thrown when there is a failure condition during the encoding process. This exception is thrown when an Encoder
+ * encounters a encoding specific exception such as invalid data, inability to calculate a checksum, characters outside
+ * of the expected range.
+ *
+ * @author Apache Software Foundation
+ * @version $Id: EncoderException.java 797804 2009-07-25 17:27:04Z ggregory $
+ */
+public class EncoderException extends Exception {
+
+    /**
+     * Declares the Serial Version Uid.
+     *
+     * @see <a href="http://c2.com/cgi/wiki?AlwaysDeclareSerialVersionUid">Always Declare Serial Version Uid</a>
+     */
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructs a new exception with <code>null</code> as its detail message. The cause is not initialized, and may
+     * subsequently be initialized by a call to {@link #initCause}.
+     *
+     * @since 1.4
+     */
+    public EncoderException() {
+        super();
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently
+     * be initialized by a call to {@link #initCause}.
+     *
+     * @param message
+     *            a useful message relating to the encoder specific error.
+     */
+    public EncoderException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message and cause.
+     *
+     * <p>
+     * Note that the detail message associated with <code>cause</code> is not automatically incorporated into this
+     * exception's detail message.
+     * </p>
+     *
+     * @param message
+     *            The detail message which is saved for later retrieval by the {@link #getMessage()} method.
+     * @param cause
+     *            The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>
+     *            value is permitted, and indicates that the cause is nonexistent or unknown.
+     * @since 1.4
+     */
+    public EncoderException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Constructs a new exception with the specified cause and a detail message of <code>(cause==null ?
+     * null : cause.toString())</code> (which typically contains the class and detail message of <code>cause</code>).
+     * This constructor is useful for exceptions that are little more than wrappers for other throwables.
+     *
+     * @param cause
+     *            The cause which is saved for later retrieval by the {@link #getCause()} method. A <code>null</code>
+     *            value is permitted, and indicates that the cause is nonexistent or unknown.
+     * @since 1.4
+     */
+    public EncoderException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/Common/src/org/apache/commons/codec/binary/Base64Codec.java b/Common/src/org/apache/commons/codec/binary/Base64Codec.java
new file mode 100644
index 0000000..20239e2
--- /dev/null
+++ b/Common/src/org/apache/commons/codec/binary/Base64Codec.java
@@ -0,0 +1,1055 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.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.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 org.apache.commons.codec.binary;
+
+import java.math.BigInteger;
+
+import org.apache.commons.codec.BinaryDecoder;
+import org.apache.commons.codec.BinaryEncoder;
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.EncoderException;
+
+/**
+ * Provides Base64 encoding and decoding as defined by RFC 2045.
+ *
+ * <p>
+ * This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
+ * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
+ * </p>
+ * <p>
+ * The class can be parameterized in the following manner with various constructors:
+ * <ul>
+ * <li>URL-safe mode: Default off.</li>
+ * <li>Line length: Default 76. Line length that aren't multiples of 4 will still essentially end up being multiples of
+ * 4 in the encoded data.
+ * <li>Line separator: Default is CRLF ("\r\n")</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode
+ * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc).
+ * </p>
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
+ * @author Apache Software Foundation
+ * @since 1.0
+ * @version $Id: Base64.java 801706 2009-08-06 16:27:06Z niallp $
+ */
+public class Base64Codec implements BinaryEncoder, BinaryDecoder {
+    private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2;
+
+    private static final int DEFAULT_BUFFER_SIZE = 8192;
+
+    /**
+     * Chunk size per RFC 2045 section 6.8.
+     *
+     * <p>
+     * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any
+     * equal signs.
+     * </p>
+     *
+     * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
+     */
+    static final int CHUNK_SIZE = 76;
+
+    /**
+     * Chunk separator per RFC 2045 section 2.1.
+     *
+     * <p>
+     * N.B. The next major release may break compatibility and make this field private.
+     * </p>
+     *
+     * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
+     */
+    static final byte[] CHUNK_SEPARATOR = {'\r', '\n'};
+
+    /**
+     * This array is a lookup table that translates 6-bit positive integer index values into their "Base64 Alphabet"
+     * equivalents as specified in Table 1 of RFC 2045.
+     *
+     * Thanks to "commons" project in ws.apache.org for this code.
+     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+     */
+    private static final byte[] STANDARD_ENCODE_TABLE = {
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
+    };
+
+    /**
+     * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and /
+     * changed to - and _ to make the encoded Base64 results more URL-SAFE.
+     * This table is only used when the Base64's mode is set to URL-SAFE.
+     */
+    private static final byte[] URL_SAFE_ENCODE_TABLE = {
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'
+    };
+
+    /**
+     * Byte used to pad output.
+     */
+    private static final byte PAD = '=';
+
+    /**
+     * This array is a lookup table that translates Unicode characters drawn from the "Base64 Alphabet" (as specified in
+     * Table 1 of RFC 2045) into their 6-bit positive integer equivalents. Characters that are not in the Base64
+     * alphabet but fall within the bounds of the array are translated to -1.
+     *
+     * Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This means decoder seamlessly handles both
+     * URL_SAFE and STANDARD base64. (The encoder, on the other hand, needs to know ahead of time what to emit).
+     *
+     * Thanks to "commons" project in ws.apache.org for this code.
+     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+     */
+    private static final byte[] DECODE_TABLE = {
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54,
+            55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
+            5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
+            24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
+            35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
+    };
+
+    /** Mask used to extract 6 bits, used when encoding */
+    private static final int MASK_6BITS = 0x3f;
+
+    /** Mask used to extract 8 bits, used in decoding base64 bytes */
+    private static final int MASK_8BITS = 0xff;
+
+    // The static final fields above are used for the original static byte[] methods on Base64.
+    // The private member fields below are used with the new streaming approach, which requires
+    // some state be preserved between calls of encode() and decode().
+
+    /**
+     * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE above remains static because it is able
+     * to decode both STANDARD and URL_SAFE streams, but the encodeTable must be a member variable so we can switch
+     * between the two modes.
+     */
+    private final byte[] encodeTable;
+
+    /**
+     * Line length for encoding. Not used when decoding. A value of zero or less implies no chunking of the base64
+     * encoded data.
+     */
+    private final int lineLength;
+
+    /**
+     * Line separator for encoding. Not used when decoding. Only used if lineLength > 0.
+     */
+    private final byte[] lineSeparator;
+
+    /**
+     * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.
+     * <code>decodeSize = 3 + lineSeparator.length;</code>
+     */
+    private final int decodeSize;
+
+    /**
+     * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.
+     * <code>encodeSize = 4 + lineSeparator.length;</code>
+     */
+    private final int encodeSize;
+
+    /**
+     * Buffer for streaming.
+     */
+    private byte[] buffer;
+
+    /**
+     * Position where next character should be written in the buffer.
+     */
+    private int pos;
+
+    /**
+     * Position where next character should be read from the buffer.
+     */
+    private int readPos;
+
+    /**
+     * Variable tracks how many characters have been written to the current line. Only used when encoding. We use it to
+     * make sure each encoded line never goes beyond lineLength (if lineLength > 0).
+     */
+    private int currentLinePos;
+
+    /**
+     * Writes to the buffer only occur after every 3 reads when encoding, an every 4 reads when decoding. This variable
+     * helps track that.
+     */
+    private int modulus;
+
+    /**
+     * Boolean flag to indicate the EOF has been reached. Once EOF has been reached, this Base64 object becomes useless,
+     * and must be thrown away.
+     */
+    private boolean eof;
+
+    /**
+     * Place holder for the 3 bytes we're dealing with for our base64 logic. Bitwise operations store and extract the
+     * base64 encoding or decoding from this variable.
+     */
+    private int x;
+
+    /**
+     * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
+     * <p>
+     * When encoding the line length is 76, the line separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE.
+     * </p>
+     *
+     * <p>
+     * When decoding all variants are supported.
+     * </p>
+     */
+    public Base64Codec() {
+        this(false);
+    }
+
+    /**
+     * Creates a Base64 codec used for decoding (all modes) and encoding in the given URL-safe mode.
+     * <p>
+     * When encoding the line length is 76, the line separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE.
+     * </p>
+     *
+     * <p>
+     * When decoding all variants are supported.
+     * </p>
+     *
+     * @param urlSafe
+     *            if <code>true</code>, URL-safe encoding is used. In most cases this should be set to
+     *            <code>false</code>.
+     * @since 1.4
+     */
+    public Base64Codec(boolean urlSafe) {
+        this(CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe);
+    }
+
+    /**
+     * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
+     * <p>
+     * When encoding the line length is given in the constructor, the line separator is CRLF, and the encoding table is
+     * STANDARD_ENCODE_TABLE.
+     * </p>
+     * <p>
+     * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.
+     * </p>
+     * <p>
+     * When decoding all variants are supported.
+     * </p>
+     *
+     * @param lineLength
+     *            Each line of encoded data will be at most of the given length (rounded down to nearest multiple of 4).
+     *            If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when decoding.
+     * @since 1.4
+     */
+    public Base64Codec(int lineLength) {
+        this(lineLength, CHUNK_SEPARATOR);
+    }
+
+    /**
+     * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
+     * <p>
+     * When encoding the line length and line separator are given in the constructor, and the encoding table is
+     * STANDARD_ENCODE_TABLE.
+     * </p>
+     * <p>
+     * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.
+     * </p>
+     * <p>
+     * When decoding all variants are supported.
+     * </p>
+     *
+     * @param lineLength
+     *            Each line of encoded data will be at most of the given length (rounded down to nearest multiple of 4).
+     *            If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when decoding.
+     * @param lineSeparator
+     *            Each line of encoded data will end with this sequence of bytes.
+     * @throws IllegalArgumentException
+     *             Thrown when the provided lineSeparator included some base64 characters.
+     * @since 1.4
+     */
+    public Base64Codec(int lineLength, byte[] lineSeparator) {
+        this(lineLength, lineSeparator, false);
+    }
+
+    /**
+     * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
+     * <p>
+     * When encoding the line length and line separator are given in the constructor, and the encoding table is
+     * STANDARD_ENCODE_TABLE.
+     * </p>
+     * <p>
+     * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.
+     * </p>
+     * <p>
+     * When decoding all variants are supported.
+     * </p>
+     *
+     * @param lineLength
+     *            Each line of encoded data will be at most of the given length (rounded down to nearest multiple of 4).
+     *            If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when decoding.
+     * @param lineSeparator
+     *            Each line of encoded data will end with this sequence of bytes.
+     * @param urlSafe
+     *            Instead of emitting '+' and '/' we emit '-' and '_' respectively. urlSafe is only applied to encode
+     *            operations. Decoding seamlessly handles both modes.
+     * @throws IllegalArgumentException
+     *             The provided lineSeparator included some base64 characters. That's not going to work!
+     * @since 1.4
+     */
+    public Base64Codec(int lineLength, byte[] lineSeparator, boolean urlSafe) {
+        if (lineSeparator == null) {
+            lineLength = 0;  // disable chunk-separating
+            lineSeparator = CHUNK_SEPARATOR;  // this just gets ignored
+        }
+        this.lineLength = lineLength > 0 ? (lineLength / 4) * 4 : 0;
+        this.lineSeparator = new byte[lineSeparator.length];
+        System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length);
+        if (lineLength > 0) {
+            this.encodeSize = 4 + lineSeparator.length;
+        } else {
+            this.encodeSize = 4;
+        }
+        this.decodeSize = this.encodeSize - 1;
+        if (containsBase64Byte(lineSeparator)) {
+            String sep = StringUtils.newStringUtf8(lineSeparator);
+            throw new IllegalArgumentException("lineSeperator must not contain base64 characters: [" + sep + "]");
+        }
+        this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE;
+    }
+
+    /**
+     * Returns our current encode mode. True if we're URL-SAFE, false otherwise.
+     *
+     * @return true if we're in URL-SAFE mode, false otherwise.
+     * @since 1.4
+     */
+    public boolean isUrlSafe() {
+        return this.encodeTable == URL_SAFE_ENCODE_TABLE;
+    }
+
+    /**
+     * Returns true if this Base64 object has buffered data for reading.
+     *
+     * @return true if there is Base64 object still available for reading.
+     */
+    boolean hasData() {
+        return this.buffer != null;
+    }
+
+    /**
+     * Returns the amount of buffered data available for reading.
+     *
+     * @return The amount of buffered data available for reading.
+     */
+    int avail() {
+        return buffer != null ? pos - readPos : 0;
+    }
+
+    /** Doubles our buffer. */
+    private void resizeBuffer() {
+        if (buffer == null) {
+            buffer = new byte[DEFAULT_BUFFER_SIZE];
+            pos = 0;
+            readPos = 0;
+        } else {
+            byte[] b = new byte[buffer.length * DEFAULT_BUFFER_RESIZE_FACTOR];
+            System.arraycopy(buffer, 0, b, 0, buffer.length);
+            buffer = b;
+        }
+    }
+
+    /**
+     * Extracts buffered data into the provided byte[] array, starting at position bPos, up to a maximum of bAvail
+     * bytes. Returns how many bytes were actually extracted.
+     *
+     * @param b
+     *            byte[] array to extract the buffered data into.
+     * @param bPos
+     *            position in byte[] array to start extraction at.
+     * @param bAvail
+     *            amount of bytes we're allowed to extract. We may extract fewer (if fewer are available).
+     * @return The number of bytes successfully extracted into the provided byte[] array.
+     */
+    int readResults(byte[] b, int bPos, int bAvail) {
+        if (buffer != null) {
+            int len = Math.min(avail(), bAvail);
+            if (buffer != b) {
+                System.arraycopy(buffer, readPos, b, bPos, len);
+                readPos += len;
+                if (readPos >= pos) {
+                    buffer = null;
+                }
+            } else {
+                // Re-using the original consumer's output array is only
+                // allowed for one round.
+                buffer = null;
+            }
+            return len;
+        }
+        return eof ? -1 : 0;
+    }
+
+    /**
+     * Sets the streaming buffer. This is a small optimization where we try to buffer directly to the consumer's output
+     * array for one round (if the consumer calls this method first) instead of starting our own buffer.
+     *
+     * @param out
+     *            byte[] array to buffer directly to.
+     * @param outPos
+     *            Position to start buffering into.
+     * @param outAvail
+     *            Amount of bytes available for direct buffering.
+     */
+    void setInitialBuffer(byte[] out, int outPos, int outAvail) {
+        // We can re-use consumer's original output array under
+        // special circumstances, saving on some System.arraycopy().
+        if (out != null && out.length == outAvail) {
+            buffer = out;
+            pos = outPos;
+            readPos = outPos;
+        }
+    }
+
+    /**
+     * <p>
+     * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with
+     * the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, so flush last
+     * remaining bytes (if not multiple of 3).
+     * </p>
+     * <p>
+     * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach.
+     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+     * </p>
+     *
+     * @param in
+     *            byte[] array of binary data to base64 encode.
+     * @param inPos
+     *            Position to start reading data from.
+     * @param inAvail
+     *            Amount of bytes available from input for encoding.
+     */
+    void encode(byte[] in, int inPos, int inAvail) {
+        if (eof) {
+            return;
+        }
+        // inAvail < 0 is how we're informed of EOF in the underlying data we're
+        // encoding.
+        if (inAvail < 0) {
+            eof = true;
+            if (buffer == null || buffer.length - pos < encodeSize) {
+                resizeBuffer();
+            }
+            switch (modulus) {
+                case 1 :
+                    buffer[pos++] = encodeTable[(x >> 2) & MASK_6BITS];
+                    buffer[pos++] = encodeTable[(x << 4) & MASK_6BITS];
+                    // URL-SAFE skips the padding to further reduce size.
+                    if (encodeTable == STANDARD_ENCODE_TABLE) {
+                        buffer[pos++] = PAD;
+                        buffer[pos++] = PAD;
+                    }
+                    break;
+
+                case 2 :
+                    buffer[pos++] = encodeTable[(x >> 10) & MASK_6BITS];
+                    buffer[pos++] = encodeTable[(x >> 4) & MASK_6BITS];
+                    buffer[pos++] = encodeTable[(x << 2) & MASK_6BITS];
+                    // URL-SAFE skips the padding to further reduce size.
+                    if (encodeTable == STANDARD_ENCODE_TABLE) {
+                        buffer[pos++] = PAD;
+                    }
+                    break;
+            }
+            if (lineLength > 0 && pos > 0) {
+                System.arraycopy(lineSeparator, 0, buffer, pos, lineSeparator.length);
+                pos += lineSeparator.length;
+            }
+        } else {
+            for (int i = 0; i < inAvail; i++) {
+                if (buffer == null || buffer.length - pos < encodeSize) {
+                    resizeBuffer();
+                }
+                modulus = (++modulus) % 3;
+                int b = in[inPos++];
+                if (b < 0) {
+                    b += 256;
+                }
+                x = (x << 8) + b;
+                if (0 == modulus) {
+                    buffer[pos++] = encodeTable[(x >> 18) & MASK_6BITS];
+                    buffer[pos++] = encodeTable[(x >> 12) & MASK_6BITS];
+                    buffer[pos++] = encodeTable[(x >> 6) & MASK_6BITS];
+                    buffer[pos++] = encodeTable[x & MASK_6BITS];
+                    currentLinePos += 4;
+                    if (lineLength > 0 && lineLength <= currentLinePos) {
+                        System.arraycopy(lineSeparator, 0, buffer, pos, lineSeparator.length);
+                        pos += lineSeparator.length;
+                        currentLinePos = 0;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * <p>
+     * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once
+     * with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1"
+     * call is not necessary when decoding, but it doesn't hurt, either.
+     * </p>
+     * <p>
+     * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are
+     * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in,
+     * garbage-out philosophy: it will not check the provided data for validity.
+     * </p>
+     * <p>
+     * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach.
+     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+     * </p>
+     *
+     * @param in
+     *            byte[] array of ascii data to base64 decode.
+     * @param inPos
+     *            Position to start reading data from.
+     * @param inAvail
+     *            Amount of bytes available from input for encoding.
+     */
+    void decode(byte[] in, int inPos, int inAvail) {
+        if (eof) {
+            return;
+        }
+        if (inAvail < 0) {
+            eof = true;
+        }
+        for (int i = 0; i < inAvail; i++) {
+            if (buffer == null || buffer.length - pos < decodeSize) {
+                resizeBuffer();
+            }
+            byte b = in[inPos++];
+            if (b == PAD) {
+                // We're done.
+                eof = true;
+                break;
+            } else {
+                if (b >= 0 && b < DECODE_TABLE.length) {
+                    int result = DECODE_TABLE[b];
+                    if (result >= 0) {
+                        modulus = (++modulus) % 4;
+                        x = (x << 6) + result;
+                        if (modulus == 0) {
+                            buffer[pos++] = (byte) ((x >> 16) & MASK_8BITS);
+                            buffer[pos++] = (byte) ((x >> 8) & MASK_8BITS);
+                            buffer[pos++] = (byte) (x & MASK_8BITS);
+                        }
+                    }
+                }
+            }
+        }
+
+        // Two forms of EOF as far as base64 decoder is concerned: actual
+        // EOF (-1) and first time '=' character is encountered in stream.
+        // This approach makes the '=' padding characters completely optional.
+        if (eof && modulus != 0) {
+            x = x << 6;
+            switch (modulus) {
+                case 2 :
+                    x = x << 6;
+                    buffer[pos++] = (byte) ((x >> 16) & MASK_8BITS);
+                    break;
+                case 3 :
+                    buffer[pos++] = (byte) ((x >> 16) & MASK_8BITS);
+                    buffer[pos++] = (byte) ((x >> 8) & MASK_8BITS);
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Returns whether or not the <code>octet</code> is in the base 64 alphabet.
+     *
+     * @param octet
+     *            The value to test
+     * @return <code>true</code> if the value is defined in the the base 64 alphabet, <code>false</code> otherwise.
+     * @since 1.4
+     */
+    public static boolean isBase64(byte octet) {
+        return octet == PAD || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1);
+    }
+
+    /**
+     * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the
+     * method treats whitespace as valid.
+     *
+     * @param arrayOctet
+     *            byte array to test
+     * @return <code>true</code> if all bytes are valid characters in the Base64 alphabet or if the byte array is empty;
+     *         false, otherwise
+     */
+    public static boolean isArrayByteBase64(byte[] arrayOctet) {
+        for (int i = 0; i < arrayOctet.length; i++) {
+            if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet.
+     *
+     * @param arrayOctet
+     *            byte array to test
+     * @return <code>true</code> if any byte is a valid character in the Base64 alphabet; false herwise
+     */
+    private static boolean containsBase64Byte(byte[] arrayOctet) {
+        for (int i = 0; i < arrayOctet.length; i++) {
+            if (isBase64(arrayOctet[i])) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Encodes binary data using the base64 algorithm but does not chunk the output.
+     *
+     * @param binaryData
+     *            binary data to encode
+     * @return byte[] containing Base64 characters in their UTF-8 representation.
+     */
+    public static byte[] encodeBase64(byte[] binaryData) {
+        return encodeBase64(binaryData, false);
+    }
+
+    /**
+     * Encodes binary data using the base64 algorithm into 76 character blocks separated by CRLF.
+     *
+     * @param binaryData
+     *            binary data to encode
+     * @return String containing Base64 characters.
+     * @since 1.4
+     */
+    public static String encodeBase64String(byte[] binaryData) {
+        return StringUtils.newStringUtf8(encodeBase64(binaryData, true));
+    }
+
+    /**
+     * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The
+     * url-safe variation emits - and _ instead of + and / characters.
+     *
+     * @param binaryData
+     *            binary data to encode
+     * @return byte[] containing Base64 characters in their UTF-8 representation.
+     * @since 1.4
+     */
+    public static byte[] encodeBase64URLSafe(byte[] binaryData) {
+        return encodeBase64(binaryData, false, true);
+    }
+
+    /**
+     * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The
+     * url-safe variation emits - and _ instead of + and / characters.
+     *
+     * @param binaryData
+     *            binary data to encode
+     * @return String containing Base64 characters
+     * @since 1.4
+     */
+    public static String encodeBase64URLSafeString(byte[] binaryData) {
+        return StringUtils.newStringUtf8(encodeBase64(binaryData, false, true));
+    }
+
+    /**
+     * Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks
+     *
+     * @param binaryData
+     *            binary data to encode
+     * @return Base64 characters chunked in 76 character blocks
+     */
+    public static byte[] encodeBase64Chunked(byte[] binaryData) {
+        return encodeBase64(binaryData, true);
+    }
+
+    /**
+     * Decodes an Object using the base64 algorithm. This method is provided in order to satisfy the requirements of the
+     * Decoder interface, and will throw a DecoderException if the supplied object is not of type byte[] or String.
+     *
+     * @param pObject
+     *            Object to decode
+     * @return An object (of type byte[]) containing the binary data which corresponds to the byte[] or String supplied.
+     * @throws DecoderException
+     *             if the parameter supplied is not of type byte[]
+     */
+    public Object decode(Object pObject) throws DecoderException {
+        if (pObject instanceof byte[]) {
+            return decode((byte[]) pObject);
+        } else if (pObject instanceof String) {
+            return decode((String) pObject);
+        } else {
+            throw new DecoderException("Parameter supplied to Base64 decode is not a byte[] or a String");
+        }
+    }
+
+    /**
+     * Decodes a String containing containing characters in the Base64 alphabet.
+     *
+     * @param pArray
+     *            A String containing Base64 character data
+     * @return a byte array containing binary data
+     * @since 1.4
+     */
+    public byte[] decode(String pArray) {
+        return decode(StringUtils.getBytesUtf8(pArray));
+    }
+
+    /**
+     * Decodes a byte[] containing containing characters in the Base64 alphabet.
+     *
+     * @param pArray
+     *            A byte array containing Base64 character data
+     * @return a byte array containing binary data
+     */
+    public byte[] decode(byte[] pArray) {
+        reset();
+        if (pArray == null || pArray.length == 0) {
+            return pArray;
+        }
+        long len = (pArray.length * 3) / 4;
+        byte[] buf = new byte[(int) len];
+        setInitialBuffer(buf, 0, buf.length);
+        decode(pArray, 0, pArray.length);
+        decode(pArray, 0, -1); // Notify decoder of EOF.
+
+        // Would be nice to just return buf (like we sometimes do in the encode
+        // logic), but we have no idea what the line-length was (could even be
+        // variable).  So we cannot determine ahead of time exactly how big an
+        // array is necessary.  Hence the need to construct a 2nd byte array to
+        // hold the final result:
+
+        byte[] result = new byte[pos];
+        readResults(result, 0, result.length);
+        return result;
+    }
+
+    /**
+     * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
+     *
+     * @param binaryData
+     *            Array containing binary data to encode.
+     * @param isChunked
+     *            if <code>true</code> this encoder will chunk the base64 output into 76 character blocks
+     * @return Base64-encoded data.
+     * @throws IllegalArgumentException
+     *             Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}
+     */
+    public static byte[] encodeBase64(byte[] binaryData, boolean isChunked) {
+        return encodeBase64(binaryData, isChunked, false);
+    }
+
+    /**
+     * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
+     *
+     * @param binaryData
+     *            Array containing binary data to encode.
+     * @param isChunked
+     *            if <code>true</code> this encoder will chunk the base64 output into 76 character blocks
+     * @param urlSafe
+     *            if <code>true</code> this encoder will emit - and _ instead of the usual + and / characters.
+     * @return Base64-encoded data.
+     * @throws IllegalArgumentException
+     *             Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}
+     * @since 1.4
+     */
+    public static byte[] encodeBase64(byte[] binaryData, boolean isChunked, boolean urlSafe) {
+        return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE);
+    }
+
+    /**
+     * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
+     *
+     * @param binaryData
+     *            Array containing binary data to encode.
+     * @param isChunked
+     *            if <code>true</code> this encoder will chunk the base64 output into 76 character blocks
+     * @param urlSafe
+     *            if <code>true</code> this encoder will emit - and _ instead of the usual + and / characters.
+     * @param maxResultSize
+     *            The maximum result size to accept.
+     * @return Base64-encoded data.
+     * @throws IllegalArgumentException
+     *             Thrown when the input array needs an output array bigger than maxResultSize
+     * @since 1.4
+     */
+    public static byte[] encodeBase64(byte[] binaryData, boolean isChunked, boolean urlSafe, int maxResultSize) {
+        if (binaryData == null || binaryData.length == 0) {
+            return binaryData;
+        }
+
+        long len = getEncodeLength(binaryData, CHUNK_SIZE, CHUNK_SEPARATOR);
+        if (len > maxResultSize) {
+            throw new IllegalArgumentException("Input array too big, the output array would be bigger (" +
+                len +
+                ") than the specified maxium size of " +
+                maxResultSize);
+        }
+
+        Base64Codec b64 = isChunked ? new Base64Codec(urlSafe) : new Base64Codec(0, CHUNK_SEPARATOR, urlSafe);
+        return b64.encode(binaryData);
+    }
+
+    /**
+     * Decodes a Base64 String into octets
+     *
+     * @param base64String
+     *            String containing Base64 data
+     * @return Array containing decoded data.
+     * @since 1.4
+     */
+    public static byte[] decodeBase64(String base64String) {
+        return new Base64Codec().decode(base64String);
+    }
+
+    /**
+     * Decodes Base64 data into octets
+     *
+     * @param base64Data
+     *            Byte array containing Base64 data
+     * @return Array containing decoded data.
+     */
+    public static byte[] decodeBase64(byte[] base64Data) {
+        return new Base64Codec().decode(base64Data);
+    }
+
+    /**
+     * Discards any whitespace from a base-64 encoded block.
+     *
+     * @param data
+     *            The base-64 encoded data to discard the whitespace from.
+     * @return The data, less whitespace (see RFC 2045).
+     * @deprecated This method is no longer needed
+     */
+    static byte[] discardWhitespace(byte[] data) {
+        byte groomedData[] = new byte[data.length];
+        int bytesCopied = 0;
+        for (int i = 0; i < data.length; i++) {
+            switch (data[i]) {
+                case ' ' :
+                case '\n' :
+                case '\r' :
+                case '\t' :
+                    break;
+                default :
+                    groomedData[bytesCopied++] = data[i];
+            }
+        }
+        byte packedData[] = new byte[bytesCopied];
+        System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
+        return packedData;
+    }
+
+    /**
+     * Checks if a byte value is whitespace or not.
+     *
+     * @param byteToCheck
+     *            the byte to check
+     * @return true if byte is whitespace, false otherwise
+     */
+    private static boolean isWhiteSpace(byte byteToCheck) {
+        switch (byteToCheck) {
+            case ' ' :
+            case '\n' :
+            case '\r' :
+            case '\t' :
+                return true;
+            default :
+                return false;
+        }
+    }
+
+    // Implementation of the Encoder Interface
+
+    /**
+     * Encodes an Object using the base64 algorithm. This method is provided in order to satisfy the requirements of the
+     * Encoder interface, and will throw an EncoderException if the supplied object is not of type byte[].
+     *
+     * @param pObject
+     *            Object to encode
+     * @return An object (of type byte[]) containing the base64 encoded data which corresponds to the byte[] supplied.
+     * @throws EncoderException
+     *             if the parameter supplied is not of type byte[]
+     */
+    public Object encode(Object pObject) throws EncoderException {
+        if (!(pObject instanceof byte[])) {
+            throw new EncoderException("Parameter supplied to Base64 encode is not a byte[]");
+        }
+        return encode((byte[]) pObject);
+    }
+
+    /**
+     * Encodes a byte[] containing binary data, into a String containing characters in the Base64 alphabet.
+     *
+     * @param pArray
+     *            a byte array containing binary data
+     * @return A String containing only Base64 character data
+     * @since 1.4
+     */
+    public String encodeToString(byte[] pArray) {
+        return StringUtils.newStringUtf8(encode(pArray));
+    }
+
+    /**
+     * Encodes a byte[] containing binary data, into a byte[] containing characters in the Base64 alphabet.
+     *
+     * @param pArray
+     *            a byte array containing binary data
+     * @return A byte array containing only Base64 character data
+     */
+    public byte[] encode(byte[] pArray) {
+        reset();
+        if (pArray == null || pArray.length == 0) {
+            return pArray;
+        }
+        long len = getEncodeLength(pArray, lineLength, lineSeparator);
+        byte[] buf = new byte[(int) len];
+        setInitialBuffer(buf, 0, buf.length);
+        encode(pArray, 0, pArray.length);
+        encode(pArray, 0, -1); // Notify encoder of EOF.
+        // Encoder might have resized, even though it was unnecessary.
+        if (buffer != buf) {
+            readResults(buf, 0, buf.length);
+        }
+        // In URL-SAFE mode we skip the padding characters, so sometimes our
+        // final length is a bit smaller.
+        if (isUrlSafe() && pos < buf.length) {
+            byte[] smallerBuf = new byte[pos];
+            System.arraycopy(buf, 0, smallerBuf, 0, pos);
+            buf = smallerBuf;
+        }
+        return buf;
+    }
+
+    /**
+     * Pre-calculates the amount of space needed to base64-encode the supplied array.
+     *
+     * @param pArray byte[] array which will later be encoded
+     * @param chunkSize line-length of the output (<= 0 means no chunking) between each
+     *        chunkSeparator (e.g. CRLF).
+     * @param chunkSeparator the sequence of bytes used to separate chunks of output (e.g. CRLF).
+     *
+     * @return amount of space needed to encoded the supplied array.  Returns
+     *         a long since a max-len array will require Integer.MAX_VALUE + 33%.
+     */
+    private static long getEncodeLength(byte[] pArray, int chunkSize, byte[] chunkSeparator) {
+        // base64 always encodes to multiples of 4.
+        chunkSize = (chunkSize / 4) * 4;
+
+        long len = (pArray.length * 4) / 3;
+        long mod = len % 4;
+        if (mod != 0) {
+            len += 4 - mod;
+        }
+        if (chunkSize > 0) {
+            boolean lenChunksPerfectly = len % chunkSize == 0;
+            len += (len / chunkSize) * chunkSeparator.length;
+            if (!lenChunksPerfectly) {
+                len += chunkSeparator.length;
+            }
+        }
+        return len;
+    }
+
+    // Implementation of integer encoding used for crypto
+    /**
+     * Decodes a byte64-encoded integer according to crypto standards such as W3C's XML-Signature
+     *
+     * @param pArray
+     *            a byte array containing base64 character data
+     * @return A BigInteger
+     * @since 1.4
+     */
+    public static BigInteger decodeInteger(byte[] pArray) {
+        return new BigInteger(1, decodeBase64(pArray));
+    }
+
+    /**
+     * Encodes to a byte64-encoded integer according to crypto standards such as W3C's XML-Signature
+     *
+     * @param bigInt
+     *            a BigInteger
+     * @return A byte array containing base64 character data
+     * @throws NullPointerException
+     *             if null is passed in
+     * @since 1.4
+     */
+    public static byte[] encodeInteger(BigInteger bigInt) {
+        if (bigInt == null) {
+            throw new NullPointerException("encodeInteger called with null parameter");
+        }
+        return encodeBase64(toIntegerBytes(bigInt), false);
+    }
+
+    /**
+     * Returns a byte-array representation of a <code>BigInteger</code> without sign bit.
+     *
+     * @param bigInt
+     *            <code>BigInteger</code> to be converted
+     * @return a byte array representation of the BigInteger parameter
+     */
+    static byte[] toIntegerBytes(BigInteger bigInt) {
+        int bitlen = bigInt.bitLength();
+        // round bitlen
+        bitlen = ((bitlen + 7) >> 3) << 3;
+        byte[] bigBytes = bigInt.toByteArray();
+
+        if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) {
+            return bigBytes;
+        }
+        // set up params for copying everything but sign bit
+        int startSrc = 0;
+        int len = bigBytes.length;
+
+        // if bigInt is exactly byte-aligned, just skip signbit in copy
+        if ((bigInt.bitLength() % 8) == 0) {
+            startSrc = 1;
+            len--;
+        }
+        int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec
+        byte[] resizedBytes = new byte[bitlen / 8];
+        System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);
+        return resizedBytes;
+    }
+
+    /**
+     * Resets this Base64 object to its initial newly constructed state.
+     */
+    private void reset() {
+        buffer = null;
+        pos = 0;
+        readPos = 0;
+        currentLinePos = 0;
+        modulus = 0;
+        eof = false;
+    }
+
+}
diff --git a/Common/src/org/apache/commons/codec/binary/StringUtils.java b/Common/src/org/apache/commons/codec/binary/StringUtils.java
new file mode 100644
index 0000000..e3af657
--- /dev/null
+++ b/Common/src/org/apache/commons/codec/binary/StringUtils.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.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.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 org.apache.commons.codec.binary;
+
+import java.io.UnsupportedEncodingException;
+
+import org.apache.commons.codec.CharEncoding;
+
+/**
+ * Converts String to and from bytes using the encodings required by the Java specification. These encodings are specified in <a
+ * href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ *
+ * @see CharEncoding
+ * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ * @author <a href="mailto:ggregory@seagullsw.com">Gary Gregory</a>
+ * @version $Id: StringUtils.java 801391 2009-08-05 19:55:54Z ggregory $
+ * @since 1.4
+ */
+public class StringUtils {
+
+    /**
+     * Encodes the given string into a sequence of bytes using the ISO-8859-1 charset, storing the result into a new
+     * byte array.
+     *
+     * @param string
+     *            the String to encode
+     * @return encoded bytes
+     * @throws IllegalStateException
+     *             Thrown when the charset is missing, which should be never according the the Java specification.
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     * @see #getBytesUnchecked(String, String)
+     */
+    public static byte[] getBytesIso8859_1(String string) {
+        return StringUtils.getBytesUnchecked(string, CharEncoding.ISO_8859_1);
+    }
+
+    /**
+     * Encodes the given string into a sequence of bytes using the US-ASCII charset, storing the result into a new byte
+     * array.
+     *
+     * @param string
+     *            the String to encode
+     * @return encoded bytes
+     * @throws IllegalStateException
+     *             Thrown when the charset is missing, which should be never according the the Java specification.
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     * @see #getBytesUnchecked(String, String)
+     */
+    public static byte[] getBytesUsAscii(String string) {
+        return StringUtils.getBytesUnchecked(string, CharEncoding.US_ASCII);
+    }
+
+    /**
+     * Encodes the given string into a sequence of bytes using the UTF-16 charset, storing the result into a new byte
+     * array.
+     *
+     * @param string
+     *            the String to encode
+     * @return encoded bytes
+     * @throws IllegalStateException
+     *             Thrown when the charset is missing, which should be never according the the Java specification.
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     * @see #getBytesUnchecked(String, String)
+     */
+    public static byte[] getBytesUtf16(String string) {
+        return StringUtils.getBytesUnchecked(string, CharEncoding.UTF_16);
+    }
+
+    /**
+     * Encodes the given string into a sequence of bytes using the UTF-16BE charset, storing the result into a new byte
+     * array.
+     *
+     * @param string
+     *            the String to encode
+     * @return encoded bytes
+     * @throws IllegalStateException
+     *             Thrown when the charset is missing, which should be never according the the Java specification.
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     * @see #getBytesUnchecked(String, String)
+     */
+    public static byte[] getBytesUtf16Be(String string) {
+        return StringUtils.getBytesUnchecked(string, CharEncoding.UTF_16BE);
+    }
+
+    /**
+     * Encodes the given string into a sequence of bytes using the UTF-16LE charset, storing the result into a new byte
+     * array.
+     *
+     * @param string
+     *            the String to encode
+     * @return encoded bytes
+     * @throws IllegalStateException
+     *             Thrown when the charset is missing, which should be never according the the Java specification.
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     * @see #getBytesUnchecked(String, String)
+     */
+    public static byte[] getBytesUtf16Le(String string) {
+        return StringUtils.getBytesUnchecked(string, CharEncoding.UTF_16LE);
+    }
+
+    /**
+     * Encodes the given string into a sequence of bytes using the UTF-8 charset, storing the result into a new byte
+     * array.
+     *
+     * @param string
+     *            the String to encode
+     * @return encoded bytes
+     * @throws IllegalStateException
+     *             Thrown when the charset is missing, which should be never according the the Java specification.
+     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+     * @see #getBytesUnchecked(String, String)
+     */
+    public static byte[] getBytesUtf8(String string) {
+        return StringUtils.getBytesUnchecked(string, CharEncoding.UTF_8);
+    }
+
+    /**
+     * Encodes the given string into a sequence of bytes using the named charset, storing the result into a new byte
+     * array.
+     * <p>
+     * This method catches {@link UnsupportedEncodingException} and rethrows it as {@link IllegalStateException}, which
+     * should never happen for a required charset name. Use this method when the encoding is required to be in the JRE.
+     * </p>
+     *
+     * @param string
+     *            the String to encode
+     * @param charsetName
+     *            The name of a required {@link java.nio.charset.Charset}
+     * @return encoded bytes
+     * @throws IllegalStateException
+     *             Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen for a
+     *             required charset name.
+     * @see CharEncoding
+     * @see String#getBytes(String)
+     */
+    public static byte[] getBytesUnchecked(String string, String charsetName) {
+        if (string == null) {
+            return null;
+        }
+        try {
+            return string.getBytes(charsetName);
+        } catch (UnsupportedEncodingException e) {
+            throw StringUtils.newIllegalStateException(charsetName, e);
+        }
+    }
+
+    private static IllegalStateException newIllegalStateException(String charsetName, UnsupportedEncodingException e) {
+        return new IllegalStateException(charsetName + ": " + e);
+    }
+
+    /**
+     * Constructs a new <code>String</code> by decoding the specified array of bytes using the given charset.
+     * <p>
+     * This method catches {@link UnsupportedEncodingException} and re-throws it as {@link IllegalStateException}, which
+     * should never happen for a required charset name. Use this method when the encoding is required to be in the JRE.
+     * </p>
+     *
+     * @param bytes
+     *            The bytes to be decoded into characters
+     * @param charsetName
+     *            The name of a required {@link java.nio.charset.Charset}
+     * @return A new <code>String</code> decoded from the specified array of bytes using the given charset.
+     * @throws IllegalStateException
+     *             Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen for a
+     *             required charset name.
+     * @see CharEncoding
+     * @see String#String(byte[], String)
+     */
+    public static String newString(byte[] bytes, String charsetName) {
+        if (bytes == null) {
+            return null;
+        }
+        try {
+            return new String(bytes, charsetName);
+        } catch (UnsupportedEncodingException e) {
+            throw StringUtils.newIllegalStateException(charsetName, e);
+        }
+    }
+
+    /**
+     * Constructs a new <code>String</code> by decoding the specified array of bytes using the ISO-8859-1 charset.
+     *
+     * @param bytes
+     *            The bytes to be decoded into characters
+     * @return A new <code>String</code> decoded from the specified array of bytes using the given charset.
+     * @throws IllegalStateException
+     *             Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+     *             charset is required.
+     */
+    public static String newStringIso8859_1(byte[] bytes) {
+        return StringUtils.newString(bytes, CharEncoding.ISO_8859_1);
+    }
+
+    /**
+     * Constructs a new <code>String</code> by decoding the specified array of bytes using the US-ASCII charset.
+     *
+     * @param bytes
+     *            The bytes to be decoded into characters
+     * @return A new <code>String</code> decoded from the specified array of bytes using the given charset.
+     * @throws IllegalStateException
+     *             Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+     *             charset is required.
+     */
+    public static String newStringUsAscii(byte[] bytes) {
+        return StringUtils.newString(bytes, CharEncoding.US_ASCII);
+    }
+
+    /**
+     * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-16 charset.
+     *
+     * @param bytes
+     *            The bytes to be decoded into characters
+     * @return A new <code>String</code> decoded from the specified array of bytes using the given charset.
+     * @throws IllegalStateException
+     *             Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+     *             charset is required.
+     */
+    public static String newStringUtf16(byte[] bytes) {
+        return StringUtils.newString(bytes, CharEncoding.UTF_16);
+    }
+
+    /**
+     * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-16BE charset.
+     *
+     * @param bytes
+     *            The bytes to be decoded into characters
+     * @return A new <code>String</code> decoded from the specified array of bytes using the given charset.
+     * @throws IllegalStateException
+     *             Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+     *             charset is required.
+     */
+    public static String newStringUtf16Be(byte[] bytes) {
+        return StringUtils.newString(bytes, CharEncoding.UTF_16BE);
+    }
+
+    /**
+     * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-16LE charset.
+     *
+     * @param bytes
+     *            The bytes to be decoded into characters
+     * @return A new <code>String</code> decoded from the specified array of bytes using the given charset.
+     * @throws IllegalStateException
+     *             Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+     *             charset is required.
+     */
+    public static String newStringUtf16Le(byte[] bytes) {
+        return StringUtils.newString(bytes, CharEncoding.UTF_16LE);
+    }
+
+    /**
+     * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-8 charset.
+     *
+     * @param bytes
+     *            The bytes to be decoded into characters
+     * @return A new <code>String</code> decoded from the specified array of bytes using the given charset.
+     * @throws IllegalStateException
+     *             Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen since the
+     *             charset is required.
+     */
+    public static String newStringUtf8(byte[] bytes) {
+        return StringUtils.newString(bytes, CharEncoding.UTF_8);
+    }
+
+}
diff --git a/Docs/generate_api_reference_md.pl b/Docs/generate_api_reference_md.pl
new file mode 100755
index 0000000..546fe28
--- /dev/null
+++ b/Docs/generate_api_reference_md.pl
@@ -0,0 +1,163 @@
+#!/usr/bin/perl
+#
+#  Copyright (C) 2015 Google, Inc.
+#
+#  Licensed under the Apache License, Version 2.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.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.
+#
+
+use strict;
+use warnings;
+use Cwd 'abs_path';
+use JSON;
+use File::Find;
+
+my $sl4a_path = $ARGV[0];
+my $md = "";
+my $md_end = "";
+
+if (not defined $sl4a_path) {
+    $sl4a_path = abs_path($0);
+    $sl4a_path =~ s/\/Docs\/generate_api_reference_md\.pl//g;
+}
+
+sub eachFile {
+    my $filename = $_;
+    my $fullpath = $File::Find::name;
+    if (-e $filename && $filename =~ m/Facade\.java/) {
+        open(FILE, $filename);
+        my @lines = <FILE>;
+        close(FILE);
+
+        my $title = $filename;
+        $title =~ s/\.java//;
+        $title = '**' . $title . '**' . "\n";
+        $md = $md . "\n$title";
+        my $description = "";
+        for (my $i = 0; $i < scalar(@lines); $i++) {
+            my $line = $lines[$i];
+            $line =~ s/\n//;
+            $line =~ s/^\s+|\s+$//g;
+
+            if ($line =~ m /^\@Rpc\(description/) {
+                $description = "";
+                for (my $j = $i; $j < scalar(@lines); $j++) {
+                    my $l = $lines[$j];
+                    $l =~ s/^\s+|\s+$//g;
+                    $description = $description . $l;
+                    if ($l =~ m/\)$/) {
+                        $i = $j;
+                        last;
+                    }
+                }
+                $description = _format_description($description);
+
+            }
+            if ($line =~ m /^public/ && $description ne "") {
+                my @words = split(/\s/, $line);
+                my $func_name = $words[2];
+                my $func_names_and_params = "";
+                if ($func_name =~ /void/) {
+                    $func_name = $words[3];
+                    if ($func_name =~ /void/) {
+                        $description = "";
+                        $func_names_and_params = "";
+                        next;
+                    }
+                }
+                if ($func_name =~ /\(/) {
+                    $func_name =~ s/\(.*//;
+                }
+                $func_name =~ s/\(//g;
+                $func_name =~ s/\)//g;
+                for (my $j = $i; $j < scalar(@lines); $j++) {
+                    $func_names_and_params = $func_names_and_params . $lines[$j];
+                    if ($lines[$j] =~ m/{$/) {
+                        last;
+                    }
+                }
+                $func_names_and_params = _format_func_names_and_params($func_names_and_params);
+                if ($func_names_and_params eq "") {
+                    $func_names_and_params = ")\n";
+                } else {
+                    $func_names_and_params = "\n" . $func_names_and_params;
+                }
+                $md_end = $md_end . "# $func_name\n```\n" .
+                    "$func_name(" . $func_names_and_params . "\n$description\n```\n\n" ;
+                $description = "";
+                $func_names_and_params = "";
+                my $lc_name = lc $func_name;
+                $md = $md . "  * [$func_name](\#$lc_name)\n";
+            }
+        }
+
+    }
+}
+
+sub _format_func_names_and_params {
+    my $fn = shift;
+    $fn =~ s/^\s+|\s+$//g;
+    my @words = split(/\n/,$fn);
+    my $format = "";
+    my $description = "";
+    my $name = "";
+    my $params = "";
+    for my $w (@words) {
+        if ($w =~ /\@RpcParameter\(name = "(.+?)", description = "(.+?)"/) {
+           $name = $1;
+           $description = $2;
+        }
+        elsif ($w =~ /\@RpcParameter\(name = "(.+?)"/) {
+           $name = $1;
+        }
+        if ($w =~ m/,$/) {
+            my @split = split(/\s/, $w);
+            $params = "$split[$#split-1] $split[$#split]";
+            if ($description eq "") {
+                $format = $params;
+            } elsif ($description ne "") {
+                $params =~ s/,//;
+                $format = $format . "  $params: $description,\n"
+            }
+            $description = "";
+            $name = "";
+            $params = "";
+        }
+    }
+    $format =~ s/,$/)/;
+    return $format;
+}
+
+sub _format_description {
+    my $description = shift;
+    $description =~ s/\@Rpc\(//;
+    $description =~ s/^\s+|\s+$//g;
+    $description =~ s/\n//g;
+    $description =~ s/description = \"//g;
+    $description =~ s/\"\)//g;
+    if ($description =~ m/returns(\s*)=/) {
+        $description =~ s/\",//;
+        my @words = split(/returns(\s*)=/, $description);
+        my $des = $words[0];
+        my $ret = $words[1];
+        $ret =~ s/^\s+|\s+$//g;
+        $ret =~ s/^"//;
+        $description = $des . "\n\n" . "Returns:\n" . "  $ret";
+    }
+    return $description;
+}
+
+find (\&eachFile, $sl4a_path);
+open(FILE, ">$sl4a_path/Docs/ApiReference.md");
+print FILE $md . "\n";
+print FILE $md_end . "\n";
+close(FILE);
diff --git a/InterpreterForAndroid/Android.mk b/InterpreterForAndroid/Android.mk
new file mode 100644
index 0000000..7f222f6
--- /dev/null
+++ b/InterpreterForAndroid/Android.mk
@@ -0,0 +1,28 @@
+#
+#  Copyright (C) 2016 Google, Inc.
+#
+#  Licensed under the Apache License, Version 2.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.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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+
+LOCAL_MODULE := sl4a.InterpreterForAndroid
+LOCAL_MODULE_OWNER := google
+LOCAL_STATIC_JAVA_LIBRARIES := guava android-common sl4a.Utils
+#LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4
+LOCAL_SRC_FILES := $(call all-java-files-under, src/com/googlecode/android_scripting)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/InterpreterForAndroid/src/com/googlecode/android_scripting/AsyncTaskListener.java b/InterpreterForAndroid/src/com/googlecode/android_scripting/AsyncTaskListener.java
new file mode 100644
index 0000000..38bb34b
--- /dev/null
+++ b/InterpreterForAndroid/src/com/googlecode/android_scripting/AsyncTaskListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+/**
+ * This listener asynchronously receives a task result whenever it finishes. Should be implemented
+ * to avoid blocking on task's get() method.
+ *
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public interface AsyncTaskListener<T> {
+  public void onTaskFinished(T result, String message);
+}
diff --git a/InterpreterForAndroid/src/com/googlecode/android_scripting/InterpreterInstaller.java b/InterpreterForAndroid/src/com/googlecode/android_scripting/InterpreterInstaller.java
new file mode 100644
index 0000000..53a1d6c
--- /dev/null
+++ b/InterpreterForAndroid/src/com/googlecode/android_scripting/InterpreterInstaller.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.preference.PreferenceManager;
+
+import com.googlecode.android_scripting.exception.Sl4aException;
+import com.googlecode.android_scripting.interpreter.InterpreterConstants;
+import com.googlecode.android_scripting.interpreter.InterpreterDescriptor;
+import com.googlecode.android_scripting.interpreter.InterpreterUtils;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * AsyncTask for installing interpreters.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public abstract class InterpreterInstaller extends AsyncTask<Void, Void, Boolean> {
+
+  protected final InterpreterDescriptor mDescriptor;
+  protected final AsyncTaskListener<Boolean> mTaskListener;
+  protected final Queue<RequestCode> mTaskQueue;
+  protected final Context mContext;
+
+  protected final Handler mainThreadHandler;
+  protected Handler mBackgroundHandler;
+
+  protected volatile AsyncTask<Void, Integer, Long> mTaskHolder;
+
+  protected final String mInterpreterRoot;
+
+  protected static enum RequestCode {
+    DOWNLOAD_INTERPRETER, DOWNLOAD_INTERPRETER_EXTRAS, DOWNLOAD_SCRIPTS, EXTRACT_INTERPRETER,
+    EXTRACT_INTERPRETER_EXTRAS, EXTRACT_SCRIPTS
+  }
+
+  // Executed in the UI thread.
+  private final Runnable mTaskStarter = new Runnable() {
+    @Override
+    public void run() {
+      RequestCode task = mTaskQueue.peek();
+      try {
+        AsyncTask<Void, Integer, Long> newTask = null;
+        switch (task) {
+        case DOWNLOAD_INTERPRETER:
+          newTask = downloadInterpreter();
+          break;
+        case DOWNLOAD_INTERPRETER_EXTRAS:
+          newTask = downloadInterpreterExtras();
+          break;
+        case DOWNLOAD_SCRIPTS:
+          newTask = downloadScripts();
+          break;
+        case EXTRACT_INTERPRETER:
+          newTask = extractInterpreter();
+          break;
+        case EXTRACT_INTERPRETER_EXTRAS:
+          newTask = extractInterpreterExtras();
+          break;
+        case EXTRACT_SCRIPTS:
+          newTask = extractScripts();
+          break;
+        }
+        mTaskHolder = newTask.execute();
+      } catch (Exception e) {
+        Log.v(e.getMessage(), e);
+      }
+
+      if (mBackgroundHandler != null) {
+        mBackgroundHandler.post(mTaskWorker);
+      }
+    }
+  };
+
+  // Executed in the background.
+  private final Runnable mTaskWorker = new Runnable() {
+    @Override
+    public void run() {
+      RequestCode request = mTaskQueue.peek();
+      try {
+        if (mTaskHolder != null && mTaskHolder.get() != null) {
+          mTaskQueue.remove();
+          mTaskHolder = null;
+          // Post processing.
+          if (request == RequestCode.EXTRACT_INTERPRETER && !chmodIntepreter()) {
+            // Chmod returned false.
+            Looper.myLooper().quit();
+          } else if (mTaskQueue.size() == 0) {
+            // We're done here.
+            Looper.myLooper().quit();
+            return;
+          } else if (mainThreadHandler != null) {
+            // There's still some work to do.
+            mainThreadHandler.post(mTaskStarter);
+            return;
+          }
+        }
+      } catch (Exception e) {
+        Log.e(e);
+      }
+      // Something went wrong...
+      switch (request) {
+      case DOWNLOAD_INTERPRETER:
+        Log.e("Downloading interpreter failed.");
+        break;
+      case DOWNLOAD_INTERPRETER_EXTRAS:
+        Log.e("Downloading interpreter extras failed.");
+        break;
+      case DOWNLOAD_SCRIPTS:
+        Log.e("Downloading scripts failed.");
+        break;
+      case EXTRACT_INTERPRETER:
+        Log.e("Extracting interpreter failed.");
+        break;
+      case EXTRACT_INTERPRETER_EXTRAS:
+        Log.e("Extracting interpreter extras failed.");
+        break;
+      case EXTRACT_SCRIPTS:
+        Log.e("Extracting scripts failed.");
+        break;
+      }
+      Looper.myLooper().quit();
+    }
+  };
+
+  // TODO(Alexey): Add Javadoc.
+  public InterpreterInstaller(InterpreterDescriptor descriptor, Context context,
+      AsyncTaskListener<Boolean> taskListener) throws Sl4aException {
+    super();
+    mDescriptor = descriptor;
+    mContext = context;
+    mTaskListener = taskListener;
+    mainThreadHandler = new Handler();
+    mTaskQueue = new LinkedList<RequestCode>();
+
+    String packageName = mDescriptor.getClass().getPackage().getName();
+
+    if (packageName.length() == 0) {
+      throw new Sl4aException("Interpreter package name is empty.");
+    }
+
+    mInterpreterRoot = InterpreterConstants.SDCARD_ROOT + packageName;
+
+    if (mDescriptor == null) {
+      throw new Sl4aException("Interpreter description not provided.");
+    }
+    if (mDescriptor.getName() == null) {
+      throw new Sl4aException("Interpreter not specified.");
+    }
+    if (isInstalled()) {
+      throw new Sl4aException("Interpreter is installed.");
+    }
+
+    if (mDescriptor.hasInterpreterArchive()) {
+      mTaskQueue.offer(RequestCode.DOWNLOAD_INTERPRETER);
+      mTaskQueue.offer(RequestCode.EXTRACT_INTERPRETER);
+    }
+    if (mDescriptor.hasExtrasArchive()) {
+      mTaskQueue.offer(RequestCode.DOWNLOAD_INTERPRETER_EXTRAS);
+      mTaskQueue.offer(RequestCode.EXTRACT_INTERPRETER_EXTRAS);
+    }
+    if (mDescriptor.hasScriptsArchive()) {
+      mTaskQueue.offer(RequestCode.DOWNLOAD_SCRIPTS);
+      mTaskQueue.offer(RequestCode.EXTRACT_SCRIPTS);
+    }
+  }
+
+  @Override
+  protected Boolean doInBackground(Void... params) {
+    new Thread(new Runnable() {
+      @Override
+      public void run() {
+        executeInBackground();
+        final boolean result = (mTaskQueue.size() == 0);
+        mainThreadHandler.post(new Runnable() {
+          @Override
+          public void run() {
+            finish(result);
+          }
+        });
+      }
+    }).start();
+    return true;
+  }
+
+  private boolean executeInBackground() {
+
+    File root = new File(mInterpreterRoot);
+    if (root.exists()) {
+      FileUtils.delete(root);
+    }
+    if (!root.mkdirs()) {
+      Log.e("Failed to make directories: " + root.getAbsolutePath());
+      return false;
+    }
+
+    if (Looper.myLooper() == null) {
+      Looper.prepare();
+    }
+    mBackgroundHandler = new Handler(Looper.myLooper());
+    mainThreadHandler.post(mTaskStarter);
+    Looper.loop();
+    // Have we executed all the tasks?
+    return (mTaskQueue.size() == 0);
+  }
+
+  protected void finish(boolean result) {
+    if (result && setup()) {
+      mTaskListener.onTaskFinished(true, "Installation successful.");
+    } else {
+      if (mTaskHolder != null) {
+        mTaskHolder.cancel(true);
+      }
+      cleanup();
+      mTaskListener.onTaskFinished(false, "Installation failed.");
+    }
+  }
+
+  protected AsyncTask<Void, Integer, Long> download(String in) throws MalformedURLException {
+    String out = mInterpreterRoot;
+    return new UrlDownloaderTask(in, out, mContext);
+  }
+
+  protected AsyncTask<Void, Integer, Long> downloadInterpreter() throws MalformedURLException {
+    return download(mDescriptor.getInterpreterArchiveUrl());
+  }
+
+  protected AsyncTask<Void, Integer, Long> downloadInterpreterExtras() throws MalformedURLException {
+    return download(mDescriptor.getExtrasArchiveUrl());
+  }
+
+  protected AsyncTask<Void, Integer, Long> downloadScripts() throws MalformedURLException {
+    return download(mDescriptor.getScriptsArchiveUrl());
+  }
+
+  protected AsyncTask<Void, Integer, Long> extract(String in, String out, boolean replaceAll)
+      throws Sl4aException {
+    return new ZipExtractorTask(in, out, mContext, replaceAll);
+  }
+
+  protected AsyncTask<Void, Integer, Long> extractInterpreter() throws Sl4aException {
+    String in =
+        new File(mInterpreterRoot, mDescriptor.getInterpreterArchiveName()).getAbsolutePath();
+    String out = InterpreterUtils.getInterpreterRoot(mContext).getAbsolutePath();
+    return extract(in, out, true);
+  }
+
+  protected AsyncTask<Void, Integer, Long> extractInterpreterExtras() throws Sl4aException {
+    String in = new File(mInterpreterRoot, mDescriptor.getExtrasArchiveName()).getAbsolutePath();
+    String out = mInterpreterRoot + InterpreterConstants.INTERPRETER_EXTRAS_ROOT;
+    return extract(in, out, true);
+  }
+
+  protected AsyncTask<Void, Integer, Long> extractScripts() throws Sl4aException {
+    String in = new File(mInterpreterRoot, mDescriptor.getScriptsArchiveName()).getAbsolutePath();
+    String out = InterpreterConstants.SCRIPTS_ROOT;
+    return extract(in, out, false);
+  }
+
+  protected boolean chmodIntepreter() {
+    int dataChmodErrno;
+    boolean interpreterChmodSuccess;
+    try {
+      dataChmodErrno = FileUtils.chmod(InterpreterUtils.getInterpreterRoot(mContext), 0755);
+      interpreterChmodSuccess =
+          FileUtils.recursiveChmod(InterpreterUtils.getInterpreterRoot(mContext, mDescriptor
+              .getName()), 0755);
+    } catch (Exception e) {
+      Log.e(e);
+      return false;
+    }
+    return dataChmodErrno == 0 && interpreterChmodSuccess;
+  }
+
+  protected boolean isInstalled() {
+    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+    return preferences.getBoolean(InterpreterConstants.INSTALLED_PREFERENCE_KEY, false);
+  }
+
+  private void cleanup() {
+    List<File> directories = new ArrayList<File>();
+
+    directories.add(new File(mInterpreterRoot));
+
+    if (mDescriptor.hasInterpreterArchive()) {
+      if (!mTaskQueue.contains(RequestCode.EXTRACT_INTERPRETER)) {
+        directories.add(InterpreterUtils.getInterpreterRoot(mContext, mDescriptor.getName()));
+      }
+    }
+
+    for (File directory : directories) {
+      FileUtils.delete(directory);
+    }
+  }
+
+  protected abstract boolean setup();
+}
diff --git a/InterpreterForAndroid/src/com/googlecode/android_scripting/InterpreterUninstaller.java b/InterpreterForAndroid/src/com/googlecode/android_scripting/InterpreterUninstaller.java
new file mode 100644
index 0000000..5d0c17f
--- /dev/null
+++ b/InterpreterForAndroid/src/com/googlecode/android_scripting/InterpreterUninstaller.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.DialogInterface.OnCancelListener;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+
+import com.googlecode.android_scripting.exception.Sl4aException;
+import com.googlecode.android_scripting.interpreter.InterpreterConstants;
+import com.googlecode.android_scripting.interpreter.InterpreterDescriptor;
+import com.googlecode.android_scripting.interpreter.InterpreterUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * AsyncTask for uninstalling interpreters.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public abstract class InterpreterUninstaller extends AsyncTask<Void, Void, Boolean> {
+
+  protected final InterpreterDescriptor mDescriptor;
+  protected final Context mContext;
+  protected final ProgressDialog mDialog;
+  protected final AsyncTaskListener<Boolean> mListener;
+
+  protected final String mInterpreterRoot;
+
+  public InterpreterUninstaller(InterpreterDescriptor descriptor, Context context,
+      AsyncTaskListener<Boolean> listener) throws Sl4aException {
+
+    super();
+
+    mDescriptor = descriptor;
+    mContext = context;
+    mListener = listener;
+
+    String packageName = mDescriptor.getClass().getPackage().getName();
+
+    if (packageName.length() == 0) {
+      throw new Sl4aException("Interpreter package name is empty.");
+    }
+
+    mInterpreterRoot = InterpreterConstants.SDCARD_ROOT + packageName;
+
+    if (mDescriptor == null) {
+      throw new Sl4aException("Interpreter description not provided.");
+    }
+    if (mDescriptor.getName() == null) {
+      throw new Sl4aException("Interpreter not specified.");
+    }
+    if (!isInstalled()) {
+      throw new Sl4aException("Interpreter not installed.");
+    }
+
+    if (context != null) {
+      mDialog = new ProgressDialog(context);
+    } else {
+      mDialog = null;
+    }
+  }
+
+  public final void execute() {
+    execute(null, null, null);
+  }
+
+  @Override
+  protected void onPreExecute() {
+    if (mDialog != null) {
+      mDialog.setMessage("Uninstalling " + mDescriptor.getNiceName());
+      mDialog.setIndeterminate(true);
+      mDialog.setOnCancelListener(new OnCancelListener() {
+        @Override
+        public void onCancel(DialogInterface dialog) {
+          cancel(true);
+        }
+      });
+      mDialog.show();
+    }
+  }
+
+  @Override
+  protected void onPostExecute(Boolean result) {
+    if (mDialog != null && mDialog.isShowing()) {
+      mDialog.dismiss();
+    }
+    if (result) {
+      mListener.onTaskFinished(result, "Uninstallation successful.");
+    } else {
+      mListener.onTaskFinished(result, "Uninstallation failed.");
+    }
+  }
+
+  @Override
+  protected Boolean doInBackground(Void... params) {
+    List<File> directories = new ArrayList<File>();
+
+    directories.add(new File(mInterpreterRoot));
+
+    if (mDescriptor.hasInterpreterArchive()) {
+      directories.add(InterpreterUtils.getInterpreterRoot(mContext, mDescriptor.getName()));
+    }
+
+    for (File directory : directories) {
+      FileUtils.delete(directory);
+    }
+
+    return cleanup();
+  }
+
+  protected boolean isInstalled() {
+    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+    return preferences.getBoolean(InterpreterConstants.INSTALLED_PREFERENCE_KEY, false);
+  }
+
+  protected abstract boolean cleanup();
+}
diff --git a/InterpreterForAndroid/src/com/googlecode/android_scripting/UrlDownloaderTask.java b/InterpreterForAndroid/src/com/googlecode/android_scripting/UrlDownloaderTask.java
new file mode 100644
index 0000000..f9ba053
--- /dev/null
+++ b/InterpreterForAndroid/src/com/googlecode/android_scripting/UrlDownloaderTask.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.AsyncTask;
+
+
+import com.googlecode.android_scripting.IoUtils;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.exception.Sl4aException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * AsyncTask for extracting ZIP files.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public class UrlDownloaderTask extends AsyncTask<Void, Integer, Long> {
+
+  private final URL mUrl;
+  private final File mFile;
+  private final ProgressDialog mDialog;
+
+  private Throwable mException;
+  private OutputStream mProgressReportingOutputStream;
+
+  private final class ProgressReportingOutputStream extends FileOutputStream {
+    private int mProgress = 0;
+
+    private ProgressReportingOutputStream(File f) throws FileNotFoundException {
+      super(f);
+    }
+
+    @Override
+    public void write(byte[] buffer, int offset, int count) throws IOException {
+      super.write(buffer, offset, count);
+      mProgress += count;
+      publishProgress(mProgress);
+    }
+  }
+
+  public UrlDownloaderTask(String url, String out, Context context) throws MalformedURLException {
+    super();
+    if (context != null) {
+      mDialog = new ProgressDialog(context);
+    } else {
+      mDialog = null;
+    }
+    mUrl = new URL(url);
+    String fileName = new File(mUrl.getFile()).getName();
+    mFile = new File(out, fileName);
+  }
+
+  @Override
+  protected void onPreExecute() {
+    Log.v("Downloading " + mUrl);
+    if (mDialog != null) {
+      mDialog.setTitle("Downloading");
+      mDialog.setMessage(mFile.getName());
+      // mDialog.setIndeterminate(true);
+      mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+      mDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+        @Override
+        public void onCancel(DialogInterface dialog) {
+          cancel(true);
+        }
+      });
+      mDialog.show();
+    }
+  }
+
+  @Override
+  protected Long doInBackground(Void... params) {
+    try {
+      return download();
+    } catch (Exception e) {
+      if (mFile.exists()) {
+        // Clean up bad downloads.
+        mFile.delete();
+      }
+      mException = e;
+      return null;
+    }
+  }
+
+  @Override
+  protected void onProgressUpdate(Integer... progress) {
+    if (mDialog == null) {
+      return;
+    }
+    if (progress.length > 1) {
+      int contentLength = progress[1];
+      if (contentLength == -1) {
+        mDialog.setIndeterminate(true);
+      } else {
+        mDialog.setMax(contentLength);
+      }
+    } else {
+      mDialog.setProgress(progress[0].intValue());
+    }
+  }
+
+  @Override
+  protected void onPostExecute(Long result) {
+    if (mDialog != null && mDialog.isShowing()) {
+      mDialog.dismiss();
+    }
+    if (isCancelled()) {
+      return;
+    }
+    if (mException != null) {
+      Log.e("Download failed.", mException);
+    }
+  }
+
+  @Override
+  protected void onCancelled() {
+    if (mDialog != null) {
+      mDialog.setTitle("Download cancelled.");
+    }
+  }
+
+  private long download() throws Exception {
+    URLConnection connection = null;
+    try {
+      connection = mUrl.openConnection();
+    } catch (IOException e) {
+      throw new Sl4aException("Cannot open URL: " + mUrl, e);
+    }
+
+    int contentLength = connection.getContentLength();
+
+    if (mFile.exists() && contentLength == mFile.length()) {
+      Log.v("Output file already exists. Skipping download.");
+      return 0l;
+    }
+
+    try {
+      mProgressReportingOutputStream = new ProgressReportingOutputStream(mFile);
+    } catch (FileNotFoundException e) {
+      throw new Sl4aException(e);
+    }
+
+    publishProgress(0, contentLength);
+
+    int bytesCopied = IoUtils.copy(connection.getInputStream(), mProgressReportingOutputStream);
+    if (bytesCopied != contentLength && contentLength != -1) {
+      throw new IOException("Download incomplete: " + bytesCopied + " != " + contentLength);
+    }
+    mProgressReportingOutputStream.close();
+    Log.v("Download completed successfully.");
+    return bytesCopied;
+  }
+}
diff --git a/InterpreterForAndroid/src/com/googlecode/android_scripting/ZipExtractorTask.java b/InterpreterForAndroid/src/com/googlecode/android_scripting/ZipExtractorTask.java
new file mode 100644
index 0000000..3d7eb01
--- /dev/null
+++ b/InterpreterForAndroid/src/com/googlecode/android_scripting/ZipExtractorTask.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.os.AsyncTask;
+
+import com.googlecode.android_scripting.exception.Sl4aException;
+import com.googlecode.android_scripting.future.FutureResult;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * AsyncTask for extracting ZIP files.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public class ZipExtractorTask extends AsyncTask<Void, Integer, Long> {
+
+  private static enum Replace {
+    YES, NO, YESTOALL, SKIPALL
+  }
+
+  private final File mInput;
+  private final File mOutput;
+  private final ProgressDialog mDialog;
+  private Throwable mException;
+  private int mProgress = 0;
+  private final Context mContext;
+  private boolean mReplaceAll;
+
+  private final class ProgressReportingOutputStream extends FileOutputStream {
+    private ProgressReportingOutputStream(File f) throws FileNotFoundException {
+      super(f);
+    }
+
+    @Override
+    public void write(byte[] buffer, int offset, int count) throws IOException {
+      super.write(buffer, offset, count);
+      mProgress += count;
+      publishProgress(mProgress);
+    }
+  }
+
+  public ZipExtractorTask(String in, String out, Context context, boolean replaceAll)
+      throws Sl4aException {
+    super();
+    mInput = new File(in);
+    mOutput = new File(out);
+    if (!mOutput.exists()) {
+      if (!mOutput.mkdirs()) {
+        throw new Sl4aException("Failed to make directories: " + mOutput.getAbsolutePath());
+      }
+    }
+    if (context != null) {
+      mDialog = new ProgressDialog(context);
+    } else {
+      mDialog = null;
+    }
+
+    mContext = context;
+    mReplaceAll = replaceAll;
+
+  }
+
+  @Override
+  protected void onPreExecute() {
+    Log.v("Extracting " + mInput.getAbsolutePath() + " to " + mOutput.getAbsolutePath());
+    if (mDialog != null) {
+      mDialog.setTitle("Extracting");
+      mDialog.setMessage(mInput.getName());
+      mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+      mDialog.setOnCancelListener(new OnCancelListener() {
+        @Override
+        public void onCancel(DialogInterface dialog) {
+          cancel(true);
+        }
+      });
+      mDialog.show();
+    }
+  }
+
+  @Override
+  protected Long doInBackground(Void... params) {
+    try {
+      return unzip();
+    } catch (Exception e) {
+      if (mInput.exists()) {
+        // Clean up bad zip file.
+        mInput.delete();
+      }
+      mException = e;
+      return null;
+    }
+  }
+
+  @Override
+  protected void onProgressUpdate(Integer... progress) {
+    if (mDialog == null) {
+      return;
+    }
+    if (progress.length > 1) {
+      int max = progress[1];
+      mDialog.setMax(max);
+    } else {
+      mDialog.setProgress(progress[0].intValue());
+    }
+  }
+
+  @Override
+  protected void onPostExecute(Long result) {
+    if (mDialog != null && mDialog.isShowing()) {
+      mDialog.dismiss();
+    }
+    if (isCancelled()) {
+      return;
+    }
+    if (mException != null) {
+      Log.e("Zip extraction failed.", mException);
+    }
+  }
+
+  @Override
+  protected void onCancelled() {
+    if (mDialog != null) {
+      mDialog.setTitle("Extraction cancelled.");
+    }
+  }
+
+  private long unzip() throws Exception {
+    long extractedSize = 0l;
+    Enumeration<? extends ZipEntry> entries;
+    ZipFile zip = new ZipFile(mInput);
+    long uncompressedSize = getOriginalSize(zip);
+
+    publishProgress(0, (int) uncompressedSize);
+
+    entries = zip.entries();
+
+    try {
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
+        if (entry.isDirectory()) {
+          // Not all zip files actually include separate directory entries.
+          // We'll just ignore them
+          // and create them as necessary for each actual entry.
+          continue;
+        }
+        File destination = new File(mOutput, entry.getName());
+        if (!destination.getParentFile().exists()) {
+          destination.getParentFile().mkdirs();
+        }
+        if (destination.exists() && mContext != null && !mReplaceAll) {
+          Replace answer = showDialog(entry.getName());
+          switch (answer) {
+          case YES:
+            break;
+          case NO:
+            continue;
+          case YESTOALL:
+            mReplaceAll = true;
+            break;
+          default:
+            return extractedSize;
+          }
+        }
+        ProgressReportingOutputStream outStream = new ProgressReportingOutputStream(destination);
+        extractedSize += IoUtils.copy(zip.getInputStream(entry), outStream);
+        outStream.close();
+      }
+    } finally {
+      try {
+        zip.close();
+      } catch (Exception e) {
+        // swallow this exception, we are only interested in the original one
+      }
+    }
+    Log.v("Extraction is complete.");
+    return extractedSize;
+  }
+
+  private long getOriginalSize(ZipFile file) {
+    Enumeration<? extends ZipEntry> entries = file.entries();
+    long originalSize = 0l;
+    while (entries.hasMoreElements()) {
+      ZipEntry entry = entries.nextElement();
+      if (entry.getSize() >= 0) {
+        originalSize += entry.getSize();
+      }
+    }
+    return originalSize;
+  }
+
+  private Replace showDialog(final String name) {
+    final FutureResult<Replace> mResult = new FutureResult<Replace>();
+
+    MainThread.run(mContext, new Runnable() {
+      @Override
+      public void run() {
+        AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+        builder.setTitle(String.format("Script \"%s\" already exist.", name));
+        builder.setMessage(String.format("Do you want to replace script \"%s\" ?", name));
+
+        DialogInterface.OnClickListener buttonListener = new DialogInterface.OnClickListener() {
+          @Override
+          public void onClick(DialogInterface dialog, int which) {
+            Replace result = Replace.SKIPALL;
+            switch (which) {
+            case DialogInterface.BUTTON_POSITIVE:
+              result = Replace.YES;
+              break;
+            case DialogInterface.BUTTON_NEGATIVE:
+              result = Replace.NO;
+              break;
+            case DialogInterface.BUTTON_NEUTRAL:
+              result = Replace.YESTOALL;
+              break;
+            }
+            mResult.set(result);
+            dialog.dismiss();
+          }
+        };
+        builder.setNegativeButton("Skip", buttonListener);
+        builder.setPositiveButton("Replace", buttonListener);
+        builder.setNeutralButton("Replace All", buttonListener);
+
+        builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+          @Override
+          public void onCancel(DialogInterface dialog) {
+            mResult.set(Replace.SKIPALL);
+            dialog.dismiss();
+          }
+        });
+        builder.show();
+      }
+    });
+
+    try {
+      return mResult.get();
+    } catch (InterruptedException e) {
+      Log.e(e);
+    }
+    return null;
+  }
+}
diff --git a/InterpreterForAndroid/src/com/googlecode/android_scripting/activity/Main.java b/InterpreterForAndroid/src/com/googlecode/android_scripting/activity/Main.java
new file mode 100644
index 0000000..2af4d17
--- /dev/null
+++ b/InterpreterForAndroid/src/com/googlecode/android_scripting/activity/Main.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.view.Gravity;
+import android.view.View;
+import android.view.Window;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+import com.googlecode.android_scripting.AsyncTaskListener;
+import com.googlecode.android_scripting.InterpreterInstaller;
+import com.googlecode.android_scripting.InterpreterUninstaller;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.exception.Sl4aException;
+import com.googlecode.android_scripting.interpreter.InterpreterConstants;
+import com.googlecode.android_scripting.interpreter.InterpreterDescriptor;
+
+/**
+ * Base activity for distributing interpreters as APK's.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public abstract class Main extends Activity {
+
+  protected final static float MARGIN_DIP = 3.0f;
+
+  protected final String mId = getClass().getPackage().getName();
+
+  protected SharedPreferences mPreferences;
+  protected InterpreterDescriptor mDescriptor;
+  protected Button mButton;
+  protected LinearLayout mLayout;
+
+  protected abstract InterpreterDescriptor getDescriptor();
+
+  protected abstract InterpreterInstaller getInterpreterInstaller(InterpreterDescriptor descriptor,
+      Context context, AsyncTaskListener<Boolean> listener) throws Sl4aException;
+
+  protected abstract InterpreterUninstaller getInterpreterUninstaller(
+      InterpreterDescriptor descriptor, Context context, AsyncTaskListener<Boolean> listener)
+      throws Sl4aException;
+
+  protected enum RunningTask {
+    INSTALL, UNINSTALL
+  }
+
+  protected volatile RunningTask mCurrentTask = null;
+
+  protected final AsyncTaskListener<Boolean> mTaskListener = new AsyncTaskListener<Boolean>() {
+    @Override
+    public void onTaskFinished(Boolean result, String message) {
+      getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
+          Window.PROGRESS_VISIBILITY_OFF);
+      if (result) {
+        switch (mCurrentTask) {
+        case INSTALL:
+          setInstalled(true);
+          prepareUninstallButton();
+          break;
+        case UNINSTALL:
+          setInstalled(false);
+          prepareInstallButton();
+          break;
+        }
+      }
+      Log.v(Main.this, message);
+      mCurrentTask = null;
+    }
+  };
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+    mDescriptor = getDescriptor();
+
+    requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+    initializeViews();
+    if (checkInstalled()) {
+      prepareUninstallButton();
+    } else {
+      prepareInstallButton();
+    }
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    finish();
+  }
+
+  // TODO(alexey): Pull out to a layout XML?
+  protected void initializeViews() {
+    mLayout = new LinearLayout(this);
+    mLayout.setOrientation(LinearLayout.VERTICAL);
+    mLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+    mLayout.setGravity(Gravity.CENTER_HORIZONTAL);
+
+    mButton = new Button(this);
+    MarginLayoutParams marginParams =
+        new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+    final float scale = getResources().getDisplayMetrics().density;
+    int marginPixels = (int) (MARGIN_DIP * scale + 0.5f);
+    marginParams.setMargins(marginPixels, marginPixels, marginPixels, marginPixels);
+    mButton.setLayoutParams(marginParams);
+    mLayout.addView(mButton);
+    setContentView(mLayout);
+  }
+
+  protected void prepareInstallButton() {
+    mButton.setText("Install");
+    mButton.setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        install();
+      }
+    });
+  }
+
+  protected void prepareUninstallButton() {
+    mButton.setText("Uninstall");
+    mButton.setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        uninstall();
+      }
+    });
+  }
+
+  protected void broadcastInstallationStateChange(boolean isInterpreterInstalled) {
+    Intent intent = new Intent();
+    intent.setData(Uri.parse("package:" + mId));
+    if (isInterpreterInstalled) {
+      intent.setAction(InterpreterConstants.ACTION_INTERPRETER_ADDED);
+    } else {
+      intent.setAction(InterpreterConstants.ACTION_INTERPRETER_REMOVED);
+    }
+    sendBroadcast(intent);
+  }
+
+  protected synchronized void install() {
+    if (mCurrentTask != null) {
+      return;
+    }
+    getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS, Window.PROGRESS_VISIBILITY_ON);
+    mCurrentTask = RunningTask.INSTALL;
+    InterpreterInstaller installTask;
+    try {
+      installTask = getInterpreterInstaller(mDescriptor, Main.this, mTaskListener);
+    } catch (Sl4aException e) {
+      Log.e(this, e.getMessage(), e);
+      return;
+    }
+    installTask.execute();
+  }
+
+  protected synchronized void uninstall() {
+    if (mCurrentTask != null) {
+      return;
+    }
+    getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS, Window.PROGRESS_VISIBILITY_ON);
+    mCurrentTask = RunningTask.UNINSTALL;
+    InterpreterUninstaller uninstallTask;
+    try {
+      uninstallTask = getInterpreterUninstaller(mDescriptor, Main.this, mTaskListener);
+    } catch (Sl4aException e) {
+      Log.e(this, e.getMessage(), e);
+      return;
+    }
+    uninstallTask.execute();
+  }
+
+  protected void setInstalled(boolean isInstalled) {
+    SharedPreferences.Editor editor = mPreferences.edit();
+    editor.putBoolean(InterpreterConstants.INSTALLED_PREFERENCE_KEY, isInstalled);
+    editor.commit();
+    broadcastInstallationStateChange(isInstalled);
+  }
+
+  protected boolean checkInstalled() {
+    boolean isInstalled =
+        mPreferences.getBoolean(InterpreterConstants.INSTALLED_PREFERENCE_KEY, false);
+    broadcastInstallationStateChange(isInstalled);
+    return isInstalled;
+  }
+
+  public LinearLayout getLayout() {
+    return mLayout;
+  }
+
+}
diff --git a/InterpreterForAndroid/src/com/googlecode/android_scripting/interpreter/InterpreterProvider.java b/InterpreterForAndroid/src/com/googlecode/android_scripting/interpreter/InterpreterProvider.java
new file mode 100644
index 0000000..500c5d4
--- /dev/null
+++ b/InterpreterForAndroid/src/com/googlecode/android_scripting/interpreter/InterpreterProvider.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A provider that can be queried to obtain execution-related interpreter info.
+ *
+ * <p>
+ * To create an interpreter APK, please extend this content provider and implement getDescriptor()
+ * and getEnvironmentSettings().<br>
+ * Please declare the provider in the android manifest xml (the authority values has to be set to
+ * your_package_name.provider_name).
+ *
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public abstract class InterpreterProvider extends ContentProvider {
+
+  private static final int PROPERTIES = 1;
+  private static final int ENVIRONMENT_VARIABLES = 2;
+  private static final int ARGUMENTS = 3;
+
+  private UriMatcher mUriMatcher;
+  private SharedPreferences mPreferences;
+
+  private InterpreterDescriptor mDescriptor;
+  private Context mContext;
+
+  public static final String MIME = "vnd.android.cursor.item/vnd.googlecode.interpreter";
+
+  public InterpreterProvider() {
+    mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+    String auth = this.getClass().getName().toLowerCase();
+    mUriMatcher.addURI(auth, InterpreterConstants.PROVIDER_PROPERTIES, PROPERTIES);
+    mUriMatcher.addURI(auth, InterpreterConstants.PROVIDER_ENVIRONMENT_VARIABLES,
+        ENVIRONMENT_VARIABLES);
+    mUriMatcher.addURI(auth, InterpreterConstants.PROVIDER_ARGUMENTS, ARGUMENTS);
+  }
+
+  /**
+   * Returns an instance of the class that implements the desired {@link InterpreterDescriptor}.
+   */
+  protected abstract InterpreterDescriptor getDescriptor();
+
+  @Override
+  public int delete(Uri uri, String selection, String[] selectionArgs) {
+    return 0;
+  }
+
+  @Override
+  public String getType(Uri uri) {
+    return MIME;
+  }
+
+  @Override
+  public Uri insert(Uri uri, ContentValues values) {
+    return null;
+  }
+
+  @Override
+  public boolean onCreate() {
+    mDescriptor = getDescriptor();
+    mContext = getContext();
+    mPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+    return false;
+  }
+
+  @Override
+  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+      String sortOrder) {
+    if (!isInterpreterInstalled()) {
+      return null;
+    }
+    Map<String, String> map;
+    switch (mUriMatcher.match(uri)) {
+    case PROPERTIES:
+      map = getProperties();
+      break;
+    case ENVIRONMENT_VARIABLES:
+      map = getEnvironmentVariables();
+      break;
+    case ARGUMENTS:
+      map = getArguments();
+      break;
+    default:
+      map = null;
+    }
+    return buildCursorFromMap(map);
+  }
+
+  @Override
+  public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+    return 0;
+  }
+
+  private boolean isInterpreterInstalled() {
+    return mPreferences.getBoolean(InterpreterConstants.INSTALLED_PREFERENCE_KEY, false);
+  }
+
+  private Cursor buildCursorFromMap(Map<String, String> map) {
+    if (map == null) {
+      return null;
+    }
+    MatrixCursor cursor = new MatrixCursor(map.keySet().toArray(new String[map.size()]));
+    cursor.addRow(map.values());
+    return cursor;
+  }
+
+  private Map<String, String> getProperties() {
+    Map<String, String> values = new HashMap<String, String>();
+    values.put(InterpreterPropertyNames.NAME, mDescriptor.getName());
+    values.put(InterpreterPropertyNames.NICE_NAME, mDescriptor.getNiceName());
+    values.put(InterpreterPropertyNames.EXTENSION, mDescriptor.getExtension());
+    values.put(InterpreterPropertyNames.BINARY, mDescriptor.getBinary(mContext).getAbsolutePath());
+    values.put(InterpreterPropertyNames.INTERACTIVE_COMMAND, mDescriptor
+        .getInteractiveCommand(mContext));
+    values.put(InterpreterPropertyNames.SCRIPT_COMMAND, mDescriptor.getScriptCommand(mContext));
+    values.put(InterpreterPropertyNames.HAS_INTERACTIVE_MODE, Boolean.toString(mDescriptor
+        .hasInteractiveMode()));
+    return values;
+  }
+
+  private Map<String, String> getEnvironmentVariables() {
+    Map<String, String> values = new HashMap<String, String>();
+    values.putAll(mDescriptor.getEnvironmentVariables(mContext));
+    return values;
+  }
+
+  private Map<String, String> getArguments() {
+    Map<String, String> values = new LinkedHashMap<String, String>();
+    int column = 0;
+    for (String argument : mDescriptor.getArguments(mContext)) {
+      values.put(Integer.toString(column), argument);
+      column++;
+    }
+    return values;
+  }
+}
diff --git a/InterpreterForAndroid/src/com/googlecode/android_scripting/interpreter/Sl4aHostedInterpreter.java b/InterpreterForAndroid/src/com/googlecode/android_scripting/interpreter/Sl4aHostedInterpreter.java
new file mode 100644
index 0000000..e6ca41c
--- /dev/null
+++ b/InterpreterForAndroid/src/com/googlecode/android_scripting/interpreter/Sl4aHostedInterpreter.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+import android.content.Context;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A description of the interpreters hosted by the SL4A project.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public abstract class Sl4aHostedInterpreter implements InterpreterDescriptor {
+
+  public static final String BASE_INSTALL_URL = "http://android-scripting.googlecode.com/files/";
+  public static final String DALVIKVM = "/system/bin/dalvikvm";
+
+  // TODO(damonkohler): Remove getVersion() and pull these three version methods up in to the base
+  // class.
+
+  public String getBaseInstallUrl() {
+    return BASE_INSTALL_URL;
+  }
+
+  public int getInterpreterVersion() {
+    return getVersion();
+  }
+
+  public int getExtrasVersion() {
+    return getVersion();
+  }
+
+  public int getScriptsVersion() {
+    return getVersion();
+  }
+
+  @Override
+  public String getInterpreterArchiveName() {
+    return String.format("%s_r%s.zip", getName(), getInterpreterVersion());
+  }
+
+  @Override
+  public String getExtrasArchiveName() {
+    return String.format("%s_extras_r%s.zip", getName(), getExtrasVersion());
+  }
+
+  @Override
+  public String getScriptsArchiveName() {
+    return String.format("%s_scripts_r%s.zip", getName(), getScriptsVersion());
+  }
+
+  @Override
+  public String getInterpreterArchiveUrl() {
+    return getBaseInstallUrl() + getInterpreterArchiveName();
+  }
+
+  @Override
+  public String getExtrasArchiveUrl() {
+    return getBaseInstallUrl() + getExtrasArchiveName();
+  }
+
+  @Override
+  public String getScriptsArchiveUrl() {
+    return getBaseInstallUrl() + getScriptsArchiveName();
+  }
+
+  @Override
+  public String getInteractiveCommand(Context context) {
+    return "";
+  }
+
+  @Override
+  public boolean hasInteractiveMode() {
+    return true;
+  }
+
+  @Override
+  public String getScriptCommand(Context context) {
+    return "%s";
+  }
+
+  @Override
+  public List<String> getArguments(Context context) {
+    return new ArrayList<String>();
+  }
+
+  @Override
+  public Map<String, String> getEnvironmentVariables(Context context) {
+    return new HashMap<String, String>();
+  }
+
+  // TODO(damonkohler): This shouldn't be public.
+  public File getExtrasPath(Context context) {
+    if (!hasInterpreterArchive() && hasExtrasArchive()) {
+      return new File(InterpreterConstants.SDCARD_ROOT + this.getClass().getPackage().getName()
+          + InterpreterConstants.INTERPRETER_EXTRAS_ROOT, getName());
+    }
+    return InterpreterUtils.getInterpreterRoot(context, getName());
+  }
+}
diff --git a/README b/README
new file mode 100644
index 0000000..cf7b58b
--- /dev/null
+++ b/README
@@ -0,0 +1,61 @@
+=============================
+Project Info
+=============================
+Scripting Layer For Android
+
+Originally authored by Damon Kohler, Scripting Layer for Android, SL4A, is an automation toolset
+for calling Android APIs in a platform-independent manner. It supports both remote automation via
+ADB as well as execution of scripts from on-device via a series of lightweight translation layers.
+
+=============================
+Build Instructions
+=============================
+
+Due to its inclusion in AOSP as a privileged app, building SL4A requires a system build.
+
+For the initial build of Android:
+
+1) cd <ANDROID_SOURCE_ROOT>
+2) source build/envsetup.sh
+3) lunch <TARGET>
+4) make [-j15]
+
+Then Build SL4A:
+
+1) cd <ANDROID_SOURCE_ROOT>/packages/apps/Test/connectivity/sl4a
+2) mm [-j15]
+
+===========================
+Install Instructions
+===========================
+
+1) adb install -r <ANDROID_SOURCE_ROOT>/out/target/product/<TARGET>/data/app/sl4a/sl4a.apk
+
+===========================
+Run Instructions
+===========================
+
+1) SL4A may be launched from Android as a normal App.
+
+or;
+
+2) To enable RPC access from the command prompt:
+
+    1) adb forward tcp:<HOST_PORT_NUM> tcp:<DEVICE_PORT_NUM>
+    2) adb shell "am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER \
+               --ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT <DEVICE_PORT_NUM> \
+               com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher"
+
+    where <HOST_PORT_NUM> and <DEVICE_PORT_NUM> are the tcp ports on the host computer and device.
+
+
+===========================
+Doc Generation Instructions
+===========================
+
+From SL4A source directory run this command:
+    1)"perl Docs/generate_api_reference_md.pl"
+
+In the Docs direcotry there should now be an ApiReference.md file that
+contains which RPC functions are available in SL4A as well as documentation
+for the RPC functions.
diff --git a/ScriptingLayer/Android.mk b/ScriptingLayer/Android.mk
new file mode 100644
index 0000000..2871eff
--- /dev/null
+++ b/ScriptingLayer/Android.mk
@@ -0,0 +1,31 @@
+#
+#  Copyright (C) 2016 Google, Inc.
+#
+#  Licensed under the Apache License, Version 2.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.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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+
+LOCAL_MODULE := sl4a.ScriptingLayer
+LOCAL_MODULE_OWNER := google
+
+LOCAL_STATIC_JAVA_LIBRARIES := guava android-common
+LOCAL_STATIC_JAVA_LIBRARIES += sl4a.Utils sl4a.Common
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src/com/googlecode/android_scripting)
+
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/ScriptingLayer/src/com/googlecode/android_scripting/AndroidProxy.java b/ScriptingLayer/src/com/googlecode/android_scripting/AndroidProxy.java
new file mode 100644
index 0000000..997582c
--- /dev/null
+++ b/ScriptingLayer/src/com/googlecode/android_scripting/AndroidProxy.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.app.Service;
+import android.content.Intent;
+
+import com.googlecode.android_scripting.facade.FacadeConfiguration;
+import com.googlecode.android_scripting.facade.FacadeManagerFactory;
+import com.googlecode.android_scripting.jsonrpc.JsonRpcServer;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManagerFactory;
+
+import java.net.InetSocketAddress;
+import java.util.UUID;
+
+public class AndroidProxy {
+
+  private InetSocketAddress mAddress;
+  private final JsonRpcServer mJsonRpcServer;
+  private final UUID mSecret;
+  private final RpcReceiverManagerFactory mFacadeManagerFactory;
+
+  /**
+   *
+   * @param service
+   *          Android service (required to build facades).
+   * @param intent
+   *          the intent that launched the proxy/script.
+   * @param requiresHandshake
+   *          indicates whether RPC security protocol should be enabled.
+   */
+  public AndroidProxy(Service service, Intent intent, boolean requiresHandshake) {
+    if (requiresHandshake) {
+      mSecret = UUID.randomUUID();
+    } else {
+      mSecret = null;
+    }
+    mFacadeManagerFactory =
+        new FacadeManagerFactory(FacadeConfiguration.getSdkLevel(), service, intent,
+            FacadeConfiguration.getFacadeClasses());
+    mJsonRpcServer = new JsonRpcServer(mFacadeManagerFactory, getSecret());
+  }
+
+  public InetSocketAddress getAddress() {
+    return mAddress;
+  }
+
+  public InetSocketAddress startLocal() {
+    return startLocal(0);
+  }
+
+  public InetSocketAddress startLocal(int port) {
+    mAddress = mJsonRpcServer.startLocal(port);
+    return mAddress;
+  }
+
+  public InetSocketAddress startPublic() {
+    return startPublic(0);
+  }
+
+  public InetSocketAddress startPublic(int port) {
+    mAddress = mJsonRpcServer.startPublic(port);
+    return mAddress;
+  }
+
+  public void shutdown() {
+    mJsonRpcServer.shutdown();
+  }
+
+  public String getSecret() {
+    if (mSecret == null) {
+      return null;
+    }
+    return mSecret.toString();
+  }
+
+  public RpcReceiverManagerFactory getRpcReceiverManagerFactory() {
+    return mFacadeManagerFactory;
+  }
+}
diff --git a/ScriptingLayer/src/com/googlecode/android_scripting/ScriptLauncher.java b/ScriptingLayer/src/com/googlecode/android_scripting/ScriptLauncher.java
new file mode 100644
index 0000000..865f077
--- /dev/null
+++ b/ScriptingLayer/src/com/googlecode/android_scripting/ScriptLauncher.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.app.Service;
+import android.content.Intent;
+
+import com.googlecode.android_scripting.facade.FacadeConfiguration;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.interpreter.InterpreterProcess;
+import com.googlecode.android_scripting.interpreter.html.HtmlActivityTask;
+import com.googlecode.android_scripting.interpreter.html.HtmlInterpreter;
+
+import java.io.File;
+
+public class ScriptLauncher {
+
+  private ScriptLauncher() {
+    // Utility class.
+  }
+
+  public static HtmlActivityTask launchHtmlScript(File script, Service service, Intent intent,
+      InterpreterConfiguration config) {
+    if (!script.exists()) {
+      throw new RuntimeException("No such script to launch.");
+    }
+    HtmlInterpreter interpreter =
+        (HtmlInterpreter) config.getInterpreterByName(HtmlInterpreter.HTML);
+    if (interpreter == null) {
+      throw new RuntimeException("HtmlInterpreter is not available.");
+    }
+    final FacadeManager manager =
+        new FacadeManager(FacadeConfiguration.getSdkLevel(), service, intent,
+            FacadeConfiguration.getFacadeClasses());
+    FutureActivityTaskExecutor executor =
+        ((BaseApplication) service.getApplication()).getTaskExecutor();
+    final HtmlActivityTask task =
+        new HtmlActivityTask(manager, interpreter.getAndroidJsSource(),
+            interpreter.getJsonSource(), script.getAbsolutePath(), true);
+    executor.execute(task);
+    return task;
+  }
+
+  public static InterpreterProcess launchInterpreter(final AndroidProxy proxy, Intent intent,
+      InterpreterConfiguration config, Runnable shutdownHook) {
+    Interpreter interpreter;
+    String interpreterName;
+    interpreterName = intent.getStringExtra(Constants.EXTRA_INTERPRETER_NAME);
+    interpreter = config.getInterpreterByName(interpreterName);
+    InterpreterProcess process = new InterpreterProcess(interpreter, proxy);
+    if (shutdownHook == null) {
+      process.start(new Runnable() {
+        @Override
+        public void run() {
+          proxy.shutdown();
+        }
+      });
+    } else {
+      process.start(shutdownHook);
+    }
+    return process;
+  }
+
+  public static ScriptProcess launchScript(File script, InterpreterConfiguration configuration,
+      final AndroidProxy proxy, Runnable shutdownHook) {
+    if (!script.exists()) {
+      throw new RuntimeException("No such script to launch.");
+    }
+    ScriptProcess process = new ScriptProcess(script, configuration, proxy);
+    if (shutdownHook == null) {
+      process.start(new Runnable() {
+        @Override
+        public void run() {
+          proxy.shutdown();
+        }
+      });
+    } else {
+      process.start(shutdownHook);
+    }
+    return process;
+  }
+}
\ No newline at end of file
diff --git a/ScriptingLayer/src/com/googlecode/android_scripting/ScriptProcess.java b/ScriptingLayer/src/com/googlecode/android_scripting/ScriptProcess.java
new file mode 100644
index 0000000..607bb64
--- /dev/null
+++ b/ScriptingLayer/src/com/googlecode/android_scripting/ScriptProcess.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.interpreter.InterpreterProcess;
+
+import java.io.File;
+
+public class ScriptProcess extends InterpreterProcess {
+
+  private final File mScript;
+
+  public ScriptProcess(File script, InterpreterConfiguration configuration, AndroidProxy proxy) {
+    super(configuration.getInterpreterForScript(script.getName()), proxy);
+    mScript = script;
+    String scriptName = script.getName();
+    setName(scriptName);
+    Interpreter interpreter = configuration.getInterpreterForScript(scriptName);
+    setCommand(String.format(interpreter.getScriptCommand(), script.getAbsolutePath()));
+  }
+
+  public String getPath() {
+    return mScript.getPath();
+  }
+
+}
diff --git a/ScriptingLayer/src/com/googlecode/android_scripting/ScriptStorageAdapter.java b/ScriptingLayer/src/com/googlecode/android_scripting/ScriptStorageAdapter.java
new file mode 100644
index 0000000..96608c1
--- /dev/null
+++ b/ScriptingLayer/src/com/googlecode/android_scripting/ScriptStorageAdapter.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.interpreter.InterpreterConstants;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Manages storage and retrieval of scripts on the file system.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class ScriptStorageAdapter {
+
+  private ScriptStorageAdapter() {
+    // Utility class.
+  }
+
+  /**
+   * Writes data to the script by name and overwrites any existing data.
+   */
+  public static void writeScript(File script, String data) {
+    if (script.getParent() == null) {
+      script = new File(InterpreterConstants.SCRIPTS_ROOT, script.getPath());
+    }
+    try {
+      FileWriter stream = new FileWriter(script, false /* overwrite */);
+      BufferedWriter out = new BufferedWriter(stream);
+      out.write(data);
+      out.close();
+    } catch (IOException e) {
+      Log.e("Failed to write script.", e);
+    }
+  }
+
+  /**
+   * Returns a list of all available script {@link File}s.
+   */
+  public static List<File> listAllScripts(File dir) {
+    if (dir == null) {
+      dir = new File(InterpreterConstants.SCRIPTS_ROOT);
+    }
+    if (dir.exists()) {
+      List<File> scripts = Arrays.asList(dir.listFiles());
+      Collections.sort(scripts, new Comparator<File>() {
+        @Override
+        public int compare(File file1, File file2) {
+          if (file1.isDirectory() && !file2.isDirectory()) {
+            return -1;
+          } else if (!file1.isDirectory() && file2.isDirectory()) {
+            return 1;
+          }
+          return file1.compareTo(file2);
+        }
+      });
+      return scripts;
+    }
+    return new ArrayList<File>();
+  }
+
+  /**
+   * Returns a list of script {@link File}s from the given folder for which there is an interpreter
+   * installed.
+   */
+  public static List<File> listExecutableScripts(File directory, InterpreterConfiguration config) {
+    // NOTE(damonkohler): Creating a LinkedList here is necessary in order to be able to filter it
+    // later.
+    List<File> scripts = new LinkedList<File>(listAllScripts(directory));
+    // Filter out any files that don't have interpreters installed.
+    for (Iterator<File> it = scripts.iterator(); it.hasNext();) {
+      File script = it.next();
+      if (script.isDirectory()) {
+        continue;
+      }
+      Interpreter interpreter = config.getInterpreterForScript(script.getName());
+      if (interpreter == null || !interpreter.isInstalled()) {
+        it.remove();
+      }
+    }
+    return scripts;
+  }
+
+  /**
+   * Returns a list of all (including subfolders) script {@link File}s for which there is an
+   * interpreter installed.
+   */
+  public static List<File> listExecutableScriptsRecursively(File directory,
+      InterpreterConfiguration config) {
+    // NOTE(damonkohler): Creating a LinkedList here is necessary in order to be able to filter it
+    // later.
+    List<File> scripts = new LinkedList<File>();
+    List<File> files = listAllScripts(directory);
+
+    // Filter out any files that don't have interpreters installed.
+    for (Iterator<File> it = files.iterator(); it.hasNext();) {
+      File file = it.next();
+      if (file.isDirectory()) {
+        scripts.addAll(listExecutableScriptsRecursively(file, config));
+      }
+      Interpreter interpreter = config.getInterpreterForScript(file.getName());
+      if (interpreter != null && interpreter.isInstalled()) {
+        scripts.add(file);
+      }
+    }
+    Collections.sort(scripts);
+    return scripts;
+  }
+}
\ No newline at end of file
diff --git a/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java b/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java
new file mode 100644
index 0000000..b297580
--- /dev/null
+++ b/ScriptingLayer/src/com/googlecode/android_scripting/facade/FacadeConfiguration.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.facade;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import com.google.common.collect.Maps;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothA2dpFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothAvrcpFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothConnectionFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothHidFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothHspFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothLeAdvertiseFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothLeScanFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothMapFacade;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothRfcommFacade;
+import com.googlecode.android_scripting.facade.bluetooth.GattClientFacade;
+import com.googlecode.android_scripting.facade.bluetooth.GattServerFacade;
+import com.googlecode.android_scripting.facade.media.AudioManagerFacade;
+import com.googlecode.android_scripting.facade.media.MediaPlayerFacade;
+import com.googlecode.android_scripting.facade.media.MediaRecorderFacade;
+import com.googlecode.android_scripting.facade.media.MediaScannerFacade;
+import com.googlecode.android_scripting.facade.media.MediaSessionFacade;
+import com.googlecode.android_scripting.facade.telephony.CarrierConfigFacade;
+import com.googlecode.android_scripting.facade.telephony.ImsManagerFacade;
+import com.googlecode.android_scripting.facade.telephony.SmsFacade;
+import com.googlecode.android_scripting.facade.telephony.SubscriptionManagerFacade;
+import com.googlecode.android_scripting.facade.telephony.TelecomCallFacade;
+import com.googlecode.android_scripting.facade.telephony.TelecomManagerFacade;
+import com.googlecode.android_scripting.facade.telephony.TelephonyManagerFacade;
+import com.googlecode.android_scripting.facade.ui.UiFacade;
+import com.googlecode.android_scripting.facade.wifi.HttpFacade;
+import com.googlecode.android_scripting.facade.wifi.WifiManagerFacade;
+import com.googlecode.android_scripting.facade.wifi.WifiNanManagerFacade;
+import com.googlecode.android_scripting.facade.wifi.WifiP2pManagerFacade;
+import com.googlecode.android_scripting.facade.wifi.WifiRttManagerFacade;
+import com.googlecode.android_scripting.facade.wifi.WifiScannerFacade;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+import com.googlecode.android_scripting.rpc.RpcDeprecated;
+import com.googlecode.android_scripting.rpc.RpcMinSdk;
+import com.googlecode.android_scripting.rpc.RpcStartEvent;
+import com.googlecode.android_scripting.rpc.RpcStopEvent;
+import com.googlecode.android_scripting.webcam.WebCamFacade;
+
+/**
+ * Encapsulates the list of supported facades and their construction.
+ */
+public class FacadeConfiguration {
+    private final static Set<Class<? extends RpcReceiver>> sFacadeClassList;
+    private final static SortedMap<String, MethodDescriptor> sRpcs =
+            new TreeMap<String, MethodDescriptor>();
+
+    private static int sSdkLevel;
+
+    static {
+        sSdkLevel = android.os.Build.VERSION.SDK_INT;
+
+        sFacadeClassList = new HashSet<Class<? extends RpcReceiver>>();
+        sFacadeClassList.add(ActivityResultFacade.class);
+        sFacadeClassList.add(AndroidFacade.class);
+        sFacadeClassList.add(ApplicationManagerFacade.class);
+        sFacadeClassList.add(AudioManagerFacade.class);
+        sFacadeClassList.add(BatteryManagerFacade.class);
+        sFacadeClassList.add(CameraFacade.class);
+        sFacadeClassList.add(CommonIntentsFacade.class);
+        sFacadeClassList.add(ContactsFacade.class);
+        sFacadeClassList.add(EventFacade.class);
+        sFacadeClassList.add(ImsManagerFacade.class);
+        sFacadeClassList.add(LocationFacade.class);
+        sFacadeClassList.add(TelephonyManagerFacade.class);
+        sFacadeClassList.add(PreferencesFacade.class);
+        sFacadeClassList.add(MediaPlayerFacade.class);
+        sFacadeClassList.add(MediaRecorderFacade.class);
+        sFacadeClassList.add(MediaScannerFacade.class);
+        sFacadeClassList.add(MediaSessionFacade.class);
+        sFacadeClassList.add(SensorManagerFacade.class);
+        sFacadeClassList.add(SettingsFacade.class);
+        sFacadeClassList.add(SmsFacade.class);
+        sFacadeClassList.add(SpeechRecognitionFacade.class);
+        sFacadeClassList.add(ToneGeneratorFacade.class);
+        sFacadeClassList.add(WakeLockFacade.class);
+        sFacadeClassList.add(HttpFacade.class);
+        sFacadeClassList.add(WifiManagerFacade.class);
+        sFacadeClassList.add(UiFacade.class);
+        sFacadeClassList.add(TextToSpeechFacade.class);
+        sFacadeClassList.add(BluetoothFacade.class);
+        sFacadeClassList.add(BluetoothA2dpFacade.class);
+        sFacadeClassList.add(BluetoothAvrcpFacade.class);
+        sFacadeClassList.add(BluetoothConnectionFacade.class);
+        sFacadeClassList.add(BluetoothHspFacade.class);
+        sFacadeClassList.add(BluetoothHidFacade.class);
+        sFacadeClassList.add(BluetoothMapFacade.class);
+        sFacadeClassList.add(BluetoothRfcommFacade.class);
+        sFacadeClassList.add(WebCamFacade.class);
+        sFacadeClassList.add(WifiP2pManagerFacade.class);
+        sFacadeClassList.add(BluetoothLeScanFacade.class);
+        sFacadeClassList.add(BluetoothLeAdvertiseFacade.class);
+        sFacadeClassList.add(GattClientFacade.class);
+        sFacadeClassList.add(GattServerFacade.class);
+        sFacadeClassList.add(ConnectivityManagerFacade.class);
+        sFacadeClassList.add(DisplayFacade.class);
+        sFacadeClassList.add(TelecomManagerFacade.class);
+        sFacadeClassList.add(WifiRttManagerFacade.class);
+        sFacadeClassList.add(WifiScannerFacade.class);
+        sFacadeClassList.add(SubscriptionManagerFacade.class);
+        sFacadeClassList.add(TelecomCallFacade.class);
+        sFacadeClassList.add(CarrierConfigFacade.class);
+
+        /*Compatibility reset to >= Marshmallow */
+        if( sSdkLevel >= 23 ) {
+            //add new facades here
+            sFacadeClassList.add(WifiNanManagerFacade.class);
+        }
+
+        for (Class<? extends RpcReceiver> recieverClass : sFacadeClassList) {
+            for (MethodDescriptor rpcMethod : MethodDescriptor.collectFrom(recieverClass)) {
+                sRpcs.put(rpcMethod.getName(), rpcMethod);
+            }
+        }
+    }
+
+    private FacadeConfiguration() {
+        // Utility class.
+    }
+
+    public static int getSdkLevel() {
+        return sSdkLevel;
+    }
+
+    /** Returns a list of {@link MethodDescriptor} objects for all facades. */
+    public static List<MethodDescriptor> collectMethodDescriptors() {
+        return new ArrayList<MethodDescriptor>(sRpcs.values());
+    }
+
+    /**
+     * Returns a list of not deprecated {@link MethodDescriptor} objects for facades supported by
+     * the current SDK version.
+     */
+    public static List<MethodDescriptor> collectSupportedMethodDescriptors() {
+        List<MethodDescriptor> list = new ArrayList<MethodDescriptor>();
+        for (MethodDescriptor descriptor : sRpcs.values()) {
+            Method method = descriptor.getMethod();
+            if (method.isAnnotationPresent(RpcDeprecated.class)) {
+                continue;
+            } else if (method.isAnnotationPresent(RpcMinSdk.class)) {
+                int requiredSdkLevel = method.getAnnotation(RpcMinSdk.class).value();
+                if (sSdkLevel < requiredSdkLevel) {
+                    continue;
+                }
+            }
+            list.add(descriptor);
+        }
+        return list;
+    }
+
+    public static Map<String, MethodDescriptor> collectStartEventMethodDescriptors() {
+        Map<String, MethodDescriptor> map = Maps.newHashMap();
+        for (MethodDescriptor descriptor : sRpcs.values()) {
+            Method method = descriptor.getMethod();
+            if (method.isAnnotationPresent(RpcStartEvent.class)) {
+                String eventName = method.getAnnotation(RpcStartEvent.class).value();
+                if (map.containsKey(eventName)) {
+                    Log.d("Duplicate eventName " + eventName);
+                    throw new RuntimeException("Duplicate start event method descriptor found.");
+                }
+                map.put(eventName, descriptor);
+            }
+        }
+        return map;
+    }
+
+    public static Map<String, MethodDescriptor> collectStopEventMethodDescriptors() {
+        Map<String, MethodDescriptor> map = Maps.newHashMap();
+        for (MethodDescriptor descriptor : sRpcs.values()) {
+            Method method = descriptor.getMethod();
+            if (method.isAnnotationPresent(RpcStopEvent.class)) {
+                String eventName = method.getAnnotation(RpcStopEvent.class).value();
+                if (map.containsKey(eventName)) {
+                    throw new RuntimeException("Duplicate stop event method descriptor found.");
+                }
+                map.put(eventName, descriptor);
+            }
+        }
+        return map;
+    }
+
+    /** Returns a method by name. */
+    public static MethodDescriptor getMethodDescriptor(String name) {
+        return sRpcs.get(name);
+    }
+
+    public static Collection<Class<? extends RpcReceiver>> getFacadeClasses() {
+        return sFacadeClassList;
+    }
+}
diff --git a/ScriptingLayer/src/com/googlecode/android_scripting/interpreter/InterpreterProcess.java b/ScriptingLayer/src/com/googlecode/android_scripting/interpreter/InterpreterProcess.java
new file mode 100644
index 0000000..52a12c0
--- /dev/null
+++ b/ScriptingLayer/src/com/googlecode/android_scripting/interpreter/InterpreterProcess.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+import com.googlecode.android_scripting.AndroidProxy;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.Process;
+import com.googlecode.android_scripting.SimpleServer;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManagerFactory;
+
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+
+/**
+ * This is a skeletal implementation of an interpreter process.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class InterpreterProcess extends Process {
+
+  private final AndroidProxy mProxy;
+  private final Interpreter mInterpreter;
+  private String mCommand;
+
+  /**
+   * Creates a new {@link InterpreterProcess}.
+   *
+   * @param launchScript
+   *          the absolute path to a script that should be launched with the interpreter
+   * @param port
+   *          the port that the AndroidProxy is listening on
+   */
+  public InterpreterProcess(Interpreter interpreter, AndroidProxy proxy) {
+    mProxy = proxy;
+    mInterpreter = interpreter;
+
+    setBinary(interpreter.getBinary());
+    setName(interpreter.getNiceName());
+    setCommand(interpreter.getInteractiveCommand());
+    addAllArguments(interpreter.getArguments());
+    putAllEnvironmentVariables(System.getenv());
+    putEnvironmentVariable("AP_HOST", getHost());
+    putEnvironmentVariable("AP_PORT", Integer.toString(getPort()));
+    if (proxy.getSecret() != null) {
+      putEnvironmentVariable("AP_HANDSHAKE", getSecret());
+    }
+    putAllEnvironmentVariables(interpreter.getEnvironmentVariables());
+  }
+
+  protected void setCommand(String command) {
+    mCommand = command;
+  }
+
+  public Interpreter getInterpreter() {
+    return mInterpreter;
+  }
+
+  public String getHost() {
+    String result = mProxy.getAddress().getHostName();
+    if (result.equals("0.0.0.0")) { // Wildcard.
+      try {
+        return SimpleServer.getPublicInetAddress().getHostName();
+      } catch (UnknownHostException e) {
+        Log.i("public address", e);
+        e.printStackTrace();
+      } catch (SocketException e) {
+        Log.i("public address", e);
+      }
+    }
+    return result;
+  }
+
+  public int getPort() {
+    return mProxy.getAddress().getPort();
+  }
+
+  public InetSocketAddress getAddress() {
+    return mProxy.getAddress();
+  }
+
+  public String getSecret() {
+    return mProxy.getSecret();
+  }
+
+  public RpcReceiverManagerFactory getRpcReceiverManagerFactory() {
+    return mProxy.getRpcReceiverManagerFactory();
+  }
+
+  @Override
+  public void start(final Runnable shutdownHook) {
+    if (!mCommand.isEmpty()) {
+      addArgument(mCommand);
+    }
+    super.start(shutdownHook);
+  }
+
+  @Override
+  public void kill() {
+    super.kill();
+    mProxy.shutdown();
+  }
+
+  @Override
+  public String getWorkingDirectory() {
+    return InterpreterConstants.SDCARD_SL4A_ROOT;
+  }
+}
diff --git a/ScriptingLayer/src/com/googlecode/android_scripting/trigger/EventGenerationControllingObserver.java b/ScriptingLayer/src/com/googlecode/android_scripting/trigger/EventGenerationControllingObserver.java
new file mode 100644
index 0000000..0c38a0f
--- /dev/null
+++ b/ScriptingLayer/src/com/googlecode/android_scripting/trigger/EventGenerationControllingObserver.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.trigger;
+
+import com.google.common.collect.Maps;
+import com.googlecode.android_scripting.facade.FacadeConfiguration;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+import com.googlecode.android_scripting.trigger.TriggerRepository.TriggerRepositoryObserver;
+
+import java.util.Map;
+
+import org.json.JSONArray;
+
+/**
+ * A {@link TriggerRepositoryObserver} that starts and stops the monitoring of events depending on
+ * whether or not triggers for the event exist.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ */
+public class EventGenerationControllingObserver implements TriggerRepositoryObserver {
+  private final FacadeManager mFacadeManager;
+  private final Map<String, MethodDescriptor> mStartEventGeneratingMethodDescriptors;
+  private final Map<String, MethodDescriptor> mStopEventGeneratingMethodDescriptors;
+  private final Map<String, Integer> mEventTriggerRefCounts = Maps.newHashMap();
+
+  /**
+   * Creates a new StartEventMonitoringObserver for the given trigger repository.
+   *
+   * @param facadeManager
+   * @param triggerRepository
+   */
+  public EventGenerationControllingObserver(FacadeManager facadeManager) {
+    mFacadeManager = facadeManager;
+    mStartEventGeneratingMethodDescriptors =
+        FacadeConfiguration.collectStartEventMethodDescriptors();
+    mStopEventGeneratingMethodDescriptors = FacadeConfiguration.collectStopEventMethodDescriptors();
+  }
+
+  private synchronized int incrementAndGetRefCount(String eventName) {
+    int refCount =
+        (mEventTriggerRefCounts.containsKey(eventName)) ? mEventTriggerRefCounts.get(eventName) : 0;
+    refCount++;
+    mEventTriggerRefCounts.put(eventName, refCount);
+    return refCount;
+  }
+
+  private synchronized int decrementAndGetRefCount(String eventName) {
+    int refCount =
+        (mEventTriggerRefCounts.containsKey(eventName)) ? mEventTriggerRefCounts.get(eventName) : 0;
+    refCount--;
+    mEventTriggerRefCounts.put(eventName, refCount);
+    return refCount;
+  }
+
+  @Override
+  public synchronized void onPut(Trigger trigger) {
+    // If we're not already monitoring the events corresponding to this trigger, do so.
+    if (incrementAndGetRefCount(trigger.getEventName()) == 1) {
+      startMonitoring(trigger.getEventName());
+    }
+  }
+
+  @Override
+  public synchronized void onRemove(Trigger trigger) {
+    // If there are no more triggers listening to this event, then we need to stop monitoring.
+    if (decrementAndGetRefCount(trigger.getEventName()) == 1) {
+      stopMonitoring(trigger.getEventName());
+    }
+  }
+
+  private void startMonitoring(String eventName) {
+    MethodDescriptor startEventGeneratingMethod =
+        mStartEventGeneratingMethodDescriptors.get(eventName);
+    try {
+      startEventGeneratingMethod.invoke(mFacadeManager, new JSONArray());
+    } catch (Throwable t) {
+      throw new RuntimeException(t);
+    }
+  }
+
+  private void stopMonitoring(String eventName) {
+    MethodDescriptor stopEventGeneratingMethod =
+        mStopEventGeneratingMethodDescriptors.get(eventName);
+    try {
+      stopEventGeneratingMethod.invoke(mFacadeManager, new JSONArray());
+    } catch (Throwable t) {
+      throw new RuntimeException(t);
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/Android.mk b/ScriptingLayerForAndroid/Android.mk
new file mode 100644
index 0000000..79f66c3
--- /dev/null
+++ b/ScriptingLayerForAndroid/Android.mk
@@ -0,0 +1,59 @@
+#
+#  Copyright (C) 2016 Google, Inc.
+#
+#  Licensed under the Apache License, Version 2.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.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.
+#
+
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PACKAGE_NAME := sl4a
+LOCAL_MODULE_OWNER := google
+LOCAL_DEX_PREOPT := false
+
+LOCAL_CERTIFICATE := platform
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_AAPT_FLAGS := --auto-add-overlay
+
+LOCAL_MULTILIB := both
+
+# Builds on the Data Partition
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := guava android-common locale_platform android-support-v4
+LOCAL_STATIC_JAVA_LIBRARIES += sl4a.Utils sl4a.Common
+LOCAL_STATIC_JAVA_LIBRARIES += sl4a.InterpreterForAndroid sl4a.ScriptingLayer
+
+LOCAL_PRIVILEGED_MODULE := true
+LOCAL_PROGUARD_ENABLED := disabled
+
+
+LOCAL_JNI_SHARED_LIBRARIES := libcom_googlecode_android_scripting_Exec
+
+include $(PREBUILT_SHARED_LIBRARY)
+
+include $(BUILD_PACKAGE)
+
+include $(CLEAR_VARS)
+LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := locale_platform:libs/locale_platform.jar
+include $(BUILD_MULTI_PREBUILT)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
+
diff --git a/ScriptingLayerForAndroid/AndroidManifest.xml b/ScriptingLayerForAndroid/AndroidManifest.xml
new file mode 100644
index 0000000..cc9f2b8
--- /dev/null
+++ b/ScriptingLayerForAndroid/AndroidManifest.xml
@@ -0,0 +1,220 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+** Copyright 2016, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.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.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.
+*/
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.googlecode.android_scripting"
+    android:sharedUserId="android.uid.system">
+
+    <uses-permission android:name="com.android.permission.WHITELIST_BLUETOOTH_DEVICE"
+        android:protectionLevel="signature" />
+    <uses-permission android:name="android.permission.ACCESS_BLUETOOTH_SHARE"
+        android:protectionLevel="signature" />
+    <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.BIND_INCALL_SERVICE" />
+    <uses-permission android:name="android.permission.CALL_PHONE" />
+    <uses-permission android:name="android.permission.CALL_PRIVILEGED" />
+    <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
+    <uses-permission android:name="android.permission.CONNECTIVITY_INTERNAL" />
+    <uses-permission android:name="android.permission.LOCATION_HARDWARE" />
+    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.RECEIVE_SMS" />
+    <uses-permission android:name="android.permission.SEND_SMS" />
+    <uses-permission android:name="android.permission.READ_SMS" />
+    <uses-permission android:name="android.permission.WRITE_SMS" />
+    <uses-permission android:name="android.permission.RECEIVE_MMS" />
+    <uses-permission android:name="android.permission.BROADCAST_WAP_PUSH" />
+    <uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" />
+    <uses-permission android:name="android.permission.WRITE_APN_SETTINGS" />
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
+    <uses-permission android:name="android.permission.PERSISTENT_ACTIVITY" />
+    <uses-permission android:name="android.permission.RESTART_PACKAGES" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.READ_LOGS" />
+    <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
+    <uses-permission android:name="android.permission.BLUETOOTH_MAP" />
+    <uses-permission android:name="android.permission.BLUETOOTH_STACK" />
+    <uses-permission android:name="android.permission.NET_ADMIN" />
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.DEVICE_POWER" />
+    <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
+    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
+    <uses-permission android:name="android.permission.NFC" />
+    <uses-permission android:name="android.permission.HARDWARE_TEST" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.MASTER_CLEAR" />
+    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
+    <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.ACCESS_WIMAX_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIMAX_STATE" />
+    <uses-permission android:name="com.android.certinstaller.INSTALL_AS_USER" />
+    <uses-permission android:name="android.permission.CLEAR_APP_USER_DATA" />
+    <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_CHECKIN_PROPERTIES" />
+    <uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
+    <uses-permission android:name="android.permission.WRITE_USER_DICTIONARY" />
+    <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES" />
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+    <uses-permission android:name="android.permission.BATTERY_STATS" />
+    <uses-permission android:name="android.permission.MOVE_PACKAGE" />
+    <uses-permission android:name="android.permission.BACKUP" />
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.READ_SYNC_STATS" />
+    <uses-permission android:name="android.permission.STATUS_BAR" />
+    <uses-permission android:name="android.permission.MANAGE_USB" />
+    <uses-permission android:name="android.permission.SET_POINTER_SPEED" />
+    <uses-permission android:name="android.permission.SET_KEYBOARD_LAYOUT" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+    <uses-permission android:name="android.permission.COPY_PROTECTED_DATA" />
+    <uses-permission android:name="android.permission.MANAGE_USERS" />
+    <uses-permission android:name="android.permission.READ_PROFILE" />
+    <uses-permission android:name="android.permission.CONFIGURE_WIFI_DISPLAY" />
+    <uses-permission android:name="android.permission.SET_TIME" />
+    <uses-permission android:name="android.permission.SET_TIME_ZONE" />
+    <uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS" />
+    <uses-permission android:name="android.permission.REBOOT" />
+    <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.READ_PRECISE_PHONE_STATE" />
+    <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
+    <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
+    <uses-permission android:name="android.permission.RECEIVE_EMERGENCY_BROADCAST"/>
+    <application android:icon="@drawable/sl4a_logo_48" android:label="@string/application_title" android:name=".Sl4aApplication"
+        android:theme="@android:style/Theme.DeviceDefault">
+        <activity android:name=".activity.ScriptManager" android:configChanges="keyboardHidden|orientation" android:windowSoftInputMode="adjustResize" android:launchMode="singleTop">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.SEARCH" />
+            </intent-filter>
+            <meta-data android:name="android.app.searchable" android:resource="@xml/searchable_scripts" />
+        </activity>
+        <activity android:name=".activity.ScriptPicker" android:configChanges="keyboardHidden|orientation" android:label="Scripts">
+            <intent-filter>
+                <action android:name="android.intent.action.CREATE_SHORTCUT" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.PICK" />
+                <data android:scheme="content" android:path="sl4a/scripts" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity android:name=".activity.InterpreterPicker" android:configChanges="keyboardHidden|orientation" android:label="Interpreters">
+            <intent-filter>
+                <action android:name="android.intent.action.CREATE_SHORTCUT" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity-alias android:name="LocalePlugin" android:targetActivity=".activity.ScriptPicker" android:label="@string/application_title" android:icon="@drawable/sl4a_logo_32">
+            <intent-filter>
+                <action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
+            </intent-filter>
+        </activity-alias>
+        <receiver android:name=".locale.LocaleReceiver">
+            <intent-filter>
+                <action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
+            </intent-filter>
+        </receiver>
+        <activity android:name=".activity.Preferences" android:theme="@android:style/Theme.DeviceDefault.Settings" />
+        <activity android:name="org.connectbot.ConsoleActivity" android:theme="@android:style/Theme.DeviceDefault.NoActionBar" android:configChanges="keyboardHidden|orientation" android:windowSoftInputMode="stateAlwaysVisible|adjustResize" android:finishOnTaskLaunch="true" android:launchMode="singleTask" />
+        <activity android:name=".activity.ScriptEditor" android:theme="@android:style/Theme.DeviceDefault.NoActionBar" android:configChanges="keyboardHidden|orientation" android:windowSoftInputMode="stateAlwaysVisible|adjustResize">
+            <intent-filter>
+                <action android:name="com.googlecode.android_scripting.action.EDIT_SCRIPT" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity android:name=".activity.ApiBrowser" android:configChanges="keyboardHidden|orientation" android:launchMode="singleTop" android:windowSoftInputMode="adjustResize">
+            <intent-filter>
+                <action android:name="android.intent.action.SEARCH" />
+            </intent-filter>
+            <meta-data android:name="android.app.searchable" android:resource="@xml/searchable_apis" />
+        </activity>
+        <activity android:name=".activity.ApiPrompt" android:theme="@android:style/Theme.DeviceDefault.NoActionBar" android:configChanges="keyboardHidden|orientation" />
+        <activity android:name=".activity.TriggerManager" android:launchMode="singleTask" android:configChanges="keyboardHidden|orientation" />
+        <activity android:name=".activity.BluetoothDeviceList" android:configChanges="keyboardHidden|orientation" />
+        <activity android:name=".activity.ScriptingLayerServiceLauncher" android:taskAffinity="" android:theme="@android:style/Theme.DeviceDefault.NoActionBar.TranslucentDecor">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity android:name=".activity.FutureActivity" android:configChanges="keyboardHidden|orientation" android:theme="@android:style/Theme.DeviceDefault.NoActionBar.TranslucentDecor" />
+        <activity android:name="org.connectbot.HelpActivity" android:configChanges="keyboardHidden|orientation" />
+        <activity android:name="org.connectbot.HelpTopicActivity" android:configChanges="keyboardHidden|orientation" />
+        <service android:name=".activity.ScriptingLayerService" />
+        <service android:name=".activity.TriggerService" />
+        <service android:name="com.googlecode.android_scripting.facade.telephony.InCallServiceImpl"
+                 android:permission="android.permission.BIND_INCALL_SERVICE" >
+            <intent-filter>
+                <action android:name="android.telecom.InCallService"/>
+            </intent-filter>
+        </service>
+        <service android:name=".service.FacadeService" android:enabled="true" android:exported="true" >
+            <intent-filter>
+                <action android:name="com.googlecode.android_scripting.service.FacadeService.ACTION_BIND" />
+            </intent-filter>
+        </service>
+        <activity android:name=".activity.InterpreterManager" android:launchMode="singleTask" android:configChanges="keyboardHidden|orientation" />
+        <activity android:name=".activity.LogcatViewer" android:launchMode="singleTask" android:configChanges="keyboardHidden|orientation" />
+        <activity android:name=".activity.ScriptsLiveFolder" android:label="Scripts" android:icon="@drawable/live_folder" android:configChanges="keyboardHidden|orientation">
+            <intent-filter>
+                <action android:name="android.intent.action.CREATE_LIVE_FOLDER" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <provider android:name=".provider.ScriptProvider" android:authorities="com.googlecode.android_scripting.provider.scriptprovider" />
+        <provider android:name=".provider.ApiProvider" android:authorities="com.googlecode.android_scripting.provider.apiprovider" />
+        <provider
+            android:name="android.support.v4.content.FileProvider"
+            android:authorities="com.googlecode.android_scripting.provider.telephonytestprovider"
+            android:grantUriPermissions="true"
+            android:exported="false">
+            <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/paths" />
+        </provider>
+        <uses-library android:name="android.test.runner" />
+        <activity android:name=".activity.ScriptProcessMonitor" android:launchMode="singleTask" android:finishOnTaskLaunch="true" />
+        <activity android:configChanges="keyboardHidden|orientation" android:name="org.connectbot.util.ColorsActivity" android:theme="@android:style/Theme.DeviceDefault.Dialog">
+            <intent-filter>
+                <action android:name="com.googlecode.android_scripting.PICK_TERMINAL_COLORS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/ScriptingLayerForAndroid/jni/Android.mk b/ScriptingLayerForAndroid/jni/Android.mk
new file mode 100644
index 0000000..541aafb
--- /dev/null
+++ b/ScriptingLayerForAndroid/jni/Android.mk
@@ -0,0 +1,37 @@
+#
+#  Copyright (C) 2016 Google, Inc.
+#
+#  Licensed under the Apache License, Version 2.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.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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := com_googlecode_android_scripting_Exec.cpp
+
+LOCAL_CFLAGS += -Wno-unused-parameter
+
+LOCAL_C_INCLUDES += \
+    $(JNI_H_INCLUDE) \
+
+LOCAL_SHARED_LIBRARIES := \
+    libandroid_runtime \
+    libnativehelper \
+    libcutils \
+    libutils \
+    liblog
+
+LOCAL_MODULE    := libcom_googlecode_android_scripting_Exec
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/ScriptingLayerForAndroid/jni/com_googlecode_android_scripting_Exec.cpp b/ScriptingLayerForAndroid/jni/com_googlecode_android_scripting_Exec.cpp
new file mode 100644
index 0000000..495f27a
--- /dev/null
+++ b/ScriptingLayerForAndroid/jni/com_googlecode_android_scripting_Exec.cpp
@@ -0,0 +1,202 @@
+//
+//  Copyright (C) 2016 Google, Inc.
+//
+//  Licensed under the Apache License, Version 2.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.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.
+//
+
+#include "com_googlecode_android_scripting_Exec.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <termios.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+ 
+#include "android/log.h"
+
+#define LOG_TAG "Exec"
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+
+int CreateSubprocess(const char* cmd, char* args[], char* vars[], char *wkdir, pid_t* pid) {
+  char* devname;
+  int ptm = open("/dev/ptmx", O_RDWR);
+  if(ptm < 0){
+    LOGE("Cannot open /dev/ptmx: %s\n", strerror(errno));
+    return -1;
+  }
+  fcntl(ptm, F_SETFD, FD_CLOEXEC);
+
+  if (grantpt(ptm) || unlockpt(ptm) ||
+      ((devname = (char*) ptsname(ptm)) == 0)) {
+    LOGE("Trouble with /dev/ptmx: %s\n", strerror(errno));
+    return -1;
+  }
+
+  *pid = fork();
+  if(*pid < 0) {
+    LOGE("Fork failed: %s\n", strerror(errno));
+    return -1;
+  }
+
+  if(*pid == 0){
+    int pts;
+    setsid();
+    pts = open(devname, O_RDWR);
+    if(pts < 0) {
+      exit(-1);
+    }
+    dup2(pts, 0);
+    dup2(pts, 1);
+    dup2(pts, 2);
+    close(ptm);
+    if (wkdir) chdir(wkdir);
+    execve(cmd, args, vars);
+    exit(-1);
+  } else {
+    return ptm;
+  }
+}
+
+void JNU_ThrowByName(JNIEnv* env, const char* name, const char* msg) {
+  jclass clazz = env->FindClass(name);
+  if (clazz != NULL) {
+    env->ThrowNew(clazz, msg);
+  }
+  env->DeleteLocalRef(clazz);
+}
+
+char* JNU_GetStringNativeChars(JNIEnv* env, jstring jstr) {
+  if (jstr == NULL) {
+    return NULL;
+  }
+  jbyteArray bytes = 0;
+  jthrowable exc;
+  char* result = 0;
+  if (env->EnsureLocalCapacity(2) < 0) {
+    return 0; /* out of memory error */
+  }
+  jclass Class_java_lang_String = env->FindClass("java/lang/String");
+  jmethodID MID_String_getBytes = env->GetMethodID(
+      Class_java_lang_String, "getBytes", "()[B");
+  bytes = (jbyteArray) env->CallObjectMethod(jstr, MID_String_getBytes);
+  exc = env->ExceptionOccurred();
+  if (!exc) {
+    jint len = env->GetArrayLength(bytes);
+    result = (char*) malloc(len + 1);
+    if (result == 0) {
+      JNU_ThrowByName(env, "java/lang/OutOfMemoryError", 0);
+      env->DeleteLocalRef(bytes);
+      return 0;
+    }
+    env->GetByteArrayRegion(bytes, 0, len, (jbyte*) result);
+    result[len] = 0; /* NULL-terminate */
+  } else {
+    env->DeleteLocalRef(exc);
+  }
+  env->DeleteLocalRef(bytes);
+  return result;
+}
+
+int JNU_GetFdFromFileDescriptor(JNIEnv* env, jobject fileDescriptor) {
+  jclass Class_java_io_FileDescriptor = env->FindClass("java/io/FileDescriptor");
+  jfieldID descriptor = env->GetFieldID(Class_java_io_FileDescriptor, "descriptor", "I");
+  return env->GetIntField(fileDescriptor, descriptor);
+}
+
+JNIEXPORT jobject JNICALL Java_com_googlecode_android_1scripting_Exec_createSubprocess(
+    JNIEnv* env, jclass clazz, jstring cmd, jobjectArray argArray, jobjectArray varArray,
+    jstring workingDirectory,
+    jintArray processIdArray) {
+  char* cmd_native = JNU_GetStringNativeChars(env, cmd);
+  char* wkdir_native = JNU_GetStringNativeChars(env, workingDirectory);
+  pid_t pid;
+  jsize len = 0;
+  if (argArray) {
+    len = env->GetArrayLength(argArray);
+  }
+  char* args[len + 2];
+  args[0] = cmd_native;
+  for (int i = 0; i < len; i++) {
+    jstring arg = (jstring) env->GetObjectArrayElement(argArray, i);
+    char* arg_native = JNU_GetStringNativeChars(env, arg);
+    args[i + 1] = arg_native;
+  }
+  args[len + 1] = NULL;
+
+  len = 0;
+  if (varArray) {
+    len = env->GetArrayLength(varArray);
+  }
+  char* vars[len + 1];
+  for (int i = 0; i < len; i++) {
+    jstring var = (jstring) env->GetObjectArrayElement(varArray, i);
+    char* var_native = JNU_GetStringNativeChars(env, var);
+    vars[i] = var_native;
+  }
+  vars[len] = NULL;
+
+  int ptm = CreateSubprocess(cmd_native, args, vars, wkdir_native, &pid);
+  if (processIdArray) {
+    if (env->GetArrayLength(processIdArray) > 0) {
+      jboolean isCopy;
+      int* proccessId = (int*) env->GetPrimitiveArrayCritical(processIdArray, &isCopy);
+      if (proccessId) {
+        *proccessId = (int) pid;
+        env->ReleasePrimitiveArrayCritical(processIdArray, proccessId, 0);
+      }
+    }
+  }
+
+  jclass Class_java_io_FileDescriptor =
+      env->FindClass("java/io/FileDescriptor");
+  jmethodID init = env->GetMethodID(Class_java_io_FileDescriptor, "<init>", "()V");
+  jobject result = env->NewObject(Class_java_io_FileDescriptor, init);
+
+  if (!result) {
+    LOGE("Couldn't create a FileDescriptor.");
+  } else {
+    jfieldID descriptor = env->GetFieldID(Class_java_io_FileDescriptor, "descriptor", "I");
+    env->SetIntField(result, descriptor, ptm);
+  }
+  return result;
+}
+
+JNIEXPORT void JNICALL Java_com_googlecode_android_1scripting_Exec_setPtyWindowSize(
+    JNIEnv* env, jclass clazz, jobject fileDescriptor, jint row, jint col, jint xpixel,
+    jint ypixel) {
+  struct winsize sz;
+  int fd = JNU_GetFdFromFileDescriptor(env, fileDescriptor);
+  if (env->ExceptionOccurred() != NULL) {
+    return;
+  }
+  sz.ws_row = row;
+  sz.ws_col = col;
+  sz.ws_xpixel = xpixel;
+  sz.ws_ypixel = ypixel;
+  ioctl(fd, TIOCSWINSZ, &sz);
+}
+
+JNIEXPORT jint JNICALL Java_com_googlecode_android_1scripting_Exec_waitFor(JNIEnv* env, jclass clazz, jint procId) {
+  int status;
+  waitpid(procId, &status, 0);
+  int result = 0;
+  if (WIFEXITED(status)) {
+    result = WEXITSTATUS(status);
+  }
+  return result;
+}
diff --git a/ScriptingLayerForAndroid/jni/com_googlecode_android_scripting_Exec.h b/ScriptingLayerForAndroid/jni/com_googlecode_android_scripting_Exec.h
new file mode 100644
index 0000000..6f6d422
--- /dev/null
+++ b/ScriptingLayerForAndroid/jni/com_googlecode_android_scripting_Exec.h
@@ -0,0 +1,37 @@
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include <jni.h>
+/* Header for class com_googlecode_android_scripting_Exec */
+
+#ifndef _Included_com_googlecode_android_scripting_Exec
+#define _Included_com_googlecode_android_scripting_Exec
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     com_googlecode_android_scripting_Exec
+ * Method:    createSubprocess
+ * Signature: (Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[I)Ljava/io/FileDescriptor;
+ */
+JNIEXPORT jobject JNICALL Java_com_googlecode_android_1scripting_Exec_createSubprocess
+  (JNIEnv *, jclass, jstring, jobjectArray, jobjectArray, jstring, jintArray);
+
+/*
+ * Class:     com_googlecode_android_scripting_Exec
+ * Method:    setPtyWindowSize
+ * Signature: (Ljava/io/FileDescriptor;IIII)V
+ */
+JNIEXPORT void JNICALL Java_com_googlecode_android_1scripting_Exec_setPtyWindowSize
+  (JNIEnv *, jclass, jobject, jint, jint, jint, jint);
+
+/*
+ * Class:     com_googlecode_android_scripting_Exec
+ * Method:    waitFor
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL Java_com_googlecode_android_1scripting_Exec_waitFor
+  (JNIEnv *, jclass, jint);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/ScriptingLayerForAndroid/libs/locale_platform.jar b/ScriptingLayerForAndroid/libs/locale_platform.jar
new file mode 100644
index 0000000..4697825
--- /dev/null
+++ b/ScriptingLayerForAndroid/libs/locale_platform.jar
Binary files differ
diff --git a/ScriptingLayerForAndroid/proguard.flags b/ScriptingLayerForAndroid/proguard.flags
new file mode 100644
index 0000000..93a2728
--- /dev/null
+++ b/ScriptingLayerForAndroid/proguard.flags
@@ -0,0 +1,5 @@
+
+-libraryjars libs
+
+-keep class net.londatiga.android.**{*;}
+
diff --git a/ScriptingLayerForAndroid/res/anim/fade_out_delayed.xml b/ScriptingLayerForAndroid/res/anim/fade_out_delayed.xml
new file mode 100644
index 0000000..20ca839
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/anim/fade_out_delayed.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+	android:fromAlpha="1.0"
+	android:toAlpha="0.0"
+	android:duration="500"
+	android:startOffset="1000"
+	android:fillAfter="true"
+	/>
diff --git a/ScriptingLayerForAndroid/res/anim/fade_stay_hidden.xml b/ScriptingLayerForAndroid/res/anim/fade_stay_hidden.xml
new file mode 100644
index 0000000..e62ca8b
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/anim/fade_stay_hidden.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+	android:fromAlpha="0.0"
+	android:toAlpha="0.0"
+	android:duration="500"
+	android:fillAfter="true"
+	/>
diff --git a/ScriptingLayerForAndroid/res/anim/keyboard_fade_in.xml b/ScriptingLayerForAndroid/res/anim/keyboard_fade_in.xml
new file mode 100644
index 0000000..edd5b94
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/anim/keyboard_fade_in.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+	android:interpolator="@android:anim/accelerate_interpolator"
+	android:fromAlpha="0.0"
+	android:toAlpha="1.0"
+	android:duration="100" />
diff --git a/ScriptingLayerForAndroid/res/anim/keyboard_fade_out.xml b/ScriptingLayerForAndroid/res/anim/keyboard_fade_out.xml
new file mode 100644
index 0000000..1f37d32
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/anim/keyboard_fade_out.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+	android:interpolator="@android:anim/accelerate_interpolator"
+	android:fromAlpha="1.0"
+	android:toAlpha="0.0"
+	android:duration="100" />
diff --git a/ScriptingLayerForAndroid/res/anim/slide_left_in.xml b/ScriptingLayerForAndroid/res/anim/slide_left_in.xml
new file mode 100644
index 0000000..29a0048
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/anim/slide_left_in.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+	<translate android:fromXDelta="100%p" android:toXDelta="0" android:duration="300"/>
+	<!--  <alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="300" />  -->
+</set>
diff --git a/ScriptingLayerForAndroid/res/anim/slide_left_out.xml b/ScriptingLayerForAndroid/res/anim/slide_left_out.xml
new file mode 100644
index 0000000..9c46442
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/anim/slide_left_out.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+	<translate android:fromXDelta="0" android:toXDelta="-100%p" android:duration="300"/>
+	<!--  <alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="300" />  -->
+</set>
diff --git a/ScriptingLayerForAndroid/res/anim/slide_right_in.xml b/ScriptingLayerForAndroid/res/anim/slide_right_in.xml
new file mode 100644
index 0000000..0d52c9f
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/anim/slide_right_in.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+	<translate android:fromXDelta="-100%p" android:toXDelta="0" android:duration="300"/>
+	<!--  <alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="300" />  -->
+</set>
diff --git a/ScriptingLayerForAndroid/res/anim/slide_right_out.xml b/ScriptingLayerForAndroid/res/anim/slide_right_out.xml
new file mode 100644
index 0000000..ace4e9d
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/anim/slide_right_out.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+	<translate android:fromXDelta="0" android:toXDelta="100%p" android:duration="300"/>
+	<!--  <alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="300" />  -->
+</set>
diff --git a/ScriptingLayerForAndroid/res/drawable/atari_small.png b/ScriptingLayerForAndroid/res/drawable/atari_small.png
new file mode 100644
index 0000000..535e295
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/atari_small.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/atari_small_notice.txt b/ScriptingLayerForAndroid/res/drawable/atari_small_notice.txt
new file mode 100644
index 0000000..afa8539
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/atari_small_notice.txt
@@ -0,0 +1,11 @@
+COMMENT  Copyright (c) 1999, Thomas A. Fine
+COMMENT
+COMMENT  License to copy, modify, and distribute for both commercial and
+COMMENT  non-commercial use is herby granted, provided this notice
+COMMENT  is preserved.
+COMMENT
+COMMENT  Email to my last name at head.cfa.harvard.edu
+COMMENT  http://hea-www.harvard.edu/~fine/
+COMMENT
+COMMENT  Produced with bdfedit, a tcl/tk font editing program
+COMMENT  written by Thomas A. Fine
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/drawable/background.png b/ScriptingLayerForAndroid/res/drawable/background.png
new file mode 100644
index 0000000..d0fc14f
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/background.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/bsh_icon.png b/ScriptingLayerForAndroid/res/drawable/bsh_icon.png
new file mode 100644
index 0000000..1d73eb5
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/bsh_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/file_bg.png b/ScriptingLayerForAndroid/res/drawable/file_bg.png
new file mode 100644
index 0000000..949613c
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/file_bg.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/folder.png b/ScriptingLayerForAndroid/res/drawable/folder.png
new file mode 100644
index 0000000..6333914
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/folder.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/html_icon.png b/ScriptingLayerForAndroid/res/drawable/html_icon.png
new file mode 100644
index 0000000..c86bee0
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/html_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/ic_dialog_time.png b/ScriptingLayerForAndroid/res/drawable/ic_dialog_time.png
new file mode 100644
index 0000000..3ce2751
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/ic_dialog_time.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/ic_menu_close_clear_cancel.png b/ScriptingLayerForAndroid/res/drawable/ic_menu_close_clear_cancel.png
new file mode 100644
index 0000000..619858c
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/ic_menu_close_clear_cancel.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/ic_menu_refresh.png b/ScriptingLayerForAndroid/res/drawable/ic_menu_refresh.png
new file mode 100644
index 0000000..77d70dd
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/ic_menu_refresh.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/ic_menu_search.png b/ScriptingLayerForAndroid/res/drawable/ic_menu_search.png
new file mode 100644
index 0000000..94446db
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/ic_menu_search.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/js_icon.png b/ScriptingLayerForAndroid/res/drawable/js_icon.png
new file mode 100644
index 0000000..86d2504
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/js_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/keyboard_icon.png b/ScriptingLayerForAndroid/res/drawable/keyboard_icon.png
new file mode 100644
index 0000000..9205d8b
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/keyboard_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/live_folder.png b/ScriptingLayerForAndroid/res/drawable/live_folder.png
new file mode 100644
index 0000000..3dac86a
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/live_folder.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/lua_icon.png b/ScriptingLayerForAndroid/res/drawable/lua_icon.png
new file mode 100644
index 0000000..0a7a2f3
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/lua_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/nut_icon.png b/ScriptingLayerForAndroid/res/drawable/nut_icon.png
new file mode 100644
index 0000000..06ef267
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/nut_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/pl_icon.png b/ScriptingLayerForAndroid/res/drawable/pl_icon.png
new file mode 100644
index 0000000..7265f46
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/pl_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/py_icon.png b/ScriptingLayerForAndroid/res/drawable/py_icon.png
new file mode 100644
index 0000000..0dcbc64
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/py_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/rb_icon.png b/ScriptingLayerForAndroid/res/drawable/rb_icon.png
new file mode 100644
index 0000000..8c33c29
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/rb_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/script_logo_48.png b/ScriptingLayerForAndroid/res/drawable/script_logo_48.png
new file mode 100644
index 0000000..488ed82
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/script_logo_48.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sh_icon.png b/ScriptingLayerForAndroid/res/drawable/sh_icon.png
new file mode 100644
index 0000000..6469c34
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sh_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_32.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_32.png
new file mode 100644
index 0000000..8fb9e6d
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_32.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48.png
new file mode 100644
index 0000000..488ed82
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_10.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_10.png
new file mode 100644
index 0000000..feac584
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_10.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_2.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_2.png
new file mode 100644
index 0000000..400d3b4
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_2.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_3.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_3.png
new file mode 100644
index 0000000..f666771
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_3.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_4.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_4.png
new file mode 100644
index 0000000..9021173
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_4.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_5.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_5.png
new file mode 100644
index 0000000..c40a9ae
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_5.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_6.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_6.png
new file mode 100644
index 0000000..a428da4
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_6.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_7.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_7.png
new file mode 100644
index 0000000..b515c3a
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_7.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_8.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_8.png
new file mode 100644
index 0000000..6474542
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_8.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_9.png b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_9.png
new file mode 100644
index 0000000..6c6ffd5
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_logo_48_9.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/sl4a_notification_logo.xml b/ScriptingLayerForAndroid/res/drawable/sl4a_notification_logo.xml
new file mode 100644
index 0000000..cea5465
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl4a_notification_logo.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<level-list xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:maxLevel="1" android:drawable="@drawable/sl4a_logo_48" />
+  <item android:maxLevel="2" android:drawable="@drawable/sl4a_logo_48_2" />
+  <item android:maxLevel="3" android:drawable="@drawable/sl4a_logo_48_3" />
+  <item android:maxLevel="4" android:drawable="@drawable/sl4a_logo_48_4" />
+  <item android:maxLevel="5" android:drawable="@drawable/sl4a_logo_48_5" />
+  <item android:maxLevel="6" android:drawable="@drawable/sl4a_logo_48_6" />
+  <item android:maxLevel="7" android:drawable="@drawable/sl4a_logo_48_7" />
+  <item android:maxLevel="8" android:drawable="@drawable/sl4a_logo_48_8" />
+  <item android:maxLevel="9" android:drawable="@drawable/sl4a_logo_48_9" />
+  <item android:maxLevel="2147483647" android:drawable="@drawable/sl4a_logo_48_10" />
+</level-list>
diff --git a/ScriptingLayerForAndroid/res/drawable/sl_icon.png b/ScriptingLayerForAndroid/res/drawable/sl_icon.png
new file mode 100644
index 0000000..a85ef87
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/sl_icon.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/stat_sys_warning.png b/ScriptingLayerForAndroid/res/drawable/stat_sys_warning.png
new file mode 100644
index 0000000..be00f47
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/stat_sys_warning.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/terminal.png b/ScriptingLayerForAndroid/res/drawable/terminal.png
new file mode 100644
index 0000000..9e25569
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/terminal.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_down_btn.xml b/ScriptingLayerForAndroid/res/drawable/timepicker_down_btn.xml
new file mode 100644
index 0000000..a0a566b
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_down_btn.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.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.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.
+-->
+<selector
+  xmlns:android="http://schemas.android.com/apk/res/android">
+  <item
+    android:state_pressed="false"
+    android:state_enabled="true"
+    android:state_focused="false"
+    android:drawable="@drawable/timepicker_down_normal" />
+  <item
+    android:state_pressed="true"
+    android:state_enabled="true"
+    android:drawable="@drawable/timepicker_down_pressed" />
+  <item
+    android:state_pressed="false"
+    android:state_enabled="true"
+    android:state_focused="true"
+    android:drawable="@drawable/timepicker_down_selected" />
+  <item
+    android:state_pressed="false"
+    android:state_enabled="false"
+    android:state_focused="false"
+    android:drawable="@drawable/timepicker_down_disabled" />
+  <item
+    android:state_pressed="false"
+    android:state_enabled="false"
+    android:state_focused="true"
+    android:drawable="@drawable/timepicker_down_disabled_focused" />
+</selector>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_down_disabled.png b/ScriptingLayerForAndroid/res/drawable/timepicker_down_disabled.png
new file mode 100644
index 0000000..af72d22
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_down_disabled.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_down_disabled_focused.png b/ScriptingLayerForAndroid/res/drawable/timepicker_down_disabled_focused.png
new file mode 100644
index 0000000..2d80424
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_down_disabled_focused.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_down_normal.png b/ScriptingLayerForAndroid/res/drawable/timepicker_down_normal.png
new file mode 100644
index 0000000..c427fc3
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_down_normal.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_down_pressed.png b/ScriptingLayerForAndroid/res/drawable/timepicker_down_pressed.png
new file mode 100644
index 0000000..ac6ac53
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_down_pressed.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_down_selected.png b/ScriptingLayerForAndroid/res/drawable/timepicker_down_selected.png
new file mode 100644
index 0000000..f710b57
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_down_selected.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_input.xml b/ScriptingLayerForAndroid/res/drawable/timepicker_input.xml
new file mode 100644
index 0000000..df73ab2
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_input.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.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.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.
+-->
+<selector
+  xmlns:android="http://schemas.android.com/apk/res/android">
+  <item
+    android:state_pressed="false"
+    android:state_enabled="true"
+    android:state_focused="false"
+    android:drawable="@drawable/timepicker_input_normal" />
+  <item
+    android:state_pressed="true"
+    android:state_enabled="true"
+    android:drawable="@drawable/timepicker_input_pressed" />
+  <item
+    android:state_pressed="false"
+    android:state_enabled="true"
+    android:state_focused="true"
+    android:drawable="@drawable/timepicker_input_selected" />
+  <item
+    android:state_pressed="false"
+    android:state_enabled="false"
+    android:state_focused="false"
+    android:drawable="@drawable/timepicker_input_disabled" />
+  <item
+    android:state_pressed="false"
+    android:state_enabled="false"
+    android:state_focused="true"
+    android:drawable="@drawable/timepicker_input_normal" />
+</selector>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_input_disabled.png b/ScriptingLayerForAndroid/res/drawable/timepicker_input_disabled.png
new file mode 100644
index 0000000..97da87a
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_input_disabled.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_input_normal.png b/ScriptingLayerForAndroid/res/drawable/timepicker_input_normal.png
new file mode 100644
index 0000000..eb101c5
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_input_normal.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_input_pressed.png b/ScriptingLayerForAndroid/res/drawable/timepicker_input_pressed.png
new file mode 100644
index 0000000..c83b1eb
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_input_pressed.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_input_selected.png b/ScriptingLayerForAndroid/res/drawable/timepicker_input_selected.png
new file mode 100644
index 0000000..e152848
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_input_selected.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_up_btn.xml b/ScriptingLayerForAndroid/res/drawable/timepicker_up_btn.xml
new file mode 100644
index 0000000..5be8d82
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_up_btn.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.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.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.
+-->
+<selector
+  xmlns:android="http://schemas.android.com/apk/res/android">
+  <item
+    android:state_pressed="false"
+    android:state_enabled="true"
+    android:state_focused="false"
+    android:drawable="@drawable/timepicker_up_normal" />
+  <item
+    android:state_pressed="true"
+    android:state_enabled="true"
+    android:drawable="@drawable/timepicker_up_pressed" />
+  <item
+    android:state_pressed="false"
+    android:state_enabled="true"
+    android:state_focused="true"
+    android:drawable="@drawable/timepicker_up_selected" />
+  <item
+    android:state_pressed="false"
+    android:state_enabled="false"
+    android:state_focused="false"
+    android:drawable="@drawable/timepicker_up_disabled" />
+  <item
+    android:state_pressed="false"
+    android:state_enabled="false"
+    android:state_focused="true"
+    android:drawable="@drawable/timepicker_up_disabled_focused" />
+</selector>
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_up_disabled.png b/ScriptingLayerForAndroid/res/drawable/timepicker_up_disabled.png
new file mode 100644
index 0000000..1814bb4
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_up_disabled.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_up_disabled_focused.png b/ScriptingLayerForAndroid/res/drawable/timepicker_up_disabled_focused.png
new file mode 100644
index 0000000..9ad5b85
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_up_disabled_focused.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_up_normal.png b/ScriptingLayerForAndroid/res/drawable/timepicker_up_normal.png
new file mode 100644
index 0000000..35fc221
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_up_normal.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_up_pressed.png b/ScriptingLayerForAndroid/res/drawable/timepicker_up_pressed.png
new file mode 100644
index 0000000..c910777
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_up_pressed.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/drawable/timepicker_up_selected.png b/ScriptingLayerForAndroid/res/drawable/timepicker_up_selected.png
new file mode 100644
index 0000000..549a7e5
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/drawable/timepicker_up_selected.png
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/layout/act_colors.xml b/ScriptingLayerForAndroid/res/layout/act_colors.xml
new file mode 100644
index 0000000..b40c2f1
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/act_colors.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<RelativeLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="fill_parent"
+	android:layout_height="fill_parent">
+  
+  <LinearLayout
+    android:id="@+id/color_layout"
+    android:layout_height="wrap_content"
+    android:layout_width="210dp"
+    android:layout_centerInParent="true"
+    android:gravity="center">
+    
+    <GridView
+    android:id="@+id/color_grid"
+    android:layout_height="wrap_content"
+    android:layout_width="wrap_content"
+    android:gravity="center"
+    android:padding="20dp"
+    android:verticalSpacing="20dp"
+    android:horizontalSpacing="20dp"
+    android:columnWidth="160dp"
+    android:numColumns="1"
+    android:stretchMode="none"/>
+  
+  </LinearLayout>	
+</RelativeLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/act_console.xml b/ScriptingLayerForAndroid/res/layout/act_console.xml
new file mode 100644
index 0000000..8993db3
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/act_console.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="fill_parent"
+	android:layout_height="fill_parent"
+	android:background="#ff000000"
+	>
+
+	<ViewFlipper
+		android:id="@+id/console_flip"
+		android:layout_width="fill_parent"
+		android:layout_height="fill_parent"
+		/>
+
+	<RelativeLayout
+		android:id="@+id/console_boolean_group"
+		android:layout_width="fill_parent"
+		android:layout_height="wrap_content"
+		android:layout_alignParentBottom="true"
+		android:padding="5dip"
+		android:background="#80000000"
+		android:fadingEdge="horizontal"
+		android:fadingEdgeLength="25dip"
+		android:visibility="gone"
+		>
+
+		<TextView
+			android:id="@+id/console_prompt"
+			android:layout_height="wrap_content"
+			android:layout_width="fill_parent"
+			android:textAppearance="?android:attr/textAppearanceMedium"
+			/>
+
+		<Button
+			android:id="@+id/console_prompt_no"
+			android:text="@string/prompt_no"
+			android:paddingTop="5dip"
+			android:paddingBottom="10dip"
+			android:paddingLeft="40dip"
+			android:paddingRight="40dip"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignParentRight="true"
+			android:layout_below="@+id/console_prompt"
+			android:clickable="false"
+			/>
+
+		<Button
+			android:id="@+id/console_prompt_yes"
+			android:text="@string/prompt_yes"
+			android:paddingTop="5dip"
+			android:paddingBottom="10dip"
+			android:paddingLeft="40dip"
+			android:paddingRight="40dip"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_toLeftOf="@+id/console_prompt_no"
+			android:layout_below="@+id/console_prompt"
+			/>
+
+	</RelativeLayout>
+
+	<ImageView
+		android:id="@+id/keyboard_button"
+		android:paddingRight="15dip"
+		android:paddingBottom="15dip"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_alignParentBottom="true"
+		android:layout_alignParentRight="true"
+		android:visibility="gone"
+		android:src="@+drawable/keyboard_icon"
+		/>
+
+</RelativeLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/act_help.xml b/ScriptingLayerForAndroid/res/layout/act_help.xml
new file mode 100644
index 0000000..2afe9e4
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/act_help.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<ScrollView
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="fill_parent"
+	android:layout_height="wrap_content"
+	android:layout_weight="1"
+	>
+
+	<LinearLayout
+		android:id="@+id/topics"
+		android:orientation="vertical"
+		android:layout_width="fill_parent"
+		android:layout_height="wrap_content"
+		android:gravity="center_horizontal"
+		>
+
+		<TextView
+      android:id="@+id/version"
+			android:layout_width="fill_parent"
+			android:layout_height="wrap_content"
+			android:paddingTop="2dip"
+			android:textAppearance="?android:attr/textAppearanceSmall"
+			android:gravity="right"
+			android:paddingRight="2dip"
+			/>
+    
+    <TextView
+      android:id="@+id/help_acks_text"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:text="@string/help_acks"
+      android:paddingTop="2dip"
+      android:linksClickable="true"
+      android:textAppearance="?android:attr/textAppearanceMedium"
+      android:gravity="center_horizontal"
+      />
+      
+		<TextView
+			android:layout_width="fill_parent"
+			android:layout_height="wrap_content"
+			android:text="@string/help_intro"
+			android:paddingTop="2dip"
+			android:textAppearance="?android:attr/textAppearanceMedium"
+			android:gravity="center_horizontal"
+			/>
+
+	</LinearLayout>
+</ScrollView>
diff --git a/ScriptingLayerForAndroid/res/layout/act_help_topic.xml b/ScriptingLayerForAndroid/res/layout/act_help_topic.xml
new file mode 100644
index 0000000..7123d63
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/act_help_topic.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:orientation="vertical"
+	android:layout_width="fill_parent"
+	android:layout_height="fill_parent"
+	>
+
+	<org.connectbot.util.HelpTopicView
+		android:id="@+id/topic_text"
+		android:layout_width="fill_parent"
+		android:layout_height="fill_parent"
+		/>
+
+</LinearLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/api_browser.xml b/ScriptingLayerForAndroid/res/layout/api_browser.xml
new file mode 100644
index 0000000..4c4f8a4
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/api_browser.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent">
+  <ListView
+    android:id="@+id/android:list"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" />
+  <TextView
+    android:id="@+id/android:empty"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:text="@string/no_apis_message" />
+</LinearLayout>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/api_prompt.xml b/ScriptingLayerForAndroid/res/layout/api_prompt.xml
new file mode 100644
index 0000000..7a65663
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/api_prompt.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="vertical"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent"
+  android:padding="2dip">
+  <ListView
+    android:id="@+id/list"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:divider="#00000000" />
+  <Button
+    android:id="@+id/done"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:text="@string/prompt_finish"
+    android:layout_alignParentBottom="true" />
+</RelativeLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/api_prompt_item.xml b/ScriptingLayerForAndroid/res/layout/api_prompt_item.xml
new file mode 100644
index 0000000..a529139
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/api_prompt_item.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="vertical"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent">
+  <TextView
+    android:id="@+id/api_prompt_item_description"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content" />
+  <EditText
+    android:id="@+id/api_prompt_item_value"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/bluetooth_device_list.xml b/ScriptingLayerForAndroid/res/layout/bluetooth_device_list.xml
new file mode 100644
index 0000000..bec4c53
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/bluetooth_device_list.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent">
+  <ListView
+    android:id="@+id/android:list"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" />
+  <TextView
+    android:id="@+id/android:empty"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:text="@string/no_bluetooth_devices_message" />
+</LinearLayout>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/dia_resize.xml b/ScriptingLayerForAndroid/res/layout/dia_resize.xml
new file mode 100644
index 0000000..32253fb
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/dia_resize.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:orientation="horizontal"
+	android:layout_width="wrap_content"
+	android:layout_height="wrap_content"
+	android:paddingLeft="10dip"
+	android:paddingRight="10dip"
+	>
+
+	<EditText
+		android:id="@+id/width"
+		android:layout_width="100dip"
+		android:layout_height="wrap_content"
+		android:singleLine="true"
+		android:numeric="integer"
+		android:text="@string/default_width"/>
+
+	<TextView
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:paddingLeft="10dip"
+		android:paddingRight="10dip"
+		android:gravity="right|bottom"
+		android:textAppearance="?android:attr/textAppearanceLarge"
+		/>
+
+
+	<EditText
+		android:id="@+id/height"
+		android:layout_width="100dip"
+		android:layout_height="wrap_content"
+		android:singleLine="true"
+		android:numeric="integer"
+		android:text="@string/default_height"/>
+</LinearLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/duration_picker.xml b/ScriptingLayerForAndroid/res/layout/duration_picker.xml
new file mode 100644
index 0000000..29e524d
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/duration_picker.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2008 OpenIntents.org
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ -->
+<RelativeLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="vertical"
+  android:layout_width="wrap_content"
+  android:layout_height="wrap_content">
+  <com.googlecode.android_scripting.widget.NumberPicker
+    android:id="@+id/day"
+    android:layout_width="70dip"
+    android:layout_height="wrap_content"
+    android:focusable="true"
+    android:focusableInTouchMode="true" />
+  <TextView
+    android:text="@string/display_days"
+    android:gravity="center_horizontal"
+    android:layout_width="70dip"
+    android:layout_height="wrap_content"
+    android:layout_below="@+id/day"
+    android:layout_alignLeft="@+id/day" />
+  <com.googlecode.android_scripting.widget.NumberPicker
+    android:id="@+id/hour"
+    android:layout_width="70dip"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="2dip"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:layout_toRightOf="@+id/day" />
+  <TextView
+    android:text="@string/display_hours"
+    android:gravity="center_horizontal"
+    android:layout_width="70dip"
+    android:layout_height="wrap_content"
+    android:layout_below="@+id/hour"
+    android:layout_alignLeft="@+id/hour" />
+   <com.googlecode.android_scripting.widget.NumberPicker
+    android:id="@+id/minute"
+    android:layout_width="70dip"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="2dip"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:layout_toRightOf="@+id/hour" />
+  <TextView
+    android:text="@string/display_minutes"
+    android:gravity="center_horizontal"
+    android:layout_width="70dip"
+    android:layout_height="wrap_content"
+    android:layout_below="@+id/minute"
+    android:layout_alignLeft="@+id/minute" />
+  <com.googlecode.android_scripting.widget.NumberPicker
+    android:id="@+id/second"
+    android:layout_width="70dip"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="2dip"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:layout_toRightOf="@+id/minute" />
+  <TextView
+    android:text="@string/display_seconds"
+    android:gravity="center_horizontal"
+    android:layout_width="70dip"
+    android:layout_height="wrap_content"
+    android:layout_below="@+id/second"
+    android:layout_alignLeft="@+id/second" />
+</RelativeLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/findreplace.xml b/ScriptingLayerForAndroid/res/layout/findreplace.xml
new file mode 100644
index 0000000..64ec004
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/findreplace.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="vertical"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+    <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/display_find"></TextView>
+    <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/searchFind">
+        <requestFocus></requestFocus>
+    </EditText>
+    <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/display_replace"></TextView>
+    <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/searchReplace"></EditText>
+    <LinearLayout android:id="@+id/linearLayout1" android:layout_width="match_parent" android:layout_height="wrap_content">
+    </LinearLayout>
+    <LinearLayout android:id="@+id/linearLayout2" android:layout_width="match_parent" android:layout_height="wrap_content">
+    </LinearLayout>
+    <TableLayout android:id="@+id/tableLayout1" android:layout_width="match_parent" android:layout_height="wrap_content">
+        <TableRow android:id="@+id/tableRow1" android:layout_width="wrap_content" android:layout_height="wrap_content">
+            <CheckBox android:text="@string/display_whole_words" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/searchWord"></CheckBox>
+            <CheckBox android:text="@string/display_case_sensitive" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/searchCase"></CheckBox>
+        </TableRow>
+        <TableRow android:id="@+id/tableRow2" android:layout_width="wrap_content" android:layout_height="wrap_content">
+            <CheckBox android:text="@string/display_replace_all" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/searchAll"></CheckBox>
+            <CheckBox android:text="@string/display_from_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/searchStart"></CheckBox>
+        </TableRow>
+    </TableLayout>
+    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/textView3" android:typeface="normal" android:text="@string/findreplace_undo_reminder"></TextView>
+</LinearLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/interpreter_manager.xml b/ScriptingLayerForAndroid/res/layout/interpreter_manager.xml
new file mode 100644
index 0000000..6b2afb9
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/interpreter_manager.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent">
+  <ListView
+    android:id="@+id/android:list"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" />
+  <TextView
+    android:id="@+id/android:empty"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:text="@string/no_interpreters" />
+</LinearLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/item_terminal.xml b/ScriptingLayerForAndroid/res/layout/item_terminal.xml
new file mode 100644
index 0000000..9a8ff19
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/item_terminal.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+-->
+
+<RelativeLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="fill_parent"
+	android:layout_height="fill_parent"
+	>
+
+	<TextView
+		android:id="@+id/terminal_overlay"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:textAppearance="?android:attr/textAppearanceLarge"
+		android:background="#aa000000"
+		android:padding="10dip"
+		android:layout_centerInParent="true"
+		/>
+
+</RelativeLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/list_item.xml b/ScriptingLayerForAndroid/res/layout/list_item.xml
new file mode 100644
index 0000000..e9f2814
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/list_item.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="horizontal"
+  android:layout_width="wrap_content"
+  android:layout_height="wrap_content"
+  android:padding="4dip"
+  android:gravity="center_vertical">
+  <ImageView
+    android:id="@+id/list_item_icon"
+    android:layout_width="32dip"
+    android:layout_height="32dip"
+    android:fadingEdgeLength="0dip" />
+  <TextView
+    android:id="@+id/list_item_title"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:paddingLeft="5dip"
+    android:textSize="22dip" />
+</LinearLayout>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/logcat_viewer.xml b/ScriptingLayerForAndroid/res/layout/logcat_viewer.xml
new file mode 100644
index 0000000..2517432
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/logcat_viewer.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent">
+  <ListView
+    android:id="@+id/android:list"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" />
+  <TextView
+    android:id="@+id/android:empty"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:text="@string/no_logcat_msg" />
+</LinearLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/notification.xml b/ScriptingLayerForAndroid/res/layout/notification.xml
new file mode 100644
index 0000000..a450fbe
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/notification.xml
@@ -0,0 +1,33 @@
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="vertical"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent"
+  android:padding="3dip">
+  <RelativeLayout
+    android:orientation="horizontal"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content" >
+    <TextView
+      android:id="@+id/notification_title"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:textColor="#000"
+      android:textStyle="bold"
+      android:textSize="12dip"
+      android:layout_alignParentLeft="true" />
+    <TextView
+      android:id="@+id/notification_action"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:textColor="#000"
+      android:textSize="12dip"
+      android:layout_alignParentRight="true" />
+  </RelativeLayout>
+  <TextView
+    android:id="@+id/notification_message"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:textColor="#000"
+    android:textSize="12dip" />
+</LinearLayout>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/number_picker.xml b/ScriptingLayerForAndroid/res/layout/number_picker.xml
new file mode 100644
index 0000000..8b1a433
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/number_picker.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.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.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.
+*/
+-->
+<merge
+  xmlns:android="http://schemas.android.com/apk/res/android">
+  <com.googlecode.android_scripting.widget.NumberPickerButton
+    android:id="@+id/increment"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:background="@drawable/timepicker_up_btn" />
+  <EditText
+    android:id="@+id/timepicker_input"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:singleLine="true"
+    style="?android:attr/textAppearanceLargeInverse"
+    android:textSize="30sp"
+    android:background="@drawable/timepicker_input"
+    android:numeric="integer" />

+  <com.googlecode.android_scripting.widget.NumberPickerButton
+    android:id="@+id/decrement"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:background="@drawable/timepicker_down_btn" />
+</merge>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/number_picker_edit.xml b/ScriptingLayerForAndroid/res/layout/number_picker_edit.xml
new file mode 100644
index 0000000..ebf34da
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/number_picker_edit.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.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.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.
+*/
+-->
+<EditText
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="fill_parent"
+  android:layout_height="wrap_content"
+  android:gravity="center_horizontal"
+  android:singleLine="true"
+  style="?android:attr/textAppearanceLargeInverse"
+  android:textSize="30sp"
+  android:background="@drawable/timepicker_input" />
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/script_editor.xml b/ScriptingLayerForAndroid/res/layout/script_editor.xml
new file mode 100644
index 0000000..be9c22e
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/script_editor.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="vertical"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent"
+  android:paddingTop="4dip">
+  <EditText
+    android:id="@+id/script_editor_title"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:hint="Script Name"
+    android:singleLine="true" />
+  <EditText
+    android:id="@+id/script_editor_body"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:hint="Script Content"
+    android:typeface="monospace"
+    android:gravity="top"
+    android:inputType="textMultiLine" android:scrollHorizontally="true" android:scrollbars="horizontal|vertical"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/script_manager.xml b/ScriptingLayerForAndroid/res/layout/script_manager.xml
new file mode 100644
index 0000000..e8ce08e
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/script_manager.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent">
+  <ListView
+    android:id="@+id/android:list"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" />
+  <TextView
+    android:id="@+id/android:empty"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:text="@string/no_scripts_message" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/script_monitor.xml b/ScriptingLayerForAndroid/res/layout/script_monitor.xml
new file mode 100644
index 0000000..7c57f3b
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/script_monitor.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="vertical"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent">
+   <ListView
+    android:id="@+id/android:list"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" />
+  <TextView
+    android:id="@+id/android:empty"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:text="@string/no_running_scripts_message" />
+</LinearLayout>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/script_monitor_list_item.xml b/ScriptingLayerForAndroid/res/layout/script_monitor_list_item.xml
new file mode 100644
index 0000000..df3c339
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/script_monitor_list_item.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="vertical"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent">
+    <RelativeLayout
+      android:layout_width="fill_parent" 
+      android:layout_height="wrap_content">
+      
+      <TextView
+      android:id="@+id/process_title"
+      android:textSize="16sp"
+      android:textStyle="bold"
+      android:textColor="#FFF"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"/>
+      <TextView
+      android:id="@+id/process_age"
+      android:layout_alignParentRight="true"
+      android:gravity="right"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"/>
+    </RelativeLayout>
+    <RelativeLayout
+      android:layout_width="fill_parent" 
+      android:layout_height="wrap_content">
+       <TextView
+      android:id="@+id/process_details"
+      android:gravity="left"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"/>
+      <TextView
+      android:id="@+id/process_status"
+      android:layout_alignParentRight="true"
+      android:gravity="right"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"/>
+    </RelativeLayout>  
+</LinearLayout>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/layout/title.xml b/ScriptingLayerForAndroid/res/layout/title.xml
new file mode 100644
index 0000000..54df631
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/title.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.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.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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/screen"
+    android:layout_width="fill_parent" android:layout_height="fill_parent"
+    android:orientation="vertical">
+    <TextView android:id="@+id/left_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:ellipsize="end" 
+        android:maxLines="1"
+        android:scrollHorizontally="true"
+        android:layout_centerVertical="true"
+        android:fadingEdge="horizontal"
+        android:layout_alignParentLeft="true" 
+        android:textStyle="bold" 
+        android:textColor="#ffffff" 
+        android:shadowColor="#000000" 
+        android:shadowRadius="1"
+        android:layout_toLeftOf="@+id/right_text"/>
+    <TextView android:id="@+id/right_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_toLeftOf="@+id/progress_bar"
+        android:layout_alignWithParentIfMissing="true"/>
+     <ProgressBar android:id="@+id/progress_bar"
+      android:layout_height="18sp"
+      android:layout_width="18sp"
+      android:layout_marginLeft="5sp"
+      android:visibility="gone"
+      style="android:attr/ProgressBar_indeterminate"
+      android:layout_centerVertical="true"
+      android:layout_alignParentRight="true"
+     />
+</RelativeLayout>
diff --git a/ScriptingLayerForAndroid/res/layout/trigger_manager.xml b/ScriptingLayerForAndroid/res/layout/trigger_manager.xml
new file mode 100644
index 0000000..926da9d
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/layout/trigger_manager.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent" android:orientation="vertical">
+  <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btnTriggerCancel" android:onClick="clickCancel" android:text="@string/cancel_all"></Button><ListView
+    android:id="@+id/android:list"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" />
+  <TextView
+    android:id="@+id/android:empty"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:text="@string/no_triggers_message" />
+
+</LinearLayout>
diff --git a/ScriptingLayerForAndroid/res/menu/terminal.xml b/ScriptingLayerForAndroid/res/menu/terminal.xml
new file mode 100644
index 0000000..136dcaf
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/menu/terminal.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.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.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.
+*/
+-->
+
+<menu
+  xmlns:android="http://schemas.android.com/apk/res/android">
+  <item
+    android:id="@+id/terminal_menu_resize"
+    android:title="@string/terminal_menu_resize"
+    android:alphabeticShortcut="s"
+    android:icon="@android:drawable/ic_menu_crop" />
+  <item
+    android:id="@+id/terminal_menu_send_email"
+    android:title="Email"
+    android:alphabeticShortcut="e"
+    android:icon="@android:drawable/ic_menu_send" />
+  <item
+    android:id="@+id/terminal_menu_preferences"
+    android:title="Preferences"
+    android:alphabeticShortcut="p"
+    android:icon="@android:drawable/ic_menu_preferences" />
+  <item
+    android:id="@+id/terminal_menu_exit_and_edit"
+    android:title="Exit &amp; Edit"
+    android:alphabeticShortcut="q"
+    android:enabled="false"
+    android:icon="@android:drawable/ic_menu_edit" />
+</menu>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/raw/bell.ogg b/ScriptingLayerForAndroid/res/raw/bell.ogg
new file mode 100644
index 0000000..674f25d
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/raw/bell.ogg
Binary files differ
diff --git a/ScriptingLayerForAndroid/res/values/arrays.xml b/ScriptingLayerForAndroid/res/values/arrays.xml
new file mode 100644
index 0000000..a4e3851
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/values/arrays.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.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.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.
+*/
+-->
+
+<resources>
+  <string-array
+    name="entries_fontsize_preference">
+    <item>4 x 8 pixels</item>
+    <item>6 pt</item>
+    <item>7 pt</item>
+    <item>8 pt</item>
+    <item>9 pt</item>
+    <item>10 pt</item>
+    <item>12 pt</item>
+    <item>14 pt</item>
+    <item>16 pt</item>
+    <item>18 pt</item>
+    <item>20 pt</item>
+    <item>22 pt</item>
+    <item>24 pt</item>
+    <item>26 pt</item>
+    <item>28 pt</item>
+    <item>30 pt</item>
+  </string-array>
+  <string-array
+    name="list_rotation">
+    <item>@string/list_rotation_default</item>
+    <item>@string/list_rotation_land</item>
+    <item>@string/list_rotation_port</item>
+    <item>@string/list_rotation_auto</item>
+  </string-array>
+  <string-array
+    name="list_rotation_values">
+    <item>Default</item>
+    <item>Force landscape</item>
+    <item>Force portrait</item>
+    <item>Automatic</item>
+  </string-array>
+  
+    <string-array name="list_keymode">
+    <item>@string/list_keymode_right</item>
+    <item>@string/list_keymode_left</item>
+    <item>@string/list_keymode_none</item>
+  </string-array>
+
+  <string-array name="list_keymode_values">
+    <item>Use right-side keys</item>
+    <item>Use left-side keys</item>
+    <item>none</item>
+  </string-array>
+  
+    <string-array name="list_camera">
+    <item>@string/list_camera_ctrlaspace</item>
+    <item>@string/list_camera_ctrla</item>
+    <item>@string/list_camera_esc</item>
+    <item>@string/list_camera_esc_a</item>
+    <item>@string/list_camera_none</item>
+  </string-array>
+
+  <string-array name="list_camera_values">
+    <item>Ctrl+A then Space</item>
+    <item>Ctrl+A</item>
+    <item>Esc</item>
+    <item>Esc+A</item>
+    <item>None</item>
+  </string-array>
+  
+    <string-array name="list_delkey">
+    <item>@string/list_delkey_del</item>
+    <item>@string/list_delkey_backspace</item>
+  </string-array>
+
+  <string-array name="list_delkey_values">
+    <item>del</item>
+    <item>backspace</item>
+  </string-array>
+  
+  <string-array
+    name="entryvalues_fontsize_preference">
+    <item>0</item>
+    <item>6</item>
+    <item>7</item>
+    <item>8</item>
+    <item>9</item>
+    <item>10</item>
+    <item>12</item>
+    <item>14</item>
+    <item>16</item>
+    <item>18</item>
+    <item>20</item>
+    <item>22</item>
+    <item>24</item>
+    <item>26</item>
+    <item>28</item>
+    <item>30</item>
+  </string-array>
+  <string-array
+    name="entries_color_preference">
+    <item>Black text on white</item>
+    <item>White text on black</item>
+    <item>White text on blue</item>
+  </string-array>
+  <string-array
+    name="entryvalues_color_preference">
+    <item>0</item>
+    <item>1</item>
+    <item>2</item>
+  </string-array>
+  <string-array
+    name="entries_controlkey_preference">
+    <item>Jog ball</item>
+    <item>\@ key</item>
+    <item>Left Alt key</item>
+    <item>Right Alt key</item>
+  </string-array>
+  <string-array
+    name="entryvalues_controlkey_preference">
+    <item>0</item>
+    <item>1</item>
+    <item>2</item>
+    <item>3</item>
+  </string-array>
+</resources>
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/res/values/strings.xml b/ScriptingLayerForAndroid/res/values/strings.xml
new file mode 100644
index 0000000..b95427e
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/values/strings.xml
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.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.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.
+*/
+-->
+
+<resources>
+    <string name="help_force_browser">Force API Browser</string>
+   <string
+    name="stop_all">Stop All</string>
+  <string
+    name="application_title">SL4A</string>
+  <string
+    name="application_nice_title">Scripting Layer for Android</string>
+  <string
+    name="text_preferences">Text</string>
+  <string
+    name="title_fontsize_preference">Font size</string>
+  <string
+    name="summary_fontsize_preference">Choose character height in pixels.</string>
+  <string
+    name="dialog_title_fontsize_preference">Font size</string>
+  <string
+    name="title_color_preference">Colors</string>
+  <string
+    name="summary_color_preference">Choose text color.</string>
+  <string
+    name="dialog_title_color_preference">Text color</string>
+  <string
+    name="keyboard_preferences">Keyboard</string>
+  <string
+    name="title_controlkey_preference">Control key</string>
+  <string
+    name="summary_controlkey_preference">Choose control key.</string>
+  <string
+    name="dialog_title_controlkey_preference">Control key</string>
+  <string
+    name="shell_preferences">Shell</string>
+  <string
+    name="title_shell_preference">Command line</string>
+  <string
+    name="summary_shell_preference">Specify the shell command line.</string>
+  <string
+    name="dialog_title_shell_preference">Shell</string>
+  <string
+    name="title_initialcommand_preference">Initial command</string>
+  <string
+    name="summary_initialcommand_preference">Sent to the shell when it starts.</string>
+  <string
+    name="dialog_title_initialcommand_preference">Initial Command</string>
+  <string
+    name="default_value_fontsize_preference">10</string>
+  <string
+    name="default_value_color_preference">1</string>
+  <string
+    name="default_value_controlkey_preference">0</string>
+  <string
+    name="no_scripts_message">Start adding scripts and interpreters by pressing the menu
+    button.</string>
+  <string
+    name="no_apis_message">No matching APIs found.</string>  
+  <string
+    name="notification_action_message">Tap to open Script Monitor.</string>
+  <string
+    name="no_running_scripts_message">There are no currently running scripts.</string>
+  <string
+    name="script_number_message">Number of running scripts:\t</string>
+  <string
+    name="no_triggers_message">No triggers.</string>
+  <string
+    name="no_bluetooth_devices_message">No Bluetooth devices found.</string>
+  <string
+    name="search_description_scripts">Search scripts</string>
+  <string
+    name="search_description_apis">Search APIs</string>
+    
+  <string name="prompt_process_exited">Process has exited.\nClose terminal?</string>
+  <string name="prompt_confirm_exit">Confirm exit.\nKill process?</string>
+  
+  <string name="terminal_copy_done">Copied %1$d bytes to clipboard</string>
+  <string name="terminal_copy_start">Touch and drag\nor use directional pad\nto select area to copy</string>
+  
+  
+  <string name="terminal_menu_close">Close</string>
+  <string name="terminal_menu_copy">Copy</string>
+  <string name="terminal_menu_paste">Paste</string>
+  <string name="terminal_menu_resize">Force Size</string>
+  
+  <string name="button_resize">Resize</string>
+  
+  <!-- Name for the scrollback size preference -->
+  <string name="pref_scrollback_title">Scrollback size</string>
+  <!-- Description of the scrollback size preference -->
+  <string name="pref_scrollback_summary">Size of scrollback buffer to keep in memory for each console</string>
+  <!-- Name for the rotation mode preference -->
+  <string name="pref_rotation_title">Rotation mode</string>
+  <!-- Summary for the rotation mode preference -->
+  <string name="pref_rotation_summary">How to change rotation when keyboard popped in/out</string>
+    <!-- Name for the full screen preference -->
+  <string name="pref_fullscreen_title">Full screen</string>
+  <!-- Summary for the full screen preference -->
+  <string name="pref_fullscreen_summary">Hide status bar while in console</string>
+    <!-- Name for the keyboard shortcuts preference -->
+  <string name="pref_keymode_title">Directory shortcuts</string>
+  <!-- Summary for the keyboard shortcuts preference -->
+  <string name="pref_keymode_summary">Select how to use Alt for \'/\' and Shift for Tab</string>
+  <!-- Name for the camera shortcut usage preference -->
+  <string name="pref_camera_title">Camera shortcut</string>
+  <!-- Summary for the camera shortcut usage preference -->
+  <string name="pref_camera_summary">Select which shortcut to trigger when the camera button is pushed</string>
+  <!-- Name for the keep screen on preference -->
+  <string name="pref_keepalive_title">Keep screen awake</string>
+  <!-- Summary for the camera shortcut usage preference -->
+  <string name="pref_keepalive_summary">Prevent the screen from turning off when working in a console</string>
+  <!-- Name for the haptic feedback (bumpy arrow) preference -->
+  <string name="pref_bumpyarrows_title">Bumpy arrows</string>
+  <!-- Summary for the haptic feedback (bumpy arrow) preference -->
+  <string name="pref_bumpyarrows_summary">Vibrate when sending arrow keys from trackball</string>
+  <string name="pref_hidekeyboard_title">Hide Keyboard</string>
+  <string name="pref_hidekeyboard_summary">Hide soft keyboard in terminal on startup</string>
+  <!-- Category title for the Terminal Bell preferences -->
+  <string name="pref_bell_category">Terminal bell</string>
+  <!-- Checkbox preference title for the audible terminal bell feature -->
+  <string name="pref_bell_title">Audible bell</string>
+  <!-- Title for the slider preference to set the volume -->
+  <string name="pref_bell_volume_title">Bell volume</string>
+  <!-- Checkbox preference title for the vibrate on terminal bell feature -->
+  <string name="pref_bell_vibrate_title">Vibrate on bell</string>
+  <string name="pref_fontsize_title">Font size (pt)</string>
+      <!-- Setting for what key code is sent to the server when DEL key is pressed. -->
+  <string name="pref_delkey_title">DEL Key</string>
+  <!-- Summary for field asking what key code is sent to the server when DEL key is pressed. -->
+  <string name="pref_delkey_summary">The key code sent when DEL key is pressed</string>
+  <!-- Host character encoding preference title -->
+  <string name="pref_encoding_title">Encoding</string>
+  <!-- Host character encoding preference summary -->
+  <string name="pref_encoding_summary">Character encoding for the host</string>
+  <string name="pref_hide_notifications">Hide Notifications</string>
+      <!-- Default screen rotation preference selection -->
+  <string name="list_rotation_default">Default</string>
+  <string name="list_rotation_land">Force landscape</string>
+  <string name="list_rotation_port">Force portrait</string>
+  <!-- Selection to indicate the rotation should be selected automatically based on the tilt sensor. -->
+  <string name="list_rotation_auto">Automatic</string>
+    <!-- Preference selection to indicate use of right side of keyboard for special shortcuts. -->
+  <string name="list_keymode_right">Use right-side keys</string>
+  <!-- Preference selection to indicate use of left side of keyboard for special shortcuts. -->
+  <string name="list_keymode_left">Use left-side keys</string>
+  <!-- Preference selection to indicate never to use special shortcut keys. -->
+  <string name="list_keymode_none">Disable</string>
+    <!-- Selection to indicate pressing the Camera button should send "Ctrl+A then Space". -->
+  <string name="list_camera_ctrlaspace">Ctrl+A then Space</string>
+  <!-- Selection to indicate pressing the Camera button should send "Ctrl+A". -->
+  <string name="list_camera_ctrla">Ctrl+A</string>
+  <!-- Selection to indicate pressing the Camera button should send the "Esc" key. -->
+  <string name="list_camera_esc">Esc</string>
+  <!-- Selection to indicate pressing the Camera button should send "Esc+A". -->
+  <string name="list_camera_esc_a">Esc+A</string>
+  <!-- Selection to indicate pressing the Camera button should send nothing at all. -->
+  <string name="list_camera_none">None</string>
+    <!-- Name for the backspace character -->
+  <string name="list_delkey_backspace">Backspace</string>
+  <!-- Name for the ASCII DEL character -->
+  <string name="list_delkey_del">Delete</string>
+  
+  <!-- Window title for Help index -->
+  <string name="title_help">Help</string>
+  
+  <string name="help_acks">SL4A uses <a href="http://code.google.com/p/connectbot/">ConnectBot</a> terminal.</string>
+  <string name="help_intro">Please select a topic below for more information on a particular subject.</string>
+  <string name="enable_auto_close">Enable Auto Close</string>
+  <string name="prompt_no">No</string>
+  <string name="prompt_yes">Yes</string>
+  <string name="prompt_finish">Finish</string>
+  <string name="default_width">80</string>
+  <string name="default_height">25</string>
+  <string name="display_days">Days</string>
+  <string name="display_hours">Hours</string>
+  <string name="display_minutes">Minutes</string>
+  <string name="display_seconds">Seconds</string>
+  <string name="display_find">Find</string>
+  <string name="display_replace">Replace</string>
+  <string name="display_whole_words">Whole Words</string>
+  <string name="display_case_sensitive">Case Sensitive</string>
+  <string name="display_replace_all">Replace All</string>
+  <string name="display_from_start">From Start</string>
+  <string name="findreplace_undo_reminder">Remember you can use the volume keys to undo mistakes.</string>
+  <string name="no_interpreters">No interpreters.</string>
+  <string name="no_logcat_msg">No logcat messages.</string>
+  <string name="cancel_all">Cancel All</string>
+
+  <string name="title_triggers">Triggers</string>
+  <string name="title_scripts">Scripts</string>
+  <string name="title_api_browser">API Browser</string>
+  <string name="title_logcat">Logcat</string>
+  <string name="title_interpreters">Interpreters</string>
+  <string name="title_script_monitor">Script Monitor</string>
+  <string name="title_bluetooth_devices">Bluetooth Devices</string>
+
+</resources>
diff --git a/ScriptingLayerForAndroid/res/xml/paths.xml b/ScriptingLayerForAndroid/res/xml/paths.xml
new file mode 100644
index 0000000..ca3b85a
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/xml/paths.xml
@@ -0,0 +1,3 @@
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+    <cache-path name="mms" path="." />
+</paths>
diff --git a/ScriptingLayerForAndroid/res/xml/preferences.xml b/ScriptingLayerForAndroid/res/xml/preferences.xml
new file mode 100644
index 0000000..e0e6d35
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/xml/preferences.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.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.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.
+*/
+-->
+
+<PreferenceScreen
+  xmlns:android="http://schemas.android.com/apk/res/android">
+  <PreferenceCategory
+    android:title="General">
+    <EditTextPreference android:summary="Port to use for running server. Leave at 0 for random selection." android:key="use_service_port" android:title="Server Port" android:defaultValue="0" android:numeric="integer"></EditTextPreference>
+  </PreferenceCategory>
+  <PreferenceCategory
+    android:title="Script Manager">
+    <CheckBoxPreference
+      android:key="show_all_files"
+      android:title="Show all files"
+      android:defaultValue="false" />
+  </PreferenceCategory>
+  <PreferenceCategory
+    android:title="Script Editor">
+    <ListPreference
+      android:key="editor_fontsize"
+      android:defaultValue="@string/default_value_fontsize_preference"
+      android:title="@string/title_fontsize_preference"
+      android:summary="@string/summary_fontsize_preference"
+      android:entries="@array/entries_fontsize_preference"
+      android:entryValues="@array/entryvalues_fontsize_preference"
+      android:dialogTitle="@string/dialog_title_fontsize_preference" />
+  <CheckBoxPreference android:title="@string/help_force_browser" android:key="helpForceBrowser" android:defaultValue="true" android:summary="Force API Help to use default Android Browser"></CheckBoxPreference>
+  <CheckBoxPreference android:summary="When enabled, quotes and brackets are automatically completed." android:key="enableAutoClose" android:title="@string/enable_auto_close" android:defaultValue="true"></CheckBoxPreference>
+  <CheckBoxPreference android:title="No Wrap" android:summary="Don't wrap text in editor" android:key="editor_no_wrap"></CheckBoxPreference>
+  <CheckBoxPreference android:key="editor_auto_indent" android:defaultValue="false" android:summaryOff="Auto Indent Disabled" android:summaryOn="Auto Indent Enabled" android:title="Auto Indent"></CheckBoxPreference>
+</PreferenceCategory>
+  <PreferenceCategory
+    android:title="Terminal">
+    <EditTextPreference
+      android:key="scrollback"
+      android:title="@string/pref_scrollback_title"
+      android:summary="@string/pref_scrollback_summary"
+      android:defaultValue="140" />
+    <EditTextPreference
+      android:key="fontsize"
+      android:title="@string/pref_fontsize_title"
+      android:defaultValue="10" />
+    <org.connectbot.util.EncodingPreference
+      android:key="encoding"
+      android:title="@string/pref_encoding_title"
+      android:summary="@string/pref_encoding_summary" />
+    <ListPreference
+      android:key="rotation"
+      android:title="@string/pref_rotation_title"
+      android:summary="@string/pref_rotation_summary"
+      android:entries="@array/list_rotation"
+      android:entryValues="@array/list_rotation_values"
+      android:defaultValue="Default" />
+    <Preference
+      android:key="color"
+      android:title="@string/title_color_preference"
+      android:summary="@string/summary_color_preference">
+      <intent
+        android:action="com.googlecode.android_scripting.PICK_TERMINAL_COLORS" />
+    </Preference>
+    <CheckBoxPreference
+      android:key="fullscreen"
+      android:title="@string/pref_fullscreen_title"
+      android:summary="@string/pref_fullscreen_summary"
+      android:defaultValue="false" />
+    <ListPreference
+      android:key="delkey"
+      android:title="@string/pref_delkey_title"
+      android:summary="@string/pref_delkey_summary"
+      android:entries="@array/list_delkey"
+      android:entryValues="@array/list_delkey_values" />
+    <ListPreference
+      android:key="keymode"
+      android:title="@string/pref_keymode_title"
+      android:summary="@string/pref_keymode_summary"
+      android:entries="@array/list_keymode"
+      android:entryValues="@array/list_keymode_values"
+      android:defaultValue="Use right-side keys" />
+    <ListPreference
+      android:key="camera"
+      android:title="@string/pref_camera_title"
+      android:summary="@string/pref_camera_summary"
+      android:entries="@array/list_camera"
+      android:entryValues="@array/list_camera_values"
+      android:defaultValue="Ctrl+A then Space" ></ListPreference>
+    <CheckBoxPreference
+      android:key="keepalive"
+      android:title="@string/pref_keepalive_title"
+      android:summary="@string/pref_keepalive_summary"
+      android:defaultValue="true" />
+    <CheckBoxPreference
+      android:key="bumpyarrows"
+      android:title="@string/pref_bumpyarrows_title"
+      android:summary="@string/pref_bumpyarrows_summary"
+      android:defaultValue="true" />
+    <CheckBoxPreference
+      android:key="hidekeyboard"
+      android:title="@string/pref_hidekeyboard_title"
+      android:summary="@string/pref_hidekeyboard_summary"
+      android:defaultValue="false" />
+  </PreferenceCategory>
+  <PreferenceCategory
+    android:title="Terminal bell">
+    <CheckBoxPreference
+      android:key="bell"
+      android:title="@string/pref_bell_title"
+      android:defaultValue="true" />
+    <org.connectbot.util.VolumePreference
+      android:key="bellVolume"
+      android:title="@string/pref_bell_volume_title" />
+    <CheckBoxPreference
+      android:key="bellVibrate"
+      android:title="@string/pref_bell_vibrate_title"
+      android:defaultValue="true" />
+  </PreferenceCategory>
+  <PreferenceCategory
+	  android:title="Trigger Behaviour">
+	  <CheckBoxPreference 
+	    android:key="hideServiceNotifications"
+		android:title="@string/pref_hide_notifications" 
+		android:defaultValue="false" />
+	</PreferenceCategory>
+</PreferenceScreen>
diff --git a/ScriptingLayerForAndroid/res/xml/searchable_apis.xml b/ScriptingLayerForAndroid/res/xml/searchable_apis.xml
new file mode 100644
index 0000000..bab5acc
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/xml/searchable_apis.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.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.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.
+*/
+-->
+
+  <searchable xmlns:android="http://schemas.android.com/apk/res/android"
+        android:label="@string/application_title"
+        android:hint="@string/search_description_apis"
+        android:searchSettingsDescription="@string/search_description_apis"
+        android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
+        android:searchSuggestAuthority="com.googlecode.android_scripting.provider.apiprovider"
+        android:searchSuggestPath="searchSuggestions">
+</searchable>
+
diff --git a/ScriptingLayerForAndroid/res/xml/searchable_scripts.xml b/ScriptingLayerForAndroid/res/xml/searchable_scripts.xml
new file mode 100644
index 0000000..b0773cc
--- /dev/null
+++ b/ScriptingLayerForAndroid/res/xml/searchable_scripts.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.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.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.
+*/
+-->
+
+  <searchable xmlns:android="http://schemas.android.com/apk/res/android"
+        android:label="@string/application_title"
+        android:hint="@string/search_description_scripts"
+        android:searchSettingsDescription="@string/search_description_scripts"
+        android:includeInGlobalSearch="true"
+        android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
+        android:searchSuggestAuthority="com.googlecode.android_scripting.provider.scriptprovider"
+        android:searchSuggestPath="searchSuggestions"
+        android:searchSuggestIntentAction="android.intent.action.SEARCH">
+</searchable>
+
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/ActivityFlinger.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/ActivityFlinger.java
new file mode 100644
index 0000000..91216ac
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/ActivityFlinger.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.View.OnTouchListener;
+
+import com.googlecode.android_scripting.activity.InterpreterManager;
+import com.googlecode.android_scripting.activity.LogcatViewer;
+import com.googlecode.android_scripting.activity.ScriptManager;
+import com.googlecode.android_scripting.activity.TriggerManager;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class ActivityFlinger {
+
+  private static final int SWIPE_MIN_DISTANCE = 120;
+  private static final int SWIPE_MAX_OFF_PATH = 100;
+  private static final int SWIPE_THRESHOLD_VELOCITY = 200;
+
+  private static class ActivityTransition {
+    Class<? extends Activity> mLeft;
+    Class<? extends Activity> mRight;
+
+    public ActivityTransition(Class<? extends Activity> left, Class<? extends Activity> right) {
+      mLeft = left;
+      mRight = right;
+    }
+  }
+
+  private static Map<Class<?>, ActivityTransition> mActivityTransitions =
+      new HashMap<Class<?>, ActivityTransition>();
+
+  private ActivityFlinger() {
+    // Utility class.
+  }
+
+  static {
+    List<Class<? extends Activity>> entries = new ArrayList<Class<? extends Activity>>();
+    entries.add(ScriptManager.class);
+    entries.add(InterpreterManager.class);
+    entries.add(TriggerManager.class);
+    entries.add(LogcatViewer.class);
+
+    Class<? extends Activity> left = null;
+    Class<? extends Activity> current = null;
+    Class<? extends Activity> right = null;
+
+    for (Iterator<Class<? extends Activity>> it = entries.iterator(); it.hasNext()
+        || current != null;) {
+      if (current == null) {
+        current = it.next();
+      }
+      if (it.hasNext()) {
+        right = it.next();
+      } else {
+        right = null;
+      }
+      mActivityTransitions.put(current, new ActivityTransition(left, right));
+      left = current;
+      current = right;
+    }
+  }
+
+  public static void attachView(View view, Context context) {
+    final LeftRightFlingListener mListener = new LeftRightFlingListener();
+    final GestureDetector mGestureDetector = new GestureDetector(mListener);
+    ActivityTransition transition = mActivityTransitions.get(context.getClass());
+    if (transition.mLeft != null) {
+      mListener.mLeftRunnable = new StartActivityRunnable(context, transition.mLeft);
+    }
+    if (transition.mRight != null) {
+      mListener.mRightRunnable = new StartActivityRunnable(context, transition.mRight);
+    }
+    view.setOnTouchListener(new OnTouchListener() {
+      @Override
+      public boolean onTouch(View v, MotionEvent event) {
+        return mGestureDetector.onTouchEvent(event);
+      }
+    });
+  }
+
+  private static class StartActivityRunnable implements Runnable {
+
+    private final Context mContext;
+    private final Class<?> mActivityClass;
+
+    private StartActivityRunnable(Context context, Class<?> activity) {
+      mContext = context;
+      mActivityClass = activity;
+    }
+
+    @Override
+    public void run() {
+      Intent intent = new Intent(mContext, mActivityClass);
+      mContext.startActivity(intent);
+    }
+  }
+
+  private static class LeftRightFlingListener extends SimpleOnGestureListener {
+    Runnable mLeftRunnable;
+    Runnable mRightRunnable;
+
+    @Override
+    public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
+      if (Math.abs(event1.getY() - event2.getY()) > SWIPE_MAX_OFF_PATH) {
+        return false;
+      }
+      if (event1.getX() - event2.getX() > SWIPE_MIN_DISTANCE
+          && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
+        if (mRightRunnable != null) {
+          mRightRunnable.run();
+        }
+      } else if (event2.getX() - event1.getX() > SWIPE_MIN_DISTANCE
+          && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
+        if (mLeftRunnable != null) {
+          mLeftRunnable.run();
+        }
+      } else {
+        return super.onFling(event1, event2, velocityX, velocityY);
+      }
+      return true;
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/ScriptListAdapter.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/ScriptListAdapter.java
new file mode 100644
index 0000000..d1e6f65
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/ScriptListAdapter.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.io.File;
+import java.util.List;
+
+public abstract class ScriptListAdapter extends BaseAdapter {
+
+  protected final Context mContext;
+  protected final LayoutInflater mInflater;
+
+  public ScriptListAdapter(Context context) {
+    mContext = context;
+    mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+  }
+
+  @Override
+  public int getCount() {
+    return getScriptList().size();
+  }
+
+  @Override
+  public Object getItem(int position) {
+    return getScriptList().get(position);
+  }
+
+  @Override
+  public long getItemId(int position) {
+    return position;
+  }
+
+  @Override
+  public View getView(int position, View convertView, ViewGroup parent) {
+    LinearLayout container;
+    File script = getScriptList().get(position);
+
+    if (convertView == null) {
+      container = (LinearLayout) mInflater.inflate(R.layout.list_item, null);
+    } else {
+      container = (LinearLayout) convertView;
+    }
+
+    ImageView icon = (ImageView) container.findViewById(R.id.list_item_icon);
+    int resourceId;
+    if (script.isDirectory()) {
+      resourceId = R.drawable.folder;
+    } else {
+      resourceId = FeaturedInterpreters.getInterpreterIcon(mContext, script.getName());
+      if (resourceId == 0) {
+        resourceId = R.drawable.sl4a_logo_32;
+      }
+    }
+    icon.setImageResource(resourceId);
+
+    TextView text = (TextView) container.findViewById(R.id.list_item_title);
+    text.setText(getScriptList().get(position).getName());
+    return container;
+  }
+
+  protected abstract List<File> getScriptList();
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/Sl4aApplication.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/Sl4aApplication.java
new file mode 100644
index 0000000..953c6aa
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/Sl4aApplication.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+public class Sl4aApplication extends BaseApplication {
+
+  @Override
+  public void onCreate() {
+    super.onCreate();
+  }
+
+  @Override
+  public void onTerminate() {
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ApiBrowser.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ApiBrowser.java
new file mode 100644
index 0000000..8255313
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ApiBrowser.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.ListActivity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.database.MatrixCursor;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+import android.widget.AlphabetIndexer;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.facade.FacadeConfiguration;
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.language.SupportedLanguages;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+import com.googlecode.android_scripting.rpc.ParameterDescriptor;
+import com.googlecode.android_scripting.rpc.RpcDeprecated;
+import com.googlecode.android_scripting.rpc.RpcMinSdk;
+
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class ApiBrowser extends ListActivity {
+
+  private boolean searchResultMode = false;
+
+  private static enum RequestCode {
+    RPC_PROMPT
+  }
+
+  private static enum MenuId {
+    EXPAND_ALL, COLLAPSE_ALL, SEARCH;
+    public int getId() {
+      return ordinal() + Menu.FIRST;
+    }
+  }
+
+  private static enum ContextMenuId {
+    INSERT_TEXT, PROMPT_PARAMETERS, HELP;
+    public int getId() {
+      return ordinal() + Menu.FIRST;
+    }
+  }
+
+  private List<MethodDescriptor> mMethodDescriptors;
+  private Set<Integer> mExpandedPositions;
+  private ApiBrowserAdapter mAdapter;
+  private boolean mIsLanguageSupported;
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    CustomizeWindow.requestCustomTitle(this, "API Browser", R.layout.api_browser);
+    getListView().setFastScrollEnabled(true);
+    mExpandedPositions = new HashSet<Integer>();
+    updateAndFilterMethodDescriptors(null);
+    String scriptName = getIntent().getStringExtra(Constants.EXTRA_SCRIPT_PATH);
+    mIsLanguageSupported = SupportedLanguages.checkLanguageSupported(scriptName);
+    mAdapter = new ApiBrowserAdapter();
+    setListAdapter(mAdapter);
+    registerForContextMenu(getListView());
+    setResult(RESULT_CANCELED);
+  }
+
+  private void updateAndFilterMethodDescriptors(final String query) {
+    mMethodDescriptors =
+        Lists.newArrayList(Collections2.filter(FacadeConfiguration.collectMethodDescriptors(),
+            new Predicate<MethodDescriptor>() {
+              @Override
+              public boolean apply(MethodDescriptor descriptor) {
+                Method method = descriptor.getMethod();
+                if (method.isAnnotationPresent(RpcDeprecated.class)) {
+                  return false;
+                } else if (method.isAnnotationPresent(RpcMinSdk.class)) {
+                  int requiredSdkLevel = method.getAnnotation(RpcMinSdk.class).value();
+                  if (FacadeConfiguration.getSdkLevel() < requiredSdkLevel) {
+                    return false;
+                  }
+                }
+                if (query == null) {
+                  return true;
+                }
+                return descriptor.getName().toLowerCase().contains(query.toLowerCase());
+              }
+            }));
+  }
+
+  @Override
+  protected void onNewIntent(Intent intent) {
+    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+      searchResultMode = true;
+      final String query = intent.getStringExtra(SearchManager.QUERY);
+      ((TextView) findViewById(R.id.left_text)).setText(query);
+      updateAndFilterMethodDescriptors(query);
+      if (mMethodDescriptors.size() == 1) {
+        mExpandedPositions.add(0);
+      } else {
+        mExpandedPositions.clear();
+      }
+      mAdapter.notifyDataSetChanged();
+    }
+  }
+
+  @Override
+  public boolean onKeyDown(int keyCode, KeyEvent event) {
+    if (keyCode == KeyEvent.KEYCODE_BACK && searchResultMode) {
+      searchResultMode = false;
+      mExpandedPositions.clear();
+      ((TextView) findViewById(R.id.left_text)).setText("API Browser");
+      updateAndFilterMethodDescriptors("");
+      mAdapter.notifyDataSetChanged();
+      return true;
+    }
+    return super.onKeyDown(keyCode, event);
+  }
+
+  @Override
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    super.onPrepareOptionsMenu(menu);
+    menu.clear();
+    menu.add(Menu.NONE, MenuId.EXPAND_ALL.getId(), Menu.NONE, "Expand All").setIcon(
+        android.R.drawable.ic_menu_add);
+    menu.add(Menu.NONE, MenuId.COLLAPSE_ALL.getId(), Menu.NONE, "Collapse All").setIcon(
+        android.R.drawable.ic_menu_close_clear_cancel);
+    menu.add(Menu.NONE, MenuId.SEARCH.getId(), Menu.NONE, "Search").setIcon(
+        R.drawable.ic_menu_search);
+    return true;
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    super.onOptionsItemSelected(item);
+    int itemId = item.getItemId();
+    if (itemId == MenuId.EXPAND_ALL.getId()) {
+      for (int i = 0; i < mMethodDescriptors.size(); i++) {
+        mExpandedPositions.add(i);
+      }
+    } else if (itemId == MenuId.COLLAPSE_ALL.getId()) {
+      mExpandedPositions.clear();
+    } else if (itemId == MenuId.SEARCH.getId()) {
+      onSearchRequested();
+    }
+
+    mAdapter.notifyDataSetInvalidated();
+    return true;
+  }
+
+  @Override
+  protected void onListItemClick(ListView l, View v, int position, long id) {
+    if (mExpandedPositions.contains(position)) {
+      mExpandedPositions.remove(position);
+    } else {
+      mExpandedPositions.add(position);
+    }
+    mAdapter.notifyDataSetInvalidated();
+  }
+
+  @Override
+  public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+    if (!mIsLanguageSupported) {
+      return;
+    }
+    menu.add(Menu.NONE, ContextMenuId.INSERT_TEXT.getId(), Menu.NONE, "Insert");
+    menu.add(Menu.NONE, ContextMenuId.PROMPT_PARAMETERS.getId(), Menu.NONE, "Prompt");
+  }
+
+  @Override
+  public boolean onContextItemSelected(MenuItem item) {
+    AdapterView.AdapterContextMenuInfo info;
+    try {
+      info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+    } catch (ClassCastException e) {
+      Log.e("Bad menuInfo", e);
+      return false;
+    }
+
+    MethodDescriptor rpc = (MethodDescriptor) getListAdapter().getItem(info.position);
+    if (rpc == null) {
+      Log.v("No RPC selected.");
+      return false;
+    }
+
+    if (item.getItemId() == ContextMenuId.INSERT_TEXT.getId()) {
+      // There's no activity to track calls to insert (like there is for prompt) so we track it
+      // here instead.
+      insertText(rpc, new String[0]);
+    } else if (item.getItemId() == ContextMenuId.PROMPT_PARAMETERS.getId()) {
+      Intent intent = new Intent(this, ApiPrompt.class);
+      intent.putExtra(Constants.EXTRA_API_PROMPT_RPC_NAME, rpc.getName());
+      ParameterDescriptor[] parameters = rpc.getParameterValues(new String[0]);
+      String[] values = new String[parameters.length];
+      int index = 0;
+      for (ParameterDescriptor parameter : parameters) {
+        values[index++] = parameter.getValue();
+      }
+      intent.putExtra(Constants.EXTRA_API_PROMPT_VALUES, values);
+      startActivityForResult(intent, RequestCode.RPC_PROMPT.ordinal());
+    } else if (item.getItemId() == ContextMenuId.HELP.getId()) {
+      String help = rpc.getDeclaringClass().getSimpleName() + ".html#" + rpc.getName();
+
+    }
+    return true;
+  }
+
+  @Override
+  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+    super.onActivityResult(requestCode, resultCode, data);
+    RequestCode request = RequestCode.values()[requestCode];
+    if (resultCode == RESULT_OK) {
+      switch (request) {
+      case RPC_PROMPT:
+        MethodDescriptor rpc =
+            FacadeConfiguration.getMethodDescriptor(data
+                .getStringExtra(Constants.EXTRA_API_PROMPT_RPC_NAME));
+        String[] values = data.getStringArrayExtra(Constants.EXTRA_API_PROMPT_VALUES);
+        insertText(rpc, values);
+        break;
+      default:
+        break;
+      }
+    } else {
+      switch (request) {
+      case RPC_PROMPT:
+        break;
+      default:
+        break;
+      }
+    }
+  }
+
+  private void insertText(MethodDescriptor rpc, String[] values) {
+    String scriptText = getIntent().getStringExtra(Constants.EXTRA_SCRIPT_TEXT);
+    InterpreterConfiguration config =
+        ((BaseApplication) getApplication()).getInterpreterConfiguration();
+
+    Interpreter interpreter =
+        config.getInterpreterByName(getIntent().getStringExtra(Constants.EXTRA_INTERPRETER_NAME));
+    String rpcHelpText = interpreter.getRpcText(scriptText, rpc, values);
+
+    Intent intent = new Intent();
+    intent.putExtra(Constants.EXTRA_RPC_HELP_TEXT, rpcHelpText);
+    setResult(RESULT_OK, intent);
+    finish();
+  }
+
+  private class ApiBrowserAdapter extends BaseAdapter implements SectionIndexer {
+
+    private final AlphabetIndexer mIndexer;
+    private final MatrixCursor mCursor;
+
+    public ApiBrowserAdapter() {
+      mCursor = new MatrixCursor(new String[] { "NAME" });
+      for (MethodDescriptor info : mMethodDescriptors) {
+        mCursor.addRow(new String[] { info.getName() });
+      }
+      mIndexer = new AlphabetIndexer(mCursor, 0, " ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+    }
+
+    @Override
+    public int getCount() {
+      return mMethodDescriptors.size();
+    }
+
+    @Override
+    public Object getItem(int position) {
+      return mMethodDescriptors.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+      return position;
+    }
+
+    @Override
+    public View getView(final int position, View convertView, ViewGroup parent) {
+      TextView view;
+      if (convertView == null) {
+        view = new TextView(ApiBrowser.this);
+      } else {
+        view = (TextView) convertView;
+      }
+      view.setPadding(4, 4, 4, 4);
+      view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
+      if (mExpandedPositions.contains(position)) {
+        view.setText(mMethodDescriptors.get(position).getHelp());
+      } else {
+        view.setText(mMethodDescriptors.get(position).getName());
+      }
+      return view;
+    }
+
+    @Override
+    public int getPositionForSection(int section) {
+      return mIndexer.getPositionForSection(section);
+    }
+
+    @Override
+    public int getSectionForPosition(int position) {
+      return mIndexer.getSectionForPosition(position);
+    }
+
+    @Override
+    public Object[] getSections() {
+      return mIndexer.getSections();
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ApiPrompt.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ApiPrompt.java
new file mode 100644
index 0000000..e9655a2
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ApiPrompt.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.facade.FacadeConfiguration;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+
+/**
+ * Prompts for API parameters.
+ *
+ * <p>
+ * This activity is started by {@link ApiBrowser} to prompt user for RPC call parameters.
+ * Input/output interface is RPC name and explicit parameter values.
+ *
+ * @author igor.v.karp@gmail.com (Igor Karp)
+ */
+public class ApiPrompt extends Activity {
+  private MethodDescriptor mRpc;
+  private String[] mHints;
+  private String[] mValues;
+  private ApiPromptAdapter mAdapter;
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.api_prompt);
+    mRpc =
+        FacadeConfiguration.getMethodDescriptor(getIntent().getStringExtra(
+            Constants.EXTRA_API_PROMPT_RPC_NAME));
+    mHints = mRpc.getParameterHints();
+    mValues = getIntent().getStringArrayExtra(Constants.EXTRA_API_PROMPT_VALUES);
+    mAdapter = new ApiPromptAdapter();
+    ((ListView) findViewById(R.id.list)).setAdapter(mAdapter);
+    ((Button) findViewById(R.id.done)).setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        Intent intent = new Intent();
+        intent.putExtra(Constants.EXTRA_API_PROMPT_RPC_NAME, mRpc.getName());
+        intent.putExtra(Constants.EXTRA_API_PROMPT_VALUES, mValues);
+        setResult(RESULT_OK, intent);
+        finish();
+      }
+    });
+    setResult(RESULT_CANCELED);
+  }
+
+  private class ApiPromptAdapter extends BaseAdapter {
+    @Override
+    public int getCount() {
+      return mHints.length;
+    }
+
+    @Override
+    public Object getItem(int position) {
+      return mValues[position];
+    }
+
+    @Override
+    public long getItemId(int position) {
+      return position;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+      View item;
+      if (convertView == null) {
+        LayoutInflater inflater =
+            (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        item =
+            inflater.inflate(R.layout.api_prompt_item, parent, false /* do not attach to root */);
+      } else {
+        item = convertView;
+      }
+      TextView description = (TextView) item.findViewById(R.id.api_prompt_item_description);
+      EditText value = (EditText) item.findViewById(R.id.api_prompt_item_value);
+      description.setText(mHints[position]);
+      value.setText(mValues[position]);
+      value.addTextChangedListener(new ValueWatcher(position));
+      return item;
+    }
+  }
+
+  private class ValueWatcher implements TextWatcher {
+    private final int mPosition;
+
+    public ValueWatcher(int position) {
+      mPosition = position;
+    }
+
+    @Override
+    public void afterTextChanged(Editable e) {
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int before, int count) {
+    }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+      mValues[mPosition] = s.toString();
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/BluetoothDeviceList.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/BluetoothDeviceList.java
new file mode 100644
index 0000000..962f69f
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/BluetoothDeviceList.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.ListActivity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothDiscoveryHelper;
+import com.googlecode.android_scripting.facade.bluetooth.BluetoothDiscoveryHelper.BluetoothDiscoveryListener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BluetoothDeviceList extends ListActivity {
+
+  private static class DeviceInfo {
+    public final String mmName;
+    public final String mmAddress;
+
+    public DeviceInfo(String name, String address) {
+      mmName = name;
+      mmAddress = address;
+    }
+  }
+
+  private final DeviceListAdapter mAdapter = new DeviceListAdapter();
+  private final BluetoothDiscoveryHelper mBluetoothHelper =
+      new BluetoothDiscoveryHelper(this, mAdapter);
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    CustomizeWindow.requestCustomTitle(this, "Bluetooth Devices", R.layout.bluetooth_device_list);
+    setListAdapter(mAdapter);
+  }
+
+  @Override
+  protected void onStart() {
+    super.onStart();
+    CustomizeWindow.toggleProgressBarVisibility(this, true);
+    mBluetoothHelper.startDiscovery();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    mBluetoothHelper.cancel();
+  }
+
+  @Override
+  protected void onListItemClick(android.widget.ListView l, View v, int position, long id) {
+    DeviceInfo device = (DeviceInfo) mAdapter.getItem(position);
+    final Intent result = new Intent();
+    result.putExtra(Constants.EXTRA_DEVICE_ADDRESS, device.mmAddress);
+    setResult(RESULT_OK, result);
+    finish();
+  };
+
+  private class DeviceListAdapter extends BaseAdapter implements BluetoothDiscoveryListener {
+    List<DeviceInfo> mmDeviceList;
+
+    public DeviceListAdapter() {
+      mmDeviceList = new ArrayList<DeviceInfo>();
+    }
+
+    @Override
+    public void addDevice(String name, String address) {
+      mmDeviceList.add(new DeviceInfo(name, address));
+      notifyDataSetChanged();
+    }
+
+    @Override
+    public int getCount() {
+      return mmDeviceList.size();
+    }
+
+    @Override
+    public Object getItem(int position) {
+      return mmDeviceList.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+      return position;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup viewGroup) {
+      final DeviceInfo device = mmDeviceList.get(position);
+      final TextView view = new TextView(BluetoothDeviceList.this);
+      view.setPadding(2, 2, 2, 2);
+      view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);
+      view.setText(device.mmName + " (" + device.mmAddress + ")");
+      return view;
+    }
+
+    @Override
+    public void addBondedDevice(String name, String address) {
+      addDevice(name, address);
+    }
+
+    @Override
+    public void scanDone() {
+      CustomizeWindow.toggleProgressBarVisibility(BluetoothDeviceList.this, false);
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/CustomizeWindow.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/CustomizeWindow.java
new file mode 100644
index 0000000..897748f
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/CustomizeWindow.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.Activity;
+import android.view.View;
+import android.view.Window;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.Version;
+
+public class CustomizeWindow {
+  private CustomizeWindow() {
+    // Utility class.
+  }
+
+  public static void requestCustomTitle(Activity activity, String title, int contentViewLayoutResId) {
+    //b/26218264
+    //activity.requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
+    //activity.setContentView(contentViewLayoutResId);
+    //activity.getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.title);
+    //((TextView) activity.findViewById(R.id.left_text)).setText(title);
+    //((TextView) activity.findViewById(R.id.right_text)).setText("SL4A r"
+    //    + Version.getVersion(activity));
+  }
+
+  public static void toggleProgressBarVisibility(Activity activity, boolean on) {
+    ((ProgressBar) activity.findViewById(R.id.progress_bar)).setVisibility(on ? View.VISIBLE
+        : View.GONE);
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/InterpreterManager.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/InterpreterManager.java
new file mode 100644
index 0000000..dcba601
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/InterpreterManager.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.DataSetObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.googlecode.android_scripting.ActivityFlinger;
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.FeaturedInterpreters;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration.ConfigurationObserver;
+import com.googlecode.android_scripting.interpreter.html.HtmlInterpreter;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+public class InterpreterManager extends ListActivity {
+
+  private InterpreterManagerAdapter mAdapter;
+  private InterpreterListObserver mObserver;
+  private List<Interpreter> mInterpreters;
+  private List<String> mFeaturedInterpreters;
+  private InterpreterConfiguration mConfiguration;
+  private SharedPreferences mPreferences;
+
+  private static enum MenuId {
+    HELP, ADD, NETWORK, PREFERENCES;
+    public int getId() {
+      return ordinal() + Menu.FIRST;
+    }
+  }
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    CustomizeWindow.requestCustomTitle(this, "Interpreters", R.layout.interpreter_manager);
+    mConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration();
+    mInterpreters = new ArrayList<Interpreter>();
+    mAdapter = new InterpreterManagerAdapter();
+    mObserver = new InterpreterListObserver();
+    mAdapter.registerDataSetObserver(mObserver);
+    setListAdapter(mAdapter);
+    ActivityFlinger.attachView(getListView(), this);
+    ActivityFlinger.attachView(getWindow().getDecorView(), this);
+    mFeaturedInterpreters = FeaturedInterpreters.getList();
+    mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+  }
+
+  @Override
+  public void onStart() {
+    super.onStart();
+    mConfiguration.registerObserver(mObserver);
+    mAdapter.notifyDataSetInvalidated();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    mAdapter.notifyDataSetInvalidated();
+  }
+
+  @Override
+  public void onStop() {
+    super.onStop();
+    mConfiguration.unregisterObserver(mObserver);
+  }
+
+  @Override
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    menu.clear();
+    buildInstallLanguagesMenu(menu);
+    menu.add(Menu.NONE, MenuId.NETWORK.getId(), Menu.NONE, "Start Server").setIcon(
+        android.R.drawable.ic_menu_share);
+    menu.add(Menu.NONE, MenuId.PREFERENCES.getId(), Menu.NONE, "Preferences").setIcon(
+        android.R.drawable.ic_menu_preferences);
+    return super.onPrepareOptionsMenu(menu);
+  }
+
+  private void buildInstallLanguagesMenu(Menu menu) {
+    SubMenu installMenu =
+        menu.addSubMenu(Menu.NONE, MenuId.ADD.getId(), Menu.NONE, "Add").setIcon(
+            android.R.drawable.ic_menu_add);
+    int i = MenuId.values().length + Menu.FIRST;
+    for (String interpreterName : mFeaturedInterpreters) {
+      installMenu.add(Menu.NONE, i++, Menu.NONE, interpreterName);
+    }
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    int itemId = item.getItemId();
+    if (itemId == MenuId.NETWORK.getId()) {
+      AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+      dialog.setItems(new CharSequence[] { "Public", "Private" }, new OnClickListener() {
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+          launchService(which == 0 /* usePublicIp */);
+        }
+      });
+      dialog.show();
+    } else if (itemId == MenuId.PREFERENCES.getId()) {
+      startActivity(new Intent(this, Preferences.class));
+    } else if (itemId >= MenuId.values().length + Menu.FIRST) {
+      int i = itemId - MenuId.values().length - Menu.FIRST;
+      if (i < mFeaturedInterpreters.size()) {
+        URL url = FeaturedInterpreters.getUrlForName(mFeaturedInterpreters.get(i));
+        Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString()));
+        startActivity(viewIntent);
+      }
+    }
+    return true;
+  }
+
+  private int getPrefInt(String key, int defaultValue) {
+    int result = defaultValue;
+    String value = mPreferences.getString(key, null);
+    if (value != null) {
+      try {
+        result = Integer.parseInt(value);
+      } catch (NumberFormatException e) {
+        result = defaultValue;
+      }
+    }
+    return result;
+  }
+
+  private void launchService(boolean usePublicIp) {
+    Intent intent = new Intent(this, ScriptingLayerService.class);
+    intent.setAction(Constants.ACTION_LAUNCH_SERVER);
+    intent.putExtra(Constants.EXTRA_USE_EXTERNAL_IP, usePublicIp);
+    intent.putExtra(Constants.EXTRA_USE_SERVICE_PORT, getPrefInt("use_service_port", 0));
+    startService(intent);
+  }
+
+  private void launchTerminal(Interpreter interpreter) {
+    if (interpreter instanceof HtmlInterpreter) {
+      return;
+    }
+    Intent intent = new Intent(this, ScriptingLayerService.class);
+    intent.setAction(Constants.ACTION_LAUNCH_INTERPRETER);
+    intent.putExtra(Constants.EXTRA_INTERPRETER_NAME, interpreter.getName());
+    startService(intent);
+  }
+
+  @Override
+  protected void onListItemClick(ListView list, View view, int position, long id) {
+    Interpreter interpreter = (Interpreter) list.getItemAtPosition(position);
+    launchTerminal(interpreter);
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    mConfiguration.unregisterObserver(mObserver);
+  }
+
+  private class InterpreterListObserver extends DataSetObserver implements ConfigurationObserver {
+    @Override
+    public void onInvalidated() {
+      mInterpreters = mConfiguration.getInteractiveInterpreters();
+    }
+
+    @Override
+    public void onChanged() {
+      mInterpreters = mConfiguration.getInteractiveInterpreters();
+    }
+
+    @Override
+    public void onConfigurationChanged() {
+      runOnUiThread(new Runnable() {
+        @Override
+        public void run() {
+          mAdapter.notifyDataSetChanged();
+        }
+      });
+    }
+  }
+
+  private class InterpreterManagerAdapter extends BaseAdapter {
+
+    @Override
+    public int getCount() {
+      return mInterpreters.size();
+    }
+
+    @Override
+    public Object getItem(int position) {
+      return mInterpreters.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+      return position;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+      LinearLayout container;
+
+      Interpreter interpreter = mInterpreters.get(position);
+
+      if (convertView == null) {
+        LayoutInflater inflater =
+            (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        container = (LinearLayout) inflater.inflate(R.layout.list_item, null);
+      } else {
+        container = (LinearLayout) convertView;
+      }
+      ImageView img = (ImageView) container.findViewById(R.id.list_item_icon);
+
+      int imgId =
+          FeaturedInterpreters.getInterpreterIcon(InterpreterManager.this,
+              interpreter.getExtension());
+      if (imgId == 0) {
+        imgId = R.drawable.sl4a_logo_32;
+      }
+
+      img.setImageResource(imgId);
+
+      TextView text = (TextView) container.findViewById(R.id.list_item_title);
+
+      text.setText(interpreter.getNiceName());
+      return container;
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/InterpreterPicker.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/InterpreterPicker.java
new file mode 100644
index 0000000..79145d5
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/InterpreterPicker.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.FeaturedInterpreters;
+import com.googlecode.android_scripting.IntentBuilders;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration.ConfigurationObserver;
+
+import java.util.List;
+
+/**
+ * Presents available scripts and returns the selected one.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class InterpreterPicker extends ListActivity {
+
+  private List<Interpreter> mInterpreters;
+  private InterpreterPickerAdapter mAdapter;
+  private InterpreterConfiguration mConfiguration;
+  private ScriptListObserver mObserver;
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    CustomizeWindow.requestCustomTitle(this, "Interpreters", R.layout.script_manager);
+    mObserver = new ScriptListObserver();
+    mConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration();
+    mInterpreters = mConfiguration.getInteractiveInterpreters();
+    mConfiguration.registerObserver(mObserver);
+    mAdapter = new InterpreterPickerAdapter();
+    mAdapter.registerDataSetObserver(mObserver);
+    setListAdapter(mAdapter);
+  }
+
+  @Override
+  protected void onListItemClick(ListView list, View view, int position, long id) {
+    final Interpreter interpreter = (Interpreter) list.getItemAtPosition(position);
+    if (Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) {
+      int icon =
+          FeaturedInterpreters.getInterpreterIcon(InterpreterPicker.this, interpreter
+              .getExtension());
+      if (icon == 0) {
+        icon = R.drawable.sl4a_logo_48;
+      }
+      Parcelable iconResource =
+          Intent.ShortcutIconResource.fromContext(InterpreterPicker.this, icon);
+      Intent intent = IntentBuilders.buildInterpreterShortcutIntent(interpreter, iconResource);
+      setResult(RESULT_OK, intent);
+    }
+    finish();
+  }
+
+  @Override
+  protected void onDestroy() {
+    super.onDestroy();
+    mConfiguration.unregisterObserver(mObserver);
+  }
+
+  private class ScriptListObserver extends DataSetObserver implements ConfigurationObserver {
+    @Override
+    public void onInvalidated() {
+      mInterpreters = mConfiguration.getInteractiveInterpreters();
+    }
+
+    @Override
+    public void onConfigurationChanged() {
+      mAdapter.notifyDataSetInvalidated();
+    }
+  }
+
+  private class InterpreterPickerAdapter extends BaseAdapter {
+
+    @Override
+    public int getCount() {
+      return mInterpreters.size();
+    }
+
+    @Override
+    public Object getItem(int position) {
+      return mInterpreters.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+      return position;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+      LinearLayout container;
+
+      Interpreter interpreter = mInterpreters.get(position);
+
+      if (convertView == null) {
+        LayoutInflater inflater =
+            (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        container = (LinearLayout) inflater.inflate(R.layout.list_item, null);
+      } else {
+        container = (LinearLayout) convertView;
+      }
+      ImageView img = (ImageView) container.findViewById(R.id.list_item_icon);
+
+      int imgId =
+          FeaturedInterpreters.getInterpreterIcon(InterpreterPicker.this, interpreter
+              .getExtension());
+      if (imgId == 0) {
+        imgId = R.drawable.sl4a_logo_32;
+      }
+
+      img.setImageResource(imgId);
+
+      TextView text = (TextView) container.findViewById(R.id.list_item_title);
+
+      text.setText(interpreter.getNiceName());
+      return container;
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/LogcatViewer.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/LogcatViewer.java
new file mode 100644
index 0000000..7025c23
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/LogcatViewer.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.ClipboardManager;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.googlecode.android_scripting.ActivityFlinger;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.Process;
+import com.googlecode.android_scripting.R;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.LinkedList;
+import java.util.List;
+
+public class LogcatViewer extends ListActivity {
+
+  private List<String> mLogcatMessages;
+  private int mOldLastPosition;
+  private LogcatViewerAdapter mAdapter;
+  private Process mLogcatProcess;
+
+  private static enum MenuId {
+    HELP, PREFERENCES, JUMP_TO_BOTTOM, SHARE, COPY;
+    public int getId() {
+      return ordinal() + Menu.FIRST;
+    }
+  }
+
+  private class LogcatWatcher implements Runnable {
+    @Override
+    public void run() {
+      mLogcatProcess = new Process();
+      mLogcatProcess.setBinary(new File("/system/bin/logcat"));
+      mLogcatProcess.start(null);
+      try {
+        BufferedReader br = new BufferedReader(new InputStreamReader(mLogcatProcess.getIn()));
+        while (true) {
+          final String line = br.readLine();
+          if (line == null) {
+            break;
+          }
+          runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+              mLogcatMessages.add(line);
+              mAdapter.notifyDataSetInvalidated();
+              // This logic performs what transcriptMode="normal" should do. Since that doesn't seem
+              // to work, we do it this way.
+              int lastVisiblePosition = getListView().getLastVisiblePosition();
+              int lastPosition = mLogcatMessages.size() - 1;
+              if (lastVisiblePosition == mOldLastPosition || lastVisiblePosition == -1) {
+                getListView().setSelection(lastPosition);
+              }
+              mOldLastPosition = lastPosition;
+            }
+          });
+        }
+      } catch (IOException e) {
+        Log.e("Failed to read from logcat process.", e);
+      } finally {
+        mLogcatProcess.kill();
+      }
+    }
+  }
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    CustomizeWindow.requestCustomTitle(this, "Logcat", R.layout.logcat_viewer);
+    mLogcatMessages = new LinkedList<String>();
+    mOldLastPosition = 0;
+    mAdapter = new LogcatViewerAdapter();
+    setListAdapter(mAdapter);
+    ActivityFlinger.attachView(getListView(), this);
+    ActivityFlinger.attachView(getWindow().getDecorView(), this);
+  }
+
+  @Override
+  public boolean onCreateOptionsMenu(Menu menu) {
+    menu.add(Menu.NONE, MenuId.PREFERENCES.getId(), Menu.NONE, "Preferences").setIcon(
+        android.R.drawable.ic_menu_preferences);
+    menu.add(Menu.NONE, MenuId.JUMP_TO_BOTTOM.getId(), Menu.NONE, "Jump to Bottom").setIcon(
+        android.R.drawable.ic_menu_revert);
+    menu.add(Menu.NONE, MenuId.SHARE.getId(), Menu.NONE, "Share").setIcon(
+        android.R.drawable.ic_menu_share);
+    menu.add(Menu.NONE, MenuId.COPY.getId(), Menu.NONE, "Copy").setIcon(
+        android.R.drawable.ic_menu_edit);
+    return super.onCreateOptionsMenu(menu);
+  }
+
+  private String getAsString() {
+    StringBuilder builder = new StringBuilder();
+    for (String message : mLogcatMessages) {
+      builder.append(message + "\n");
+    }
+    return builder.toString();
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    int itemId = item.getItemId();
+    if (itemId == MenuId.JUMP_TO_BOTTOM.getId()) {
+      getListView().setSelection(mLogcatMessages.size() - 1);
+    } else if (itemId == MenuId.PREFERENCES.getId()) {
+      startActivity(new Intent(this, Preferences.class));
+    } else if (itemId == MenuId.SHARE.getId()) {
+      Intent intent = new Intent(Intent.ACTION_SEND);
+      intent.putExtra(Intent.EXTRA_TEXT, getAsString().toString());
+      intent.putExtra(Intent.EXTRA_SUBJECT, "Logcat Dump");
+      intent.setType("text/plain");
+      startActivity(Intent.createChooser(intent, "Send Logcat to:"));
+    } else if (itemId == MenuId.COPY.getId()) {
+      ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+      clipboard.setText(getAsString());
+      Toast.makeText(this, "Copied to clipboard", Toast.LENGTH_SHORT).show();
+    }
+    return super.onOptionsItemSelected(item);
+  }
+
+  @Override
+  protected void onStart() {
+    mLogcatMessages.clear();
+    Thread logcatWatcher = new Thread(new LogcatWatcher());
+    logcatWatcher.setPriority(Thread.NORM_PRIORITY - 1);
+    logcatWatcher.start();
+    mAdapter.notifyDataSetInvalidated();
+    super.onStart();
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    mLogcatProcess.kill();
+  }
+
+  private class LogcatViewerAdapter extends BaseAdapter {
+
+    @Override
+    public int getCount() {
+      return mLogcatMessages.size();
+    }
+
+    @Override
+    public Object getItem(int position) {
+      return mLogcatMessages.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+      return position;
+    }
+
+    @Override
+    public View getView(final int position, View convertView, ViewGroup parent) {
+      TextView view = new TextView(LogcatViewer.this);
+      view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
+      view.setText(mLogcatMessages.get(position));
+      return view;
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/Preferences.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/Preferences.java
new file mode 100644
index 0000000..5197d9e
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/Preferences.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+import com.googlecode.android_scripting.R;
+
+public class Preferences extends PreferenceActivity {
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    // Load the preferences from an XML resource
+    addPreferencesFromResource(R.xml.preferences);
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptEditor.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptEditor.java
new file mode 100644
index 0000000..b83720e
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptEditor.java
@@ -0,0 +1,621 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.Selection;
+import android.text.Spanned;
+import android.text.TextWatcher;
+import android.text.style.UnderlineSpan;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.FileUtils;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.ScriptStorageAdapter;
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Vector;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A text editor for scripts.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class ScriptEditor extends Activity implements OnClickListener {
+  private static final int DIALOG_FIND_REPLACE = 2;
+  private static final int DIALOG_LINE = 1;
+  private EditText mNameText;
+  private EditText mContentText;
+  private boolean mScheduleMoveLeft;
+  private String mLastSavedContent;
+  private SharedPreferences mPreferences;
+  private InterpreterConfiguration mConfiguration;
+  private ContentTextWatcher mWatcher;
+  private EditHistory mHistory;
+  private File mScript;
+  private EditText mLineNo;
+
+  private boolean mIsUndoOrRedo = false;
+  private boolean mEnableAutoClose;
+  private boolean mAutoIndent;
+
+  private EditText mSearchFind;
+  private EditText mSearchReplace;
+  private CheckBox mSearchCase;
+  private CheckBox mSearchWord;
+  private CheckBox mSearchAll;
+  private CheckBox mSearchStart;
+
+  private static enum MenuId {
+    SAVE, SAVE_AND_RUN, PREFERENCES, API_BROWSER, HELP, SHARE, GOTO, SEARCH;
+    public int getId() {
+      return ordinal() + Menu.FIRST;
+    }
+  }
+
+  private static enum RequestCode {
+    RPC_HELP
+  }
+
+  private int readIntPref(String key, int defaultValue, int maxValue) {
+    int val;
+    try {
+      val = Integer.parseInt(mPreferences.getString(key, Integer.toString(defaultValue)));
+    } catch (NumberFormatException e) {
+      val = defaultValue;
+    }
+    val = Math.max(0, Math.min(val, maxValue));
+    return val;
+  }
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.script_editor);
+    mNameText = (EditText) findViewById(R.id.script_editor_title);
+    mContentText = (EditText) findViewById(R.id.script_editor_body);
+    mHistory = new EditHistory();
+    mWatcher = new ContentTextWatcher(mHistory);
+    mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+    updatePreferences();
+
+    mScript = new File(getIntent().getStringExtra(Constants.EXTRA_SCRIPT_PATH));
+    mNameText.setText(mScript.getName());
+    mNameText.setSelected(true);
+    // NOTE: This appears to be the only way to get Android to put the cursor to the beginning of
+    // the EditText field.
+    mNameText.setSelection(1);
+    mNameText.extendSelection(0);
+    mNameText.setSelection(0);
+    mLastSavedContent = getIntent().getStringExtra(Constants.EXTRA_SCRIPT_CONTENT);
+    if (mLastSavedContent == null) {
+      try {
+        mLastSavedContent = FileUtils.readToString(mScript);
+      } catch (IOException e) {
+        Log.e("Failed to read script.", e);
+        mLastSavedContent = "";
+      } finally {
+      }
+    }
+
+    mContentText.setText(mLastSavedContent);
+    InputFilter[] oldFilters = mContentText.getFilters();
+    List<InputFilter> filters = new ArrayList<InputFilter>(oldFilters.length + 1);
+    filters.addAll(Arrays.asList(oldFilters));
+    filters.add(new ContentInputFilter());
+    mContentText.setFilters(filters.toArray(oldFilters));
+    mContentText.addTextChangedListener(mWatcher);
+    mConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration();
+    // Disables volume key beep.
+    setVolumeControlStream(AudioManager.STREAM_MUSIC);
+    mLineNo = new EditText(this);
+    mLineNo.setInputType(InputType.TYPE_CLASS_NUMBER);
+    int lastLocation = mPreferences.getInt("lasteditpos." + mScript, -1);
+    if (lastLocation >= 0) {
+      mContentText.requestFocus();
+      mContentText.setSelection(lastLocation);
+    }
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    updatePreferences();
+  }
+
+  private void updatePreferences() {
+    mContentText.setTextSize(readIntPref("editor_fontsize", 10, 30));
+    mEnableAutoClose = mPreferences.getBoolean("enableAutoClose", true);
+    mAutoIndent = mPreferences.getBoolean("editor_auto_indent", false);
+    mContentText.setHorizontallyScrolling(mPreferences.getBoolean("editor_no_wrap", false));
+  }
+
+  @Override
+  public boolean onCreateOptionsMenu(Menu menu) {
+    super.onCreateOptionsMenu(menu);
+    menu.add(0, MenuId.SAVE.getId(), 0, "Save & Exit").setIcon(android.R.drawable.ic_menu_save);
+    menu.add(0, MenuId.SAVE_AND_RUN.getId(), 0, "Save & Run").setIcon(
+        android.R.drawable.ic_media_play);
+    menu.add(0, MenuId.PREFERENCES.getId(), 0, "Preferences").setIcon(
+        android.R.drawable.ic_menu_preferences);
+    menu.add(0, MenuId.API_BROWSER.getId(), 0, "API Browser").setIcon(
+        android.R.drawable.ic_menu_info_details);
+    menu.add(0, MenuId.SHARE.getId(), 0, "Share").setIcon(android.R.drawable.ic_menu_share);
+    menu.add(0, MenuId.GOTO.getId(), 0, "GoTo").setIcon(android.R.drawable.ic_menu_directions);
+    menu.add(0, MenuId.SEARCH.getId(), 0, "Find").setIcon(android.R.drawable.ic_menu_search);
+    return true;
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    if (item.getItemId() == MenuId.SAVE.getId()) {
+      save();
+      finish();
+    } else if (item.getItemId() == MenuId.SAVE_AND_RUN.getId()) {
+      save();
+      Interpreter interpreter =
+          mConfiguration.getInterpreterForScript(mNameText.getText().toString());
+      if (interpreter != null) { // We may be editing an unknown type.
+        Intent intent = new Intent(this, ScriptingLayerService.class);
+        intent.setAction(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT);
+        intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mScript.getAbsolutePath());
+        startService(intent);
+      } else {
+        // TODO(damonkohler): Should remove menu option.
+        Toast.makeText(this, "Can't run this type.", Toast.LENGTH_SHORT).show();
+      }
+      finish();
+    } else if (item.getItemId() == MenuId.PREFERENCES.getId()) {
+      startActivity(new Intent(this, Preferences.class));
+    } else if (item.getItemId() == MenuId.API_BROWSER.getId()) {
+      Intent intent = new Intent(this, ApiBrowser.class);
+      intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mNameText.getText().toString());
+      intent.putExtra(Constants.EXTRA_INTERPRETER_NAME,
+          mConfiguration.getInterpreterForScript(mNameText.getText().toString()).getName());
+      intent.putExtra(Constants.EXTRA_SCRIPT_TEXT, mContentText.getText().toString());
+      startActivityForResult(intent, RequestCode.RPC_HELP.ordinal());
+    } else if (item.getItemId() == MenuId.SHARE.getId()) {
+      Intent intent = new Intent(Intent.ACTION_SEND);
+      intent.putExtra(Intent.EXTRA_TEXT, mContentText.getText().toString());
+      intent.putExtra(Intent.EXTRA_SUBJECT, "Share " + mNameText.getText().toString());
+      intent.setType("text/plain");
+      startActivity(Intent.createChooser(intent, "Send Script to:"));
+    } else if (item.getItemId() == MenuId.GOTO.getId()) {
+      showDialog(DIALOG_LINE);
+    } else if (item.getItemId() == MenuId.SEARCH.getId()) {
+      showDialog(DIALOG_FIND_REPLACE);
+    }
+    return super.onOptionsItemSelected(item);
+  }
+
+  @Override
+  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+    super.onActivityResult(requestCode, resultCode, data);
+    RequestCode request = RequestCode.values()[requestCode];
+
+    if (resultCode == RESULT_OK) {
+      switch (request) {
+      case RPC_HELP:
+        String rpcText = data.getStringExtra(Constants.EXTRA_RPC_HELP_TEXT);
+        insertContent(rpcText);
+        break;
+      default:
+        break;
+      }
+    } else {
+      switch (request) {
+      case RPC_HELP:
+        break;
+      default:
+        break;
+      }
+    }
+  }
+
+  private void save() {
+    int start = mContentText.getSelectionStart();
+    mLastSavedContent = mContentText.getText().toString();
+    mScript = new File(mScript.getParent(), mNameText.getText().toString());
+    ScriptStorageAdapter.writeScript(mScript, mLastSavedContent);
+    Toast.makeText(this, "Saved " + mNameText.getText().toString(), Toast.LENGTH_SHORT).show();
+    Editor e = mPreferences.edit();
+    e.putInt("lasteditpos." + mScript, start);
+    e.commit();
+  }
+
+  private void insertContent(String text) {
+    int selectionStart = Math.min(mContentText.getSelectionStart(), mContentText.getSelectionEnd());
+    int selectionEnd = Math.max(mContentText.getSelectionStart(), mContentText.getSelectionEnd());
+    mContentText.getEditableText().replace(selectionStart, selectionEnd, text);
+  }
+
+  @Override
+  public boolean onKeyDown(int keyCode, KeyEvent event) {
+    if (keyCode == KeyEvent.KEYCODE_BACK && hasContentChanged()) {
+      AlertDialog.Builder alert = new AlertDialog.Builder(this);
+      setVolumeControlStream(AudioManager.STREAM_MUSIC);
+      alert.setCancelable(false);
+      alert.setTitle("Confirm exit");
+      alert.setMessage("Would you like to save?");
+      alert.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
+        public void onClick(DialogInterface dialog, int whichButton) {
+          save();
+          finish();
+        }
+      });
+      alert.setNegativeButton("No", new DialogInterface.OnClickListener() {
+        public void onClick(DialogInterface dialog, int whichButton) {
+          finish();
+        }
+      });
+      alert.setNeutralButton("Cancel", new DialogInterface.OnClickListener() {
+        public void onClick(DialogInterface dialog, int whichButton) {
+        }
+      });
+      alert.show();
+      return true;
+    } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+      redo();
+      return true;
+    } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
+      undo();
+      return true;
+    } else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
+      showDialog(DIALOG_FIND_REPLACE);
+      return true;
+    } else {
+      return super.onKeyDown(keyCode, event);
+    }
+  }
+
+  @Override
+  protected Dialog onCreateDialog(int id, Bundle args) {
+    AlertDialog.Builder b = new AlertDialog.Builder(this);
+    if (id == DIALOG_LINE) {
+      b.setTitle("Goto Line");
+      b.setView(mLineNo);
+      b.setPositiveButton("Ok", new OnClickListener() {
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+          gotoLine(Integer.parseInt(mLineNo.getText().toString()));
+        }
+      });
+      b.setNegativeButton("Cancel", null);
+      return b.create();
+    } else if (id == DIALOG_FIND_REPLACE) {
+      View v = getLayoutInflater().inflate(R.layout.findreplace, null);
+      mSearchFind = (EditText) v.findViewById(R.id.searchFind);
+      mSearchReplace = (EditText) v.findViewById(R.id.searchReplace);
+      mSearchAll = (CheckBox) v.findViewById(R.id.searchAll);
+      mSearchCase = (CheckBox) v.findViewById(R.id.searchCase);
+      mSearchStart = (CheckBox) v.findViewById(R.id.searchStart);
+      mSearchWord = (CheckBox) v.findViewById(R.id.searchWord);
+      b.setTitle("Search and Replace");
+      b.setView(v);
+      b.setPositiveButton("Find", this);
+      b.setNeutralButton("Next", this);
+      b.setNegativeButton("Replace", this);
+      return b.create();
+    }
+
+    return super.onCreateDialog(id, args);
+  }
+
+  @Override
+  protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
+    if (id == DIALOG_LINE) {
+      mLineNo.setText(String.valueOf(getLineNo()));
+    } else if (id == DIALOG_FIND_REPLACE) {
+      mSearchStart.setChecked(false);
+    }
+    super.onPrepareDialog(id, dialog, args);
+  }
+
+  protected int getLineNo() {
+    int pos = mContentText.getSelectionStart();
+    String text = mContentText.getText().toString();
+    int i = 0;
+    int n = 1;
+    while (i < pos) {
+      int j = text.indexOf("\n", i);
+      if (j < 0) {
+        break;
+      }
+      i = j + 1;
+      if (i < pos) {
+        n += 1;
+      }
+    }
+    return n;
+  }
+
+  protected void gotoLine(int line) {
+    String text = mContentText.getText().toString();
+    if (text.length() < 1) {
+      return;
+    }
+    int i = 0;
+    int n = 1;
+    while (i < text.length() && n < line) {
+      int j = text.indexOf("\n", i);
+      if (j < 0) {
+        break;
+      }
+      i = j + 1;
+      n += 1;
+    }
+    mContentText.setSelection(Math.min(text.length() - 1, i));
+  }
+
+  @Override
+  protected void onUserLeaveHint() {
+    if (hasContentChanged()) {
+      save();
+    }
+  }
+
+  private boolean hasContentChanged() {
+    return !mLastSavedContent.equals(mContentText.getText().toString());
+  }
+
+  private final class ContentInputFilter implements InputFilter {
+    @Override
+    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
+        int dend) {
+      if (end - start == 1) {
+        Interpreter ip = mConfiguration.getInterpreterForScript(mNameText.getText().toString());
+        String auto = null;
+        if (ip != null && mEnableAutoClose) {
+          auto = ip.getLanguage().autoClose(source.charAt(start));
+        }
+        // Auto indent code?
+        if (auto == null && source.charAt(start) == '\n' && mAutoIndent) {
+          int i = dstart - 1;
+          int spaces = 0;
+          while ((i >= 0) && dest.charAt(i) != '\n') {
+            i -= 1; // Find start of line.
+          }
+          i += 1;
+          while (i < dest.length() && dest.charAt(i++) == ' ') {
+            spaces += 1;
+          }
+          if (spaces > 0) {
+            return String.format("\n%" + spaces + "s", " ");
+          }
+        }
+        if (auto != null) {
+          mScheduleMoveLeft = true;
+          return auto;
+        }
+      }
+      return null;
+    }
+  }
+
+  private final class ContentTextWatcher implements TextWatcher {
+    private final EditHistory mmEditHistory;
+    private CharSequence mmBeforeChange;
+    private CharSequence mmAfterChange;
+
+    private ContentTextWatcher(EditHistory history) {
+      mmEditHistory = history;
+    }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+      if (!mIsUndoOrRedo) {
+        mmAfterChange = s.subSequence(start, start + count);
+        mmEditHistory.add(new EditItem(start, mmBeforeChange, mmAfterChange));
+      }
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+      if (!mIsUndoOrRedo) {
+        mmBeforeChange = s.subSequence(start, start + count);
+      }
+    }
+
+    @Override
+    public void afterTextChanged(Editable s) {
+      if (mScheduleMoveLeft) {
+        mScheduleMoveLeft = false;
+        Selection.moveLeft(mContentText.getText(), mContentText.getLayout());
+      }
+    }
+  }
+
+  /**
+   * Keeps track of all the edit history of a text.
+   */
+  private final class EditHistory {
+    int mmPosition = 0;
+    private final Vector<EditItem> mmHistory = new Vector<EditItem>();
+
+    /**
+     * Adds a new edit operation to the history at the current position. If executed after a call to
+     * getPrevious() removes all the future history (elements with positions >= current history
+     * position).
+     *
+     */
+    private void add(EditItem item) {
+      mmHistory.setSize(mmPosition);
+      mmHistory.add(item);
+      mmPosition++;
+    }
+
+    /**
+     * Traverses the history backward by one position, returns and item at that position.
+     */
+    private EditItem getPrevious() {
+      if (mmPosition == 0) {
+        return null;
+      }
+      mmPosition--;
+      return mmHistory.get(mmPosition);
+    }
+
+    /**
+     * Traverses the history forward by one position, returns and item at that position.
+     */
+    private EditItem getNext() {
+      if (mmPosition == mmHistory.size()) {
+        return null;
+      }
+      EditItem item = mmHistory.get(mmPosition);
+      mmPosition++;
+      return item;
+    }
+  }
+
+  /**
+   * Represents a single edit operation.
+   */
+  private final class EditItem {
+    private final int mmIndex;
+    private final CharSequence mmBefore;
+    private final CharSequence mmAfter;
+
+    /**
+     * Constructs EditItem of a modification that was applied at position start and replaced
+     * CharSequence before with CharSequence after.
+     */
+    public EditItem(int start, CharSequence before, CharSequence after) {
+      mmIndex = start;
+      mmBefore = before;
+      mmAfter = after;
+    }
+  }
+
+  private void undo() {
+    EditItem edit = mHistory.getPrevious();
+    if (edit == null) {
+      return;
+    }
+    Editable text = mContentText.getText();
+    int start = edit.mmIndex;
+    int end = start + (edit.mmAfter != null ? edit.mmAfter.length() : 0);
+    mIsUndoOrRedo = true;
+    text.replace(start, end, edit.mmBefore);
+    mIsUndoOrRedo = false;
+    // This will get rid of underlines inserted when editor tries to come up with a suggestion.
+    for (Object o : text.getSpans(0, text.length(), UnderlineSpan.class)) {
+      text.removeSpan(o);
+    }
+    Selection.setSelection(text, edit.mmBefore == null ? start : (start + edit.mmBefore.length()));
+  }
+
+  private void redo() {
+    EditItem edit = mHistory.getNext();
+    if (edit == null) {
+      return;
+    }
+    Editable text = mContentText.getText();
+    int start = edit.mmIndex;
+    int end = start + (edit.mmBefore != null ? edit.mmBefore.length() : 0);
+    mIsUndoOrRedo = true;
+    text.replace(start, end, edit.mmAfter);
+    mIsUndoOrRedo = false;
+    for (Object o : text.getSpans(0, text.length(), UnderlineSpan.class)) {
+      text.removeSpan(o);
+    }
+    Selection.setSelection(text, edit.mmAfter == null ? start : (start + edit.mmAfter.length()));
+  }
+
+  @Override
+  public void onClick(DialogInterface dialog, int which) {
+    int start = mContentText.getSelectionStart();
+    int end = mContentText.getSelectionEnd();
+    String original = mContentText.getText().toString();
+    if (start == end || which != AlertDialog.BUTTON_NEGATIVE) {
+      end = original.length();
+    }
+    if (which == AlertDialog.BUTTON_NEUTRAL) {
+      start += 1;
+    }
+    if (mSearchStart.isChecked()) {
+      start = 0;
+      end = original.length();
+    }
+    String findText = mSearchFind.getText().toString();
+    String replaceText = mSearchReplace.getText().toString();
+    String search = Pattern.quote(findText);
+    int flags = 0;
+    if (!mSearchCase.isChecked()) {
+      flags |= Pattern.CASE_INSENSITIVE;
+    }
+    if (mSearchWord.isChecked()) {
+      search = "\\b" + search + "\\b";
+    }
+    Pattern p = Pattern.compile(search, flags);
+    Matcher m = p.matcher(original);
+    m.region(start, end);
+    if (!m.find()) {
+      Toast.makeText(this, "Search not found.", Toast.LENGTH_SHORT).show();
+      return;
+    }
+    int foundpos = m.start();
+    if (which != AlertDialog.BUTTON_NEGATIVE) { // Find
+      mContentText.setSelection(foundpos, foundpos + findText.length());
+    } else { // Replace
+      String s;
+      // Seems to be a bug in the android 2.2 implementation of replace... regions not returning
+      // whole string.
+      m = p.matcher(original.substring(start, end));
+      String replace = Matcher.quoteReplacement(replaceText);
+      if (mSearchAll.isChecked()) {
+        s = m.replaceAll(replace);
+      } else {
+        s = m.replaceFirst(replace);
+      }
+      mContentText.setText(original.substring(0, start) + s + original.substring(end));
+      mContentText.setSelection(foundpos, foundpos + replaceText.length());
+    }
+    mContentText.requestFocus();
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptManager.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptManager.java
new file mode 100644
index 0000000..9cc7c97
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptManager.java
@@ -0,0 +1,579 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.app.SearchManager;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.DataSetObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import com.googlecode.android_scripting.ActivityFlinger;
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.FileUtils;
+import com.googlecode.android_scripting.IntentBuilders;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.ScriptListAdapter;
+import com.googlecode.android_scripting.ScriptStorageAdapter;
+import com.googlecode.android_scripting.facade.FacadeConfiguration;
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration.ConfigurationObserver;
+import com.googlecode.android_scripting.interpreter.InterpreterConstants;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * Manages creation, deletion, and execution of stored scripts.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class ScriptManager extends ListActivity {
+
+  private final static String EMPTY = "";
+
+  private List<File> mScripts;
+  private ScriptManagerAdapter mAdapter;
+  private SharedPreferences mPreferences;
+  private HashMap<Integer, Interpreter> mAddMenuIds;
+  private ScriptListObserver mObserver;
+  private InterpreterConfiguration mConfiguration;
+  private SearchManager mManager;
+  private boolean mInSearchResultMode = false;
+  private String mQuery = EMPTY;
+  private File mCurrentDir;
+  private final File mBaseDir = new File(InterpreterConstants.SCRIPTS_ROOT);
+  private final Handler mHandler = new Handler();
+  private File mCurrent;
+
+  private static enum RequestCode {
+    INSTALL_INTERPETER, QRCODE_ADD
+  }
+
+  private static enum MenuId {
+    DELETE, HELP, FOLDER_ADD, QRCODE_ADD, INTERPRETER_MANAGER, PREFERENCES, LOGCAT_VIEWER,
+    TRIGGER_MANAGER, REFRESH, SEARCH, RENAME, EXTERNAL;
+    public int getId() {
+      return ordinal() + Menu.FIRST;
+    }
+  }
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    CustomizeWindow.requestCustomTitle(this, "Scripts", R.layout.script_manager);
+    if (FileUtils.externalStorageMounted()) {
+      File sl4a = mBaseDir.getParentFile();
+      if (!sl4a.exists()) {
+        sl4a.mkdir();
+        try {
+          FileUtils.chmod(sl4a, 0755); // Handle the sl4a parent folder first.
+        } catch (Exception e) {
+          // Not much we can do here if it doesn't work.
+        }
+      }
+      if (!FileUtils.makeDirectories(mBaseDir, 0755)) {
+        new AlertDialog.Builder(this)
+            .setTitle("Error")
+            .setMessage(
+                "Failed to create scripts directory.\n" + mBaseDir + "\n"
+                    + "Please check the permissions of your external storage media.")
+            .setIcon(android.R.drawable.ic_dialog_alert).setPositiveButton("Ok", null).show();
+      }
+    } else {
+      new AlertDialog.Builder(this).setTitle("External Storage Unavilable")
+          .setMessage("Scripts will be unavailable as long as external storage is unavailable.")
+          .setIcon(android.R.drawable.ic_dialog_alert).setPositiveButton("Ok", null).show();
+    }
+
+    mCurrentDir = mBaseDir;
+    mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+    mAdapter = new ScriptManagerAdapter(this);
+    mObserver = new ScriptListObserver();
+    mAdapter.registerDataSetObserver(mObserver);
+    mConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration();
+    mManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+
+    registerForContextMenu(getListView());
+    updateAndFilterScriptList(mQuery);
+    setListAdapter(mAdapter);
+    ActivityFlinger.attachView(getListView(), this);
+    ActivityFlinger.attachView(getWindow().getDecorView(), this);
+    startService(IntentBuilders.buildTriggerServiceIntent());
+    handleIntent(getIntent());
+  }
+
+  @Override
+  protected void onNewIntent(Intent intent) {
+    handleIntent(intent);
+  }
+
+  @SuppressWarnings("serial")
+  private void updateAndFilterScriptList(final String query) {
+    List<File> scripts;
+    if (mPreferences.getBoolean("show_all_files", false)) {
+      scripts = ScriptStorageAdapter.listAllScripts(mCurrentDir);
+    } else {
+      scripts = ScriptStorageAdapter.listExecutableScripts(mCurrentDir, mConfiguration);
+    }
+    mScripts = Lists.newArrayList(Collections2.filter(scripts, new Predicate<File>() {
+      @Override
+      public boolean apply(File file) {
+        return file.getName().toLowerCase().contains(query.toLowerCase());
+      }
+    }));
+
+    // TODO(tturney): Add a text view that shows the queried text.
+    synchronized (mQuery) {
+      if (!mQuery.equals(query)) {
+        if (query != null || !query.equals(EMPTY)) {
+          mQuery = query;
+        }
+      }
+    }
+
+    if ((mScripts.size() == 0) && findViewById(android.R.id.empty) != null) {
+      ((TextView) findViewById(android.R.id.empty)).setText("No matches found.");
+    }
+
+    // TODO(damonkohler): Extending the File class here seems odd.
+    if (!mCurrentDir.equals(mBaseDir)) {
+      mScripts.add(0, new File(mCurrentDir.getParent()) {
+        @Override
+        public boolean isDirectory() {
+          return true;
+        }
+
+        @Override
+        public String getName() {
+          return "..";
+        }
+      });
+    }
+  }
+
+  private void handleIntent(Intent intent) {
+    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+      mInSearchResultMode = true;
+      String query = intent.getStringExtra(SearchManager.QUERY);
+      updateAndFilterScriptList(query);
+      mAdapter.notifyDataSetChanged();
+    }
+  }
+
+  @Override
+  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+    menu.add(Menu.NONE, MenuId.RENAME.getId(), Menu.NONE, "Rename");
+    menu.add(Menu.NONE, MenuId.DELETE.getId(), Menu.NONE, "Delete");
+  }
+
+  @Override
+  public boolean onContextItemSelected(MenuItem item) {
+    AdapterView.AdapterContextMenuInfo info;
+    try {
+      info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+    } catch (ClassCastException e) {
+      Log.e("Bad menuInfo", e);
+      return false;
+    }
+    File file = (File) mAdapter.getItem(info.position);
+    int itemId = item.getItemId();
+    if (itemId == MenuId.DELETE.getId()) {
+      delete(file);
+      return true;
+    } else if (itemId == MenuId.RENAME.getId()) {
+      rename(file);
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public boolean onKeyDown(int keyCode, KeyEvent event) {
+    if (keyCode == KeyEvent.KEYCODE_BACK && mInSearchResultMode) {
+      mInSearchResultMode = false;
+      mAdapter.notifyDataSetInvalidated();
+      return true;
+    }
+    return super.onKeyDown(keyCode, event);
+  }
+
+  @Override
+  public void onStop() {
+    super.onStop();
+    mConfiguration.unregisterObserver(mObserver);
+  }
+
+  @Override
+  public void onStart() {
+    super.onStart();
+    mConfiguration.registerObserver(mObserver);
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    if (!mInSearchResultMode && findViewById(android.R.id.empty) != null) {
+      ((TextView) findViewById(android.R.id.empty)).setText(R.string.no_scripts_message);
+    }
+    updateAndFilterScriptList(mQuery);
+    mAdapter.notifyDataSetChanged();
+  }
+
+  @Override
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    super.onPrepareOptionsMenu(menu);
+    menu.clear();
+    buildMenuIdMaps();
+    buildAddMenu(menu);
+    buildSwitchActivityMenu(menu);
+    menu.add(Menu.NONE, MenuId.SEARCH.getId(), Menu.NONE, "Search").setIcon(
+        R.drawable.ic_menu_search);
+    menu.add(Menu.NONE, MenuId.PREFERENCES.getId(), Menu.NONE, "Preferences").setIcon(
+        android.R.drawable.ic_menu_preferences);
+    menu.add(Menu.NONE, MenuId.REFRESH.getId(), Menu.NONE, "Refresh").setIcon(
+        R.drawable.ic_menu_refresh);
+    return true;
+  }
+
+  private void buildSwitchActivityMenu(Menu menu) {
+    Menu subMenu =
+        menu.addSubMenu(Menu.NONE, Menu.NONE, Menu.NONE, "View").setIcon(
+            android.R.drawable.ic_menu_more);
+    subMenu.add(Menu.NONE, MenuId.INTERPRETER_MANAGER.getId(), Menu.NONE, "Interpreters");
+    subMenu.add(Menu.NONE, MenuId.TRIGGER_MANAGER.getId(), Menu.NONE, "Triggers");
+    subMenu.add(Menu.NONE, MenuId.LOGCAT_VIEWER.getId(), Menu.NONE, "Logcat");
+  }
+
+  private void buildMenuIdMaps() {
+    mAddMenuIds = new LinkedHashMap<Integer, Interpreter>();
+    int i = MenuId.values().length + Menu.FIRST;
+    List<Interpreter> installed = mConfiguration.getInstalledInterpreters();
+    Collections.sort(installed, new Comparator<Interpreter>() {
+      @Override
+      public int compare(Interpreter interpreterA, Interpreter interpreterB) {
+        return interpreterA.getNiceName().compareTo(interpreterB.getNiceName());
+      }
+    });
+    for (Interpreter interpreter : installed) {
+      mAddMenuIds.put(i, interpreter);
+      ++i;
+    }
+  }
+
+  private void buildAddMenu(Menu menu) {
+    Menu addMenu =
+        menu.addSubMenu(Menu.NONE, Menu.NONE, Menu.NONE, "Add").setIcon(
+            android.R.drawable.ic_menu_add);
+    addMenu.add(Menu.NONE, MenuId.FOLDER_ADD.getId(), Menu.NONE, "Folder");
+    for (Entry<Integer, Interpreter> entry : mAddMenuIds.entrySet()) {
+      addMenu.add(Menu.NONE, entry.getKey(), Menu.NONE, entry.getValue().getNiceName());
+    }
+    addMenu.add(Menu.NONE, MenuId.QRCODE_ADD.getId(), Menu.NONE, "Scan Barcode");
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    int itemId = item.getItemId();
+    if (itemId == MenuId.INTERPRETER_MANAGER.getId()) {
+      // Show interpreter manger.
+      Intent i = new Intent(this, InterpreterManager.class);
+      startActivity(i);
+    } else if (mAddMenuIds.containsKey(itemId)) {
+      // Add a new script.
+      Intent intent = new Intent(Constants.ACTION_EDIT_SCRIPT);
+      Interpreter interpreter = mAddMenuIds.get(itemId);
+      intent.putExtra(Constants.EXTRA_SCRIPT_PATH,
+          new File(mCurrentDir.getPath(), interpreter.getExtension()).getPath());
+      intent.putExtra(Constants.EXTRA_SCRIPT_CONTENT, interpreter.getContentTemplate());
+      intent.putExtra(Constants.EXTRA_IS_NEW_SCRIPT, true);
+      startActivity(intent);
+      synchronized (mQuery) {
+        mQuery = EMPTY;
+      }
+    } else if (itemId == MenuId.QRCODE_ADD.getId()) {
+      try {
+        Intent intent = new Intent("com.google.zxing.client.android.SCAN");
+        startActivityForResult(intent, RequestCode.QRCODE_ADD.ordinal());
+      }catch(ActivityNotFoundException e) {
+        Log.e("No handler found to Scan a QR Code!", e);
+      }
+    } else if (itemId == MenuId.FOLDER_ADD.getId()) {
+      addFolder();
+    } else if (itemId == MenuId.PREFERENCES.getId()) {
+      startActivity(new Intent(this, Preferences.class));
+    } else if (itemId == MenuId.TRIGGER_MANAGER.getId()) {
+      startActivity(new Intent(this, TriggerManager.class));
+    } else if (itemId == MenuId.LOGCAT_VIEWER.getId()) {
+      startActivity(new Intent(this, LogcatViewer.class));
+    } else if (itemId == MenuId.REFRESH.getId()) {
+      updateAndFilterScriptList(mQuery);
+      mAdapter.notifyDataSetChanged();
+    } else if (itemId == MenuId.SEARCH.getId()) {
+      onSearchRequested();
+    }
+    return true;
+  }
+
+  @Override
+  protected void onListItemClick(ListView list, View view, int position, long id) {
+    final File file = (File) list.getItemAtPosition(position);
+    mCurrent = file;
+    if (file.isDirectory()) {
+      mCurrentDir = file;
+      mAdapter.notifyDataSetInvalidated();
+      return;
+    }
+    doDialogMenu();
+    return;
+  }
+
+  // Quickedit chokes on sdk 3 or below, and some Android builds. Provides alternative menu.
+  private void doDialogMenu() {
+    AlertDialog.Builder builder = new AlertDialog.Builder(this);
+    final CharSequence[] menuList =
+        { "Run Foreground", "Run Background", "Edit", "Delete", "Rename" };
+    builder.setTitle(mCurrent.getName());
+    builder.setItems(menuList, new DialogInterface.OnClickListener() {
+
+      @Override
+      public void onClick(DialogInterface dialog, int which) {
+        Intent intent;
+        switch (which) {
+        case 0:
+          intent = new Intent(ScriptManager.this, ScriptingLayerService.class);
+          intent.setAction(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT);
+          intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mCurrent.getPath());
+          startService(intent);
+          break;
+        case 1:
+          intent = new Intent(ScriptManager.this, ScriptingLayerService.class);
+          intent.setAction(Constants.ACTION_LAUNCH_BACKGROUND_SCRIPT);
+          intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mCurrent.getPath());
+          startService(intent);
+          break;
+        case 2:
+          editScript(mCurrent);
+          break;
+        case 3:
+          delete(mCurrent);
+          break;
+        case 4:
+          rename(mCurrent);
+          break;
+        }
+      }
+    });
+    builder.show();
+  }
+
+  /**
+   * Opens the script for editing.
+   *
+   * @param script
+   *          the name of the script to edit
+   */
+  private void editScript(File script) {
+    Intent i = new Intent(Constants.ACTION_EDIT_SCRIPT);
+    i.putExtra(Constants.EXTRA_SCRIPT_PATH, script.getAbsolutePath());
+    startActivity(i);
+  }
+
+  private void delete(final File file) {
+    AlertDialog.Builder alert = new AlertDialog.Builder(this);
+    alert.setTitle("Delete");
+    alert.setMessage("Would you like to delete " + file.getName() + "?");
+    alert.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
+      public void onClick(DialogInterface dialog, int whichButton) {
+        FileUtils.delete(file);
+        mScripts.remove(file);
+        mAdapter.notifyDataSetChanged();
+      }
+    });
+    alert.setNegativeButton("No", new DialogInterface.OnClickListener() {
+      public void onClick(DialogInterface dialog, int whichButton) {
+        // Ignore.
+      }
+    });
+    alert.show();
+  }
+
+  private void addFolder() {
+    final EditText folderName = new EditText(this);
+    folderName.setHint("Folder Name");
+    AlertDialog.Builder alert = new AlertDialog.Builder(this);
+    alert.setTitle("Add Folder");
+    alert.setView(folderName);
+    alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
+      public void onClick(DialogInterface dialog, int whichButton) {
+        String name = folderName.getText().toString();
+        if (name.length() == 0) {
+          Log.e(ScriptManager.this, "Folder name is empty.");
+          return;
+        } else {
+          for (File f : mScripts) {
+            if (f.getName().equals(name)) {
+              Log.e(ScriptManager.this, String.format("Folder \"%s\" already exists.", name));
+              return;
+            }
+          }
+        }
+        File dir = new File(mCurrentDir, name);
+        if (!FileUtils.makeDirectories(dir, 0755)) {
+          Log.e(ScriptManager.this, String.format("Cannot create folder \"%s\".", name));
+        }
+        mAdapter.notifyDataSetInvalidated();
+      }
+    });
+    alert.show();
+  }
+
+  private void rename(final File file) {
+    final EditText newName = new EditText(this);
+    newName.setText(file.getName());
+    AlertDialog.Builder alert = new AlertDialog.Builder(this);
+    alert.setTitle("Rename");
+    alert.setView(newName);
+    alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
+      public void onClick(DialogInterface dialog, int whichButton) {
+        String name = newName.getText().toString();
+        if (name.length() == 0) {
+          Log.e(ScriptManager.this, "Name is empty.");
+          return;
+        } else {
+          for (File f : mScripts) {
+            if (f.getName().equals(name)) {
+              Log.e(ScriptManager.this, String.format("\"%s\" already exists.", name));
+              return;
+            }
+          }
+        }
+        if (!FileUtils.rename(file, name)) {
+          throw new RuntimeException(String.format("Cannot rename \"%s\".", file.getPath()));
+        }
+        mAdapter.notifyDataSetInvalidated();
+      }
+    });
+    alert.show();
+  }
+
+  @Override
+  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+    RequestCode request = RequestCode.values()[requestCode];
+    if (resultCode == RESULT_OK) {
+      switch (request) {
+      case QRCODE_ADD:
+        writeScriptFromBarcode(data);
+        break;
+      default:
+        break;
+      }
+    } else {
+      switch (request) {
+      case QRCODE_ADD:
+        break;
+      default:
+        break;
+      }
+    }
+    mAdapter.notifyDataSetInvalidated();
+  }
+
+  private void writeScriptFromBarcode(Intent data) {
+    String result = data.getStringExtra("SCAN_RESULT");
+    if (result == null) {
+      Log.e(this, "Invalid QR code content.");
+      return;
+    }
+    String contents[] = result.split("\n", 2);
+    if (contents.length != 2) {
+      Log.e(this, "Invalid QR code content.");
+      return;
+    }
+    String title = contents[0];
+    String body = contents[1];
+    File script = new File(mCurrentDir, title);
+    ScriptStorageAdapter.writeScript(script, body);
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    mConfiguration.unregisterObserver(mObserver);
+    mManager.setOnCancelListener(null);
+  }
+
+  private class ScriptListObserver extends DataSetObserver implements ConfigurationObserver {
+    @Override
+    public void onInvalidated() {
+      updateAndFilterScriptList(EMPTY);
+    }
+
+    @Override
+    public void onConfigurationChanged() {
+      runOnUiThread(new Runnable() {
+        @Override
+        public void run() {
+          updateAndFilterScriptList(mQuery);
+          mAdapter.notifyDataSetChanged();
+        }
+      });
+    }
+  }
+
+  private class ScriptManagerAdapter extends ScriptListAdapter {
+    public ScriptManagerAdapter(Context context) {
+      super(context);
+    }
+
+    @Override
+    protected List<File> getScriptList() {
+      return mScripts;
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptPicker.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptPicker.java
new file mode 100644
index 0000000..9385382
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptPicker.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ListView;
+
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.FeaturedInterpreters;
+import com.googlecode.android_scripting.IntentBuilders;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.ScriptListAdapter;
+import com.googlecode.android_scripting.ScriptStorageAdapter;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.interpreter.InterpreterConstants;
+
+import java.io.File;
+import java.util.List;
+
+
+/**
+ * Presents available scripts and returns the selected one.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class ScriptPicker extends ListActivity {
+
+  private List<File> mScripts;
+  private ScriptPickerAdapter mAdapter;
+  private InterpreterConfiguration mConfiguration;
+  private File mCurrentDir;
+  private final File mBaseDir = new File(InterpreterConstants.SCRIPTS_ROOT);
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    CustomizeWindow.requestCustomTitle(this, "Scripts", R.layout.script_manager);
+    mCurrentDir = mBaseDir;
+    mConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration();
+    mScripts = ScriptStorageAdapter.listExecutableScripts(null, mConfiguration);
+    mAdapter = new ScriptPickerAdapter(this);
+    mAdapter.registerDataSetObserver(new ScriptListObserver());
+    setListAdapter(mAdapter);
+  }
+
+  @Override
+  protected void onListItemClick(ListView list, View view, int position, long id) {
+    final File script = (File) list.getItemAtPosition(position);
+
+    if (script.isDirectory()) {
+      mCurrentDir = script;
+      mAdapter.notifyDataSetInvalidated();
+      return;
+    }
+
+    //TODO: Take action here based on item click
+  }
+
+  private class ScriptListObserver extends DataSetObserver {
+    @SuppressWarnings("serial")
+    @Override
+    public void onInvalidated() {
+      mScripts = ScriptStorageAdapter.listExecutableScripts(mCurrentDir, mConfiguration);
+      // TODO(damonkohler): Extending the File class here seems odd.
+      if (!mCurrentDir.equals(mBaseDir)) {
+        mScripts.add(0, new File(mCurrentDir.getParent()) {
+          @Override
+          public boolean isDirectory() {
+            return true;
+          }
+
+          @Override
+          public String getName() {
+            return "..";
+          }
+        });
+      }
+    }
+  }
+
+  private class ScriptPickerAdapter extends ScriptListAdapter {
+
+    public ScriptPickerAdapter(Context context) {
+      super(context);
+    }
+
+    @Override
+    protected List<File> getScriptList() {
+      return mScripts;
+    }
+
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptProcessMonitor.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptProcessMonitor.java
new file mode 100644
index 0000000..614890a
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptProcessMonitor.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.ListActivity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.interpreter.InterpreterProcess;
+
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.connectbot.ConsoleActivity;
+
+/**
+ * An activity that allows to monitor running scripts.
+ *
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public class ScriptProcessMonitor extends ListActivity {
+
+  private final static int UPDATE_INTERVAL_SECS = 1;
+
+  private final Timer mTimer = new Timer();
+
+  private volatile ScriptingLayerService mService;
+
+  private ScriptListAdapter mUpdater;
+  private List<InterpreterProcess> mProcessList;
+  private ScriptMonitorAdapter mAdapter;
+  private boolean mIsConnected = false;
+
+  private ServiceConnection mConnection = new ServiceConnection() {
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder service) {
+      mService = ((ScriptingLayerService.LocalBinder) service).getService();
+      mUpdater = new ScriptListAdapter();
+      mTimer.scheduleAtFixedRate(mUpdater, 0, UPDATE_INTERVAL_SECS * 1000);
+      mIsConnected = true;
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+      mService = null;
+      mIsConnected = false;
+      mProcessList = null;
+      mAdapter.notifyDataSetChanged();
+    }
+  };
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    bindService(new Intent(this, ScriptingLayerService.class), mConnection, 0);
+    CustomizeWindow.requestCustomTitle(this, "Script Monitor", R.layout.script_monitor);
+    mAdapter = new ScriptMonitorAdapter();
+    setListAdapter(mAdapter);
+    registerForContextMenu(getListView());
+  }
+
+  @Override
+  public void onPause() {
+    super.onPause();
+    if (mUpdater != null) {
+      mUpdater.cancel();
+    }
+    mTimer.purge();
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    if (mIsConnected) {
+      try {
+        mUpdater = new ScriptListAdapter();
+        mTimer.scheduleAtFixedRate(mUpdater, 0, UPDATE_INTERVAL_SECS * 1000);
+      } catch (IllegalStateException e) {
+        Log.e(e.getMessage(), e);
+      }
+    }
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    mTimer.cancel();
+    unbindService(mConnection);
+  }
+
+  @Override
+  protected void onListItemClick(ListView list, View view, int position, long id) {
+    final InterpreterProcess script = (InterpreterProcess) list.getItemAtPosition(position);
+    Intent intent = new Intent(this, ConsoleActivity.class);
+    intent.putExtra(Constants.EXTRA_PROXY_PORT, script.getPort());
+    startActivity(intent);
+  }
+
+  @Override
+  public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+    menu.add(Menu.NONE, 0, Menu.NONE, "Stop");
+  }
+
+  @Override
+  public boolean onContextItemSelected(MenuItem item) {
+    AdapterView.AdapterContextMenuInfo info;
+    try {
+      info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+    } catch (ClassCastException e) {
+      Log.e("Bad menuInfo", e);
+      return false;
+    }
+
+    InterpreterProcess script = mAdapter.getItem(info.position);
+    if (script == null) {
+      Log.v("No script selected.");
+      return false;
+    }
+
+    Intent intent = new Intent(ScriptProcessMonitor.this, ScriptingLayerService.class);
+    intent.setAction(Constants.ACTION_KILL_PROCESS);
+    intent.putExtra(Constants.EXTRA_PROXY_PORT, script.getPort());
+    startService(intent);
+
+    return true;
+  }
+
+  @Override
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    menu.clear();
+    // TODO(damonkohler): How could mProcessList ever be null?
+    if (mProcessList != null && !mProcessList.isEmpty()) {
+      menu.add(Menu.NONE, 0, Menu.NONE, R.string.stop_all).setIcon(
+          android.R.drawable.ic_menu_close_clear_cancel);
+    }
+    return super.onPrepareOptionsMenu(menu);
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    Intent intent = new Intent(this, ScriptingLayerService.class);
+    intent.setAction(Constants.ACTION_KILL_ALL);
+    startService(intent);
+    return true;
+  }
+
+  private class ScriptListAdapter extends TimerTask {
+    private int mmExpectedModCount = 0;
+    private volatile List<InterpreterProcess> mmList;
+
+    @Override
+    public void run() {
+      if (mService == null) {
+        mmList.clear();
+        mTimer.cancel();
+      } else {
+        int freshModCount = mService.getModCount();
+        if (freshModCount != mmExpectedModCount) {
+          mmExpectedModCount = freshModCount;
+          mmList = mService.getScriptProcessesList();
+        }
+      }
+      runOnUiThread(new Runnable() {
+        public void run() {
+          mProcessList = mUpdater.getFreshProcessList();
+          mAdapter.notifyDataSetChanged();
+        }
+      });
+    }
+
+    private List<InterpreterProcess> getFreshProcessList() {
+      return mmList;
+    }
+  }
+
+  private class ScriptMonitorAdapter extends BaseAdapter {
+
+    @Override
+    public int getCount() {
+      if (mProcessList == null) {
+        return 0;
+      }
+      return mProcessList.size();
+    }
+
+    @Override
+    public InterpreterProcess getItem(int position) {
+      return mProcessList.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+      return position;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+      View itemView;
+      if (convertView == null) {
+        LayoutInflater inflater =
+            (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        itemView = inflater.inflate(R.layout.script_monitor_list_item, parent, false);
+      } else {
+        itemView = convertView;
+      }
+      InterpreterProcess process = mProcessList.get(position);
+      ((TextView) itemView.findViewById(R.id.process_title)).setText(process.getName());
+      ((TextView) itemView.findViewById(R.id.process_age)).setText(process.getUptime());
+      ((TextView) itemView.findViewById(R.id.process_details)).setText(process.getHost() + ":"
+          + process.getPort());
+      ((TextView) itemView.findViewById(R.id.process_status)).setText("PID " + process.getPid());
+      return itemView;
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptingLayerService.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptingLayerService.java
new file mode 100644
index 0000000..b39c984
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptingLayerService.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Build;
+import android.os.StrictMode;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.googlecode.android_scripting.AndroidProxy;
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.ForegroundService;
+import com.googlecode.android_scripting.NotificationIdFactory;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.ScriptLauncher;
+import com.googlecode.android_scripting.ScriptProcess;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.interpreter.InterpreterProcess;
+import com.googlecode.android_scripting.interpreter.html.HtmlInterpreter;
+import com.googlecode.android_scripting.interpreter.shell.ShellInterpreter;
+
+import org.connectbot.ConsoleActivity;
+import org.connectbot.service.TerminalManager;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A service that allows scripts and the RPC server to run in the background.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class ScriptingLayerService extends ForegroundService {
+  private static final int NOTIFICATION_ID = NotificationIdFactory.create();
+
+  private final IBinder mBinder;
+  private final Map<Integer, InterpreterProcess> mProcessMap;
+  private final String LOG_TAG = "sl4a";
+  private volatile int mModCount = 0;
+  private NotificationManager mNotificationManager;
+  private Notification mNotification;
+  private PendingIntent mNotificationPendingIntent;
+  private InterpreterConfiguration mInterpreterConfiguration;
+
+  private volatile WeakReference<InterpreterProcess> mRecentlyKilledProcess;
+
+  private TerminalManager mTerminalManager;
+
+  private SharedPreferences mPreferences = null;
+  private boolean mHide;
+
+  public class LocalBinder extends Binder {
+    public ScriptingLayerService getService() {
+      return ScriptingLayerService.this;
+    }
+  }
+
+  @Override
+  public IBinder onBind(Intent intent) {
+    return mBinder;
+  }
+
+  public ScriptingLayerService() {
+    super(NOTIFICATION_ID);
+    mProcessMap = new ConcurrentHashMap<Integer, InterpreterProcess>();
+    mBinder = new LocalBinder();
+  }
+
+  @Override
+  public void onCreate() {
+    super.onCreate();
+    mInterpreterConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration();
+    mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+    mRecentlyKilledProcess = new WeakReference<InterpreterProcess>(null);
+    mTerminalManager = new TerminalManager(this);
+    mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+    mHide = mPreferences.getBoolean(Constants.HIDE_NOTIFY, false);
+  }
+
+  @Override
+  protected Notification createNotification() {
+    Intent notificationIntent = new Intent(this, ScriptingLayerService.class);
+    notificationIntent.setAction(Constants.ACTION_SHOW_RUNNING_SCRIPTS);
+    mNotificationPendingIntent = PendingIntent.getService(this, 0, notificationIntent, 0);
+
+    Notification.Builder builder = new Notification.Builder(this);
+    builder.setSmallIcon(R.drawable.sl4a_notification_logo)
+           .setTicker(null)
+           .setWhen(System.currentTimeMillis())
+           .setContentTitle("SL4A Service")
+           .setContentText("Tap to view running scripts")
+           .setContentIntent(mNotificationPendingIntent);
+    mNotification = builder.build();
+    mNotification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
+    return mNotification;
+  }
+
+  private void updateNotification(String tickerText) {
+    if (tickerText.equals(mNotification.tickerText)) {
+      // Consequent notifications with the same ticker-text are displayed without any ticker-text.
+      // This is a way around. Alternatively, we can display process name and port.
+      tickerText = tickerText + " ";
+    }
+    String msg;
+    if (mProcessMap.size() <= 1) {
+      msg = "Tap to view " + Integer.toString(mProcessMap.size()) + " running script";
+    } else {
+      msg = "Tap to view " + Integer.toString(mProcessMap.size()) + " running scripts";
+    }
+    Notification.Builder builder = new Notification.Builder(this);
+    builder.setContentTitle("SL4A Service")
+           .setContentText(msg)
+           .setContentIntent(mNotificationPendingIntent)
+           .setSmallIcon(mNotification.icon, mProcessMap.size())
+           .setWhen(mNotification.when)
+           .setTicker(tickerText);
+
+    mNotification = builder.build();
+    mNotificationManager.notify(NOTIFICATION_ID, mNotification);
+  }
+
+  @Override
+  public int onStartCommand(Intent intent, int flags, int startId) {
+    super.onStartCommand(intent, flags, startId);
+    String errmsg = null;
+    if (intent == null) {
+      return START_REDELIVER_INTENT;
+    }
+    if (intent.getAction().equals(Constants.ACTION_KILL_ALL)) {
+      killAll();
+      stopSelf(startId);
+      return START_REDELIVER_INTENT;
+    }
+
+    if (intent.getAction().equals(Constants.ACTION_KILL_PROCESS)) {
+      killProcess(intent);
+      if (mProcessMap.isEmpty()) {
+        stopSelf(startId);
+      }
+      return START_REDELIVER_INTENT;
+    }
+
+    if (intent.getAction().equals(Constants.ACTION_SHOW_RUNNING_SCRIPTS)) {
+      showRunningScripts();
+      return START_REDELIVER_INTENT;
+    }
+
+    String name = intent.getStringExtra(Constants.EXTRA_SCRIPT_PATH);
+    if (name != null && name.endsWith(HtmlInterpreter.HTML_EXTENSION)) {
+      if (Integer.valueOf(android.os.Build.VERSION.SDK) > 21) {
+          Log.e(LOG_TAG, "Starting a WebViewClient not permitted for your SDK Version.");
+      } else {
+          launchHtmlScript(intent);
+      }
+      if (mProcessMap.isEmpty()) {
+        stopSelf(startId);
+      }
+      return START_REDELIVER_INTENT;
+    }
+
+    //TODO: b/26538940 We need to go back to a strict policy and fix the problems
+    StrictMode.ThreadPolicy sl4aPolicy = new StrictMode.ThreadPolicy.Builder()
+        .detectAll()
+        .penaltyLog()
+        .build();
+
+    StrictMode.setThreadPolicy(sl4aPolicy);
+
+    AndroidProxy proxy = null;
+    InterpreterProcess interpreterProcess = null;
+    if (intent.getAction().equals(Constants.ACTION_LAUNCH_SERVER)) {
+      proxy = launchServer(intent, false);
+      // TODO(damonkohler): This is just to make things easier. Really, we shouldn't need to start
+      // an interpreter when all we want is a server.
+      interpreterProcess = new InterpreterProcess(new ShellInterpreter(), proxy);
+      interpreterProcess.setName("Server");
+    } else {
+      proxy = launchServer(intent, true);
+      if (intent.getAction().equals(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT)) {
+        launchTerminal(proxy.getAddress());
+        try {
+          interpreterProcess = launchScript(intent, proxy);
+        } catch (RuntimeException e) {
+          errmsg =
+              "Unable to run " + intent.getStringExtra(Constants.EXTRA_SCRIPT_PATH) + "\n"
+                  + e.getMessage();
+          interpreterProcess = null;
+        }
+      } else if (intent.getAction().equals(Constants.ACTION_LAUNCH_BACKGROUND_SCRIPT)) {
+        interpreterProcess = launchScript(intent, proxy);
+      } else if (intent.getAction().equals(Constants.ACTION_LAUNCH_INTERPRETER)) {
+        launchTerminal(proxy.getAddress());
+        interpreterProcess = launchInterpreter(intent, proxy);
+      }
+    }
+    if (errmsg != null) {
+      updateNotification(errmsg);
+    } else if (interpreterProcess == null) {
+      updateNotification("Action not implemented: " + intent.getAction());
+    } else {
+      addProcess(interpreterProcess);
+    }
+    return START_REDELIVER_INTENT;
+  }
+
+  private boolean tryPort(AndroidProxy androidProxy, boolean usePublicIp, int usePort) {
+    if (usePublicIp) {
+      return (androidProxy.startPublic(usePort) != null);
+    } else {
+      return (androidProxy.startLocal(usePort) != null);
+    }
+  }
+
+  private AndroidProxy launchServer(Intent intent, boolean requiresHandshake) {
+    AndroidProxy androidProxy = new AndroidProxy(this, intent, requiresHandshake);
+    boolean usePublicIp = intent.getBooleanExtra(Constants.EXTRA_USE_EXTERNAL_IP, false);
+    int usePort = intent.getIntExtra(Constants.EXTRA_USE_SERVICE_PORT, 0);
+    // If port is in use, fall back to default behaviour
+    if (!tryPort(androidProxy, usePublicIp, usePort)) {
+      if (usePort != 0) {
+        tryPort(androidProxy, usePublicIp, 0);
+      }
+    }
+    return androidProxy;
+  }
+
+  private void launchHtmlScript(Intent intent) {
+    File script = new File(intent.getStringExtra(Constants.EXTRA_SCRIPT_PATH));
+    ScriptLauncher.launchHtmlScript(script, this, intent, mInterpreterConfiguration);
+  }
+
+  private ScriptProcess launchScript(Intent intent, AndroidProxy proxy) {
+    final int port = proxy.getAddress().getPort();
+    File script = new File(intent.getStringExtra(Constants.EXTRA_SCRIPT_PATH));
+    return ScriptLauncher.launchScript(script, mInterpreterConfiguration, proxy, new Runnable() {
+      @Override
+      public void run() {
+        // TODO(damonkohler): This action actually kills the script rather than notifying the
+        // service that script exited on its own. We should distinguish between these two cases.
+        Intent intent = new Intent(ScriptingLayerService.this, ScriptingLayerService.class);
+        intent.setAction(Constants.ACTION_KILL_PROCESS);
+        intent.putExtra(Constants.EXTRA_PROXY_PORT, port);
+        startService(intent);
+      }
+    });
+  }
+
+  private InterpreterProcess launchInterpreter(Intent intent, AndroidProxy proxy) {
+    InterpreterConfiguration config =
+        ((BaseApplication) getApplication()).getInterpreterConfiguration();
+    final int port = proxy.getAddress().getPort();
+    return ScriptLauncher.launchInterpreter(proxy, intent, config, new Runnable() {
+      @Override
+      public void run() {
+        // TODO(damonkohler): This action actually kills the script rather than notifying the
+        // service that script exited on its own. We should distinguish between these two cases.
+        Intent intent = new Intent(ScriptingLayerService.this, ScriptingLayerService.class);
+        intent.setAction(Constants.ACTION_KILL_PROCESS);
+        intent.putExtra(Constants.EXTRA_PROXY_PORT, port);
+        startService(intent);
+      }
+    });
+  }
+
+  private void launchTerminal(InetSocketAddress address) {
+    Intent i = new Intent(this, ConsoleActivity.class);
+    i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    i.putExtra(Constants.EXTRA_PROXY_PORT, address.getPort());
+    startActivity(i);
+  }
+
+  private void showRunningScripts() {
+    Intent i = new Intent(this, ScriptProcessMonitor.class);
+    i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    startActivity(i);
+  }
+
+  private void addProcess(InterpreterProcess process) {
+    mProcessMap.put(process.getPort(), process);
+    mModCount++;
+    if (!mHide) {
+      updateNotification(process.getName() + " started.");
+    }
+  }
+
+  private InterpreterProcess removeProcess(int port) {
+    InterpreterProcess process = mProcessMap.remove(port);
+    if (process == null) {
+      return null;
+    }
+    mModCount++;
+    if (!mHide) {
+      updateNotification(process.getName() + " exited.");
+    }
+    return process;
+  }
+
+  private void killProcess(Intent intent) {
+    int processId = intent.getIntExtra(Constants.EXTRA_PROXY_PORT, 0);
+    InterpreterProcess process = removeProcess(processId);
+    if (process != null) {
+      process.kill();
+      mRecentlyKilledProcess = new WeakReference<InterpreterProcess>(process);
+    }
+  }
+
+  public int getModCount() {
+    return mModCount;
+  }
+
+  private void killAll() {
+    for (InterpreterProcess process : getScriptProcessesList()) {
+      process = removeProcess(process.getPort());
+      if (process != null) {
+        process.kill();
+      }
+    }
+  }
+
+  public List<InterpreterProcess> getScriptProcessesList() {
+    ArrayList<InterpreterProcess> result = new ArrayList<InterpreterProcess>();
+    result.addAll(mProcessMap.values());
+    return result;
+  }
+
+  public InterpreterProcess getProcess(int port) {
+    InterpreterProcess p = mProcessMap.get(port);
+    if (p == null) {
+      return mRecentlyKilledProcess.get();
+    }
+    return p;
+  }
+
+  public TerminalManager getTerminalManager() {
+    return mTerminalManager;
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptingLayerServiceLauncher.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptingLayerServiceLauncher.java
new file mode 100644
index 0000000..e373670
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptingLayerServiceLauncher.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.SystemProperties;
+
+import android.util.Log;
+
+import com.googlecode.android_scripting.Constants;
+
+public class ScriptingLayerServiceLauncher extends Activity {
+
+    private static final int DEBUGGABLE_BUILD = 1;
+
+    private static final String LOG_TAG = "sl4a";
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    // Forward the intent that launched us to start the service.
+
+    if(SystemProperties.getInt("ro.debuggable", 0) == DEBUGGABLE_BUILD) {
+        Intent intent = getIntent();
+        intent.setComponent(Constants.SL4A_SERVICE_COMPONENT_NAME);
+        startService(intent);
+        finish();
+    }
+    else {
+        Log.e(LOG_TAG, "ERROR: Cannot to start SL4A on non-debuggable build!");
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptsLiveFolder.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptsLiveFolder.java
new file mode 100644
index 0000000..191c641
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ScriptsLiveFolder.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Intent.ShortcutIconResource;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.LiveFolders;
+
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.provider.ScriptProvider;
+
+public class ScriptsLiveFolder extends Activity {
+
+  public static final Uri CONTENT_URI =
+      Uri.parse("content://" + ScriptProvider.AUTHORITY + "/" + ScriptProvider.LIVEFOLDER);
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    final Intent intent = getIntent();
+    final String action = intent.getAction();
+    if (LiveFolders.ACTION_CREATE_LIVE_FOLDER.equals(action)) {
+      setResult(RESULT_OK, createLiveFolder(this, CONTENT_URI, "Scripts", R.drawable.live_folder));
+    } else {
+      setResult(RESULT_CANCELED);
+    }
+    finish();
+  }
+
+  private Intent createLiveFolder(Context context, Uri uri, String name, int icon) {
+    final Intent intent = new Intent();
+    intent.setData(uri);
+    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME, name);
+    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON, ShortcutIconResource
+        .fromContext(this, icon));
+    intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE, LiveFolders.DISPLAY_MODE_LIST);
+    return intent;
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ServiceUtils.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ServiceUtils.java
new file mode 100644
index 0000000..ddb5937
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/ServiceUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+
+import com.googlecode.android_scripting.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * A utility class supplying helper methods for {@link Service} objects.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ */
+public class ServiceUtils {
+  private ServiceUtils() {
+  }
+
+  /**
+   * Marks the service as a foreground service. This uses reflection to figure out whether the new
+   * APIs for marking a service as a foreground service are available. If not, it falls back to the
+   * old {@link #setForeground(boolean)} call.
+   *
+   * @param service
+   *          the service to put in foreground mode
+   * @param notificationId
+   *          id of the notification to show
+   * @param notification
+   *          the notification to show
+   */
+  public static void setForeground(Service service, Integer notificationId,
+      Notification notification) {
+    final Class<?>[] startForegroundSignature = new Class[] { int.class, Notification.class };
+    Method startForeground = null;
+    try {
+      startForeground = service.getClass().getMethod("startForeground", startForegroundSignature);
+
+      try {
+        startForeground.invoke(service, new Object[] { notificationId, notification });
+      } catch (IllegalArgumentException e) {
+        // Should not happen!
+        Log.e("Could not set TriggerService to foreground mode.", e);
+      } catch (IllegalAccessException e) {
+        // Should not happen!
+        Log.e("Could not set TriggerService to foreground mode.", e);
+      } catch (InvocationTargetException e) {
+        // Should not happen!
+        Log.e("Could not set TriggerService to foreground mode.", e);
+      }
+
+    } catch (NoSuchMethodException e) {
+      // Fall back on old API.
+      // service.setForeground(true); //too old to be supported
+
+      NotificationManager manager =
+          (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE);
+      manager.notify(notificationId, notification);
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/TriggerManager.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/TriggerManager.java
new file mode 100644
index 0000000..fc37796
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/TriggerManager.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.google.common.collect.Lists;
+import com.googlecode.android_scripting.ActivityFlinger;
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.facade.FacadeConfiguration;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+import com.googlecode.android_scripting.trigger.ScriptTrigger;
+import com.googlecode.android_scripting.trigger.Trigger;
+import com.googlecode.android_scripting.trigger.TriggerRepository;
+import com.googlecode.android_scripting.trigger.TriggerRepository.TriggerRepositoryObserver;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class TriggerManager extends ListActivity {
+  private final List<ScriptTrigger> mTriggers = Lists.newArrayList();
+
+  private ScriptTriggerAdapter mAdapter;
+  private TriggerRepository mTriggerRepository;
+
+  private static enum ContextMenuId {
+    REMOVE;
+    public int getId() {
+      return ordinal() + Menu.FIRST;
+    }
+  }
+
+  private static enum MenuId {
+    ADD, PREFERENCES, HELP;
+    public int getId() {
+      return ordinal() + Menu.FIRST;
+    }
+  }
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    CustomizeWindow.requestCustomTitle(this, "Triggers", R.layout.trigger_manager);
+    ScriptTriggerListObserver observer = new ScriptTriggerListObserver();
+    mAdapter = new ScriptTriggerAdapter();
+    setListAdapter(mAdapter);
+    registerForContextMenu(getListView());
+    mTriggerRepository = ((BaseApplication) getApplication()).getTriggerRepository();
+    mTriggerRepository.bootstrapObserver(observer);
+    ActivityFlinger.attachView(getListView(), this);
+    ActivityFlinger.attachView(getWindow().getDecorView(), this);
+  }
+
+  @Override
+  public boolean onCreateOptionsMenu(Menu menu) {
+    menu.add(Menu.NONE, MenuId.ADD.getId(), Menu.NONE, "Add").setIcon(
+        android.R.drawable.ic_menu_add);
+    menu.add(Menu.NONE, MenuId.PREFERENCES.getId(), Menu.NONE, "Preferences").setIcon(
+        android.R.drawable.ic_menu_preferences);
+    return true;
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    int itemId = item.getItemId();
+    if (itemId == MenuId.PREFERENCES.getId()) {
+      startActivity(new Intent(this, Preferences.class));
+    } else if (itemId != Menu.NONE) {
+      Intent intent = new Intent(this, ScriptPicker.class);
+      intent.setAction(Intent.ACTION_PICK);
+      startActivityForResult(intent, itemId);
+    }
+    return true;
+  }
+
+  @Override
+  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+    menu.add(Menu.NONE, ContextMenuId.REMOVE.getId(), Menu.NONE, "Remove");
+  }
+
+  @Override
+  public boolean onContextItemSelected(MenuItem item) {
+    AdapterView.AdapterContextMenuInfo info;
+    try {
+      info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+    } catch (ClassCastException e) {
+      Log.e("Bad menuInfo", e);
+      return false;
+    }
+
+    Trigger trigger = mAdapter.getItem(info.position);
+    if (trigger == null) {
+      Log.v("No trigger selected.");
+      return false;
+    }
+
+    if (item.getItemId() == ContextMenuId.REMOVE.getId()) {
+      mTriggerRepository.remove(trigger);
+    }
+    return true;
+  }
+
+  @Override
+  public void onListItemClick(ListView l, View v, int position, long id) {
+    mAdapter.notifyDataSetInvalidated();
+  }
+
+  private class ScriptTriggerListObserver implements TriggerRepositoryObserver {
+
+    @Override
+    public void onPut(Trigger trigger) {
+      mTriggers.add((ScriptTrigger) trigger);
+      mAdapter.notifyDataSetInvalidated();
+    }
+
+    @Override
+    public void onRemove(Trigger trigger) {
+      mTriggers.remove(trigger);
+      mAdapter.notifyDataSetInvalidated();
+    }
+  }
+
+  private class ScriptTriggerAdapter extends BaseAdapter {
+
+    @Override
+    public int getCount() {
+      return mTriggers.size();
+    }
+
+    @Override
+    public Trigger getItem(int position) {
+      return mTriggers.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+      return position;
+    }
+
+    @Override
+    public View getView(final int position, View convertView, ViewGroup parent) {
+      ScriptTrigger trigger = mTriggers.get(position);
+      TextView textView = new TextView(TriggerManager.this);
+      textView.setText(trigger.getEventName() + " " + trigger.getScript().getName());
+      return textView;
+    }
+  }
+
+  @Override
+  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+    if (resultCode == RESULT_OK) {
+      final File script = new File(data.getStringExtra(Constants.EXTRA_SCRIPT_PATH));
+      if (requestCode == MenuId.ADD.getId()) {
+        Map<String, MethodDescriptor> eventMethodDescriptors =
+            FacadeConfiguration.collectStartEventMethodDescriptors();
+        final List<String> eventNames = Lists.newArrayList(eventMethodDescriptors.keySet());
+        Collections.sort(eventNames);
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setItems(eventNames.toArray(new CharSequence[eventNames.size()]),
+            new OnClickListener() {
+              @Override
+              public void onClick(DialogInterface dialog, int position) {
+                mTriggerRepository.put(new ScriptTrigger(eventNames.get(position), script));
+              }
+            });
+        builder.show();
+      }
+    }
+  }
+
+  public void clickCancel(View v) {
+    for (Trigger t : mTriggerRepository.getAllTriggers().values()) {
+      mTriggerRepository.remove(t);
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/TriggerService.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/TriggerService.java
new file mode 100644
index 0000000..8024533
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/activity/TriggerService.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.activity;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import com.google.common.base.Preconditions;
+import com.googlecode.android_scripting.BaseApplication;
+import com.googlecode.android_scripting.ForegroundService;
+import com.googlecode.android_scripting.IntentBuilders;
+import com.googlecode.android_scripting.NotificationIdFactory;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.event.Event;
+import com.googlecode.android_scripting.event.EventObserver;
+import com.googlecode.android_scripting.facade.EventFacade;
+import com.googlecode.android_scripting.facade.FacadeConfiguration;
+import com.googlecode.android_scripting.facade.FacadeManager;
+import com.googlecode.android_scripting.trigger.EventGenerationControllingObserver;
+import com.googlecode.android_scripting.trigger.Trigger;
+import com.googlecode.android_scripting.trigger.TriggerRepository;
+import com.googlecode.android_scripting.trigger.TriggerRepository.TriggerRepositoryObserver;
+
+/**
+ * The trigger service takes care of installing triggers serialized to the preference storage.
+ *
+ * <p>
+ * The service also installs an alarm that keeps it running, unless the user force-quits the
+ * service.
+ *
+ * <p>
+ * When no triggers are installed the service shuts down silently as to not consume resources
+ * unnecessarily.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class TriggerService extends ForegroundService {
+  private static final int NOTIFICATION_ID = NotificationIdFactory.create();
+  private static final long PING_MILLIS = 10 * 1000 * 60;
+
+  private final IBinder mBinder;
+  private TriggerRepository mTriggerRepository;
+  private FacadeManager mFacadeManager;
+  private EventFacade mEventFacade;
+
+  public class LocalBinder extends Binder {
+    public TriggerService getService() {
+      return TriggerService.this;
+    }
+  }
+
+  public TriggerService() {
+    super(NOTIFICATION_ID);
+    mBinder = new LocalBinder();
+  }
+
+  @Override
+  public IBinder onBind(Intent intent) {
+    return mBinder;
+  }
+
+  @Override
+  public void onCreate() {
+    super.onCreate();
+
+    mFacadeManager =
+        new FacadeManager(FacadeConfiguration.getSdkLevel(), this, null,
+            FacadeConfiguration.getFacadeClasses());
+    mEventFacade = mFacadeManager.getReceiver(EventFacade.class);
+
+    mTriggerRepository = ((BaseApplication) getApplication()).getTriggerRepository();
+    mTriggerRepository.bootstrapObserver(new RepositoryObserver());
+    mTriggerRepository.bootstrapObserver(new EventGenerationControllingObserver(mFacadeManager));
+    installAlarm();
+  }
+
+  @Override
+  public void onStart(Intent intent, int startId) {
+    if (mTriggerRepository.isEmpty()) {
+      stopSelfResult(startId);
+      return;
+    }
+  }
+
+  /** Returns the notification to display whenever the service is running. */
+  @Override
+  protected Notification createNotification() {
+    Intent notificationIntent = new Intent(this, TriggerManager.class);
+    Notification.Builder builder = new Notification.Builder(this);
+    builder.setSmallIcon(R.drawable.sl4a_logo_48)
+           .setTicker("SL4A Trigger Service started.")
+           .setWhen(System.currentTimeMillis())
+           .setContentTitle("SL4A Trigger Service")
+           .setContentText("Tap to view triggers")
+           .setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent, 0));
+    Notification notification = builder.build();
+    notification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
+    return notification;
+  }
+
+  private class TriggerEventObserver implements EventObserver {
+    private final Trigger mTrigger;
+
+    public TriggerEventObserver(Trigger trigger) {
+      mTrigger = trigger;
+    }
+
+    @Override
+    public void onEventReceived(Event event) {
+      mTrigger.handleEvent(event, TriggerService.this);
+    }
+  }
+
+  private class RepositoryObserver implements TriggerRepositoryObserver {
+    int mTriggerCount = 0;
+
+    @Override
+    public void onPut(Trigger trigger) {
+      mTriggerCount++;
+      mEventFacade.addNamedEventObserver(trigger.getEventName(), new TriggerEventObserver(trigger));
+    }
+
+    @Override
+    public void onRemove(Trigger trigger) {
+      Preconditions.checkArgument(mTriggerCount > 0);
+      // TODO(damonkohler): Tear down EventObserver associated with trigger.
+      if (--mTriggerCount == 0) {
+        // TODO(damonkohler): Use stopSelfResult() which would require tracking startId.
+        stopSelf();
+      }
+    }
+  }
+
+  private void installAlarm() {
+    AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+    alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + PING_MILLIS,
+        PING_MILLIS, IntentBuilders.buildTriggerServicePendingIntent(this));
+  }
+
+  private void uninstallAlarm() {
+    AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+    alarmManager.cancel(IntentBuilders.buildTriggerServicePendingIntent(this));
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    uninstallAlarm();
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/dialog/DurationPickerDialog.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/dialog/DurationPickerDialog.java
new file mode 100644
index 0000000..6a53fac
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/dialog/DurationPickerDialog.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.dialog;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.widget.DurationPicker;
+
+public class DurationPickerDialog {
+
+  private DurationPickerDialog() {
+    // Utility class.
+  }
+
+  public interface DurationPickedListener {
+    public void onSet(double duration);
+
+    public void onCancel();
+  }
+
+  public static void getDurationFromDialog(Activity activity, String title,
+      final DurationPickedListener done) {
+    final DurationPicker picker = new DurationPicker(activity);
+    AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+    alert.setIcon(R.drawable.ic_dialog_time);
+    alert.setTitle(title);
+    alert.setView(picker);
+    alert.setPositiveButton("Set", new DialogInterface.OnClickListener() {
+      @Override
+      public void onClick(DialogInterface dialog, int whichButton) {
+        done.onSet(picker.getDuration());
+      }
+    });
+    alert.setOnCancelListener(new DialogInterface.OnCancelListener() {
+      @Override
+      public void onCancel(DialogInterface arg0) {
+        done.onCancel();
+      }
+    });
+    alert.show();
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/locale/LocaleReceiver.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/locale/LocaleReceiver.java
new file mode 100644
index 0000000..9c51720
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/locale/LocaleReceiver.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.locale;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.IntentBuilders;
+
+import java.io.File;
+
+public class LocaleReceiver extends BroadcastReceiver {
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+
+    final File script =
+        new File(intent.getBundleExtra(com.twofortyfouram.locale.platform.Intent.EXTRA_BUNDLE)
+            .getString(Constants.EXTRA_SCRIPT_PATH));
+    Log.v("LocaleReceiver", "Locale initiated launch of " + script);
+    Intent launchIntent;
+    if (intent.getBooleanExtra(Constants.EXTRA_LAUNCH_IN_BACKGROUND, false)) {
+      launchIntent = IntentBuilders.buildStartInBackgroundIntent(script);
+    } else {
+      launchIntent = IntentBuilders.buildStartInTerminalIntent(script);
+    }
+    launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    context.startActivity(launchIntent);
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/provider/ApiProvider.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/provider/ApiProvider.java
new file mode 100644
index 0000000..5bc1754
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/provider/ApiProvider.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.provider;
+
+import android.app.SearchManager;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.googlecode.android_scripting.facade.FacadeConfiguration;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+import com.googlecode.android_scripting.rpc.Rpc;
+import com.googlecode.android_scripting.rpc.RpcDeprecated;
+import com.googlecode.android_scripting.rpc.RpcMinSdk;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+
+public class ApiProvider extends ContentProvider {
+
+  public static final String SINGLE_MIME = "vnd.android.cursor.item/vnd.sl4a.api";
+
+  private static final int SUGGESTIONS_ID = 1;
+
+  public static final String AUTHORITY = ApiProvider.class.getName().toLowerCase();
+  public static final String SUGGESTIONS = "searchSuggestions/*/*";
+
+  private final UriMatcher mUriMatcher;
+  private final Collection<MethodDescriptor> mMethodDescriptors;
+
+  public ApiProvider() {
+    mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+    mUriMatcher.addURI(AUTHORITY, SUGGESTIONS, SUGGESTIONS_ID);
+    mMethodDescriptors =
+        Collections2.filter(FacadeConfiguration.collectMethodDescriptors(),
+            new Predicate<MethodDescriptor>() {
+              @Override
+              public boolean apply(MethodDescriptor descriptor) {
+                Method method = descriptor.getMethod();
+                if (method.isAnnotationPresent(RpcDeprecated.class)) {
+                  return false;
+                } else if (method.isAnnotationPresent(RpcMinSdk.class)) {
+                  int requiredSdkLevel = method.getAnnotation(RpcMinSdk.class).value();
+                  if (FacadeConfiguration.getSdkLevel() < requiredSdkLevel) {
+                    return false;
+                  }
+                }
+                return true;
+              }
+            });
+  }
+
+  @Override
+  public int delete(Uri uri, String selection, String[] selectionArgs) {
+    return 0;
+  }
+
+  @Override
+  public String getType(Uri uri) {
+    return SINGLE_MIME;
+  }
+
+  @Override
+  public Uri insert(Uri uri, ContentValues values) {
+    return null;
+  }
+
+  @Override
+  public boolean onCreate() {
+    return true;
+  }
+
+  @Override
+  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+      String sortOrder) {
+    switch (mUriMatcher.match(uri)) {
+    case SUGGESTIONS_ID:
+      String query = uri.getLastPathSegment().toLowerCase();
+      return querySearchSuggestions(query);
+    }
+    return null;
+  }
+
+  private Cursor querySearchSuggestions(String query) {
+    String[] columns =
+        { BaseColumns._ID, SearchManager.SUGGEST_COLUMN_TEXT_1,
+          SearchManager.SUGGEST_COLUMN_TEXT_2, SearchManager.SUGGEST_COLUMN_QUERY };
+    MatrixCursor cursor = new MatrixCursor(columns);
+    int index = 0;
+    for (MethodDescriptor descriptor : mMethodDescriptors) {
+      String name = descriptor.getMethod().getName();
+      if (!name.toLowerCase().contains(query)) {
+        continue;
+      }
+      String description = descriptor.getMethod().getAnnotation(Rpc.class).description();
+      description = description.replaceAll("\\s+", " ");
+      Object[] row = { index, name, description, name };
+      cursor.addRow(row);
+      ++index;
+    }
+    return cursor;
+  }
+
+  @Override
+  public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+    return 0;
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/provider/ScriptProvider.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/provider/ScriptProvider.java
new file mode 100644
index 0000000..0f16e0d
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/provider/ScriptProvider.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.provider;
+
+import android.app.SearchManager;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.content.Intent.ShortcutIconResource;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.LiveFolders;
+
+import com.googlecode.android_scripting.FeaturedInterpreters;
+import com.googlecode.android_scripting.IntentBuilders;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.ScriptStorageAdapter;
+import com.googlecode.android_scripting.interpreter.Interpreter;
+import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
+import com.googlecode.android_scripting.interpreter.InterpreterConstants;
+
+import java.io.File;
+
+public class ScriptProvider extends ContentProvider {
+
+  public static final String SINGLE_MIME = "vnd.android.cursor.item/vnd.sl4a.script";
+  public static final String MULTIPLE_MIME = "vnd.android.cursor.dir/vnd.sl4a.script";
+
+  private static final int LIVEFOLDER_ID = 1;
+  private static final int SUGGESTIONS_ID = 2;
+
+  public static final String AUTHORITY = ScriptProvider.class.getName().toLowerCase();
+  public static final String LIVEFOLDER = "liveFolder";
+  public static final String SUGGESTIONS = "searchSuggestions/*/*";
+
+  private final UriMatcher mUriMatcher;
+
+  private Context mContext;
+  private InterpreterConfiguration mConfiguration;
+
+  public ScriptProvider() {
+    mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+    mUriMatcher.addURI(AUTHORITY, LIVEFOLDER, LIVEFOLDER_ID);
+    mUriMatcher.addURI(AUTHORITY, SUGGESTIONS, SUGGESTIONS_ID);
+  }
+
+  @Override
+  public int delete(Uri uri, String selection, String[] selectionArgs) {
+    return 0;
+  }
+
+  @Override
+  public String getType(Uri uri) {
+    if (uri.getLastPathSegment().equals("scripts")) {
+      return MULTIPLE_MIME;
+    }
+    return SINGLE_MIME;
+  }
+
+  @Override
+  public Uri insert(Uri uri, ContentValues values) {
+    return null;
+  }
+
+  @Override
+  public boolean onCreate() {
+    mContext = getContext();
+    mConfiguration = new InterpreterConfiguration(mContext);
+    mConfiguration.startDiscovering();
+    return true;
+  }
+
+  @Override
+  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+      String sortOrder) {
+    switch (mUriMatcher.match(uri)) {
+    case LIVEFOLDER_ID:
+      return queryLiveFolder();
+    case SUGGESTIONS_ID:
+      String query = uri.getLastPathSegment().toLowerCase();
+      return querySearchSuggestions(query);
+    default:
+      return null;
+    }
+  }
+
+  private Cursor querySearchSuggestions(String query) {
+    String[] columns =
+        { BaseColumns._ID, SearchManager.SUGGEST_COLUMN_TEXT_1,
+          SearchManager.SUGGEST_COLUMN_TEXT_2, SearchManager.SUGGEST_COLUMN_ICON_2,
+          SearchManager.SUGGEST_COLUMN_QUERY, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID };
+    MatrixCursor cursor = new MatrixCursor(columns);
+    int index = 0;
+    for (File script : ScriptStorageAdapter.listExecutableScripts(null, mConfiguration)) {
+      String scriptName = script.getName().toLowerCase();
+      if (!scriptName.contains(query)) {
+        continue;
+      }
+      Interpreter interpreter = mConfiguration.getInterpreterForScript(scriptName);
+      String secondLine = interpreter.getNiceName();
+      int icon = FeaturedInterpreters.getInterpreterIcon(mContext, interpreter.getExtension());
+      Object[] row =
+          { index, scriptName, secondLine, icon, scriptName,
+            SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT };
+      cursor.addRow(row);
+      ++index;
+    }
+    return cursor;
+  }
+
+  private Cursor queryLiveFolder() {
+    String[] columns =
+        { BaseColumns._ID, LiveFolders.NAME, LiveFolders.INTENT, LiveFolders.ICON_RESOURCE,
+          LiveFolders.ICON_PACKAGE, LiveFolders.DESCRIPTION };
+    MatrixCursor cursor = new MatrixCursor(columns);
+    int index = 0;
+    for (File script : ScriptStorageAdapter.listExecutableScriptsRecursively(null, mConfiguration)) {
+      int iconId = 0;
+      if (script.isDirectory()) {
+        iconId = R.drawable.folder;
+      } else {
+        iconId = FeaturedInterpreters.getInterpreterIcon(mContext, script.getName());
+        if (iconId == 0) {
+          iconId = R.drawable.sl4a_logo_32;
+        }
+      }
+      ShortcutIconResource icon = ShortcutIconResource.fromContext(mContext, iconId);
+      Intent intent = IntentBuilders.buildStartInBackgroundIntent(script);
+      intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+      String description = script.getAbsolutePath();
+      if (description.startsWith(InterpreterConstants.SCRIPTS_ROOT)) {
+        description = description.replaceAll(InterpreterConstants.SCRIPTS_ROOT, "scripts/");
+      }
+      Object[] row =
+          { index, script.getName(), intent.toURI(), icon.resourceName, icon.packageName,
+            description };
+      cursor.addRow(row);
+      ++index;
+    }
+    return cursor;
+  }
+
+  @Override
+  public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+    return 0;
+  }
+
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/provider/TelephonyTestProvider.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/provider/TelephonyTestProvider.java
new file mode 100644
index 0000000..2ab7c24
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/provider/TelephonyTestProvider.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import com.googlecode.android_scripting.Log;
+
+/**
+ * A simple provider to send MMS PDU to platform MMS service
+ */
+public class TelephonyTestProvider extends ContentProvider {
+
+    public static final String AUTHORITY = "telephonytestauthority";
+
+    private final boolean DBG = false;
+
+    @Override
+    public boolean onCreate() {
+        if(DBG) Log.d("TelephonTestProvider Successfully created!");
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        // Not supported
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        // Not supported
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        // Not supported
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        // Not supported
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        // Not supported
+        return 0;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String fileMode) throws FileNotFoundException {
+        Log.d(String.format("Entered ParcelFileDescriptor: Uri(%s), Mode(%s)", uri.toString(),
+                fileMode));
+        File file = new File(getContext().getCacheDir(), uri.getPath());
+        file.setReadable(true);
+
+        Log.d(String.format("Looking for file at %s", getContext().getCacheDir() + uri.getPath()));
+        int mode = (TextUtils.equals(fileMode, "r") ? ParcelFileDescriptor.MODE_READ_ONLY :
+                ParcelFileDescriptor.MODE_WRITE_ONLY
+                | ParcelFileDescriptor.MODE_TRUNCATE
+                | ParcelFileDescriptor.MODE_CREATE);
+
+        try {
+            ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, mode);
+
+            if(DBG) {
+                try {
+                    BufferedReader br = new BufferedReader(new
+                            FileReader(descriptor.getFileDescriptor()));
+                    String line = null;
+                    while ((line = br.readLine()) != null) {
+                        Log.d("MMS:" + line);
+                    }
+                } catch (IOException e) {
+                }
+            }
+
+            return descriptor;
+        } catch (FileNotFoundException fnf) {
+            Log.e(fnf);
+            return null;
+        }
+    }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/service/FacadeService.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/service/FacadeService.java
new file mode 100644
index 0000000..7ca4e18
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/service/FacadeService.java
@@ -0,0 +1,55 @@
+package com.googlecode.android_scripting.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Messenger;
+
+import com.googlecode.android_scripting.facade.FacadeConfiguration;
+import com.googlecode.android_scripting.facade.FacadeManagerFactory;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManagerFactory;
+
+/**
+ * FacadeService exposes SL4A's Facade methods through a {@link Service} so
+ * they can be invoked from third-party apps.
+ * <p>
+ * Example binding code:<br>
+ * {@code
+ *   Intent intent = new Intent();
+ *   intent.setPackage("com.googlecode.android_scripting");
+ *   intent.setAction("com.googlecode.android_scripting.service.FacadeService.ACTION_BIND");
+ *   sl4aService = bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ * }
+ * Example using the service:<br>
+ * {@code
+ *   Bundle sl4aBundle = new Bundle;
+ *   bundle.putString{"sl4aMethod", "makeToast"};
+ *   bundle.putString{"message", "Hello World!"};
+ *   Message msg = Message.obtain(null, SL4A_ACTION);
+ *   msg.setData(sl4aBundle);
+ *   msg.replyTo = myReplyHandler; // Set a Messenger if you need the response
+ *   mSl4aService.send(msg);
+ * }
+ * <p>
+ * For more info on binding a {@link Service} using a {@link Messenger} please
+ * refer to Android's public developer documentation.
+ */
+public class FacadeService extends Service {
+
+    private RpcReceiverManagerFactory rpcReceiverManagerFactory;
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (rpcReceiverManagerFactory == null) {
+            rpcReceiverManagerFactory =
+                    new FacadeManagerFactory(FacadeConfiguration.getSdkLevel(), this, null,
+                            FacadeConfiguration.getFacadeClasses());
+        }
+        HandlerThread handlerThread = new HandlerThread("MessageHandlerThread");
+        handlerThread.start();
+        Messenger aMessenger = new Messenger(new MessageHandler(handlerThread,
+                rpcReceiverManagerFactory));
+        return aMessenger.getBinder();
+    }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/service/MessageHandler.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/service/MessageHandler.java
new file mode 100644
index 0000000..8c6fb3f
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/service/MessageHandler.java
@@ -0,0 +1,116 @@
+package com.googlecode.android_scripting.service;
+
+import android.annotation.TargetApi;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.RemoteException;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.jsonrpc.JsonRpcResult;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManagerFactory;
+import com.googlecode.android_scripting.rpc.MethodDescriptor;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Class responsible for Handling messages that came through the FacadeService
+ * interface.
+ * <br>
+ * Please refer to {@link FacadeService} for details on how to use.
+ */
+@TargetApi(3)
+public class MessageHandler extends Handler {
+
+    private static final int SL4A_ACTION = 0;
+    private static final int DEFAULT_SENDING_ID = 0;
+
+    // Android sets this to -1 when the message is not sent by a Messenger.
+    // see http://developer.android.com/reference/android/os/Message.html#sendingUid
+    private static final int DEFAULT_UNSET_SENDING_ID = 1;
+
+    // Keys for the Bundles.
+    private static final String SL4A_METHOD = "sl4aMethod";
+    private static final String SL4A_RESULT = "sl4aResult";
+
+    private final RpcReceiverManagerFactory mRpcReceiverManagerFactory;
+
+    public MessageHandler(HandlerThread handlerThread,
+                          RpcReceiverManagerFactory rpcReceiverManagerFactory) {
+        super(handlerThread.getLooper());
+        this.mRpcReceiverManagerFactory = rpcReceiverManagerFactory;
+    }
+
+    /**
+     * Handles messages for the service. It does this via the same mechanism used
+     * for RPCs through RpcManagers.
+     *
+     * @param message The message that contains the method and parameters to
+     *     execute.
+     */
+    @Override
+    public void handleMessage(Message message) {
+        Log.d("Handling Remote request");
+        int senderId = message.sendingUid == DEFAULT_UNSET_SENDING_ID ?
+                DEFAULT_SENDING_ID : message.sendingUid;
+        if (message.what == SL4A_ACTION) {
+            RpcReceiverManager receiverManager;
+            if (mRpcReceiverManagerFactory.getRpcReceiverManagers().containsKey(senderId)) {
+                receiverManager = mRpcReceiverManagerFactory.getRpcReceiverManagers().get(senderId);
+            } else {
+                receiverManager = mRpcReceiverManagerFactory.create(senderId);
+            }
+            Bundle sl4aRequest = message.getData();
+            String method = sl4aRequest.getString(SL4A_METHOD);
+            if (method == null || "".equals(method)) {
+                Log.e("No SL4A method specified on the Bundle. Specify one with "
+                        + SL4A_METHOD);
+                return;
+            }
+            MethodDescriptor rpc = receiverManager.getMethodDescriptor(method);
+            if (rpc == null) {
+                Log.e("Unknown RPC: \"" + method + "\"");
+                return;
+            }
+            try {
+                Log.d("Invoking method " + rpc.getName());
+                Object result = rpc.invoke(receiverManager, sl4aRequest);
+                // Only return a result if we were passed a Messenger. Otherwise assume
+                // client did not care for the response.
+                if (message.replyTo != null) {
+                    Message reply = Message.obtain();
+                    Bundle sl4aResponse = new Bundle();
+                    putResult(senderId, result, sl4aResponse);
+                    reply.setData(sl4aResponse);
+                    message.replyTo.send(reply);
+                }
+            } catch (RemoteException e) {
+                Log.e("Could not send reply back to client", e);
+            } catch (Throwable t) {
+                Log.e("Exception while executing sl4a method", t);
+            }
+        }
+    }
+
+    private void putResult(int id, Object result, Bundle reply) {
+        JSONObject json;
+        try {
+            if (result instanceof Throwable) {
+                json = JsonRpcResult.error(id, (Throwable) result);
+            } else {
+                json = JsonRpcResult.result(id, result);
+            }
+        } catch (JSONException e) {
+            // There was an error converting the result to JSON. This shouldn't
+            // happen normally.
+            Log.e("Caught exception when filling JSON result.", e);
+            reply.putString(SL4A_RESULT, e.toString());
+            return;
+        }
+        Log.d("Returning result: " + json.toString());
+        reply.putString(SL4A_RESULT, json.toString());
+    }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/widget/DurationPicker.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/widget/DurationPicker.java
new file mode 100644
index 0000000..7652471
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/widget/DurationPicker.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+
+import com.googlecode.android_scripting.R;
+
+/**
+ * A view for selecting the a duration using days, hours, minutes, and seconds.
+ */
+public class DurationPicker extends FrameLayout {
+
+  private int mCurrentDay = 0; // 0-99
+  private int mCurrentHour = 0; // 0-23
+  private int mCurrentMinute = 0; // 0-59
+  private int mCurrentSecond = 0; // 0-59
+
+  private final NumberPicker mDayPicker;
+  private final NumberPicker mHourPicker;
+  private final NumberPicker mMinutePicker;
+  private final NumberPicker mSecondPicker;
+
+  public DurationPicker(Context context) {
+    this(context, null);
+  }
+
+  public DurationPicker(Context context, AttributeSet attrs) {
+    this(context, attrs, 0);
+  }
+
+  public DurationPicker(Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+    LayoutInflater inflater =
+        (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    inflater.inflate(R.layout.duration_picker, this, true);
+
+    mDayPicker = (NumberPicker) findViewById(R.id.day);
+    mDayPicker.setRange(0, 99);
+    mDayPicker.setSpeed(100);
+    // mHourPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
+    mDayPicker.setOnChangeListener(new NumberPicker.OnChangedListener() {
+      public void onChanged(NumberPicker spinner, int oldVal, int newVal) {
+        mCurrentDay = newVal;
+      }
+    });
+
+    mHourPicker = (NumberPicker) findViewById(R.id.hour);
+    mHourPicker.setRange(0, 23);
+    mHourPicker.setSpeed(100);
+    // mHourPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
+    mHourPicker.setOnChangeListener(new NumberPicker.OnChangedListener() {
+      public void onChanged(NumberPicker spinner, int oldVal, int newVal) {
+        mCurrentHour = newVal;
+      }
+    });
+
+    mMinutePicker = (NumberPicker) findViewById(R.id.minute);
+    mMinutePicker.setRange(0, 59);
+    mMinutePicker.setSpeed(100);
+    mMinutePicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
+    mMinutePicker.setOnChangeListener(new NumberPicker.OnChangedListener() {
+      public void onChanged(NumberPicker spinner, int oldVal, int newVal) {
+        mCurrentMinute = newVal;
+      }
+    });
+
+    mSecondPicker = (NumberPicker) findViewById(R.id.second);
+    mSecondPicker.setRange(0, 59);
+    mSecondPicker.setSpeed(100);
+    mSecondPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
+    mSecondPicker.setOnChangeListener(new NumberPicker.OnChangedListener() {
+      public void onChanged(NumberPicker spinner, int oldVal, int newVal) {
+        mCurrentSecond = newVal;
+      }
+    });
+  }
+
+  @Override
+  public void setEnabled(boolean enabled) {
+    super.setEnabled(enabled);
+    mDayPicker.setEnabled(enabled);
+    mHourPicker.setEnabled(enabled);
+    mMinutePicker.setEnabled(enabled);
+    mSecondPicker.setEnabled(enabled);
+  }
+
+  /**
+   * Returns the current day.
+   */
+  public Integer getCurrentDay() {
+    return mCurrentDay;
+  }
+
+  /**
+   * Set the current hour.
+   */
+  public void setCurrentDay(Integer currentDay) {
+    mCurrentDay = currentDay;
+    updateDayDisplay();
+  }
+
+  /**
+   * Returns the current hour.
+   */
+  public Integer getCurrentHour() {
+    return mCurrentHour;
+  }
+
+  /**
+   * Set the current hour.
+   */
+  public void setCurrentHour(Integer currentHour) {
+    mCurrentHour = currentHour;
+    updateHourDisplay();
+  }
+
+  /**
+   * Returns the current minute.
+   */
+  public Integer getCurrentMinute() {
+    return mCurrentMinute;
+  }
+
+  /**
+   * Set the current minute.
+   */
+  public void setCurrentMinute(Integer currentMinute) {
+    mCurrentMinute = currentMinute;
+    updateMinuteDisplay();
+  }
+
+  /**
+   * Returns the current second.
+   */
+  public Integer getCurrentSecond() {
+    return mCurrentSecond;
+  }
+
+  /**
+   * Set the current minute.
+   */
+  public void setCurrentSecond(Integer currentSecond) {
+    mCurrentSecond = currentSecond;
+    updateSecondDisplay();
+  }
+
+  /**
+   * Set the state of the spinners appropriate to the current day.
+   */
+  private void updateDayDisplay() {
+    int currentDay = mCurrentDay;
+    mDayPicker.setCurrent(currentDay);
+  }
+
+  /**
+   * Set the state of the spinners appropriate to the current hour.
+   */
+  private void updateHourDisplay() {
+    int currentHour = mCurrentHour;
+    mHourPicker.setCurrent(currentHour);
+  }
+
+  /**
+   * Set the state of the spinners appropriate to the current minute.
+   */
+  private void updateMinuteDisplay() {
+    mMinutePicker.setCurrent(mCurrentMinute);
+  }
+
+  /**
+   * Set the state of the spinners appropriate to the current minute.
+   */
+  private void updateSecondDisplay() {
+    mSecondPicker.setCurrent(mCurrentSecond);
+  }
+
+  /**
+   * Returns the duration in seconds.
+   */
+  public double getDuration() {
+    // The text views may still have focus so clear theirs focus which will trigger the on focus
+    // changed and any typed values to be pulled.
+    mDayPicker.clearFocus();
+    mHourPicker.clearFocus();
+    mMinutePicker.clearFocus();
+    mSecondPicker.clearFocus();
+    return (((((mCurrentDay * 24l + mCurrentHour) * 60) + mCurrentMinute) * 60) + mCurrentSecond);
+  }
+
+  /**
+   * Sets the duration in milliseconds.
+   *
+   * @return
+   */
+  public void setDuration(long duration) {
+    double seconds = duration / 1000;
+    double minutes = seconds / 60;
+    seconds = seconds % 60;
+    double hours = minutes / 60;
+    minutes = minutes % 60;
+    double days = hours / 24;
+    hours = hours % 24;
+
+    setCurrentDay((int) days);
+    setCurrentHour((int) hours);
+    setCurrentMinute((int) minutes);
+    setCurrentSecond((int) seconds);
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/widget/NumberPicker.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/widget/NumberPicker.java
new file mode 100644
index 0000000..061eacd
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/widget/NumberPicker.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.widget;
+
+import android.content.Context;
+import android.os.Handler;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.Spanned;
+import android.text.method.NumberKeyListener;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnFocusChangeListener;
+import android.view.View.OnLongClickListener;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.googlecode.android_scripting.R;
+
+public class NumberPicker extends LinearLayout implements OnClickListener, OnFocusChangeListener,
+    OnLongClickListener {
+
+  public interface OnChangedListener {
+    void onChanged(NumberPicker picker, int oldVal, int newVal);
+  }
+
+  public interface Formatter {
+    String toString(int value);
+  }
+
+  /*
+   * Use a custom NumberPicker formatting callback to use two-digit minutes strings like "01".
+   * Keeping a static formatter etc. is the most efficient way to do this; it avoids creating
+   * temporary objects on every call to format().
+   */
+  public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() {
+    final StringBuilder mBuilder = new StringBuilder();
+    final java.util.Formatter mFmt = new java.util.Formatter(mBuilder);
+    final Object[] mArgs = new Object[1];
+
+    public String toString(int value) {
+      mArgs[0] = value;
+      mBuilder.delete(0, mBuilder.length());
+      mFmt.format("%02d", mArgs);
+      return mFmt.toString();
+    }
+  };
+
+  private final Handler mHandler;
+  private final Runnable mRunnable = new Runnable() {
+    public void run() {
+      if (mIncrement) {
+        changeCurrent(mCurrent + 1);
+        mHandler.postDelayed(this, mSpeed);
+      } else if (mDecrement) {
+        changeCurrent(mCurrent - 1);
+        mHandler.postDelayed(this, mSpeed);
+      }
+    }
+  };
+
+  private final EditText mText;
+  private final InputFilter mNumberInputFilter;
+
+  private String[] mDisplayedValues;
+  private int mStart;
+  private int mEnd;
+  private int mCurrent;
+  private int mPrevious;
+  private OnChangedListener mListener;
+  private Formatter mFormatter;
+  private long mSpeed = 300;
+
+  private boolean mIncrement;
+  private boolean mDecrement;
+
+  public NumberPicker(Context context) {
+    this(context, null);
+  }
+
+  public NumberPicker(Context context, AttributeSet attrs) {
+    this(context, attrs, 0);
+  }
+
+  public NumberPicker(Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs);
+    setOrientation(VERTICAL);
+    LayoutInflater inflater =
+        (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    inflater.inflate(R.layout.number_picker, this, true);
+    mHandler = new Handler();
+    InputFilter inputFilter = new NumberPickerInputFilter();
+    mNumberInputFilter = new NumberRangeKeyListener();
+    mIncrementButton = (NumberPickerButton) findViewById(R.id.increment);
+    mIncrementButton.setOnClickListener(this);
+    mIncrementButton.setOnLongClickListener(this);
+    mIncrementButton.setNumberPicker(this);
+    mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement);
+    mDecrementButton.setOnClickListener(this);
+    mDecrementButton.setOnLongClickListener(this);
+    mDecrementButton.setNumberPicker(this);
+
+    mText = (EditText) findViewById(R.id.timepicker_input);
+    mText.setOnFocusChangeListener(this);
+    mText.setFilters(new InputFilter[] { inputFilter });
+    mText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
+
+    if (!isEnabled()) {
+      setEnabled(false);
+    }
+  }
+
+  @Override
+  public void setEnabled(boolean enabled) {
+    super.setEnabled(enabled);
+    mIncrementButton.setEnabled(enabled);
+    mDecrementButton.setEnabled(enabled);
+    mText.setEnabled(enabled);
+  }
+
+  public void setOnChangeListener(OnChangedListener listener) {
+    mListener = listener;
+  }
+
+  public void setFormatter(Formatter formatter) {
+    mFormatter = formatter;
+  }
+
+  /**
+   * Set the range of numbers allowed for the number picker. The current value will be automatically
+   * set to the start.
+   *
+   * @param start
+   *          the start of the range (inclusive)
+   * @param end
+   *          the end of the range (inclusive)
+   */
+  public void setRange(int start, int end) {
+    mStart = start;
+    mEnd = end;
+    mCurrent = start;
+    updateView();
+  }
+
+  /**
+   * Set the range of numbers allowed for the number picker. The current value will be automatically
+   * set to the start. Also provide a mapping for values used to display to the user.
+   *
+   * @param start
+   *          the start of the range (inclusive)
+   * @param end
+   *          the end of the range (inclusive)
+   * @param displayedValues
+   *          the values displayed to the user.
+   */
+  public void setRange(int start, int end, String[] displayedValues) {
+    mDisplayedValues = displayedValues;
+    mStart = start;
+    mEnd = end;
+    mCurrent = start;
+    updateView();
+  }
+
+  public void setCurrent(int current) {
+    mCurrent = current;
+    updateView();
+  }
+
+  /**
+   * The speed (in milliseconds) at which the numbers will scroll when the the +/- buttons are
+   * longpressed. Default is 300ms.
+   */
+  public void setSpeed(long speed) {
+    mSpeed = speed;
+  }
+
+  public void onClick(View v) {
+    validateInput(mText);
+    if (!mText.hasFocus()) {
+      mText.requestFocus();
+    }
+
+    // now perform the increment/decrement
+    if (R.id.increment == v.getId()) {
+      changeCurrent(mCurrent + 1);
+    } else if (R.id.decrement == v.getId()) {
+      changeCurrent(mCurrent - 1);
+    }
+  }
+
+  private String formatNumber(int value) {
+    return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value);
+  }
+
+  private void changeCurrent(int current) {
+
+    // Wrap around the values if we go past the start or end
+    if (current > mEnd) {
+      current = mStart;
+    } else if (current < mStart) {
+      current = mEnd;
+    }
+    mPrevious = mCurrent;
+    mCurrent = current;
+    notifyChange();
+    updateView();
+  }
+
+  private void notifyChange() {
+    if (mListener != null) {
+      mListener.onChanged(this, mPrevious, mCurrent);
+    }
+  }
+
+  private void updateView() {
+
+    /*
+     * If we don't have displayed values then use the current number else find the correct value in
+     * the displayed values for the current number.
+     */
+    if (mDisplayedValues == null) {
+      mText.setText(formatNumber(mCurrent));
+    } else {
+      mText.setText(mDisplayedValues[mCurrent - mStart]);
+    }
+    mText.setSelection(mText.getText().length());
+  }
+
+  private void validateCurrentView(CharSequence str) {
+    int val = getSelectedPos(str.toString());
+    if ((val >= mStart) && (val <= mEnd)) {
+      mPrevious = mCurrent;
+      mCurrent = val;
+      notifyChange();
+    }
+    updateView();
+  }
+
+  public void onFocusChange(View v, boolean hasFocus) {
+
+    /*
+     * When focus is lost check that the text field has valid values.
+     */
+    if (!hasFocus) {
+      validateInput(v);
+    }
+  }
+
+  private void validateInput(View v) {
+    String str = String.valueOf(((TextView) v).getText());
+    if ("".equals(str)) {
+
+      // Restore to the old value as we don't allow empty values
+      updateView();
+    } else {
+
+      // Check the new value and ensure it's in range
+      validateCurrentView(str);
+    }
+  }
+
+  /**
+   * We start the long click here but rely on the {@link NumberPickerButton} to inform us when the
+   * long click has ended.
+   */
+  public boolean onLongClick(View v) {
+
+    /*
+     * The text view may still have focus so clear it's focus which will trigger the on focus
+     * changed and any typed values to be pulled.
+     */
+    mText.clearFocus();
+
+    if (R.id.increment == v.getId()) {
+      mIncrement = true;
+      mHandler.post(mRunnable);
+    } else if (R.id.decrement == v.getId()) {
+      mDecrement = true;
+      mHandler.post(mRunnable);
+    }
+    return true;
+  }
+
+  public void cancelIncrement() {
+    mIncrement = false;
+  }
+
+  public void cancelDecrement() {
+    mDecrement = false;
+  }
+
+  private static final char[] DIGIT_CHARACTERS =
+      new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
+
+  private final NumberPickerButton mIncrementButton;
+  private final NumberPickerButton mDecrementButton;
+
+  private class NumberPickerInputFilter implements InputFilter {
+    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
+        int dend) {
+      if (mDisplayedValues == null) {
+        return mNumberInputFilter.filter(source, start, end, dest, dstart, dend);
+      }
+      CharSequence filtered = String.valueOf(source.subSequence(start, end));
+      String result =
+          String.valueOf(dest.subSequence(0, dstart)) + filtered
+              + dest.subSequence(dend, dest.length());
+      String str = String.valueOf(result).toLowerCase();
+      for (String val : mDisplayedValues) {
+        val = val.toLowerCase();
+        if (val.startsWith(str)) {
+          return filtered;
+        }
+      }
+      return "";
+    }
+  }
+
+  private class NumberRangeKeyListener extends NumberKeyListener {
+
+    // XXX This doesn't allow for range limits when controlled by a
+    // soft input method!
+    public int getInputType() {
+      return InputType.TYPE_CLASS_NUMBER;
+    }
+
+    @Override
+    protected char[] getAcceptedChars() {
+      return DIGIT_CHARACTERS;
+    }
+
+    @Override
+    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
+        int dend) {
+
+      CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
+      if (filtered == null) {
+        filtered = source.subSequence(start, end);
+      }
+
+      String result =
+          String.valueOf(dest.subSequence(0, dstart)) + filtered
+              + dest.subSequence(dend, dest.length());
+
+      if ("".equals(result)) {
+        return result;
+      }
+      int val = getSelectedPos(result);
+
+      /*
+       * Ensure the user can't type in a value greater than the max allowed. We have to allow less
+       * than min as the user might want to delete some numbers and then type a new number.
+       */
+      if (val > mEnd) {
+        return "";
+      } else {
+        return filtered;
+      }
+    }
+  }
+
+  private int getSelectedPos(String str) {
+    if (mDisplayedValues == null) {
+      return Integer.parseInt(str);
+    } else {
+      for (int i = 0; i < mDisplayedValues.length; i++) {
+
+        /* Don't force the user to type in jan when ja will do */
+        str = str.toLowerCase();
+        if (mDisplayedValues[i].toLowerCase().startsWith(str)) {
+          return mStart + i;
+        }
+      }
+
+      /*
+       * The user might have typed in a number into the month field i.e. 10 instead of OCT so
+       * support that too.
+       */
+      try {
+        return Integer.parseInt(str);
+      } catch (NumberFormatException e) {
+
+        /* Ignore as if it's not a number we don't care */
+      }
+    }
+    return mStart;
+  }
+
+  /**
+   * @return the current value.
+   */
+  public int getCurrent() {
+    return mCurrent;
+  }
+}
\ No newline at end of file
diff --git a/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/widget/NumberPickerButton.java b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/widget/NumberPickerButton.java
new file mode 100644
index 0000000..cb8d184
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/com/googlecode/android_scripting/widget/NumberPickerButton.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.widget.ImageButton;
+
+import com.googlecode.android_scripting.R;
+
+/**
+ * This class exists purely to cancel long click events.
+ */
+public class NumberPickerButton extends ImageButton {
+
+  private NumberPicker mNumberPicker;
+
+  public NumberPickerButton(Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+  }
+
+  public NumberPickerButton(Context context, AttributeSet attrs) {
+    super(context, attrs);
+  }
+
+  public NumberPickerButton(Context context) {
+    super(context);
+  }
+
+  public void setNumberPicker(NumberPicker picker) {
+    mNumberPicker = picker;
+  }
+
+  @Override
+  public boolean onTouchEvent(MotionEvent event) {
+    cancelLongpressIfRequired(event);
+    return super.onTouchEvent(event);
+  }
+
+  @Override
+  public boolean onTrackballEvent(MotionEvent event) {
+    cancelLongpressIfRequired(event);
+    return super.onTrackballEvent(event);
+  }
+
+  @Override
+  public boolean onKeyUp(int keyCode, KeyEvent event) {
+    if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER) || (keyCode == KeyEvent.KEYCODE_ENTER)) {
+      cancelLongpress();
+    }
+    return super.onKeyUp(keyCode, event);
+  }
+
+  private void cancelLongpressIfRequired(MotionEvent event) {
+    if ((event.getAction() == MotionEvent.ACTION_CANCEL)
+        || (event.getAction() == MotionEvent.ACTION_UP)) {
+      cancelLongpress();
+    }
+  }
+
+  private void cancelLongpress() {
+    if (R.id.increment == getId()) {
+      mNumberPicker.cancelIncrement();
+    } else if (R.id.decrement == getId()) {
+      mNumberPicker.cancelDecrement();
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/de/mud/terminal/Precomposer.java b/ScriptingLayerForAndroid/src/de/mud/terminal/Precomposer.java
new file mode 100644
index 0000000..edad64c
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/de/mud/terminal/Precomposer.java
@@ -0,0 +1,1052 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 de.mud.terminal;
+
+/**
+ * @author Kenny Root
+ * This data was taken from xterm's precompose.c
+ */
+public class Precomposer {
+	public final static char precompositions[][] = {
+		{ 0x226E, 0x003C, 0x0338},
+		{ 0x2260, 0x003D, 0x0338},
+		{ 0x226F, 0x003E, 0x0338},
+		{ 0x00C0, 0x0041, 0x0300},
+		{ 0x00C1, 0x0041, 0x0301},
+		{ 0x00C2, 0x0041, 0x0302},
+		{ 0x00C3, 0x0041, 0x0303},
+		{ 0x0100, 0x0041, 0x0304},
+		{ 0x0102, 0x0041, 0x0306},
+		{ 0x0226, 0x0041, 0x0307},
+		{ 0x00C4, 0x0041, 0x0308},
+		{ 0x1EA2, 0x0041, 0x0309},
+		{ 0x00C5, 0x0041, 0x030A},
+		{ 0x01CD, 0x0041, 0x030C},
+		{ 0x0200, 0x0041, 0x030F},
+		{ 0x0202, 0x0041, 0x0311},
+		{ 0x1EA0, 0x0041, 0x0323},
+		{ 0x1E00, 0x0041, 0x0325},
+		{ 0x0104, 0x0041, 0x0328},
+		{ 0x1E02, 0x0042, 0x0307},
+		{ 0x1E04, 0x0042, 0x0323},
+		{ 0x1E06, 0x0042, 0x0331},
+		{ 0x0106, 0x0043, 0x0301},
+		{ 0x0108, 0x0043, 0x0302},
+		{ 0x010A, 0x0043, 0x0307},
+		{ 0x010C, 0x0043, 0x030C},
+		{ 0x00C7, 0x0043, 0x0327},
+		{ 0x1E0A, 0x0044, 0x0307},
+		{ 0x010E, 0x0044, 0x030C},
+		{ 0x1E0C, 0x0044, 0x0323},
+		{ 0x1E10, 0x0044, 0x0327},
+		{ 0x1E12, 0x0044, 0x032D},
+		{ 0x1E0E, 0x0044, 0x0331},
+		{ 0x00C8, 0x0045, 0x0300},
+		{ 0x00C9, 0x0045, 0x0301},
+		{ 0x00CA, 0x0045, 0x0302},
+		{ 0x1EBC, 0x0045, 0x0303},
+		{ 0x0112, 0x0045, 0x0304},
+		{ 0x0114, 0x0045, 0x0306},
+		{ 0x0116, 0x0045, 0x0307},
+		{ 0x00CB, 0x0045, 0x0308},
+		{ 0x1EBA, 0x0045, 0x0309},
+		{ 0x011A, 0x0045, 0x030C},
+		{ 0x0204, 0x0045, 0x030F},
+		{ 0x0206, 0x0045, 0x0311},
+		{ 0x1EB8, 0x0045, 0x0323},
+		{ 0x0228, 0x0045, 0x0327},
+		{ 0x0118, 0x0045, 0x0328},
+		{ 0x1E18, 0x0045, 0x032D},
+		{ 0x1E1A, 0x0045, 0x0330},
+		{ 0x1E1E, 0x0046, 0x0307},
+		{ 0x01F4, 0x0047, 0x0301},
+		{ 0x011C, 0x0047, 0x0302},
+		{ 0x1E20, 0x0047, 0x0304},
+		{ 0x011E, 0x0047, 0x0306},
+		{ 0x0120, 0x0047, 0x0307},
+		{ 0x01E6, 0x0047, 0x030C},
+		{ 0x0122, 0x0047, 0x0327},
+		{ 0x0124, 0x0048, 0x0302},
+		{ 0x1E22, 0x0048, 0x0307},
+		{ 0x1E26, 0x0048, 0x0308},
+		{ 0x021E, 0x0048, 0x030C},
+		{ 0x1E24, 0x0048, 0x0323},
+		{ 0x1E28, 0x0048, 0x0327},
+		{ 0x1E2A, 0x0048, 0x032E},
+		{ 0x00CC, 0x0049, 0x0300},
+		{ 0x00CD, 0x0049, 0x0301},
+		{ 0x00CE, 0x0049, 0x0302},
+		{ 0x0128, 0x0049, 0x0303},
+		{ 0x012A, 0x0049, 0x0304},
+		{ 0x012C, 0x0049, 0x0306},
+		{ 0x0130, 0x0049, 0x0307},
+		{ 0x00CF, 0x0049, 0x0308},
+		{ 0x1EC8, 0x0049, 0x0309},
+		{ 0x01CF, 0x0049, 0x030C},
+		{ 0x0208, 0x0049, 0x030F},
+		{ 0x020A, 0x0049, 0x0311},
+		{ 0x1ECA, 0x0049, 0x0323},
+		{ 0x012E, 0x0049, 0x0328},
+		{ 0x1E2C, 0x0049, 0x0330},
+		{ 0x0134, 0x004A, 0x0302},
+		{ 0x1E30, 0x004B, 0x0301},
+		{ 0x01E8, 0x004B, 0x030C},
+		{ 0x1E32, 0x004B, 0x0323},
+		{ 0x0136, 0x004B, 0x0327},
+		{ 0x1E34, 0x004B, 0x0331},
+		{ 0x0139, 0x004C, 0x0301},
+		{ 0x013D, 0x004C, 0x030C},
+		{ 0x1E36, 0x004C, 0x0323},
+		{ 0x013B, 0x004C, 0x0327},
+		{ 0x1E3C, 0x004C, 0x032D},
+		{ 0x1E3A, 0x004C, 0x0331},
+		{ 0x1E3E, 0x004D, 0x0301},
+		{ 0x1E40, 0x004D, 0x0307},
+		{ 0x1E42, 0x004D, 0x0323},
+		{ 0x01F8, 0x004E, 0x0300},
+		{ 0x0143, 0x004E, 0x0301},
+		{ 0x00D1, 0x004E, 0x0303},
+		{ 0x1E44, 0x004E, 0x0307},
+		{ 0x0147, 0x004E, 0x030C},
+		{ 0x1E46, 0x004E, 0x0323},
+		{ 0x0145, 0x004E, 0x0327},
+		{ 0x1E4A, 0x004E, 0x032D},
+		{ 0x1E48, 0x004E, 0x0331},
+		{ 0x00D2, 0x004F, 0x0300},
+		{ 0x00D3, 0x004F, 0x0301},
+		{ 0x00D4, 0x004F, 0x0302},
+		{ 0x00D5, 0x004F, 0x0303},
+		{ 0x014C, 0x004F, 0x0304},
+		{ 0x014E, 0x004F, 0x0306},
+		{ 0x022E, 0x004F, 0x0307},
+		{ 0x00D6, 0x004F, 0x0308},
+		{ 0x1ECE, 0x004F, 0x0309},
+		{ 0x0150, 0x004F, 0x030B},
+		{ 0x01D1, 0x004F, 0x030C},
+		{ 0x020C, 0x004F, 0x030F},
+		{ 0x020E, 0x004F, 0x0311},
+		{ 0x01A0, 0x004F, 0x031B},
+		{ 0x1ECC, 0x004F, 0x0323},
+		{ 0x01EA, 0x004F, 0x0328},
+		{ 0x1E54, 0x0050, 0x0301},
+		{ 0x1E56, 0x0050, 0x0307},
+		{ 0x0154, 0x0052, 0x0301},
+		{ 0x1E58, 0x0052, 0x0307},
+		{ 0x0158, 0x0052, 0x030C},
+		{ 0x0210, 0x0052, 0x030F},
+		{ 0x0212, 0x0052, 0x0311},
+		{ 0x1E5A, 0x0052, 0x0323},
+		{ 0x0156, 0x0052, 0x0327},
+		{ 0x1E5E, 0x0052, 0x0331},
+		{ 0x015A, 0x0053, 0x0301},
+		{ 0x015C, 0x0053, 0x0302},
+		{ 0x1E60, 0x0053, 0x0307},
+		{ 0x0160, 0x0053, 0x030C},
+		{ 0x1E62, 0x0053, 0x0323},
+		{ 0x0218, 0x0053, 0x0326},
+		{ 0x015E, 0x0053, 0x0327},
+		{ 0x1E6A, 0x0054, 0x0307},
+		{ 0x0164, 0x0054, 0x030C},
+		{ 0x1E6C, 0x0054, 0x0323},
+		{ 0x021A, 0x0054, 0x0326},
+		{ 0x0162, 0x0054, 0x0327},
+		{ 0x1E70, 0x0054, 0x032D},
+		{ 0x1E6E, 0x0054, 0x0331},
+		{ 0x00D9, 0x0055, 0x0300},
+		{ 0x00DA, 0x0055, 0x0301},
+		{ 0x00DB, 0x0055, 0x0302},
+		{ 0x0168, 0x0055, 0x0303},
+		{ 0x016A, 0x0055, 0x0304},
+		{ 0x016C, 0x0055, 0x0306},
+		{ 0x00DC, 0x0055, 0x0308},
+		{ 0x1EE6, 0x0055, 0x0309},
+		{ 0x016E, 0x0055, 0x030A},
+		{ 0x0170, 0x0055, 0x030B},
+		{ 0x01D3, 0x0055, 0x030C},
+		{ 0x0214, 0x0055, 0x030F},
+		{ 0x0216, 0x0055, 0x0311},
+		{ 0x01AF, 0x0055, 0x031B},
+		{ 0x1EE4, 0x0055, 0x0323},
+		{ 0x1E72, 0x0055, 0x0324},
+		{ 0x0172, 0x0055, 0x0328},
+		{ 0x1E76, 0x0055, 0x032D},
+		{ 0x1E74, 0x0055, 0x0330},
+		{ 0x1E7C, 0x0056, 0x0303},
+		{ 0x1E7E, 0x0056, 0x0323},
+		{ 0x1E80, 0x0057, 0x0300},
+		{ 0x1E82, 0x0057, 0x0301},
+		{ 0x0174, 0x0057, 0x0302},
+		{ 0x1E86, 0x0057, 0x0307},
+		{ 0x1E84, 0x0057, 0x0308},
+		{ 0x1E88, 0x0057, 0x0323},
+		{ 0x1E8A, 0x0058, 0x0307},
+		{ 0x1E8C, 0x0058, 0x0308},
+		{ 0x1EF2, 0x0059, 0x0300},
+		{ 0x00DD, 0x0059, 0x0301},
+		{ 0x0176, 0x0059, 0x0302},
+		{ 0x1EF8, 0x0059, 0x0303},
+		{ 0x0232, 0x0059, 0x0304},
+		{ 0x1E8E, 0x0059, 0x0307},
+		{ 0x0178, 0x0059, 0x0308},
+		{ 0x1EF6, 0x0059, 0x0309},
+		{ 0x1EF4, 0x0059, 0x0323},
+		{ 0x0179, 0x005A, 0x0301},
+		{ 0x1E90, 0x005A, 0x0302},
+		{ 0x017B, 0x005A, 0x0307},
+		{ 0x017D, 0x005A, 0x030C},
+		{ 0x1E92, 0x005A, 0x0323},
+		{ 0x1E94, 0x005A, 0x0331},
+		{ 0x00E0, 0x0061, 0x0300},
+		{ 0x00E1, 0x0061, 0x0301},
+		{ 0x00E2, 0x0061, 0x0302},
+		{ 0x00E3, 0x0061, 0x0303},
+		{ 0x0101, 0x0061, 0x0304},
+		{ 0x0103, 0x0061, 0x0306},
+		{ 0x0227, 0x0061, 0x0307},
+		{ 0x00E4, 0x0061, 0x0308},
+		{ 0x1EA3, 0x0061, 0x0309},
+		{ 0x00E5, 0x0061, 0x030A},
+		{ 0x01CE, 0x0061, 0x030C},
+		{ 0x0201, 0x0061, 0x030F},
+		{ 0x0203, 0x0061, 0x0311},
+		{ 0x1EA1, 0x0061, 0x0323},
+		{ 0x1E01, 0x0061, 0x0325},
+		{ 0x0105, 0x0061, 0x0328},
+		{ 0x1E03, 0x0062, 0x0307},
+		{ 0x1E05, 0x0062, 0x0323},
+		{ 0x1E07, 0x0062, 0x0331},
+		{ 0x0107, 0x0063, 0x0301},
+		{ 0x0109, 0x0063, 0x0302},
+		{ 0x010B, 0x0063, 0x0307},
+		{ 0x010D, 0x0063, 0x030C},
+		{ 0x00E7, 0x0063, 0x0327},
+		{ 0x1E0B, 0x0064, 0x0307},
+		{ 0x010F, 0x0064, 0x030C},
+		{ 0x1E0D, 0x0064, 0x0323},
+		{ 0x1E11, 0x0064, 0x0327},
+		{ 0x1E13, 0x0064, 0x032D},
+		{ 0x1E0F, 0x0064, 0x0331},
+		{ 0x00E8, 0x0065, 0x0300},
+		{ 0x00E9, 0x0065, 0x0301},
+		{ 0x00EA, 0x0065, 0x0302},
+		{ 0x1EBD, 0x0065, 0x0303},
+		{ 0x0113, 0x0065, 0x0304},
+		{ 0x0115, 0x0065, 0x0306},
+		{ 0x0117, 0x0065, 0x0307},
+		{ 0x00EB, 0x0065, 0x0308},
+		{ 0x1EBB, 0x0065, 0x0309},
+		{ 0x011B, 0x0065, 0x030C},
+		{ 0x0205, 0x0065, 0x030F},
+		{ 0x0207, 0x0065, 0x0311},
+		{ 0x1EB9, 0x0065, 0x0323},
+		{ 0x0229, 0x0065, 0x0327},
+		{ 0x0119, 0x0065, 0x0328},
+		{ 0x1E19, 0x0065, 0x032D},
+		{ 0x1E1B, 0x0065, 0x0330},
+		{ 0x1E1F, 0x0066, 0x0307},
+		{ 0x01F5, 0x0067, 0x0301},
+		{ 0x011D, 0x0067, 0x0302},
+		{ 0x1E21, 0x0067, 0x0304},
+		{ 0x011F, 0x0067, 0x0306},
+		{ 0x0121, 0x0067, 0x0307},
+		{ 0x01E7, 0x0067, 0x030C},
+		{ 0x0123, 0x0067, 0x0327},
+		{ 0x0125, 0x0068, 0x0302},
+		{ 0x1E23, 0x0068, 0x0307},
+		{ 0x1E27, 0x0068, 0x0308},
+		{ 0x021F, 0x0068, 0x030C},
+		{ 0x1E25, 0x0068, 0x0323},
+		{ 0x1E29, 0x0068, 0x0327},
+		{ 0x1E2B, 0x0068, 0x032E},
+		{ 0x1E96, 0x0068, 0x0331},
+		{ 0x00EC, 0x0069, 0x0300},
+		{ 0x00ED, 0x0069, 0x0301},
+		{ 0x00EE, 0x0069, 0x0302},
+		{ 0x0129, 0x0069, 0x0303},
+		{ 0x012B, 0x0069, 0x0304},
+		{ 0x012D, 0x0069, 0x0306},
+		{ 0x00EF, 0x0069, 0x0308},
+		{ 0x1EC9, 0x0069, 0x0309},
+		{ 0x01D0, 0x0069, 0x030C},
+		{ 0x0209, 0x0069, 0x030F},
+		{ 0x020B, 0x0069, 0x0311},
+		{ 0x1ECB, 0x0069, 0x0323},
+		{ 0x012F, 0x0069, 0x0328},
+		{ 0x1E2D, 0x0069, 0x0330},
+		{ 0x0135, 0x006A, 0x0302},
+		{ 0x01F0, 0x006A, 0x030C},
+		{ 0x1E31, 0x006B, 0x0301},
+		{ 0x01E9, 0x006B, 0x030C},
+		{ 0x1E33, 0x006B, 0x0323},
+		{ 0x0137, 0x006B, 0x0327},
+		{ 0x1E35, 0x006B, 0x0331},
+		{ 0x013A, 0x006C, 0x0301},
+		{ 0x013E, 0x006C, 0x030C},
+		{ 0x1E37, 0x006C, 0x0323},
+		{ 0x013C, 0x006C, 0x0327},
+		{ 0x1E3D, 0x006C, 0x032D},
+		{ 0x1E3B, 0x006C, 0x0331},
+		{ 0x1E3F, 0x006D, 0x0301},
+		{ 0x1E41, 0x006D, 0x0307},
+		{ 0x1E43, 0x006D, 0x0323},
+		{ 0x01F9, 0x006E, 0x0300},
+		{ 0x0144, 0x006E, 0x0301},
+		{ 0x00F1, 0x006E, 0x0303},
+		{ 0x1E45, 0x006E, 0x0307},
+		{ 0x0148, 0x006E, 0x030C},
+		{ 0x1E47, 0x006E, 0x0323},
+		{ 0x0146, 0x006E, 0x0327},
+		{ 0x1E4B, 0x006E, 0x032D},
+		{ 0x1E49, 0x006E, 0x0331},
+		{ 0x00F2, 0x006F, 0x0300},
+		{ 0x00F3, 0x006F, 0x0301},
+		{ 0x00F4, 0x006F, 0x0302},
+		{ 0x00F5, 0x006F, 0x0303},
+		{ 0x014D, 0x006F, 0x0304},
+		{ 0x014F, 0x006F, 0x0306},
+		{ 0x022F, 0x006F, 0x0307},
+		{ 0x00F6, 0x006F, 0x0308},
+		{ 0x1ECF, 0x006F, 0x0309},
+		{ 0x0151, 0x006F, 0x030B},
+		{ 0x01D2, 0x006F, 0x030C},
+		{ 0x020D, 0x006F, 0x030F},
+		{ 0x020F, 0x006F, 0x0311},
+		{ 0x01A1, 0x006F, 0x031B},
+		{ 0x1ECD, 0x006F, 0x0323},
+		{ 0x01EB, 0x006F, 0x0328},
+		{ 0x1E55, 0x0070, 0x0301},
+		{ 0x1E57, 0x0070, 0x0307},
+		{ 0x0155, 0x0072, 0x0301},
+		{ 0x1E59, 0x0072, 0x0307},
+		{ 0x0159, 0x0072, 0x030C},
+		{ 0x0211, 0x0072, 0x030F},
+		{ 0x0213, 0x0072, 0x0311},
+		{ 0x1E5B, 0x0072, 0x0323},
+		{ 0x0157, 0x0072, 0x0327},
+		{ 0x1E5F, 0x0072, 0x0331},
+		{ 0x015B, 0x0073, 0x0301},
+		{ 0x015D, 0x0073, 0x0302},
+		{ 0x1E61, 0x0073, 0x0307},
+		{ 0x0161, 0x0073, 0x030C},
+		{ 0x1E63, 0x0073, 0x0323},
+		{ 0x0219, 0x0073, 0x0326},
+		{ 0x015F, 0x0073, 0x0327},
+		{ 0x1E6B, 0x0074, 0x0307},
+		{ 0x1E97, 0x0074, 0x0308},
+		{ 0x0165, 0x0074, 0x030C},
+		{ 0x1E6D, 0x0074, 0x0323},
+		{ 0x021B, 0x0074, 0x0326},
+		{ 0x0163, 0x0074, 0x0327},
+		{ 0x1E71, 0x0074, 0x032D},
+		{ 0x1E6F, 0x0074, 0x0331},
+		{ 0x00F9, 0x0075, 0x0300},
+		{ 0x00FA, 0x0075, 0x0301},
+		{ 0x00FB, 0x0075, 0x0302},
+		{ 0x0169, 0x0075, 0x0303},
+		{ 0x016B, 0x0075, 0x0304},
+		{ 0x016D, 0x0075, 0x0306},
+		{ 0x00FC, 0x0075, 0x0308},
+		{ 0x1EE7, 0x0075, 0x0309},
+		{ 0x016F, 0x0075, 0x030A},
+		{ 0x0171, 0x0075, 0x030B},
+		{ 0x01D4, 0x0075, 0x030C},
+		{ 0x0215, 0x0075, 0x030F},
+		{ 0x0217, 0x0075, 0x0311},
+		{ 0x01B0, 0x0075, 0x031B},
+		{ 0x1EE5, 0x0075, 0x0323},
+		{ 0x1E73, 0x0075, 0x0324},
+		{ 0x0173, 0x0075, 0x0328},
+		{ 0x1E77, 0x0075, 0x032D},
+		{ 0x1E75, 0x0075, 0x0330},
+		{ 0x1E7D, 0x0076, 0x0303},
+		{ 0x1E7F, 0x0076, 0x0323},
+		{ 0x1E81, 0x0077, 0x0300},
+		{ 0x1E83, 0x0077, 0x0301},
+		{ 0x0175, 0x0077, 0x0302},
+		{ 0x1E87, 0x0077, 0x0307},
+		{ 0x1E85, 0x0077, 0x0308},
+		{ 0x1E98, 0x0077, 0x030A},
+		{ 0x1E89, 0x0077, 0x0323},
+		{ 0x1E8B, 0x0078, 0x0307},
+		{ 0x1E8D, 0x0078, 0x0308},
+		{ 0x1EF3, 0x0079, 0x0300},
+		{ 0x00FD, 0x0079, 0x0301},
+		{ 0x0177, 0x0079, 0x0302},
+		{ 0x1EF9, 0x0079, 0x0303},
+		{ 0x0233, 0x0079, 0x0304},
+		{ 0x1E8F, 0x0079, 0x0307},
+		{ 0x00FF, 0x0079, 0x0308},
+		{ 0x1EF7, 0x0079, 0x0309},
+		{ 0x1E99, 0x0079, 0x030A},
+		{ 0x1EF5, 0x0079, 0x0323},
+		{ 0x017A, 0x007A, 0x0301},
+		{ 0x1E91, 0x007A, 0x0302},
+		{ 0x017C, 0x007A, 0x0307},
+		{ 0x017E, 0x007A, 0x030C},
+		{ 0x1E93, 0x007A, 0x0323},
+		{ 0x1E95, 0x007A, 0x0331},
+		{ 0x1FED, 0x00A8, 0x0300},
+		{ 0x0385, 0x00A8, 0x0301},
+		{ 0x1FC1, 0x00A8, 0x0342},
+		{ 0x1EA6, 0x00C2, 0x0300},
+		{ 0x1EA4, 0x00C2, 0x0301},
+		{ 0x1EAA, 0x00C2, 0x0303},
+		{ 0x1EA8, 0x00C2, 0x0309},
+		{ 0x01DE, 0x00C4, 0x0304},
+		{ 0x01FA, 0x00C5, 0x0301},
+		{ 0x01FC, 0x00C6, 0x0301},
+		{ 0x01E2, 0x00C6, 0x0304},
+		{ 0x1E08, 0x00C7, 0x0301},
+		{ 0x1EC0, 0x00CA, 0x0300},
+		{ 0x1EBE, 0x00CA, 0x0301},
+		{ 0x1EC4, 0x00CA, 0x0303},
+		{ 0x1EC2, 0x00CA, 0x0309},
+		{ 0x1E2E, 0x00CF, 0x0301},
+		{ 0x1ED2, 0x00D4, 0x0300},
+		{ 0x1ED0, 0x00D4, 0x0301},
+		{ 0x1ED6, 0x00D4, 0x0303},
+		{ 0x1ED4, 0x00D4, 0x0309},
+		{ 0x1E4C, 0x00D5, 0x0301},
+		{ 0x022C, 0x00D5, 0x0304},
+		{ 0x1E4E, 0x00D5, 0x0308},
+		{ 0x022A, 0x00D6, 0x0304},
+		{ 0x01FE, 0x00D8, 0x0301},
+		{ 0x01DB, 0x00DC, 0x0300},
+		{ 0x01D7, 0x00DC, 0x0301},
+		{ 0x01D5, 0x00DC, 0x0304},
+		{ 0x01D9, 0x00DC, 0x030C},
+		{ 0x1EA7, 0x00E2, 0x0300},
+		{ 0x1EA5, 0x00E2, 0x0301},
+		{ 0x1EAB, 0x00E2, 0x0303},
+		{ 0x1EA9, 0x00E2, 0x0309},
+		{ 0x01DF, 0x00E4, 0x0304},
+		{ 0x01FB, 0x00E5, 0x0301},
+		{ 0x01FD, 0x00E6, 0x0301},
+		{ 0x01E3, 0x00E6, 0x0304},
+		{ 0x1E09, 0x00E7, 0x0301},
+		{ 0x1EC1, 0x00EA, 0x0300},
+		{ 0x1EBF, 0x00EA, 0x0301},
+		{ 0x1EC5, 0x00EA, 0x0303},
+		{ 0x1EC3, 0x00EA, 0x0309},
+		{ 0x1E2F, 0x00EF, 0x0301},
+		{ 0x1ED3, 0x00F4, 0x0300},
+		{ 0x1ED1, 0x00F4, 0x0301},
+		{ 0x1ED7, 0x00F4, 0x0303},
+		{ 0x1ED5, 0x00F4, 0x0309},
+		{ 0x1E4D, 0x00F5, 0x0301},
+		{ 0x022D, 0x00F5, 0x0304},
+		{ 0x1E4F, 0x00F5, 0x0308},
+		{ 0x022B, 0x00F6, 0x0304},
+		{ 0x01FF, 0x00F8, 0x0301},
+		{ 0x01DC, 0x00FC, 0x0300},
+		{ 0x01D8, 0x00FC, 0x0301},
+		{ 0x01D6, 0x00FC, 0x0304},
+		{ 0x01DA, 0x00FC, 0x030C},
+		{ 0x1EB0, 0x0102, 0x0300},
+		{ 0x1EAE, 0x0102, 0x0301},
+		{ 0x1EB4, 0x0102, 0x0303},
+		{ 0x1EB2, 0x0102, 0x0309},
+		{ 0x1EB1, 0x0103, 0x0300},
+		{ 0x1EAF, 0x0103, 0x0301},
+		{ 0x1EB5, 0x0103, 0x0303},
+		{ 0x1EB3, 0x0103, 0x0309},
+		{ 0x1E14, 0x0112, 0x0300},
+		{ 0x1E16, 0x0112, 0x0301},
+		{ 0x1E15, 0x0113, 0x0300},
+		{ 0x1E17, 0x0113, 0x0301},
+		{ 0x1E50, 0x014C, 0x0300},
+		{ 0x1E52, 0x014C, 0x0301},
+		{ 0x1E51, 0x014D, 0x0300},
+		{ 0x1E53, 0x014D, 0x0301},
+		{ 0x1E64, 0x015A, 0x0307},
+		{ 0x1E65, 0x015B, 0x0307},
+		{ 0x1E66, 0x0160, 0x0307},
+		{ 0x1E67, 0x0161, 0x0307},
+		{ 0x1E78, 0x0168, 0x0301},
+		{ 0x1E79, 0x0169, 0x0301},
+		{ 0x1E7A, 0x016A, 0x0308},
+		{ 0x1E7B, 0x016B, 0x0308},
+		{ 0x1E9B, 0x017F, 0x0307},
+		{ 0x1EDC, 0x01A0, 0x0300},
+		{ 0x1EDA, 0x01A0, 0x0301},
+		{ 0x1EE0, 0x01A0, 0x0303},
+		{ 0x1EDE, 0x01A0, 0x0309},
+		{ 0x1EE2, 0x01A0, 0x0323},
+		{ 0x1EDD, 0x01A1, 0x0300},
+		{ 0x1EDB, 0x01A1, 0x0301},
+		{ 0x1EE1, 0x01A1, 0x0303},
+		{ 0x1EDF, 0x01A1, 0x0309},
+		{ 0x1EE3, 0x01A1, 0x0323},
+		{ 0x1EEA, 0x01AF, 0x0300},
+		{ 0x1EE8, 0x01AF, 0x0301},
+		{ 0x1EEE, 0x01AF, 0x0303},
+		{ 0x1EEC, 0x01AF, 0x0309},
+		{ 0x1EF0, 0x01AF, 0x0323},
+		{ 0x1EEB, 0x01B0, 0x0300},
+		{ 0x1EE9, 0x01B0, 0x0301},
+		{ 0x1EEF, 0x01B0, 0x0303},
+		{ 0x1EED, 0x01B0, 0x0309},
+		{ 0x1EF1, 0x01B0, 0x0323},
+		{ 0x01EE, 0x01B7, 0x030C},
+		{ 0x01EC, 0x01EA, 0x0304},
+		{ 0x01ED, 0x01EB, 0x0304},
+		{ 0x01E0, 0x0226, 0x0304},
+		{ 0x01E1, 0x0227, 0x0304},
+		{ 0x1E1C, 0x0228, 0x0306},
+		{ 0x1E1D, 0x0229, 0x0306},
+		{ 0x0230, 0x022E, 0x0304},
+		{ 0x0231, 0x022F, 0x0304},
+		{ 0x01EF, 0x0292, 0x030C},
+		{ 0x0344, 0x0308, 0x0301},
+		{ 0x1FBA, 0x0391, 0x0300},
+		{ 0x0386, 0x0391, 0x0301},
+		{ 0x1FB9, 0x0391, 0x0304},
+		{ 0x1FB8, 0x0391, 0x0306},
+		{ 0x1F08, 0x0391, 0x0313},
+		{ 0x1F09, 0x0391, 0x0314},
+		{ 0x1FBC, 0x0391, 0x0345},
+		{ 0x1FC8, 0x0395, 0x0300},
+		{ 0x0388, 0x0395, 0x0301},
+		{ 0x1F18, 0x0395, 0x0313},
+		{ 0x1F19, 0x0395, 0x0314},
+		{ 0x1FCA, 0x0397, 0x0300},
+		{ 0x0389, 0x0397, 0x0301},
+		{ 0x1F28, 0x0397, 0x0313},
+		{ 0x1F29, 0x0397, 0x0314},
+		{ 0x1FCC, 0x0397, 0x0345},
+		{ 0x1FDA, 0x0399, 0x0300},
+		{ 0x038A, 0x0399, 0x0301},
+		{ 0x1FD9, 0x0399, 0x0304},
+		{ 0x1FD8, 0x0399, 0x0306},
+		{ 0x03AA, 0x0399, 0x0308},
+		{ 0x1F38, 0x0399, 0x0313},
+		{ 0x1F39, 0x0399, 0x0314},
+		{ 0x1FF8, 0x039F, 0x0300},
+		{ 0x038C, 0x039F, 0x0301},
+		{ 0x1F48, 0x039F, 0x0313},
+		{ 0x1F49, 0x039F, 0x0314},
+		{ 0x1FEC, 0x03A1, 0x0314},
+		{ 0x1FEA, 0x03A5, 0x0300},
+		{ 0x038E, 0x03A5, 0x0301},
+		{ 0x1FE9, 0x03A5, 0x0304},
+		{ 0x1FE8, 0x03A5, 0x0306},
+		{ 0x03AB, 0x03A5, 0x0308},
+		{ 0x1F59, 0x03A5, 0x0314},
+		{ 0x1FFA, 0x03A9, 0x0300},
+		{ 0x038F, 0x03A9, 0x0301},
+		{ 0x1F68, 0x03A9, 0x0313},
+		{ 0x1F69, 0x03A9, 0x0314},
+		{ 0x1FFC, 0x03A9, 0x0345},
+		{ 0x1FB4, 0x03AC, 0x0345},
+		{ 0x1FC4, 0x03AE, 0x0345},
+		{ 0x1F70, 0x03B1, 0x0300},
+		{ 0x03AC, 0x03B1, 0x0301},
+		{ 0x1FB1, 0x03B1, 0x0304},
+		{ 0x1FB0, 0x03B1, 0x0306},
+		{ 0x1F00, 0x03B1, 0x0313},
+		{ 0x1F01, 0x03B1, 0x0314},
+		{ 0x1FB6, 0x03B1, 0x0342},
+		{ 0x1FB3, 0x03B1, 0x0345},
+		{ 0x1F72, 0x03B5, 0x0300},
+		{ 0x03AD, 0x03B5, 0x0301},
+		{ 0x1F10, 0x03B5, 0x0313},
+		{ 0x1F11, 0x03B5, 0x0314},
+		{ 0x1F74, 0x03B7, 0x0300},
+		{ 0x03AE, 0x03B7, 0x0301},
+		{ 0x1F20, 0x03B7, 0x0313},
+		{ 0x1F21, 0x03B7, 0x0314},
+		{ 0x1FC6, 0x03B7, 0x0342},
+		{ 0x1FC3, 0x03B7, 0x0345},
+		{ 0x1F76, 0x03B9, 0x0300},
+		{ 0x03AF, 0x03B9, 0x0301},
+		{ 0x1FD1, 0x03B9, 0x0304},
+		{ 0x1FD0, 0x03B9, 0x0306},
+		{ 0x03CA, 0x03B9, 0x0308},
+		{ 0x1F30, 0x03B9, 0x0313},
+		{ 0x1F31, 0x03B9, 0x0314},
+		{ 0x1FD6, 0x03B9, 0x0342},
+		{ 0x1F78, 0x03BF, 0x0300},
+		{ 0x03CC, 0x03BF, 0x0301},
+		{ 0x1F40, 0x03BF, 0x0313},
+		{ 0x1F41, 0x03BF, 0x0314},
+		{ 0x1FE4, 0x03C1, 0x0313},
+		{ 0x1FE5, 0x03C1, 0x0314},
+		{ 0x1F7A, 0x03C5, 0x0300},
+		{ 0x03CD, 0x03C5, 0x0301},
+		{ 0x1FE1, 0x03C5, 0x0304},
+		{ 0x1FE0, 0x03C5, 0x0306},
+		{ 0x03CB, 0x03C5, 0x0308},
+		{ 0x1F50, 0x03C5, 0x0313},
+		{ 0x1F51, 0x03C5, 0x0314},
+		{ 0x1FE6, 0x03C5, 0x0342},
+		{ 0x1F7C, 0x03C9, 0x0300},
+		{ 0x03CE, 0x03C9, 0x0301},
+		{ 0x1F60, 0x03C9, 0x0313},
+		{ 0x1F61, 0x03C9, 0x0314},
+		{ 0x1FF6, 0x03C9, 0x0342},
+		{ 0x1FF3, 0x03C9, 0x0345},
+		{ 0x1FD2, 0x03CA, 0x0300},
+		{ 0x0390, 0x03CA, 0x0301},
+		{ 0x1FD7, 0x03CA, 0x0342},
+		{ 0x1FE2, 0x03CB, 0x0300},
+		{ 0x03B0, 0x03CB, 0x0301},
+		{ 0x1FE7, 0x03CB, 0x0342},
+		{ 0x1FF4, 0x03CE, 0x0345},
+		{ 0x03D3, 0x03D2, 0x0301},
+		{ 0x03D4, 0x03D2, 0x0308},
+		{ 0x0407, 0x0406, 0x0308},
+		{ 0x04D0, 0x0410, 0x0306},
+		{ 0x04D2, 0x0410, 0x0308},
+		{ 0x0403, 0x0413, 0x0301},
+		{ 0x0400, 0x0415, 0x0300},
+		{ 0x04D6, 0x0415, 0x0306},
+		{ 0x0401, 0x0415, 0x0308},
+		{ 0x04C1, 0x0416, 0x0306},
+		{ 0x04DC, 0x0416, 0x0308},
+		{ 0x04DE, 0x0417, 0x0308},
+		{ 0x040D, 0x0418, 0x0300},
+		{ 0x04E2, 0x0418, 0x0304},
+		{ 0x0419, 0x0418, 0x0306},
+		{ 0x04E4, 0x0418, 0x0308},
+		{ 0x040C, 0x041A, 0x0301},
+		{ 0x04E6, 0x041E, 0x0308},
+		{ 0x04EE, 0x0423, 0x0304},
+		{ 0x040E, 0x0423, 0x0306},
+		{ 0x04F0, 0x0423, 0x0308},
+		{ 0x04F2, 0x0423, 0x030B},
+		{ 0x04F4, 0x0427, 0x0308},
+		{ 0x04F8, 0x042B, 0x0308},
+		{ 0x04EC, 0x042D, 0x0308},
+		{ 0x04D1, 0x0430, 0x0306},
+		{ 0x04D3, 0x0430, 0x0308},
+		{ 0x0453, 0x0433, 0x0301},
+		{ 0x0450, 0x0435, 0x0300},
+		{ 0x04D7, 0x0435, 0x0306},
+		{ 0x0451, 0x0435, 0x0308},
+		{ 0x04C2, 0x0436, 0x0306},
+		{ 0x04DD, 0x0436, 0x0308},
+		{ 0x04DF, 0x0437, 0x0308},
+		{ 0x045D, 0x0438, 0x0300},
+		{ 0x04E3, 0x0438, 0x0304},
+		{ 0x0439, 0x0438, 0x0306},
+		{ 0x04E5, 0x0438, 0x0308},
+		{ 0x045C, 0x043A, 0x0301},
+		{ 0x04E7, 0x043E, 0x0308},
+		{ 0x04EF, 0x0443, 0x0304},
+		{ 0x045E, 0x0443, 0x0306},
+		{ 0x04F1, 0x0443, 0x0308},
+		{ 0x04F3, 0x0443, 0x030B},
+		{ 0x04F5, 0x0447, 0x0308},
+		{ 0x04F9, 0x044B, 0x0308},
+		{ 0x04ED, 0x044D, 0x0308},
+		{ 0x0457, 0x0456, 0x0308},
+		{ 0x0476, 0x0474, 0x030F},
+		{ 0x0477, 0x0475, 0x030F},
+		{ 0x04DA, 0x04D8, 0x0308},
+		{ 0x04DB, 0x04D9, 0x0308},
+		{ 0x04EA, 0x04E8, 0x0308},
+		{ 0x04EB, 0x04E9, 0x0308},
+		{ 0xFB2E, 0x05D0, 0x05B7},
+		{ 0xFB2F, 0x05D0, 0x05B8},
+		{ 0xFB30, 0x05D0, 0x05BC},
+		{ 0xFB31, 0x05D1, 0x05BC},
+		{ 0xFB4C, 0x05D1, 0x05BF},
+		{ 0xFB32, 0x05D2, 0x05BC},
+		{ 0xFB33, 0x05D3, 0x05BC},
+		{ 0xFB34, 0x05D4, 0x05BC},
+		{ 0xFB4B, 0x05D5, 0x05B9},
+		{ 0xFB35, 0x05D5, 0x05BC},
+		{ 0xFB36, 0x05D6, 0x05BC},
+		{ 0xFB38, 0x05D8, 0x05BC},
+		{ 0xFB1D, 0x05D9, 0x05B4},
+		{ 0xFB39, 0x05D9, 0x05BC},
+		{ 0xFB3A, 0x05DA, 0x05BC},
+		{ 0xFB3B, 0x05DB, 0x05BC},
+		{ 0xFB4D, 0x05DB, 0x05BF},
+		{ 0xFB3C, 0x05DC, 0x05BC},
+		{ 0xFB3E, 0x05DE, 0x05BC},
+		{ 0xFB40, 0x05E0, 0x05BC},
+		{ 0xFB41, 0x05E1, 0x05BC},
+		{ 0xFB43, 0x05E3, 0x05BC},
+		{ 0xFB44, 0x05E4, 0x05BC},
+		{ 0xFB4E, 0x05E4, 0x05BF},
+		{ 0xFB46, 0x05E6, 0x05BC},
+		{ 0xFB47, 0x05E7, 0x05BC},
+		{ 0xFB48, 0x05E8, 0x05BC},
+		{ 0xFB49, 0x05E9, 0x05BC},
+		{ 0xFB2A, 0x05E9, 0x05C1},
+		{ 0xFB2B, 0x05E9, 0x05C2},
+		{ 0xFB4A, 0x05EA, 0x05BC},
+		{ 0xFB1F, 0x05F2, 0x05B7},
+		{ 0x0622, 0x0627, 0x0653},
+		{ 0x0623, 0x0627, 0x0654},
+		{ 0x0625, 0x0627, 0x0655},
+		{ 0x0624, 0x0648, 0x0654},
+		{ 0x0626, 0x064A, 0x0654},
+		{ 0x06C2, 0x06C1, 0x0654},
+		{ 0x06D3, 0x06D2, 0x0654},
+		{ 0x06C0, 0x06D5, 0x0654},
+		{ 0x0958, 0x0915, 0x093C},
+		{ 0x0959, 0x0916, 0x093C},
+		{ 0x095A, 0x0917, 0x093C},
+		{ 0x095B, 0x091C, 0x093C},
+		{ 0x095C, 0x0921, 0x093C},
+		{ 0x095D, 0x0922, 0x093C},
+		{ 0x0929, 0x0928, 0x093C},
+		{ 0x095E, 0x092B, 0x093C},
+		{ 0x095F, 0x092F, 0x093C},
+		{ 0x0931, 0x0930, 0x093C},
+		{ 0x0934, 0x0933, 0x093C},
+		{ 0x09DC, 0x09A1, 0x09BC},
+		{ 0x09DD, 0x09A2, 0x09BC},
+		{ 0x09DF, 0x09AF, 0x09BC},
+		{ 0x09CB, 0x09C7, 0x09BE},
+		{ 0x09CC, 0x09C7, 0x09D7},
+		{ 0x0A59, 0x0A16, 0x0A3C},
+		{ 0x0A5A, 0x0A17, 0x0A3C},
+		{ 0x0A5B, 0x0A1C, 0x0A3C},
+		{ 0x0A5E, 0x0A2B, 0x0A3C},
+		{ 0x0A33, 0x0A32, 0x0A3C},
+		{ 0x0A36, 0x0A38, 0x0A3C},
+		{ 0x0B5C, 0x0B21, 0x0B3C},
+		{ 0x0B5D, 0x0B22, 0x0B3C},
+		{ 0x0B4B, 0x0B47, 0x0B3E},
+		{ 0x0B48, 0x0B47, 0x0B56},
+		{ 0x0B4C, 0x0B47, 0x0B57},
+		{ 0x0B94, 0x0B92, 0x0BD7},
+		{ 0x0BCA, 0x0BC6, 0x0BBE},
+		{ 0x0BCC, 0x0BC6, 0x0BD7},
+		{ 0x0BCB, 0x0BC7, 0x0BBE},
+		{ 0x0C48, 0x0C46, 0x0C56},
+		{ 0x0CC0, 0x0CBF, 0x0CD5},
+		{ 0x0CCA, 0x0CC6, 0x0CC2},
+		{ 0x0CC7, 0x0CC6, 0x0CD5},
+		{ 0x0CC8, 0x0CC6, 0x0CD6},
+		{ 0x0CCB, 0x0CCA, 0x0CD5},
+		{ 0x0D4A, 0x0D46, 0x0D3E},
+		{ 0x0D4C, 0x0D46, 0x0D57},
+		{ 0x0D4B, 0x0D47, 0x0D3E},
+		{ 0x0DDA, 0x0DD9, 0x0DCA},
+		{ 0x0DDC, 0x0DD9, 0x0DCF},
+		{ 0x0DDE, 0x0DD9, 0x0DDF},
+		{ 0x0DDD, 0x0DDC, 0x0DCA},
+		{ 0x0F69, 0x0F40, 0x0FB5},
+		{ 0x0F43, 0x0F42, 0x0FB7},
+		{ 0x0F4D, 0x0F4C, 0x0FB7},
+		{ 0x0F52, 0x0F51, 0x0FB7},
+		{ 0x0F57, 0x0F56, 0x0FB7},
+		{ 0x0F5C, 0x0F5B, 0x0FB7},
+		{ 0x0F73, 0x0F71, 0x0F72},
+		{ 0x0F75, 0x0F71, 0x0F74},
+		{ 0x0F81, 0x0F71, 0x0F80},
+		{ 0x0FB9, 0x0F90, 0x0FB5},
+		{ 0x0F93, 0x0F92, 0x0FB7},
+		{ 0x0F9D, 0x0F9C, 0x0FB7},
+		{ 0x0FA2, 0x0FA1, 0x0FB7},
+		{ 0x0FA7, 0x0FA6, 0x0FB7},
+		{ 0x0FAC, 0x0FAB, 0x0FB7},
+		{ 0x0F76, 0x0FB2, 0x0F80},
+		{ 0x0F78, 0x0FB3, 0x0F80},
+		{ 0x1026, 0x1025, 0x102E},
+		{ 0x1B06, 0x1B05, 0x1B35},
+		{ 0x1B08, 0x1B07, 0x1B35},
+		{ 0x1B0A, 0x1B09, 0x1B35},
+		{ 0x1B0C, 0x1B0B, 0x1B35},
+		{ 0x1B0E, 0x1B0D, 0x1B35},
+		{ 0x1B12, 0x1B11, 0x1B35},
+		{ 0x1B3B, 0x1B3A, 0x1B35},
+		{ 0x1B3D, 0x1B3C, 0x1B35},
+		{ 0x1B40, 0x1B3E, 0x1B35},
+		{ 0x1B41, 0x1B3F, 0x1B35},
+		{ 0x1B43, 0x1B42, 0x1B35},
+		{ 0x1E38, 0x1E36, 0x0304},
+		{ 0x1E39, 0x1E37, 0x0304},
+		{ 0x1E5C, 0x1E5A, 0x0304},
+		{ 0x1E5D, 0x1E5B, 0x0304},
+		{ 0x1E68, 0x1E62, 0x0307},
+		{ 0x1E69, 0x1E63, 0x0307},
+		{ 0x1EAC, 0x1EA0, 0x0302},
+		{ 0x1EB6, 0x1EA0, 0x0306},
+		{ 0x1EAD, 0x1EA1, 0x0302},
+		{ 0x1EB7, 0x1EA1, 0x0306},
+		{ 0x1EC6, 0x1EB8, 0x0302},
+		{ 0x1EC7, 0x1EB9, 0x0302},
+		{ 0x1ED8, 0x1ECC, 0x0302},
+		{ 0x1ED9, 0x1ECD, 0x0302},
+		{ 0x1F02, 0x1F00, 0x0300},
+		{ 0x1F04, 0x1F00, 0x0301},
+		{ 0x1F06, 0x1F00, 0x0342},
+		{ 0x1F80, 0x1F00, 0x0345},
+		{ 0x1F03, 0x1F01, 0x0300},
+		{ 0x1F05, 0x1F01, 0x0301},
+		{ 0x1F07, 0x1F01, 0x0342},
+		{ 0x1F81, 0x1F01, 0x0345},
+		{ 0x1F82, 0x1F02, 0x0345},
+		{ 0x1F83, 0x1F03, 0x0345},
+		{ 0x1F84, 0x1F04, 0x0345},
+		{ 0x1F85, 0x1F05, 0x0345},
+		{ 0x1F86, 0x1F06, 0x0345},
+		{ 0x1F87, 0x1F07, 0x0345},
+		{ 0x1F0A, 0x1F08, 0x0300},
+		{ 0x1F0C, 0x1F08, 0x0301},
+		{ 0x1F0E, 0x1F08, 0x0342},
+		{ 0x1F88, 0x1F08, 0x0345},
+		{ 0x1F0B, 0x1F09, 0x0300},
+		{ 0x1F0D, 0x1F09, 0x0301},
+		{ 0x1F0F, 0x1F09, 0x0342},
+		{ 0x1F89, 0x1F09, 0x0345},
+		{ 0x1F8A, 0x1F0A, 0x0345},
+		{ 0x1F8B, 0x1F0B, 0x0345},
+		{ 0x1F8C, 0x1F0C, 0x0345},
+		{ 0x1F8D, 0x1F0D, 0x0345},
+		{ 0x1F8E, 0x1F0E, 0x0345},
+		{ 0x1F8F, 0x1F0F, 0x0345},
+		{ 0x1F12, 0x1F10, 0x0300},
+		{ 0x1F14, 0x1F10, 0x0301},
+		{ 0x1F13, 0x1F11, 0x0300},
+		{ 0x1F15, 0x1F11, 0x0301},
+		{ 0x1F1A, 0x1F18, 0x0300},
+		{ 0x1F1C, 0x1F18, 0x0301},
+		{ 0x1F1B, 0x1F19, 0x0300},
+		{ 0x1F1D, 0x1F19, 0x0301},
+		{ 0x1F22, 0x1F20, 0x0300},
+		{ 0x1F24, 0x1F20, 0x0301},
+		{ 0x1F26, 0x1F20, 0x0342},
+		{ 0x1F90, 0x1F20, 0x0345},
+		{ 0x1F23, 0x1F21, 0x0300},
+		{ 0x1F25, 0x1F21, 0x0301},
+		{ 0x1F27, 0x1F21, 0x0342},
+		{ 0x1F91, 0x1F21, 0x0345},
+		{ 0x1F92, 0x1F22, 0x0345},
+		{ 0x1F93, 0x1F23, 0x0345},
+		{ 0x1F94, 0x1F24, 0x0345},
+		{ 0x1F95, 0x1F25, 0x0345},
+		{ 0x1F96, 0x1F26, 0x0345},
+		{ 0x1F97, 0x1F27, 0x0345},
+		{ 0x1F2A, 0x1F28, 0x0300},
+		{ 0x1F2C, 0x1F28, 0x0301},
+		{ 0x1F2E, 0x1F28, 0x0342},
+		{ 0x1F98, 0x1F28, 0x0345},
+		{ 0x1F2B, 0x1F29, 0x0300},
+		{ 0x1F2D, 0x1F29, 0x0301},
+		{ 0x1F2F, 0x1F29, 0x0342},
+		{ 0x1F99, 0x1F29, 0x0345},
+		{ 0x1F9A, 0x1F2A, 0x0345},
+		{ 0x1F9B, 0x1F2B, 0x0345},
+		{ 0x1F9C, 0x1F2C, 0x0345},
+		{ 0x1F9D, 0x1F2D, 0x0345},
+		{ 0x1F9E, 0x1F2E, 0x0345},
+		{ 0x1F9F, 0x1F2F, 0x0345},
+		{ 0x1F32, 0x1F30, 0x0300},
+		{ 0x1F34, 0x1F30, 0x0301},
+		{ 0x1F36, 0x1F30, 0x0342},
+		{ 0x1F33, 0x1F31, 0x0300},
+		{ 0x1F35, 0x1F31, 0x0301},
+		{ 0x1F37, 0x1F31, 0x0342},
+		{ 0x1F3A, 0x1F38, 0x0300},
+		{ 0x1F3C, 0x1F38, 0x0301},
+		{ 0x1F3E, 0x1F38, 0x0342},
+		{ 0x1F3B, 0x1F39, 0x0300},
+		{ 0x1F3D, 0x1F39, 0x0301},
+		{ 0x1F3F, 0x1F39, 0x0342},
+		{ 0x1F42, 0x1F40, 0x0300},
+		{ 0x1F44, 0x1F40, 0x0301},
+		{ 0x1F43, 0x1F41, 0x0300},
+		{ 0x1F45, 0x1F41, 0x0301},
+		{ 0x1F4A, 0x1F48, 0x0300},
+		{ 0x1F4C, 0x1F48, 0x0301},
+		{ 0x1F4B, 0x1F49, 0x0300},
+		{ 0x1F4D, 0x1F49, 0x0301},
+		{ 0x1F52, 0x1F50, 0x0300},
+		{ 0x1F54, 0x1F50, 0x0301},
+		{ 0x1F56, 0x1F50, 0x0342},
+		{ 0x1F53, 0x1F51, 0x0300},
+		{ 0x1F55, 0x1F51, 0x0301},
+		{ 0x1F57, 0x1F51, 0x0342},
+		{ 0x1F5B, 0x1F59, 0x0300},
+		{ 0x1F5D, 0x1F59, 0x0301},
+		{ 0x1F5F, 0x1F59, 0x0342},
+		{ 0x1F62, 0x1F60, 0x0300},
+		{ 0x1F64, 0x1F60, 0x0301},
+		{ 0x1F66, 0x1F60, 0x0342},
+		{ 0x1FA0, 0x1F60, 0x0345},
+		{ 0x1F63, 0x1F61, 0x0300},
+		{ 0x1F65, 0x1F61, 0x0301},
+		{ 0x1F67, 0x1F61, 0x0342},
+		{ 0x1FA1, 0x1F61, 0x0345},
+		{ 0x1FA2, 0x1F62, 0x0345},
+		{ 0x1FA3, 0x1F63, 0x0345},
+		{ 0x1FA4, 0x1F64, 0x0345},
+		{ 0x1FA5, 0x1F65, 0x0345},
+		{ 0x1FA6, 0x1F66, 0x0345},
+		{ 0x1FA7, 0x1F67, 0x0345},
+		{ 0x1F6A, 0x1F68, 0x0300},
+		{ 0x1F6C, 0x1F68, 0x0301},
+		{ 0x1F6E, 0x1F68, 0x0342},
+		{ 0x1FA8, 0x1F68, 0x0345},
+		{ 0x1F6B, 0x1F69, 0x0300},
+		{ 0x1F6D, 0x1F69, 0x0301},
+		{ 0x1F6F, 0x1F69, 0x0342},
+		{ 0x1FA9, 0x1F69, 0x0345},
+		{ 0x1FAA, 0x1F6A, 0x0345},
+		{ 0x1FAB, 0x1F6B, 0x0345},
+		{ 0x1FAC, 0x1F6C, 0x0345},
+		{ 0x1FAD, 0x1F6D, 0x0345},
+		{ 0x1FAE, 0x1F6E, 0x0345},
+		{ 0x1FAF, 0x1F6F, 0x0345},
+		{ 0x1FB2, 0x1F70, 0x0345},
+		{ 0x1FC2, 0x1F74, 0x0345},
+		{ 0x1FF2, 0x1F7C, 0x0345},
+		{ 0x1FB7, 0x1FB6, 0x0345},
+		{ 0x1FCD, 0x1FBF, 0x0300},
+		{ 0x1FCE, 0x1FBF, 0x0301},
+		{ 0x1FCF, 0x1FBF, 0x0342},
+		{ 0x1FC7, 0x1FC6, 0x0345},
+		{ 0x1FF7, 0x1FF6, 0x0345},
+		{ 0x1FDD, 0x1FFE, 0x0300},
+		{ 0x1FDE, 0x1FFE, 0x0301},
+		{ 0x1FDF, 0x1FFE, 0x0342},
+		{ 0x219A, 0x2190, 0x0338},
+		{ 0x219B, 0x2192, 0x0338},
+		{ 0x21AE, 0x2194, 0x0338},
+		{ 0x21CD, 0x21D0, 0x0338},
+		{ 0x21CF, 0x21D2, 0x0338},
+		{ 0x21CE, 0x21D4, 0x0338},
+		{ 0x2204, 0x2203, 0x0338},
+		{ 0x2209, 0x2208, 0x0338},
+		{ 0x220C, 0x220B, 0x0338},
+		{ 0x2224, 0x2223, 0x0338},
+		{ 0x2226, 0x2225, 0x0338},
+		{ 0x2241, 0x223C, 0x0338},
+		{ 0x2244, 0x2243, 0x0338},
+		{ 0x2247, 0x2245, 0x0338},
+		{ 0x2249, 0x2248, 0x0338},
+		{ 0x226D, 0x224D, 0x0338},
+		{ 0x2262, 0x2261, 0x0338},
+		{ 0x2270, 0x2264, 0x0338},
+		{ 0x2271, 0x2265, 0x0338},
+		{ 0x2274, 0x2272, 0x0338},
+		{ 0x2275, 0x2273, 0x0338},
+		{ 0x2278, 0x2276, 0x0338},
+		{ 0x2279, 0x2277, 0x0338},
+		{ 0x2280, 0x227A, 0x0338},
+		{ 0x2281, 0x227B, 0x0338},
+		{ 0x22E0, 0x227C, 0x0338},
+		{ 0x22E1, 0x227D, 0x0338},
+		{ 0x2284, 0x2282, 0x0338},
+		{ 0x2285, 0x2283, 0x0338},
+		{ 0x2288, 0x2286, 0x0338},
+		{ 0x2289, 0x2287, 0x0338},
+		{ 0x22E2, 0x2291, 0x0338},
+		{ 0x22E3, 0x2292, 0x0338},
+		{ 0x22AC, 0x22A2, 0x0338},
+		{ 0x22AD, 0x22A8, 0x0338},
+		{ 0x22AE, 0x22A9, 0x0338},
+		{ 0x22AF, 0x22AB, 0x0338},
+		{ 0x22EA, 0x22B2, 0x0338},
+		{ 0x22EB, 0x22B3, 0x0338},
+		{ 0x22EC, 0x22B4, 0x0338},
+		{ 0x22ED, 0x22B5, 0x0338},
+		{ 0x2ADC, 0x2ADD, 0x0338},
+		{ 0x3094, 0x3046, 0x3099},
+		{ 0x304C, 0x304B, 0x3099},
+		{ 0x304E, 0x304D, 0x3099},
+		{ 0x3050, 0x304F, 0x3099},
+		{ 0x3052, 0x3051, 0x3099},
+		{ 0x3054, 0x3053, 0x3099},
+		{ 0x3056, 0x3055, 0x3099},
+		{ 0x3058, 0x3057, 0x3099},
+		{ 0x305A, 0x3059, 0x3099},
+		{ 0x305C, 0x305B, 0x3099},
+		{ 0x305E, 0x305D, 0x3099},
+		{ 0x3060, 0x305F, 0x3099},
+		{ 0x3062, 0x3061, 0x3099},
+		{ 0x3065, 0x3064, 0x3099},
+		{ 0x3067, 0x3066, 0x3099},
+		{ 0x3069, 0x3068, 0x3099},
+		{ 0x3070, 0x306F, 0x3099},
+		{ 0x3071, 0x306F, 0x309A},
+		{ 0x3073, 0x3072, 0x3099},
+		{ 0x3074, 0x3072, 0x309A},
+		{ 0x3076, 0x3075, 0x3099},
+		{ 0x3077, 0x3075, 0x309A},
+		{ 0x3079, 0x3078, 0x3099},
+		{ 0x307A, 0x3078, 0x309A},
+		{ 0x307C, 0x307B, 0x3099},
+		{ 0x307D, 0x307B, 0x309A},
+		{ 0x309E, 0x309D, 0x3099},
+		{ 0x30F4, 0x30A6, 0x3099},
+		{ 0x30AC, 0x30AB, 0x3099},
+		{ 0x30AE, 0x30AD, 0x3099},
+		{ 0x30B0, 0x30AF, 0x3099},
+		{ 0x30B2, 0x30B1, 0x3099},
+		{ 0x30B4, 0x30B3, 0x3099},
+		{ 0x30B6, 0x30B5, 0x3099},
+		{ 0x30B8, 0x30B7, 0x3099},
+		{ 0x30BA, 0x30B9, 0x3099},
+		{ 0x30BC, 0x30BB, 0x3099},
+		{ 0x30BE, 0x30BD, 0x3099},
+		{ 0x30C0, 0x30BF, 0x3099},
+		{ 0x30C2, 0x30C1, 0x3099},
+		{ 0x30C5, 0x30C4, 0x3099},
+		{ 0x30C7, 0x30C6, 0x3099},
+		{ 0x30C9, 0x30C8, 0x3099},
+		{ 0x30D0, 0x30CF, 0x3099},
+		{ 0x30D1, 0x30CF, 0x309A},
+		{ 0x30D3, 0x30D2, 0x3099},
+		{ 0x30D4, 0x30D2, 0x309A},
+		{ 0x30D6, 0x30D5, 0x3099},
+		{ 0x30D7, 0x30D5, 0x309A},
+		{ 0x30D9, 0x30D8, 0x3099},
+		{ 0x30DA, 0x30D8, 0x309A},
+		{ 0x30DC, 0x30DB, 0x3099},
+		{ 0x30DD, 0x30DB, 0x309A},
+		{ 0x30F7, 0x30EF, 0x3099},
+		{ 0x30F8, 0x30F0, 0x3099},
+		{ 0x30F9, 0x30F1, 0x3099},
+		{ 0x30FA, 0x30F2, 0x3099},
+		{ 0x30FE, 0x30FD, 0x3099},
+		{ 0xFB2C, 0xFB49, 0x05C1},
+		{ 0xFB2D, 0xFB49, 0x05C2},
+	};
+
+	private static final int UNICODE_SHIFT = 21;
+
+	public static char precompose(char base, char comb) {
+		int min = 0;
+		int max = precompositions.length - 1;
+		int mid;
+
+		long sought = base << UNICODE_SHIFT | comb;
+		long that;
+
+		while (max >= min) {
+			mid = (min + max) / 2;
+			that = precompositions[mid][1] << UNICODE_SHIFT | precompositions[mid][2];
+			if (that < sought)
+				min = mid + 1;
+			else if (that > sought)
+				max = mid - 1;
+			else
+				return precompositions[mid][0];
+		}
+
+		// No match; return character without combiner
+		return base;
+	}
+}
diff --git a/ScriptingLayerForAndroid/src/de/mud/terminal/VDUBuffer.java b/ScriptingLayerForAndroid/src/de/mud/terminal/VDUBuffer.java
new file mode 100644
index 0000000..58cf20b
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/de/mud/terminal/VDUBuffer.java
@@ -0,0 +1,931 @@
+/*
+ * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform".
+ *
+ * (c) Matthias L. Jugel, Marcus Mei�ner 1996-2005. All Rights Reserved.
+ *
+ * Please visit http://javatelnet.org/ for updates and contact.
+ *
+ * --LICENSE NOTICE--
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ * --LICENSE NOTICE--
+ *
+ */
+
+package de.mud.terminal;
+
+import java.util.Arrays;
+
+/**
+ * Implementation of a Video Display Unit (VDU) buffer. This class contains all methods to
+ * manipulate the buffer that stores characters and their attributes as well as the regions
+ * displayed.
+ *
+ * @author Matthias L. Jugel, Marcus Meißner
+ * @version $Id: VDUBuffer.java 503 2005-10-24 07:34:13Z marcus $
+ */
+public class VDUBuffer {
+
+  /** The current version id tag */
+  public final static String ID = "$Id: VDUBuffer.java 503 2005-10-24 07:34:13Z marcus $";
+
+  /** Enable debug messages. */
+  public final static int debug = 0;
+
+  public int height, width; /* rows and columns */
+  public boolean[] update; /* contains the lines that need update */
+  public char[][] charArray; /* contains the characters */
+  public int[][] charAttributes; /* contains character attrs */
+  public int bufSize;
+  public int maxBufSize; /* buffer sizes */
+  public int screenBase; /* the actual screen start */
+  public int windowBase; /* where the start displaying */
+  public int scrollMarker; /* marks the last line inserted */
+
+  private int topMargin; /* top scroll margin */
+  private int bottomMargin; /* bottom scroll margin */
+
+  // cursor variables
+  protected boolean showcursor = true;
+  protected int cursorX, cursorY;
+
+  /** Scroll up when inserting a line. */
+  public final static boolean SCROLL_UP = false;
+  /** Scroll down when inserting a line. */
+  public final static boolean SCROLL_DOWN = true;
+
+  /*
+   * Attributes bit-field usage:
+   *
+   * 8421 8421 8421 8421 8421 8421 8421 8421 |||| |||| |||| |||| |||| |||| |||| |||`- Bold |||| ||||
+   * |||| |||| |||| |||| |||| ||`-- Underline |||| |||| |||| |||| |||| |||| |||| |`--- Invert ||||
+   * |||| |||| |||| |||| |||| |||| `---- Low |||| |||| |||| |||| |||| |||| |||`------ Invisible ||||
+   * |||| |||| |||| ||`+-++++-+++------- Foreground Color |||| |||| |`++-++++-++------------------
+   * Background Color |||| |||| `----------------------------- Fullwidth character
+   * `+++-++++------------------------------- Reserved for future use
+   */
+
+  /** Make character normal. */
+  public final static int NORMAL = 0x00;
+  /** Make character bold. */
+  public final static int BOLD = 0x01;
+  /** Underline character. */
+  public final static int UNDERLINE = 0x02;
+  /** Invert character. */
+  public final static int INVERT = 0x04;
+  /** Lower intensity character. */
+  public final static int LOW = 0x08;
+  /** Invisible character. */
+  public final static int INVISIBLE = 0x10;
+  /** Unicode full-width character (CJK, et al.) */
+  public final static int FULLWIDTH = 0x8000000;
+
+  /** how much to left shift the foreground color */
+  public final static int COLOR_FG_SHIFT = 5;
+  /** how much to left shift the background color */
+  public final static int COLOR_BG_SHIFT = 14;
+  /** color mask */
+  public final static int COLOR = 0x7fffe0; /* 0000 0000 0111 1111 1111 1111 1110 0000 */
+  /** foreground color mask */
+  public final static int COLOR_FG = 0x3fe0; /* 0000 0000 0000 0000 0011 1111 1110 0000 */
+  /** background color mask */
+  public final static int COLOR_BG = 0x7fc000; /* 0000 0000 0111 1111 1100 0000 0000 0000 */
+
+  /**
+   * Create a new video display buffer with the passed width and height in characters.
+   *
+   * @param width
+   *          the length of the character lines
+   * @param height
+   *          the amount of lines on the screen
+   */
+  public VDUBuffer(int width, int height) {
+    // set the display screen size
+    setScreenSize(width, height, false);
+  }
+
+  /**
+   * Create a standard video display buffer with 80 columns and 24 lines.
+   */
+  public VDUBuffer() {
+    this(80, 24);
+  }
+
+  /**
+   * Put a character on the screen with normal font and outline. The character previously on that
+   * position will be overwritten. You need to call redraw() to update the screen.
+   *
+   * @param c
+   *          x-coordinate (column)
+   * @param l
+   *          y-coordinate (line)
+   * @param ch
+   *          the character to show on the screen
+   * @see #insertChar
+   * @see #deleteChar
+   * @see #redraw
+   */
+  public void putChar(int c, int l, char ch) {
+    putChar(c, l, ch, NORMAL);
+  }
+
+  /**
+   * Put a character on the screen with specific font and outline. The character previously on that
+   * position will be overwritten. You need to call redraw() to update the screen.
+   *
+   * @param c
+   *          x-coordinate (column)
+   * @param l
+   *          y-coordinate (line)
+   * @param ch
+   *          the character to show on the screen
+   * @param attributes
+   *          the character attributes
+   * @see #BOLD
+   * @see #UNDERLINE
+   * @see #INVERT
+   * @see #INVISIBLE
+   * @see #NORMAL
+   * @see #LOW
+   * @see #insertChar
+   * @see #deleteChar
+   * @see #redraw
+   */
+
+  public void putChar(int c, int l, char ch, int attributes) {
+    charArray[screenBase + l][c] = ch;
+    charAttributes[screenBase + l][c] = attributes;
+    if (l < height) {
+      update[l + 1] = true;
+    }
+  }
+
+  /**
+   * Get the character at the specified position.
+   *
+   * @param c
+   *          x-coordinate (column)
+   * @param l
+   *          y-coordinate (line)
+   * @see #putChar
+   */
+  public char getChar(int c, int l) {
+    return charArray[screenBase + l][c];
+  }
+
+  /**
+   * Get the attributes for the specified position.
+   *
+   * @param c
+   *          x-coordinate (column)
+   * @param l
+   *          y-coordinate (line)
+   * @see #putChar
+   */
+  public int getAttributes(int c, int l) {
+    return charAttributes[screenBase + l][c];
+  }
+
+  /**
+   * Insert a character at a specific position on the screen. All character right to from this
+   * position will be moved one to the right. You need to call redraw() to update the screen.
+   *
+   * @param c
+   *          x-coordinate (column)
+   * @param l
+   *          y-coordinate (line)
+   * @param ch
+   *          the character to insert
+   * @param attributes
+   *          the character attributes
+   * @see #BOLD
+   * @see #UNDERLINE
+   * @see #INVERT
+   * @see #INVISIBLE
+   * @see #NORMAL
+   * @see #LOW
+   * @see #putChar
+   * @see #deleteChar
+   * @see #redraw
+   */
+  public void insertChar(int c, int l, char ch, int attributes) {
+    System.arraycopy(charArray[screenBase + l], c, charArray[screenBase + l], c + 1, width - c - 1);
+    System.arraycopy(charAttributes[screenBase + l], c, charAttributes[screenBase + l], c + 1,
+        width - c - 1);
+    putChar(c, l, ch, attributes);
+  }
+
+  /**
+   * Delete a character at a given position on the screen. All characters right to the position will
+   * be moved one to the left. You need to call redraw() to update the screen.
+   *
+   * @param c
+   *          x-coordinate (column)
+   * @param l
+   *          y-coordinate (line)
+   * @see #putChar
+   * @see #insertChar
+   * @see #redraw
+   */
+  public void deleteChar(int c, int l) {
+    if (c < width - 1) {
+      System.arraycopy(charArray[screenBase + l], c + 1, charArray[screenBase + l], c, width - c
+          - 1);
+      System.arraycopy(charAttributes[screenBase + l], c + 1, charAttributes[screenBase + l], c,
+          width - c - 1);
+    }
+    putChar(width - 1, l, (char) 0);
+  }
+
+  /**
+   * Put a String at a specific position. Any characters previously on that position will be
+   * overwritten. You need to call redraw() for screen update.
+   *
+   * @param c
+   *          x-coordinate (column)
+   * @param l
+   *          y-coordinate (line)
+   * @param s
+   *          the string to be shown on the screen
+   * @see #BOLD
+   * @see #UNDERLINE
+   * @see #INVERT
+   * @see #INVISIBLE
+   * @see #NORMAL
+   * @see #LOW
+   * @see #putChar
+   * @see #insertLine
+   * @see #deleteLine
+   * @see #redraw
+   */
+  public void putString(int c, int l, String s) {
+    putString(c, l, s, NORMAL);
+  }
+
+  /**
+   * Put a String at a specific position giving all characters the same attributes. Any characters
+   * previously on that position will be overwritten. You need to call redraw() to update the
+   * screen.
+   *
+   * @param c
+   *          x-coordinate (column)
+   * @param l
+   *          y-coordinate (line)
+   * @param s
+   *          the string to be shown on the screen
+   * @param attributes
+   *          character attributes
+   * @see #BOLD
+   * @see #UNDERLINE
+   * @see #INVERT
+   * @see #INVISIBLE
+   * @see #NORMAL
+   * @see #LOW
+   * @see #putChar
+   * @see #insertLine
+   * @see #deleteLine
+   * @see #redraw
+   */
+  public void putString(int c, int l, String s, int attributes) {
+    for (int i = 0; i < s.length() && c + i < width; i++) {
+      putChar(c + i, l, s.charAt(i), attributes);
+    }
+  }
+
+  /**
+   * Insert a blank line at a specific position. The current line and all previous lines are
+   * scrolled one line up. The top line is lost. You need to call redraw() to update the screen.
+   *
+   * @param l
+   *          the y-coordinate to insert the line
+   * @see #deleteLine
+   * @see #redraw
+   */
+  public void insertLine(int l) {
+    insertLine(l, 1, SCROLL_UP);
+  }
+
+  /**
+   * Insert blank lines at a specific position. You need to call redraw() to update the screen
+   *
+   * @param l
+   *          the y-coordinate to insert the line
+   * @param n
+   *          amount of lines to be inserted
+   * @see #deleteLine
+   * @see #redraw
+   */
+  public void insertLine(int l, int n) {
+    insertLine(l, n, SCROLL_UP);
+  }
+
+  /**
+   * Insert a blank line at a specific position. Scroll text according to the argument. You need to
+   * call redraw() to update the screen
+   *
+   * @param l
+   *          the y-coordinate to insert the line
+   * @param scrollDown
+   *          scroll down
+   * @see #deleteLine
+   * @see #SCROLL_UP
+   * @see #SCROLL_DOWN
+   * @see #redraw
+   */
+  public void insertLine(int l, boolean scrollDown) {
+    insertLine(l, 1, scrollDown);
+  }
+
+  /**
+   * Insert blank lines at a specific position. The current line and all previous lines are scrolled
+   * one line up. The top line is lost. You need to call redraw() to update the screen.
+   *
+   * @param l
+   *          the y-coordinate to insert the line
+   * @param n
+   *          number of lines to be inserted
+   * @param scrollDown
+   *          scroll down
+   * @see #deleteLine
+   * @see #SCROLL_UP
+   * @see #SCROLL_DOWN
+   * @see #redraw
+   */
+  public synchronized void insertLine(int l, int n, boolean scrollDown) {
+    char cbuf[][] = null;
+    int abuf[][] = null;
+    int offset = 0;
+    int oldBase = screenBase;
+
+    int newScreenBase = screenBase;
+    int newWindowBase = windowBase;
+    int newBufSize = bufSize;
+
+    if (l > bottomMargin) {
+      return;
+    }
+    int top =
+        (l < topMargin ? 0 : (l > bottomMargin ? (bottomMargin + 1 < height ? bottomMargin + 1
+            : height - 1) : topMargin));
+    int bottom =
+        (l > bottomMargin ? height - 1 : (l < topMargin ? (topMargin > 0 ? topMargin - 1 : 0)
+            : bottomMargin));
+
+    // System.out.println("l is "+l+", top is "+top+", bottom is "+bottom+", bottomargin is "+bottomMargin+", topMargin is "+topMargin);
+
+    if (scrollDown) {
+      if (n > (bottom - top)) {
+        n = (bottom - top);
+      }
+      int size = bottom - l - (n - 1);
+      if (size < 0) {
+        size = 0;
+      }
+      cbuf = new char[size][];
+      abuf = new int[size][];
+
+      System.arraycopy(charArray, oldBase + l, cbuf, 0, bottom - l - (n - 1));
+      System.arraycopy(charAttributes, oldBase + l, abuf, 0, bottom - l - (n - 1));
+      System.arraycopy(cbuf, 0, charArray, oldBase + l + n, bottom - l - (n - 1));
+      System.arraycopy(abuf, 0, charAttributes, oldBase + l + n, bottom - l - (n - 1));
+      cbuf = charArray;
+      abuf = charAttributes;
+    } else {
+      try {
+        if (n > (bottom - top) + 1) {
+          n = (bottom - top) + 1;
+        }
+        if (bufSize < maxBufSize) {
+          if (bufSize + n > maxBufSize) {
+            offset = n - (maxBufSize - bufSize);
+            scrollMarker += offset;
+            newBufSize = maxBufSize;
+            newScreenBase = maxBufSize - height - 1;
+            newWindowBase = screenBase;
+          } else {
+            scrollMarker += n;
+            newScreenBase += n;
+            newWindowBase += n;
+            newBufSize += n;
+          }
+
+          cbuf = new char[newBufSize][];
+          abuf = new int[newBufSize][];
+        } else {
+          offset = n;
+          cbuf = charArray;
+          abuf = charAttributes;
+        }
+        // copy anything from the top of the buffer (+offset) to the new top
+        // up to the screenBase.
+        if (oldBase > 0) {
+          System.arraycopy(charArray, offset, cbuf, 0, oldBase - offset);
+          System.arraycopy(charAttributes, offset, abuf, 0, oldBase - offset);
+        }
+        // copy anything from the top of the screen (screenBase) up to the
+        // topMargin to the new screen
+        if (top > 0) {
+          System.arraycopy(charArray, oldBase, cbuf, newScreenBase, top);
+          System.arraycopy(charAttributes, oldBase, abuf, newScreenBase, top);
+        }
+        // copy anything from the topMargin up to the amount of lines inserted
+        // to the gap left over between scrollback buffer and screenBase
+        if (oldBase >= 0) {
+          System.arraycopy(charArray, oldBase + top, cbuf, oldBase - offset, n);
+          System.arraycopy(charAttributes, oldBase + top, abuf, oldBase - offset, n);
+        }
+        // copy anything from topMargin + n up to the line linserted to the
+        // topMargin
+        System
+            .arraycopy(charArray, oldBase + top + n, cbuf, newScreenBase + top, l - top - (n - 1));
+        System.arraycopy(charAttributes, oldBase + top + n, abuf, newScreenBase + top, l - top
+            - (n - 1));
+        //
+        // copy the all lines next to the inserted to the new buffer
+        if (l < height - 1) {
+          System.arraycopy(charArray, oldBase + l + 1, cbuf, newScreenBase + l + 1, (height - 1)
+              - l);
+          System.arraycopy(charAttributes, oldBase + l + 1, abuf, newScreenBase + l + 1,
+              (height - 1) - l);
+        }
+      } catch (ArrayIndexOutOfBoundsException e) {
+        // this should not happen anymore, but I will leave the code
+        // here in case something happens anyway. That code above is
+        // so complex I always have a hard time understanding what
+        // I did, even though there are comments
+        System.err.println("*** Error while scrolling up:");
+        System.err.println("--- BEGIN STACK TRACE ---");
+        e.printStackTrace();
+        System.err.println("--- END STACK TRACE ---");
+        System.err.println("bufSize=" + bufSize + ", maxBufSize=" + maxBufSize);
+        System.err.println("top=" + top + ", bottom=" + bottom);
+        System.err.println("n=" + n + ", l=" + l);
+        System.err.println("screenBase=" + screenBase + ", windowBase=" + windowBase);
+        System.err.println("newScreenBase=" + newScreenBase + ", newWindowBase=" + newWindowBase);
+        System.err.println("oldBase=" + oldBase);
+        System.err.println("size.width=" + width + ", size.height=" + height);
+        System.err.println("abuf.length=" + abuf.length + ", cbuf.length=" + cbuf.length);
+        System.err.println("*** done dumping debug information");
+      }
+    }
+
+    // this is a little helper to mark the scrolling
+    scrollMarker -= n;
+
+    for (int i = 0; i < n; i++) {
+      cbuf[(newScreenBase + l) + (scrollDown ? i : -i)] = new char[width];
+      Arrays.fill(cbuf[(newScreenBase + l) + (scrollDown ? i : -i)], ' ');
+      abuf[(newScreenBase + l) + (scrollDown ? i : -i)] = new int[width];
+    }
+
+    charArray = cbuf;
+    charAttributes = abuf;
+    screenBase = newScreenBase;
+    windowBase = newWindowBase;
+    bufSize = newBufSize;
+
+    if (scrollDown) {
+      markLine(l, bottom - l + 1);
+    } else {
+      markLine(top, l - top + 1);
+    }
+
+    display.updateScrollBar();
+  }
+
+  /**
+   * Delete a line at a specific position. Subsequent lines will be scrolled up to fill the space
+   * and a blank line is inserted at the end of the screen.
+   *
+   * @param l
+   *          the y-coordinate to insert the line
+   * @see #deleteLine
+   */
+  public void deleteLine(int l) {
+    int bottom = (l > bottomMargin ? height - 1 : (l < topMargin ? topMargin : bottomMargin + 1));
+    int numRows = bottom - l - 1;
+
+    char[] discardedChars = charArray[screenBase + l];
+    int[] discardedAttributes = charAttributes[screenBase + l];
+
+    if (numRows > 0) {
+      System.arraycopy(charArray, screenBase + l + 1, charArray, screenBase + l, numRows);
+      System.arraycopy(charAttributes, screenBase + l + 1, charAttributes, screenBase + l, numRows);
+    }
+
+    int newBottomRow = screenBase + bottom - 1;
+    charArray[newBottomRow] = discardedChars;
+    charAttributes[newBottomRow] = discardedAttributes;
+    Arrays.fill(charArray[newBottomRow], ' ');
+    Arrays.fill(charAttributes[newBottomRow], 0);
+
+    markLine(l, bottom - l);
+  }
+
+  /**
+   * Delete a rectangular portion of the screen. You need to call redraw() to update the screen.
+   *
+   * @param c
+   *          x-coordinate (column)
+   * @param l
+   *          y-coordinate (row)
+   * @param w
+   *          with of the area in characters
+   * @param h
+   *          height of the area in characters
+   * @param curAttr
+   *          attribute to fill
+   * @see #deleteChar
+   * @see #deleteLine
+   * @see #redraw
+   */
+  public void deleteArea(int c, int l, int w, int h, int curAttr) {
+    int endColumn = c + w;
+    int targetRow = screenBase + l;
+    for (int i = 0; i < h && l + i < height; i++) {
+      Arrays.fill(charAttributes[targetRow], c, endColumn, curAttr);
+      Arrays.fill(charArray[targetRow], c, endColumn, ' ');
+      targetRow++;
+    }
+    markLine(l, h);
+  }
+
+  /**
+   * Delete a rectangular portion of the screen. You need to call redraw() to update the screen.
+   *
+   * @param c
+   *          x-coordinate (column)
+   * @param l
+   *          y-coordinate (row)
+   * @param w
+   *          with of the area in characters
+   * @param h
+   *          height of the area in characters
+   * @see #deleteChar
+   * @see #deleteLine
+   * @see #redraw
+   */
+  public void deleteArea(int c, int l, int w, int h) {
+    deleteArea(c, l, w, h, 0);
+  }
+
+  /**
+   * Sets whether the cursor is visible or not.
+   *
+   * @param doshow
+   */
+  public void showCursor(boolean doshow) {
+    showcursor = doshow;
+  }
+
+  /**
+   * Check whether the cursor is currently visible.
+   *
+   * @return visibility
+   */
+  public boolean isCursorVisible() {
+    return showcursor;
+  }
+
+  /**
+   * Puts the cursor at the specified position.
+   *
+   * @param c
+   *          column
+   * @param l
+   *          line
+   */
+  public void setCursorPosition(int c, int l) {
+    cursorX = c;
+    cursorY = l;
+  }
+
+  /**
+   * Get the current column of the cursor position.
+   */
+  public int getCursorColumn() {
+    return cursorX;
+  }
+
+  /**
+   * Get the current line of the cursor position.
+   */
+  public int getCursorRow() {
+    return cursorY;
+  }
+
+  /**
+   * Set the current window base. This allows to view the scrollback buffer.
+   *
+   * @param line
+   *          the line where the screen window starts
+   * @see #setBufferSize
+   * @see #getBufferSize
+   */
+  public void setWindowBase(int line) {
+    if (line > screenBase) {
+      line = screenBase;
+    } else if (line < 0) {
+      line = 0;
+    }
+    windowBase = line;
+    update[0] = true;
+    redraw();
+  }
+
+  /**
+   * Get the current window base.
+   *
+   * @see #setWindowBase
+   */
+  public int getWindowBase() {
+    return windowBase;
+  }
+
+  /**
+   * Set the scroll margins simultaneously. If they're out of bounds, trim them.
+   *
+   * @param l1
+   *          line that is the top
+   * @param l2
+   *          line that is the bottom
+   */
+  public void setMargins(int l1, int l2) {
+    if (l1 > l2) {
+      return;
+    }
+
+    if (l1 < 0) {
+      l1 = 0;
+    }
+    if (l2 >= height) {
+      l2 = height - 1;
+    }
+
+    topMargin = l1;
+    bottomMargin = l2;
+  }
+
+  /**
+   * Set the top scroll margin for the screen. If the current bottom margin is smaller it will
+   * become the top margin and the line will become the bottom margin.
+   *
+   * @param l
+   *          line that is the margin
+   */
+  public void setTopMargin(int l) {
+    if (l > bottomMargin) {
+      topMargin = bottomMargin;
+      bottomMargin = l;
+    } else {
+      topMargin = l;
+    }
+    if (topMargin < 0) {
+      topMargin = 0;
+    }
+    if (bottomMargin >= height) {
+      bottomMargin = height - 1;
+    }
+  }
+
+  /**
+   * Get the top scroll margin.
+   */
+  public int getTopMargin() {
+    return topMargin;
+  }
+
+  /**
+   * Set the bottom scroll margin for the screen. If the current top margin is bigger it will become
+   * the bottom margin and the line will become the top margin.
+   *
+   * @param l
+   *          line that is the margin
+   */
+  public void setBottomMargin(int l) {
+    if (l < topMargin) {
+      bottomMargin = topMargin;
+      topMargin = l;
+    } else {
+      bottomMargin = l;
+    }
+    if (topMargin < 0) {
+      topMargin = 0;
+    }
+    if (bottomMargin >= height) {
+      bottomMargin = height - 1;
+    }
+  }
+
+  /**
+   * Get the bottom scroll margin.
+   */
+  public int getBottomMargin() {
+    return bottomMargin;
+  }
+
+  /**
+   * Set scrollback buffer size.
+   *
+   * @param amount
+   *          new size of the buffer
+   */
+  public void setBufferSize(int amount) {
+    if (amount < height) {
+      amount = height;
+    }
+    if (amount < maxBufSize) {
+      char cbuf[][] = new char[amount][width];
+      int abuf[][] = new int[amount][width];
+      int copyStart = bufSize - amount < 0 ? 0 : bufSize - amount;
+      int copyCount = bufSize - amount < 0 ? bufSize : amount;
+      if (charArray != null) {
+        System.arraycopy(charArray, copyStart, cbuf, 0, copyCount);
+      }
+      if (charAttributes != null) {
+        System.arraycopy(charAttributes, copyStart, abuf, 0, copyCount);
+      }
+      charArray = cbuf;
+      charAttributes = abuf;
+      bufSize = copyCount;
+      screenBase = bufSize - height;
+      windowBase = screenBase;
+    }
+    maxBufSize = amount;
+
+    update[0] = true;
+    redraw();
+  }
+
+  /**
+   * Retrieve current scrollback buffer size.
+   *
+   * @see #setBufferSize
+   */
+  public int getBufferSize() {
+    return bufSize;
+  }
+
+  /**
+   * Retrieve maximum buffer Size.
+   *
+   * @see #getBufferSize
+   */
+  public int getMaxBufferSize() {
+    return maxBufSize;
+  }
+
+  /**
+   * Change the size of the screen. This will include adjustment of the scrollback buffer.
+   *
+   * @param w
+   *          of the screen
+   * @param h
+   *          of the screen
+   */
+  @SuppressWarnings("unused")
+  public void setScreenSize(int w, int h, boolean broadcast) {
+    char cbuf[][];
+    int abuf[][];
+    int maxSize = bufSize;
+
+    if (w < 1 || h < 1) {
+      return;
+    }
+
+    if (debug > 0) {
+      System.err.println("VDU: screen size [" + w + "," + h + "]");
+    }
+
+    if (h > maxBufSize) {
+      maxBufSize = h;
+    }
+
+    if (h > bufSize) {
+      bufSize = h;
+      screenBase = 0;
+      windowBase = 0;
+    }
+
+    if (windowBase + h >= bufSize) {
+      windowBase = bufSize - h;
+    }
+
+    if (screenBase + h >= bufSize) {
+      screenBase = bufSize - h;
+    }
+
+    cbuf = new char[bufSize][w];
+    abuf = new int[bufSize][w];
+
+    for (int i = 0; i < bufSize; i++) {
+      Arrays.fill(cbuf[i], ' ');
+    }
+
+    if (bufSize < maxSize) {
+      maxSize = bufSize;
+    }
+
+    int rowLength;
+    if (charArray != null && charAttributes != null) {
+      for (int i = 0; i < maxSize && charArray[i] != null; i++) {
+        rowLength = charArray[i].length;
+        System.arraycopy(charArray[i], 0, cbuf[i], 0, w < rowLength ? w : rowLength);
+        System.arraycopy(charAttributes[i], 0, abuf[i], 0, w < rowLength ? w : rowLength);
+      }
+    }
+
+    int C = getCursorColumn();
+    if (C < 0) {
+      C = 0;
+    } else if (C >= width) {
+      C = width - 1;
+    }
+
+    int R = getCursorRow();
+    if (R < 0) {
+      R = 0;
+    } else if (R >= height) {
+      R = height - 1;
+    }
+
+    setCursorPosition(C, R);
+
+    charArray = cbuf;
+    charAttributes = abuf;
+    width = w;
+    height = h;
+    topMargin = 0;
+    bottomMargin = h - 1;
+    update = new boolean[h + 1];
+    update[0] = true;
+    /*
+     * FIXME: ??? if(resizeStrategy == RESIZE_FONT) setBounds(getBounds());
+     */
+  }
+
+  /**
+   * Get amount of rows on the screen.
+   */
+  public int getRows() {
+    return height;
+  }
+
+  /**
+   * Get amount of columns on the screen.
+   */
+  public int getColumns() {
+    return width;
+  }
+
+  /**
+   * Mark lines to be updated with redraw().
+   *
+   * @param l
+   *          starting line
+   * @param n
+   *          amount of lines to be updated
+   * @see #redraw
+   */
+  public void markLine(int l, int n) {
+    for (int i = 0; (i < n) && (l + i < height); i++) {
+      update[l + i + 1] = true;
+    }
+  }
+
+  // private static int checkBounds(int value, int lower, int upper) {
+  // if (value < lower)
+  // return lower;
+  // else if (value > upper)
+  // return upper;
+  // else
+  // return value;
+  // }
+
+  /** a generic display that should redraw on demand */
+  protected VDUDisplay display;
+
+  public void setDisplay(VDUDisplay display) {
+    this.display = display;
+  }
+
+  /**
+   * Trigger a redraw on the display.
+   */
+  protected void redraw() {
+    if (display != null) {
+      display.redraw();
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/de/mud/terminal/VDUDisplay.java b/ScriptingLayerForAndroid/src/de/mud/terminal/VDUDisplay.java
new file mode 100644
index 0000000..0fe06f3
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/de/mud/terminal/VDUDisplay.java
@@ -0,0 +1,40 @@
+/*
+ * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform".
+ *
+ * (c) Matthias L. Jugel, Marcus Meißner 1996-2005. All Rights Reserved.
+ *
+ * Please visit http://javatelnet.org/ for updates and contact.
+ *
+ * --LICENSE NOTICE--
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ * --LICENSE NOTICE--
+ *
+ */
+
+package de.mud.terminal;
+
+/**
+ * Generic display
+ */
+public interface VDUDisplay {
+  public void redraw();
+  public void updateScrollBar();
+
+  public void setVDUBuffer(VDUBuffer buffer);
+  public VDUBuffer getVDUBuffer();
+
+  public void setColor(int index, int red, int green, int blue);
+  public void resetColors();
+}
diff --git a/ScriptingLayerForAndroid/src/de/mud/terminal/VDUInput.java b/ScriptingLayerForAndroid/src/de/mud/terminal/VDUInput.java
new file mode 100644
index 0000000..43c88de
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/de/mud/terminal/VDUInput.java
@@ -0,0 +1,90 @@
+/*
+ * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform".
+ *
+ * (c) Matthias L. Jugel, Marcus Meißner 1996-2005. All Rights Reserved.
+ *
+ * Please visit http://javatelnet.org/ for updates and contact.
+ *
+ * --LICENSE NOTICE--
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ * --LICENSE NOTICE--
+ *
+ */
+package de.mud.terminal;
+
+import java.util.Properties;
+
+/**
+ * An interface for a terminal that accepts input from keyboard and mouse.
+ *
+ * @author Matthias L. Jugel, Marcus Meißner
+ * @version $Id: VDUInput.java 499 2005-09-29 08:24:54Z leo $
+ */
+public interface VDUInput {
+
+  public final static int KEY_CONTROL = 0x01;
+  public final static int KEY_SHIFT = 0x02;
+  public final static int KEY_ALT = 0x04;
+  public final static int KEY_ACTION = 0x08;
+
+
+
+  /**
+   * Direct access to writing data ...
+   * @param b
+   */
+  void write(byte b[]);
+
+  /**
+   * Terminal is mouse-aware and requires (x,y) coordinates of
+   * on the terminal (character coordinates) and the button clicked.
+   * @param x
+   * @param y
+   * @param modifiers
+   */
+  void mousePressed(int x, int y, int modifiers);
+
+  /**
+   * Terminal is mouse-aware and requires the coordinates and button
+   * of the release.
+   * @param x
+   * @param y
+   * @param modifiers
+   */
+  void mouseReleased(int x, int y, int modifiers);
+
+  /**
+   * Override the standard key codes used by the terminal emulation.
+   * @param codes a properties object containing key code definitions
+   */
+  void setKeyCodes(Properties codes);
+
+  /**
+   * main keytyping event handler...
+   * @param keyCode the key code
+   * @param keyChar the character represented by the key
+   * @param modifiers shift/alt/control modifiers
+   */
+  void keyPressed(int keyCode, char keyChar, int modifiers);
+
+  /**
+   * Handle key Typed events for the terminal, this will get
+   * all normal key types, but no shift/alt/control/numlock.
+   * @param keyCode the key code
+   * @param keyChar the character represented by the key
+   * @param modifiers shift/alt/control modifiers
+   */
+  void keyTyped(int keyCode, char keyChar, int modifiers);
+}
diff --git a/ScriptingLayerForAndroid/src/de/mud/terminal/vt320.java b/ScriptingLayerForAndroid/src/de/mud/terminal/vt320.java
new file mode 100644
index 0000000..b713228
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/de/mud/terminal/vt320.java
@@ -0,0 +1,3236 @@
+/*
+ * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform".
+ *
+ * (c) Matthias L. Jugel, Marcus Meiner 1996-2005. All Rights Reserved.
+ *
+ * Please visit http://javatelnet.org/ for updates and contact.
+ *
+ * --LICENSE NOTICE--
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ * --LICENSE NOTICE--
+ *
+ */
+
+package de.mud.terminal;
+
+import java.util.Properties;
+
+/**
+ * Implementation of a VT terminal emulation plus ANSI compatible.
+ * <P>
+ * <B>Maintainer:</B> Marcus Meißner
+ *
+ * @version $Id: vt320.java 507 2005-10-25 10:14:52Z marcus $
+ * @author Matthias L. Jugel, Marcus Meißner
+ */
+@SuppressWarnings("unused")
+public abstract class vt320 extends VDUBuffer implements VDUInput {
+
+  /**
+   * The current version id tag.
+   * <P>
+   * $Id: vt320.java 507 2005-10-25 10:14:52Z marcus $
+   *
+   */
+  public final static String ID = "$Id: vt320.java 507 2005-10-25 10:14:52Z marcus $";
+
+  /** the debug level */
+  private final static int debug = 0;
+  private StringBuilder debugStr;
+
+  public abstract void debug(String notice);
+
+  /**
+   * Write an answer back to the remote host. This is needed to be able to send terminal answers
+   * requests like status and type information.
+   *
+   * @param b
+   *          the array of bytes to be sent
+   */
+  public abstract void write(byte[] b);
+
+  /**
+   * Write an answer back to the remote host. This is needed to be able to send terminal answers
+   * requests like status and type information.
+   *
+   * @param b
+   *          the array of bytes to be sent
+   */
+  public abstract void write(int b);
+
+  /**
+   * Play the beep sound ...
+   */
+  public void beep() { /* do nothing by default */
+  }
+
+  /**
+   * Convenience function for putString(char[], int, int)
+   */
+  public void putString(String s) {
+    int len = s.length();
+    char[] tmp = new char[len];
+    s.getChars(0, len, tmp, 0);
+    putString(tmp, null, 0, len);
+  }
+
+  /**
+   * Put string at current cursor position. Moves cursor according to the String. Does NOT wrap.
+   *
+   * @param s
+   *          character array
+   * @param start
+   *          place to start in array
+   * @param len
+   *          number of characters to process
+   */
+  public void putString(char[] s, byte[] fullwidths, int start, int len) {
+    if (len > 0) {
+      // markLine(R, 1);
+      int lastChar = -1;
+      char c;
+      boolean isWide = false;
+
+      for (int i = 0; i < len; i++) {
+        c = s[start + i];
+        // Shortcut for my favorite ASCII
+        if (c <= 0x7F) {
+          if (lastChar != -1) {
+            putChar((char) lastChar, isWide, false);
+          }
+          lastChar = c;
+          isWide = false;
+        } else if (!Character.isLowSurrogate(c) && !Character.isHighSurrogate(c)) {
+          if (Character.getType(c) == Character.NON_SPACING_MARK) {
+            if (lastChar != -1) {
+              char nc = Precomposer.precompose((char) lastChar, c);
+              putChar(nc, isWide, false);
+              lastChar = -1;
+            }
+          } else {
+            if (lastChar != -1) {
+              putChar((char) lastChar, isWide, false);
+            }
+            lastChar = c;
+            if (fullwidths != null) {
+              isWide = fullwidths[i] == 1;
+            }
+          }
+        }
+      }
+
+      if (lastChar != -1) {
+        putChar((char) lastChar, isWide, false);
+      }
+
+      setCursorPosition(C, R);
+      redraw();
+    }
+  }
+
+  protected void sendTelnetCommand(byte cmd) {
+
+  }
+
+  /**
+   * Sent the changed window size from the terminal to all listeners.
+   */
+  protected void setWindowSize(int c, int r) {
+    /* To be overridden by Terminal.java */
+  }
+
+  @Override
+  public void setScreenSize(int c, int r, boolean broadcast) {
+    // int oldrows = height;
+
+    if (debug > 2) {
+      if (debugStr == null) {
+        debugStr = new StringBuilder();
+      }
+
+      debugStr.append("setscreensize (").append(c).append(',').append(r).append(',')
+          .append(broadcast).append(')');
+      debug(debugStr.toString());
+      debugStr.setLength(0);
+    }
+
+    super.setScreenSize(c, r, false);
+
+    boolean cursorChanged = false;
+
+    // Don't let the cursor go off the screen.
+    if (C >= c) {
+      C = c - 1;
+      cursorChanged = true;
+    }
+
+    if (R >= r) {
+      R = r - 1;
+      cursorChanged = true;
+    }
+
+    if (cursorChanged) {
+      setCursorPosition(C, R);
+      redraw();
+    }
+
+    if (broadcast) {
+      setWindowSize(c, r); /* broadcast up */
+    }
+  }
+
+  /**
+   * Create a new vt320 terminal and intialize it with useful settings.
+   */
+  public vt320(int width, int height) {
+    super(width, height);
+
+    debugStr = new StringBuilder();
+
+    setVMS(false);
+    setIBMCharset(false);
+    setTerminalID("vt320");
+    setBufferSize(100);
+    // setBorder(2, false);
+
+    gx = new char[4];
+    reset();
+
+    /* top row of numpad */
+    PF1 = "\u001bOP";
+    PF2 = "\u001bOQ";
+    PF3 = "\u001bOR";
+    PF4 = "\u001bOS";
+
+    /* the 3x2 keyblock on PC keyboards */
+    Insert = new String[4];
+    Remove = new String[4];
+    KeyHome = new String[4];
+    KeyEnd = new String[4];
+    NextScn = new String[4];
+    PrevScn = new String[4];
+    Escape = new String[4];
+    BackSpace = new String[4];
+    TabKey = new String[4];
+    Insert[0] = Insert[1] = Insert[2] = Insert[3] = "\u001b[2~";
+    Remove[0] = Remove[1] = Remove[2] = Remove[3] = "\u001b[3~";
+    PrevScn[0] = PrevScn[1] = PrevScn[2] = PrevScn[3] = "\u001b[5~";
+    NextScn[0] = NextScn[1] = NextScn[2] = NextScn[3] = "\u001b[6~";
+    KeyHome[0] = KeyHome[1] = KeyHome[2] = KeyHome[3] = "\u001b[H";
+    KeyEnd[0] = KeyEnd[1] = KeyEnd[2] = KeyEnd[3] = "\u001b[F";
+    Escape[0] = Escape[1] = Escape[2] = Escape[3] = "\u001b";
+    if (vms) {
+      BackSpace[1] = "" + (char) 10; // VMS shift deletes word back
+      BackSpace[2] = "\u0018"; // VMS control deletes line back
+      BackSpace[0] = BackSpace[3] = "\u007f"; // VMS other is delete
+    } else {
+      // BackSpace[0] = BackSpace[1] = BackSpace[2] = BackSpace[3] = "\b";
+      // ConnectBot modifications.
+      BackSpace[0] = "\b";
+      BackSpace[1] = "\u007f";
+      BackSpace[2] = "\u001b[3~";
+      BackSpace[3] = "\u001b[2~";
+    }
+
+    /* some more VT100 keys */
+    Find = "\u001b[1~";
+    Select = "\u001b[4~";
+    Help = "\u001b[28~";
+    Do = "\u001b[29~";
+
+    FunctionKey = new String[21];
+    FunctionKey[0] = "";
+    FunctionKey[1] = PF1;
+    FunctionKey[2] = PF2;
+    FunctionKey[3] = PF3;
+    FunctionKey[4] = PF4;
+    /* following are defined differently for vt220 / vt132 ... */
+    FunctionKey[5] = "\u001b[15~";
+    FunctionKey[6] = "\u001b[17~";
+    FunctionKey[7] = "\u001b[18~";
+    FunctionKey[8] = "\u001b[19~";
+    FunctionKey[9] = "\u001b[20~";
+    FunctionKey[10] = "\u001b[21~";
+    FunctionKey[11] = "\u001b[23~";
+    FunctionKey[12] = "\u001b[24~";
+    FunctionKey[13] = "\u001b[25~";
+    FunctionKey[14] = "\u001b[26~";
+    FunctionKey[15] = Help;
+    FunctionKey[16] = Do;
+    FunctionKey[17] = "\u001b[31~";
+    FunctionKey[18] = "\u001b[32~";
+    FunctionKey[19] = "\u001b[33~";
+    FunctionKey[20] = "\u001b[34~";
+
+    FunctionKeyShift = new String[21];
+    FunctionKeyAlt = new String[21];
+    FunctionKeyCtrl = new String[21];
+
+    for (int i = 0; i < 20; i++) {
+      FunctionKeyShift[i] = "";
+      FunctionKeyAlt[i] = "";
+      FunctionKeyCtrl[i] = "";
+    }
+    FunctionKeyShift[15] = Find;
+    FunctionKeyShift[16] = Select;
+
+    TabKey[0] = "\u0009";
+    TabKey[1] = "\u001bOP\u0009";
+    TabKey[2] = TabKey[3] = "";
+
+    KeyUp = new String[4];
+    KeyUp[0] = "\u001b[A";
+    KeyDown = new String[4];
+    KeyDown[0] = "\u001b[B";
+    KeyRight = new String[4];
+    KeyRight[0] = "\u001b[C";
+    KeyLeft = new String[4];
+    KeyLeft[0] = "\u001b[D";
+    Numpad = new String[10];
+    Numpad[0] = "\u001bOp";
+    Numpad[1] = "\u001bOq";
+    Numpad[2] = "\u001bOr";
+    Numpad[3] = "\u001bOs";
+    Numpad[4] = "\u001bOt";
+    Numpad[5] = "\u001bOu";
+    Numpad[6] = "\u001bOv";
+    Numpad[7] = "\u001bOw";
+    Numpad[8] = "\u001bOx";
+    Numpad[9] = "\u001bOy";
+    KPMinus = PF4;
+    KPComma = "\u001bOl";
+    KPPeriod = "\u001bOn";
+    KPEnter = "\u001bOM";
+
+    NUMPlus = new String[4];
+    NUMPlus[0] = "+";
+    NUMDot = new String[4];
+    NUMDot[0] = ".";
+  }
+
+  public void setBackspace(int type) {
+    switch (type) {
+    case DELETE_IS_DEL:
+      BackSpace[0] = "\u007f";
+      BackSpace[1] = "\b";
+      break;
+    case DELETE_IS_BACKSPACE:
+      BackSpace[0] = "\b";
+      BackSpace[1] = "\u007f";
+      break;
+    }
+  }
+
+  /**
+   * Create a default vt320 terminal with 80 columns and 24 lines.
+   */
+  public vt320() {
+    this(80, 24);
+  }
+
+  /**
+   * Terminal is mouse-aware and requires (x,y) coordinates of on the terminal (character
+   * coordinates) and the button clicked.
+   *
+   * @param x
+   * @param y
+   * @param modifiers
+   */
+  public void mousePressed(int x, int y, int modifiers) {
+    if (mouserpt == 0) {
+      return;
+    }
+
+    int mods = modifiers;
+    mousebut = 3;
+    if ((mods & 16) == 16) {
+      mousebut = 0;
+    }
+    if ((mods & 8) == 8) {
+      mousebut = 1;
+    }
+    if ((mods & 4) == 4) {
+      mousebut = 2;
+    }
+
+    int mousecode;
+    if (mouserpt == 9) {
+      mousecode = 0x20 | mousebut;
+    } else {
+      mousecode = mousebut | 0x20 | ((mods & 7) << 2);
+    }
+
+    byte b[] = new byte[6];
+
+    b[0] = 27;
+    b[1] = (byte) '[';
+    b[2] = (byte) 'M';
+    b[3] = (byte) mousecode;
+    b[4] = (byte) (0x20 + x + 1);
+    b[5] = (byte) (0x20 + y + 1);
+
+    write(b); // FIXME: writeSpecial here
+  }
+
+  /**
+   * Terminal is mouse-aware and requires the coordinates and button of the release.
+   *
+   * @param x
+   * @param y
+   * @param modifiers
+   */
+  public void mouseReleased(int x, int y, int modifiers) {
+    if (mouserpt == 0) {
+      return;
+    }
+
+    /*
+     * problem is tht modifiers still have the released button set in them. int mods = modifiers;
+     * mousebut = 3; if ((mods & 16)==16) mousebut=0; if ((mods & 8)==8 ) mousebut=1; if ((mods &
+     * 4)==4 ) mousebut=2;
+     */
+
+    int mousecode;
+    if (mouserpt == 9) {
+      mousecode = 0x20 + mousebut; /* same as press? appears so. */
+    } else {
+      mousecode = '#';
+    }
+
+    byte b[] = new byte[6];
+    b[0] = 27;
+    b[1] = (byte) '[';
+    b[2] = (byte) 'M';
+    b[3] = (byte) mousecode;
+    b[4] = (byte) (0x20 + x + 1);
+    b[5] = (byte) (0x20 + y + 1);
+    write(b); // FIXME: writeSpecial here
+    mousebut = 0;
+  }
+
+  /** we should do localecho (passed from other modules). false is default */
+  private boolean localecho = false;
+
+  /**
+   * Enable or disable the local echo property of the terminal.
+   *
+   * @param echo
+   *          true if the terminal should echo locally
+   */
+  public void setLocalEcho(boolean echo) {
+    localecho = echo;
+  }
+
+  /**
+   * Enable the VMS mode of the terminal to handle some things differently for VMS hosts.
+   *
+   * @param vms
+   *          true for vms mode, false for normal mode
+   */
+  public void setVMS(boolean vms) {
+    this.vms = vms;
+  }
+
+  /**
+   * Enable the usage of the IBM character set used by some BBS's. Special graphical character are
+   * available in this mode.
+   *
+   * @param ibm
+   *          true to use the ibm character set
+   */
+  public void setIBMCharset(boolean ibm) {
+    useibmcharset = ibm;
+  }
+
+  /**
+   * Override the standard key codes used by the terminal emulation.
+   *
+   * @param codes
+   *          a properties object containing key code definitions
+   */
+  public void setKeyCodes(Properties codes) {
+    String res, prefixes[] = { "", "S", "C", "A" };
+    int i;
+
+    for (i = 0; i < 10; i++) {
+      res = codes.getProperty("NUMPAD" + i);
+      if (res != null) {
+        Numpad[i] = unEscape(res);
+      }
+    }
+    for (i = 1; i < 20; i++) {
+      res = codes.getProperty("F" + i);
+      if (res != null) {
+        FunctionKey[i] = unEscape(res);
+      }
+      res = codes.getProperty("SF" + i);
+      if (res != null) {
+        FunctionKeyShift[i] = unEscape(res);
+      }
+      res = codes.getProperty("CF" + i);
+      if (res != null) {
+        FunctionKeyCtrl[i] = unEscape(res);
+      }
+      res = codes.getProperty("AF" + i);
+      if (res != null) {
+        FunctionKeyAlt[i] = unEscape(res);
+      }
+    }
+    for (i = 0; i < 4; i++) {
+      res = codes.getProperty(prefixes[i] + "PGUP");
+      if (res != null) {
+        PrevScn[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "PGDOWN");
+      if (res != null) {
+        NextScn[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "END");
+      if (res != null) {
+        KeyEnd[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "HOME");
+      if (res != null) {
+        KeyHome[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "INSERT");
+      if (res != null) {
+        Insert[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "REMOVE");
+      if (res != null) {
+        Remove[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "UP");
+      if (res != null) {
+        KeyUp[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "DOWN");
+      if (res != null) {
+        KeyDown[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "LEFT");
+      if (res != null) {
+        KeyLeft[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "RIGHT");
+      if (res != null) {
+        KeyRight[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "ESCAPE");
+      if (res != null) {
+        Escape[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "BACKSPACE");
+      if (res != null) {
+        BackSpace[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "TAB");
+      if (res != null) {
+        TabKey[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "NUMPLUS");
+      if (res != null) {
+        NUMPlus[i] = unEscape(res);
+      }
+      res = codes.getProperty(prefixes[i] + "NUMDECIMAL");
+      if (res != null) {
+        NUMDot[i] = unEscape(res);
+      }
+    }
+  }
+
+  /**
+   * Set the terminal id used to identify this terminal.
+   *
+   * @param terminalID
+   *          the id string
+   */
+  public void setTerminalID(String terminalID) {
+    this.terminalID = terminalID;
+
+    if (terminalID.equals("scoansi")) {
+      FunctionKey[1] = "\u001b[M";
+      FunctionKey[2] = "\u001b[N";
+      FunctionKey[3] = "\u001b[O";
+      FunctionKey[4] = "\u001b[P";
+      FunctionKey[5] = "\u001b[Q";
+      FunctionKey[6] = "\u001b[R";
+      FunctionKey[7] = "\u001b[S";
+      FunctionKey[8] = "\u001b[T";
+      FunctionKey[9] = "\u001b[U";
+      FunctionKey[10] = "\u001b[V";
+      FunctionKey[11] = "\u001b[W";
+      FunctionKey[12] = "\u001b[X";
+      FunctionKey[13] = "\u001b[Y";
+      FunctionKey[14] = "?";
+      FunctionKey[15] = "\u001b[a";
+      FunctionKey[16] = "\u001b[b";
+      FunctionKey[17] = "\u001b[c";
+      FunctionKey[18] = "\u001b[d";
+      FunctionKey[19] = "\u001b[e";
+      FunctionKey[20] = "\u001b[f";
+      PrevScn[0] = PrevScn[1] = PrevScn[2] = PrevScn[3] = "\u001b[I";
+      NextScn[0] = NextScn[1] = NextScn[2] = NextScn[3] = "\u001b[G";
+      // more theoretically.
+    }
+  }
+
+  public void setAnswerBack(String ab) {
+    answerBack = unEscape(ab);
+  }
+
+  /**
+   * Get the terminal id used to identify this terminal.
+   */
+  public String getTerminalID() {
+    return terminalID;
+  }
+
+  /**
+   * A small conveniance method thar converts the string to a byte array for sending.
+   *
+   * @param s
+   *          the string to be sent
+   */
+  private boolean write(String s, boolean doecho) {
+    if (debug > 2) {
+      debugStr.append("write(|").append(s).append("|,").append(doecho);
+      debug(debugStr.toString());
+      debugStr.setLength(0);
+    }
+    if (s == null) {
+      return true;
+      /*
+       * NOTE: getBytes() honours some locale, it *CONVERTS* the string. However, we output only
+       * 7bit stuff towards the target, and *some* 8 bit control codes. We must not mess up the
+       * latter, so we do hand by hand copy.
+       */
+    }
+
+    byte arr[] = new byte[s.length()];
+    for (int i = 0; i < s.length(); i++) {
+      arr[i] = (byte) s.charAt(i);
+    }
+    write(arr);
+
+    if (doecho) {
+      putString(s);
+    }
+    return true;
+  }
+
+  private boolean write(int s, boolean doecho) {
+    if (debug > 2) {
+      debugStr.append("write(|").append(s).append("|,").append(doecho);
+      debug(debugStr.toString());
+      debugStr.setLength(0);
+    }
+
+    write(s);
+
+    // TODO check if character is wide
+    if (doecho) {
+      putChar((char) s, false, false);
+    }
+    return true;
+  }
+
+  private boolean write(String s) {
+    return write(s, localecho);
+  }
+
+  // ===================================================================
+  // the actual terminal emulation code comes here:
+  // ===================================================================
+
+  private String terminalID = "vt320";
+  private String answerBack = "Use Terminal.answerback to set ...\n";
+
+  // X - COLUMNS, Y - ROWS
+  int R, C;
+  int attributes = 0;
+
+  int Sc, Sr, Sa, Stm, Sbm;
+  char Sgr, Sgl;
+  char Sgx[];
+
+  int insertmode = 0;
+  int statusmode = 0;
+  boolean vt52mode = false;
+  boolean keypadmode = false; /* false - numeric, true - application */
+  boolean output8bit = false;
+  int normalcursor = 0;
+  boolean moveoutsidemargins = true;
+  boolean wraparound = true;
+  boolean sendcrlf = true;
+  boolean capslock = false;
+  boolean numlock = false;
+  int mouserpt = 0;
+  byte mousebut = 0;
+
+  boolean useibmcharset = false;
+
+  int lastwaslf = 0;
+  boolean usedcharsets = false;
+
+  private final static char ESC = 27;
+  private final static char IND = 132;
+  private final static char NEL = 133;
+  private final static char RI = 141;
+  private final static char SS2 = 142;
+  private final static char SS3 = 143;
+  private final static char DCS = 144;
+  private final static char HTS = 136;
+  private final static char CSI = 155;
+  private final static char OSC = 157;
+  private final static int TSTATE_DATA = 0;
+  private final static int TSTATE_ESC = 1; /* ESC */
+  private final static int TSTATE_CSI = 2; /* ESC [ */
+  private final static int TSTATE_DCS = 3; /* ESC P */
+  private final static int TSTATE_DCEQ = 4; /* ESC [? */
+  private final static int TSTATE_ESCSQUARE = 5; /* ESC # */
+  private final static int TSTATE_OSC = 6; /* ESC ] */
+  private final static int TSTATE_SETG0 = 7; /* ESC (? */
+  private final static int TSTATE_SETG1 = 8; /* ESC )? */
+  private final static int TSTATE_SETG2 = 9; /* ESC *? */
+  private final static int TSTATE_SETG3 = 10; /* ESC +? */
+  private final static int TSTATE_CSI_DOLLAR = 11; /* ESC [ Pn $ */
+  private final static int TSTATE_CSI_EX = 12; /* ESC [ ! */
+  private final static int TSTATE_ESCSPACE = 13; /* ESC <space> */
+  private final static int TSTATE_VT52X = 14;
+  private final static int TSTATE_VT52Y = 15;
+  private final static int TSTATE_CSI_TICKS = 16;
+  private final static int TSTATE_CSI_EQUAL = 17; /* ESC [ = */
+  private final static int TSTATE_TITLE = 18; /* xterm title */
+
+  /* Keys we support */
+  public final static int KEY_PAUSE = 1;
+  public final static int KEY_F1 = 2;
+  public final static int KEY_F2 = 3;
+  public final static int KEY_F3 = 4;
+  public final static int KEY_F4 = 5;
+  public final static int KEY_F5 = 6;
+  public final static int KEY_F6 = 7;
+  public final static int KEY_F7 = 8;
+  public final static int KEY_F8 = 9;
+  public final static int KEY_F9 = 10;
+  public final static int KEY_F10 = 11;
+  public final static int KEY_F11 = 12;
+  public final static int KEY_F12 = 13;
+  public final static int KEY_UP = 14;
+  public final static int KEY_DOWN = 15;
+  public final static int KEY_LEFT = 16;
+  public final static int KEY_RIGHT = 17;
+  public final static int KEY_PAGE_DOWN = 18;
+  public final static int KEY_PAGE_UP = 19;
+  public final static int KEY_INSERT = 20;
+  public final static int KEY_DELETE = 21;
+  public final static int KEY_BACK_SPACE = 22;
+  public final static int KEY_HOME = 23;
+  public final static int KEY_END = 24;
+  public final static int KEY_NUM_LOCK = 25;
+  public final static int KEY_CAPS_LOCK = 26;
+  public final static int KEY_SHIFT = 27;
+  public final static int KEY_CONTROL = 28;
+  public final static int KEY_ALT = 29;
+  public final static int KEY_ENTER = 30;
+  public final static int KEY_NUMPAD0 = 31;
+  public final static int KEY_NUMPAD1 = 32;
+  public final static int KEY_NUMPAD2 = 33;
+  public final static int KEY_NUMPAD3 = 34;
+  public final static int KEY_NUMPAD4 = 35;
+  public final static int KEY_NUMPAD5 = 36;
+  public final static int KEY_NUMPAD6 = 37;
+  public final static int KEY_NUMPAD7 = 38;
+  public final static int KEY_NUMPAD8 = 39;
+  public final static int KEY_NUMPAD9 = 40;
+  public final static int KEY_DECIMAL = 41;
+  public final static int KEY_ADD = 42;
+  public final static int KEY_ESCAPE = 43;
+
+  public final static int DELETE_IS_DEL = 0;
+  public final static int DELETE_IS_BACKSPACE = 1;
+
+  /*
+   * The graphics charsets B - default ASCII A - ISO Latin 1 0 - DEC SPECIAL < - User defined ....
+   */
+  char gx[];
+  char gl; // GL (left charset)
+  char gr; // GR (right charset)
+  int onegl; // single shift override for GL.
+
+  // Map from scoansi linedrawing to DEC _and_ unicode (for the stuff which
+  // is not in linedrawing). Got from experimenting with scoadmin.
+  private final static String scoansi_acs =
+      "Tm7k3x4u?kZl@mYjEnB\u2566DqCtAvM\u2550:\u2551N\u2557I\u2554;\u2557H\u255a0a<\u255d";
+  // array to store DEC Special -> Unicode mapping
+  // Unicode DEC Unicode name (DEC name)
+  private static char DECSPECIAL[] = { '\u0040', // 5f blank
+    '\u2666', // 60 black diamond
+    '\u2592', // 61 grey square
+    '\u2409', // 62 Horizontal tab (ht) pict. for control
+    '\u240c', // 63 Form Feed (ff) pict. for control
+    '\u240d', // 64 Carriage Return (cr) pict. for control
+    '\u240a', // 65 Line Feed (lf) pict. for control
+    '\u00ba', // 66 Masculine ordinal indicator
+    '\u00b1', // 67 Plus or minus sign
+    '\u2424', // 68 New Line (nl) pict. for control
+    '\u240b', // 69 Vertical Tab (vt) pict. for control
+    '\u2518', // 6a Forms light up and left
+    '\u2510', // 6b Forms light down and left
+    '\u250c', // 6c Forms light down and right
+    '\u2514', // 6d Forms light up and right
+    '\u253c', // 6e Forms light vertical and horizontal
+    '\u2594', // 6f Upper 1/8 block (Scan 1)
+    '\u2580', // 70 Upper 1/2 block (Scan 3)
+    '\u2500', // 71 Forms light horizontal or ?em dash? (Scan 5)
+    '\u25ac', // 72 \u25ac black rect. or \u2582 lower 1/4 (Scan 7)
+    '\u005f', // 73 \u005f underscore or \u2581 lower 1/8 (Scan 9)
+    '\u251c', // 74 Forms light vertical and right
+    '\u2524', // 75 Forms light vertical and left
+    '\u2534', // 76 Forms light up and horizontal
+    '\u252c', // 77 Forms light down and horizontal
+    '\u2502', // 78 vertical bar
+    '\u2264', // 79 less than or equal
+    '\u2265', // 7a greater than or equal
+    '\u00b6', // 7b paragraph
+    '\u2260', // 7c not equal
+    '\u00a3', // 7d Pound Sign (british)
+    '\u00b7' // 7e Middle Dot
+      };
+
+  /** Strings to send on function key pressing */
+  private String Numpad[];
+  private String FunctionKey[];
+  private String FunctionKeyShift[];
+  private String FunctionKeyCtrl[];
+  private String FunctionKeyAlt[];
+  private String TabKey[];
+  private String KeyUp[], KeyDown[], KeyLeft[], KeyRight[];
+  private String KPMinus, KPComma, KPPeriod, KPEnter;
+  private String PF1, PF2, PF3, PF4;
+  private String Help, Do, Find, Select;
+
+  private String KeyHome[], KeyEnd[], Insert[], Remove[], PrevScn[], NextScn[];
+  private String Escape[], BackSpace[], NUMDot[], NUMPlus[];
+
+  private String osc, dcs; /* to memorize OSC & DCS control sequence */
+
+  /** vt320 state variable (internal) */
+  private int term_state = TSTATE_DATA;
+  /** in vms mode, set by Terminal.VMS property */
+  private boolean vms = false;
+  /** Tabulators */
+  private byte[] Tabs;
+  /** The list of integers as used by CSI */
+  private int[] DCEvars = new int[30];
+  private int DCEvar;
+
+  /**
+   * Replace escape code characters (backslash + identifier) with their respective codes.
+   *
+   * @param tmp
+   *          the string to be parsed
+   * @return a unescaped string
+   */
+  static String unEscape(String tmp) {
+    int idx = 0, oldidx = 0;
+    String cmd;
+    // f.println("unescape("+tmp+")");
+    cmd = "";
+    while ((idx = tmp.indexOf('\\', oldidx)) >= 0 && ++idx <= tmp.length()) {
+      cmd += tmp.substring(oldidx, idx - 1);
+      if (idx == tmp.length()) {
+        return cmd;
+      }
+      switch (tmp.charAt(idx)) {
+      case 'b':
+        cmd += "\b";
+        break;
+      case 'e':
+        cmd += "\u001b";
+        break;
+      case 'n':
+        cmd += "\n";
+        break;
+      case 'r':
+        cmd += "\r";
+        break;
+      case 't':
+        cmd += "\t";
+        break;
+      case 'v':
+        cmd += "\u000b";
+        break;
+      case 'a':
+        cmd += "\u0012";
+        break;
+      default:
+        if ((tmp.charAt(idx) >= '0') && (tmp.charAt(idx) <= '9')) {
+          int i;
+          for (i = idx; i < tmp.length(); i++) {
+            if ((tmp.charAt(i) < '0') || (tmp.charAt(i) > '9')) {
+              break;
+            }
+          }
+          cmd += (char) Integer.parseInt(tmp.substring(idx, i));
+          idx = i - 1;
+        } else {
+          cmd += tmp.substring(idx, ++idx);
+        }
+        break;
+      }
+      oldidx = ++idx;
+    }
+    if (oldidx <= tmp.length()) {
+      cmd += tmp.substring(oldidx);
+    }
+    return cmd;
+  }
+
+  /**
+   * A small conveniance method thar converts a 7bit string to the 8bit version depending on
+   * VT52/Output8Bit mode.
+   *
+   * @param s
+   *          the string to be sent
+   */
+  private boolean writeSpecial(String s) {
+    if (s == null) {
+      return true;
+    }
+    if (((s.length() >= 3) && (s.charAt(0) == 27) && (s.charAt(1) == 'O'))) {
+      if (vt52mode) {
+        if ((s.charAt(2) >= 'P') && (s.charAt(2) <= 'S')) {
+          s = "\u001b" + s.substring(2); /* ESC x */
+        } else {
+          s = "\u001b?" + s.substring(2); /* ESC ? x */
+        }
+      } else {
+        if (output8bit) {
+          s = "\u008f" + s.substring(2); /* SS3 x */
+        } /* else keep string as it is */
+      }
+    }
+    if (((s.length() >= 3) && (s.charAt(0) == 27) && (s.charAt(1) == '['))) {
+      if (output8bit) {
+        s = "\u009b" + s.substring(2); /* CSI ... */
+      } /* else keep */
+    }
+    return write(s, false);
+  }
+
+  /**
+   * main keytyping event handler...
+   */
+  public void keyPressed(int keyCode, char keyChar, int modifiers) {
+    boolean control = (modifiers & VDUInput.KEY_CONTROL) != 0;
+    boolean shift = (modifiers & VDUInput.KEY_SHIFT) != 0;
+    boolean alt = (modifiers & VDUInput.KEY_ALT) != 0;
+
+    if (debug > 1) {
+      debugStr.append("keyPressed(").append(keyCode).append(", ").append((int) keyChar)
+          .append(", ").append(modifiers).append(')');
+      debug(debugStr.toString());
+      debugStr.setLength(0);
+    }
+
+    int xind;
+    String fmap[];
+    xind = 0;
+    fmap = FunctionKey;
+    if (shift) {
+      fmap = FunctionKeyShift;
+      xind = 1;
+    }
+    if (control) {
+      fmap = FunctionKeyCtrl;
+      xind = 2;
+    }
+    if (alt) {
+      fmap = FunctionKeyAlt;
+      xind = 3;
+    }
+
+    switch (keyCode) {
+    case KEY_PAUSE:
+      if (shift || control) {
+        sendTelnetCommand((byte) 243); // BREAK
+      }
+      break;
+    case KEY_F1:
+      writeSpecial(fmap[1]);
+      break;
+    case KEY_F2:
+      writeSpecial(fmap[2]);
+      break;
+    case KEY_F3:
+      writeSpecial(fmap[3]);
+      break;
+    case KEY_F4:
+      writeSpecial(fmap[4]);
+      break;
+    case KEY_F5:
+      writeSpecial(fmap[5]);
+      break;
+    case KEY_F6:
+      writeSpecial(fmap[6]);
+      break;
+    case KEY_F7:
+      writeSpecial(fmap[7]);
+      break;
+    case KEY_F8:
+      writeSpecial(fmap[8]);
+      break;
+    case KEY_F9:
+      writeSpecial(fmap[9]);
+      break;
+    case KEY_F10:
+      writeSpecial(fmap[10]);
+      break;
+    case KEY_F11:
+      writeSpecial(fmap[11]);
+      break;
+    case KEY_F12:
+      writeSpecial(fmap[12]);
+      break;
+    case KEY_UP:
+      writeSpecial(KeyUp[xind]);
+      break;
+    case KEY_DOWN:
+      writeSpecial(KeyDown[xind]);
+      break;
+    case KEY_LEFT:
+      writeSpecial(KeyLeft[xind]);
+      break;
+    case KEY_RIGHT:
+      writeSpecial(KeyRight[xind]);
+      break;
+    case KEY_PAGE_DOWN:
+      writeSpecial(NextScn[xind]);
+      break;
+    case KEY_PAGE_UP:
+      writeSpecial(PrevScn[xind]);
+      break;
+    case KEY_INSERT:
+      writeSpecial(Insert[xind]);
+      break;
+    case KEY_DELETE:
+      writeSpecial(Remove[xind]);
+      break;
+    case KEY_BACK_SPACE:
+      writeSpecial(BackSpace[xind]);
+      if (localecho) {
+        if (BackSpace[xind] == "\b") {
+          putString("\b \b"); // make the last char 'deleted'
+        } else {
+          putString(BackSpace[xind]); // echo it
+        }
+      }
+      break;
+    case KEY_HOME:
+      writeSpecial(KeyHome[xind]);
+      break;
+    case KEY_END:
+      writeSpecial(KeyEnd[xind]);
+      break;
+    case KEY_NUM_LOCK:
+      if (vms && control) {
+        writeSpecial(PF1);
+      }
+      if (!control) {
+        numlock = !numlock;
+      }
+      break;
+    case KEY_CAPS_LOCK:
+      capslock = !capslock;
+      return;
+    case KEY_SHIFT:
+    case KEY_CONTROL:
+    case KEY_ALT:
+      return;
+    default:
+      break;
+    }
+  }
+
+  /*
+   * public void keyReleased(KeyEvent evt) { if (debug > 1) debug("keyReleased("+evt+")"); // ignore
+   * }
+   */
+  /**
+   * Handle key Typed events for the terminal, this will get all normal key types, but no
+   * shift/alt/control/numlock.
+   */
+  public void keyTyped(int keyCode, char keyChar, int modifiers) {
+    boolean control = (modifiers & VDUInput.KEY_CONTROL) != 0;
+    boolean shift = (modifiers & VDUInput.KEY_SHIFT) != 0;
+    boolean alt = (modifiers & VDUInput.KEY_ALT) != 0;
+
+    if (debug > 1) {
+      debug("keyTyped(" + keyCode + ", " + (int) keyChar + ", " + modifiers + ")");
+    }
+
+    if (keyChar == '\t') {
+      if (shift) {
+        write(TabKey[1], false);
+      } else {
+        if (control) {
+          write(TabKey[2], false);
+        } else {
+          if (alt) {
+            write(TabKey[3], false);
+          } else {
+            write(TabKey[0], false);
+          }
+        }
+      }
+      return;
+    }
+    if (alt) {
+      write(((char) (keyChar | 0x80)));
+      return;
+    }
+
+    if (((keyCode == KEY_ENTER) || (keyChar == 10)) && !control) {
+      write('\r');
+      if (localecho) {
+        putString("\r\n"); // bad hack
+      }
+      return;
+    }
+
+    if ((keyCode == 10) && !control) {
+      debug("Sending \\r");
+      write('\r');
+      return;
+    }
+
+    // FIXME: on german PC keyboards you have to use Alt-Ctrl-q to get an @,
+    // so we can't just use it here... will probably break some other VMS
+    // codes. -Marcus
+    // if(((!vms && keyChar == '2') || keyChar == '@' || keyChar == ' ')
+    // && control)
+    if (((!vms && keyChar == '2') || keyChar == ' ') && control) {
+      write(0);
+    }
+
+    if (vms) {
+      if (keyChar == 127 && !control) {
+        if (shift) {
+          writeSpecial(Insert[0]); // VMS shift delete = insert
+        } else {
+          writeSpecial(Remove[0]); // VMS delete = remove
+        }
+        return;
+      } else if (control) {
+        switch (keyChar) {
+        case '0':
+          writeSpecial(Numpad[0]);
+          return;
+        case '1':
+          writeSpecial(Numpad[1]);
+          return;
+        case '2':
+          writeSpecial(Numpad[2]);
+          return;
+        case '3':
+          writeSpecial(Numpad[3]);
+          return;
+        case '4':
+          writeSpecial(Numpad[4]);
+          return;
+        case '5':
+          writeSpecial(Numpad[5]);
+          return;
+        case '6':
+          writeSpecial(Numpad[6]);
+          return;
+        case '7':
+          writeSpecial(Numpad[7]);
+          return;
+        case '8':
+          writeSpecial(Numpad[8]);
+          return;
+        case '9':
+          writeSpecial(Numpad[9]);
+          return;
+        case '.':
+          writeSpecial(KPPeriod);
+          return;
+        case '-':
+        case 31:
+          writeSpecial(KPMinus);
+          return;
+        case '+':
+          writeSpecial(KPComma);
+          return;
+        case 10:
+          writeSpecial(KPEnter);
+          return;
+        case '/':
+          writeSpecial(PF2);
+          return;
+        case '*':
+          writeSpecial(PF3);
+          return;
+          /* NUMLOCK handled in keyPressed */
+        default:
+          break;
+        }
+        /*
+         * Now what does this do and how did it get here. -Marcus if (shift && keyChar < 32) {
+         * write(PF1+(char)(keyChar + 64)); return; }
+         */
+      }
+    }
+
+    // FIXME: not used?
+    // String fmap[];
+    int xind;
+    xind = 0;
+    // fmap = FunctionKey;
+    if (shift) {
+      // fmap = FunctionKeyShift;
+      xind = 1;
+    }
+    if (control) {
+      // fmap = FunctionKeyCtrl;
+      xind = 2;
+    }
+    if (alt) {
+      // fmap = FunctionKeyAlt;
+      xind = 3;
+    }
+
+    if (keyCode == KEY_ESCAPE) {
+      writeSpecial(Escape[xind]);
+      return;
+    }
+
+    if ((modifiers & VDUInput.KEY_ACTION) != 0) {
+      switch (keyCode) {
+      case KEY_NUMPAD0:
+        writeSpecial(Numpad[0]);
+        return;
+      case KEY_NUMPAD1:
+        writeSpecial(Numpad[1]);
+        return;
+      case KEY_NUMPAD2:
+        writeSpecial(Numpad[2]);
+        return;
+      case KEY_NUMPAD3:
+        writeSpecial(Numpad[3]);
+        return;
+      case KEY_NUMPAD4:
+        writeSpecial(Numpad[4]);
+        return;
+      case KEY_NUMPAD5:
+        writeSpecial(Numpad[5]);
+        return;
+      case KEY_NUMPAD6:
+        writeSpecial(Numpad[6]);
+        return;
+      case KEY_NUMPAD7:
+        writeSpecial(Numpad[7]);
+        return;
+      case KEY_NUMPAD8:
+        writeSpecial(Numpad[8]);
+        return;
+      case KEY_NUMPAD9:
+        writeSpecial(Numpad[9]);
+        return;
+      case KEY_DECIMAL:
+        writeSpecial(NUMDot[xind]);
+        return;
+      case KEY_ADD:
+        writeSpecial(NUMPlus[xind]);
+        return;
+      }
+    }
+
+    if (!((keyChar == 8) || (keyChar == 127) || (keyChar == '\r') || (keyChar == '\n'))) {
+      write(keyChar);
+      return;
+    }
+  }
+
+  private void handle_dcs(String dcs) {
+    debugStr.append("DCS: ").append(dcs);
+    debug(debugStr.toString());
+    debugStr.setLength(0);
+  }
+
+  private void handle_osc(String osc) {
+    if (osc.length() > 2 && osc.substring(0, 2).equals("4;")) {
+      // Define color palette
+      String[] colorData = osc.split(";");
+
+      try {
+        int colorIndex = Integer.parseInt(colorData[1]);
+
+        if ("rgb:".equals(colorData[2].substring(0, 4))) {
+          String[] rgb = colorData[2].substring(4).split("/");
+
+          int red = Integer.parseInt(rgb[0].substring(0, 2), 16) & 0xFF;
+          int green = Integer.parseInt(rgb[1].substring(0, 2), 16) & 0xFF;
+          int blue = Integer.parseInt(rgb[2].substring(0, 2), 16) & 0xFF;
+          display.setColor(colorIndex, red, green, blue);
+        }
+      } catch (Exception e) {
+        debugStr.append("OSC: invalid color sequence encountered: ").append(osc);
+        debug(debugStr.toString());
+        debugStr.setLength(0);
+      }
+    } else {
+      debug("OSC: " + osc);
+    }
+  }
+
+  private final static char unimap[] = {
+    // #
+    // # Name: cp437_DOSLatinUS to Unicode table
+    // # Unicode version: 1.1
+    // # Table version: 1.1
+    // # Table format: Format A
+    // # Date: 03/31/95
+    // # Authors: Michel Suignard <michelsu@microsoft.com>
+    // # Lori Hoerth <lorih@microsoft.com>
+    // # General notes: none
+    // #
+    // # Format: Three tab-separated columns
+    // # Column #1 is the cp1255_WinHebrew code (in hex)
+    // # Column #2 is the Unicode (in hex as 0xXXXX)
+    // # Column #3 is the Unicode name (follows a comment sign, '#')
+    // #
+    // # The entries are in cp437_DOSLatinUS order
+    // #
+
+    0x0000, // #NULL
+    0x0001, // #START OF HEADING
+    0x0002, // #START OF TEXT
+    0x0003, // #END OF TEXT
+    0x0004, // #END OF TRANSMISSION
+    0x0005, // #ENQUIRY
+    0x0006, // #ACKNOWLEDGE
+    0x0007, // #BELL
+    0x0008, // #BACKSPACE
+    0x0009, // #HORIZONTAL TABULATION
+    0x000a, // #LINE FEED
+    0x000b, // #VERTICAL TABULATION
+    0x000c, // #FORM FEED
+    0x000d, // #CARRIAGE RETURN
+    0x000e, // #SHIFT OUT
+    0x000f, // #SHIFT IN
+    0x0010, // #DATA LINK ESCAPE
+    0x0011, // #DEVICE CONTROL ONE
+    0x0012, // #DEVICE CONTROL TWO
+    0x0013, // #DEVICE CONTROL THREE
+    0x0014, // #DEVICE CONTROL FOUR
+    0x0015, // #NEGATIVE ACKNOWLEDGE
+    0x0016, // #SYNCHRONOUS IDLE
+    0x0017, // #END OF TRANSMISSION BLOCK
+    0x0018, // #CANCEL
+    0x0019, // #END OF MEDIUM
+    0x001a, // #SUBSTITUTE
+    0x001b, // #ESCAPE
+    0x001c, // #FILE SEPARATOR
+    0x001d, // #GROUP SEPARATOR
+    0x001e, // #RECORD SEPARATOR
+    0x001f, // #UNIT SEPARATOR
+    0x0020, // #SPACE
+    0x0021, // #EXCLAMATION MARK
+    0x0022, // #QUOTATION MARK
+    0x0023, // #NUMBER SIGN
+    0x0024, // #DOLLAR SIGN
+    0x0025, // #PERCENT SIGN
+    0x0026, // #AMPERSAND
+    0x0027, // #APOSTROPHE
+    0x0028, // #LEFT PARENTHESIS
+    0x0029, // #RIGHT PARENTHESIS
+    0x002a, // #ASTERISK
+    0x002b, // #PLUS SIGN
+    0x002c, // #COMMA
+    0x002d, // #HYPHEN-MINUS
+    0x002e, // #FULL STOP
+    0x002f, // #SOLIDUS
+    0x0030, // #DIGIT ZERO
+    0x0031, // #DIGIT ONE
+    0x0032, // #DIGIT TWO
+    0x0033, // #DIGIT THREE
+    0x0034, // #DIGIT FOUR
+    0x0035, // #DIGIT FIVE
+    0x0036, // #DIGIT SIX
+    0x0037, // #DIGIT SEVEN
+    0x0038, // #DIGIT EIGHT
+    0x0039, // #DIGIT NINE
+    0x003a, // #COLON
+    0x003b, // #SEMICOLON
+    0x003c, // #LESS-THAN SIGN
+    0x003d, // #EQUALS SIGN
+    0x003e, // #GREATER-THAN SIGN
+    0x003f, // #QUESTION MARK
+    0x0040, // #COMMERCIAL AT
+    0x0041, // #LATIN CAPITAL LETTER A
+    0x0042, // #LATIN CAPITAL LETTER B
+    0x0043, // #LATIN CAPITAL LETTER C
+    0x0044, // #LATIN CAPITAL LETTER D
+    0x0045, // #LATIN CAPITAL LETTER E
+    0x0046, // #LATIN CAPITAL LETTER F
+    0x0047, // #LATIN CAPITAL LETTER G
+    0x0048, // #LATIN CAPITAL LETTER H
+    0x0049, // #LATIN CAPITAL LETTER I
+    0x004a, // #LATIN CAPITAL LETTER J
+    0x004b, // #LATIN CAPITAL LETTER K
+    0x004c, // #LATIN CAPITAL LETTER L
+    0x004d, // #LATIN CAPITAL LETTER M
+    0x004e, // #LATIN CAPITAL LETTER N
+    0x004f, // #LATIN CAPITAL LETTER O
+    0x0050, // #LATIN CAPITAL LETTER P
+    0x0051, // #LATIN CAPITAL LETTER Q
+    0x0052, // #LATIN CAPITAL LETTER R
+    0x0053, // #LATIN CAPITAL LETTER S
+    0x0054, // #LATIN CAPITAL LETTER T
+    0x0055, // #LATIN CAPITAL LETTER U
+    0x0056, // #LATIN CAPITAL LETTER V
+    0x0057, // #LATIN CAPITAL LETTER W
+    0x0058, // #LATIN CAPITAL LETTER X
+    0x0059, // #LATIN CAPITAL LETTER Y
+    0x005a, // #LATIN CAPITAL LETTER Z
+    0x005b, // #LEFT SQUARE BRACKET
+    0x005c, // #REVERSE SOLIDUS
+    0x005d, // #RIGHT SQUARE BRACKET
+    0x005e, // #CIRCUMFLEX ACCENT
+    0x005f, // #LOW LINE
+    0x0060, // #GRAVE ACCENT
+    0x0061, // #LATIN SMALL LETTER A
+    0x0062, // #LATIN SMALL LETTER B
+    0x0063, // #LATIN SMALL LETTER C
+    0x0064, // #LATIN SMALL LETTER D
+    0x0065, // #LATIN SMALL LETTER E
+    0x0066, // #LATIN SMALL LETTER F
+    0x0067, // #LATIN SMALL LETTER G
+    0x0068, // #LATIN SMALL LETTER H
+    0x0069, // #LATIN SMALL LETTER I
+    0x006a, // #LATIN SMALL LETTER J
+    0x006b, // #LATIN SMALL LETTER K
+    0x006c, // #LATIN SMALL LETTER L
+    0x006d, // #LATIN SMALL LETTER M
+    0x006e, // #LATIN SMALL LETTER N
+    0x006f, // #LATIN SMALL LETTER O
+    0x0070, // #LATIN SMALL LETTER P
+    0x0071, // #LATIN SMALL LETTER Q
+    0x0072, // #LATIN SMALL LETTER R
+    0x0073, // #LATIN SMALL LETTER S
+    0x0074, // #LATIN SMALL LETTER T
+    0x0075, // #LATIN SMALL LETTER U
+    0x0076, // #LATIN SMALL LETTER V
+    0x0077, // #LATIN SMALL LETTER W
+    0x0078, // #LATIN SMALL LETTER X
+    0x0079, // #LATIN SMALL LETTER Y
+    0x007a, // #LATIN SMALL LETTER Z
+    0x007b, // #LEFT CURLY BRACKET
+    0x007c, // #VERTICAL LINE
+    0x007d, // #RIGHT CURLY BRACKET
+    0x007e, // #TILDE
+    0x007f, // #DELETE
+    0x00c7, // #LATIN CAPITAL LETTER C WITH CEDILLA
+    0x00fc, // #LATIN SMALL LETTER U WITH DIAERESIS
+    0x00e9, // #LATIN SMALL LETTER E WITH ACUTE
+    0x00e2, // #LATIN SMALL LETTER A WITH CIRCUMFLEX
+    0x00e4, // #LATIN SMALL LETTER A WITH DIAERESIS
+    0x00e0, // #LATIN SMALL LETTER A WITH GRAVE
+    0x00e5, // #LATIN SMALL LETTER A WITH RING ABOVE
+    0x00e7, // #LATIN SMALL LETTER C WITH CEDILLA
+    0x00ea, // #LATIN SMALL LETTER E WITH CIRCUMFLEX
+    0x00eb, // #LATIN SMALL LETTER E WITH DIAERESIS
+    0x00e8, // #LATIN SMALL LETTER E WITH GRAVE
+    0x00ef, // #LATIN SMALL LETTER I WITH DIAERESIS
+    0x00ee, // #LATIN SMALL LETTER I WITH CIRCUMFLEX
+    0x00ec, // #LATIN SMALL LETTER I WITH GRAVE
+    0x00c4, // #LATIN CAPITAL LETTER A WITH DIAERESIS
+    0x00c5, // #LATIN CAPITAL LETTER A WITH RING ABOVE
+    0x00c9, // #LATIN CAPITAL LETTER E WITH ACUTE
+    0x00e6, // #LATIN SMALL LIGATURE AE
+    0x00c6, // #LATIN CAPITAL LIGATURE AE
+    0x00f4, // #LATIN SMALL LETTER O WITH CIRCUMFLEX
+    0x00f6, // #LATIN SMALL LETTER O WITH DIAERESIS
+    0x00f2, // #LATIN SMALL LETTER O WITH GRAVE
+    0x00fb, // #LATIN SMALL LETTER U WITH CIRCUMFLEX
+    0x00f9, // #LATIN SMALL LETTER U WITH GRAVE
+    0x00ff, // #LATIN SMALL LETTER Y WITH DIAERESIS
+    0x00d6, // #LATIN CAPITAL LETTER O WITH DIAERESIS
+    0x00dc, // #LATIN CAPITAL LETTER U WITH DIAERESIS
+    0x00a2, // #CENT SIGN
+    0x00a3, // #POUND SIGN
+    0x00a5, // #YEN SIGN
+    0x20a7, // #PESETA SIGN
+    0x0192, // #LATIN SMALL LETTER F WITH HOOK
+    0x00e1, // #LATIN SMALL LETTER A WITH ACUTE
+    0x00ed, // #LATIN SMALL LETTER I WITH ACUTE
+    0x00f3, // #LATIN SMALL LETTER O WITH ACUTE
+    0x00fa, // #LATIN SMALL LETTER U WITH ACUTE
+    0x00f1, // #LATIN SMALL LETTER N WITH TILDE
+    0x00d1, // #LATIN CAPITAL LETTER N WITH TILDE
+    0x00aa, // #FEMININE ORDINAL INDICATOR
+    0x00ba, // #MASCULINE ORDINAL INDICATOR
+    0x00bf, // #INVERTED QUESTION MARK
+    0x2310, // #REVERSED NOT SIGN
+    0x00ac, // #NOT SIGN
+    0x00bd, // #VULGAR FRACTION ONE HALF
+    0x00bc, // #VULGAR FRACTION ONE QUARTER
+    0x00a1, // #INVERTED EXCLAMATION MARK
+    0x00ab, // #LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+    0x00bb, // #RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+    0x2591, // #LIGHT SHADE
+    0x2592, // #MEDIUM SHADE
+    0x2593, // #DARK SHADE
+    0x2502, // #BOX DRAWINGS LIGHT VERTICAL
+    0x2524, // #BOX DRAWINGS LIGHT VERTICAL AND LEFT
+    0x2561, // #BOX DRAWINGS VERTICAL SINGLE AND LEFT DOUBLE
+    0x2562, // #BOX DRAWINGS VERTICAL DOUBLE AND LEFT SINGLE
+    0x2556, // #BOX DRAWINGS DOWN DOUBLE AND LEFT SINGLE
+    0x2555, // #BOX DRAWINGS DOWN SINGLE AND LEFT DOUBLE
+    0x2563, // #BOX DRAWINGS DOUBLE VERTICAL AND LEFT
+    0x2551, // #BOX DRAWINGS DOUBLE VERTICAL
+    0x2557, // #BOX DRAWINGS DOUBLE DOWN AND LEFT
+    0x255d, // #BOX DRAWINGS DOUBLE UP AND LEFT
+    0x255c, // #BOX DRAWINGS UP DOUBLE AND LEFT SINGLE
+    0x255b, // #BOX DRAWINGS UP SINGLE AND LEFT DOUBLE
+    0x2510, // #BOX DRAWINGS LIGHT DOWN AND LEFT
+    0x2514, // #BOX DRAWINGS LIGHT UP AND RIGHT
+    0x2534, // #BOX DRAWINGS LIGHT UP AND HORIZONTAL
+    0x252c, // #BOX DRAWINGS LIGHT DOWN AND HORIZONTAL
+    0x251c, // #BOX DRAWINGS LIGHT VERTICAL AND RIGHT
+    0x2500, // #BOX DRAWINGS LIGHT HORIZONTAL
+    0x253c, // #BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL
+    0x255e, // #BOX DRAWINGS VERTICAL SINGLE AND RIGHT DOUBLE
+    0x255f, // #BOX DRAWINGS VERTICAL DOUBLE AND RIGHT SINGLE
+    0x255a, // #BOX DRAWINGS DOUBLE UP AND RIGHT
+    0x2554, // #BOX DRAWINGS DOUBLE DOWN AND RIGHT
+    0x2569, // #BOX DRAWINGS DOUBLE UP AND HORIZONTAL
+    0x2566, // #BOX DRAWINGS DOUBLE DOWN AND HORIZONTAL
+    0x2560, // #BOX DRAWINGS DOUBLE VERTICAL AND RIGHT
+    0x2550, // #BOX DRAWINGS DOUBLE HORIZONTAL
+    0x256c, // #BOX DRAWINGS DOUBLE VERTICAL AND HORIZONTAL
+    0x2567, // #BOX DRAWINGS UP SINGLE AND HORIZONTAL DOUBLE
+    0x2568, // #BOX DRAWINGS UP DOUBLE AND HORIZONTAL SINGLE
+    0x2564, // #BOX DRAWINGS DOWN SINGLE AND HORIZONTAL DOUBLE
+    0x2565, // #BOX DRAWINGS DOWN DOUBLE AND HORIZONTAL SINGLE
+    0x2559, // #BOX DRAWINGS UP DOUBLE AND RIGHT SINGLE
+    0x2558, // #BOX DRAWINGS UP SINGLE AND RIGHT DOUBLE
+    0x2552, // #BOX DRAWINGS DOWN SINGLE AND RIGHT DOUBLE
+    0x2553, // #BOX DRAWINGS DOWN DOUBLE AND RIGHT SINGLE
+    0x256b, // #BOX DRAWINGS VERTICAL DOUBLE AND HORIZONTAL SINGLE
+    0x256a, // #BOX DRAWINGS VERTICAL SINGLE AND HORIZONTAL DOUBLE
+    0x2518, // #BOX DRAWINGS LIGHT UP AND LEFT
+    0x250c, // #BOX DRAWINGS LIGHT DOWN AND RIGHT
+    0x2588, // #FULL BLOCK
+    0x2584, // #LOWER HALF BLOCK
+    0x258c, // #LEFT HALF BLOCK
+    0x2590, // #RIGHT HALF BLOCK
+    0x2580, // #UPPER HALF BLOCK
+    0x03b1, // #GREEK SMALL LETTER ALPHA
+    0x00df, // #LATIN SMALL LETTER SHARP S
+    0x0393, // #GREEK CAPITAL LETTER GAMMA
+    0x03c0, // #GREEK SMALL LETTER PI
+    0x03a3, // #GREEK CAPITAL LETTER SIGMA
+    0x03c3, // #GREEK SMALL LETTER SIGMA
+    0x00b5, // #MICRO SIGN
+    0x03c4, // #GREEK SMALL LETTER TAU
+    0x03a6, // #GREEK CAPITAL LETTER PHI
+    0x0398, // #GREEK CAPITAL LETTER THETA
+    0x03a9, // #GREEK CAPITAL LETTER OMEGA
+    0x03b4, // #GREEK SMALL LETTER DELTA
+    0x221e, // #INFINITY
+    0x03c6, // #GREEK SMALL LETTER PHI
+    0x03b5, // #GREEK SMALL LETTER EPSILON
+    0x2229, // #INTERSECTION
+    0x2261, // #IDENTICAL TO
+    0x00b1, // #PLUS-MINUS SIGN
+    0x2265, // #GREATER-THAN OR EQUAL TO
+    0x2264, // #LESS-THAN OR EQUAL TO
+    0x2320, // #TOP HALF INTEGRAL
+    0x2321, // #BOTTOM HALF INTEGRAL
+    0x00f7, // #DIVISION SIGN
+    0x2248, // #ALMOST EQUAL TO
+    0x00b0, // #DEGREE SIGN
+    0x2219, // #BULLET OPERATOR
+    0x00b7, // #MIDDLE DOT
+    0x221a, // #SQUARE ROOT
+    0x207f, // #SUPERSCRIPT LATIN SMALL LETTER N
+    0x00b2, // #SUPERSCRIPT TWO
+    0x25a0, // #BLACK SQUARE
+    0x00a0, // #NO-BREAK SPACE
+  };
+
+  public char map_cp850_unicode(char x) {
+    if (x >= 0x100) {
+      return x;
+    }
+    return unimap[x];
+  }
+
+  private void _SetCursor(int row, int col) {
+    int maxr = height - 1;
+    int tm = getTopMargin();
+
+    R = (row < 0) ? 0 : row;
+    C = (col < 0) ? 0 : (col >= width) ? width - 1 : col;
+
+    if (!moveoutsidemargins) {
+      R += tm;
+      maxr = getBottomMargin();
+    }
+    if (R > maxr) {
+      R = maxr;
+    }
+  }
+
+  private void putChar(char c, boolean isWide, boolean doshowcursor) {
+    int rows = height; // statusline
+    int columns = width;
+    // byte msg[];
+
+    // if (debug > 4) {
+    // debugStr.append("putChar(")
+    // .append(c)
+    // .append(" [")
+    // .append((int) c)
+    // .append("]) at R=")
+    // .append(R)
+    // .append(" , C=")
+    // .append(C)
+    // .append(", columns=")
+    // .append(columns)
+    // .append(", rows=")
+    // .append(rows);
+    // debug(debugStr.toString());
+    // debugStr.setLength(0);
+    // }
+    // markLine(R, 1);
+    // if (c > 255) {
+    // if (debug > 0)
+    // debug("char > 255:" + (int) c);
+    // //return;
+    // }
+
+    switch (term_state) {
+    case TSTATE_DATA:
+      /*
+       * FIXME: we shouldn't use chars with bit 8 set if ibmcharset. probably... but some BBS do
+       * anyway...
+       */
+      if (!useibmcharset) {
+        boolean doneflag = true;
+        switch (c) {
+        case OSC:
+          osc = "";
+          term_state = TSTATE_OSC;
+          break;
+        case RI:
+          if (R > getTopMargin()) {
+            R--;
+          } else {
+            insertLine(R, 1, SCROLL_DOWN);
+          }
+          if (debug > 1) {
+            debug("RI");
+          }
+          break;
+        case IND:
+          if (debug > 2) {
+            debugStr.append("IND at ").append(R).append(", tm is ").append(getTopMargin())
+                .append(", bm is ").append(getBottomMargin());
+            debug(debugStr.toString());
+            debugStr.setLength(0);
+          }
+          if (R == getBottomMargin() || R == rows - 1) {
+            insertLine(R, 1, SCROLL_UP);
+          } else {
+            R++;
+          }
+          if (debug > 1) {
+            debug("IND (at " + R + " )");
+          }
+          break;
+        case NEL:
+          if (R == getBottomMargin() || R == rows - 1) {
+            insertLine(R, 1, SCROLL_UP);
+          } else {
+            R++;
+          }
+          C = 0;
+          if (debug > 1) {
+            debug("NEL (at " + R + " )");
+          }
+          break;
+        case HTS:
+          Tabs[C] = 1;
+          if (debug > 1) {
+            debug("HTS");
+          }
+          break;
+        case DCS:
+          dcs = "";
+          term_state = TSTATE_DCS;
+          break;
+        default:
+          doneflag = false;
+          break;
+        }
+        if (doneflag) {
+          break;
+        }
+      }
+      switch (c) {
+      case SS3:
+        onegl = 3;
+        break;
+      case SS2:
+        onegl = 2;
+        break;
+      case CSI: // should be in the 8bit section, but some BBS use this
+        DCEvar = 0;
+        DCEvars[0] = 0;
+        DCEvars[1] = 0;
+        DCEvars[2] = 0;
+        DCEvars[3] = 0;
+        term_state = TSTATE_CSI;
+        break;
+      case ESC:
+        term_state = TSTATE_ESC;
+        lastwaslf = 0;
+        break;
+      case 5: /* ENQ */
+        write(answerBack, false);
+        break;
+      case 12:
+        /* FormFeed, Home for the BBS world */
+        deleteArea(0, 0, columns, rows, attributes);
+        C = R = 0;
+        break;
+      case '\b': /* 8 */
+        C--;
+        if (C < 0) {
+          C = 0;
+        }
+        lastwaslf = 0;
+        break;
+      case '\t':
+        do {
+          // Don't overwrite or insert! TABS are not destructive, but movement!
+          C++;
+        } while (C < columns && (Tabs[C] == 0));
+        lastwaslf = 0;
+        break;
+      case '\r': // 13 CR
+        C = 0;
+        break;
+      case '\n': // 10 LF
+        if (debug > 3) {
+          debug("R= " + R + ", bm " + getBottomMargin() + ", tm=" + getTopMargin() + ", rows="
+              + rows);
+        }
+        if (!vms) {
+          if (lastwaslf != 0 && lastwaslf != c) {
+            break;
+          }
+          lastwaslf = c;
+          /* C = 0; */
+        }
+        if (R == getBottomMargin() || R >= rows - 1) {
+          insertLine(R, 1, SCROLL_UP);
+        } else {
+          R++;
+        }
+        break;
+      case 7:
+        beep();
+        break;
+      case '\016': /* SMACS , as */
+        /* ^N, Shift out - Put G1 into GL */
+        gl = 1;
+        usedcharsets = true;
+        break;
+      case '\017': /* RMACS , ae */
+        /* ^O, Shift in - Put G0 into GL */
+        gl = 0;
+        usedcharsets = true;
+        break;
+      default: {
+        int thisgl = gl;
+
+        if (onegl >= 0) {
+          thisgl = onegl;
+          onegl = -1;
+        }
+        lastwaslf = 0;
+        if (c < 32) {
+          if (c != 0) {
+            if (debug > 0) {
+              debug("TSTATE_DATA char: " + ((int) c));
+            }
+          }
+          /* break; some BBS really want those characters, like hearst etc. */
+          if (c == 0) {
+            break;
+          }
+        }
+        if (C >= columns) {
+          if (wraparound) {
+            int bot = rows;
+
+            // If we're in the scroll region, check against the bottom margin
+            if (R <= getBottomMargin() && R >= getTopMargin()) {
+              bot = getBottomMargin() + 1;
+            }
+
+            if (R < bot - 1) {
+              R++;
+            } else {
+              if (debug > 3) {
+                debug("scrolling due to wrap at " + R);
+              }
+              insertLine(R, 1, SCROLL_UP);
+            }
+            C = 0;
+          } else {
+            // cursor stays on last character.
+            C = columns - 1;
+          }
+        }
+
+        boolean mapped = false;
+
+        // Mapping if DEC Special is chosen charset
+        if (usedcharsets) {
+          if (c >= '\u0020' && c <= '\u007f') {
+            switch (gx[thisgl]) {
+            case '0':
+              // Remap SCOANSI line drawing to VT100 line drawing chars
+              // for our SCO using customers.
+              if (terminalID.equals("scoansi") || terminalID.equals("ansi")) {
+                for (int i = 0; i < scoansi_acs.length(); i += 2) {
+                  if (c == scoansi_acs.charAt(i)) {
+                    c = scoansi_acs.charAt(i + 1);
+                    break;
+                  }
+                }
+              }
+              if (c >= '\u005f' && c <= '\u007e') {
+                c = DECSPECIAL[(short) c - 0x5f];
+                mapped = true;
+              }
+              break;
+            case '<': // 'user preferred' is currently 'ISO Latin-1 suppl
+              c = (char) ((c & 0x7f) | 0x80);
+              mapped = true;
+              break;
+            case 'A':
+            case 'B': // Latin-1 , ASCII -> fall through
+              mapped = true;
+              break;
+            default:
+              debug("Unsupported GL mapping: " + gx[thisgl]);
+              break;
+            }
+          }
+          if (!mapped && (c >= '\u0080' && c <= '\u00ff')) {
+            switch (gx[gr]) {
+            case '0':
+              if (c >= '\u00df' && c <= '\u00fe') {
+                c = DECSPECIAL[c - '\u00df'];
+                mapped = true;
+              }
+              break;
+            case '<':
+            case 'A':
+            case 'B':
+              mapped = true;
+              break;
+            default:
+              debug("Unsupported GR mapping: " + gx[gr]);
+              break;
+            }
+          }
+        }
+        if (!mapped && useibmcharset) {
+          c = map_cp850_unicode(c);
+        }
+
+        /* if(true || (statusmode == 0)) { */
+        if (isWide) {
+          if (C >= columns - 1) {
+            if (wraparound) {
+              int bot = rows;
+
+              // If we're in the scroll region, check against the bottom margin
+              if (R <= getBottomMargin() && R >= getTopMargin()) {
+                bot = getBottomMargin() + 1;
+              }
+
+              if (R < bot - 1) {
+                R++;
+              } else {
+                if (debug > 3) {
+                  debug("scrolling due to wrap at " + R);
+                }
+                insertLine(R, 1, SCROLL_UP);
+              }
+              C = 0;
+            } else {
+              // cursor stays on last wide character.
+              C = columns - 2;
+            }
+          }
+        }
+
+        if (insertmode == 1) {
+          if (isWide) {
+            insertChar(C++, R, c, attributes | FULLWIDTH);
+            insertChar(C, R, ' ', attributes | FULLWIDTH);
+          } else {
+            insertChar(C, R, c, attributes);
+          }
+        } else {
+          if (isWide) {
+            putChar(C++, R, c, attributes | FULLWIDTH);
+            putChar(C, R, ' ', attributes | FULLWIDTH);
+          } else {
+            putChar(C, R, c, attributes);
+          }
+        }
+
+        /*
+         * } else { if (insertmode==1) { insertChar(C, rows, c, attributes); } else { putChar(C,
+         * rows, c, attributes); } }
+         */
+        C++;
+        break;
+      }
+      } /* switch(c) */
+      break;
+    case TSTATE_OSC:
+      if ((c < 0x20) && (c != ESC)) {// NP - No printing character
+        handle_osc(osc);
+        term_state = TSTATE_DATA;
+        break;
+      }
+      // but check for vt102 ESC \
+      if (c == '\\' && osc.charAt(osc.length() - 1) == ESC) {
+        handle_osc(osc);
+        term_state = TSTATE_DATA;
+        break;
+      }
+      osc = osc + c;
+      break;
+    case TSTATE_ESCSPACE:
+      term_state = TSTATE_DATA;
+      switch (c) {
+      case 'F': /* S7C1T, Disable output of 8-bit controls, use 7-bit */
+        output8bit = false;
+        break;
+      case 'G': /* S8C1T, Enable output of 8-bit control codes */
+        output8bit = true;
+        break;
+      default:
+        debug("ESC <space> " + c + " unhandled.");
+      }
+      break;
+    case TSTATE_ESC:
+      term_state = TSTATE_DATA;
+      switch (c) {
+      case ' ':
+        term_state = TSTATE_ESCSPACE;
+        break;
+      case '#':
+        term_state = TSTATE_ESCSQUARE;
+        break;
+      case 'c':
+        /* Hard terminal reset */
+        reset();
+        break;
+      case '[':
+        DCEvar = 0;
+        DCEvars[0] = 0;
+        DCEvars[1] = 0;
+        DCEvars[2] = 0;
+        DCEvars[3] = 0;
+        term_state = TSTATE_CSI;
+        break;
+      case ']':
+        osc = "";
+        term_state = TSTATE_OSC;
+        break;
+      case 'P':
+        dcs = "";
+        term_state = TSTATE_DCS;
+        break;
+      case 'A': /* CUU */
+        R--;
+        if (R < 0) {
+          R = 0;
+        }
+        break;
+      case 'B': /* CUD */
+        R++;
+        if (R >= rows) {
+          R = rows - 1;
+        }
+        break;
+      case 'C':
+        C++;
+        if (C >= columns) {
+          C = columns - 1;
+        }
+        break;
+      case 'I': // RI
+        insertLine(R, 1, SCROLL_DOWN);
+        break;
+      case 'E': /* NEL */
+        if (R == getBottomMargin() || R == rows - 1) {
+          insertLine(R, 1, SCROLL_UP);
+        } else {
+          R++;
+        }
+        C = 0;
+        if (debug > 1) {
+          debug("ESC E (at " + R + ")");
+        }
+        break;
+      case 'D': /* IND */
+        if (R == getBottomMargin() || R == rows - 1) {
+          insertLine(R, 1, SCROLL_UP);
+        } else {
+          R++;
+        }
+        if (debug > 1) {
+          debug("ESC D (at " + R + " )");
+        }
+        break;
+      case 'J': /* erase to end of screen */
+        if (R < rows - 1) {
+          deleteArea(0, R + 1, columns, rows - R - 1, attributes);
+        }
+        if (C < columns - 1) {
+          deleteArea(C, R, columns - C, 1, attributes);
+        }
+        break;
+      case 'K':
+        if (C < columns - 1) {
+          deleteArea(C, R, columns - C, 1, attributes);
+        }
+        break;
+      case 'M': // RI
+        debug("ESC M : R is " + R + ", tm is " + getTopMargin() + ", bm is " + getBottomMargin());
+        if (R > getTopMargin()) { // just go up 1 line.
+          R--;
+        } else { // scroll down
+          insertLine(R, 1, SCROLL_DOWN);
+        }
+        /* else do nothing ; */
+        if (debug > 2) {
+          debug("ESC M ");
+        }
+        break;
+      case 'H':
+        if (debug > 1) {
+          debug("ESC H at " + C);
+        }
+        /* right border probably ... */
+        if (C >= columns) {
+          C = columns - 1;
+        }
+        Tabs[C] = 1;
+        break;
+      case 'N': // SS2
+        onegl = 2;
+        break;
+      case 'O': // SS3
+        onegl = 3;
+        break;
+      case '=':
+        /* application keypad */
+        if (debug > 0) {
+          debug("ESC =");
+        }
+        keypadmode = true;
+        break;
+      case '<': /* vt52 mode off */
+        vt52mode = false;
+        break;
+      case '>': /* normal keypad */
+        if (debug > 0) {
+          debug("ESC >");
+        }
+        keypadmode = false;
+        break;
+      case '7': /* DECSC: save cursor, attributes */
+        Sc = C;
+        Sr = R;
+        Sgl = gl;
+        Sgr = gr;
+        Sa = attributes;
+        Sgx = new char[4];
+        for (int i = 0; i < 4; i++) {
+          Sgx[i] = gx[i];
+        }
+        if (debug > 1) {
+          debug("ESC 7");
+        }
+        break;
+      case '8': /* DECRC: restore cursor, attributes */
+        C = Sc;
+        R = Sr;
+        gl = Sgl;
+        gr = Sgr;
+        if (Sgx != null) {
+          for (int i = 0; i < 4; i++) {
+            gx[i] = Sgx[i];
+          }
+        }
+        attributes = Sa;
+        if (debug > 1) {
+          debug("ESC 8");
+        }
+        break;
+      case '(': /* Designate G0 Character set (ISO 2022) */
+        term_state = TSTATE_SETG0;
+        usedcharsets = true;
+        break;
+      case ')': /* Designate G1 character set (ISO 2022) */
+        term_state = TSTATE_SETG1;
+        usedcharsets = true;
+        break;
+      case '*': /* Designate G2 Character set (ISO 2022) */
+        term_state = TSTATE_SETG2;
+        usedcharsets = true;
+        break;
+      case '+': /* Designate G3 Character set (ISO 2022) */
+        term_state = TSTATE_SETG3;
+        usedcharsets = true;
+        break;
+      case '~': /* Locking Shift 1, right */
+        gr = 1;
+        usedcharsets = true;
+        break;
+      case 'n': /* Locking Shift 2 */
+        gl = 2;
+        usedcharsets = true;
+        break;
+      case '}': /* Locking Shift 2, right */
+        gr = 2;
+        usedcharsets = true;
+        break;
+      case 'o': /* Locking Shift 3 */
+        gl = 3;
+        usedcharsets = true;
+        break;
+      case '|': /* Locking Shift 3, right */
+        gr = 3;
+        usedcharsets = true;
+        break;
+      case 'Y': /* vt52 cursor address mode , next chars are x,y */
+        term_state = TSTATE_VT52Y;
+        break;
+      case '_':
+        term_state = TSTATE_TITLE;
+        break;
+      case '\\':
+        // TODO save title
+        term_state = TSTATE_DATA;
+        break;
+      default:
+        debug("ESC unknown letter: " + c + " (" + ((int) c) + ")");
+        break;
+      }
+      break;
+    case TSTATE_VT52X:
+      C = c - 37;
+      if (C < 0) {
+        C = 0;
+      } else if (C >= width) {
+        C = width - 1;
+      }
+      term_state = TSTATE_VT52Y;
+      break;
+    case TSTATE_VT52Y:
+      R = c - 37;
+      if (R < 0) {
+        R = 0;
+      } else if (R >= height) {
+        R = height - 1;
+      }
+      term_state = TSTATE_DATA;
+      break;
+    case TSTATE_SETG0:
+      if (c != '0' && c != 'A' && c != 'B' && c != '<') {
+        debug("ESC ( " + c + ": G0 char set?  (" + ((int) c) + ")");
+      } else {
+        if (debug > 2) {
+          debug("ESC ( : G0 char set  (" + c + " " + ((int) c) + ")");
+        }
+        gx[0] = c;
+      }
+      term_state = TSTATE_DATA;
+      break;
+    case TSTATE_SETG1:
+      if (c != '0' && c != 'A' && c != 'B' && c != '<') {
+        debug("ESC ) " + c + " (" + ((int) c) + ") :G1 char set?");
+      } else {
+        if (debug > 2) {
+          debug("ESC ) :G1 char set  (" + c + " " + ((int) c) + ")");
+        }
+        gx[1] = c;
+      }
+      term_state = TSTATE_DATA;
+      break;
+    case TSTATE_SETG2:
+      if (c != '0' && c != 'A' && c != 'B' && c != '<') {
+        debug("ESC*:G2 char set?  (" + ((int) c) + ")");
+      } else {
+        if (debug > 2) {
+          debug("ESC*:G2 char set  (" + c + " " + ((int) c) + ")");
+        }
+        gx[2] = c;
+      }
+      term_state = TSTATE_DATA;
+      break;
+    case TSTATE_SETG3:
+      if (c != '0' && c != 'A' && c != 'B' && c != '<') {
+        debug("ESC+:G3 char set?  (" + ((int) c) + ")");
+      } else {
+        if (debug > 2) {
+          debug("ESC+:G3 char set  (" + c + " " + ((int) c) + ")");
+        }
+        gx[3] = c;
+      }
+      term_state = TSTATE_DATA;
+      break;
+    case TSTATE_ESCSQUARE:
+      switch (c) {
+      case '8':
+        for (int i = 0; i < columns; i++) {
+          for (int j = 0; j < rows; j++) {
+            putChar(i, j, 'E', 0);
+          }
+        }
+        break;
+      default:
+        debug("ESC # " + c + " not supported.");
+        break;
+      }
+      term_state = TSTATE_DATA;
+      break;
+    case TSTATE_DCS:
+      if (c == '\\' && dcs.charAt(dcs.length() - 1) == ESC) {
+        handle_dcs(dcs);
+        term_state = TSTATE_DATA;
+        break;
+      }
+      dcs = dcs + c;
+      break;
+
+    case TSTATE_DCEQ:
+      term_state = TSTATE_DATA;
+      switch (c) {
+      case '0':
+      case '1':
+      case '2':
+      case '3':
+      case '4':
+      case '5':
+      case '6':
+      case '7':
+      case '8':
+      case '9':
+        DCEvars[DCEvar] = DCEvars[DCEvar] * 10 + (c) - 48;
+        term_state = TSTATE_DCEQ;
+        break;
+      case ';':
+        DCEvar++;
+        DCEvars[DCEvar] = 0;
+        term_state = TSTATE_DCEQ;
+        break;
+      case 's': // XTERM_SAVE missing!
+        if (true || debug > 1) {
+          debug("ESC [ ? " + DCEvars[0] + " s unimplemented!");
+        }
+        break;
+      case 'r': // XTERM_RESTORE
+        if (true || debug > 1) {
+          debug("ESC [ ? " + DCEvars[0] + " r");
+        }
+        /* DEC Mode reset */
+        for (int i = 0; i <= DCEvar; i++) {
+          switch (DCEvars[i]) {
+          case 3: /* 80 columns */
+            setScreenSize(80, height, true);
+            break;
+          case 4: /* scrolling mode, smooth */
+            break;
+          case 5: /* light background */
+            break;
+          case 6: /* DECOM (Origin Mode) move inside margins. */
+            moveoutsidemargins = true;
+            break;
+          case 7: /* DECAWM: Autowrap Mode */
+            wraparound = false;
+            break;
+          case 12:/* local echo off */
+            break;
+          case 9: /* X10 mouse */
+          case 1000: /* xterm style mouse report on */
+          case 1001:
+          case 1002:
+          case 1003:
+            mouserpt = DCEvars[i];
+            break;
+          default:
+            debug("ESC [ ? " + DCEvars[0] + " r, unimplemented!");
+          }
+        }
+        break;
+      case 'h': // DECSET
+        if (debug > 0) {
+          debug("ESC [ ? " + DCEvars[0] + " h");
+        }
+        /* DEC Mode set */
+        for (int i = 0; i <= DCEvar; i++) {
+          switch (DCEvars[i]) {
+          case 1: /* Application cursor keys */
+            KeyUp[0] = "\u001bOA";
+            KeyDown[0] = "\u001bOB";
+            KeyRight[0] = "\u001bOC";
+            KeyLeft[0] = "\u001bOD";
+            break;
+          case 2: /* DECANM */
+            vt52mode = false;
+            break;
+          case 3: /* 132 columns */
+            setScreenSize(132, height, true);
+            break;
+          case 6: /* DECOM: move inside margins. */
+            moveoutsidemargins = false;
+            break;
+          case 7: /* DECAWM: Autowrap Mode */
+            wraparound = true;
+            break;
+          case 25: /* turn cursor on */
+            showCursor(true);
+            break;
+          case 9: /* X10 mouse */
+          case 1000: /* xterm style mouse report on */
+          case 1001:
+          case 1002:
+          case 1003:
+            mouserpt = DCEvars[i];
+            break;
+
+          /* unimplemented stuff, fall through */
+          /* 4 - scrolling mode, smooth */
+          /* 5 - light background */
+          /* 12 - local echo off */
+          /* 18 - DECPFF - Printer Form Feed Mode -> On */
+          /* 19 - DECPEX - Printer Extent Mode -> Screen */
+          default:
+            debug("ESC [ ? " + DCEvars[0] + " h, unsupported.");
+            break;
+          }
+        }
+        break;
+      case 'i': // DEC Printer Control, autoprint, echo screenchars to printer
+        // This is different to CSI i!
+        // Also: "Autoprint prints a final display line only when the
+        // cursor is moved off the line by an autowrap or LF, FF, or
+        // VT (otherwise do not print the line)."
+        switch (DCEvars[0]) {
+        case 1:
+          if (debug > 1) {
+            debug("CSI ? 1 i : Print line containing cursor");
+          }
+          break;
+        case 4:
+          if (debug > 1) {
+            debug("CSI ? 4 i : Start passthrough printing");
+          }
+          break;
+        case 5:
+          if (debug > 1) {
+            debug("CSI ? 4 i : Stop passthrough printing");
+          }
+          break;
+        }
+        break;
+      case 'l': // DECRST
+        /* DEC Mode reset */
+        if (debug > 0) {
+          debug("ESC [ ? " + DCEvars[0] + " l");
+        }
+        for (int i = 0; i <= DCEvar; i++) {
+          switch (DCEvars[i]) {
+          case 1: /* Application cursor keys */
+            KeyUp[0] = "\u001b[A";
+            KeyDown[0] = "\u001b[B";
+            KeyRight[0] = "\u001b[C";
+            KeyLeft[0] = "\u001b[D";
+            break;
+          case 2: /* DECANM */
+            vt52mode = true;
+            break;
+          case 3: /* 80 columns */
+            setScreenSize(80, height, true);
+            break;
+          case 6: /* DECOM: move outside margins. */
+            moveoutsidemargins = true;
+            break;
+          case 7: /* DECAWM: Autowrap Mode OFF */
+            wraparound = false;
+            break;
+          case 25: /* turn cursor off */
+            showCursor(false);
+            break;
+          /* Unimplemented stuff: */
+          /* 4 - scrolling mode, jump */
+          /* 5 - dark background */
+          /* 7 - DECAWM - no wrap around mode */
+          /* 12 - local echo on */
+          /* 18 - DECPFF - Printer Form Feed Mode -> Off */
+          /* 19 - DECPEX - Printer Extent Mode -> Scrolling Region */
+          case 9: /* X10 mouse */
+          case 1000: /* xterm style mouse report OFF */
+          case 1001:
+          case 1002:
+          case 1003:
+            mouserpt = 0;
+            break;
+          default:
+            debug("ESC [ ? " + DCEvars[0] + " l, unsupported.");
+            break;
+          }
+        }
+        break;
+      case 'n':
+        if (debug > 0) {
+          debug("ESC [ ? " + DCEvars[0] + " n");
+        }
+        switch (DCEvars[0]) {
+        case 15:
+          /* printer? no printer. */
+          write((ESC) + "[?13n", false);
+          debug("ESC[5n");
+          break;
+        default:
+          debug("ESC [ ? " + DCEvars[0] + " n, unsupported.");
+          break;
+        }
+        break;
+      default:
+        debug("ESC [ ? " + DCEvars[0] + " " + c + ", unsupported.");
+        break;
+      }
+      break;
+    case TSTATE_CSI_EX:
+      term_state = TSTATE_DATA;
+      switch (c) {
+      case ESC:
+        term_state = TSTATE_ESC;
+        break;
+      default:
+        debug("Unknown character ESC[! character is " + (int) c);
+        break;
+      }
+      break;
+    case TSTATE_CSI_TICKS:
+      term_state = TSTATE_DATA;
+      switch (c) {
+      case 'p':
+        debug("Conformance level: " + DCEvars[0] + " (unsupported)," + DCEvars[1]);
+        if (DCEvars[0] == 61) {
+          output8bit = false;
+          break;
+        }
+        if (DCEvars[1] == 1) {
+          output8bit = false;
+        } else {
+          output8bit = true; /* 0 or 2 */
+        }
+        break;
+      default:
+        debug("Unknown ESC [...  \"" + c);
+        break;
+      }
+      break;
+    case TSTATE_CSI_EQUAL:
+      term_state = TSTATE_DATA;
+      switch (c) {
+      case '0':
+      case '1':
+      case '2':
+      case '3':
+      case '4':
+      case '5':
+      case '6':
+      case '7':
+      case '8':
+      case '9':
+        DCEvars[DCEvar] = DCEvars[DCEvar] * 10 + (c) - 48;
+        term_state = TSTATE_CSI_EQUAL;
+        break;
+      case ';':
+        DCEvar++;
+        DCEvars[DCEvar] = 0;
+        term_state = TSTATE_CSI_EQUAL;
+        break;
+
+      case 'F': /* SCO ANSI foreground */
+      {
+        int newcolor;
+
+        debug("ESC [ = " + DCEvars[0] + " F");
+
+        attributes &= ~COLOR_FG;
+        newcolor = ((DCEvars[0] & 1) << 2) | (DCEvars[0] & 2) | ((DCEvars[0] & 4) >> 2);
+        attributes |= (newcolor + 1) << COLOR_FG_SHIFT;
+
+        break;
+      }
+      case 'G': /* SCO ANSI background */
+      {
+        int newcolor;
+
+        debug("ESC [ = " + DCEvars[0] + " G");
+
+        attributes &= ~COLOR_BG;
+        newcolor = ((DCEvars[0] & 1) << 2) | (DCEvars[0] & 2) | ((DCEvars[0] & 4) >> 2);
+        attributes |= (newcolor + 1) << COLOR_BG_SHIFT;
+        break;
+      }
+
+      default:
+        debugStr.append("Unknown ESC [ = ");
+        for (int i = 0; i <= DCEvar; i++) {
+          debugStr.append(DCEvars[i]).append(',');
+        }
+        debugStr.append(c);
+        debug(debugStr.toString());
+        debugStr.setLength(0);
+        break;
+      }
+      break;
+    case TSTATE_CSI_DOLLAR:
+      term_state = TSTATE_DATA;
+      switch (c) {
+      case '}':
+        debug("Active Status Display now " + DCEvars[0]);
+        statusmode = DCEvars[0];
+        break;
+      /*
+       * bad documentation? case '-': debug("Set Status Display now "+DCEvars[0]); break;
+       */
+      case '~':
+        debug("Status Line mode now " + DCEvars[0]);
+        break;
+      default:
+        debug("UNKNOWN Status Display code " + c + ", with Pn=" + DCEvars[0]);
+        break;
+      }
+      break;
+    case TSTATE_CSI:
+      term_state = TSTATE_DATA;
+      switch (c) {
+      case '"':
+        term_state = TSTATE_CSI_TICKS;
+        break;
+      case '$':
+        term_state = TSTATE_CSI_DOLLAR;
+        break;
+      case '=':
+        term_state = TSTATE_CSI_EQUAL;
+        break;
+      case '!':
+        term_state = TSTATE_CSI_EX;
+        break;
+      case '?':
+        DCEvar = 0;
+        DCEvars[0] = 0;
+        term_state = TSTATE_DCEQ;
+        break;
+      case '0':
+      case '1':
+      case '2':
+      case '3':
+      case '4':
+      case '5':
+      case '6':
+      case '7':
+      case '8':
+      case '9':
+        DCEvars[DCEvar] = DCEvars[DCEvar] * 10 + (c) - 48;
+        term_state = TSTATE_CSI;
+        break;
+      case ';':
+        DCEvar++;
+        DCEvars[DCEvar] = 0;
+        term_state = TSTATE_CSI;
+        break;
+      case 'c':/* send primary device attributes */
+        /* send (ESC[?61c) */
+
+        String subcode = "";
+        if (terminalID.equals("vt320")) {
+          subcode = "63;";
+        }
+        if (terminalID.equals("vt220")) {
+          subcode = "62;";
+        }
+        if (terminalID.equals("vt100")) {
+          subcode = "61;";
+        }
+        write((ESC) + "[?" + subcode + "1;2c", false);
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " c");
+        }
+        break;
+      case 'q':
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " q");
+        }
+        break;
+      case 'g':
+        /* used for tabsets */
+        switch (DCEvars[0]) {
+        case 3:/* clear them */
+          Tabs = new byte[width];
+          break;
+        case 0:
+          Tabs[C] = 0;
+          break;
+        }
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " g");
+        }
+        break;
+      case 'h':
+        switch (DCEvars[0]) {
+        case 4:
+          insertmode = 1;
+          break;
+        case 20:
+          debug("Setting CRLF to TRUE");
+          sendcrlf = true;
+          break;
+        default:
+          debug("unsupported: ESC [ " + DCEvars[0] + " h");
+          break;
+        }
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " h");
+        }
+        break;
+      case 'i': // Printer Controller mode.
+        // "Transparent printing sends all output, except the CSI 4 i
+        // termination string, to the printer and not the screen,
+        // uses an 8-bit channel if no parity so NUL and DEL will be
+        // seen by the printer and by the termination recognizer code,
+        // and all translation and character set selections are
+        // bypassed."
+        switch (DCEvars[0]) {
+        case 0:
+          if (debug > 1) {
+            debug("CSI 0 i:  Print Screen, not implemented.");
+          }
+          break;
+        case 4:
+          if (debug > 1) {
+            debug("CSI 4 i:  Enable Transparent Printing, not implemented.");
+          }
+          break;
+        case 5:
+          if (debug > 1) {
+            debug("CSI 4/5 i:  Disable Transparent Printing, not implemented.");
+          }
+          break;
+        default:
+          debug("ESC [ " + DCEvars[0] + " i, unimplemented!");
+        }
+        break;
+      case 'l':
+        switch (DCEvars[0]) {
+        case 4:
+          insertmode = 0;
+          break;
+        case 20:
+          debug("Setting CRLF to FALSE");
+          sendcrlf = false;
+          break;
+        default:
+          debug("ESC [ " + DCEvars[0] + " l, unimplemented!");
+          break;
+        }
+        break;
+      case 'A': // CUU
+      {
+        int limit;
+        /* FIXME: xterm only cares about 0 and topmargin */
+        if (R >= getTopMargin()) {
+          limit = getTopMargin();
+        } else {
+          limit = 0;
+        }
+        if (DCEvars[0] == 0) {
+          R--;
+        } else {
+          R -= DCEvars[0];
+        }
+        if (R < limit) {
+          R = limit;
+        }
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " A");
+        }
+        break;
+      }
+      case 'B': // CUD
+        /* cursor down n (1) times */
+      {
+        int limit;
+        if (R <= getBottomMargin()) {
+          limit = getBottomMargin();
+        } else {
+          limit = rows - 1;
+        }
+        if (DCEvars[0] == 0) {
+          R++;
+        } else {
+          R += DCEvars[0];
+        }
+        if (R > limit) {
+          R = limit;
+        } else {
+          if (debug > 2) {
+            debug("Not limited.");
+          }
+        }
+        if (debug > 2) {
+          debug("to: " + R);
+        }
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " B (at C=" + C + ")");
+        }
+        break;
+      }
+      case 'C':
+        if (DCEvars[0] == 0) {
+          DCEvars[0] = 1;
+        }
+        while (DCEvars[0]-- > 0) {
+          C++;
+        }
+        if (C >= columns) {
+          C = columns - 1;
+        }
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " C");
+        }
+        break;
+      case 'd': // CVA
+        R = DCEvars[0];
+        if (R < 0) {
+          R = 0;
+        } else if (R >= height) {
+          R = height - 1;
+        }
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " d");
+        }
+        break;
+      case 'D':
+        if (DCEvars[0] == 0) {
+          DCEvars[0] = 1;
+        }
+        while (DCEvars[0]-- > 0) {
+          C--;
+        }
+        if (C < 0) {
+          C = 0;
+        }
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " D");
+        }
+        break;
+      case 'r': // DECSTBM
+        if (DCEvar > 0) // Ray: Any argument is optional
+        {
+          R = DCEvars[1] - 1;
+          if (R < 0) {
+            R = rows - 1;
+          } else if (R >= rows) {
+            R = rows - 1;
+          }
+        } else {
+          R = rows - 1;
+        }
+        int bot = R;
+        if (R >= DCEvars[0]) {
+          R = DCEvars[0] - 1;
+          if (R < 0) {
+            R = 0;
+          }
+        }
+        setMargins(R, bot);
+        _SetCursor(0, 0);
+        if (debug > 1) {
+          debug("ESC [" + DCEvars[0] + " ; " + DCEvars[1] + " r");
+        }
+        break;
+      case 'G': /* CUP / cursor absolute column */
+        C = DCEvars[0];
+        if (C < 0) {
+          C = 0;
+        } else if (C >= width) {
+          C = width - 1;
+        }
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " G");
+        }
+        break;
+      case 'H': /* CUP / cursor position */
+        /* gets 2 arguments */
+        _SetCursor(DCEvars[0] - 1, DCEvars[1] - 1);
+        if (debug > 2) {
+          debug("ESC [ " + DCEvars[0] + ";" + DCEvars[1] + " H, moveoutsidemargins "
+              + moveoutsidemargins);
+          debug("	-> R now " + R + ", C now " + C);
+        }
+        break;
+      case 'f': /* move cursor 2 */
+        /* gets 2 arguments */
+        R = DCEvars[0] - 1;
+        C = DCEvars[1] - 1;
+        if (C < 0) {
+          C = 0;
+        } else if (C >= width) {
+          C = width - 1;
+        }
+        if (R < 0) {
+          R = 0;
+        } else if (R >= height) {
+          R = height - 1;
+        }
+        if (debug > 2) {
+          debug("ESC [ " + DCEvars[0] + ";" + DCEvars[1] + " f");
+        }
+        break;
+      case 'S': /* ind aka 'scroll forward' */
+        if (DCEvars[0] == 0) {
+          insertLine(rows - 1, SCROLL_UP);
+        } else {
+          insertLine(rows - 1, DCEvars[0], SCROLL_UP);
+        }
+        break;
+      case 'L':
+        /* insert n lines */
+        if (DCEvars[0] == 0) {
+          insertLine(R, SCROLL_DOWN);
+        } else {
+          insertLine(R, DCEvars[0], SCROLL_DOWN);
+        }
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + "" + (c) + " (at R " + R + ")");
+        }
+        break;
+      case 'T': /* 'ri' aka scroll backward */
+        if (DCEvars[0] == 0) {
+          insertLine(0, SCROLL_DOWN);
+        } else {
+          insertLine(0, DCEvars[0], SCROLL_DOWN);
+        }
+        break;
+      case 'M':
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + "" + (c) + " at R=" + R);
+        }
+        if (DCEvars[0] == 0) {
+          deleteLine(R);
+        } else {
+          for (int i = 0; i < DCEvars[0]; i++) {
+            deleteLine(R);
+          }
+        }
+        break;
+      case 'K':
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " K");
+        }
+        /* clear in line */
+        switch (DCEvars[0]) {
+        case 6: /* 97801 uses ESC[6K for delete to end of line */
+        case 0:/* clear to right */
+          if (C < columns - 1) {
+            deleteArea(C, R, columns - C, 1, attributes);
+          }
+          break;
+        case 1:/* clear to the left, including this */
+          if (C > 0) {
+            deleteArea(0, R, C + 1, 1, attributes);
+          }
+          break;
+        case 2:/* clear whole line */
+          deleteArea(0, R, columns, 1, attributes);
+          break;
+        }
+        break;
+      case 'J':
+        /* clear below current line */
+        switch (DCEvars[0]) {
+        case 0:
+          if (R < rows - 1) {
+            deleteArea(0, R + 1, columns, rows - R - 1, attributes);
+          }
+          if (C < columns - 1) {
+            deleteArea(C, R, columns - C, 1, attributes);
+          }
+          break;
+        case 1:
+          if (R > 0) {
+            deleteArea(0, 0, columns, R, attributes);
+          }
+          if (C > 0) {
+            deleteArea(0, R, C + 1, 1, attributes);// include up to and including current
+          }
+          break;
+        case 2:
+          deleteArea(0, 0, columns, rows, attributes);
+          break;
+        }
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " J");
+        }
+        break;
+      case '@':
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " @");
+        }
+        for (int i = 0; i < DCEvars[0]; i++) {
+          insertChar(C, R, ' ', attributes);
+        }
+        break;
+      case 'X': {
+        int toerase = DCEvars[0];
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " X, C=" + C + ",R=" + R);
+        }
+        if (toerase == 0) {
+          toerase = 1;
+        }
+        if (toerase + C > columns) {
+          toerase = columns - C;
+        }
+        deleteArea(C, R, toerase, 1, attributes);
+        // does not change cursor position
+        break;
+      }
+      case 'P':
+        if (debug > 1) {
+          debug("ESC [ " + DCEvars[0] + " P, C=" + C + ",R=" + R);
+        }
+        if (DCEvars[0] == 0) {
+          DCEvars[0] = 1;
+        }
+        for (int i = 0; i < DCEvars[0]; i++) {
+          deleteChar(C, R);
+        }
+        break;
+      case 'n':
+        switch (DCEvars[0]) {
+        case 5: /* malfunction? No malfunction. */
+          writeSpecial((ESC) + "[0n");
+          if (debug > 1) {
+            debug("ESC[5n");
+          }
+          break;
+        case 6:
+          // DO NOT offset R and C by 1! (checked against /usr/X11R6/bin/resize
+          // FIXME check again.
+          // FIXME: but vttest thinks different???
+          writeSpecial((ESC) + "[" + R + ";" + C + "R");
+          if (debug > 1) {
+            debug("ESC[6n");
+          }
+          break;
+        default:
+          if (debug > 0) {
+            debug("ESC [ " + DCEvars[0] + " n??");
+          }
+          break;
+        }
+        break;
+      case 's': /* DECSC - save cursor */
+        Sc = C;
+        Sr = R;
+        Sa = attributes;
+        if (debug > 3) {
+          debug("ESC[s");
+        }
+        break;
+      case 'u': /* DECRC - restore cursor */
+        C = Sc;
+        R = Sr;
+        attributes = Sa;
+        if (debug > 3) {
+          debug("ESC[u");
+        }
+        break;
+      case 'm': /* attributes as color, bold , blink, */
+        if (debug > 3) {
+          debug("ESC [ ");
+        }
+        if (DCEvar == 0 && DCEvars[0] == 0) {
+          attributes = 0;
+        }
+        for (int i = 0; i <= DCEvar; i++) {
+          switch (DCEvars[i]) {
+          case 0:
+            if (DCEvar > 0) {
+              if (terminalID.equals("scoansi")) {
+                attributes &= COLOR; /* Keeps color. Strange but true. */
+              } else {
+                attributes = 0;
+              }
+            }
+            break;
+          case 1:
+            attributes |= BOLD;
+            attributes &= ~LOW;
+            break;
+          case 2:
+            /* SCO color hack mode */
+            if (terminalID.equals("scoansi") && ((DCEvar - i) >= 2)) {
+              int ncolor;
+              attributes &= ~(COLOR | BOLD);
+
+              ncolor = DCEvars[i + 1];
+              if ((ncolor & 8) == 8) {
+                attributes |= BOLD;
+              }
+              ncolor = ((ncolor & 1) << 2) | (ncolor & 2) | ((ncolor & 4) >> 2);
+              attributes |= ((ncolor) + 1) << COLOR_FG_SHIFT;
+              ncolor = DCEvars[i + 2];
+              ncolor = ((ncolor & 1) << 2) | (ncolor & 2) | ((ncolor & 4) >> 2);
+              attributes |= ((ncolor) + 1) << COLOR_BG_SHIFT;
+              i += 2;
+            } else {
+              attributes |= LOW;
+            }
+            break;
+          case 3: /* italics */
+            attributes |= INVERT;
+            break;
+          case 4:
+            attributes |= UNDERLINE;
+            break;
+          case 7:
+            attributes |= INVERT;
+            break;
+          case 8:
+            attributes |= INVISIBLE;
+            break;
+          case 5: /* blink on */
+            break;
+          /*
+           * 10 - ANSI X3.64-1979, select primary font, don't display control chars, don't set bit 8
+           * on output
+           */
+          case 10:
+            gl = 0;
+            usedcharsets = true;
+            break;
+          /*
+           * 11 - ANSI X3.64-1979, select second alt. font, display control chars, set bit 8 on
+           * output
+           */
+          case 11: /* SMACS , as */
+          case 12:
+            gl = 1;
+            usedcharsets = true;
+            break;
+          case 21: /* normal intensity */
+            attributes &= ~(LOW | BOLD);
+            break;
+          case 23: /* italics off */
+            attributes &= ~INVERT;
+            break;
+          case 25: /* blinking off */
+            break;
+          case 27:
+            attributes &= ~INVERT;
+            break;
+          case 28:
+            attributes &= ~INVISIBLE;
+            break;
+          case 24:
+            attributes &= ~UNDERLINE;
+            break;
+          case 22:
+            attributes &= ~BOLD;
+            break;
+          case 30:
+          case 31:
+          case 32:
+          case 33:
+          case 34:
+          case 35:
+          case 36:
+          case 37:
+            attributes &= ~COLOR_FG;
+            attributes |= ((DCEvars[i] - 30) + 1) << COLOR_FG_SHIFT;
+            break;
+          case 38:
+            if (DCEvars[i + 1] == 5) {
+              attributes &= ~COLOR_FG;
+              attributes |= ((DCEvars[i + 2]) + 1) << COLOR_FG_SHIFT;
+              i += 2;
+            }
+            break;
+          case 39:
+            attributes &= ~COLOR_FG;
+            break;
+          case 40:
+          case 41:
+          case 42:
+          case 43:
+          case 44:
+          case 45:
+          case 46:
+          case 47:
+            attributes &= ~COLOR_BG;
+            attributes |= ((DCEvars[i] - 40) + 1) << COLOR_BG_SHIFT;
+            break;
+          case 48:
+            if (DCEvars[i + 1] == 5) {
+              attributes &= ~COLOR_BG;
+              attributes |= (DCEvars[i + 2] + 1) << COLOR_BG_SHIFT;
+              i += 2;
+            }
+            break;
+          case 49:
+            attributes &= ~COLOR_BG;
+            break;
+          case 90:
+          case 91:
+          case 92:
+          case 93:
+          case 94:
+          case 95:
+          case 96:
+          case 97:
+            attributes &= ~COLOR_FG;
+            attributes |= ((DCEvars[i] - 82) + 1) << COLOR_FG_SHIFT;
+            break;
+          case 100:
+          case 101:
+          case 102:
+          case 103:
+          case 104:
+          case 105:
+          case 106:
+          case 107:
+            attributes &= ~COLOR_BG;
+            attributes |= ((DCEvars[i] - 92) + 1) << COLOR_BG_SHIFT;
+            break;
+
+          default:
+            debugStr.append("ESC [ ").append(DCEvars[i]).append(" m unknown...");
+            debug(debugStr.toString());
+            debugStr.setLength(0);
+            break;
+          }
+          if (debug > 3) {
+            debugStr.append(DCEvars[i]).append(';');
+            debug(debugStr.toString());
+            debugStr.setLength(0);
+          }
+        }
+        if (debug > 3) {
+          debugStr.append(" (attributes = ").append(attributes).append(")m");
+          debug(debugStr.toString());
+          debugStr.setLength(0);
+        }
+        break;
+      default:
+        debugStr.append("ESC [ unknown letter: ").append(c).append(" (").append((int) c)
+            .append(')');
+        debug(debugStr.toString());
+        debugStr.setLength(0);
+        break;
+      }
+      break;
+    case TSTATE_TITLE:
+      switch (c) {
+      case ESC:
+        term_state = TSTATE_ESC;
+        break;
+      default:
+        // TODO save title
+        break;
+      }
+      break;
+    default:
+      term_state = TSTATE_DATA;
+      break;
+    }
+
+    setCursorPosition(C, R);
+  }
+
+  /* hard reset the terminal */
+  public void reset() {
+    gx[0] = 'B';
+    gx[1] = 'B';
+    gx[2] = 'B';
+    gx[3] = 'B';
+
+    gl = 0; // default GL to G0
+    gr = 2; // default GR to G2
+
+    onegl = -1; // Single shift override
+
+    /* reset tabs */
+    int nw = width;
+    if (nw < 132) {
+      nw = 132;
+    }
+    Tabs = new byte[nw];
+    for (int i = 0; i < nw; i += 8) {
+      Tabs[i] = 1;
+    }
+
+    deleteArea(0, 0, width, height, attributes);
+    setMargins(0, height);
+    C = R = 0;
+    _SetCursor(0, 0);
+
+    if (display != null) {
+      display.resetColors();
+    }
+
+    showCursor(true);
+    /* FIXME: */
+    term_state = TSTATE_DATA;
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/apache/harmony/niochar/charset/additional/IBM437.java b/ScriptingLayerForAndroid/src/org/apache/harmony/niochar/charset/additional/IBM437.java
new file mode 100644
index 0000000..1f01579
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/apache/harmony/niochar/charset/additional/IBM437.java
@@ -0,0 +1,423 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.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.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 org.apache.harmony.niochar.charset.additional;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+
+/* TODO: support direct byte buffers
+import org.apache.harmony.nio.AddressUtil;
+import org.apache.harmony.niochar.CharsetProviderImpl;
+*/
+
+public class IBM437 extends Charset {
+
+        public IBM437(String csName, String[] aliases) {
+            super(csName, aliases);
+        }
+
+        public boolean contains(Charset cs) {
+            return cs.name().equalsIgnoreCase("IBM367") || cs.name().equalsIgnoreCase("IBM437") || cs.name().equalsIgnoreCase("US-ASCII") ;
+        }
+
+        public CharsetDecoder newDecoder() {
+            return new Decoder(this);
+        }
+
+        public CharsetEncoder newEncoder() {
+            return new Encoder(this);
+        }
+
+	private static final class Decoder extends CharsetDecoder{
+		private Decoder(Charset cs){
+			super(cs, 1, 1);
+
+		}
+
+		private native int nDecode(char[] array, int arrPosition, int remaining, long outAddr, int absolutePos);
+
+
+		protected CoderResult decodeLoop(ByteBuffer bb, CharBuffer cb){
+                        int cbRemaining = cb.remaining();
+/* TODO: support direct byte buffers
+		        if(CharsetProviderImpl.hasLoadedNatives() && bb.isDirect() && bb.hasRemaining() && cb.hasArray()){
+		            int toProceed = bb.remaining();
+		            int cbPos = cb.position();
+		            int bbPos = bb.position();
+		            boolean throwOverflow = false;
+		            if( cbRemaining < toProceed ) {
+		                toProceed = cbRemaining;
+                                throwOverflow = true;
+                            }
+                            int res = nDecode(cb.array(), cb.arrayOffset()+cbPos, toProceed, AddressUtil.getDirectBufferAddress(bb), bbPos);
+                            bb.position(bbPos+res);
+                            cb.position(cbPos+res);
+                            if(throwOverflow) return CoderResult.OVERFLOW;
+                        }else{
+*/
+                            if(bb.hasArray() && cb.hasArray()) {
+                                int rem = bb.remaining();
+                                rem = cbRemaining >= rem ? rem : cbRemaining;
+                                byte[] bArr = bb.array();
+                                char[] cArr = cb.array();
+                                int bStart = bb.position();
+                                int cStart = cb.position();
+                                int i;
+                                for(i=bStart; i<bStart+rem; i++) {
+                                    char in = (char)(bArr[i] & 0xFF);
+                                    if(in >= 26){
+                                        int index = (int)in - 26;
+                                        cArr[cStart++] = (char)arr[index];
+                                    }else {
+                                        cArr[cStart++] = (char)(in & 0xFF);
+                                    }
+                                }
+                                bb.position(i);
+                                cb.position(cStart);
+                                if(rem == cbRemaining && bb.hasRemaining()) return CoderResult.OVERFLOW;
+                            } else {
+                                while(bb.hasRemaining()){
+                                    if( cbRemaining == 0 ) return CoderResult.OVERFLOW;
+                                    char in = (char)(bb.get() & 0xFF);
+                                    if(in >= 26){
+                                        int index = (int)in - 26;
+                                        cb.put(arr[index]);
+                                    }else {
+                                        cb.put((char)(in & 0xFF));
+                                    }
+                                cbRemaining--;
+                                }
+/*
+                            }
+*/
+			}
+                        return CoderResult.UNDERFLOW;
+		}
+
+		final static char[] arr = {
+                0x001C,0x001B,0x007F,0x001D,0x001E,0x001F,
+                0x0020,0x0021,0x0022,0x0023,0x0024,0x0025,0x0026,0x0027,
+                0x0028,0x0029,0x002A,0x002B,0x002C,0x002D,0x002E,0x002F,
+                0x0030,0x0031,0x0032,0x0033,0x0034,0x0035,0x0036,0x0037,
+                0x0038,0x0039,0x003A,0x003B,0x003C,0x003D,0x003E,0x003F,
+                0x0040,0x0041,0x0042,0x0043,0x0044,0x0045,0x0046,0x0047,
+                0x0048,0x0049,0x004A,0x004B,0x004C,0x004D,0x004E,0x004F,
+                0x0050,0x0051,0x0052,0x0053,0x0054,0x0055,0x0056,0x0057,
+                0x0058,0x0059,0x005A,0x005B,0x005C,0x005D,0x005E,0x005F,
+                0x0060,0x0061,0x0062,0x0063,0x0064,0x0065,0x0066,0x0067,
+                0x0068,0x0069,0x006A,0x006B,0x006C,0x006D,0x006E,0x006F,
+                0x0070,0x0071,0x0072,0x0073,0x0074,0x0075,0x0076,0x0077,
+                0x0078,0x0079,0x007A,0x007B,0x007C,0x007D,0x007E,0x001A,
+                0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7,
+                0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5,
+                0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9,
+                0x00FF,0x00D6,0x00DC,0x00A2,0x00A3,0x00A5,0x20A7,0x0192,
+                0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA,
+                0x00BF,0x2310,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB,
+                0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556,
+                0x2555,0x2563,0x2551,0x2557,0x255D,0x255C,0x255B,0x2510,
+                0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x255E,0x255F,
+                0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x2567,
+                0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256B,
+                0x256A,0x2518,0x250C,0x2588,0x2584,0x258C,0x2590,0x2580,
+                0x03B1,0x00DF,0x0393,0x03C0,0x03A3,0x03C3,0x03BC,0x03C4,
+                0x03A6,0x0398,0x03A9,0x03B4,0x221E,0x03C6,0x03B5,0x2229,
+                0x2261,0x00B1,0x2265,0x2264,0x2320,0x2321,0x00F7,0x2248,
+                0x00B0,0x2219,0x00B7,0x221A,0x207F,0x00B2,0x25A0,0x00A0
+		};
+        }
+
+	private static final class Encoder extends CharsetEncoder{
+		private Encoder(Charset cs){
+			super(cs, 1, 1);
+		}
+
+		private native void nEncode(long outAddr, int absolutePos, char[] array, int arrPosition, int[] res);
+
+		protected CoderResult encodeLoop(CharBuffer cb, ByteBuffer bb){
+                        int bbRemaining = bb.remaining();
+/* TODO: support direct byte buffers
+                        if(CharsetProviderImpl.hasLoadedNatives() && bb.isDirect() && cb.hasRemaining() && cb.hasArray()){
+		            int toProceed = cb.remaining();
+		            int cbPos = cb.position();
+		            int bbPos = bb.position();
+		            boolean throwOverflow = false;
+		            if( bbRemaining < toProceed ) {
+		                toProceed = bbRemaining;
+                                throwOverflow = true;
+                            }
+                            int[] res = {toProceed, 0};
+                            nEncode(AddressUtil.getDirectBufferAddress(bb), bbPos, cb.array(), cb.arrayOffset()+cbPos, res);
+                            if( res[0] <= 0 ) {
+                                bb.position(bbPos-res[0]);
+                                cb.position(cbPos-res[0]);
+                                if(res[1]!=0) {
+                                    if(res[1] < 0)
+                                        return CoderResult.malformedForLength(-res[1]);
+                                    else
+                                        return CoderResult.unmappableForLength(res[1]);
+                                }
+                            }else{
+                                bb.position(bbPos+res[0]);
+                                cb.position(cbPos+res[0]);
+                                if(throwOverflow) return CoderResult.OVERFLOW;
+                            }
+                        }else{
+*/
+                            if(bb.hasArray() && cb.hasArray()) {
+                                byte[] byteArr = bb.array();
+                                char[] charArr = cb.array();
+                                int rem = cb.remaining();
+                                int byteArrStart = bb.position();
+                                rem = bbRemaining <= rem ? bbRemaining : rem;
+                                int x;
+                                for(x = cb.position(); x < cb.position()+rem; x++) {
+                                    char c = charArr[x];
+                                    if(c > (char)0x25A0){
+                                        if (c >= 0xD800 && c <= 0xDFFF) {
+                                            if(x+1 < cb.limit()) {
+                                                char c1 = charArr[x+1];
+                                                if(c1 >= 0xD800 && c1 <= 0xDFFF) {
+                                                    cb.position(x); bb.position(byteArrStart);
+                                                    return CoderResult.unmappableForLength(2);
+                                                }
+                                            } else {
+                                                cb.position(x); bb.position(byteArrStart);
+                                                return CoderResult.UNDERFLOW;
+                                            }
+                                            cb.position(x); bb.position(byteArrStart);
+                                            return CoderResult.malformedForLength(1);
+                                        }
+                                        cb.position(x); bb.position(byteArrStart);
+                                        return CoderResult.unmappableForLength(1);
+                                    }else{
+                                        if(c < 0x1A) {
+                                            byteArr[byteArrStart++] = (byte)c;
+                                        } else {
+                                            int index = (int)c >> 8;
+                                            index = encodeIndex[index];
+                                            if(index < 0) {
+                                                cb.position(x); bb.position(byteArrStart);
+                                                return CoderResult.unmappableForLength(1);
+                                            }
+                                            index <<= 8;
+                                            index += (int)c & 0xFF;
+                                            if((byte)arr[index] != 0){
+                                                byteArr[byteArrStart++] = (byte)arr[index];
+                                            }else{
+                                                cb.position(x); bb.position(byteArrStart);
+                                                return CoderResult.unmappableForLength(1);
+                                            }
+                                        }
+                                    }
+                                }
+                                cb.position(x);
+                                bb.position(byteArrStart);
+                                if(rem == bbRemaining && cb.hasRemaining()) {
+                                    return CoderResult.OVERFLOW;
+                                }
+                            } else {
+                                while(cb.hasRemaining()){
+                                    if( bbRemaining == 0 ) return CoderResult.OVERFLOW;
+                                    char c = cb.get();
+                                    if(c > (char)0x25A0){
+                                        if (c >= 0xD800 && c <= 0xDFFF) {
+                                            if(cb.hasRemaining()) {
+                                                char c1 = cb.get();
+                                                if(c1 >= 0xD800 && c1 <= 0xDFFF) {
+                                                    cb.position(cb.position()-2);
+                                                    return CoderResult.unmappableForLength(2);
+                                                } else {
+                                                    cb.position(cb.position()-1);
+                                                }
+                                            } else {
+                                                cb.position(cb.position()-1);
+                                                return CoderResult.UNDERFLOW;
+                                            }
+                                            cb.position(cb.position()-1);
+                                            return CoderResult.malformedForLength(1);
+                                        }
+                                        cb.position(cb.position()-1);
+                                        return CoderResult.unmappableForLength(1);
+                                    }else{
+                                        if(c < 0x1A) {
+                                            bb.put((byte)c);
+                                        } else {
+                                            int index = (int)c >> 8;
+                                            index = encodeIndex[index];
+                                            if(index < 0) {
+                                                cb.position(cb.position()-1);
+                                                return CoderResult.unmappableForLength(1);
+                                            }
+                                            index <<= 8;
+                                            index += (int)c & 0xFF;
+                                            if((byte)arr[index] != 0){
+                                                bb.put((byte)arr[index]);
+                                            }else{
+                                                cb.position(cb.position()-1);
+                                                return CoderResult.unmappableForLength(1);
+                                            }
+                                        }
+                                        bbRemaining--;
+                                    }
+                                }
+/* TODO: support direct byte buffers
+			    }
+*/
+			}
+			return CoderResult.UNDERFLOW;
+		}
+
+                final static char arr[] = {
+
+                0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,
+                0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x7F,0x1B,0x1A,0x1D,0x1E,0x1F,
+                0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,
+                0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,
+                0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,
+                0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,
+                0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F,
+                0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x1C,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0xFF,0xAD,0x9B,0x9C,0x00,0x9D,0x00,0x00,0x00,0x00,0xA6,0xAE,0xAA,0x00,0x00,0x00,
+                0xF8,0xF1,0xFD,0x00,0x00,0x00,0x00,0xFA,0x00,0x00,0xA7,0xAF,0xAC,0xAB,0x00,0xA8,
+                0x00,0x00,0x00,0x00,0x8E,0x8F,0x92,0x80,0x00,0x90,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0xA5,0x00,0x00,0x00,0x00,0x99,0x00,0x00,0x00,0x00,0x00,0x9A,0x00,0x00,0xE1,
+                0x85,0xA0,0x83,0x00,0x84,0x86,0x91,0x87,0x8A,0x82,0x88,0x89,0x8D,0xA1,0x8C,0x8B,
+                0x00,0xA4,0x95,0xA2,0x93,0x00,0x94,0xF6,0x00,0x97,0xA3,0x96,0x81,0x00,0x00,0x98,
+
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x9F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0xE2,0x00,0x00,0x00,0x00,0xE9,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0xE4,0x00,0x00,0xE8,0x00,0x00,0xEA,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0xE0,0x00,0x00,0xEB,0xEE,0x00,0x00,0x00,0x00,0x00,0x00,0xE6,0x00,0x00,0x00,
+                0xE3,0x00,0x00,0xE5,0xE7,0x00,0xED,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFC,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x9E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF9,0xFB,0x00,0x00,0x00,0xEC,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xEF,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0xF0,0x00,0x00,0xF3,0xF2,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0xA9,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0xF4,0xF5,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+
+                0xC4,0x00,0xB3,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xDA,0x00,0x00,0x00,
+                0xBF,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xD9,0x00,0x00,0x00,0xC3,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0xB4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC2,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0xC1,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC5,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0xCD,0xBA,0xD5,0xD6,0xC9,0xB8,0xB7,0xBB,0xD4,0xD3,0xC8,0xBE,0xBD,0xBC,0xC6,0xC7,
+                0xCC,0xB5,0xB6,0xB9,0xD1,0xD2,0xCB,0xCF,0xD0,0xCA,0xD8,0xD7,0xCE,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0xDF,0x00,0x00,0x00,0xDC,0x00,0x00,0x00,0xDB,0x00,0x00,0x00,0xDD,0x00,0x00,0x00,
+                0xDE,0xB0,0xB1,0xB2,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0xFE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+                0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
+                };
+
+                final static int[] encodeIndex = {
+                 0,1,-1,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+                 3,-1,4,5,-1,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+                 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+                 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+                 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+                 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+                 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+                 -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
+                };
+	}
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/ConsoleActivity.java b/ScriptingLayerForAndroid/src/org/connectbot/ConsoleActivity.java
new file mode 100644
index 0000000..bb4490a
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/ConsoleActivity.java
@@ -0,0 +1,985 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+
+/**
+ * @author modified by raaar
+ *
+ */
+
+package org.connectbot;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.preference.PreferenceManager;
+import android.text.ClipboardManager;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
+
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.ScriptProcess;
+import com.googlecode.android_scripting.activity.Preferences;
+import com.googlecode.android_scripting.activity.ScriptingLayerService;
+
+import de.mud.terminal.VDUBuffer;
+import de.mud.terminal.vt320;
+
+import org.connectbot.service.PromptHelper;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.PreferenceConstants;
+import org.connectbot.util.SelectionArea;
+
+public class ConsoleActivity extends Activity {
+
+  protected static final int REQUEST_EDIT = 1;
+
+  private static final int CLICK_TIME = 250;
+  private static final float MAX_CLICK_DISTANCE = 25f;
+  private static final int KEYBOARD_DISPLAY_TIME = 1250;
+
+  // Direction to shift the ViewFlipper
+  private static final int SHIFT_LEFT = 0;
+  private static final int SHIFT_RIGHT = 1;
+
+  protected ViewFlipper flip = null;
+  protected TerminalManager manager = null;
+  protected ScriptingLayerService mService = null;
+  protected LayoutInflater inflater = null;
+
+  private SharedPreferences prefs = null;
+
+  private PowerManager.WakeLock wakelock = null;
+
+  protected Integer processID;
+
+  protected ClipboardManager clipboard;
+
+  private RelativeLayout booleanPromptGroup;
+  private TextView booleanPrompt;
+  private Button booleanYes, booleanNo;
+
+  private Animation slide_left_in, slide_left_out, slide_right_in, slide_right_out,
+      fade_stay_hidden, fade_out_delayed;
+
+  private Animation keyboard_fade_in, keyboard_fade_out;
+  private ImageView keyboardButton;
+  private float lastX, lastY;
+
+  private int mTouchSlopSquare;
+
+  private InputMethodManager inputManager;
+
+  protected TerminalBridge copySource = null;
+  private int lastTouchRow, lastTouchCol;
+
+  private boolean forcedOrientation;
+
+  private Handler handler = new Handler();
+
+  private static enum MenuId {
+    EDIT, PREFS, EMAIL, RESIZE, COPY, PASTE;
+    public int getId() {
+      return ordinal() + Menu.FIRST;
+    }
+  }
+
+  private final ServiceConnection mConnection = new ServiceConnection() {
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder service) {
+      mService = ((ScriptingLayerService.LocalBinder) service).getService();
+      manager = mService.getTerminalManager();
+      // let manager know about our event handling services
+      manager.setDisconnectHandler(disconnectHandler);
+
+      Log.d(String.format("Connected to TerminalManager and found bridges.size=%d", manager
+          .getBridgeList().size()));
+
+      manager.setResizeAllowed(true);
+
+      // clear out any existing bridges and record requested index
+      flip.removeAllViews();
+
+      int requestedIndex = 0;
+
+      TerminalBridge requestedBridge = manager.getConnectedBridge(processID);
+
+      // If we didn't find the requested connection, try opening it
+      if (processID != null && requestedBridge == null) {
+        try {
+          Log.d(String.format(
+              "We couldnt find an existing bridge with id = %d, so creating one now", processID));
+          requestedBridge = manager.openConnection(processID);
+        } catch (Exception e) {
+          Log.e("Problem while trying to create new requested bridge", e);
+        }
+      }
+
+      // create views for all bridges on this service
+      for (TerminalBridge bridge : manager.getBridgeList()) {
+
+        final int currentIndex = addNewTerminalView(bridge);
+
+        // check to see if this bridge was requested
+        if (bridge == requestedBridge) {
+          requestedIndex = currentIndex;
+        }
+      }
+
+      setDisplayedTerminal(requestedIndex);
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+      manager = null;
+      mService = null;
+    }
+  };
+
+  protected Handler promptHandler = new Handler() {
+    @Override
+    public void handleMessage(Message msg) {
+      // someone below us requested to display a prompt
+      updatePromptVisible();
+    }
+  };
+
+  protected Handler disconnectHandler = new Handler() {
+    @Override
+    public void handleMessage(Message msg) {
+      Log.d("Someone sending HANDLE_DISCONNECT to parentHandler");
+      TerminalBridge bridge = (TerminalBridge) msg.obj;
+      closeBridge(bridge);
+    }
+  };
+
+  /**
+   * @param bridge
+   */
+  private void closeBridge(final TerminalBridge bridge) {
+    synchronized (flip) {
+      final int flipIndex = getFlipIndex(bridge);
+
+      if (flipIndex >= 0) {
+        if (flip.getDisplayedChild() == flipIndex) {
+          shiftCurrentTerminal(SHIFT_LEFT);
+        }
+        flip.removeViewAt(flipIndex);
+
+        /*
+         * TODO Remove this workaround when ViewFlipper is fixed to listen to view removals. Android
+         * Issue 1784
+         */
+        final int numChildren = flip.getChildCount();
+        if (flip.getDisplayedChild() >= numChildren && numChildren > 0) {
+          flip.setDisplayedChild(numChildren - 1);
+        }
+      }
+
+      // If we just closed the last bridge, go back to the previous activity.
+      if (flip.getChildCount() == 0) {
+        finish();
+      }
+    }
+  }
+
+  protected View findCurrentView(int id) {
+    View view = flip.getCurrentView();
+    if (view == null) {
+      return null;
+    }
+    return view.findViewById(id);
+  }
+
+  protected PromptHelper getCurrentPromptHelper() {
+    View view = findCurrentView(R.id.console_flip);
+    if (!(view instanceof TerminalView)) {
+      return null;
+    }
+    return ((TerminalView) view).bridge.getPromptHelper();
+  }
+
+  protected void hideAllPrompts() {
+    booleanPromptGroup.setVisibility(View.GONE);
+  }
+
+  @Override
+  public void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+
+    this.setContentView(R.layout.act_console);
+
+    clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+    prefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+    // hide status bar if requested by user
+    if (prefs.getBoolean(PreferenceConstants.FULLSCREEN, false)) {
+      getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+          WindowManager.LayoutParams.FLAG_FULLSCREEN);
+    }
+
+    // TODO find proper way to disable volume key beep if it exists.
+    setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+    PowerManager manager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+    wakelock = manager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getPackageName());
+
+    // handle requested console from incoming intent
+    int id = getIntent().getIntExtra(Constants.EXTRA_PROXY_PORT, -1);
+
+    if (id > 0) {
+      processID = id;
+    }
+
+    inflater = LayoutInflater.from(this);
+
+    flip = (ViewFlipper) findViewById(R.id.console_flip);
+    booleanPromptGroup = (RelativeLayout) findViewById(R.id.console_boolean_group);
+    booleanPrompt = (TextView) findViewById(R.id.console_prompt);
+
+    booleanYes = (Button) findViewById(R.id.console_prompt_yes);
+    booleanYes.setOnClickListener(new OnClickListener() {
+      public void onClick(View v) {
+        PromptHelper helper = getCurrentPromptHelper();
+        if (helper == null) {
+          return;
+        }
+        helper.setResponse(Boolean.TRUE);
+        updatePromptVisible();
+      }
+    });
+
+    booleanNo = (Button) findViewById(R.id.console_prompt_no);
+    booleanNo.setOnClickListener(new OnClickListener() {
+      public void onClick(View v) {
+        PromptHelper helper = getCurrentPromptHelper();
+        if (helper == null) {
+          return;
+        }
+        helper.setResponse(Boolean.FALSE);
+        updatePromptVisible();
+      }
+    });
+
+    // preload animations for terminal switching
+    slide_left_in = AnimationUtils.loadAnimation(this, R.anim.slide_left_in);
+    slide_left_out = AnimationUtils.loadAnimation(this, R.anim.slide_left_out);
+    slide_right_in = AnimationUtils.loadAnimation(this, R.anim.slide_right_in);
+    slide_right_out = AnimationUtils.loadAnimation(this, R.anim.slide_right_out);
+
+    fade_out_delayed = AnimationUtils.loadAnimation(this, R.anim.fade_out_delayed);
+    fade_stay_hidden = AnimationUtils.loadAnimation(this, R.anim.fade_stay_hidden);
+
+    // Preload animation for keyboard button
+    keyboard_fade_in = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_in);
+    keyboard_fade_out = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_out);
+
+    inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+    keyboardButton = (ImageView) findViewById(R.id.keyboard_button);
+    keyboardButton.setOnClickListener(new OnClickListener() {
+      public void onClick(View view) {
+        View flip = findCurrentView(R.id.console_flip);
+        if (flip == null) {
+          return;
+        }
+
+        inputManager.showSoftInput(flip, InputMethodManager.SHOW_FORCED);
+        keyboardButton.setVisibility(View.GONE);
+      }
+    });
+    if (prefs.getBoolean(PreferenceConstants.HIDE_KEYBOARD, false)) {
+      // Force hidden keyboard.
+      getWindow().setSoftInputMode(
+          WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
+              | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
+    }
+    final ViewConfiguration configuration = ViewConfiguration.get(this);
+    int touchSlop = configuration.getScaledTouchSlop();
+    mTouchSlopSquare = touchSlop * touchSlop;
+
+    // detect fling gestures to switch between terminals
+    final GestureDetector detect =
+        new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
+          private float totalY = 0;
+
+          @Override
+          public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+
+            final float distx = e2.getRawX() - e1.getRawX();
+            final float disty = e2.getRawY() - e1.getRawY();
+            final int goalwidth = flip.getWidth() / 2;
+
+            // need to slide across half of display to trigger console change
+            // make sure user kept a steady hand horizontally
+            if (Math.abs(disty) < (flip.getHeight() / 4)) {
+              if (distx > goalwidth) {
+                shiftCurrentTerminal(SHIFT_RIGHT);
+                return true;
+              }
+
+              if (distx < -goalwidth) {
+                shiftCurrentTerminal(SHIFT_LEFT);
+                return true;
+              }
+
+            }
+
+            return false;
+          }
+
+          @Override
+          public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+
+            // if copying, then ignore
+            if (copySource != null && copySource.isSelectingForCopy()) {
+              return false;
+            }
+
+            if (e1 == null || e2 == null) {
+              return false;
+            }
+
+            // if releasing then reset total scroll
+            if (e2.getAction() == MotionEvent.ACTION_UP) {
+              totalY = 0;
+            }
+
+            // activate consider if within x tolerance
+            if (Math.abs(e1.getX() - e2.getX()) < ViewConfiguration.getTouchSlop() * 4) {
+
+              View flip = findCurrentView(R.id.console_flip);
+              if (flip == null) {
+                return false;
+              }
+              TerminalView terminal = (TerminalView) flip;
+
+              // estimate how many rows we have scrolled through
+              // accumulate distance that doesn't trigger immediate scroll
+              totalY += distanceY;
+              final int moved = (int) (totalY / terminal.bridge.charHeight);
+
+              VDUBuffer buffer = terminal.bridge.getVDUBuffer();
+
+              // consume as scrollback only if towards right half of screen
+              if (e2.getX() > flip.getWidth() / 2) {
+                if (moved != 0) {
+                  int base = buffer.getWindowBase();
+                  buffer.setWindowBase(base + moved);
+                  totalY = 0;
+                  return true;
+                }
+              } else {
+                // otherwise consume as pgup/pgdown for every 5 lines
+                if (moved > 5) {
+                  ((vt320) buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0);
+                  terminal.bridge.tryKeyVibrate();
+                  totalY = 0;
+                  return true;
+                } else if (moved < -5) {
+                  ((vt320) buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0);
+                  terminal.bridge.tryKeyVibrate();
+                  totalY = 0;
+                  return true;
+                }
+
+              }
+
+            }
+
+            return false;
+          }
+
+        });
+
+    flip.setOnCreateContextMenuListener(this);
+
+    flip.setOnTouchListener(new OnTouchListener() {
+
+      public boolean onTouch(View v, MotionEvent event) {
+
+        // when copying, highlight the area
+        if (copySource != null && copySource.isSelectingForCopy()) {
+          int row = (int) Math.floor(event.getY() / copySource.charHeight);
+          int col = (int) Math.floor(event.getX() / copySource.charWidth);
+
+          SelectionArea area = copySource.getSelectionArea();
+
+          switch (event.getAction()) {
+          case MotionEvent.ACTION_DOWN:
+            // recording starting area
+            if (area.isSelectingOrigin()) {
+              area.setRow(row);
+              area.setColumn(col);
+              lastTouchRow = row;
+              lastTouchCol = col;
+              copySource.redraw();
+            }
+            return true;
+          case MotionEvent.ACTION_MOVE:
+            /*
+             * ignore when user hasn't moved since last time so we can fine-tune with directional
+             * pad
+             */
+            if (row == lastTouchRow && col == lastTouchCol) {
+              return true;
+            }
+            // if the user moves, start the selection for other corner
+            area.finishSelectingOrigin();
+
+            // update selected area
+            area.setRow(row);
+            area.setColumn(col);
+            lastTouchRow = row;
+            lastTouchCol = col;
+            copySource.redraw();
+            return true;
+          case MotionEvent.ACTION_UP:
+            /*
+             * If they didn't move their finger, maybe they meant to select the rest of the text
+             * with the directional pad.
+             */
+            if (area.getLeft() == area.getRight() && area.getTop() == area.getBottom()) {
+              return true;
+            }
+
+            // copy selected area to clipboard
+            String copiedText = area.copyFrom(copySource.getVDUBuffer());
+
+            clipboard.setText(copiedText);
+            Toast.makeText(ConsoleActivity.this,
+                getString(R.string.terminal_copy_done, copiedText.length()), Toast.LENGTH_LONG)
+                .show();
+            // fall through to clear state
+
+          case MotionEvent.ACTION_CANCEL:
+            // make sure we clear any highlighted area
+            area.reset();
+            copySource.setSelectingForCopy(false);
+            copySource.redraw();
+            return true;
+          }
+        }
+
+        Configuration config = getResources().getConfiguration();
+
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+          lastX = event.getX();
+          lastY = event.getY();
+        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
+          final int deltaX = (int) (lastX - event.getX());
+          final int deltaY = (int) (lastY - event.getY());
+          int distance = (deltaX * deltaX) + (deltaY * deltaY);
+          if (distance > mTouchSlopSquare) {
+            // If currently scheduled long press event is not canceled here,
+            // GestureDetector.onScroll is executed, which takes a while, and by the time we are
+            // back in the view's dispatchTouchEvent
+            // mPendingCheckForLongPress is already executed
+            flip.cancelLongPress();
+          }
+        } else if (event.getAction() == MotionEvent.ACTION_UP) {
+          // Same as above, except now GestureDetector.onFling is called.
+          flip.cancelLongPress();
+          if (config.hardKeyboardHidden != Configuration.KEYBOARDHIDDEN_NO
+              && keyboardButton.getVisibility() == View.GONE
+              && event.getEventTime() - event.getDownTime() < CLICK_TIME
+              && Math.abs(event.getX() - lastX) < MAX_CLICK_DISTANCE
+              && Math.abs(event.getY() - lastY) < MAX_CLICK_DISTANCE) {
+            keyboardButton.startAnimation(keyboard_fade_in);
+            keyboardButton.setVisibility(View.VISIBLE);
+
+            handler.postDelayed(new Runnable() {
+              public void run() {
+                if (keyboardButton.getVisibility() == View.GONE) {
+                  return;
+                }
+
+                keyboardButton.startAnimation(keyboard_fade_out);
+                keyboardButton.setVisibility(View.GONE);
+              }
+            }, KEYBOARD_DISPLAY_TIME);
+
+            return false;
+          }
+        }
+        // pass any touch events back to detector
+        return detect.onTouchEvent(event);
+      }
+
+    });
+
+  }
+
+  private void configureOrientation() {
+    String rotateDefault;
+    if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS) {
+      rotateDefault = PreferenceConstants.ROTATION_PORTRAIT;
+    } else {
+      rotateDefault = PreferenceConstants.ROTATION_LANDSCAPE;
+    }
+
+    String rotate = prefs.getString(PreferenceConstants.ROTATION, rotateDefault);
+    if (PreferenceConstants.ROTATION_DEFAULT.equals(rotate)) {
+      rotate = rotateDefault;
+    }
+
+    // request a forced orientation if requested by user
+    if (PreferenceConstants.ROTATION_LANDSCAPE.equals(rotate)) {
+      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+      forcedOrientation = true;
+    } else if (PreferenceConstants.ROTATION_PORTRAIT.equals(rotate)) {
+      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+      forcedOrientation = true;
+    } else {
+      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+      forcedOrientation = false;
+    }
+  }
+
+  @Override
+  public boolean onCreateOptionsMenu(Menu menu) {
+    super.onCreateOptionsMenu(menu);
+    getMenuInflater().inflate(R.menu.terminal, menu);
+    menu.setQwertyMode(true);
+    return true;
+  }
+
+  @Override
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    super.onPrepareOptionsMenu(menu);
+    setVolumeControlStream(AudioManager.STREAM_NOTIFICATION);
+    TerminalBridge bridge = ((TerminalView) findCurrentView(R.id.console_flip)).bridge;
+    boolean sessionOpen = bridge.isSessionOpen();
+    menu.findItem(R.id.terminal_menu_resize).setEnabled(sessionOpen);
+    if (bridge.getProcess() instanceof ScriptProcess) {
+      menu.findItem(R.id.terminal_menu_exit_and_edit).setEnabled(true);
+    }
+    bridge.onPrepareOptionsMenu(menu);
+    return true;
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    if (item.getItemId() == R.id.terminal_menu_resize) {
+      doResize();
+    } else if (item.getItemId() == R.id.terminal_menu_preferences) {
+      doPreferences();
+    } else if (item.getItemId() == R.id.terminal_menu_send_email) {
+      doEmailTranscript();
+    } else if (item.getItemId() == R.id.terminal_menu_exit_and_edit) {
+      TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+      TerminalBridge bridge = terminalView.bridge;
+      if (manager != null) {
+        manager.closeConnection(bridge, true);
+      } else {
+        Intent intent = new Intent(this, ScriptingLayerService.class);
+        intent.setAction(Constants.ACTION_KILL_PROCESS);
+        intent.putExtra(Constants.EXTRA_PROXY_PORT, bridge.getId());
+        startService(intent);
+        Message.obtain(disconnectHandler, -1, bridge).sendToTarget();
+      }
+      Intent intent = new Intent(Constants.ACTION_EDIT_SCRIPT);
+      ScriptProcess process = (ScriptProcess) bridge.getProcess();
+      intent.putExtra(Constants.EXTRA_SCRIPT_PATH, process.getPath());
+      startActivity(intent);
+      finish();
+    }
+    return super.onOptionsItemSelected(item);
+  }
+
+  @Override
+  public void onOptionsMenuClosed(Menu menu) {
+    super.onOptionsMenuClosed(menu);
+    setVolumeControlStream(AudioManager.STREAM_MUSIC);
+  }
+
+  private void doResize() {
+    closeOptionsMenu();
+    final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+    final View resizeView = inflater.inflate(R.layout.dia_resize, null, false);
+    new AlertDialog.Builder(ConsoleActivity.this).setView(resizeView)
+        .setPositiveButton(R.string.button_resize, new DialogInterface.OnClickListener() {
+          public void onClick(DialogInterface dialog, int which) {
+            int width, height;
+            try {
+              width =
+                  Integer.parseInt(((EditText) resizeView.findViewById(R.id.width)).getText()
+                      .toString());
+              height =
+                  Integer.parseInt(((EditText) resizeView.findViewById(R.id.height)).getText()
+                      .toString());
+            } catch (NumberFormatException nfe) {
+              return;
+            }
+            terminalView.forceSize(width, height);
+          }
+        }).setNegativeButton(android.R.string.cancel, null).create().show();
+  }
+
+  private void doPreferences() {
+    startActivity(new Intent(this, Preferences.class));
+  }
+
+  private void doEmailTranscript() {
+    // Don't really want to supply an address, but currently it's required,
+    // otherwise we get an exception.
+    TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+    TerminalBridge bridge = terminalView.bridge;
+    // TODO(raaar): Replace with process log.
+    VDUBuffer buffer = bridge.getVDUBuffer();
+    int height = buffer.getRows();
+    int width = buffer.getColumns();
+    StringBuilder string = new StringBuilder();
+    for (int i = 0; i < height; i++) {
+      for (int j = 0; j < width; j++) {
+        string.append(buffer.getChar(j, i));
+      }
+    }
+    String addr = "user@example.com";
+    Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:" + addr));
+    intent.putExtra("body", string.toString().trim());
+    startActivity(intent);
+  }
+
+  @Override
+  public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+    TerminalBridge bridge = ((TerminalView) findCurrentView(R.id.console_flip)).bridge;
+    boolean sessionOpen = bridge.isSessionOpen();
+    menu.add(Menu.NONE, MenuId.COPY.getId(), Menu.NONE, R.string.terminal_menu_copy);
+    if (clipboard.hasText() && sessionOpen) {
+      menu.add(Menu.NONE, MenuId.PASTE.getId(), Menu.NONE, R.string.terminal_menu_paste);
+    }
+    bridge.onCreateContextMenu(menu, view, menuInfo);
+  }
+
+  @Override
+  public boolean onContextItemSelected(MenuItem item) {
+    int itemId = item.getItemId();
+    if (itemId == MenuId.COPY.getId()) {
+      TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+      copySource = terminalView.bridge;
+      SelectionArea area = copySource.getSelectionArea();
+      area.reset();
+      area.setBounds(copySource.getVDUBuffer().getColumns(), copySource.getVDUBuffer().getRows());
+      copySource.setSelectingForCopy(true);
+      // Make sure we show the initial selection
+      copySource.redraw();
+      Toast.makeText(ConsoleActivity.this, getString(R.string.terminal_copy_start),
+          Toast.LENGTH_LONG).show();
+      return true;
+    } else if (itemId == MenuId.PASTE.getId()) {
+      TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip);
+      TerminalBridge bridge = terminalView.bridge;
+      // pull string from clipboard and generate all events to force down
+      String clip = clipboard.getText().toString();
+      bridge.injectString(clip);
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public void onStart() {
+    super.onStart();
+    // connect with manager service to find all bridges
+    // when connected it will insert all views
+    bindService(new Intent(this, ScriptingLayerService.class), mConnection, 0);
+  }
+
+  @Override
+  public void onPause() {
+    super.onPause();
+    Log.d("onPause called");
+
+    // Allow the screen to dim and fall asleep.
+    if (wakelock != null && wakelock.isHeld()) {
+      wakelock.release();
+    }
+
+    if (forcedOrientation && manager != null) {
+      manager.setResizeAllowed(false);
+    }
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    Log.d("onResume called");
+
+    // Make sure we don't let the screen fall asleep.
+    // This also keeps the Wi-Fi chipset from disconnecting us.
+    if (wakelock != null && prefs.getBoolean(PreferenceConstants.KEEP_ALIVE, true)) {
+      wakelock.acquire();
+    }
+
+    configureOrientation();
+
+    if (forcedOrientation && manager != null) {
+      manager.setResizeAllowed(true);
+    }
+  }
+
+  /*
+   * (non-Javadoc)
+   *
+   * @see android.app.Activity#onNewIntent(android.content.Intent)
+   */
+  @Override
+  protected void onNewIntent(Intent intent) {
+    super.onNewIntent(intent);
+
+    Log.d("onNewIntent called");
+
+    int id = intent.getIntExtra(Constants.EXTRA_PROXY_PORT, -1);
+
+    if (id > 0) {
+      processID = id;
+    }
+
+    if (processID == null) {
+      Log.e("Got null intent data in onNewIntent()");
+      return;
+    }
+
+    if (manager == null) {
+      Log.e("We're not bound in onNewIntent()");
+      return;
+    }
+
+    TerminalBridge requestedBridge = manager.getConnectedBridge(processID);
+    int requestedIndex = 0;
+
+    synchronized (flip) {
+      if (requestedBridge == null) {
+        // If we didn't find the requested connection, try opening it
+
+        try {
+          Log.d(String.format("We couldnt find an existing bridge with id = %d,"
+              + "so creating one now", processID));
+          requestedBridge = manager.openConnection(processID);
+        } catch (Exception e) {
+          Log.e("Problem while trying to create new requested bridge", e);
+        }
+
+        requestedIndex = addNewTerminalView(requestedBridge);
+      } else {
+        final int flipIndex = getFlipIndex(requestedBridge);
+        if (flipIndex > requestedIndex) {
+          requestedIndex = flipIndex;
+        }
+      }
+
+      setDisplayedTerminal(requestedIndex);
+    }
+  }
+
+  @Override
+  public void onStop() {
+    super.onStop();
+    unbindService(mConnection);
+  }
+
+  protected void shiftCurrentTerminal(final int direction) {
+    View overlay;
+    synchronized (flip) {
+      boolean shouldAnimate = flip.getChildCount() > 1;
+
+      // Only show animation if there is something else to go to.
+      if (shouldAnimate) {
+        // keep current overlay from popping up again
+        overlay = findCurrentView(R.id.terminal_overlay);
+        if (overlay != null) {
+          overlay.startAnimation(fade_stay_hidden);
+        }
+
+        if (direction == SHIFT_LEFT) {
+          flip.setInAnimation(slide_left_in);
+          flip.setOutAnimation(slide_left_out);
+          flip.showNext();
+        } else if (direction == SHIFT_RIGHT) {
+          flip.setInAnimation(slide_right_in);
+          flip.setOutAnimation(slide_right_out);
+          flip.showPrevious();
+        }
+      }
+
+      if (shouldAnimate) {
+        // show overlay on new slide and start fade
+        overlay = findCurrentView(R.id.terminal_overlay);
+        if (overlay != null) {
+          overlay.startAnimation(fade_out_delayed);
+        }
+      }
+
+      updatePromptVisible();
+    }
+  }
+
+  /**
+   * Show any prompts requested by the currently visible {@link TerminalView}.
+   */
+  protected void updatePromptVisible() {
+    // check if our currently-visible terminalbridge is requesting any prompt services
+    View view = findCurrentView(R.id.console_flip);
+
+    // Hide all the prompts in case a prompt request was canceled
+    hideAllPrompts();
+
+    if (!(view instanceof TerminalView)) {
+      // we dont have an active view, so hide any prompts
+      return;
+    }
+
+    PromptHelper prompt = ((TerminalView) view).bridge.getPromptHelper();
+
+    if (Boolean.class.equals(prompt.promptRequested)) {
+      booleanPromptGroup.setVisibility(View.VISIBLE);
+      booleanPrompt.setText(prompt.promptHint);
+      booleanYes.requestFocus();
+    } else {
+      hideAllPrompts();
+      view.requestFocus();
+    }
+  }
+
+  @Override
+  public void onConfigurationChanged(Configuration newConfig) {
+    super.onConfigurationChanged(newConfig);
+
+    Log.d(String.format(
+        "onConfigurationChanged; requestedOrientation=%d, newConfig.orientation=%d",
+        getRequestedOrientation(), newConfig.orientation));
+    if (manager != null) {
+      if (forcedOrientation
+          && (newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE && getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
+          || (newConfig.orientation != Configuration.ORIENTATION_PORTRAIT && getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)) {
+        manager.setResizeAllowed(false);
+      } else {
+        manager.setResizeAllowed(true);
+      }
+
+      manager
+          .setHardKeyboardHidden(newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES);
+    }
+  }
+
+  /**
+   * Adds a new TerminalBridge to the current set of views in our ViewFlipper.
+   *
+   * @param bridge
+   *          TerminalBridge to add to our ViewFlipper
+   * @return the child index of the new view in the ViewFlipper
+   */
+  private int addNewTerminalView(TerminalBridge bridge) {
+    // let them know about our prompt handler services
+    bridge.getPromptHelper().setHandler(promptHandler);
+
+    // inflate each terminal view
+    RelativeLayout view = (RelativeLayout) inflater.inflate(R.layout.item_terminal, flip, false);
+
+    // set the terminal overlay text
+    TextView overlay = (TextView) view.findViewById(R.id.terminal_overlay);
+    overlay.setText(bridge.getName());
+
+    // and add our terminal view control, using index to place behind overlay
+    TerminalView terminal = new TerminalView(ConsoleActivity.this, bridge);
+    terminal.setId(R.id.console_flip);
+    view.addView(terminal, 0);
+
+    synchronized (flip) {
+      // finally attach to the flipper
+      flip.addView(view);
+      return flip.getChildCount() - 1;
+    }
+  }
+
+  private int getFlipIndex(TerminalBridge bridge) {
+    synchronized (flip) {
+      final int children = flip.getChildCount();
+      for (int i = 0; i < children; i++) {
+        final View view = flip.getChildAt(i).findViewById(R.id.console_flip);
+
+        if (view == null || !(view instanceof TerminalView)) {
+          // How did that happen?
+          continue;
+        }
+
+        final TerminalView tv = (TerminalView) view;
+
+        if (tv.bridge == bridge) {
+          return i;
+        }
+      }
+    }
+
+    return -1;
+  }
+
+  /**
+   * Displays the child in the ViewFlipper at the requestedIndex and updates the prompts.
+   *
+   * @param requestedIndex
+   *          the index of the terminal view to display
+   */
+  private void setDisplayedTerminal(int requestedIndex) {
+    synchronized (flip) {
+      try {
+        // show the requested bridge if found, also fade out overlay
+        flip.setDisplayedChild(requestedIndex);
+        flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_out_delayed);
+      } catch (NullPointerException npe) {
+        Log.d("View went away when we were about to display it", npe);
+      }
+      updatePromptVisible();
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/HelpActivity.java b/ScriptingLayerForAndroid/src/org/connectbot/HelpActivity.java
new file mode 100644
index 0000000..0ec485e
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/HelpActivity.java
@@ -0,0 +1,88 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.AssetManager;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.Version;
+
+import java.io.IOException;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class HelpActivity extends Activity {
+  public final static String TAG = "ConnectBot.HelpActivity";
+
+  public final static String HELPDIR = "help";
+  public final static String SUFFIX = ".html";
+
+  @Override
+  public void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+    setContentView(R.layout.act_help);
+
+    this.setTitle(String.format("%s: %s", getResources().getText(R.string.application_nice_title),
+        getResources().getText(R.string.title_help)));
+
+    TextView subtitle = (TextView) findViewById(R.id.version);
+    subtitle.setText(getText(R.string.application_title) + " r"
+        + Version.getVersion(getApplication()));
+
+    ((TextView) findViewById(R.id.help_acks_text)).setMovementMethod(LinkMovementMethod
+        .getInstance());
+
+    AssetManager am = getAssets();
+    LinearLayout content = (LinearLayout) findViewById(R.id.topics);
+
+    try {
+      for (String name : am.list(HELPDIR)) {
+        if (name.endsWith(SUFFIX)) {
+          Button button = new Button(this);
+          final String topic = name.substring(0, name.length() - SUFFIX.length());
+          button.setText(topic);
+
+          button.setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+              Intent intent = new Intent(HelpActivity.this, HelpTopicActivity.class);
+              intent.putExtra(Intent.EXTRA_TITLE, topic);
+              HelpActivity.this.startActivity(intent);
+            }
+          });
+
+          content.addView(button);
+        }
+      }
+    } catch (IOException e) {
+      // TODO Auto-generated catch block
+      Log.e("couldn't get list of help assets", e);
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/HelpTopicActivity.java b/ScriptingLayerForAndroid/src/org/connectbot/HelpTopicActivity.java
new file mode 100644
index 0000000..6453607
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/HelpTopicActivity.java
@@ -0,0 +1,49 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.googlecode.android_scripting.R;
+
+import org.connectbot.util.HelpTopicView;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class HelpTopicActivity extends Activity {
+  public final static String TAG = "ConnectBot.HelpActivity";
+
+  @Override
+  public void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+    setContentView(R.layout.act_help_topic);
+
+    String topic = getIntent().getStringExtra(Intent.EXTRA_TITLE);
+
+    this.setTitle(String.format("%s: %s - %s", getResources().getText(R.string.application_title),
+        getResources().getText(R.string.title_help), topic));
+
+    HelpTopicView helpTopic = (HelpTopicView) findViewById(R.id.topic_text);
+
+    helpTopic.setTopic(topic);
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/TerminalView.java b/ScriptingLayerForAndroid/src/org/connectbot/TerminalView.java
new file mode 100644
index 0000000..dfe231b
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/TerminalView.java
@@ -0,0 +1,271 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelXorXfermode;
+import android.graphics.RectF;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.Toast;
+import de.mud.terminal.VDUBuffer;
+
+import org.connectbot.service.FontSizeChangedListener;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalKeyListener;
+import org.connectbot.util.SelectionArea;
+
+/**
+ * User interface {@link View} for showing a TerminalBridge in an {@link Activity}. Handles drawing
+ * bitmap updates and passing keystrokes down to terminal.
+ * @author jsharkey
+ */
+public class TerminalView extends View implements FontSizeChangedListener {
+
+  private final Context context;
+  public final TerminalBridge bridge;
+  private final Paint paint;
+  private final Paint cursorPaint;
+  private final Paint cursorStrokePaint;
+
+  // Cursor paints to distinguish modes
+  private Path ctrlCursor, altCursor, shiftCursor;
+  private RectF tempSrc, tempDst;
+  private Matrix scaleMatrix;
+  private static final Matrix.ScaleToFit scaleType = Matrix.ScaleToFit.FILL;
+
+  private Toast notification = null;
+  private String lastNotification = null;
+  private volatile boolean notifications = true;
+
+  public TerminalView(Context context, TerminalBridge bridge) {
+    super(context);
+
+    this.context = context;
+    this.bridge = bridge;
+    paint = new Paint();
+
+    setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
+    setFocusable(true);
+    setFocusableInTouchMode(true);
+
+    cursorPaint = new Paint();
+    cursorPaint.setColor(bridge.getForegroundColor());
+    cursorPaint.setXfermode(new PixelXorXfermode(bridge.getBackgroundColor()));
+    cursorPaint.setAntiAlias(true);
+
+    cursorStrokePaint = new Paint(cursorPaint);
+    cursorStrokePaint.setStrokeWidth(0.1f);
+    cursorStrokePaint.setStyle(Paint.Style.STROKE);
+
+    /*
+     * Set up our cursor indicators on a 1x1 Path object which we can later transform to our
+     * character width and height
+     */
+    // TODO make this into a resource somehow
+    shiftCursor = new Path();
+    shiftCursor.lineTo(0.5f, 0.33f);
+    shiftCursor.lineTo(1.0f, 0.0f);
+
+    altCursor = new Path();
+    altCursor.moveTo(0.0f, 1.0f);
+    altCursor.lineTo(0.5f, 0.66f);
+    altCursor.lineTo(1.0f, 1.0f);
+
+    ctrlCursor = new Path();
+    ctrlCursor.moveTo(0.0f, 0.25f);
+    ctrlCursor.lineTo(1.0f, 0.5f);
+    ctrlCursor.lineTo(0.0f, 0.75f);
+
+    // For creating the transform when the terminal resizes
+    tempSrc = new RectF();
+    tempSrc.set(0.0f, 0.0f, 1.0f, 1.0f);
+    tempDst = new RectF();
+    scaleMatrix = new Matrix();
+
+    bridge.addFontSizeChangedListener(this);
+
+    // connect our view up to the bridge
+    setOnKeyListener(bridge.getKeyHandler());
+  }
+
+  public void destroy() {
+    // tell bridge to destroy its bitmap
+    bridge.parentDestroyed();
+  }
+
+  @Override
+  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+    super.onSizeChanged(w, h, oldw, oldh);
+
+    bridge.parentChanged(this);
+
+    scaleCursors();
+  }
+
+  public void onFontSizeChanged(float size) {
+    scaleCursors();
+  }
+
+  private void scaleCursors() {
+    // Create a scale matrix to scale our 1x1 representation of the cursor
+    tempDst.set(0.0f, 0.0f, bridge.charWidth, bridge.charHeight);
+    scaleMatrix.setRectToRect(tempSrc, tempDst, scaleType);
+  }
+
+  @Override
+  public void onDraw(Canvas canvas) {
+    if (bridge.getBitmap() != null) {
+      // draw the bitmap
+      bridge.onDraw();
+
+      // draw the bridge bitmap if it exists
+      canvas.drawBitmap(bridge.getBitmap(), 0, 0, paint);
+
+      VDUBuffer buffer = bridge.getVDUBuffer();
+
+      // also draw cursor if visible
+      if (buffer.isCursorVisible()) {
+
+        int cursorColumn = buffer.getCursorColumn();
+        final int cursorRow = buffer.getCursorRow();
+
+        final int columns = buffer.getColumns();
+
+        if (cursorColumn == columns) {
+          cursorColumn = columns - 1;
+        }
+
+        if (cursorColumn < 0 || cursorRow < 0) {
+          return;
+        }
+
+        int currentAttribute = buffer.getAttributes(cursorColumn, cursorRow);
+        boolean onWideCharacter = (currentAttribute & VDUBuffer.FULLWIDTH) != 0;
+
+        int x = cursorColumn * bridge.charWidth;
+        int y = (buffer.getCursorRow() + buffer.screenBase - buffer.windowBase) * bridge.charHeight;
+
+        // Save the current clip and translation
+        canvas.save();
+
+        canvas.translate(x, y);
+        canvas.clipRect(0, 0, bridge.charWidth * (onWideCharacter ? 2 : 1), bridge.charHeight);
+        canvas.drawPaint(cursorPaint);
+
+        // Make sure we scale our decorations to the correct size.
+        canvas.concat(scaleMatrix);
+
+        int metaState = bridge.getKeyHandler().getMetaState();
+
+        if ((metaState & TerminalKeyListener.META_SHIFT_ON) != 0) {
+          canvas.drawPath(shiftCursor, cursorStrokePaint);
+        } else if ((metaState & TerminalKeyListener.META_SHIFT_LOCK) != 0) {
+          canvas.drawPath(shiftCursor, cursorPaint);
+        }
+
+        if ((metaState & TerminalKeyListener.META_ALT_ON) != 0) {
+          canvas.drawPath(altCursor, cursorStrokePaint);
+        } else if ((metaState & TerminalKeyListener.META_ALT_LOCK) != 0) {
+          canvas.drawPath(altCursor, cursorPaint);
+        }
+
+        if ((metaState & TerminalKeyListener.META_CTRL_ON) != 0) {
+          canvas.drawPath(ctrlCursor, cursorStrokePaint);
+        } else if ((metaState & TerminalKeyListener.META_CTRL_LOCK) != 0) {
+          canvas.drawPath(ctrlCursor, cursorPaint);
+        }
+
+        // Restore previous clip region
+        canvas.restore();
+      }
+
+      // draw any highlighted area
+      if (bridge.isSelectingForCopy()) {
+        SelectionArea area = bridge.getSelectionArea();
+        canvas.save(Canvas.CLIP_SAVE_FLAG);
+        canvas.clipRect(area.getLeft() * bridge.charWidth, area.getTop() * bridge.charHeight, (area
+            .getRight() + 1)
+            * bridge.charWidth, (area.getBottom() + 1) * bridge.charHeight);
+        canvas.drawPaint(cursorPaint);
+        canvas.restore();
+      }
+    }
+  }
+
+  public void notifyUser(String message) {
+    if (!notifications) {
+      return;
+    }
+
+    if (notification != null) {
+      // Don't keep telling the user the same thing.
+      if (lastNotification != null && lastNotification.equals(message)) {
+        return;
+      }
+
+      notification.setText(message);
+      notification.show();
+    } else {
+      notification = Toast.makeText(context, message, Toast.LENGTH_SHORT);
+      notification.show();
+    }
+
+    lastNotification = message;
+  }
+
+  /**
+   * Ask the {@link TerminalBridge} we're connected to to resize to a specific size.
+   * @param width
+   * @param height
+   */
+  public void forceSize(int width, int height) {
+    bridge.resizeComputed(width, height, getWidth(), getHeight());
+  }
+
+  /**
+   * Sets the ability for the TerminalView to display Toast notifications to the user.
+   * @param value
+   *          whether to enable notifications or not
+   */
+  public void setNotifications(boolean value) {
+    notifications = value;
+  }
+
+  @Override
+  public boolean onCheckIsTextEditor() {
+    return true;
+  }
+
+  @Override
+  public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+    outAttrs.imeOptions |=
+        EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_ENTER_ACTION
+            | EditorInfo.IME_ACTION_NONE;
+    outAttrs.inputType = EditorInfo.TYPE_NULL;
+    return new BaseInputConnection(this, false);
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/service/FontSizeChangedListener.java b/ScriptingLayerForAndroid/src/org/connectbot/service/FontSizeChangedListener.java
new file mode 100644
index 0000000..eb1c33d
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/service/FontSizeChangedListener.java
@@ -0,0 +1,31 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.service;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public interface FontSizeChangedListener {
+
+	/**
+	 * @param size
+	 *            new font size
+	 */
+	void onFontSizeChanged(float size);
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/service/PromptHelper.java b/ScriptingLayerForAndroid/src/org/connectbot/service/PromptHelper.java
new file mode 100644
index 0000000..f0a37be
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/service/PromptHelper.java
@@ -0,0 +1,159 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.service;
+
+import java.util.concurrent.Semaphore;
+
+import android.os.Handler;
+import android.os.Message;
+
+/**
+ * Helps provide a relay for prompts and responses between a possible user
+ * interface and some underlying service.
+ *
+ * @author jsharkey
+ */
+public class PromptHelper {
+	private final Object tag;
+
+	private Handler handler = null;
+
+	private Semaphore promptToken;
+	private Semaphore promptResponse;
+
+	public String promptInstructions = null;
+	public String promptHint = null;
+	public Object promptRequested = null;
+
+	private Object response = null;
+
+	public PromptHelper(Object tag) {
+		this.tag = tag;
+
+		// Threads must acquire this before they can send a prompt.
+		promptToken = new Semaphore(1);
+
+		// Responses will release this semaphore.
+		promptResponse = new Semaphore(0);
+	}
+
+
+	/**
+	 * Register a user interface handler, if available.
+	 */
+	public void setHandler(Handler handler) {
+		this.handler = handler;
+	}
+
+	/**
+	 * Set an incoming value from an above user interface. Will automatically
+	 * notify any waiting requests.
+	 */
+	public void setResponse(Object value) {
+		response = value;
+		promptRequested = null;
+		promptInstructions = null;
+		promptHint = null;
+		promptResponse.release();
+	}
+
+	/**
+	 * Return the internal response value just before erasing and returning it.
+	 */
+	protected Object popResponse() {
+		Object value = response;
+		response = null;
+		return value;
+	}
+
+
+	/**
+	 * Request a prompt response from parent. This is a blocking call until user
+	 * interface returns a value.
+	 * Only one thread can call this at a time. cancelPrompt() will force this to
+	 * immediately return.
+	 */
+	private Object requestPrompt(String instructions, String hint, Object type) throws InterruptedException {
+		Object response = null;
+
+		promptToken.acquire();
+
+		try {
+			promptInstructions = instructions;
+			promptHint = hint;
+			promptRequested = type;
+
+			// notify any parent watching for live events
+			if (handler != null)
+				Message.obtain(handler, -1, tag).sendToTarget();
+
+			// acquire lock until user passes back value
+			promptResponse.acquire();
+
+			response = popResponse();
+		} finally {
+			promptToken.release();
+		}
+
+		return response;
+	}
+
+	/**
+	 * Request a string response from parent. This is a blocking call until user
+	 * interface returns a value.
+	 * @param hint prompt hint for user to answer
+	 * @return string user has entered
+	 */
+	public String requestStringPrompt(String instructions, String hint) {
+		String value = null;
+		try {
+			value = (String)this.requestPrompt(instructions, hint, String.class);
+		} catch(Exception e) {
+		}
+		return value;
+	}
+
+	/**
+	 * Request a boolean response from parent. This is a blocking call until user
+	 * interface returns a value.
+	 * @param hint prompt hint for user to answer
+	 * @return choice user has made (yes/no)
+	 */
+	public Boolean requestBooleanPrompt(String instructions, String hint) {
+		Boolean value = null;
+		try {
+			value = (Boolean)this.requestPrompt(instructions, hint, Boolean.class);
+		} catch(Exception e) {
+		}
+		return value;
+	}
+
+	/**
+	 * Cancel an in-progress prompt.
+	 */
+	public void cancelPrompt() {
+		if (!promptToken.tryAcquire()) {
+			// A thread has the token, so try to interrupt it
+			response = null;
+			promptResponse.release();
+		} else {
+			// No threads have acquired the token
+			promptToken.release();
+		}
+	}
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/service/Relay.java b/ScriptingLayerForAndroid/src/org/connectbot/service/Relay.java
new file mode 100644
index 0000000..0925321
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/service/Relay.java
@@ -0,0 +1,160 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.service;
+
+import com.googlecode.android_scripting.Log;
+
+import de.mud.terminal.vt320;
+
+import org.apache.harmony.niochar.charset.additional.IBM437;
+import org.connectbot.transport.AbsTransport;
+import org.connectbot.util.EastAsianWidth;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+
+/**
+ * @author Kenny Root
+ */
+public class Relay implements Runnable {
+
+  private static final int BUFFER_SIZE = 4096;
+
+  private static boolean useJNI = true;
+
+  private TerminalBridge bridge;
+
+  private Charset currentCharset;
+  private CharsetDecoder decoder;
+  private boolean isLegacyEastAsian = false;
+
+  private AbsTransport transport;
+
+  private vt320 buffer;
+
+  private ByteBuffer byteBuffer;
+  private CharBuffer charBuffer;
+
+  private byte[] byteArray;
+  private char[] charArray;
+
+  static {
+    useJNI = EastAsianWidth.useJNI;
+  }
+
+  public Relay(TerminalBridge bridge, AbsTransport transport, vt320 buffer, String encoding) {
+    setCharset(encoding);
+    this.bridge = bridge;
+    this.transport = transport;
+    this.buffer = buffer;
+  }
+
+  public void setCharset(String encoding) {
+    Log.d("changing charset to " + encoding);
+    Charset charset;
+    if (encoding.equals("CP437")) {
+      charset = new IBM437("IBM437", new String[] { "IBM437", "CP437" });
+    } else {
+      charset = Charset.forName(encoding);
+    }
+
+    if (charset == currentCharset || charset == null) {
+      return;
+    }
+
+    CharsetDecoder newCd = charset.newDecoder();
+    newCd.onUnmappableCharacter(CodingErrorAction.REPLACE);
+    newCd.onMalformedInput(CodingErrorAction.REPLACE);
+
+    currentCharset = charset;
+    synchronized (this) {
+      decoder = newCd;
+    }
+  }
+
+  public void run() {
+    byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
+    charBuffer = CharBuffer.allocate(BUFFER_SIZE);
+
+    /* for both JNI and non-JNI method */
+    byte[] wideAttribute = new byte[BUFFER_SIZE];
+
+    /* non-JNI fallback method */
+    float[] widths = null;
+
+    if (!useJNI) {
+      widths = new float[BUFFER_SIZE];
+    }
+
+    byteArray = byteBuffer.array();
+    charArray = charBuffer.array();
+
+    CoderResult result;
+
+    int bytesRead = 0;
+    byteBuffer.limit(0);
+    int bytesToRead;
+    int offset;
+    int charWidth;
+
+    try {
+      while (true) {
+        charWidth = bridge.charWidth;
+        bytesToRead = byteBuffer.capacity() - byteBuffer.limit();
+        offset = byteBuffer.arrayOffset() + byteBuffer.limit();
+        bytesRead = transport.read(byteArray, offset, bytesToRead);
+
+        if (bytesRead > 0) {
+          byteBuffer.limit(byteBuffer.limit() + bytesRead);
+
+          synchronized (this) {
+            result = decoder.decode(byteBuffer, charBuffer, false);
+          }
+
+          if (result.isUnderflow() && byteBuffer.limit() == byteBuffer.capacity()) {
+            byteBuffer.compact();
+            byteBuffer.limit(byteBuffer.position());
+            byteBuffer.position(0);
+          }
+
+          offset = charBuffer.position();
+
+          if (!useJNI) {
+            bridge.getPaint().getTextWidths(charArray, 0, offset, widths);
+            for (int i = 0; i < offset; i++) {
+              wideAttribute[i] = (byte) (((int) widths[i] != charWidth) ? 1 : 0);
+            }
+          } else {
+            EastAsianWidth.measure(charArray, 0, charBuffer.position(), wideAttribute,
+                isLegacyEastAsian);
+          }
+          buffer.putString(charArray, wideAttribute, 0, charBuffer.position());
+          charBuffer.clear();
+          bridge.redraw();
+        }
+      }
+    } catch (IOException e) {
+      Log.e("Problem while handling incoming data in relay thread", e);
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/service/TerminalBridge.java b/ScriptingLayerForAndroid/src/org/connectbot/service/TerminalBridge.java
new file mode 100644
index 0000000..9a5936b
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/service/TerminalBridge.java
@@ -0,0 +1,889 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.service;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.Bitmap.Config;
+import android.graphics.Paint.FontMetrics;
+import android.text.ClipboardManager;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.facade.ui.UiFacade;
+import com.googlecode.android_scripting.interpreter.InterpreterProcess;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
+import com.googlecode.android_scripting.jsonrpc.RpcReceiverManagerFactory;
+
+import de.mud.terminal.VDUBuffer;
+import de.mud.terminal.VDUDisplay;
+import de.mud.terminal.vt320;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.connectbot.TerminalView;
+import org.connectbot.transport.AbsTransport;
+import org.connectbot.util.Colors;
+import org.connectbot.util.PreferenceConstants;
+import org.connectbot.util.SelectionArea;
+
+/**
+ * Provides a bridge between a MUD terminal buffer and a possible TerminalView. This separation
+ * allows us to keep the TerminalBridge running in a background service. A TerminalView shares down
+ * a bitmap that we can use for rendering when available.
+ *
+ * @author ConnectBot Dev Team
+ * @author raaar
+ *
+ */
+public class TerminalBridge implements VDUDisplay, OnSharedPreferenceChangeListener {
+
+  private final static int FONT_SIZE_STEP = 2;
+
+  private final int[] color = new int[Colors.defaults.length];
+
+  private final TerminalManager manager;
+
+  private final InterpreterProcess mProcess;
+
+  private int mDefaultFgColor;
+  private int mDefaultBgColor;
+
+  private int scrollback;
+
+  private String delKey;
+  private String encoding;
+
+  private AbsTransport transport;
+
+  private final Paint defaultPaint;
+
+  private Relay relay;
+
+  private Bitmap bitmap = null;
+  private final VDUBuffer buffer;
+
+  private TerminalView parent = null;
+  private final Canvas canvas = new Canvas();
+
+  private boolean forcedSize = false;
+  private int columns;
+  private int rows;
+
+  private final TerminalKeyListener keyListener;
+
+  private boolean selectingForCopy = false;
+  private final SelectionArea selectionArea;
+  private ClipboardManager clipboard;
+
+  public int charWidth = -1;
+  public int charHeight = -1;
+  private int charTop = -1;
+
+  private float fontSize = -1;
+
+  private final List<FontSizeChangedListener> fontSizeChangedListeners;
+
+  /**
+   * Flag indicating if we should perform a full-screen redraw during our next rendering pass.
+   */
+  private boolean fullRedraw = false;
+
+  private final PromptHelper promptHelper;
+
+  /**
+   * Create a new terminal bridge suitable for unit testing.
+   */
+  public TerminalBridge() {
+    buffer = new vt320() {
+      @Override
+      public void write(byte[] b) {
+      }
+
+      @Override
+      public void write(int b) {
+      }
+
+      @Override
+      public void sendTelnetCommand(byte cmd) {
+      }
+
+      @Override
+      public void setWindowSize(int c, int r) {
+      }
+
+      @Override
+      public void debug(String s) {
+      }
+    };
+
+    manager = null;
+
+    defaultPaint = new Paint();
+
+    selectionArea = new SelectionArea();
+    scrollback = 1;
+
+    fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();
+
+    transport = null;
+
+    keyListener = new TerminalKeyListener(manager, this, buffer, null);
+
+    mProcess = null;
+
+    mDefaultFgColor = 0;
+    mDefaultBgColor = 0;
+    promptHelper = null;
+
+    updateCharset();
+  }
+
+  /**
+   * Create new terminal bridge with following parameters.
+   */
+  public TerminalBridge(final TerminalManager manager, InterpreterProcess process, AbsTransport t)
+      throws IOException {
+    this.manager = manager;
+    transport = t;
+    mProcess = process;
+
+    String string = manager.getStringParameter(PreferenceConstants.SCROLLBACK, null);
+    if (string != null) {
+      scrollback = Integer.parseInt(string);
+    } else {
+      scrollback = PreferenceConstants.DEFAULT_SCROLLBACK;
+    }
+
+    string = manager.getStringParameter(PreferenceConstants.FONTSIZE, null);
+    if (string != null) {
+      fontSize = Float.parseFloat(string);
+    } else {
+      fontSize = PreferenceConstants.DEFAULT_FONT_SIZE;
+    }
+
+    mDefaultFgColor =
+        manager.getIntParameter(PreferenceConstants.COLOR_FG, PreferenceConstants.DEFAULT_FG_COLOR);
+    mDefaultBgColor =
+        manager.getIntParameter(PreferenceConstants.COLOR_BG, PreferenceConstants.DEFAULT_BG_COLOR);
+
+    delKey = manager.getStringParameter(PreferenceConstants.DELKEY, PreferenceConstants.DELKEY_DEL);
+
+    // create prompt helper to relay password and hostkey requests up to gui
+    promptHelper = new PromptHelper(this);
+
+    // create our default paint
+    defaultPaint = new Paint();
+    defaultPaint.setAntiAlias(true);
+    defaultPaint.setTypeface(Typeface.MONOSPACE);
+    defaultPaint.setFakeBoldText(true); // more readable?
+
+    fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();
+
+    setFontSize(fontSize);
+
+    // create terminal buffer and handle outgoing data
+    // this is probably status reply information
+    buffer = new vt320() {
+      @Override
+      public void debug(String s) {
+        Log.d(s);
+      }
+
+      @Override
+      public void write(byte[] b) {
+        try {
+          if (b != null && transport != null) {
+            transport.write(b);
+          }
+        } catch (IOException e) {
+          Log.e("Problem writing outgoing data in vt320() thread", e);
+        }
+      }
+
+      @Override
+      public void write(int b) {
+        try {
+          if (transport != null) {
+            transport.write(b);
+          }
+        } catch (IOException e) {
+          Log.e("Problem writing outgoing data in vt320() thread", e);
+        }
+      }
+
+      // We don't use telnet sequences.
+      @Override
+      public void sendTelnetCommand(byte cmd) {
+      }
+
+      // We don't want remote to resize our window.
+      @Override
+      public void setWindowSize(int c, int r) {
+      }
+
+      @Override
+      public void beep() {
+        if (parent.isShown()) {
+          manager.playBeep();
+        }
+      }
+    };
+
+    // Don't keep any scrollback if a session is not being opened.
+
+    buffer.setBufferSize(scrollback);
+
+    resetColors();
+    buffer.setDisplay(this);
+
+    selectionArea = new SelectionArea();
+
+    keyListener = new TerminalKeyListener(manager, this, buffer, encoding);
+
+    updateCharset();
+
+    manager.registerOnSharedPreferenceChangeListener(this);
+
+  }
+
+  /**
+   * Spawn thread to open connection and start login process.
+   */
+  protected void connect() {
+    transport.setBridge(this);
+    transport.setManager(manager);
+    transport.connect();
+
+    ((vt320) buffer).reset();
+
+    // previously tried vt100 and xterm for emulation modes
+    // "screen" works the best for color and escape codes
+    ((vt320) buffer).setAnswerBack("screen");
+
+    if (PreferenceConstants.DELKEY_BACKSPACE.equals(delKey)) {
+      ((vt320) buffer).setBackspace(vt320.DELETE_IS_BACKSPACE);
+    } else {
+      ((vt320) buffer).setBackspace(vt320.DELETE_IS_DEL);
+    }
+
+    // create thread to relay incoming connection data to buffer
+    relay = new Relay(this, transport, (vt320) buffer, encoding);
+    Thread relayThread = new Thread(relay);
+    relayThread.setDaemon(true);
+    relayThread.setName("Relay");
+    relayThread.start();
+
+    // force font-size to make sure we resizePTY as needed
+    setFontSize(fontSize);
+
+  }
+
+  private void updateCharset() {
+    encoding =
+        manager.getStringParameter(PreferenceConstants.ENCODING, Charset.defaultCharset().name());
+    if (relay != null) {
+      relay.setCharset(encoding);
+    }
+    keyListener.setCharset(encoding);
+  }
+
+  /**
+   * Inject a specific string into this terminal. Used for post-login strings and pasting clipboard.
+   */
+  public void injectString(final String string) {
+    if (string == null || string.length() == 0) {
+      return;
+    }
+
+    Thread injectStringThread = new Thread(new Runnable() {
+      public void run() {
+        try {
+          transport.write(string.getBytes(encoding));
+        } catch (Exception e) {
+          Log.e("Couldn't inject string to remote host: ", e);
+        }
+      }
+    });
+    injectStringThread.setName("InjectString");
+    injectStringThread.start();
+  }
+
+  /**
+   * @return whether a session is open or not
+   */
+  public boolean isSessionOpen() {
+    if (transport != null) {
+      return transport.isSessionOpen();
+    }
+    return false;
+  }
+
+  /**
+   * Force disconnection of this terminal bridge.
+   */
+  public void dispatchDisconnect(boolean immediate) {
+
+    // Cancel any pending prompts.
+    promptHelper.cancelPrompt();
+
+    if (immediate) {
+      manager.closeConnection(TerminalBridge.this, true);
+    } else {
+      Thread disconnectPromptThread = new Thread(new Runnable() {
+        public void run() {
+          String prompt = null;
+          if (transport != null && transport.isConnected()) {
+            prompt = manager.getResources().getString(R.string.prompt_confirm_exit);
+          } else {
+            prompt = manager.getResources().getString(R.string.prompt_process_exited);
+          }
+          Boolean result = promptHelper.requestBooleanPrompt(null, prompt);
+
+          if (transport != null && transport.isConnected()) {
+            manager.closeConnection(TerminalBridge.this, result != null && result.booleanValue());
+          } else if (result != null && result.booleanValue()) {
+            manager.closeConnection(TerminalBridge.this, false);
+          }
+        }
+      });
+      disconnectPromptThread.setName("DisconnectPrompt");
+      disconnectPromptThread.setDaemon(true);
+      disconnectPromptThread.start();
+    }
+  }
+
+  public void setSelectingForCopy(boolean selectingForCopy) {
+    this.selectingForCopy = selectingForCopy;
+  }
+
+  public boolean isSelectingForCopy() {
+    return selectingForCopy;
+  }
+
+  public SelectionArea getSelectionArea() {
+    return selectionArea;
+  }
+
+  public synchronized void tryKeyVibrate() {
+    manager.tryKeyVibrate();
+  }
+
+  /**
+   * Request a different font size. Will make call to parentChanged() to make sure we resize PTY if
+   * needed.
+   */
+  /* package */final void setFontSize(float size) {
+    if (size <= 0.0) {
+      return;
+    }
+
+    defaultPaint.setTextSize(size);
+    fontSize = size;
+
+    // read new metrics to get exact pixel dimensions
+    FontMetrics fm = defaultPaint.getFontMetrics();
+    charTop = (int) Math.ceil(fm.top);
+
+    float[] widths = new float[1];
+    defaultPaint.getTextWidths("X", widths);
+    charWidth = (int) Math.ceil(widths[0]);
+    charHeight = (int) Math.ceil(fm.descent - fm.top);
+
+    // refresh any bitmap with new font size
+    if (parent != null) {
+      parentChanged(parent);
+    }
+
+    for (FontSizeChangedListener ofscl : fontSizeChangedListeners) {
+      ofscl.onFontSizeChanged(size);
+    }
+    forcedSize = false;
+  }
+
+  /**
+   * Add an {@link FontSizeChangedListener} to the list of listeners for this bridge.
+   *
+   * @param listener
+   *          listener to add
+   */
+  public void addFontSizeChangedListener(FontSizeChangedListener listener) {
+    fontSizeChangedListeners.add(listener);
+  }
+
+  /**
+   * Remove an {@link FontSizeChangedListener} from the list of listeners for this bridge.
+   *
+   * @param listener
+   */
+  public void removeFontSizeChangedListener(FontSizeChangedListener listener) {
+    fontSizeChangedListeners.remove(listener);
+  }
+
+  /**
+   * Something changed in our parent {@link TerminalView}, maybe it's a new parent, or maybe it's an
+   * updated font size. We should recalculate terminal size information and request a PTY resize.
+   */
+  public final synchronized void parentChanged(TerminalView parent) {
+    if (manager != null && !manager.isResizeAllowed()) {
+      Log.d("Resize is not allowed now");
+      return;
+    }
+
+    this.parent = parent;
+    final int width = parent.getWidth();
+    final int height = parent.getHeight();
+
+    // Something has gone wrong with our layout; we're 0 width or height!
+    if (width <= 0 || height <= 0) {
+      return;
+    }
+
+    clipboard = (ClipboardManager) parent.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+    keyListener.setClipboardManager(clipboard);
+
+    if (!forcedSize) {
+      // recalculate buffer size
+      int newColumns, newRows;
+
+      newColumns = width / charWidth;
+      newRows = height / charHeight;
+
+      // If nothing has changed in the terminal dimensions and not an intial
+      // draw then don't blow away scroll regions and such.
+      if (newColumns == columns && newRows == rows) {
+        return;
+      }
+
+      columns = newColumns;
+      rows = newRows;
+    }
+
+    // reallocate new bitmap if needed
+    boolean newBitmap = (bitmap == null);
+    if (bitmap != null) {
+      newBitmap = (bitmap.getWidth() != width || bitmap.getHeight() != height);
+    }
+
+    if (newBitmap) {
+      discardBitmap();
+      bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
+      canvas.setBitmap(bitmap);
+    }
+
+    // clear out any old buffer information
+    defaultPaint.setColor(Color.BLACK);
+    canvas.drawPaint(defaultPaint);
+
+    // Stroke the border of the terminal if the size is being forced;
+    if (forcedSize) {
+      int borderX = (columns * charWidth) + 1;
+      int borderY = (rows * charHeight) + 1;
+
+      defaultPaint.setColor(Color.GRAY);
+      defaultPaint.setStrokeWidth(0.0f);
+      if (width >= borderX) {
+        canvas.drawLine(borderX, 0, borderX, borderY + 1, defaultPaint);
+      }
+      if (height >= borderY) {
+        canvas.drawLine(0, borderY, borderX + 1, borderY, defaultPaint);
+      }
+    }
+
+    try {
+      // request a terminal pty resize
+      synchronized (buffer) {
+        buffer.setScreenSize(columns, rows, true);
+      }
+
+      if (transport != null) {
+        transport.setDimensions(columns, rows, width, height);
+      }
+    } catch (Exception e) {
+      Log.e("Problem while trying to resize screen or PTY", e);
+    }
+
+    // force full redraw with new buffer size
+    fullRedraw = true;
+    redraw();
+
+    parent.notifyUser(String.format("%d x %d", columns, rows));
+
+    Log.i(String.format("parentChanged() now width=%d, height=%d", columns, rows));
+  }
+
+  /**
+   * Somehow our parent {@link TerminalView} was destroyed. Now we don't need to redraw anywhere,
+   * and we can recycle our internal bitmap.
+   */
+  public synchronized void parentDestroyed() {
+    parent = null;
+    discardBitmap();
+  }
+
+  private void discardBitmap() {
+    if (bitmap != null) {
+      bitmap.recycle();
+    }
+    bitmap = null;
+  }
+
+  public void onDraw() {
+    int fg, bg;
+    synchronized (buffer) {
+      boolean entireDirty = buffer.update[0] || fullRedraw;
+      boolean isWideCharacter = false;
+
+      // walk through all lines in the buffer
+      for (int l = 0; l < buffer.height; l++) {
+
+        // check if this line is dirty and needs to be repainted
+        // also check for entire-buffer dirty flags
+        if (!entireDirty && !buffer.update[l + 1]) {
+          continue;
+        }
+
+        // reset dirty flag for this line
+        buffer.update[l + 1] = false;
+
+        // walk through all characters in this line
+        for (int c = 0; c < buffer.width; c++) {
+          int addr = 0;
+          int currAttr = buffer.charAttributes[buffer.windowBase + l][c];
+          // check if foreground color attribute is set
+          if ((currAttr & VDUBuffer.COLOR_FG) != 0) {
+            int fgcolor = ((currAttr & VDUBuffer.COLOR_FG) >> VDUBuffer.COLOR_FG_SHIFT) - 1;
+            if (fgcolor < 8 && (currAttr & VDUBuffer.BOLD) != 0) {
+              fg = color[fgcolor + 8];
+            } else {
+              fg = color[fgcolor];
+            }
+          } else {
+            fg = mDefaultFgColor;
+          }
+
+          // check if background color attribute is set
+          if ((currAttr & VDUBuffer.COLOR_BG) != 0) {
+            bg = color[((currAttr & VDUBuffer.COLOR_BG) >> VDUBuffer.COLOR_BG_SHIFT) - 1];
+          } else {
+            bg = mDefaultBgColor;
+          }
+
+          // support character inversion by swapping background and foreground color
+          if ((currAttr & VDUBuffer.INVERT) != 0) {
+            int swapc = bg;
+            bg = fg;
+            fg = swapc;
+          }
+
+          // set underlined attributes if requested
+          defaultPaint.setUnderlineText((currAttr & VDUBuffer.UNDERLINE) != 0);
+
+          isWideCharacter = (currAttr & VDUBuffer.FULLWIDTH) != 0;
+
+          if (isWideCharacter) {
+            addr++;
+          } else {
+            // determine the amount of continuous characters with the same settings and print them
+            // all at once
+            while (c + addr < buffer.width
+                && buffer.charAttributes[buffer.windowBase + l][c + addr] == currAttr) {
+              addr++;
+            }
+          }
+
+          // Save the current clip region
+          canvas.save(Canvas.CLIP_SAVE_FLAG);
+
+          // clear this dirty area with background color
+          defaultPaint.setColor(bg);
+          if (isWideCharacter) {
+            canvas.clipRect(c * charWidth, l * charHeight, (c + 2) * charWidth, (l + 1)
+                * charHeight);
+          } else {
+            canvas.clipRect(c * charWidth, l * charHeight, (c + addr) * charWidth, (l + 1)
+                * charHeight);
+          }
+          canvas.drawPaint(defaultPaint);
+
+          // write the text string starting at 'c' for 'addr' number of characters
+          defaultPaint.setColor(fg);
+          if ((currAttr & VDUBuffer.INVISIBLE) == 0) {
+            canvas.drawText(buffer.charArray[buffer.windowBase + l], c, addr, c * charWidth,
+                (l * charHeight) - charTop, defaultPaint);
+          }
+
+          // Restore the previous clip region
+          canvas.restore();
+
+          // advance to the next text block with different characteristics
+          c += addr - 1;
+          if (isWideCharacter) {
+            c++;
+          }
+        }
+      }
+
+      // reset entire-buffer flags
+      buffer.update[0] = false;
+    }
+    fullRedraw = false;
+  }
+
+  public void redraw() {
+    if (parent != null) {
+      parent.postInvalidate();
+    }
+  }
+
+  // We don't have a scroll bar.
+  public void updateScrollBar() {
+  }
+
+  /**
+   * Resize terminal to fit [rows]x[cols] in screen of size [width]x[height]
+   *
+   * @param rows
+   * @param cols
+   * @param width
+   * @param height
+   */
+  public synchronized void resizeComputed(int cols, int rows, int width, int height) {
+    float size = 8.0f;
+    float step = 8.0f;
+    float limit = 0.125f;
+
+    int direction;
+
+    while ((direction = fontSizeCompare(size, cols, rows, width, height)) < 0) {
+      size += step;
+    }
+
+    if (direction == 0) {
+      Log.d(String.format("Fontsize: found match at %f", size));
+      return;
+    }
+
+    step /= 2.0f;
+    size -= step;
+
+    while ((direction = fontSizeCompare(size, cols, rows, width, height)) != 0 && step >= limit) {
+      step /= 2.0f;
+      if (direction > 0) {
+        size -= step;
+      } else {
+        size += step;
+      }
+    }
+
+    if (direction > 0) {
+      size -= step;
+    }
+
+    columns = cols;
+    this.rows = rows;
+    setFontSize(size);
+    forcedSize = true;
+  }
+
+  private int fontSizeCompare(float size, int cols, int rows, int width, int height) {
+    // read new metrics to get exact pixel dimensions
+    defaultPaint.setTextSize(size);
+    FontMetrics fm = defaultPaint.getFontMetrics();
+
+    float[] widths = new float[1];
+    defaultPaint.getTextWidths("X", widths);
+    int termWidth = (int) widths[0] * cols;
+    int termHeight = (int) Math.ceil(fm.descent - fm.top) * rows;
+
+    Log.d(String.format("Fontsize: font size %f resulted in %d x %d", size, termWidth, termHeight));
+
+    // Check to see if it fits in resolution specified.
+    if (termWidth > width || termHeight > height) {
+      return 1;
+    }
+
+    if (termWidth == width || termHeight == height) {
+      return 0;
+    }
+
+    return -1;
+  }
+
+  /*
+   * (non-Javadoc)
+   *
+   * @see de.mud.terminal.VDUDisplay#setVDUBuffer(de.mud.terminal.VDUBuffer)
+   */
+  @Override
+  public void setVDUBuffer(VDUBuffer buffer) {
+  }
+
+  /*
+   * (non-Javadoc)
+   *
+   * @see de.mud.terminal.VDUDisplay#setColor(byte, byte, byte, byte)
+   */
+  public void setColor(int index, int red, int green, int blue) {
+    // Don't allow the system colors to be overwritten for now. May violate specs.
+    if (index < color.length && index >= 16) {
+      color[index] = 0xff000000 | red << 16 | green << 8 | blue;
+    }
+  }
+
+  public final void resetColors() {
+    System.arraycopy(Colors.defaults, 0, color, 0, Colors.defaults.length);
+  }
+
+  public TerminalKeyListener getKeyHandler() {
+    return keyListener;
+  }
+
+  public void resetScrollPosition() {
+    // if we're in scrollback, scroll to bottom of window on input
+    if (buffer.windowBase != buffer.screenBase) {
+      buffer.setWindowBase(buffer.screenBase);
+    }
+  }
+
+  public void increaseFontSize() {
+    setFontSize(fontSize + FONT_SIZE_STEP);
+  }
+
+  public void decreaseFontSize() {
+    setFontSize(fontSize - FONT_SIZE_STEP);
+  }
+
+  public int getId() {
+    return mProcess.getPort();
+  }
+
+  public String getName() {
+    return mProcess.getName();
+  }
+
+  public InterpreterProcess getProcess() {
+    return mProcess;
+  }
+
+  public int getForegroundColor() {
+    return mDefaultFgColor;
+  }
+
+  public int getBackgroundColor() {
+    return mDefaultBgColor;
+  }
+
+  public VDUBuffer getVDUBuffer() {
+    return buffer;
+  }
+
+  public PromptHelper getPromptHelper() {
+    return promptHelper;
+  }
+
+  public Bitmap getBitmap() {
+    return bitmap;
+  }
+
+  public AbsTransport getTransport() {
+    return transport;
+  }
+
+  public Paint getPaint() {
+    return defaultPaint;
+  }
+
+  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+    if (mProcess.isAlive()) {
+      RpcReceiverManagerFactory rpcReceiverManagerFactory = mProcess.getRpcReceiverManagerFactory();
+      for (RpcReceiverManager manager : rpcReceiverManagerFactory.getRpcReceiverManagers().values()) {
+        UiFacade facade = manager.getReceiver(UiFacade.class);
+        facade.onCreateContextMenu(menu, v, menuInfo);
+      }
+    }
+  }
+
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    boolean returnValue = false;
+    if (mProcess.isAlive()) {
+      RpcReceiverManagerFactory rpcReceiverManagerFactory = mProcess.getRpcReceiverManagerFactory();
+      for (RpcReceiverManager manager : rpcReceiverManagerFactory.getRpcReceiverManagers().values()) {
+        UiFacade facade = manager.getReceiver(UiFacade.class);
+        returnValue = returnValue || facade.onPrepareOptionsMenu(menu);
+      }
+      return returnValue;
+    }
+    return false;
+  }
+
+  public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+    if (PreferenceConstants.ENCODING.equals(key)) {
+      updateCharset();
+    } else if (PreferenceConstants.FONTSIZE.equals(key)) {
+      String string = manager.getStringParameter(PreferenceConstants.FONTSIZE, null);
+      if (string != null) {
+        fontSize = Float.parseFloat(string);
+      } else {
+        fontSize = PreferenceConstants.DEFAULT_FONT_SIZE;
+      }
+      setFontSize(fontSize);
+      fullRedraw = true;
+    } else if (PreferenceConstants.SCROLLBACK.equals(key)) {
+      String string = manager.getStringParameter(PreferenceConstants.SCROLLBACK, null);
+      if (string != null) {
+        scrollback = Integer.parseInt(string);
+      } else {
+        scrollback = PreferenceConstants.DEFAULT_SCROLLBACK;
+      }
+      buffer.setBufferSize(scrollback);
+    } else if (PreferenceConstants.COLOR_FG.equals(key)) {
+      mDefaultFgColor =
+          manager.getIntParameter(PreferenceConstants.COLOR_FG,
+              PreferenceConstants.DEFAULT_FG_COLOR);
+      fullRedraw = true;
+    } else if (PreferenceConstants.COLOR_BG.equals(key)) {
+      mDefaultBgColor =
+          manager.getIntParameter(PreferenceConstants.COLOR_BG,
+              PreferenceConstants.DEFAULT_BG_COLOR);
+      fullRedraw = true;
+    }
+    if (PreferenceConstants.DELKEY.equals(key)) {
+      delKey =
+          manager.getStringParameter(PreferenceConstants.DELKEY, PreferenceConstants.DELKEY_DEL);
+      if (PreferenceConstants.DELKEY_BACKSPACE.equals(delKey)) {
+        ((vt320) buffer).setBackspace(vt320.DELETE_IS_BACKSPACE);
+      } else {
+        ((vt320) buffer).setBackspace(vt320.DELETE_IS_DEL);
+      }
+    }
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/service/TerminalKeyListener.java b/ScriptingLayerForAndroid/src/org/connectbot/service/TerminalKeyListener.java
new file mode 100644
index 0000000..9fce1e8
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/service/TerminalKeyListener.java
@@ -0,0 +1,510 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2010 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.service;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.Configuration;
+import android.text.ClipboardManager;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnKeyListener;
+
+import com.googlecode.android_scripting.Log;
+
+import de.mud.terminal.VDUBuffer;
+import de.mud.terminal.vt320;
+
+import java.io.IOException;
+
+import org.connectbot.TerminalView;
+import org.connectbot.transport.AbsTransport;
+import org.connectbot.util.PreferenceConstants;
+import org.connectbot.util.SelectionArea;
+
+/**
+ * @author kenny
+ * @author modified by raaar
+ */
+public class TerminalKeyListener implements OnKeyListener, OnSharedPreferenceChangeListener {
+
+  public final static int META_CTRL_ON = 0x01;
+  public final static int META_CTRL_LOCK = 0x02;
+  public final static int META_ALT_ON = 0x04;
+  public final static int META_ALT_LOCK = 0x08;
+  public final static int META_SHIFT_ON = 0x10;
+  public final static int META_SHIFT_LOCK = 0x20;
+  public final static int META_SLASH = 0x40;
+  public final static int META_TAB = 0x80;
+
+  // The bit mask of momentary and lock states for each
+  public final static int META_CTRL_MASK = META_CTRL_ON | META_CTRL_LOCK;
+  public final static int META_ALT_MASK = META_ALT_ON | META_ALT_LOCK;
+  public final static int META_SHIFT_MASK = META_SHIFT_ON | META_SHIFT_LOCK;
+
+  // All the transient key codes
+  public final static int META_TRANSIENT = META_CTRL_ON | META_ALT_ON | META_SHIFT_ON;
+
+  public final static int KEYBOARD_META_CTRL_ON = 0x1000; // Ctrl key mask for API 11+
+  private final TerminalManager manager;
+  private final TerminalBridge bridge;
+  private final VDUBuffer buffer;
+
+  protected KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
+
+  private String keymode = null;
+  private boolean hardKeyboard = false;
+
+  private int metaState = 0;
+
+  private ClipboardManager clipboard = null;
+  private boolean selectingForCopy = false;
+  private final SelectionArea selectionArea;
+
+  private String encoding;
+
+  public TerminalKeyListener(TerminalManager manager, TerminalBridge bridge, VDUBuffer buffer,
+      String encoding) {
+    this.manager = manager;
+    this.bridge = bridge;
+    this.buffer = buffer;
+    this.encoding = encoding;
+
+    selectionArea = new SelectionArea();
+
+    manager.registerOnSharedPreferenceChangeListener(this);
+
+    hardKeyboard =
+        (manager.getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY);
+
+    updateKeymode();
+  }
+
+  /**
+   * Handle onKey() events coming down from a {@link TerminalView} above us. Modify the keys to make
+   * more sense to a host then pass it to the transport.
+   */
+  public boolean onKey(View v, int keyCode, KeyEvent event) {
+    try {
+      final boolean hardKeyboardHidden = manager.isHardKeyboardHidden();
+
+      AbsTransport transport = bridge.getTransport();
+
+      // Ignore all key-up events except for the special keys
+      if (event.getAction() == KeyEvent.ACTION_UP) {
+        // There's nothing here for virtual keyboard users.
+        if (!hardKeyboard || (hardKeyboard && hardKeyboardHidden)) {
+          return false;
+        }
+
+        // skip keys if we aren't connected yet or have been disconnected
+        if (transport == null || !transport.isSessionOpen()) {
+          return false;
+        }
+
+        if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
+          if (keyCode == KeyEvent.KEYCODE_ALT_RIGHT && (metaState & META_SLASH) != 0) {
+            metaState &= ~(META_SLASH | META_TRANSIENT);
+            transport.write('/');
+            return true;
+          } else if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT && (metaState & META_TAB) != 0) {
+            metaState &= ~(META_TAB | META_TRANSIENT);
+            transport.write(0x09);
+            return true;
+          }
+        } else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
+          if (keyCode == KeyEvent.KEYCODE_ALT_LEFT && (metaState & META_SLASH) != 0) {
+            metaState &= ~(META_SLASH | META_TRANSIENT);
+            transport.write('/');
+            return true;
+          } else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT && (metaState & META_TAB) != 0) {
+            metaState &= ~(META_TAB | META_TRANSIENT);
+            transport.write(0x09);
+            return true;
+          }
+        }
+
+        return false;
+      }
+
+      if (keyCode == KeyEvent.KEYCODE_BACK && transport != null) {
+        bridge.dispatchDisconnect(!transport.isSessionOpen());
+        return true;
+      }
+
+      // check for terminal resizing keys
+      if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+        bridge.increaseFontSize();
+        return true;
+      } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
+        bridge.decreaseFontSize();
+        return true;
+      }
+
+      // skip keys if we aren't connected yet or have been disconnected
+      if (transport == null || !transport.isSessionOpen()) {
+        return false;
+      }
+
+      bridge.resetScrollPosition();
+
+      boolean printing = (keymap.isPrintingKey(keyCode) || keyCode == KeyEvent.KEYCODE_SPACE);
+
+      // otherwise pass through to existing session
+      // print normal keys
+      if (printing) {
+        int curMetaState = event.getMetaState();
+
+        metaState &= ~(META_SLASH | META_TAB);
+
+        if ((metaState & META_SHIFT_MASK) != 0) {
+          curMetaState |= KeyEvent.META_SHIFT_ON;
+          metaState &= ~META_SHIFT_ON;
+          bridge.redraw();
+        }
+
+        if ((metaState & META_ALT_MASK) != 0) {
+          curMetaState |= KeyEvent.META_ALT_ON;
+          metaState &= ~META_ALT_ON;
+          bridge.redraw();
+        }
+
+        int key = keymap.get(keyCode, curMetaState);
+        if ((curMetaState & KEYBOARD_META_CTRL_ON) != 0) {
+          metaState |= META_CTRL_ON;
+          key = keymap.get(keyCode, 0);
+        }
+
+        if ((metaState & META_CTRL_MASK) != 0) {
+          metaState &= ~META_CTRL_ON;
+          bridge.redraw();
+
+          if ((!hardKeyboard || (hardKeyboard && hardKeyboardHidden)) && sendFunctionKey(keyCode)) {
+            return true;
+          }
+
+          // Support CTRL-a through CTRL-z
+          if (key >= 0x61 && key <= 0x7A) {
+            key -= 0x60;
+          } else if (key >= 0x41 && key <= 0x5F) {
+            key -= 0x40;
+          } else if (key == 0x20) {
+            key = 0x00;
+          } else if (key == 0x3F) {
+            key = 0x7F;
+          }
+        }
+
+        // handle pressing f-keys
+        // Doesn't work properly with asus keyboards... may never have worked. RM 09-Apr-2012
+        /*
+         * if ((hardKeyboard && !hardKeyboardHidden) && (curMetaState & KeyEvent.META_SHIFT_ON) != 0
+         * && sendFunctionKey(keyCode)) { return true; }
+         */
+
+        if (key < 0x80) {
+          transport.write(key);
+        } else {
+          // TODO write encoding routine that doesn't allocate each time
+          transport.write(new String(Character.toChars(key)).getBytes(encoding));
+        }
+
+        return true;
+      }
+
+      if (keyCode == KeyEvent.KEYCODE_UNKNOWN && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
+        byte[] input = event.getCharacters().getBytes(encoding);
+        transport.write(input);
+        return true;
+      }
+
+      // try handling keymode shortcuts
+      if (hardKeyboard && !hardKeyboardHidden && event.getRepeatCount() == 0) {
+        if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
+          switch (keyCode) {
+          case KeyEvent.KEYCODE_ALT_RIGHT:
+            metaState |= META_SLASH;
+            return true;
+          case KeyEvent.KEYCODE_SHIFT_RIGHT:
+            metaState |= META_TAB;
+            return true;
+          case KeyEvent.KEYCODE_SHIFT_LEFT:
+            metaPress(META_SHIFT_ON);
+            return true;
+          case KeyEvent.KEYCODE_ALT_LEFT:
+            metaPress(META_ALT_ON);
+            return true;
+          }
+        } else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
+          switch (keyCode) {
+          case KeyEvent.KEYCODE_ALT_LEFT:
+            metaState |= META_SLASH;
+            return true;
+          case KeyEvent.KEYCODE_SHIFT_LEFT:
+            metaState |= META_TAB;
+            return true;
+          case KeyEvent.KEYCODE_SHIFT_RIGHT:
+            metaPress(META_SHIFT_ON);
+            return true;
+          case KeyEvent.KEYCODE_ALT_RIGHT:
+            metaPress(META_ALT_ON);
+            return true;
+          }
+        } else {
+          switch (keyCode) {
+          case KeyEvent.KEYCODE_ALT_LEFT:
+          case KeyEvent.KEYCODE_ALT_RIGHT:
+            metaPress(META_ALT_ON);
+            return true;
+          case KeyEvent.KEYCODE_SHIFT_LEFT:
+          case KeyEvent.KEYCODE_SHIFT_RIGHT:
+            metaPress(META_SHIFT_ON);
+            return true;
+          }
+        }
+      }
+
+      // look for special chars
+      switch (keyCode) {
+      case KeyEvent.KEYCODE_CAMERA:
+
+        // check to see which shortcut the camera button triggers
+        String camera =
+            manager.getStringParameter(PreferenceConstants.CAMERA,
+                PreferenceConstants.CAMERA_CTRLA_SPACE);
+        if (PreferenceConstants.CAMERA_CTRLA_SPACE.equals(camera)) {
+          transport.write(0x01);
+          transport.write(' ');
+        } else if (PreferenceConstants.CAMERA_CTRLA.equals(camera)) {
+          transport.write(0x01);
+        } else if (PreferenceConstants.CAMERA_ESC.equals(camera)) {
+          ((vt320) buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
+        } else if (PreferenceConstants.CAMERA_ESC_A.equals(camera)) {
+          ((vt320) buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
+          transport.write('a');
+        }
+
+        break;
+
+      case KeyEvent.KEYCODE_DEL:
+        ((vt320) buffer).keyPressed(vt320.KEY_BACK_SPACE, ' ', getStateForBuffer());
+        metaState &= ~META_TRANSIENT;
+        return true;
+      case KeyEvent.KEYCODE_ENTER:
+        ((vt320) buffer).keyTyped(vt320.KEY_ENTER, ' ', 0);
+        metaState &= ~META_TRANSIENT;
+        return true;
+
+      case KeyEvent.KEYCODE_DPAD_LEFT:
+        if (selectingForCopy) {
+          selectionArea.decrementColumn();
+          bridge.redraw();
+        } else {
+          ((vt320) buffer).keyPressed(vt320.KEY_LEFT, ' ', getStateForBuffer());
+          metaState &= ~META_TRANSIENT;
+          bridge.tryKeyVibrate();
+        }
+        return true;
+
+      case KeyEvent.KEYCODE_DPAD_UP:
+        if (selectingForCopy) {
+          selectionArea.decrementRow();
+          bridge.redraw();
+        } else {
+          ((vt320) buffer).keyPressed(vt320.KEY_UP, ' ', getStateForBuffer());
+          metaState &= ~META_TRANSIENT;
+          bridge.tryKeyVibrate();
+        }
+        return true;
+
+      case KeyEvent.KEYCODE_DPAD_DOWN:
+        if (selectingForCopy) {
+          selectionArea.incrementRow();
+          bridge.redraw();
+        } else {
+          ((vt320) buffer).keyPressed(vt320.KEY_DOWN, ' ', getStateForBuffer());
+          metaState &= ~META_TRANSIENT;
+          bridge.tryKeyVibrate();
+        }
+        return true;
+
+      case KeyEvent.KEYCODE_DPAD_RIGHT:
+        if (selectingForCopy) {
+          selectionArea.incrementColumn();
+          bridge.redraw();
+        } else {
+          ((vt320) buffer).keyPressed(vt320.KEY_RIGHT, ' ', getStateForBuffer());
+          metaState &= ~META_TRANSIENT;
+          bridge.tryKeyVibrate();
+        }
+        return true;
+
+      case KeyEvent.KEYCODE_DPAD_CENTER:
+        if (selectingForCopy) {
+          if (selectionArea.isSelectingOrigin()) {
+            selectionArea.finishSelectingOrigin();
+          } else {
+            if (clipboard != null) {
+              // copy selected area to clipboard
+              String copiedText = selectionArea.copyFrom(buffer);
+
+              clipboard.setText(copiedText);
+              // XXX STOPSHIP
+              // manager.notifyUser(manager.getString(
+              // R.string.console_copy_done,
+              // copiedText.length()));
+
+              selectingForCopy = false;
+              selectionArea.reset();
+            }
+          }
+        } else {
+          if ((metaState & META_CTRL_ON) != 0) {
+            ((vt320) buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
+            metaState &= ~META_CTRL_ON;
+          } else {
+            metaState |= META_CTRL_ON;
+          }
+        }
+
+        bridge.redraw();
+
+        return true;
+      }
+
+    } catch (IOException e) {
+      Log.e("Problem while trying to handle an onKey() event", e);
+      try {
+        bridge.getTransport().flush();
+      } catch (IOException ioe) {
+        Log.d("Our transport was closed, dispatching disconnect event");
+        bridge.dispatchDisconnect(false);
+      }
+    } catch (NullPointerException npe) {
+      Log.d("Input before connection established ignored.");
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * @param keyCode
+   * @return successful
+   */
+  private boolean sendFunctionKey(int keyCode) {
+    switch (keyCode) {
+    case KeyEvent.KEYCODE_1:
+      ((vt320) buffer).keyPressed(vt320.KEY_F1, ' ', 0);
+      return true;
+    case KeyEvent.KEYCODE_2:
+      ((vt320) buffer).keyPressed(vt320.KEY_F2, ' ', 0);
+      return true;
+    case KeyEvent.KEYCODE_3:
+      ((vt320) buffer).keyPressed(vt320.KEY_F3, ' ', 0);
+      return true;
+    case KeyEvent.KEYCODE_4:
+      ((vt320) buffer).keyPressed(vt320.KEY_F4, ' ', 0);
+      return true;
+    case KeyEvent.KEYCODE_5:
+      ((vt320) buffer).keyPressed(vt320.KEY_F5, ' ', 0);
+      return true;
+    case KeyEvent.KEYCODE_6:
+      ((vt320) buffer).keyPressed(vt320.KEY_F6, ' ', 0);
+      return true;
+    case KeyEvent.KEYCODE_7:
+      ((vt320) buffer).keyPressed(vt320.KEY_F7, ' ', 0);
+      return true;
+    case KeyEvent.KEYCODE_8:
+      ((vt320) buffer).keyPressed(vt320.KEY_F8, ' ', 0);
+      return true;
+    case KeyEvent.KEYCODE_9:
+      ((vt320) buffer).keyPressed(vt320.KEY_F9, ' ', 0);
+      return true;
+    case KeyEvent.KEYCODE_0:
+      ((vt320) buffer).keyPressed(vt320.KEY_F10, ' ', 0);
+      return true;
+    default:
+      return false;
+    }
+  }
+
+  /**
+   * Handle meta key presses where the key can be locked on.
+   * <p>
+   * 1st press: next key to have meta state<br />
+   * 2nd press: meta state is locked on<br />
+   * 3rd press: disable meta state
+   *
+   * @param code
+   */
+  private void metaPress(int code) {
+    if ((metaState & (code << 1)) != 0) {
+      metaState &= ~(code << 1);
+    } else if ((metaState & code) != 0) {
+      metaState &= ~code;
+      metaState |= code << 1;
+    } else {
+      metaState |= code;
+    }
+    bridge.redraw();
+  }
+
+  public void setTerminalKeyMode(String keymode) {
+    this.keymode = keymode;
+  }
+
+  private int getStateForBuffer() {
+    int bufferState = 0;
+
+    if ((metaState & META_CTRL_MASK) != 0) {
+      bufferState |= vt320.KEY_CONTROL;
+    }
+    if ((metaState & META_SHIFT_MASK) != 0) {
+      bufferState |= vt320.KEY_SHIFT;
+    }
+    if ((metaState & META_ALT_MASK) != 0) {
+      bufferState |= vt320.KEY_ALT;
+    }
+
+    return bufferState;
+  }
+
+  public int getMetaState() {
+    return metaState;
+  }
+
+  public void setClipboardManager(ClipboardManager clipboard) {
+    this.clipboard = clipboard;
+  }
+
+  public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+    if (PreferenceConstants.KEYMODE.equals(key)) {
+      updateKeymode();
+    }
+  }
+
+  private void updateKeymode() {
+    keymode =
+        manager.getStringParameter(PreferenceConstants.KEYMODE, PreferenceConstants.KEYMODE_RIGHT);
+  }
+
+  public void setCharset(String encoding) {
+    this.encoding = encoding;
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/service/TerminalManager.java b/ScriptingLayerForAndroid/src/org/connectbot/service/TerminalManager.java
new file mode 100644
index 0000000..bc83a92
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/service/TerminalManager.java
@@ -0,0 +1,306 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.service;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Vibrator;
+import android.preference.PreferenceManager;
+
+import com.googlecode.android_scripting.Constants;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.R;
+import com.googlecode.android_scripting.activity.ScriptingLayerService;
+import com.googlecode.android_scripting.exception.Sl4aException;
+import com.googlecode.android_scripting.interpreter.InterpreterProcess;
+
+import org.connectbot.transport.ProcessTransport;
+import org.connectbot.util.PreferenceConstants;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Manager for SSH connections that runs as a background service. This service holds a list of
+ * currently connected SSH bridges that are ready for connection up to a GUI if needed.
+ *
+ * @author jsharkey
+ * @author modified by raaar
+ */
+public class TerminalManager implements OnSharedPreferenceChangeListener {
+
+  private static final long VIBRATE_DURATION = 30;
+
+  private final List<TerminalBridge> bridges = new CopyOnWriteArrayList<TerminalBridge>();
+
+  private final Map<Integer, WeakReference<TerminalBridge>> mHostBridgeMap =
+      new ConcurrentHashMap<Integer, WeakReference<TerminalBridge>>();
+
+  private Handler mDisconnectHandler = null;
+
+  private final Resources mResources;
+
+  private final SharedPreferences mPreferences;
+
+  private boolean hardKeyboardHidden;
+
+  private Vibrator vibrator;
+  private boolean wantKeyVibration;
+  private boolean wantBellVibration;
+  private boolean wantAudible;
+  private boolean resizeAllowed = false;
+  private MediaPlayer mediaPlayer;
+
+  private final ScriptingLayerService mService;
+
+  public TerminalManager(ScriptingLayerService service) {
+    mService = service;
+    mPreferences = PreferenceManager.getDefaultSharedPreferences(mService);
+    registerOnSharedPreferenceChangeListener(this);
+    mResources = mService.getResources();
+    hardKeyboardHidden =
+        (mResources.getConfiguration().hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES);
+    vibrator = (Vibrator) mService.getSystemService(Context.VIBRATOR_SERVICE);
+    wantKeyVibration = mPreferences.getBoolean(PreferenceConstants.BUMPY_ARROWS, true);
+    wantBellVibration = mPreferences.getBoolean(PreferenceConstants.BELL_VIBRATE, true);
+    wantAudible = mPreferences.getBoolean(PreferenceConstants.BELL, true);
+    if (wantAudible) {
+      enableMediaPlayer();
+    }
+  }
+
+  /**
+   * Disconnect all currently connected bridges.
+   */
+  private void disconnectAll() {
+    TerminalBridge[] bridgesArray = null;
+    if (bridges.size() > 0) {
+      bridgesArray = bridges.toArray(new TerminalBridge[bridges.size()]);
+    }
+    if (bridgesArray != null) {
+      // disconnect and dispose of any existing bridges
+      for (TerminalBridge bridge : bridgesArray) {
+        bridge.dispatchDisconnect(true);
+      }
+    }
+  }
+
+  /**
+   * Open a new session using the given parameters.
+   *
+   * @throws InterruptedException
+   * @throws Sl4aException
+   */
+  public TerminalBridge openConnection(int id) throws IllegalArgumentException, IOException,
+      InterruptedException, Sl4aException {
+    // throw exception if terminal already open
+    if (getConnectedBridge(id) != null) {
+      throw new IllegalArgumentException("Connection already open");
+    }
+
+    InterpreterProcess process = mService.getProcess(id);
+
+    TerminalBridge bridge = new TerminalBridge(this, process, new ProcessTransport(process));
+    bridge.connect();
+
+    WeakReference<TerminalBridge> wr = new WeakReference<TerminalBridge>(bridge);
+    bridges.add(bridge);
+    mHostBridgeMap.put(id, wr);
+
+    return bridge;
+  }
+
+  /**
+   * Find a connected {@link TerminalBridge} with the given HostBean.
+   *
+   * @param id
+   *          the HostBean to search for
+   * @return TerminalBridge that uses the HostBean
+   */
+  public TerminalBridge getConnectedBridge(int id) {
+    WeakReference<TerminalBridge> wr = mHostBridgeMap.get(id);
+    if (wr != null) {
+      return wr.get();
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Called by child bridge when somehow it's been disconnected.
+   */
+  public void closeConnection(TerminalBridge bridge, boolean killProcess) {
+    if (killProcess) {
+      bridges.remove(bridge);
+      mHostBridgeMap.remove(bridge.getId());
+      if (mService.getProcess(bridge.getId()).isAlive()) {
+        Intent intent = new Intent(mService, mService.getClass());
+        intent.setAction(Constants.ACTION_KILL_PROCESS);
+        intent.putExtra(Constants.EXTRA_PROXY_PORT, bridge.getId());
+        mService.startService(intent);
+      }
+    }
+    if (mDisconnectHandler != null) {
+      Message.obtain(mDisconnectHandler, -1, bridge).sendToTarget();
+    }
+  }
+
+  /**
+   * Allow {@link TerminalBridge} to resize when the parent has changed.
+   *
+   * @param resizeAllowed
+   */
+  public void setResizeAllowed(boolean resizeAllowed) {
+    this.resizeAllowed = resizeAllowed;
+  }
+
+  public boolean isResizeAllowed() {
+    return resizeAllowed;
+  }
+
+  public void stop() {
+    resizeAllowed = false;
+    disconnectAll();
+    disableMediaPlayer();
+  }
+
+  public int getIntParameter(String key, int defValue) {
+    return mPreferences.getInt(key, defValue);
+  }
+
+  public String getStringParameter(String key, String defValue) {
+    return mPreferences.getString(key, defValue);
+  }
+
+  public void tryKeyVibrate() {
+    if (wantKeyVibration) {
+      vibrate();
+    }
+  }
+
+  private void vibrate() {
+    if (vibrator != null) {
+      vibrator.vibrate(VIBRATE_DURATION);
+    }
+  }
+
+  private void enableMediaPlayer() {
+    mediaPlayer = new MediaPlayer();
+
+    float volume =
+        mPreferences.getFloat(PreferenceConstants.BELL_VOLUME,
+            PreferenceConstants.DEFAULT_BELL_VOLUME);
+
+    mediaPlayer.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
+    mediaPlayer.setOnCompletionListener(new BeepListener());
+
+    AssetFileDescriptor file = mResources.openRawResourceFd(R.raw.bell);
+    try {
+      mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength());
+      file.close();
+      mediaPlayer.setVolume(volume, volume);
+      mediaPlayer.prepare();
+    } catch (IOException e) {
+      Log.e("Error setting up bell media player", e);
+    }
+  }
+
+  private void disableMediaPlayer() {
+    if (mediaPlayer != null) {
+      mediaPlayer.release();
+      mediaPlayer = null;
+    }
+  }
+
+  public void playBeep() {
+    if (mediaPlayer != null) {
+      mediaPlayer.start();
+    }
+    if (wantBellVibration) {
+      vibrate();
+    }
+  }
+
+  private static class BeepListener implements OnCompletionListener {
+    public void onCompletion(MediaPlayer mp) {
+      mp.seekTo(0);
+    }
+  }
+
+  public boolean isHardKeyboardHidden() {
+    return hardKeyboardHidden;
+  }
+
+  public void setHardKeyboardHidden(boolean b) {
+    hardKeyboardHidden = b;
+  }
+
+  @Override
+  public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+    if (PreferenceConstants.BELL.equals(key)) {
+      wantAudible = sharedPreferences.getBoolean(PreferenceConstants.BELL, true);
+      if (wantAudible && mediaPlayer == null) {
+        enableMediaPlayer();
+      } else if (!wantAudible && mediaPlayer != null) {
+        disableMediaPlayer();
+      }
+    } else if (PreferenceConstants.BELL_VOLUME.equals(key)) {
+      if (mediaPlayer != null) {
+        float volume =
+            sharedPreferences.getFloat(PreferenceConstants.BELL_VOLUME,
+                PreferenceConstants.DEFAULT_BELL_VOLUME);
+        mediaPlayer.setVolume(volume, volume);
+      }
+    } else if (PreferenceConstants.BELL_VIBRATE.equals(key)) {
+      wantBellVibration = sharedPreferences.getBoolean(PreferenceConstants.BELL_VIBRATE, true);
+    } else if (PreferenceConstants.BUMPY_ARROWS.equals(key)) {
+      wantKeyVibration = sharedPreferences.getBoolean(PreferenceConstants.BUMPY_ARROWS, true);
+    }
+  }
+
+  public void setDisconnectHandler(Handler disconnectHandler) {
+    mDisconnectHandler = disconnectHandler;
+  }
+
+  public List<TerminalBridge> getBridgeList() {
+    return bridges;
+  }
+
+  public Resources getResources() {
+    return mResources;
+  }
+
+  public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+    mPreferences.registerOnSharedPreferenceChangeListener(listener);
+  }
+
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/transport/AbsTransport.java b/ScriptingLayerForAndroid/src/org/connectbot/transport/AbsTransport.java
new file mode 100644
index 0000000..3d8c943
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/transport/AbsTransport.java
@@ -0,0 +1,119 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.transport;
+
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+
+import java.io.IOException;
+
+/**
+ * @author Kenny Root
+ * @author modified by raaar
+ */
+public abstract class AbsTransport {
+
+  TerminalBridge bridge;
+  TerminalManager manager;
+
+  /**
+   * Causes transport to connect to the target host. After connecting but before a session is
+   * started, must call back to {@link TerminalBridge#onConnected()}. After that call a session may
+   * be opened.
+   */
+  public abstract void connect();
+
+  /**
+   * Reads from the transport. Transport must support reading into a the byte array
+   * <code>buffer</code> at the start of <code>offset</code> and a maximum of <code>length</code>
+   * bytes. If the remote host disconnects, throw an {@link IOException}.
+   *
+   * @param buffer
+   *          byte buffer to store read bytes into
+   * @param offset
+   *          where to start writing in the buffer
+   * @param length
+   *          maximum number of bytes to read
+   * @return number of bytes read
+   * @throws IOException
+   *           when remote host disconnects
+   */
+  public abstract int read(byte[] buffer, int offset, int length) throws IOException;
+
+  /**
+   * Writes to the transport. If the host is not yet connected, simply return without doing
+   * anything. An {@link IOException} should be thrown if there is an error after connection.
+   *
+   * @param buffer
+   *          bytes to write to transport
+   * @throws IOException
+   *           when there is a problem writing after connection
+   */
+  public abstract void write(byte[] buffer) throws IOException;
+
+  /**
+   * Writes to the transport. See {@link #write(byte[])} for behavior details.
+   *
+   * @param c
+   *          character to write to the transport
+   * @throws IOException
+   *           when there is a problem writing after connection
+   */
+  public abstract void write(int c) throws IOException;
+
+  /**
+   * Flushes the write commands to the transport.
+   *
+   * @throws IOException
+   *           when there is a problem writing after connection
+   */
+  public abstract void flush() throws IOException;
+
+  /**
+   * Closes the connection to the terminal. Note that the resulting failure to read should call
+   * {@link TerminalBridge#dispatchDisconnect(boolean)}.
+   */
+  public abstract void close();
+
+  /**
+   * Tells the transport what dimensions the display is currently
+   *
+   * @param columns
+   *          columns of text
+   * @param rows
+   *          rows of text
+   * @param width
+   *          width in pixels
+   * @param height
+   *          height in pixels
+   */
+  public abstract void setDimensions(int columns, int rows, int width, int height);
+
+  public void setBridge(TerminalBridge bridge) {
+    this.bridge = bridge;
+  }
+
+  public void setManager(TerminalManager manager) {
+    this.manager = manager;
+  }
+
+  public abstract boolean isConnected();
+
+  public abstract boolean isSessionOpen();
+
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/transport/ProcessTransport.java b/ScriptingLayerForAndroid/src/org/connectbot/transport/ProcessTransport.java
new file mode 100644
index 0000000..ff894f0
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/transport/ProcessTransport.java
@@ -0,0 +1,108 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.transport;
+
+import com.googlecode.android_scripting.Exec;
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.Process;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class ProcessTransport extends AbsTransport {
+
+  private FileDescriptor shellFd;
+  private final Process mProcess;
+  private InputStream is;
+  private OutputStream os;
+  private boolean mConnected = false;
+
+  public ProcessTransport(Process process) {
+    mProcess = process;
+    shellFd = process.getFd();
+    is = process.getIn();
+    os = process.getOut();
+  }
+
+  @Override
+  public void close() {
+    mProcess.kill();
+  }
+
+  @Override
+  public void connect() {
+    mConnected = true;
+  }
+
+  @Override
+  public void flush() throws IOException {
+    os.flush();
+  }
+
+  @Override
+  public boolean isConnected() {
+    return mConnected && mProcess.isAlive();
+  }
+
+  @Override
+  public boolean isSessionOpen() {
+    return mProcess.isAlive();
+  }
+
+  @Override
+  public int read(byte[] buffer, int start, int len) throws IOException {
+    if (is == null) {
+      mConnected = false;
+      bridge.dispatchDisconnect(false);
+      throw new IOException("session closed");
+    }
+    try {
+      return is.read(buffer, start, len);
+    } catch (IOException e) {
+      mConnected = false;
+      bridge.dispatchDisconnect(false);
+      throw new IOException("session closed");
+    }
+  }
+
+  @Override
+  public void setDimensions(int columns, int rows, int width, int height) {
+    try {
+      Exec.setPtyWindowSize(shellFd, rows, columns, width, height);
+    } catch (Exception e) {
+      Log.e("Couldn't resize pty", e);
+    }
+  }
+
+  @Override
+  public void write(byte[] buffer) throws IOException {
+    if (os != null) {
+      os.write(buffer);
+    }
+  }
+
+  @Override
+  public void write(int c) throws IOException {
+    if (os != null) {
+      os.write(c);
+    }
+  }
+
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/util/Colors.java b/ScriptingLayerForAndroid/src/org/connectbot/util/Colors.java
new file mode 100644
index 0000000..13310e2
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/util/Colors.java
@@ -0,0 +1,78 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.util;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class Colors {
+  public final static int[] defaults =
+      new int[] {
+        0xff000000, // black
+        0xffcc0000, // red
+        0xff00cc00, // green
+        0xffcccc00, // brown
+        0xff0000cc, // blue
+        0xffcc00cc, // purple
+        0xff00cccc, // cyan
+        0xffcccccc, // light grey
+        0xff444444, // dark grey
+        0xffff4444, // light red
+        0xff44ff44, // light green
+        0xffffff44, // yellow
+        0xff4444ff, // light blue
+        0xffff44ff, // light purple
+        0xff44ffff, // light cyan
+        0xffffffff, // white
+        0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00,
+        0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff, 0xff008700, 0xff00875f,
+        0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87,
+        0xff00afaf, 0xff00afd7, 0xff00afff, 0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af,
+        0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7,
+        0xff00ffff, 0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff,
+        0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff, 0xff5f8700,
+        0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f,
+        0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff, 0xff5fd700, 0xff5fd75f, 0xff5fd787,
+        0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf,
+        0xff5fffd7, 0xff5fffff, 0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7,
+        0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
+        0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00,
+        0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff, 0xff87d700, 0xff87d75f,
+        0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87,
+        0xff87ffaf, 0xff87ffd7, 0xff87ffff, 0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af,
+        0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7,
+        0xffaf5fff, 0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff,
+        0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff, 0xffafd700,
+        0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f,
+        0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff, 0xffd70000, 0xffd7005f, 0xffd70087,
+        0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf,
+        0xffd75fd7, 0xffd75fff, 0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7,
+        0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
+        0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00,
+        0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff, 0xffff0000, 0xffff005f,
+        0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87,
+        0xffff5faf, 0xffff5fd7, 0xffff5fff, 0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af,
+        0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7,
+        0xffffafff, 0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff,
+        0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffff, 0xff080808,
+        0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e,
+        0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676, 0xff808080, 0xff8a8a8a, 0xff949494,
+        0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada,
+        0xffe4e4e4, 0xffeeeeee, };
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/util/ColorsActivity.java b/ScriptingLayerForAndroid/src/org/connectbot/util/ColorsActivity.java
new file mode 100644
index 0000000..532da35
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/util/ColorsActivity.java
@@ -0,0 +1,286 @@
+package org.connectbot.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.LinearLayout;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.googlecode.android_scripting.R;
+
+import org.connectbot.util.UberColorPickerDialog.OnColorChangedListener;
+
+/**
+ * @author modified by raaar
+ */
+public class ColorsActivity extends Activity implements OnItemClickListener, OnColorChangedListener {
+
+  private SharedPreferences mPreferences;
+
+  private static int sLayoutLanscapeWidth = 400;
+  private static int sLayoutPortraitWidth = 210;
+
+  private GridView mColorGrid;
+  private LinearLayout mLayout;
+
+  private int mFgColor;
+  private int mBgColor;
+
+  private int mCurrentColor = 0;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+
+    mFgColor =
+        mPreferences.getInt(PreferenceConstants.COLOR_FG, PreferenceConstants.DEFAULT_FG_COLOR);
+    mBgColor =
+        mPreferences.getInt(PreferenceConstants.COLOR_BG, PreferenceConstants.DEFAULT_BG_COLOR);
+
+    setContentView(R.layout.act_colors);
+
+    this.setTitle("Terminal Colors");
+
+    mLayout = (LinearLayout) findViewById(R.id.color_layout);
+
+    mColorGrid = (GridView) findViewById(R.id.color_grid);
+    mColorGrid.setOnItemClickListener(this);
+    mColorGrid.setSelection(0);
+
+    if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
+      mColorGrid.setNumColumns(2);
+      LayoutParams params = mLayout.getLayoutParams();
+      params.height = params.width;
+      params.width = LayoutParams.WRAP_CONTENT;
+    }
+    mColorGrid.setAdapter(new ColorsAdapter(true));
+
+  }
+
+  private class ColorsAdapter extends BaseAdapter {
+    private boolean mSquareViews;
+
+    public ColorsAdapter(boolean squareViews) {
+      mSquareViews = squareViews;
+    }
+
+    public View getView(int position, View convertView, ViewGroup parent) {
+      ColorView c;
+      if (convertView == null) {
+        c = new ColorView(ColorsActivity.this, mSquareViews);
+      } else {
+        c = (ColorView) convertView;
+      }
+      if (position == 0) {
+        c.setColor(mFgColor);
+        c.setTitle("Foreground color");
+      } else {
+        c.setColor(mBgColor);
+        c.setTitle("Background color");
+      }
+      return c;
+    }
+
+    public int getCount() {
+      return 2;
+    }
+
+    public Object getItem(int position) {
+      return (position == 0) ? mFgColor : mBgColor;
+    }
+
+    public long getItemId(int position) {
+      return position;
+    }
+  }
+
+  private class ColorView extends View {
+    private boolean mSquare;
+
+    private Paint mTextPaint;
+    private Paint mShadowPaint;
+    // Things we paint
+    private int mBackgroundColor;
+    private String mText;
+
+    private int mAscent;
+    private int mWidthCenter;
+    private int mHeightCenter;
+
+    public ColorView(Context context, boolean square) {
+      super(context);
+
+      mSquare = square;
+
+      mTextPaint = new Paint();
+      mTextPaint.setAntiAlias(true);
+      mTextPaint.setTextSize(16);
+      mTextPaint.setColor(0xFFFFFFFF);
+      mTextPaint.setTextAlign(Paint.Align.CENTER);
+
+      mShadowPaint = new Paint(mTextPaint);
+      mShadowPaint.setStyle(Paint.Style.STROKE);
+      mShadowPaint.setStrokeCap(Paint.Cap.ROUND);
+      mShadowPaint.setStrokeJoin(Paint.Join.ROUND);
+      mShadowPaint.setStrokeWidth(4f);
+      mShadowPaint.setColor(0xFF000000);
+
+      setPadding(20, 20, 20, 20);
+    }
+
+    public void setColor(int color) {
+      mBackgroundColor = color;
+    }
+
+    public void setTitle(String title) {
+      mText = title;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+      int width = measureWidth(widthMeasureSpec);
+
+      int height;
+      if (mSquare) {
+        height = width;
+      } else {
+        height = measureHeight(heightMeasureSpec);
+      }
+
+      mAscent = (int) mTextPaint.ascent();
+      mWidthCenter = width / 2;
+      mHeightCenter = height / 2 - mAscent / 2;
+
+      setMeasuredDimension(width, height);
+    }
+
+    private int measureWidth(int measureSpec) {
+      int result = 0;
+      int specMode = MeasureSpec.getMode(measureSpec);
+      int specSize = MeasureSpec.getSize(measureSpec);
+
+      if (specMode == MeasureSpec.EXACTLY) {
+        // We were told how big to be
+        result = specSize;
+      } else {
+        // Measure the text
+        result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + getPaddingRight();
+        if (specMode == MeasureSpec.AT_MOST) {
+          // Respect AT_MOST value if that was what is called for by
+          // measureSpec
+          result = Math.min(result, specSize);
+        }
+      }
+
+      return result;
+    }
+
+    private int measureHeight(int measureSpec) {
+      int result = 0;
+      int specMode = MeasureSpec.getMode(measureSpec);
+      int specSize = MeasureSpec.getSize(measureSpec);
+
+      mAscent = (int) mTextPaint.ascent();
+      if (specMode == MeasureSpec.EXACTLY) {
+        // We were told how big to be
+        result = specSize;
+      } else {
+        // Measure the text (beware: ascent is a negative number)
+        result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop() + getPaddingBottom();
+        if (specMode == MeasureSpec.AT_MOST) {
+          // Respect AT_MOST value if that was what is called for by
+          // measureSpec
+          result = Math.min(result, specSize);
+        }
+      }
+      return result;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+      super.onDraw(canvas);
+      canvas.drawColor(mBackgroundColor);
+      canvas.drawText(mText, mWidthCenter, mHeightCenter, mShadowPaint);
+      canvas.drawText(mText, mWidthCenter, mHeightCenter, mTextPaint);
+    }
+  }
+
+  private void editColor(int colorNumber) {
+    mCurrentColor = colorNumber;
+    new UberColorPickerDialog(this, this, (colorNumber == 0) ? mFgColor : mBgColor).show();
+  }
+
+  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+    editColor(position);
+  }
+
+  public void onNothingSelected(AdapterView<?> arg0) {
+  }
+
+  public void colorChanged(int value) {
+    SharedPreferences.Editor editor = mPreferences.edit();
+    if (mCurrentColor == 0) {
+      mFgColor = value;
+      editor.putInt(PreferenceConstants.COLOR_FG, mFgColor);
+    } else {
+      mBgColor = value;
+      editor.putInt(PreferenceConstants.COLOR_BG, mBgColor);
+    }
+    editor.commit();
+    mColorGrid.invalidateViews();
+  }
+
+  @Override
+  public void onConfigurationChanged(Configuration newConfig) {
+    super.onConfigurationChanged(newConfig);
+    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+      mColorGrid.setNumColumns(2);
+      LayoutParams params = mLayout.getLayoutParams();
+      params.height = params.width;
+      params.width = sLayoutLanscapeWidth;
+    } else {
+      mColorGrid.setNumColumns(1);
+      LayoutParams params = mLayout.getLayoutParams();
+      params.height = LayoutParams.WRAP_CONTENT;
+      params.width = sLayoutPortraitWidth;
+    }
+  }
+
+  @Override
+  public boolean onCreateOptionsMenu(Menu menu) {
+    super.onCreateOptionsMenu(menu);
+    MenuItem reset = menu.add("Reset");
+    reset.setAlphabeticShortcut('r');
+    reset.setNumericShortcut('1');
+    reset.setIcon(android.R.drawable.ic_menu_revert);
+    reset.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+      public boolean onMenuItemClick(MenuItem arg0) {
+        mFgColor = PreferenceConstants.DEFAULT_FG_COLOR;
+        mBgColor = PreferenceConstants.DEFAULT_BG_COLOR;
+        SharedPreferences.Editor editor = mPreferences.edit();
+        editor.putInt(PreferenceConstants.COLOR_FG, mFgColor);
+        editor.putInt(PreferenceConstants.COLOR_BG, mBgColor);
+        editor.commit();
+        mColorGrid.invalidateViews();
+        return true;
+      }
+    });
+    return true;
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/util/EastAsianWidth.java b/ScriptingLayerForAndroid/src/org/connectbot/util/EastAsianWidth.java
new file mode 100644
index 0000000..b27cd48
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/util/EastAsianWidth.java
@@ -0,0 +1,58 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.util;
+
+import android.util.Log;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class EastAsianWidth {
+	public static boolean useJNI = false;
+	private static final String TAG = "ConnectBot.EastAsianWidth";
+
+	/**
+	 * @param charArray
+	 * @param i
+	 * @param position
+	 * @param wideAttribute
+	 * @param isLegacyEastAsian
+	 */
+	public native static void measure(char[] charArray, int start, int end,
+			byte[] wideAttribute, boolean isLegacyEastAsian);
+
+	static {
+		try {
+			System.loadLibrary("org_connectbot_util_EastAsianWidth");
+
+			char[] testInput = {(char)0x4EBA};
+			byte[] testResult = new byte[1];
+			measure(testInput, 0, 1, testResult, true);
+
+			if (testResult[0] == 1)
+				useJNI = true;
+			else
+				Log.d(TAG, "EastAsianWidth JNI measuring not available");
+		} catch (Exception e) {
+			// Failure
+		} catch (UnsatisfiedLinkError e1) {
+			// Failure
+		}
+	}
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/util/EncodingPreference.java b/ScriptingLayerForAndroid/src/org/connectbot/util/EncodingPreference.java
new file mode 100644
index 0000000..8a6d7aa
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/util/EncodingPreference.java
@@ -0,0 +1,37 @@
+package org.connectbot.util;
+
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+
+import java.nio.charset.Charset;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map.Entry;
+
+public class EncodingPreference extends ListPreference {
+
+  public EncodingPreference(Context context, AttributeSet attrs) {
+    super(context, attrs);
+
+    List<CharSequence> charsetIdsList = new LinkedList<CharSequence>();
+    List<CharSequence> charsetNamesList = new LinkedList<CharSequence>();
+
+    for (Entry<String, Charset> entry : Charset.availableCharsets().entrySet()) {
+      Charset c = entry.getValue();
+      if (c.canEncode() && c.isRegistered()) {
+        String key = entry.getKey();
+        if (key.startsWith("cp")) {
+          // Custom CP437 charset changes
+          charsetIdsList.add("CP437");
+          charsetNamesList.add("CP437");
+        }
+        charsetIdsList.add(entry.getKey());
+        charsetNamesList.add(c.displayName());
+      }
+    }
+
+    this.setEntryValues(charsetIdsList.toArray(new CharSequence[charsetIdsList.size()]));
+    this.setEntries(charsetNamesList.toArray(new CharSequence[charsetNamesList.size()]));
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/util/HelpTopicView.java b/ScriptingLayerForAndroid/src/org/connectbot/util/HelpTopicView.java
new file mode 100644
index 0000000..bf6f026
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/util/HelpTopicView.java
@@ -0,0 +1,63 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.util;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+
+import org.connectbot.HelpActivity;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class HelpTopicView extends WebView {
+  public HelpTopicView(Context context, AttributeSet attrs, int defStyle) {
+    super(context, attrs, defStyle);
+    initialize();
+  }
+
+  public HelpTopicView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    initialize();
+  }
+
+  public HelpTopicView(Context context) {
+    super(context);
+    initialize();
+  }
+
+  private void initialize() {
+    WebSettings wSet = getSettings();
+    //wSet.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
+    wSet.setUseWideViewPort(false);
+  }
+
+  public HelpTopicView setTopic(String topic) {
+    String path =
+        String.format("file:///android_asset/%s/%s%s", HelpActivity.HELPDIR, topic,
+            HelpActivity.SUFFIX);
+    loadUrl(path);
+
+    computeScroll();
+
+    return this;
+  }
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/util/PreferenceConstants.java b/ScriptingLayerForAndroid/src/org/connectbot/util/PreferenceConstants.java
new file mode 100644
index 0000000..d2c54d6
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/util/PreferenceConstants.java
@@ -0,0 +1,74 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.util;
+
+/**
+ * @author Kenny Root
+ * @author modified by raaar
+ */
+public class PreferenceConstants {
+
+  public static final String SCROLLBACK = "scrollback";
+  public static final String FONTSIZE = "fontsize";
+  public static final String KEYMODE = "keymode";
+  public static final String ENCODING = "encoding";
+
+  public static final String DELKEY = "delkey";
+  public static final String DELKEY_BACKSPACE = "backspace";
+  public static final String DELKEY_DEL = "del";
+
+  public static final String ROTATION = "rotation";
+
+  public static final String ROTATION_DEFAULT = "Default";
+  public static final String ROTATION_LANDSCAPE = "Force landscape";
+  public static final String ROTATION_PORTRAIT = "Force portrait";
+  public static final String ROTATION_AUTOMATIC = "Automatic";
+
+  public static final String FULLSCREEN = "fullscreen";
+
+  public static final String KEYMODE_RIGHT = "Use right-side keys";
+  public static final String KEYMODE_LEFT = "Use left-side keys";
+
+  public static final String CAMERA = "camera";
+
+  public static final String CAMERA_CTRLA_SPACE = "Ctrl+A then Space";
+  public static final String CAMERA_CTRLA = "Ctrl+A";
+  public static final String CAMERA_ESC = "Esc";
+  public static final String CAMERA_ESC_A = "Esc+A";
+
+  public static final String KEEP_ALIVE = "keepalive";
+
+  public static final String BUMPY_ARROWS = "bumpyarrows";
+  public static final String HIDE_KEYBOARD = "hidekeyboard";
+
+  public static final String SORT_BY_COLOR = "sortByColor";
+
+  public static final String BELL = "bell";
+  public static final String BELL_VOLUME = "bellVolume";
+  public static final String BELL_VIBRATE = "bellVibrate";
+  public static final float DEFAULT_BELL_VOLUME = 0.25f;
+
+  public final static int DEFAULT_FG_COLOR = 0xffcccccc;
+  public final static int DEFAULT_BG_COLOR = 0xff000000;
+  public final static int DEFAULT_SCROLLBACK = 140;
+  public final static float DEFAULT_FONT_SIZE = 10;
+
+  public final static String COLOR_FG = "color_fg";
+  public final static String COLOR_BG = "color_bg";
+
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/util/SelectionArea.java b/ScriptingLayerForAndroid/src/org/connectbot/util/SelectionArea.java
new file mode 100644
index 0000000..5735e55
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/util/SelectionArea.java
@@ -0,0 +1,201 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.util;
+
+import de.mud.terminal.VDUBuffer;
+
+/**
+ * @author Kenny Root
+ * Keep track of a selection area for the terminal copying mechanism.
+ * If the orientation is flipped one way, swap the bottom and top or
+ * left and right to keep it in the correct orientation.
+ */
+public class SelectionArea {
+	private int top;
+	private int bottom;
+	private int left;
+	private int right;
+	private int maxColumns;
+	private int maxRows;
+	private boolean selectingOrigin;
+
+	public SelectionArea() {
+		reset();
+	}
+
+	public final void reset() {
+		top = left = bottom = right = 0;
+		selectingOrigin = true;
+	}
+
+	/**
+	 * @param columns
+	 * @param rows
+	 */
+	public void setBounds(int columns, int rows) {
+		maxColumns = columns - 1;
+		maxRows = rows - 1;
+	}
+
+	private int checkBounds(int value, int max) {
+		if (value < 0)
+			return 0;
+		else if (value > max)
+			return max;
+		else
+			return value;
+	}
+
+	public boolean isSelectingOrigin() {
+		return selectingOrigin;
+	}
+
+	public void finishSelectingOrigin() {
+		selectingOrigin = false;
+	}
+
+	public void decrementRow() {
+		if (selectingOrigin)
+			setTop(top - 1);
+		else
+			setBottom(bottom - 1);
+	}
+
+	public void incrementRow() {
+		if (selectingOrigin)
+			setTop(top + 1);
+		else
+			setBottom(bottom + 1);
+	}
+
+	public void setRow(int row) {
+		if (selectingOrigin)
+			setTop(row);
+		else
+			setBottom(row);
+	}
+
+	private void setTop(int top) {
+		this.top = bottom = checkBounds(top, maxRows);
+	}
+
+	public int getTop() {
+		return Math.min(top, bottom);
+	}
+
+	private void setBottom(int bottom) {
+		this.bottom = checkBounds(bottom, maxRows);
+	}
+
+	public int getBottom() {
+		return Math.max(top, bottom);
+	}
+
+	public void decrementColumn() {
+		if (selectingOrigin)
+			setLeft(left - 1);
+		else
+			setRight(right - 1);
+	}
+
+	public void incrementColumn() {
+		if (selectingOrigin)
+			setLeft(left + 1);
+		else
+			setRight(right + 1);
+	}
+
+	public void setColumn(int column) {
+		if (selectingOrigin)
+			setLeft(column);
+		else
+			setRight(column);
+	}
+
+	private void setLeft(int left) {
+		this.left = right = checkBounds(left, maxColumns);
+	}
+
+	public int getLeft() {
+		return Math.min(left, right);
+	}
+
+	private void setRight(int right) {
+		this.right = checkBounds(right, maxColumns);
+	}
+
+	public int getRight() {
+		return Math.max(left, right);
+	}
+
+	public String copyFrom(VDUBuffer vb) {
+		int size = (getRight() - getLeft() + 1) * (getBottom() - getTop() + 1);
+
+		StringBuffer buffer = new StringBuffer(size);
+
+		for(int y = getTop(); y <= getBottom(); y++) {
+			int lastNonSpace = buffer.length();
+
+			for (int x = getLeft(); x <= getRight(); x++) {
+				// only copy printable chars
+				char c = vb.getChar(x, y);
+
+				if (!Character.isDefined(c) ||
+						(Character.isISOControl(c) && c != '\t'))
+					c = ' ';
+
+				if (c != ' ')
+					lastNonSpace = buffer.length();
+
+				buffer.append(c);
+			}
+
+			// Don't leave a bunch of spaces in our copy buffer.
+			if (buffer.length() > lastNonSpace)
+				buffer.delete(lastNonSpace + 1, buffer.length());
+
+			if (y != bottom)
+				buffer.append("\n");
+		}
+
+		return buffer.toString();
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder buffer = new StringBuilder();
+
+		buffer.append("SelectionArea[top=");
+		buffer.append(top);
+		buffer.append(", bottom=");
+		buffer.append(bottom);
+		buffer.append(", left=");
+		buffer.append(left);
+		buffer.append(", right=");
+		buffer.append(right);
+		buffer.append(", maxColumns=");
+		buffer.append(maxColumns);
+		buffer.append(", maxRows=");
+		buffer.append(maxRows);
+		buffer.append(", isSelectingOrigin=");
+		buffer.append(isSelectingOrigin());
+		buffer.append("]");
+
+		return buffer.toString();
+	}
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/util/UberColorPickerDialog.java b/ScriptingLayerForAndroid/src/org/connectbot/util/UberColorPickerDialog.java
new file mode 100644
index 0000000..e12307e
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/util/UberColorPickerDialog.java
@@ -0,0 +1,982 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.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.
+ */
+
+/*
+ * 090408
+ * Keith Wiley
+ * kwiley@keithwiley.com
+ * http://keithwiley.com
+ *
+ * UberColorPickerDialog v1.1
+ *
+ * This color picker was implemented as a (significant) extension of the
+ * ColorPickerDialog class provided in the Android API Demos.  You are free
+ * to drop it unchanged into your own projects or to modify it as you see
+ * fit.  I would appreciate it if this comment block were let intact,
+ * merely for credit's sake.
+ *
+ * Enjoy!
+ */
+
+package org.connectbot.util;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ComposeShader;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.SweepGradient;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.GradientDrawable.Orientation;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * UberColorPickerDialog is a seriously enhanced version of the UberColorPickerDialog
+ * class provided in the Android API Demos.<p>
+ *
+ * NOTE (from Kenny Root): This is a VERY slimmed down version custom for ConnectBot.
+ * Visit Keith's site for the full version at the URL listed in the author line.<p>
+ *
+ * @author Keith Wiley, kwiley@keithwiley.com, http://keithwiley.com
+ */
+public class UberColorPickerDialog extends Dialog {
+	private OnColorChangedListener mListener;
+	private int mInitialColor;
+
+	/**
+	 * Callback to the creator of the dialog, informing the creator of a new color and notifying that the dialog is about to dismiss.
+	 */
+	public interface OnColorChangedListener {
+		void colorChanged(int color);
+	}
+
+	/**
+	 * Ctor
+	 * @param context
+	 * @param listener
+	 * @param initialColor
+	 * @param showTitle If true, a title is shown across the top of the dialog.  If false a toast is shown instead.
+	 */
+	public UberColorPickerDialog(Context context,
+							OnColorChangedListener listener,
+							int initialColor) {
+		super(context);
+
+		mListener = listener;
+		mInitialColor = initialColor;
+	}
+
+	/**
+	 * Activity entry point
+	 */
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		OnColorChangedListener l = new OnColorChangedListener() {
+			public void colorChanged(int color) {
+				mListener.colorChanged(color);
+				dismiss();
+			}
+		};
+
+		DisplayMetrics dm = new DisplayMetrics();
+		getWindow().getWindowManager().getDefaultDisplay().getMetrics(dm);
+		int screenWidth = dm.widthPixels;
+		int screenHeight = dm.heightPixels;
+
+		setTitle("Pick a color (try the trackball)");
+
+		try {
+			setContentView(new ColorPickerView(getContext(), l, screenWidth, screenHeight, mInitialColor));
+		}
+		catch (Exception e) {
+			//There is currently only one kind of ctor exception, that where no methods are enabled.
+			dismiss();	//This doesn't work!  The dialog is still shown (its title at least, the layout is empty from the exception being thrown).  <sigh>
+		}
+	}
+
+	/**
+	 * ColorPickerView is the meat of this color picker (as opposed to the enclosing class).
+	 * All the heavy lifting is done directly by this View subclass.
+	 * <P>
+	 * You can enable/disable whichever color chooser methods you want by modifying the ENABLED_METHODS switches.  They *should*
+	 * do all the work required to properly enable/disable methods without losing track of what goes with what and what maps to what.
+	 * <P>
+	 * If you add a new color chooser method, do a text search for "NEW_METHOD_WORK_NEEDED_HERE".  That tag indicates all
+	 * the locations in the code that will have to be amended in order to properly add a new color chooser method.
+	 * I highly recommend adding new methods to the end of the list.  If you want to try to reorder the list, you're on your own.
+	 */
+	private static class ColorPickerView extends View {
+		private static int SWATCH_WIDTH = 95;
+		private static final int SWATCH_HEIGHT = 60;
+
+		private static int PALETTE_POS_X = 0;
+		private static int PALETTE_POS_Y = SWATCH_HEIGHT;
+		private static final int PALETTE_DIM = SWATCH_WIDTH * 2;
+		private static final int PALETTE_RADIUS = PALETTE_DIM / 2;
+		private static final int PALETTE_CENTER_X = PALETTE_RADIUS;
+		private static final int PALETTE_CENTER_Y = PALETTE_RADIUS;
+
+		private static final int SLIDER_THICKNESS = 40;
+
+		private static int VIEW_DIM_X = PALETTE_DIM;
+		private static int VIEW_DIM_Y = SWATCH_HEIGHT;
+
+		//NEW_METHOD_WORK_NEEDED_HERE
+		private static final int METHOD_HS_V_PALETTE = 0;
+
+		//NEW_METHOD_WORK_NEEDED_HERE
+		//Add a new entry to the list for each controller in the new method
+		private static final int TRACKED_NONE = -1;	//No object on screen is currently being tracked
+		private static final int TRACK_SWATCH_OLD = 10;
+		private static final int TRACK_SWATCH_NEW = 11;
+		private static final int TRACK_HS_PALETTE = 30;
+		private static final int TRACK_VER_VALUE_SLIDER = 31;
+
+		private static final int TEXT_SIZE = 12;
+		private static int[] TEXT_HSV_POS = new int[2];
+		private static int[] TEXT_RGB_POS = new int[2];
+		private static int[] TEXT_YUV_POS = new int[2];
+		private static int[] TEXT_HEX_POS = new int[2];
+
+		private static final float PI = 3.141592653589793f;
+
+		private int mMethod = METHOD_HS_V_PALETTE;
+		private int mTracking = TRACKED_NONE;	//What object on screen is currently being tracked for movement
+
+		//Zillions of persistant Paint objecs for drawing the View
+
+		private Paint mSwatchOld, mSwatchNew;
+
+		//NEW_METHOD_WORK_NEEDED_HERE
+		//Add Paints to represent the palettes of the new method's UI controllers
+		private Paint mOvalHueSat;
+
+		private Bitmap mVerSliderBM;
+		private Canvas mVerSliderCv;
+
+		private Bitmap[] mHorSlidersBM = new Bitmap[3];
+		private Canvas[] mHorSlidersCv = new Canvas[3];
+
+		private Paint mValDimmer;
+
+		//NEW_METHOD_WORK_NEEDED_HERE
+		//Add Paints to represent the icon for the new method
+		private Paint mOvalHueSatSmall;
+
+		private Paint mPosMarker;
+		private Paint mText;
+
+		private Rect mOldSwatchRect = new Rect();
+		private Rect mNewSwatchRect = new Rect();
+		private Rect mPaletteRect = new Rect();
+		private Rect mVerSliderRect = new Rect();
+
+		private int[] mSpectrumColorsRev;
+		private int mOriginalColor = 0;	//The color passed in at the beginning, which can be reverted to at any time by tapping the old swatch.
+		private float[] mHSV = new float[3];
+		private int[] mRGB = new int[3];
+		private float[] mYUV = new float[3];
+		private String mHexStr = "";
+		private boolean mHSVenabled = true;	//Only true if an HSV method is enabled
+		private boolean mRGBenabled = true;	//Only true if an RGB method is enabled
+		private boolean mYUVenabled = true;	//Only true if a YUV method is enabled
+		private boolean mHexenabled = true;	//Only true if an RGB method is enabled
+		private int[] mCoord = new int[3];		//For drawing slider/palette markers
+		private int mFocusedControl = -1;	//Which control receives trackball events.
+		private OnColorChangedListener mListener;
+
+		/**
+		 * Ctor.
+		 * @param c
+		 * @param l
+		 * @param width Used to determine orientation and adjust layout accordingly
+		 * @param height Used to determine orientation and adjust layout accordingly
+		 * @param color The initial color
+		 * @throws Exception
+		 */
+		ColorPickerView(Context c, OnColorChangedListener l, int width, int height, int color)
+		throws Exception {
+			super(c);
+
+			//We need to make the dialog focusable to retrieve trackball events.
+			setFocusable(true);
+
+			mListener = l;
+
+			mOriginalColor = color;
+
+			Color.colorToHSV(color, mHSV);
+
+			updateAllFromHSV();
+
+			//Setup the layout based on whether this is a portrait or landscape orientation.
+			if (width <= height) {	//Portrait layout
+				SWATCH_WIDTH = (PALETTE_DIM + SLIDER_THICKNESS) / 2;
+
+				PALETTE_POS_X = 0;
+				PALETTE_POS_Y = TEXT_SIZE * 4 + SWATCH_HEIGHT;
+
+				//Set more rects, lots of rects
+				mOldSwatchRect.set(0, TEXT_SIZE * 4, SWATCH_WIDTH, TEXT_SIZE * 4 + SWATCH_HEIGHT);
+				mNewSwatchRect.set(SWATCH_WIDTH, TEXT_SIZE * 4, SWATCH_WIDTH * 2, TEXT_SIZE * 4 + SWATCH_HEIGHT);
+				mPaletteRect.set(0, PALETTE_POS_Y, PALETTE_DIM, PALETTE_POS_Y + PALETTE_DIM);
+				mVerSliderRect.set(PALETTE_DIM, PALETTE_POS_Y, PALETTE_DIM + SLIDER_THICKNESS, PALETTE_POS_Y + PALETTE_DIM);
+
+				TEXT_HSV_POS[0] = 3;
+				TEXT_HSV_POS[1] = 0;
+				TEXT_RGB_POS[0] = TEXT_HSV_POS[0] + 50;
+				TEXT_RGB_POS[1] = TEXT_HSV_POS[1];
+				TEXT_YUV_POS[0] = TEXT_HSV_POS[0] + 100;
+				TEXT_YUV_POS[1] = TEXT_HSV_POS[1];
+				TEXT_HEX_POS[0] = TEXT_HSV_POS[0] + 150;
+				TEXT_HEX_POS[1] = TEXT_HSV_POS[1];
+
+				VIEW_DIM_X = PALETTE_DIM + SLIDER_THICKNESS;
+				VIEW_DIM_Y = SWATCH_HEIGHT + PALETTE_DIM + TEXT_SIZE * 4;
+			}
+			else {	//Landscape layout
+				SWATCH_WIDTH = 110;
+
+				PALETTE_POS_X = SWATCH_WIDTH;
+				PALETTE_POS_Y = 0;
+
+				//Set more rects, lots of rects
+				mOldSwatchRect.set(0, TEXT_SIZE * 7, SWATCH_WIDTH, TEXT_SIZE * 7 + SWATCH_HEIGHT);
+				mNewSwatchRect.set(0, TEXT_SIZE * 7 + SWATCH_HEIGHT, SWATCH_WIDTH, TEXT_SIZE * 7 + SWATCH_HEIGHT * 2);
+				mPaletteRect.set(SWATCH_WIDTH, PALETTE_POS_Y, SWATCH_WIDTH + PALETTE_DIM, PALETTE_POS_Y + PALETTE_DIM);
+				mVerSliderRect.set(SWATCH_WIDTH + PALETTE_DIM, PALETTE_POS_Y, SWATCH_WIDTH + PALETTE_DIM + SLIDER_THICKNESS, PALETTE_POS_Y + PALETTE_DIM);
+
+				TEXT_HSV_POS[0] = 3;
+				TEXT_HSV_POS[1] = 0;
+				TEXT_RGB_POS[0] = TEXT_HSV_POS[0];
+				TEXT_RGB_POS[1] = (int)(TEXT_HSV_POS[1] + TEXT_SIZE * 3.5);
+				TEXT_YUV_POS[0] = TEXT_HSV_POS[0] + 50;
+				TEXT_YUV_POS[1] = (int)(TEXT_HSV_POS[1] + TEXT_SIZE * 3.5);
+				TEXT_HEX_POS[0] = TEXT_HSV_POS[0] + 50;
+				TEXT_HEX_POS[1] = TEXT_HSV_POS[1];
+
+				VIEW_DIM_X = PALETTE_POS_X + PALETTE_DIM + SLIDER_THICKNESS;
+				VIEW_DIM_Y = Math.max(mNewSwatchRect.bottom, PALETTE_DIM);
+			}
+
+			//Rainbows make everybody happy!
+			mSpectrumColorsRev = new int[] {
+				0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF,
+				0xFF00FF00, 0xFFFFFF00, 0xFFFF0000,
+			};
+
+			//Setup all the Paint and Shader objects.  There are lots of them!
+
+			//NEW_METHOD_WORK_NEEDED_HERE
+			//Add Paints to represent the palettes of the new method's UI controllers
+
+			mSwatchOld = new Paint(Paint.ANTI_ALIAS_FLAG);
+			mSwatchOld.setStyle(Paint.Style.FILL);
+			mSwatchOld.setColor(Color.HSVToColor(mHSV));
+
+			mSwatchNew = new Paint(Paint.ANTI_ALIAS_FLAG);
+			mSwatchNew.setStyle(Paint.Style.FILL);
+			mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+			Shader shaderA = new SweepGradient(0, 0, mSpectrumColorsRev, null);
+			Shader shaderB = new RadialGradient(0, 0, PALETTE_CENTER_X, 0xFFFFFFFF, 0xFF000000, Shader.TileMode.CLAMP);
+			Shader shader = new ComposeShader(shaderA, shaderB, PorterDuff.Mode.SCREEN);
+			mOvalHueSat = new Paint(Paint.ANTI_ALIAS_FLAG);
+			mOvalHueSat.setShader(shader);
+			mOvalHueSat.setStyle(Paint.Style.FILL);
+			mOvalHueSat.setDither(true);
+
+			mVerSliderBM = Bitmap.createBitmap(SLIDER_THICKNESS, PALETTE_DIM, Bitmap.Config.RGB_565);
+			mVerSliderCv = new Canvas(mVerSliderBM);
+
+			for (int i = 0; i < 3; i++) {
+				mHorSlidersBM[i] = Bitmap.createBitmap(PALETTE_DIM, SLIDER_THICKNESS, Bitmap.Config.RGB_565);
+				mHorSlidersCv[i] = new Canvas(mHorSlidersBM[i]);
+			}
+
+			mValDimmer = new Paint(Paint.ANTI_ALIAS_FLAG);
+			mValDimmer.setStyle(Paint.Style.FILL);
+			mValDimmer.setDither(true);
+			mValDimmer.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
+
+			//Whew, we're done making the big Paints and Shaders for the swatches, palettes, and sliders.
+			//Now we need to make the Paints and Shaders that will draw the little method icons in the method selector list.
+
+			//NEW_METHOD_WORK_NEEDED_HERE
+			//Add Paints to represent the icon for the new method
+
+			shaderA = new SweepGradient(0, 0, mSpectrumColorsRev, null);
+			shaderB = new RadialGradient(0, 0, PALETTE_DIM / 2, 0xFFFFFFFF, 0xFF000000, Shader.TileMode.CLAMP);
+			shader = new ComposeShader(shaderA, shaderB, PorterDuff.Mode.SCREEN);
+			mOvalHueSatSmall = new Paint(Paint.ANTI_ALIAS_FLAG);
+			mOvalHueSatSmall.setShader(shader);
+			mOvalHueSatSmall.setStyle(Paint.Style.FILL);
+
+			//Make a simple stroking Paint for drawing markers and borders and stuff like that.
+			mPosMarker = new Paint(Paint.ANTI_ALIAS_FLAG);
+			mPosMarker.setStyle(Paint.Style.STROKE);
+			mPosMarker.setStrokeWidth(2);
+
+			//Make a basic text Paint.
+			mText = new Paint(Paint.ANTI_ALIAS_FLAG);
+			mText.setTextSize(TEXT_SIZE);
+			mText.setColor(Color.WHITE);
+
+			//Kickstart
+			initUI();
+		}
+
+		/**
+		 * Draw the entire view (the entire dialog).
+		 */
+		@Override
+		protected void onDraw(Canvas canvas) {
+			//Draw the old and new swatches
+			drawSwatches(canvas);
+
+			//Write the text
+			writeColorParams(canvas);
+
+			//Draw the palette and sliders (the UI)
+			if (mMethod == METHOD_HS_V_PALETTE)
+				drawHSV1Palette(canvas);
+		}
+
+		/**
+		 * Draw the old and new swatches.
+		 * @param canvas
+		 */
+		private void drawSwatches(Canvas canvas) {
+			float[] hsv = new float[3];
+
+			mText.setTextSize(16);
+
+			//Draw the original swatch
+			canvas.drawRect(mOldSwatchRect, mSwatchOld);
+			Color.colorToHSV(mOriginalColor, hsv);
+			//if (UberColorPickerDialog.isGray(mColor))	//Don't need this right here, but imp't to note
+			//	hsv[1] = 0;
+			if (hsv[2] > .5)
+				mText.setColor(Color.BLACK);
+			canvas.drawText("Revert", mOldSwatchRect.left + SWATCH_WIDTH / 2 - mText.measureText("Revert") / 2, mOldSwatchRect.top + 16, mText);
+			mText.setColor(Color.WHITE);
+
+			//Draw the new swatch
+			canvas.drawRect(mNewSwatchRect, mSwatchNew);
+			if (mHSV[2] > .5)
+				mText.setColor(Color.BLACK);
+			canvas.drawText("Accept", mNewSwatchRect.left + SWATCH_WIDTH / 2 - mText.measureText("Accept") / 2, mNewSwatchRect.top + 16, mText);
+			mText.setColor(Color.WHITE);
+
+			mText.setTextSize(TEXT_SIZE);
+		}
+
+		/**
+		 * Write the color parametes (HSV, RGB, YUV, Hex, etc.).
+		 * @param canvas
+		 */
+		private void writeColorParams(Canvas canvas) {
+			if (mHSVenabled) {
+				canvas.drawText("H: " + Integer.toString((int)(mHSV[0] / 360.0f * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE, mText);
+				canvas.drawText("S: " + Integer.toString((int)(mHSV[1] * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE * 2, mText);
+				canvas.drawText("V: " + Integer.toString((int)(mHSV[2] * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE * 3, mText);
+			}
+
+			if (mRGBenabled) {
+				canvas.drawText("R: " + mRGB[0], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE, mText);
+				canvas.drawText("G: " + mRGB[1], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE * 2, mText);
+				canvas.drawText("B: " + mRGB[2], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE * 3, mText);
+			}
+
+			if (mYUVenabled) {
+				canvas.drawText("Y: " + Integer.toString((int)(mYUV[0] * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE, mText);
+				canvas.drawText("U: " + Integer.toString((int)((mYUV[1] + .5f) * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE * 2, mText);
+				canvas.drawText("V: " + Integer.toString((int)((mYUV[2] + .5f) * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE * 3, mText);
+			}
+
+			if (mHexenabled)
+				canvas.drawText("#" + mHexStr, TEXT_HEX_POS[0], TEXT_HEX_POS[1] + TEXT_SIZE, mText);
+		}
+
+		/**
+		 * Place a small circle on the 2D palette to indicate the current values.
+		 * @param canvas
+		 * @param markerPosX
+		 * @param markerPosY
+		 */
+		private void mark2DPalette(Canvas canvas, int markerPosX, int markerPosY) {
+			mPosMarker.setColor(Color.BLACK);
+			canvas.drawOval(new RectF(markerPosX - 5, markerPosY - 5, markerPosX + 5, markerPosY + 5), mPosMarker);
+			mPosMarker.setColor(Color.WHITE);
+			canvas.drawOval(new RectF(markerPosX - 3, markerPosY - 3, markerPosX + 3, markerPosY + 3), mPosMarker);
+		}
+
+		/**
+		 * Draw a line across the slider to indicate its current value.
+		 * @param canvas
+		 * @param markerPos
+		 */
+		private void markVerSlider(Canvas canvas, int markerPos) {
+			mPosMarker.setColor(Color.BLACK);
+			canvas.drawRect(new Rect(0, markerPos - 2, SLIDER_THICKNESS, markerPos + 3), mPosMarker);
+			mPosMarker.setColor(Color.WHITE);
+			canvas.drawRect(new Rect(0, markerPos, SLIDER_THICKNESS, markerPos + 1), mPosMarker);
+		}
+
+		/**
+		 * Frame the slider to indicate that it has trackball focus.
+		 * @param canvas
+		 */
+		private void hilightFocusedVerSlider(Canvas canvas) {
+			mPosMarker.setColor(Color.WHITE);
+			canvas.drawRect(new Rect(0, 0, SLIDER_THICKNESS, PALETTE_DIM), mPosMarker);
+			mPosMarker.setColor(Color.BLACK);
+			canvas.drawRect(new Rect(2, 2, SLIDER_THICKNESS - 2, PALETTE_DIM - 2), mPosMarker);
+		}
+
+		/**
+		 * Frame the 2D palette to indicate that it has trackball focus.
+		 * @param canvas
+		 */
+		private void hilightFocusedOvalPalette(Canvas canvas) {
+			mPosMarker.setColor(Color.WHITE);
+			canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mPosMarker);
+			mPosMarker.setColor(Color.BLACK);
+			canvas.drawOval(new RectF(-PALETTE_RADIUS + 2, -PALETTE_RADIUS + 2, PALETTE_RADIUS - 2, PALETTE_RADIUS - 2), mPosMarker);
+		}
+
+		//NEW_METHOD_WORK_NEEDED_HERE
+		//To add a new method, replicate the basic draw functions here.  Use the 2D palette or 1D sliders as templates for the new method.
+		/**
+		 * Draw the UI for HSV with angular H and radial S combined in 2D and a 1D V slider.
+		 * @param canvas
+		 */
+		private void drawHSV1Palette(Canvas canvas) {
+			canvas.save();
+
+			canvas.translate(PALETTE_POS_X, PALETTE_POS_Y);
+
+			//Draw the 2D palette
+			canvas.translate(PALETTE_CENTER_X, PALETTE_CENTER_Y);
+			canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mOvalHueSat);
+			canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mValDimmer);
+			if (mFocusedControl == 0)
+				hilightFocusedOvalPalette(canvas);
+			mark2DPalette(canvas, mCoord[0], mCoord[1]);
+			canvas.translate(-PALETTE_CENTER_X, -PALETTE_CENTER_Y);
+
+			//Draw the 1D slider
+			canvas.translate(PALETTE_DIM, 0);
+			canvas.drawBitmap(mVerSliderBM, 0, 0, null);
+			if (mFocusedControl == 1)
+				hilightFocusedVerSlider(canvas);
+			markVerSlider(canvas, mCoord[2]);
+
+			canvas.restore();
+		}
+
+		/**
+		 * Initialize the current color chooser's UI (set its color parameters and set its palette and slider values accordingly).
+		 */
+		private void initUI() {
+			initHSV1Palette();
+
+			//Focus on the first controller (arbitrary).
+			mFocusedControl = 0;
+		}
+
+		//NEW_METHOD_WORK_NEEDED_HERE
+		//To add a new method, replicate and extend the last init function shown below
+		/**
+		 * Initialize a color chooser.
+		 */
+		private void initHSV1Palette() {
+			setOvalValDimmer();
+			setVerValSlider();
+
+			float angle = 2*PI - mHSV[0] / (180 / 3.1415927f);
+			float radius = mHSV[1] * PALETTE_RADIUS;
+			mCoord[0] = (int)(Math.cos(angle) * radius);
+			mCoord[1] = (int)(Math.sin(angle) * radius);
+
+			mCoord[2] = PALETTE_DIM - (int)(mHSV[2] * PALETTE_DIM);
+		}
+
+		//NEW_METHOD_WORK_NEEDED_HERE
+		//To add a new method, replicate and extend the set functions below, one per UI controller in the new method
+		/**
+		 * Adjust a Paint which, when painted, dims its underlying object to show the effects of varying value (brightness).
+		 */
+		private void setOvalValDimmer() {
+			float[] hsv = new float[3];
+			hsv[0] = mHSV[0];
+			hsv[1] = 0;
+			hsv[2] = mHSV[2];
+			int gray = Color.HSVToColor(hsv);
+			mValDimmer.setColor(gray);
+		}
+
+		/**
+		 * Create a linear gradient shader to show variations in value.
+		 */
+		private void setVerValSlider() {
+			float[] hsv = new float[3];
+			hsv[0] = mHSV[0];
+			hsv[1] = mHSV[1];
+			hsv[2] = 1;
+			int col = Color.HSVToColor(hsv);
+
+			int colors[] = new int[2];
+			colors[0] = col;
+			colors[1] = 0xFF000000;
+			GradientDrawable gradDraw = new GradientDrawable(Orientation.TOP_BOTTOM, colors);
+			gradDraw.setDither(true);
+			gradDraw.setLevel(10000);
+			gradDraw.setBounds(0, 0, SLIDER_THICKNESS, PALETTE_DIM);
+			gradDraw.draw(mVerSliderCv);
+		}
+
+		/**
+		 * Report the correct tightly bounded dimensions of the view.
+		 */
+		@Override
+		protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+			setMeasuredDimension(VIEW_DIM_X, VIEW_DIM_Y);
+		}
+
+		/**
+		 * Wrap Math.round().  I'm not a Java expert.  Is this the only way to avoid writing "(int)Math.round" everywhere?
+		 * @param x
+		 * @return
+		 */
+		private int round(double x) {
+			return (int)Math.round(x);
+		}
+
+		/**
+		 * Limit a value to the range [0,1].
+		 * @param n
+		 * @return
+		 */
+		private float pinToUnit(float n) {
+			if (n < 0) {
+				n = 0;
+			} else if (n > 1) {
+				n = 1;
+			}
+			return n;
+		}
+
+		/**
+		 * Limit a value to the range [0,max].
+		 * @param n
+		 * @param max
+		 * @return
+		 */
+		private float pin(float n, float max) {
+			if (n < 0) {
+				n = 0;
+			} else if (n > max) {
+				n = max;
+			}
+			return n;
+		}
+
+		/**
+		 * Limit a value to the range [min,max].
+		 * @param n
+		 * @param min
+		 * @param max
+		 * @return
+		 */
+		private float pin(float n, float min, float max) {
+			if (n < min) {
+				n = min;
+			} else if (n > max) {
+				n = max;
+			}
+			return n;
+		}
+
+		/**
+		 * No clue what this does (some sort of average/mean I presume).  It came with the original UberColorPickerDialog
+		 * in the API Demos and wasn't documented.  I don't feel like spending any time figuring it out, I haven't looked at it at all.
+		 * @param s
+		 * @param d
+		 * @param p
+		 * @return
+		 */
+		private int ave(int s, int d, float p) {
+			return s + round(p * (d - s));
+		}
+
+		/**
+		 * Came with the original UberColorPickerDialog in the API Demos, wasn't documented.  I believe it takes an array of
+		 * colors and a value in the range [0,1] and interpolates a resulting color in a seemingly predictable manner.
+		 * I haven't looked at it at all.
+		 * @param colors
+		 * @param unit
+		 * @return
+		 */
+		private int interpColor(int colors[], float unit) {
+			if (unit <= 0) {
+				return colors[0];
+			}
+			if (unit >= 1) {
+				return colors[colors.length - 1];
+			}
+
+			float p = unit * (colors.length - 1);
+			int i = (int)p;
+			p -= i;
+
+			// now p is just the fractional part [0...1) and i is the index
+			int c0 = colors[i];
+			int c1 = colors[i+1];
+			int a = ave(Color.alpha(c0), Color.alpha(c1), p);
+			int r = ave(Color.red(c0), Color.red(c1), p);
+			int g = ave(Color.green(c0), Color.green(c1), p);
+			int b = ave(Color.blue(c0), Color.blue(c1), p);
+
+			return Color.argb(a, r, g, b);
+		}
+
+		/**
+		 * A standard point-in-rect routine.
+		 * @param x
+		 * @param y
+		 * @param r
+		 * @return true if point x,y is in rect r
+		 */
+		public boolean ptInRect(int x, int y, Rect r) {
+			return x > r.left && x < r.right && y > r.top && y < r.bottom;
+		}
+
+		/**
+		 * Process trackball events.  Used mainly for fine-tuned color adjustment, or alternatively to switch between slider controls.
+		 */
+		@Override
+		public boolean dispatchTrackballEvent(MotionEvent event) {
+			float x = event.getX();
+			float y = event.getY();
+
+			//A longer event history implies faster trackball movement.
+			//Use it to infer a larger jump and therefore faster palette/slider adjustment.
+			int jump = event.getHistorySize() + 1;
+
+			switch (event.getAction()) {
+				case MotionEvent.ACTION_DOWN: {
+					}
+					break;
+				case MotionEvent.ACTION_MOVE: {
+						//NEW_METHOD_WORK_NEEDED_HERE
+						//To add a new method, replicate and extend the appropriate entry in this list,
+						//depending on whether you use 1D or 2D controllers
+						switch (mMethod) {
+							case METHOD_HS_V_PALETTE:
+								if (mFocusedControl == 0) {
+									changeHSPalette(x, y, jump);
+								}
+								else if (mFocusedControl == 1) {
+									if (y < 0)
+										changeSlider(mFocusedControl, true, jump);
+									else if (y > 0)
+										changeSlider(mFocusedControl, false, jump);
+								}
+								break;
+						}
+					}
+					break;
+				case MotionEvent.ACTION_UP: {
+					}
+					break;
+			}
+
+			return true;
+		}
+
+		//NEW_METHOD_WORK_NEEDED_HERE
+		//To add a new method, replicate and extend the appropriate functions below,
+		//one per UI controller in the new method
+		/**
+		 * Effect a trackball change to a 2D palette.
+		 * @param x -1: negative x change, 0: no x change, +1: positive x change.
+		 * @param y -1: negative y change, 0, no y change, +1: positive y change.
+		 * @param jump the amount by which to change.
+		 */
+		private void changeHSPalette(float x, float y, int jump) {
+			int x2 = 0, y2 = 0;
+			if (x < 0)
+				x2 = -jump;
+			else if (x > 0)
+				x2 = jump;
+			if (y < 0)
+				y2 = -jump;
+			else if (y > 0)
+				y2 = jump;
+
+		 	mCoord[0] += x2;
+		 	mCoord[1] += y2;
+
+		 	if (mCoord[0] < -PALETTE_RADIUS)
+		 		mCoord[0] = -PALETTE_RADIUS;
+		 	else if (mCoord[0] > PALETTE_RADIUS)
+		 		mCoord[0] = PALETTE_RADIUS;
+		 	if (mCoord[1] < -PALETTE_RADIUS)
+		 		mCoord[1] = -PALETTE_RADIUS;
+		 	else if (mCoord[1] > PALETTE_RADIUS)
+		 		mCoord[1] = PALETTE_RADIUS;
+
+			float radius = (float)java.lang.Math.sqrt(mCoord[0] * mCoord[0] + mCoord[1] * mCoord[1]);
+			if (radius > PALETTE_RADIUS)
+				radius = PALETTE_RADIUS;
+
+			float angle = (float)java.lang.Math.atan2(mCoord[1], mCoord[0]);
+			// need to turn angle [-PI ... PI] into unit [0....1]
+			float unit = angle/(2*PI);
+			if (unit < 0) {
+				unit += 1;
+			}
+
+			mCoord[0] = round(Math.cos(angle) * radius);
+			mCoord[1] = round(Math.sin(angle) * radius);
+
+			int c = interpColor(mSpectrumColorsRev, unit);
+			float[] hsv = new float[3];
+			Color.colorToHSV(c, hsv);
+			mHSV[0] = hsv[0];
+			mHSV[1] = radius / PALETTE_RADIUS;
+			updateAllFromHSV();
+			mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+			setVerValSlider();
+
+			invalidate();
+		}
+
+		/**
+		 * Effect a trackball change to a 1D slider.
+		 * @param slider id of the slider to be effected
+		 * @param increase true if the change is an increase, false if a decrease
+		 * @param jump the amount by which to change in units of the range [0,255]
+		 */
+		private void changeSlider(int slider, boolean increase, int jump) {
+			//NEW_METHOD_WORK_NEEDED_HERE
+			//It is only necessary to add an entry here for a new method if the new method uses a 1D slider.
+			//Note, some sliders are horizontal and others are vertical.
+			//They differ a bit, especially in a sign flip on the vertical axis.
+			if (mMethod == METHOD_HS_V_PALETTE) {
+				//slider *must* equal 1
+
+				mHSV[2] += (increase ? jump : -jump) / 256.0f;
+				mHSV[2] = pinToUnit(mHSV[2]);
+				updateAllFromHSV();
+				mCoord[2] = PALETTE_DIM - (int)(mHSV[2] * PALETTE_DIM);
+
+				mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+				setOvalValDimmer();
+
+				invalidate();
+			}
+		}
+
+		/**
+		 * Keep all colorspace representations in sync.
+		 */
+		private void updateRGBfromHSV() {
+			int color = Color.HSVToColor(mHSV);
+			mRGB[0] = Color.red(color);
+			mRGB[1] = Color.green(color);
+			mRGB[2] = Color.blue(color);
+		}
+
+		/**
+		 * Keep all colorspace representations in sync.
+		 */
+		private void updateYUVfromRGB() {
+			float r = mRGB[0] / 255.0f;
+			float g = mRGB[1] / 255.0f;
+			float b = mRGB[2] / 255.0f;
+
+			ColorMatrix cm = new ColorMatrix();
+			cm.setRGB2YUV();
+			final float[] a = cm.getArray();
+
+			mYUV[0] = a[0] * r + a[1] * g + a[2] * b;
+			mYUV[0] = pinToUnit(mYUV[0]);
+			mYUV[1] = a[5] * r + a[6] * g + a[7] * b;
+			mYUV[1] = pin(mYUV[1], -.5f, .5f);
+			mYUV[2] = a[10] * r + a[11] * g + a[12] * b;
+			mYUV[2] = pin(mYUV[2], -.5f, .5f);
+		}
+
+		/**
+		 * Keep all colorspace representations in sync.
+		 */
+		private void updateHexFromHSV() {
+			//For now, assume 100% opacity
+			mHexStr = Integer.toHexString(Color.HSVToColor(mHSV)).toUpperCase();
+			mHexStr = mHexStr.substring(2, mHexStr.length());
+		}
+
+		/**
+		 * Keep all colorspace representations in sync.
+		 */
+		private void updateAllFromHSV() {
+			//Update mRGB
+			if (mRGBenabled || mYUVenabled)
+				updateRGBfromHSV();
+
+			//Update mYUV
+			if (mYUVenabled)
+				updateYUVfromRGB();
+
+			//Update mHexStr
+			if (mRGBenabled)
+				updateHexFromHSV();
+		}
+
+		/**
+		 * Process touch events: down, move, and up
+		 */
+		@Override
+		public boolean onTouchEvent(MotionEvent event) {
+			float x = event.getX();
+			float y = event.getY();
+
+			//Generate coordinates which are palette=local with the origin at the upper left of the main 2D palette
+			int y2 = (int)(pin(round(y - PALETTE_POS_Y), PALETTE_DIM));
+
+			//Generate coordinates which are palette-local with the origin at the center of the main 2D palette
+			float circlePinnedX = x - PALETTE_POS_X - PALETTE_CENTER_X;
+			float circlePinnedY = y - PALETTE_POS_Y - PALETTE_CENTER_Y;
+
+			//Is the event in a swatch?
+			boolean inSwatchOld = ptInRect(round(x), round(y), mOldSwatchRect);
+			boolean inSwatchNew = ptInRect(round(x), round(y), mNewSwatchRect);
+
+			//Get the event's distance from the center of the main 2D palette
+			float radius = (float)java.lang.Math.sqrt(circlePinnedX * circlePinnedX + circlePinnedY * circlePinnedY);
+
+			//Is the event in a circle-pinned 2D palette?
+			boolean inOvalPalette = radius <= PALETTE_RADIUS;
+
+			//Pin the radius
+			if (radius > PALETTE_RADIUS)
+				radius = PALETTE_RADIUS;
+
+			//Is the event in a vertical slider to the right of the main 2D palette
+			boolean inVerSlider = ptInRect(round(x), round(y), mVerSliderRect);
+
+			switch (event.getAction()) {
+				case MotionEvent.ACTION_DOWN:
+					mTracking = TRACKED_NONE;
+
+					if (inSwatchOld)
+						mTracking = TRACK_SWATCH_OLD;
+					else if (inSwatchNew)
+						mTracking = TRACK_SWATCH_NEW;
+
+					//NEW_METHOD_WORK_NEEDED_HERE
+					//To add a new method, replicate and extend the last entry in this list
+					else if (mMethod == METHOD_HS_V_PALETTE) {
+						if (inOvalPalette) {
+							mTracking = TRACK_HS_PALETTE;
+							mFocusedControl = 0;
+						}
+						else if (inVerSlider) {
+							mTracking = TRACK_VER_VALUE_SLIDER;
+							mFocusedControl = 1;
+						}
+					}
+				case MotionEvent.ACTION_MOVE:
+					//NEW_METHOD_WORK_NEEDED_HERE
+					//To add a new method, replicate and extend the entries in this list,
+					//one per UI controller the new method requires.
+					if (mTracking == TRACK_HS_PALETTE) {
+						float angle = (float)java.lang.Math.atan2(circlePinnedY, circlePinnedX);
+						// need to turn angle [-PI ... PI] into unit [0....1]
+						float unit = angle/(2*PI);
+						if (unit < 0) {
+							unit += 1;
+						}
+
+						mCoord[0] = round(Math.cos(angle) * radius);
+						mCoord[1] = round(Math.sin(angle) * radius);
+
+						int c = interpColor(mSpectrumColorsRev, unit);
+						float[] hsv = new float[3];
+						Color.colorToHSV(c, hsv);
+						mHSV[0] = hsv[0];
+						mHSV[1] = radius / PALETTE_RADIUS;
+						updateAllFromHSV();
+						mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+						setVerValSlider();
+
+						invalidate();
+					}
+					else if (mTracking == TRACK_VER_VALUE_SLIDER) {
+						if (mCoord[2] != y2) {
+							mCoord[2] = y2;
+							float value = 1.0f - (float)y2 / (float)PALETTE_DIM;
+
+							mHSV[2] = value;
+							updateAllFromHSV();
+							mSwatchNew.setColor(Color.HSVToColor(mHSV));
+
+							setOvalValDimmer();
+
+							invalidate();
+						}
+					}
+					break;
+				case MotionEvent.ACTION_UP:
+					//NEW_METHOD_WORK_NEEDED_HERE
+					//To add a new method, replicate and extend the last entry in this list.
+					if (mTracking == TRACK_SWATCH_OLD && inSwatchOld) {
+						Color.colorToHSV(mOriginalColor, mHSV);
+						mSwatchNew.setColor(mOriginalColor);
+						initUI();
+						invalidate();
+					}
+					else if (mTracking == TRACK_SWATCH_NEW && inSwatchNew) {
+						mListener.colorChanged(mSwatchNew.getColor());
+						invalidate();
+					}
+
+					mTracking= TRACKED_NONE;
+					break;
+			}
+
+			return true;
+		}
+	}
+}
diff --git a/ScriptingLayerForAndroid/src/org/connectbot/util/VolumePreference.java b/ScriptingLayerForAndroid/src/org/connectbot/util/VolumePreference.java
new file mode 100644
index 0000000..2e7f61c
--- /dev/null
+++ b/ScriptingLayerForAndroid/src/org/connectbot/util/VolumePreference.java
@@ -0,0 +1,72 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.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.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 org.connectbot.util;
+
+import android.content.Context;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+/**
+ * @author kenny
+ *
+ */
+public class VolumePreference extends DialogPreference implements OnSeekBarChangeListener {
+	/**
+	 * @param context
+	 * @param attrs
+	 */
+	public VolumePreference(Context context, AttributeSet attrs) {
+		super(context, attrs);
+
+		setupLayout(context, attrs);
+	}
+
+	public VolumePreference(Context context, AttributeSet attrs, int defStyle) {
+		super(context, attrs, defStyle);
+
+		setupLayout(context, attrs);
+	}
+
+	private void setupLayout(Context context, AttributeSet attrs) {
+		setPersistent(true);
+	}
+
+	@Override
+	protected View onCreateDialogView() {
+		SeekBar sb = new SeekBar(getContext());
+
+		sb.setMax(100);
+		sb.setProgress((int)(getPersistedFloat(
+				PreferenceConstants.DEFAULT_BELL_VOLUME) * 100));
+		sb.setPadding(10, 10, 10, 10);
+		sb.setOnSeekBarChangeListener(this);
+
+		return sb;
+	}
+
+	public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+		persistFloat(progress / 100f);
+	}
+
+	public void onStartTrackingTouch(SeekBar seekBar) { }
+
+	public void onStopTrackingTouch(SeekBar seekBar) { }
+}
diff --git a/Utils/Android.mk b/Utils/Android.mk
new file mode 100644
index 0000000..5417389
--- /dev/null
+++ b/Utils/Android.mk
@@ -0,0 +1,27 @@
+#
+#  Copyright (C) 2016 Google, Inc.
+#
+#  Licensed under the Apache License, Version 2.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.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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+
+LOCAL_MODULE := sl4a.Utils
+LOCAL_MODULE_OWNER := google
+LOCAL_STATIC_JAVA_LIBRARIES := guava android-common
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/Utils/src/com/googlecode/android_scripting/ConvertUtils.java b/Utils/src/com/googlecode/android_scripting/ConvertUtils.java
new file mode 100644
index 0000000..3e6956d
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/ConvertUtils.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+public class ConvertUtils {
+    /**
+     * Converts a String of comma separated bytes to a byte array
+     *
+     * @param value The value to convert
+     * @return the byte array
+     */
+    public static byte[] convertStringToByteArray(String value) {
+        if (value.equals("")) {
+            return new byte[0];
+        }
+        String[] parseString = value.split(",");
+        byte[] byteArray = new byte[parseString.length];
+        if (byteArray.length > 0) {
+            for (int i = 0; i < parseString.length; i++) {
+                byte byteValue = Byte.valueOf(parseString[i].trim());
+                byteArray[i] = byteValue;
+            }
+        }
+        return byteArray;
+    }
+
+    /**
+     * Converts a byte array to a comma separated String
+     *
+     * @param byteArray
+     * @return comma separated string of bytes
+     */
+    public static String convertByteArrayToString(byte[] byteArray) {
+        String ret = "";
+        if (byteArray != null) {
+            for (int i = 0; i < byteArray.length; i++) {
+                if ((i + 1) != byteArray.length) {
+                    ret = ret + Byte.valueOf(byteArray[i]) + ",";
+                }
+                else {
+                    ret = ret + Byte.valueOf(byteArray[i]);
+                }
+            }
+        }
+        return ret;
+    }
+
+}
diff --git a/Utils/src/com/googlecode/android_scripting/FileUtils.java b/Utils/src/com/googlecode/android_scripting/FileUtils.java
new file mode 100644
index 0000000..91abc2c
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/FileUtils.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.os.Environment;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+
+/**
+ * Utility functions for handling files.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class FileUtils {
+
+  private FileUtils() {
+    // Utility class.
+  }
+
+  static public boolean externalStorageMounted() {
+    String state = Environment.getExternalStorageState();
+    return Environment.MEDIA_MOUNTED.equals(state)
+        || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
+  }
+
+  public static int chmod(File path, int mode) throws Exception {
+    Class<?> fileUtils = Class.forName("android.os.FileUtils");
+    Method setPermissions =
+        fileUtils.getMethod("setPermissions", String.class, int.class, int.class, int.class);
+    return (Integer) setPermissions.invoke(null, path.getAbsolutePath(), mode, -1, -1);
+  }
+
+  public static boolean recursiveChmod(File root, int mode) throws Exception {
+    boolean success = chmod(root, mode) == 0;
+    for (File path : root.listFiles()) {
+      if (path.isDirectory()) {
+        success = recursiveChmod(path, mode);
+      }
+      success &= (chmod(path, mode) == 0);
+    }
+    return success;
+  }
+
+  public static boolean delete(File path) {
+    boolean result = true;
+    if (path.exists()) {
+      if (path.isDirectory()) {
+        for (File child : path.listFiles()) {
+          result &= delete(child);
+        }
+        result &= path.delete(); // Delete empty directory.
+      }
+      if (path.isFile()) {
+        result &= path.delete();
+      }
+      if (!result) {
+        Log.e("Delete failed;");
+      }
+      return result;
+    } else {
+      Log.e("File does not exist.");
+      return false;
+    }
+  }
+
+  public static File copyFromStream(String name, InputStream input) {
+    if (name == null || name.length() == 0) {
+      Log.e("No script name specified.");
+      return null;
+    }
+    File file = new File(name);
+    if (!makeDirectories(file.getParentFile(), 0755)) {
+      return null;
+    }
+    try {
+      OutputStream output = new FileOutputStream(file);
+      IoUtils.copy(input, output);
+    } catch (Exception e) {
+      Log.e(e);
+      return null;
+    }
+    return file;
+  }
+
+  public static boolean makeDirectories(File directory, int mode) {
+    File parent = directory;
+    while (parent.getParentFile() != null && !parent.exists()) {
+      parent = parent.getParentFile();
+    }
+    if (!directory.exists()) {
+      Log.v("Creating directory: " + directory.getName());
+      if (!directory.mkdirs()) {
+        Log.e("Failed to create directory.");
+        return false;
+      }
+    }
+    try {
+      recursiveChmod(parent, mode);
+    } catch (Exception e) {
+      Log.e(e);
+      return false;
+    }
+    return true;
+  }
+
+  public static File getExternalDownload() {
+    try {
+      Class<?> c = Class.forName("android.os.Environment");
+      Method m = c.getDeclaredMethod("getExternalStoragePublicDirectory", String.class);
+      String download = c.getDeclaredField("DIRECTORY_DOWNLOADS").get(null).toString();
+      return (File) m.invoke(null, download);
+    } catch (Exception e) {
+      return new File(Environment.getExternalStorageDirectory(), "Download");
+    }
+  }
+
+  public static boolean rename(File file, String name) {
+    return file.renameTo(new File(file.getParent(), name));
+  }
+
+  public static String readToString(File file) throws IOException {
+    if (file == null || !file.exists()) {
+      return null;
+    }
+    FileReader reader = new FileReader(file);
+    StringBuilder out = new StringBuilder();
+    char[] buffer = new char[1024 * 4];
+    int numRead = 0;
+    while ((numRead = reader.read(buffer)) > -1) {
+      out.append(String.valueOf(buffer, 0, numRead));
+    }
+    reader.close();
+    return out.toString();
+  }
+
+  public static String readFromAssetsFile(Context context, String name) throws IOException {
+    AssetManager am = context.getAssets();
+    BufferedReader reader = new BufferedReader(new InputStreamReader(am.open(name)));
+    String line;
+    StringBuilder builder = new StringBuilder();
+    while ((line = reader.readLine()) != null) {
+      builder.append(line);
+    }
+    reader.close();
+    return builder.toString();
+  }
+
+}
diff --git a/Utils/src/com/googlecode/android_scripting/ForegroundService.java b/Utils/src/com/googlecode/android_scripting/ForegroundService.java
new file mode 100644
index 0000000..64fbe24
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/ForegroundService.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+
+import java.lang.reflect.Method;
+
+public abstract class ForegroundService extends Service {
+  private static final Class<?>[] mStartForegroundSignature =
+      new Class[] { int.class, Notification.class };
+  private static final Class<?>[] mStopForegroundSignature = new Class[] { boolean.class };
+
+  private final int mNotificationId;
+
+  private NotificationManager mNotificationManager;
+  private Method mStartForeground;
+  private Method mStopForeground;
+  private Object[] mStartForegroundArgs = new Object[2];
+  private Object[] mStopForegroundArgs = new Object[1];
+
+  public ForegroundService(int id) {
+    mNotificationId = id;
+  }
+
+  protected abstract Notification createNotification();
+
+  /**
+   * This is a wrapper around the new startForeground method, using the older APIs if it is not
+   * available.
+   */
+  private void startForegroundCompat(Notification notification) {
+    // If we have the new startForeground API, then use it.
+    if (mStartForeground != null) {
+      mStartForegroundArgs[0] = Integer.valueOf(mNotificationId);
+      mStartForegroundArgs[1] = notification;
+      try {
+        mStartForeground.invoke(this, mStartForegroundArgs);
+      } catch (Exception e) {
+        Log.e(e);
+      }
+      return;
+    }
+
+    // Fall back on the old API.
+    setForeground(true);
+    if (notification != null) {
+      mNotificationManager.notify(mNotificationId, notification);
+    }
+  }
+
+  /**
+   * This is a wrapper around the new stopForeground method, using the older APIs if it is not
+   * available.
+   */
+  private void stopForegroundCompat() {
+    // If we have the new stopForeground API, then use it.
+    if (mStopForeground != null) {
+      mStopForegroundArgs[0] = Boolean.TRUE;
+      try {
+        mStopForeground.invoke(this, mStopForegroundArgs);
+      } catch (Exception e) {
+        Log.e(e);
+      }
+      return;
+    }
+
+    // Fall back on the old API. Note to cancel BEFORE changing the
+    // foreground state, since we could be killed at that point.
+    mNotificationManager.cancel(mNotificationId);
+    setForeground(false);
+  }
+
+  @Override
+  public void onCreate() {
+    mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+    try {
+      mStartForeground = getClass().getMethod("startForeground", mStartForegroundSignature);
+      mStopForeground = getClass().getMethod("stopForeground", mStopForegroundSignature);
+    } catch (NoSuchMethodException e) {
+      // Running on an older platform.
+      mStartForeground = mStopForeground = null;
+    }
+    startForegroundCompat(createNotification());
+  }
+
+  @Override
+  public void onDestroy() {
+    // Make sure our notification is gone.
+    stopForegroundCompat();
+  }
+}
diff --git a/Utils/src/com/googlecode/android_scripting/IoUtils.java b/Utils/src/com/googlecode/android_scripting/IoUtils.java
new file mode 100644
index 0000000..0fd0ca0
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/IoUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class IoUtils {
+  private static final int BUFFER_SIZE = 1024 * 8;
+
+  private IoUtils() {
+    // Utility class.
+  }
+
+  public static int copy(InputStream input, OutputStream output) throws Exception, IOException {
+    byte[] buffer = new byte[BUFFER_SIZE];
+
+    BufferedInputStream in = new BufferedInputStream(input, BUFFER_SIZE);
+    BufferedOutputStream out = new BufferedOutputStream(output, BUFFER_SIZE);
+    int count = 0, n = 0;
+    try {
+      while ((n = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
+        out.write(buffer, 0, n);
+        count += n;
+      }
+      out.flush();
+    } finally {
+      try {
+        out.close();
+      } catch (IOException e) {
+        Log.e(e.getMessage(), e);
+      }
+      try {
+        in.close();
+      } catch (IOException e) {
+        Log.e(e.getMessage(), e);
+      }
+    }
+    return count;
+  }
+
+}
diff --git a/Utils/src/com/googlecode/android_scripting/Log.java b/Utils/src/com/googlecode/android_scripting/Log.java
new file mode 100644
index 0000000..4947a0c
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/Log.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.widget.Toast;
+
+public class Log {
+  private Log() {
+    // Utility class.
+  }
+
+  private static String getTag() {
+    StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
+    String fullClassName = stackTraceElements[4].getClassName();
+    String className = fullClassName.substring(fullClassName.lastIndexOf(".") + 1);
+    int lineNumber = stackTraceElements[4].getLineNumber();
+    return "sl4a." + className + ":" + lineNumber;
+  }
+
+  private static void toast(Context context, String message) {
+    Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+  }
+
+  public static void notify(Context context, String title, String contentTitle, String message) {
+    android.util.Log.v(getTag(), String.format("%s %s", contentTitle, message));
+
+    String packageName = context.getPackageName();
+    int iconId = context.getResources().getIdentifier("stat_sys_warning", "drawable", packageName);
+    NotificationManager notificationManager =
+        (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+    Notification.Builder builder = new Notification.Builder(context);
+    builder.setSmallIcon(iconId > 0 ? iconId : -1)
+           .setTicker(title)
+           .setWhen(0)
+           .setContentTitle(contentTitle)
+           .setContentText(message)
+           .setContentIntent(PendingIntent.getService(context, 0, null, 0));
+    Notification note = builder.build();
+    note.contentView.getLayoutId();
+    notificationManager.notify(NotificationIdFactory.create(), note);
+  }
+
+  public static void showDialog(final Context context, final String title, final String message) {
+    android.util.Log.v(getTag(), String.format("%s %s", title, message));
+
+    MainThread.run(context, new Runnable() {
+      @Override
+      public void run() {
+        AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        builder.setTitle(title);
+        builder.setMessage(message);
+
+        DialogInterface.OnClickListener buttonListener = new DialogInterface.OnClickListener() {
+          @Override
+          public void onClick(DialogInterface dialog, int which) {
+            dialog.dismiss();
+          }
+        };
+        builder.setPositiveButton("Ok", buttonListener);
+        builder.show();
+      }
+    });
+  }
+
+  public static void v(String message) {
+    android.util.Log.v(getTag(), message);
+  }
+
+  public static void v(String message, Throwable e) {
+    android.util.Log.v(getTag(), message, e);
+  }
+
+  public static void v(Context context, String message) {
+    toast(context, message);
+    android.util.Log.v(getTag(), message);
+  }
+
+  public static void v(Context context, String message, Throwable e) {
+    toast(context, message);
+    android.util.Log.v(getTag(), message, e);
+  }
+
+  public static void e(Throwable e) {
+    android.util.Log.e(getTag(), "Error", e);
+  }
+
+  public static void e(String message) {
+    android.util.Log.e(getTag(), message);
+  }
+
+  public static void e(String message, Throwable e) {
+    android.util.Log.e(getTag(), message, e);
+  }
+
+  public static void e(Context context, String message) {
+    toast(context, message);
+    android.util.Log.e(getTag(), message);
+  }
+
+  public static void e(Context context, String message, Throwable e) {
+    toast(context, message);
+    android.util.Log.e(getTag(), message, e);
+  }
+
+  public static void w(Throwable e) {
+    android.util.Log.w(getTag(), "Warning", e);
+  }
+
+  public static void w(String message) {
+    android.util.Log.w(getTag(), message);
+  }
+
+  public static void w(String message, Throwable e) {
+    android.util.Log.w(getTag(), message, e);
+  }
+
+  public static void w(Context context, String message) {
+    toast(context, message);
+    android.util.Log.w(getTag(), message);
+  }
+
+  public static void w(Context context, String message, Throwable e) {
+    toast(context, message);
+    android.util.Log.w(getTag(), message, e);
+  }
+
+  public static void d(String message) {
+    android.util.Log.d(getTag(), message);
+  }
+
+  public static void d(String message, Throwable e) {
+    android.util.Log.d(getTag(), message, e);
+  }
+
+  public static void d(Context context, String message) {
+    toast(context, message);
+    android.util.Log.d(getTag(), message);
+  }
+
+  public static void d(Context context, String message, Throwable e) {
+    toast(context, message);
+    android.util.Log.d(getTag(), message, e);
+  }
+
+  public static void i(String message) {
+    android.util.Log.i(getTag(), message);
+  }
+
+  public static void i(String message, Throwable e) {
+    android.util.Log.i(getTag(), message, e);
+  }
+
+  public static void i(Context context, String message) {
+    toast(context, message);
+    android.util.Log.i(getTag(), message);
+  }
+
+  public static void i(Context context, String message, Throwable e) {
+    toast(context, message);
+    android.util.Log.i(getTag(), message, e);
+  }
+}
diff --git a/Utils/src/com/googlecode/android_scripting/MainThread.java b/Utils/src/com/googlecode/android_scripting/MainThread.java
new file mode 100644
index 0000000..840c1e7
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/MainThread.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.googlecode.android_scripting.future.FutureResult;
+
+import java.util.concurrent.Callable;
+
+public class MainThread {
+
+  private MainThread() {
+    // Utility class.
+  }
+
+  /**
+   * Executed in the main thread, returns the result of an execution. Anything that runs here should
+   * finish quickly to avoid hanging the UI thread.
+   */
+  public static <T> T run(Context context, final Callable<T> task) {
+    final FutureResult<T> result = new FutureResult<T>();
+    Handler handler = new Handler(context.getMainLooper());
+    handler.post(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          result.set(task.call());
+        } catch (Exception e) {
+          Log.e(e);
+          result.set(null);
+        }
+      }
+    });
+    try {
+      return result.get();
+    } catch (InterruptedException e) {
+      Log.e(e);
+    }
+    return null;
+  }
+
+  public static void run(Context context, final Runnable task) {
+    Handler handler = new Handler(context.getMainLooper());
+    handler.post(new Runnable() {
+      @Override
+      public void run() {
+        task.run();
+      }
+    });
+  }
+}
diff --git a/Utils/src/com/googlecode/android_scripting/NotificationIdFactory.java b/Utils/src/com/googlecode/android_scripting/NotificationIdFactory.java
new file mode 100644
index 0000000..c87d94d
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/NotificationIdFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Creates unique ids to identify the notifications created by the android scripting service and the
+ * trigger service.
+ *
+ * @author Felix Arends (felix.arends@gmail.com)
+ *
+ */
+public final class NotificationIdFactory {
+  private static final AtomicInteger mNextId = new AtomicInteger(0);
+
+  public static int create() {
+    return mNextId.incrementAndGet();
+  }
+
+  private NotificationIdFactory() {
+  }
+}
diff --git a/Utils/src/com/googlecode/android_scripting/SimpleServer.java b/Utils/src/com/googlecode/android_scripting/SimpleServer.java
new file mode 100644
index 0000000..b93b6cb
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/SimpleServer.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import com.google.common.collect.Lists;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.BindException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+//import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
+
+/**
+ * A simple server.
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public abstract class SimpleServer {
+  private static int threadIndex = 0;
+  private final ConcurrentHashMap<Integer, ConnectionThread> mConnectionThreads =
+      new ConcurrentHashMap<Integer, ConnectionThread>();
+  private final List<SimpleServerObserver> mObservers = Lists.newArrayList();
+  private volatile boolean mStopServer = false;
+  private ServerSocket mServer;
+  private Thread mServerThread;
+
+  public interface SimpleServerObserver {
+    public void onConnect();
+    public void onDisconnect();
+  }
+
+  protected abstract void handleConnection(Socket socket) throws Exception;
+  protected abstract void handleRPCConnection(Socket socket,
+                                              Integer UID,
+                                              BufferedReader reader,
+                                              PrintWriter writer) throws Exception;
+
+  /** Adds an observer. */
+  public void addObserver(SimpleServerObserver observer) {
+    mObservers.add(observer);
+  }
+
+  /** Removes an observer. */
+  public void removeObserver(SimpleServerObserver observer) {
+    mObservers.remove(observer);
+  }
+
+  private void notifyOnConnect() {
+    for (SimpleServerObserver observer : mObservers) {
+      observer.onConnect();
+    }
+  }
+
+  private void notifyOnDisconnect() {
+    for (SimpleServerObserver observer : mObservers) {
+      observer.onDisconnect();
+    }
+  }
+
+  private final class ConnectionThread extends Thread {
+    private final Socket mmSocket;
+    private final BufferedReader reader;
+    private final PrintWriter writer;
+    private final Integer UID;
+    private final boolean isRpc;
+
+    private ConnectionThread(Socket socket, boolean rpc, Integer uid, BufferedReader reader, PrintWriter writer) {
+      setName("SimpleServer ConnectionThread " + getId());
+      mmSocket = socket;
+      this.UID = uid;
+      this.reader = reader;
+      this.writer = writer;
+      this.isRpc = rpc;
+    }
+
+    @Override
+    public void run() {
+      Log.v("Server thread " + getId() + " started.");
+      try {
+        if(isRpc) {
+          Log.d("Handling RPC connection in "+getId());
+          handleRPCConnection(mmSocket, UID, reader, writer);
+        }else{
+          Log.d("Handling Non-RPC connection in "+getId());
+          handleConnection(mmSocket);
+        }
+      } catch (Exception e) {
+        if (!mStopServer) {
+          Log.e("Server error.", e);
+        }
+      } finally {
+        close();
+        mConnectionThreads.remove(this.UID);
+        notifyOnDisconnect();
+        Log.v("Server thread " + getId() + " stopped.");
+      }
+    }
+
+    private void close() {
+      if (mmSocket != null) {
+        try {
+          mmSocket.close();
+        } catch (IOException e) {
+          Log.e(e.getMessage(), e);
+        }
+      }
+    }
+  }
+
+  /** Returns the number of active connections to this server. */
+  public int getNumberOfConnections() {
+    return mConnectionThreads.size();
+  }
+
+  public static InetAddress getPrivateInetAddress() throws UnknownHostException, SocketException {
+
+    InetAddress candidate = null;
+    Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
+    for (NetworkInterface netint : Collections.list(nets)) {
+      if (!netint.isLoopback() || !netint.isUp()) { // Ignore if localhost or not active
+        continue;
+      }
+      Enumeration<InetAddress> addresses = netint.getInetAddresses();
+      for (InetAddress address : Collections.list(addresses)) {
+        if (address instanceof Inet4Address) {
+          Log.d("local address " + address);
+          return address; // Prefer ipv4
+        }
+        candidate = address; // Probably an ipv6
+      }
+    }
+    if (candidate != null) {
+      return candidate; // return ipv6 address if no suitable ipv6
+    }
+    return InetAddress.getLocalHost(); // No damn matches. Give up, return local host.
+  }
+
+  public static InetAddress getPublicInetAddress() throws UnknownHostException, SocketException {
+
+    InetAddress candidate = null;
+    Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
+    for (NetworkInterface netint : Collections.list(nets)) {
+      if (netint.isLoopback() || !netint.isUp()) { // Ignore if localhost or not active
+        continue;
+      }
+      Enumeration<InetAddress> addresses = netint.getInetAddresses();
+      for (InetAddress address : Collections.list(addresses)) {
+        if (address instanceof Inet4Address) {
+          return address; // Prefer ipv4
+        }
+        candidate = address; // Probably an ipv6
+      }
+    }
+    if (candidate != null) {
+      return candidate; // return ipv6 address if no suitable ipv6
+    }
+    return InetAddress.getLocalHost(); // No damn matches. Give up, return local host.
+  }
+
+  /**
+   * Starts the RPC server bound to the localhost address.
+   *
+   * @param port
+   *          the port to bind to or 0 to pick any unused port
+   *
+   * @return the port that the server is bound to
+   * @throws IOException
+   */
+  public InetSocketAddress startLocal(int port) {
+    InetAddress address;
+    try {
+      // address = InetAddress.getLocalHost();
+      address = getPrivateInetAddress();
+      mServer = new ServerSocket(port, 5, address);
+    } catch (BindException e) {
+      Log.e("Port " + port + " already in use.");
+      try {
+        address = getPrivateInetAddress();
+        mServer = new ServerSocket(0, 5, address);
+      } catch (IOException e1) {
+        e1.printStackTrace();
+        return null;
+      }
+    } catch (Exception e) {
+      Log.e("Failed to start server.", e);
+      return null;
+    }
+    int boundPort = start();
+    return InetSocketAddress.createUnresolved(mServer.getInetAddress().getHostAddress(), boundPort);
+  }
+
+  /**
+   * data Starts the RPC server bound to the public facing address.
+   *
+   * @param port
+   *          the port to bind to or 0 to pick any unused port
+   *
+   * @return the port that the server is bound to
+   */
+  public InetSocketAddress startPublic(int port) {
+    InetAddress address;
+    try {
+      // address = getPublicInetAddress();
+      address = null;
+      mServer = new ServerSocket(port, 5 /* backlog */, address);
+    } catch (Exception e) {
+      Log.e("Failed to start server.", e);
+      return null;
+    }
+    int boundPort = start();
+    return InetSocketAddress.createUnresolved(mServer.getInetAddress().getHostAddress(), boundPort);
+  }
+
+  /**
+   * data Starts the RPC server bound to all interfaces
+   *
+   * @param port
+   *          the port to bind to or 0 to pick any unused port
+   *
+   * @return the port that the server is bound to
+   */
+  public InetSocketAddress startAllInterfaces(int port) {
+    try {
+      mServer = new ServerSocket(port, 5 /* backlog */);
+    } catch (Exception e) {
+      Log.e("Failed to start server.", e);
+      return null;
+    }
+    int boundPort = start();
+    return InetSocketAddress.createUnresolved(mServer.getInetAddress().getHostAddress(), boundPort);
+  }
+
+  private int start() {
+    mServerThread = new Thread() {
+      @Override
+      public void run() {
+        while (!mStopServer) {
+          try {
+            Socket sock = mServer.accept();
+            if (!mStopServer) {
+              startConnectionThread(sock);
+            } else {
+              sock.close();
+            }
+          } catch (IOException e) {
+            if (!mStopServer) {
+              Log.e("Failed to accept connection.", e);
+            }
+          } catch (JSONException e) {
+            if (!mStopServer) {
+              Log.e("Failed to parse request.", e);
+            }
+          }
+        }
+      }
+    };
+    mServerThread.start();
+    Log.v("Bound to " + mServer.getInetAddress());
+    return mServer.getLocalPort();
+  }
+
+  private void startConnectionThread(final Socket sock) throws IOException, JSONException {
+    BufferedReader reader =
+        new BufferedReader(new InputStreamReader(sock.getInputStream()), 8192);
+    PrintWriter writer = new PrintWriter(sock.getOutputStream(), true);
+    String data;
+    if((data = reader.readLine()) != null) {
+      Log.v("Received: " + data);
+      JSONObject request = new JSONObject(data);
+      if(request.has("cmd") && request.has("uid")) {
+        String cmd = request.getString("cmd");
+        int uid = request.getInt("uid");
+        JSONObject result = new JSONObject();
+        if(cmd.equals("initiate")) {
+          Log.d("Initiate a new session");
+          threadIndex += 1;
+          int mUID = threadIndex;
+          ConnectionThread networkThread = new ConnectionThread(sock,true,mUID,reader,writer);
+          mConnectionThreads.put(mUID, networkThread);
+          networkThread.start();
+          notifyOnConnect();
+          result.put("uid", mUID);
+          result.put("status",true);
+          result.put("error", null);
+        }else if(cmd.equals("continue")) {
+          Log.d("Continue an existing session");
+          Log.d("keys: "+mConnectionThreads.keySet().toString());
+          if(!mConnectionThreads.containsKey(uid)) {
+            result.put("uid", uid);
+            result.put("status",false);
+            result.put("error", "Session does not exist.");
+          }else{
+            ConnectionThread networkThread = new ConnectionThread(sock,true,uid,reader,writer);
+            mConnectionThreads.put(uid, networkThread);
+            networkThread.start();
+            notifyOnConnect();
+            result.put("uid", uid);
+            result.put("status",true);
+            result.put("error", null);
+          }
+        }else {
+          result.put("uid", uid);
+          result.put("status",false);
+          result.put("error", "Unrecognized command.");
+        }
+        writer.write(result + "\n");
+        writer.flush();
+        Log.v("Sent: " + result);
+      }else{
+        ConnectionThread networkThread = new ConnectionThread(sock,false,0,reader,writer);
+        mConnectionThreads.put(0, networkThread);
+        networkThread.start();
+        notifyOnConnect();
+      }
+    }
+  }
+
+  public void shutdown() {
+    // Stop listening on the server socket to ensure that
+    // beyond this point there are no incoming requests.
+    mStopServer = true;
+    try {
+      mServer.close();
+    } catch (IOException e) {
+      Log.e("Failed to close server socket.", e);
+    }
+    // Since the server is not running, the mNetworkThreads set can only
+    // shrink from this point onward. We can just stop all of the running helper
+    // threads. In the worst case, one of the running threads will already have
+    // shut down. Since this is a CopyOnWriteList, we don't have to worry about
+    // concurrency issues while iterating over the set of threads.
+    for (ConnectionThread connectionThread : mConnectionThreads.values()) {
+      connectionThread.close();
+    }
+    for (SimpleServerObserver observer : mObservers) {
+      removeObserver(observer);
+    }
+  }
+}
diff --git a/Utils/src/com/googlecode/android_scripting/SingleThreadExecutor.java b/Utils/src/com/googlecode/android_scripting/SingleThreadExecutor.java
new file mode 100644
index 0000000..7c5b1c5
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/SingleThreadExecutor.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class SingleThreadExecutor extends ThreadPoolExecutor {
+
+  public SingleThreadExecutor() {
+    super(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
+  }
+
+  @Override
+  protected void afterExecute(Runnable r, Throwable t) {
+    if (t != null) {
+      throw new RuntimeException(t);
+    }
+  }
+}
\ No newline at end of file
diff --git a/Utils/src/com/googlecode/android_scripting/exception/Sl4aException.java b/Utils/src/com/googlecode/android_scripting/exception/Sl4aException.java
new file mode 100644
index 0000000..2f85b4b
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/exception/Sl4aException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.exception;
+
+@SuppressWarnings("serial")
+public class Sl4aException extends Exception {
+
+  public Sl4aException(Exception e) {
+    super(e);
+  }
+
+  public Sl4aException(String message) {
+    super(message);
+  }
+
+  public Sl4aException(String message, Exception e) {
+    super(message, e);
+  }
+
+}
diff --git a/Utils/src/com/googlecode/android_scripting/future/FutureResult.java b/Utils/src/com/googlecode/android_scripting/future/FutureResult.java
new file mode 100644
index 0000000..573424c
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/future/FutureResult.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.future;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * FutureResult represents an eventual execution result for asynchronous operations.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ */
+public class FutureResult<T> implements Future<T> {
+
+    private final CountDownLatch mLatch = new CountDownLatch(1);
+    private volatile T mResult = null;
+
+    public void set(T result) {
+        mResult = result;
+        mLatch.countDown();
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        return false;
+    }
+
+    @Override
+    public T get() throws InterruptedException {
+        mLatch.await();
+        return mResult;
+    }
+
+    @Override
+    public T get(long timeout, TimeUnit unit) throws InterruptedException {
+        mLatch.await(timeout, unit);
+        return mResult;
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return false;
+    }
+
+    @Override
+    public boolean isDone() {
+        return mResult != null;
+    }
+
+}
diff --git a/Utils/src/com/googlecode/android_scripting/interpreter/ExternalClassLoader.java b/Utils/src/com/googlecode/android_scripting/interpreter/ExternalClassLoader.java
new file mode 100644
index 0000000..3bbe349
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/interpreter/ExternalClassLoader.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+import dalvik.system.DexClassLoader;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+public class ExternalClassLoader {
+
+  public Object load(Collection<String> dexPaths, Collection<String> nativePaths, String className)
+      throws Exception {
+    String dexOutputDir = "/sdcard/dexoutput";
+    String joinedDexPaths = join(dexPaths, ":");
+    String joinedNativeLibPaths = nativePaths != null ? join(nativePaths, ":") : null;
+    DexClassLoader loader =
+        new DexClassLoader(joinedDexPaths, dexOutputDir, joinedNativeLibPaths, this.getClass()
+            .getClassLoader());
+    Class<?> classToLoad = Class.forName(className, true, loader);
+    return classToLoad.newInstance();
+  }
+
+    private static String join(Collection<String> collection, String delimiter) {
+        StringBuffer buffer = new StringBuffer();
+        Iterator<String> iter = collection.iterator();
+        while (iter.hasNext()) {
+            buffer.append(iter.next());
+            if (iter.hasNext()) {
+                buffer.append(delimiter);
+            }
+        }
+        return buffer.toString();
+    }
+}
\ No newline at end of file
diff --git a/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterConstants.java b/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterConstants.java
new file mode 100644
index 0000000..cf4667a
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterConstants.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+import android.os.Environment;
+
+/**
+ * A collection of constants required for installation/removal of an interpreter.
+ *
+ * @author Damon Kohler (damonkohler@gmail.com)
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public interface InterpreterConstants {
+
+  public static final String SDCARD_ROOT =
+      Environment.getExternalStorageDirectory().getAbsolutePath() + "/";
+
+  public static final String SDCARD_SL4A_ROOT = SDCARD_ROOT + "sl4a/";
+
+  public static final String SCRIPTS_ROOT = SDCARD_SL4A_ROOT + "scripts/";
+
+  public static final String SDCARD_SL4A_DOC = SDCARD_SL4A_ROOT + "doc/";
+
+  public static final String SL4A_DALVIK_CACHE_ROOT = "/dalvik-cache/";
+
+  public static final String INTERPRETER_EXTRAS_ROOT = "/extras/";
+
+  // Interpreters discovery mechanism.
+  public static final String ACTION_DISCOVER_INTERPRETERS =
+      "com.googlecode.android_scripting.DISCOVER_INTERPRETERS";
+
+  // Interpreters broadcasts.
+  public static final String ACTION_INTERPRETER_ADDED =
+      "com.googlecode.android_scripting.INTERPRETER_ADDED";
+  public static final String ACTION_INTERPRETER_REMOVED =
+      "com.googlecode.android_scripting.INTERPRETER_REMOVED";
+
+  // Interpreter content provider.
+  public static final String PROVIDER_PROPERTIES = "com.googlecode.android_scripting.base";
+  public static final String PROVIDER_ENVIRONMENT_VARIABLES =
+      "com.googlecode.android_scripting.env";
+  public static final String PROVIDER_ARGUMENTS = "com.googlecode.android_scripting.args";
+
+  public static final String INSTALLED_PREFERENCE_KEY = "SL4A.interpreter.installed";
+
+  public static final String MIME = "script/";
+}
diff --git a/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterDescriptor.java b/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterDescriptor.java
new file mode 100644
index 0000000..519798c
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterDescriptor.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+import android.content.Context;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides interpreter-specific info for execution/installation/removal purposes.
+ *
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public interface InterpreterDescriptor {
+
+  /**
+   * Returns unique name of the interpreter.
+   */
+  public String getName();
+
+  /**
+   * Returns display name of the interpreter.
+   */
+  public String getNiceName();
+
+  /**
+   * Returns supported script-file extension.
+   */
+  public String getExtension();
+
+  /**
+   * Returns interpreter version number.
+   */
+  public int getVersion();
+
+  /**
+   * Returns the binary as a File object. Context is the InterpreterProvider's {@link Context} and
+   * is provided to find the interpreter installation directory.
+   */
+  public File getBinary(Context context);
+
+  /**
+   * Returns execution parameters in case when script name is not provided (when interpreter is
+   * started in a shell mode);
+   */
+  public String getInteractiveCommand(Context context);
+
+  /**
+   * Returns command line arguments to execute a with a given script (format string with one
+   * argument).
+   */
+  public String getScriptCommand(Context context);
+
+  /**
+   * Returns an array of command line arguments required to execute the interpreter (it's essential
+   * that the order in the array is consistent with order of arguments in the command line).
+   */
+  public List<String> getArguments(Context context);
+
+  /**
+   * Should return a map of environment variables names and their values (or null if interpreter
+   * does not require any environment variables).
+   */
+  public Map<String, String> getEnvironmentVariables(Context context);
+
+  /**
+   * Returns true if interpreter has an archive.
+   */
+  public boolean hasInterpreterArchive();
+
+  /**
+   * Returns true if interpreter has an extras archive.
+   */
+  public boolean hasExtrasArchive();
+
+  /**
+   * Returns true if interpreter comes with a scripts archive.
+   */
+  public boolean hasScriptsArchive();
+
+  /**
+   * Returns file name of the interpreter archive.
+   */
+  public String getInterpreterArchiveName();
+
+  /**
+   * Returns file name of the extras archive.
+   */
+  public String getExtrasArchiveName();
+
+  /**
+   * Returns file name of the scripts archive.
+   */
+  public String getScriptsArchiveName();
+
+  /**
+   * Returns URL location of the interpreter archive.
+   */
+  public String getInterpreterArchiveUrl();
+
+  /**
+   * Returns URL location of the scripts archive.
+   */
+  public String getScriptsArchiveUrl();
+
+  /**
+   * Returns URL location of the extras archive.
+   */
+  public String getExtrasArchiveUrl();
+
+  /**
+   * Returns true if interpreter can be executed in interactive mode.
+   */
+  public boolean hasInteractiveMode();
+}
diff --git a/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterPropertyNames.java b/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterPropertyNames.java
new file mode 100644
index 0000000..b8995a7
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterPropertyNames.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+/**
+ * A collection of {@link String} keys for querying an InterpreterProvider.
+ *
+ * @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
+ */
+public interface InterpreterPropertyNames {
+
+  /**
+   * Unique name of the interpreter.
+   */
+  public static final String NAME = "name";
+
+  /**
+   * Display name of the interpreter.
+   */
+  public static final String NICE_NAME = "niceName";
+
+  /**
+   * Supported script file extension.
+   */
+  public static final String EXTENSION = "extension";
+
+  /**
+   * Absolute path of the interpreter executable.
+   */
+  public static final String BINARY = "binary";
+
+  /**
+   * Final argument to interpreter binary when running the interpreter interactively.
+   */
+  public static final String INTERACTIVE_COMMAND = "interactiveCommand";
+
+  /**
+   * Final argument to interpreter binary when running a script.
+   */
+  public static final String SCRIPT_COMMAND = "scriptCommand";
+
+  /**
+   * Interpreter interactive mode flag.
+   */
+  public static final String HAS_INTERACTIVE_MODE = "hasInteractiveMode";
+
+}
diff --git a/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterUtils.java b/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterUtils.java
new file mode 100644
index 0000000..df81261
--- /dev/null
+++ b/Utils/src/com/googlecode/android_scripting/interpreter/InterpreterUtils.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.googlecode.android_scripting.interpreter;
+
+import android.content.Context;
+
+import java.io.File;
+
+public class InterpreterUtils {
+
+  private InterpreterUtils() {
+    // Utility class
+  }
+
+  public static File getInterpreterRoot(Context context) {
+    return context.getFilesDir();
+  }
+
+  public static File getInterpreterRoot(Context context, String interpreterName) {
+    return new File(getInterpreterRoot(context), interpreterName);
+  }
+}
diff --git a/Utils/src/com/trilead/ssh2/StreamGobbler.java b/Utils/src/com/trilead/ssh2/StreamGobbler.java
new file mode 100644
index 0000000..2d67337
--- /dev/null
+++ b/Utils/src/com/trilead/ssh2/StreamGobbler.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.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.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 com.trilead.ssh2;
+
+import com.googlecode.android_scripting.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A <code>StreamGobbler</code> is an InputStream that uses an internal worker thread to constantly
+ * consume input from another InputStream. It uses a buffer to store the consumed data. The buffer
+ * size is automatically adjusted, if needed.
+ * <p>
+ * This class is sometimes very convenient - if you wrap a session's STDOUT and STDERR InputStreams
+ * with instances of this class, then you don't have to bother about the shared window of STDOUT and
+ * STDERR in the low level SSH-2 protocol, since all arriving data will be immediatelly consumed by
+ * the worker threads. Also, as a side effect, the streams will be buffered (e.g., single byte
+ * read() operations are faster).
+ * <p>
+ * Other SSH for Java libraries include this functionality by default in their STDOUT and STDERR
+ * InputStream implementations, however, please be aware that this approach has also a downside:
+ * <p>
+ * If you do not call the StreamGobbler's <code>read()</code> method often enough and the peer is
+ * constantly sending huge amounts of data, then you will sooner or later encounter a low memory
+ * situation due to the aggregated data (well, it also depends on the Java heap size). Joe Average
+ * will like this class anyway - a paranoid programmer would never use such an approach.
+ * <p>
+ * The term "StreamGobbler" was taken from an article called "When Runtime.exec() won't", see
+ * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: StreamGobbler.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $
+ */
+
+public class StreamGobbler extends InputStream {
+  class GobblerThread extends Thread {
+    @Override
+    public void run() {
+
+      while (true) {
+        try {
+          byte[] saveBuffer = null;
+
+          int avail = is.read(buffer, write_pos, buffer.length - write_pos);
+
+          synchronized (synchronizer) {
+            if (avail <= 0) {
+              isEOF = true;
+              synchronizer.notifyAll();
+              break;
+            }
+            write_pos += avail;
+
+            int space_available = buffer.length - write_pos;
+
+            if (space_available == 0) {
+              if (read_pos > 0) {
+                saveBuffer = new byte[read_pos];
+                System.arraycopy(buffer, 0, saveBuffer, 0, read_pos);
+                System.arraycopy(buffer, read_pos, buffer, 0, buffer.length - read_pos);
+                write_pos -= read_pos;
+                read_pos = 0;
+              } else {
+                write_pos = 0;
+                saveBuffer = buffer;
+              }
+            }
+
+            synchronizer.notifyAll();
+          }
+
+          writeToFile(saveBuffer);
+
+        } catch (IOException e) {
+          synchronized (synchronizer) {
+            exception = e;
+            synchronizer.notifyAll();
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  private InputStream is;
+  private GobblerThread t;
+
+  private Object synchronizer = new Object();
+
+  private boolean isEOF = false;
+  private boolean isClosed = false;
+  private IOException exception = null;
+
+  private byte[] buffer;
+  private int read_pos = 0;
+  private int write_pos = 0;
+  private final FileOutputStream mLogStream;
+  private final int mBufferSize;
+
+  public StreamGobbler(InputStream is, File log, int buffer_size) {
+    this.is = is;
+    mBufferSize = buffer_size;
+    FileOutputStream out = null;
+    try {
+      out = new FileOutputStream(log, false);
+    } catch (IOException e) {
+      Log.e(e);
+    }
+    mLogStream = out;
+    buffer = new byte[mBufferSize];
+    t = new GobblerThread();
+    t.setDaemon(true);
+    t.start();
+  }
+
+  public void writeToFile(byte[] buffer) {
+    if (mLogStream != null && buffer != null) {
+      try {
+        mLogStream.write(buffer);
+      } catch (IOException e) {
+        Log.e(e);
+      }
+    }
+  }
+
+  @Override
+  public int read() throws IOException {
+    synchronized (synchronizer) {
+      if (isClosed) {
+        throw new IOException("This StreamGobbler is closed.");
+      }
+
+      while (read_pos == write_pos) {
+        if (exception != null) {
+          throw exception;
+        }
+
+        if (isEOF) {
+          return -1;
+        }
+
+        try {
+          synchronizer.wait();
+        } catch (InterruptedException e) {
+        }
+      }
+
+      int b = buffer[read_pos++] & 0xff;
+
+      return b;
+    }
+  }
+
+  @Override
+  public int available() throws IOException {
+    synchronized (synchronizer) {
+      if (isClosed) {
+        throw new IOException("This StreamGobbler is closed.");
+      }
+
+      return write_pos - read_pos;
+    }
+  }
+
+  @Override
+  public int read(byte[] b) throws IOException {
+    return read(b, 0, b.length);
+  }
+
+  @Override
+  public void close() throws IOException {
+    synchronized (synchronizer) {
+      if (isClosed) {
+        return;
+      }
+      isClosed = true;
+      isEOF = true;
+      synchronizer.notifyAll();
+      is.close();
+    }
+  }
+
+  @Override
+  public int read(byte[] b, int off, int len) throws IOException {
+    if (b == null) {
+      throw new NullPointerException();
+    }
+
+    if ((off < 0) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0) || (off > b.length)) {
+      throw new IndexOutOfBoundsException();
+    }
+
+    if (len == 0) {
+      return 0;
+    }
+
+    synchronized (synchronizer) {
+      if (isClosed) {
+        throw new IOException("This StreamGobbler is closed.");
+      }
+
+      while (read_pos == write_pos) {
+        if (exception != null) {
+          throw exception;
+        }
+
+        if (isEOF) {
+          return -1;
+        }
+
+        try {
+          synchronizer.wait();
+        } catch (InterruptedException e) {
+        }
+      }
+
+      int avail = write_pos - read_pos;
+
+      avail = (avail > len) ? len : avail;
+
+      System.arraycopy(buffer, read_pos, b, off, avail);
+
+      read_pos += avail;
+
+      return avail;
+    }
+  }
+}