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 & 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;
+ }
+ }
+}