aboutsummaryrefslogtreecommitdiff
path: root/third_party
diff options
context:
space:
mode:
Diffstat (limited to 'third_party')
-rw-r--r--third_party/sl4a/LICENSE154
-rw-r--r--third_party/sl4a/README.google22
-rw-r--r--third_party/sl4a/build.gradle163
-rw-r--r--third_party/sl4a/gradle.properties6
-rw-r--r--third_party/sl4a/src/main/AndroidManifest.xml4
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Snippet.java23
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java39
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetRunner.java199
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventCache.java103
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventSnippet.java87
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/SnippetEvent.java92
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.java60
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java290
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java65
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java37
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java49
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java201
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java79
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java121
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonSerializable.java24
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java252
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java36
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java25
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java29
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java36
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java285
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/schedulerpc/ScheduleRpcSnippet.java42
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java199
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/EmptyTestClass.java28
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java146
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java90
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.java33
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/RpcUtil.java139
-rw-r--r--third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java33
34 files changed, 3191 insertions, 0 deletions
diff --git a/third_party/sl4a/LICENSE b/third_party/sl4a/LICENSE
new file mode 100644
index 0000000..a095299
--- /dev/null
+++ b/third_party/sl4a/LICENSE
@@ -0,0 +1,154 @@
+Copied from http://www.apache.org/licenses/LICENSE-2.0:
+
+Apache License
+
+Version 2.0, January 2004
+
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of this
+License, each Contributor hereby grants to You a perpetual, worldwide, non-
+exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce,
+prepare Derivative Works of, publicly display, publicly perform, sublicense, and
+distribute the Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this License,
+each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-
+charge, royalty-free, irrevocable (except as stated in this section) patent
+license to make, have made, use, offer to sell, sell, import, and otherwise
+transfer the Work, where such license applies only to those patent claims
+licensable by such Contributor that are necessarily infringed by their
+Contribution(s) alone or by combination of their Contribution(s) with the Work
+to which such Contribution(s) was submitted. If You institute patent litigation
+against any entity (including a cross-claim or counterclaim in a lawsuit)
+alleging that the Work or a Contribution incorporated within the Work
+constitutes direct or contributory patent infringement, then any patent licenses
+granted to You under this License for that Work shall terminate as of the date
+such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or
+Derivative Works thereof in any medium, with or without modifications, and in
+Source or Object form, provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and You must cause any modified files to carry prominent notices
+stating that You changed the files; and You must retain, in the Source form of
+any Derivative Works that You distribute, all copyright, patent, trademark, and
+attribution notices from the Source form of the Work, excluding those notices
+that do not pertain to any part of the Derivative Works; and If the Work
+includes a "NOTICE" text file as part of its distribution, then any Derivative
+Works that You distribute must include a readable copy of the attribution
+notices contained within such NOTICE file, excluding those notices that do not
+pertain to any part of the Derivative Works, in at least one of the following
+places: within a NOTICE text file distributed as part of the Derivative Works;
+within the Source form or documentation, if provided along with the Derivative
+Works; or, within a display generated by the Derivative Works, if and wherever
+such third-party notices normally appear. The contents of the NOTICE file are
+for informational purposes only and do not modify the License. You may add Your
+own attribution notices within Derivative Works that You distribute, alongside
+or as an addendum to the NOTICE text from the Work, provided that such
+additional attribution notices cannot be construed as modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise, any
+Contribution intentionally submitted for inclusion in the Work by You to the
+Licensor shall be under the terms and conditions of this License, without any
+additional terms or conditions. Notwithstanding the above, nothing herein shall
+supersede or modify the terms of any separate license agreement you may have
+executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names,
+trademarks, service marks, or product names of the Licensor, except as required
+for reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
+writing, Licensor provides the Work (and each Contributor provides its
+Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied, including, without limitation, any warranties
+or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Work and assume any risks
+associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether in
+tort (including negligence), contract, or otherwise, unless required by
+applicable law (such as deliberate and grossly negligent acts) or agreed to in
+writing, shall any Contributor be liable to You for damages, including any
+direct, indirect, special, incidental, or consequential damages of any character
+arising as a result of this License or out of the use or inability to use the
+Work (including but not limited to damages for loss of goodwill, work stoppage,
+computer failure or malfunction, or any and all other commercial damages or
+losses), even if such Contributor has been advised of the possibility of such
+damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work or
+Derivative Works thereof, You may choose to offer, and charge a fee for,
+acceptance of support, warranty, indemnity, or other liability obligations
+and/or rights consistent with this License. However, in accepting such
+obligations, You may act only on Your own behalf and on Your sole
+responsibility, not on behalf of any other Contributor, and only if You agree to
+indemnify, defend, and hold each Contributor harmless for any liability incurred
+by, or claims asserted against, such Contributor by reason of your accepting any
+such warranty or additional liability.
diff --git a/third_party/sl4a/README.google b/third_party/sl4a/README.google
new file mode 100644
index 0000000..1efc19f
--- /dev/null
+++ b/third_party/sl4a/README.google
@@ -0,0 +1,22 @@
+URL: https://android.googlesource.com/platform/external/sl4a/+archive/f0a094f3709187319222e64d8434f168ad8ffac9.tar.gz
+Version: f0a094f3709187319222e64d8434f168ad8ffac9
+License: Apache 2.0
+License File: LICENSE
+
+Description:
+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.
+
+This version of sl4a is heavily modified to act as an RPC library for
+third-party facades (called code snippets). It no longer has much in common with
+the original sl4a project.
+
+
+Local Modifications:
+- LICENSE file has been created for compliance purposes. Not included in
+ original distribution.
+- This version of sl4a has been extensively refactored to remove all Android
+ facades and scripting interpreters. Only the RPC layer and facade registration
+ engine remain. This is to facilitate its use as a library in user projects.
diff --git a/third_party/sl4a/build.gradle b/third_party/sl4a/build.gradle
new file mode 100644
index 0000000..48446b4
--- /dev/null
+++ b/third_party/sl4a/build.gradle
@@ -0,0 +1,163 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ google()
+ }
+}
+
+plugins {
+ id 'com.github.sherter.google-java-format' version '0.9'
+ id 'maven-publish'
+ id 'signing'
+}
+
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode VERSION_CODE.toInteger()
+ versionName VERSION_NAME
+
+ // Need to set up some project properties to publish to bintray.
+ project.group = GROUP_ID
+ project.archivesBaseName = ARTIFACT_ID
+ project.version = VERSION_NAME
+ }
+
+ splits {
+ abi {
+ enable true
+ reset()
+ // Specifies a list of ABIs that Gradle should create APKs for.
+ include "arm64-v8a", "armeabi-v7a", "armeabi"
+ universalApk true
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardwareIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation 'junit:junit:4.13.2'
+ implementation 'androidx.test:runner:1.4.0'
+}
+
+googleJavaFormat {
+ options style: 'AOSP'
+}
+
+task sourcesJar(type: Jar) {
+ from android.sourceSets.main.java.srcDirs
+ classifier = 'sources'
+}
+
+task javadoc(type: Javadoc) {
+ source = android.sourceSets.main.java.srcDirs
+ classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+ options.addStringOption('Xdoclint:none', '-quiet')
+ options.addStringOption('encoding', 'UTF-8')
+ options.addStringOption('charSet', 'UTF-8')
+ failOnError false
+}
+
+task javadocJar(type: Jar, dependsOn: javadoc) {
+ classifier = 'javadoc'
+ from javadoc.destinationDir
+}
+
+artifacts {
+ archives javadocJar
+ archives sourcesJar
+}
+
+
+afterEvaluate {
+ publishing {
+ publications {
+ release(MavenPublication) {
+ groupId GROUP_ID
+ artifactId ARTIFACT_ID
+ version VERSION_NAME
+ from components.release
+
+ artifact sourcesJar
+ artifact javadocJar
+
+ pom {
+ name = ARTIFACT_ID
+ description = 'Android library for triggering device-side ' +
+ 'code from host-side Mobly tests.'
+ url = 'https://github.com/google/mobly-snippet-lib'
+ licenses {
+ license {
+ name = 'The Apache Software License, Version 2.0'
+ url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution = 'repo'
+ }
+ }
+ developers {
+ developer {
+ name = 'The Mobly Team'
+ }
+ }
+ scm {
+ connection = 'https://github.com/google/mobly-snippet-lib.git'
+ url = 'https://github.com/google/mobly-snippet-lib'
+ }
+ }
+ }
+ }
+
+ repositories {
+ maven {
+ def releasesRepoUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/'
+ def snapshotsRepoUrl = 'https://oss.sonatype.org/content/repositories/snapshots/'
+ url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
+ credentials {
+ username ossrhUsername
+ password ossrhPassword
+ }
+ }
+ }
+ }
+ signing {
+ sign publishing.publications.release
+ }
+}
+
+// Open lint's HTML report in your default browser or viewer.
+task openLintReport(type: Exec) {
+ def lint_report = "build/reports/lint-results-debug.html"
+ def cmd = "cat"
+ def platform = System.getProperty('os.name').toLowerCase(Locale.ROOT)
+ if (platform.contains("linux")) {
+ cmd = "xdg-open"
+ } else if (platform.contains("mac os x")) {
+ cmd = "open"
+ } else if (platform.contains("windows")) {
+ cmd = "launch"
+ }
+ commandLine cmd, lint_report
+}
+
+task presubmit {
+ dependsOn { ['googleJavaFormat', 'lint', 'openLintReport'] }
+ doLast {
+ println "Fix any lint issues you see. When it looks good, submit the pull request."
+ }
+}
+
diff --git a/third_party/sl4a/gradle.properties b/third_party/sl4a/gradle.properties
new file mode 100644
index 0000000..86f8068
--- /dev/null
+++ b/third_party/sl4a/gradle.properties
@@ -0,0 +1,6 @@
+# This version code implements the versioning recommendations in:
+# https://blog.jayway.com/2015/03/11/automatic-versioncode-generation-in-android-gradle/
+VERSION_CODE=1030199
+VERSION_NAME=1.3.1
+GROUP_ID=com.google.android.mobly
+ARTIFACT_ID=mobly-snippet-lib
diff --git a/third_party/sl4a/src/main/AndroidManifest.xml b/third_party/sl4a/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5b6c4cf
--- /dev/null
+++ b/third_party/sl4a/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet">
+ <uses-permission android:name="android.permission.INTERNET" />
+</manifest>
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Snippet.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Snippet.java
new file mode 100644
index 0000000..646a3d9
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/Snippet.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet;
+
+public interface Snippet {
+ /** Invoked when the receiver is shut down. */
+ default void shutdown() throws Exception {}
+ ;
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java
new file mode 100644
index 0000000..b9a101b
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetObjectConverter.java
@@ -0,0 +1,39 @@
+package com.google.android.mobly.snippet;
+
+import java.lang.reflect.Type;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Interface for a converter that serializes and de-serializes objects.
+ *
+ * <p>Classes implementing this interface are meant to provide custom serialization/de-serialization
+ * logic for complex types.
+ *
+ * <p>Serialization here means converting a Java object to {@link JSONObject}, which can be
+ * transported over Snippet's Rpc protocol. De-serialization is this process in reverse.
+ */
+public interface SnippetObjectConverter {
+ /**
+ * Serializes a complex type object to {@link JSONObject}.
+ *
+ * <p>Return null to signify the complex type is not supported.
+ *
+ * @param object The object to convert to "serialize".
+ * @return A JSONObject representation of the input object, or `null` if the input object type
+ * is not supported.
+ * @throws JSONException
+ */
+ JSONObject serialize(Object object) throws JSONException;
+
+ /**
+ * Deserializes a {@link JSONObject} to a Java complex type object.
+ *
+ * @param jsonObject A {@link JSONObject} passed from the Rpc client.
+ * @param type The expected {@link Type} of the Java object.
+ * @return A Java object of the specified {@link Type}, or `null` if the {@link Type} is not
+ * supported.
+ * @throws JSONException
+ */
+ Object deserialize(JSONObject jsonObject, Type type) throws JSONException;
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetRunner.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetRunner.java
new file mode 100644
index 0000000..36d1348
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/SnippetRunner.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.mobly.snippet;
+
+import android.app.Instrumentation;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import androidx.test.runner.AndroidJUnitRunner;
+import com.google.android.mobly.snippet.rpc.AndroidProxy;
+import com.google.android.mobly.snippet.util.EmptyTestClass;
+import com.google.android.mobly.snippet.util.Log;
+import com.google.android.mobly.snippet.util.NotificationIdFactory;
+import java.io.IOException;
+import java.net.SocketException;
+import java.util.Locale;
+
+/**
+ * A launcher that starts the snippet server as an instrumentation so that it has access to the
+ * target app's context.
+ *
+ * <p>We have to extend some subclass of {@link androidx.test.runner.AndroidJUnitRunner} because
+ * snippets are launched with 'am instrument', and snippet APKs need to access {@link
+ * androidx.test.platform.app.InstrumentationRegistry}.
+ *
+ * <p>The launch and communication protocol between snippet and client is versionated and reported
+ * as follows:
+ *
+ * <ul>
+ * <li>v0 (not reported):
+ * <ul>
+ * <li>Launch as Instrumentation with SnippetRunner.
+ * <li>No protocol-specific messages reported through instrumentation output.
+ * <li>'stop' action prints 'OK (0 tests)'
+ * <li>'start' action prints nothing.
+ * </ul>
+ * <li>v1.0: New instrumentation output added to track bringup process
+ * <ul>
+ * <li>"SNIPPET START, PROTOCOL &lt;major&gt; &lt;minor&gt;" upon snippet start
+ * <li>"SNIPPET SERVING, PORT &lt;port&gt;" once server is ready
+ * </ul>
+ * </ul>
+ */
+public class SnippetRunner extends AndroidJUnitRunner {
+
+ /**
+ * Major version of the launch and communication protocol.
+ *
+ * <p>Incrementing this means that compatibility with clients using the older version is broken.
+ * Avoid breaking compatibility unless there is no other choice.
+ */
+ public static final int PROTOCOL_MAJOR_VERSION = 1;
+
+ /**
+ * Minor version of the launch and communication protocol.
+ *
+ * <p>Increment this when new features are added to the launch and communication protocol that
+ * are backwards compatible with the old protocol and don't break existing clients.
+ */
+ public static final int PROTOCOL_MINOR_VERSION = 0;
+
+ private static final String ARG_ACTION = "action";
+ private static final String ARG_PORT = "port";
+
+ /**
+ * Values needed to create a notification channel. This applies to versions > O (26).
+ */
+ private static final String NOTIFICATION_CHANNEL_ID = "msl_channel";
+ private static final String NOTIFICATION_CHANNEL_DESC = "Channel reserved for mobly-snippet-lib.";
+ private static final CharSequence NOTIFICATION_CHANNEL_NAME = "msl";
+
+ private enum Action {
+ START,
+ STOP
+ };
+
+ private static final int NOTIFICATION_ID = NotificationIdFactory.create();
+
+ private Bundle mArguments;
+ private NotificationManager mNotificationManager;
+ private Notification mNotification;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ mArguments = arguments;
+
+ // First-run static setup
+ Log.initLogTag(getContext());
+
+ // First order of business is to report HELLO to instrumentation output.
+ sendString(
+ "SNIPPET START, PROTOCOL " + PROTOCOL_MAJOR_VERSION + " " + PROTOCOL_MINOR_VERSION);
+
+ // Prevent this runner from triggering any real JUnit tests in the snippet by feeding it a
+ // hardcoded empty test class.
+ mArguments.putString("class", EmptyTestClass.class.getCanonicalName());
+ mNotificationManager =
+ (NotificationManager)
+ getTargetContext().getSystemService(Context.NOTIFICATION_SERVICE);
+ super.onCreate(mArguments);
+ }
+
+ @Override
+ public void onStart() {
+ String actionStr = mArguments.getString(ARG_ACTION);
+ if (actionStr == null) {
+ throw new IllegalArgumentException("\"--e action <action>\" was not specified");
+ }
+ Action action = Action.valueOf(actionStr.toUpperCase(Locale.ROOT));
+ switch (action) {
+ case START:
+ String servicePort = mArguments.getString(ARG_PORT);
+ int port = 0 /* auto chosen */;
+ if (servicePort != null) {
+ port = Integer.parseInt(servicePort);
+ }
+ startServer(port);
+ break;
+ case STOP:
+ mNotificationManager.cancel(NOTIFICATION_ID);
+ mNotificationManager.cancelAll();
+ super.onStart();
+ }
+ }
+
+ private void startServer(int port) {
+ AndroidProxy androidProxy = new AndroidProxy(getContext());
+ try {
+ androidProxy.startLocal(port);
+ } catch (SocketException e) {
+ if ("Permission denied".equals(e.getMessage())) {
+ throw new RuntimeException(
+ "Failed to start server. No permission to create a socket. Does the *MAIN* "
+ + "app manifest declare the INTERNET permission?",
+ e);
+ }
+ throw new RuntimeException("Failed to start server", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to start server", e);
+ }
+ createNotification();
+ int actualPort = androidProxy.getPort();
+ sendString("SNIPPET SERVING, PORT " + actualPort);
+ Log.i("Snippet server started for process " + Process.myPid() + " on port " + actualPort);
+ }
+
+ @SuppressWarnings("deprecation") // Depreciated calls needed for versions < O (26)
+ private void createNotification() {
+ Notification.Builder builder;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ builder = new Notification.Builder(getTargetContext());
+ builder.setSmallIcon(android.R.drawable.btn_star)
+ .setTicker(null)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle("Snippet Service");
+ mNotification = builder.getNotification();
+ } else {
+ // Create a new channel for notifications. Needed for versions >= O
+ NotificationChannel channel = new NotificationChannel(
+ NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
+ channel.setDescription(NOTIFICATION_CHANNEL_DESC);
+ mNotificationManager.createNotificationChannel(channel);
+
+ // Build notification
+ builder = new Notification.Builder(getTargetContext(), NOTIFICATION_CHANNEL_ID);
+ builder.setSmallIcon(android.R.drawable.btn_star)
+ .setTicker(null)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle("Snippet Service");
+ mNotification = builder.build();
+ }
+ mNotification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
+ mNotificationManager.notify(NOTIFICATION_ID, mNotification);
+ }
+
+ private void sendString(String string) {
+ Log.i("Sending protocol message: " + string);
+ Bundle bundle = new Bundle();
+ bundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT, string + "\n");
+ sendStatus(0, bundle);
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventCache.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventCache.java
new file mode 100644
index 0000000..a3106f5
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventCache.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.event;
+
+import com.google.android.mobly.snippet.util.Log;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.LinkedBlockingDeque;
+
+/**
+ * Manage the event queue.
+ *
+ * <p>EventCache APIs interact with the SnippetEvent cache - a data structure that holds {@link
+ * SnippetEvent} objects posted from snippet classes. The SnippetEvent cache provides a useful means
+ * of recording background events (such as sensor data) when the phone is busy with foreground
+ * activities.
+ */
+public class EventCache {
+ private static final String EVENT_DEQUE_ID_TEMPLATE = "%s|%s";
+ private static final int EVENT_DEQUE_MAX_SIZE = 1024;
+
+ // A Map with each value being the queue for a particular type of event, and the key being the
+ // unique ID of the queue. The ID is composed of a callback ID and an event's name.
+ private final Map<String, LinkedBlockingDeque<SnippetEvent>> mEventDeques = new HashMap<>();
+
+ private static volatile EventCache mEventCache;
+
+ private EventCache() {}
+
+ public static EventCache getInstance() {
+ if (mEventCache == null) {
+ synchronized (EventCache.class) {
+ if (mEventCache == null) {
+ mEventCache = new EventCache();
+ }
+ }
+ }
+ return mEventCache;
+ }
+
+ public static String getQueueId(String callbackId, String name) {
+ return String.format(Locale.US, EVENT_DEQUE_ID_TEMPLATE, callbackId, name);
+ }
+
+ public LinkedBlockingDeque<SnippetEvent> getEventDeque(String qId) {
+ synchronized (mEventDeques) {
+ LinkedBlockingDeque<SnippetEvent> eventDeque = mEventDeques.get(qId);
+ if (eventDeque == null) {
+ eventDeque = new LinkedBlockingDeque<>(EVENT_DEQUE_MAX_SIZE);
+ mEventDeques.put(qId, eventDeque);
+ }
+ return eventDeque;
+ }
+ }
+
+ /**
+ * Post an {@link SnippetEvent} object to the Event cache.
+ *
+ * <p>Snippet classes should use this method to post events. If EVENT_DEQUE_MAX_SIZE is reached,
+ * the oldest elements will be retired until the new event could be posted.
+ *
+ * @param snippetEvent The snippetEvent to post to {@link EventCache}.
+ */
+ public void postEvent(SnippetEvent snippetEvent) {
+ String qId = getQueueId(snippetEvent.getCallbackId(), snippetEvent.getName());
+ Deque<SnippetEvent> q = getEventDeque(qId);
+ synchronized (q) {
+ while (!q.offer(snippetEvent)) {
+ SnippetEvent retiredEvent = q.removeFirst();
+ Log.v(
+ String.format(
+ Locale.US,
+ "Retired event %s due to deque reaching the size limit (%s).",
+ retiredEvent,
+ EVENT_DEQUE_MAX_SIZE));
+ }
+ }
+ Log.v(String.format(Locale.US, "Posted event(%s)", qId));
+ }
+
+ /** Clears all cached events. */
+ public void clearAll() {
+ synchronized (mEventDeques) {
+ mEventDeques.clear();
+ }
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventSnippet.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventSnippet.java
new file mode 100644
index 0000000..4cebb74
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/EventSnippet.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.event;
+
+import androidx.annotation.Nullable;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class EventSnippet implements Snippet {
+ private static class EventSnippetException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public EventSnippetException(String msg) {
+ super(msg);
+ }
+ }
+
+ private static final int DEFAULT_TIMEOUT_MILLISECOND = 60 * 1000;
+ private final EventCache mEventCache = EventCache.getInstance();
+
+ @Rpc(
+ description =
+ "Blocks until an event of a specified type has been received. The returned event is removed from the cache. Default timeout is 60s.")
+ public JSONObject eventWaitAndGet(
+ String callbackId, String eventName, @Nullable Integer timeout)
+ throws InterruptedException, JSONException, EventSnippetException {
+ // The server side should never wait forever, so we'll use a default timeout is one is not
+ // provided.
+ if (timeout == null) {
+ timeout = DEFAULT_TIMEOUT_MILLISECOND;
+ }
+ String qId = EventCache.getQueueId(callbackId, eventName);
+ LinkedBlockingDeque<SnippetEvent> q = mEventCache.getEventDeque(qId);
+ SnippetEvent result = q.pollFirst(timeout, TimeUnit.MILLISECONDS);
+ if (result == null) {
+ throw new EventSnippetException("timeout.");
+ }
+ return result.toJson();
+ }
+
+ @Rpc(
+ description =
+ "Gets and removes all the events of a certain name that have been received so far. "
+ + "Non-blocking. Potentially racey since it does not guarantee no event of "
+ + "the same name will occur after the call.")
+ public List<JSONObject> eventGetAll(String callbackId, String eventName)
+ throws InterruptedException, JSONException {
+ String qId = EventCache.getQueueId(callbackId, eventName);
+ LinkedBlockingDeque<SnippetEvent> q = mEventCache.getEventDeque(qId);
+ ArrayList<JSONObject> results = new ArrayList<>(q.size());
+ ArrayList<SnippetEvent> buffer = new ArrayList<>(q.size());
+ q.drainTo(buffer);
+ for (SnippetEvent snippetEvent : buffer) {
+ results.add(snippetEvent.toJson());
+ }
+ if (results.size() == 0) {
+ return Collections.emptyList();
+ }
+ return results;
+ }
+
+ @Override
+ public void shutdown() {
+ mEventCache.clearAll();
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/SnippetEvent.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/SnippetEvent.java
new file mode 100644
index 0000000..a90d9eb
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/event/SnippetEvent.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.event;
+
+import android.os.Bundle;
+import com.google.android.mobly.snippet.rpc.JsonBuilder;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Class used to store information from a callback event. */
+public class SnippetEvent {
+
+ // The ID used to associate an event to a callback object on the client side.
+ private final String mCallbackId;
+ // The name of this event, e.g. startXxxServiceOnSuccess.
+ private final String mName;
+ // The content of this event. We use Android's Bundle because it adheres to Android convention
+ // and adding data to it does not throw checked exceptions, which makes the world a better
+ // place.
+ private final Bundle mData = new Bundle();
+
+ private final long mCreationTime;
+
+ /**
+ * Constructs an {@link SnippetEvent} object.
+ *
+ * <p>The object is used to store information from a callback method associated with a call to
+ * an {@link com.google.android.mobly.snippet.rpc.AsyncRpc} method.
+ *
+ * @param callbackId The callbackId passed to the {@link
+ * com.google.android.mobly.snippet.rpc.AsyncRpc} method.
+ * @param name The name of the event.
+ */
+ public SnippetEvent(String callbackId, String name) {
+ if (callbackId == null) {
+ throw new IllegalArgumentException("SnippetEvent's callback ID shall not be null.");
+ }
+ if (name == null) {
+ throw new IllegalArgumentException("SnippetEvent's name shall not be null.");
+ }
+ mCallbackId = callbackId;
+ mName = name;
+ mCreationTime = System.currentTimeMillis();
+ }
+
+ public String getCallbackId() {
+ return mCallbackId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Get the internal bundle of this event.
+ *
+ * <p>This is the only way to add data to the event, because we can't inherit Bundle type and we
+ * don't want to dup all the getter and setters of {@link Bundle}.
+ *
+ * @return The Bundle that holds user data for this {@link SnippetEvent}.
+ */
+ public Bundle getData() {
+ return mData;
+ }
+
+ public long getCreationTime() {
+ return mCreationTime;
+ }
+
+ public JSONObject toJson() throws JSONException {
+ JSONObject result = new JSONObject();
+ result.put("callbackId", getCallbackId());
+ result.put("name", getName());
+ result.put("time", getCreationTime());
+ result.put("data", JsonBuilder.build(mData));
+ return result;
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.java
new file mode 100644
index 0000000..5e6edd3
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/future/FutureResult.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.google.android.mobly.snippet.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. */
+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/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java
new file mode 100644
index 0000000..7707de2
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetManager.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.manager;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.SnippetObjectConverter;
+import com.google.android.mobly.snippet.event.EventSnippet;
+import com.google.android.mobly.snippet.rpc.MethodDescriptor;
+import com.google.android.mobly.snippet.rpc.RpcMinSdk;
+import com.google.android.mobly.snippet.rpc.RunOnUiThread;
+import com.google.android.mobly.snippet.schedulerpc.ScheduleRpcSnippet;
+import com.google.android.mobly.snippet.util.Log;
+import com.google.android.mobly.snippet.util.MainThread;
+import com.google.android.mobly.snippet.util.SnippetLibException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.Callable;
+
+public class SnippetManager {
+ /**
+ * Name of the XML tag specifying what snippet classes to look for RPCs in.
+ *
+ * <p>Comma delimited list of full package names for classes that implements the Snippet
+ * interface.
+ */
+ private static final String TAG_NAME_SNIPPET_LIST = "mobly-snippets";
+ /** Name of the XML tag specifying the custom object converter class to use. */
+ private static final String TAG_NAME_OBJECT_CONVERTER = "mobly-object-converter";
+
+ private final Map<Class<? extends Snippet>, Snippet> mSnippets;
+ /** A map of strings to known RPCs. */
+ private final Map<String, MethodDescriptor> mKnownRpcs;
+
+ private static SnippetManager sInstance = null;
+ private boolean mShutdown = false;
+
+ private SnippetManager(Collection<Class<? extends Snippet>> classList) {
+ // Synchronized for multiple connections on the same session. Can't use ConcurrentHashMap
+ // because we have to put in a value of 'null' before the class is constructed, but
+ // ConcurrentHashMap does not allow null values.
+ mSnippets = Collections.synchronizedMap(new HashMap<Class<? extends Snippet>, Snippet>());
+ Map<String, MethodDescriptor> knownRpcs = new HashMap<>();
+ for (Class<? extends Snippet> receiverClass : classList) {
+ mSnippets.put(receiverClass, null);
+ Collection<MethodDescriptor> methodList = MethodDescriptor.collectFrom(receiverClass);
+ for (MethodDescriptor m : methodList) {
+ if (knownRpcs.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.");
+ }
+ knownRpcs.put(m.getName(), m);
+ }
+ }
+ // Does not need to be concurrent because this map is read only, so it is safe to access
+ // from multiple threads. Wrap in an unmodifiableMap to enforce this.
+ mKnownRpcs = Collections.unmodifiableMap(knownRpcs);
+ }
+
+ public static synchronized SnippetManager initSnippetManager(Context context) {
+ if (sInstance != null) {
+ throw new IllegalStateException("SnippetManager should not be re-initialized");
+ }
+ // Add custom object converter if user provided one.
+ Class<? extends SnippetObjectConverter> converterClazz =
+ findSnippetObjectConverterFromMetadata(context);
+ if (converterClazz != null) {
+ Log.d("Found custom converter class, adding...");
+ SnippetObjectConverterManager.addConverter(converterClazz);
+ }
+ Collection<Class<? extends Snippet>> classList = findSnippetClassesFromMetadata(context);
+ sInstance = new SnippetManager(classList);
+ return sInstance;
+ }
+
+ public static SnippetManager getInstance() {
+ if (sInstance == null) {
+ throw new IllegalStateException("getInstance() called before init()");
+ }
+ if (sInstance.isShutdown()) {
+ throw new IllegalStateException("shutdown() called before getInstance()");
+ }
+ return sInstance;
+ }
+
+ public MethodDescriptor getMethodDescriptor(String methodName) {
+ return mKnownRpcs.get(methodName);
+ }
+
+ public SortedSet<String> getMethodNames() {
+ return new TreeSet<>(mKnownRpcs.keySet());
+ }
+
+ public Object invoke(Class<? extends Snippet> clazz, Method method, Object[] args)
+ throws Throwable {
+ if (method.isAnnotationPresent(RpcMinSdk.class)) {
+ int requiredSdkLevel = method.getAnnotation(RpcMinSdk.class).value();
+ if (Build.VERSION.SDK_INT < requiredSdkLevel) {
+ throw new SnippetLibException(
+ String.format(
+ Locale.US,
+ "%s requires API level %d, current level is %d",
+ method.getName(),
+ requiredSdkLevel,
+ Build.VERSION.SDK_INT));
+ }
+ }
+ Snippet object;
+ try {
+ object = get(clazz);
+ return invoke(object, method, args);
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+
+ public void shutdown() throws Exception {
+ for (final Entry<Class<? extends Snippet>, Snippet> entry : mSnippets.entrySet()) {
+ if (entry.getValue() == null) {
+ continue;
+ }
+ Method method = entry.getKey().getMethod("shutdown");
+ if (method.isAnnotationPresent(RunOnUiThread.class)) {
+ Log.d("Shutting down " + entry.getKey().getName() + " on the main thread");
+ MainThread.run(
+ new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ entry.getValue().shutdown();
+ return null;
+ }
+ });
+ } else {
+ Log.d("Shutting down " + entry.getKey().getName());
+ entry.getValue().shutdown();
+ }
+ }
+ mSnippets.clear();
+ mKnownRpcs.clear();
+ mShutdown = true;
+ }
+
+ public boolean isShutdown() {
+ return mShutdown;
+ }
+
+ private static Bundle findMetadata(Context context) {
+ ApplicationInfo appInfo;
+ try {
+ appInfo =
+ context.getPackageManager()
+ .getApplicationInfo(
+ context.getPackageName(), PackageManager.GET_META_DATA);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new IllegalStateException(
+ "Failed to find ApplicationInfo with package name: "
+ + context.getPackageName());
+ }
+ return appInfo.metaData;
+ }
+
+ private static Class<? extends SnippetObjectConverter> findSnippetObjectConverterFromMetadata(
+ Context context) {
+ String className = findMetadata(context).getString(TAG_NAME_OBJECT_CONVERTER);
+ if (className == null) {
+ Log.i("No object converter provided.");
+ return null;
+ }
+ try {
+ return Class.forName(className).asSubclass(SnippetObjectConverter.class);
+ } catch (ClassNotFoundException | ClassCastException e) {
+ Log.e("Failed to find class " + className);
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Set<Class<? extends Snippet>> findSnippetClassesFromMetadata(Context context) {
+ String snippets = findMetadata(context).getString(TAG_NAME_SNIPPET_LIST);
+ if (snippets == null) {
+ throw new IllegalStateException(
+ "AndroidManifest.xml does not contain a <metadata> tag with "
+ + "name=\""
+ + TAG_NAME_SNIPPET_LIST
+ + "\"");
+ }
+ String[] snippetClassNames = snippets.split("\\s*,\\s*");
+ Set<Class<? extends Snippet>> receiverSet = new HashSet<>();
+ /** Add the event snippet class which is provided within the Snippet Lib. */
+ receiverSet.add(EventSnippet.class);
+ /** Add the schedule RPC snippet class which is provided within the Snippet Lib. */
+ receiverSet.add(ScheduleRpcSnippet.class);
+ for (String snippetClassName : snippetClassNames) {
+ try {
+ Log.i("Trying to load Snippet class: " + snippetClassName);
+ Class<? extends Snippet> snippetClass =
+ Class.forName(snippetClassName).asSubclass(Snippet.class);
+ receiverSet.add(snippetClass);
+ } catch (ClassNotFoundException | ClassCastException e) {
+ Log.e("Failed to find class " + snippetClassName);
+ throw new RuntimeException(e);
+ }
+ }
+ if (receiverSet.isEmpty()) {
+ throw new IllegalStateException("Found no subclasses of Snippet.");
+ }
+ return receiverSet;
+ }
+
+ private Snippet get(Class<? extends Snippet> clazz) throws Exception {
+ Snippet snippetImpl = mSnippets.get(clazz);
+ if (snippetImpl == null) {
+ // First time calling an RPC for this snippet; construct an instance under lock.
+ synchronized (clazz) {
+ snippetImpl = mSnippets.get(clazz);
+ if (snippetImpl == null) {
+ final Constructor<? extends Snippet> constructor = clazz.getConstructor();
+ if (constructor.isAnnotationPresent(RunOnUiThread.class)) {
+ Log.d("Constructing " + clazz + " on the main thread");
+ snippetImpl =
+ MainThread.run(
+ new Callable<Snippet>() {
+ @Override
+ public Snippet call() throws Exception {
+ return constructor.newInstance();
+ }
+ });
+ } else {
+ Log.d("Constructing " + clazz);
+ snippetImpl = constructor.newInstance();
+ }
+ mSnippets.put(clazz, snippetImpl);
+ }
+ }
+ }
+ return snippetImpl;
+ }
+
+ private Object invoke(final Snippet snippetImpl, final Method method, final Object[] args)
+ throws Exception {
+ if (method.isAnnotationPresent(RunOnUiThread.class)) {
+ Log.d(
+ "Invoking RPC method "
+ + method.getDeclaringClass()
+ + "#"
+ + method.getName()
+ + " on the main thread");
+ return MainThread.run(
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ return method.invoke(snippetImpl, args);
+ }
+ });
+ } else {
+ Log.d("Invoking RPC method " + method.getDeclaringClass() + "#" + method.getName());
+ return method.invoke(snippetImpl, args);
+ }
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java
new file mode 100644
index 0000000..df8af53
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/manager/SnippetObjectConverterManager.java
@@ -0,0 +1,65 @@
+package com.google.android.mobly.snippet.manager;
+
+import com.google.android.mobly.snippet.SnippetObjectConverter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Type;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Manager for classes that implement {@link SnippetObjectConverter}.
+ *
+ * <p>This class is created to separate how Snippet Lib handles object conversion internally from
+ * how the conversion scheme for complex types is defined for users.
+ *
+ * <p>Snippet Lib can pull in the custom serializers and deserializers through here in various
+ * stages of execution, whereas users can have a clean interface for supplying these methods without
+ * worrying about internal states of Snippet Lib.
+ *
+ * <p>This gives us the flexibility of changing Snippet Lib internal structure or expanding support
+ * without impacting users. E.g. we can support multiple converter classes in the future.
+ */
+public class SnippetObjectConverterManager {
+ private static SnippetObjectConverter mConverter;
+ private static volatile SnippetObjectConverterManager mManager;
+
+ private SnippetObjectConverterManager() {}
+
+ public static synchronized SnippetObjectConverterManager getInstance() {
+ if (mManager == null) {
+ mManager = new SnippetObjectConverterManager();
+ }
+ return mManager;
+ }
+
+ static void addConverter(Class<? extends SnippetObjectConverter> converterClass) {
+ if (mConverter != null) {
+ throw new RuntimeException("A converter has been added, cannot add again.");
+ }
+ try {
+ mConverter = converterClass.getConstructor().newInstance();
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("No default constructor found for the converter class.");
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ } catch (InstantiationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Object objectToJson(Object object) throws JSONException {
+ if (mConverter == null) {
+ return null;
+ }
+ return mConverter.serialize(object);
+ }
+
+ public Object jsonToObject(JSONObject jsonObject, Type type) throws JSONException {
+ if (mConverter == null) {
+ return null;
+ }
+ return mConverter.deserialize(jsonObject, type);
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.java
new file mode 100644
index 0000000..9428c82
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AndroidProxy.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.google.android.mobly.snippet.rpc;
+
+import android.content.Context;
+import java.io.IOException;
+
+public class AndroidProxy {
+
+ private final JsonRpcServer mJsonRpcServer;
+
+ public AndroidProxy(Context context) {
+ mJsonRpcServer = new JsonRpcServer(context);
+ }
+
+ public void startLocal(int port) throws IOException {
+ mJsonRpcServer.startLocal(port);
+ }
+
+ public int getPort() {
+ return mJsonRpcServer.getPort();
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java
new file mode 100644
index 0000000..6bdd8ca
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/AsyncRpc.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.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 AsyncRpc} annotation is used to annotate server-side implementations of RPCs that
+ * trigger asynchronous events. This behaves generally the same as {@link Rpc}, but methods that are
+ * annotated with {@link AsyncRpc} are expected to take the extra parameter which is the ID to use
+ * when posting async events.
+ *
+ * <p>Sample Usage:
+ *
+ * <pre>{@code
+ * {@literal @}AsyncRpc(description = "An example showing the usage of AsyncRpc")
+ * public void doSomethingAsync(String callbackId, ...) {
+ * // start some async operation and post a Snippet Event object with the given callbackId.
+ * }
+ * }</pre>
+ *
+ * AsyncRpc methods can still return serializable values, which will be transported in the regular
+ * return value field of the Rpc protocol.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+@Documented
+public @interface AsyncRpc {
+ /** Returns brief description of the function. Should be limited to one or two sentences. */
+ String description();
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java
new file mode 100644
index 0000000..a1d3425
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import com.google.android.mobly.snippet.manager.SnippetObjectConverterManager;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URL;
+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.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class JsonBuilder {
+
+ private 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<>((Set<?>) data);
+ return buildJsonList(items);
+ }
+ if (data instanceof Collection<?>) {
+ List<Object> items = new ArrayList<>((Collection<?>) data);
+ return buildJsonList(items);
+ }
+ if (data instanceof List<?>) {
+ return buildJsonList((List<?>) data);
+ }
+ if (data instanceof Bundle) {
+ return buildJsonBundle((Bundle) data);
+ }
+ if (data instanceof Intent) {
+ return buildJsonIntent((Intent) 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();
+ }
+ // TODO(xpconanfan): Deprecate the following default non-primitive type builders.
+ 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 byte[]) {
+ JSONArray result = new JSONArray();
+ for (byte b : (byte[]) data) {
+ result.put(b & 0xFF);
+ }
+ return result;
+ }
+ if (data instanceof Object[]) {
+ return buildJSONArray((Object[]) data);
+ }
+ // Try with custom converter provided by user.
+ Object result = SnippetObjectConverterManager.getInstance().objectToJson(data);
+ if (result != null) {
+ return result;
+ }
+ return data.toString();
+ }
+
+ 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 JSONArray buildJSONArray(Object[] data) throws JSONException {
+ JSONArray result = new JSONArray();
+ for (Object o : data) {
+ result.put(build(o));
+ }
+ return result;
+ }
+
+ private static JSONObject buildJsonBundle(Bundle bundle) throws JSONException {
+ JSONObject result = new JSONObject();
+ bundle.setClassLoader(JsonBuilder.class.getClassLoader());
+ for (String key : bundle.keySet()) {
+ result.put(key, build(bundle.get(key)));
+ }
+ 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 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 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;
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.java
new file mode 100644
index 0000000..90cf8f9
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcResult.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.google.android.mobly.snippet.rpc;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents a JSON RPC result.
+ *
+ * @see <a href="http://json-rpc.org/wiki/specification">http://json-rpc.org/wiki/specification</a>
+ */
+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("callback", 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("callback", JSONObject.NULL);
+ json.put("error", JSONObject.NULL);
+ return json;
+ }
+
+ public static JSONObject callback(int id, Object data, String callbackId) throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put("id", id);
+ json.put("result", JsonBuilder.build(data));
+ json.put("callback", callbackId);
+ json.put("error", JSONObject.NULL);
+ return json;
+ }
+
+ public static JSONObject error(int id, Throwable t) throws JSONException {
+ String stackTrace = getStackTrace(t);
+ JSONObject json = new JSONObject();
+ json.put("id", id);
+ json.put("result", JSONObject.NULL);
+ json.put("callback", JSONObject.NULL);
+ json.put("error", stackTrace);
+ return json;
+ }
+
+ public static String getStackTrace(Throwable throwable) {
+ StringWriter stackTraceWriter = new StringWriter();
+ stackTraceWriter.write("\n-------------- Java Stacktrace ---------------\n");
+ throwable.printStackTrace(new PrintWriter(stackTraceWriter));
+ stackTraceWriter.write("----------------------------------------------");
+ return stackTraceWriter.toString();
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java
new file mode 100644
index 0000000..1dde423
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonRpcServer.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import android.content.Context;
+import com.google.android.mobly.snippet.manager.SnippetManager;
+import com.google.android.mobly.snippet.util.Log;
+import com.google.android.mobly.snippet.util.RpcUtil;
+import java.io.BufferedReader;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** A JSON RPC server that forwards RPC calls to a specified receiver object. */
+public class JsonRpcServer extends SimpleServer {
+ private static final String CMD_CLOSE_SESSION = "closeSl4aSession";
+ private static final String CMD_HELP = "help";
+
+ private final SnippetManager mSnippetManager;
+ private final RpcUtil mRpcUtil;
+
+ /** Construct a {@link JsonRpcServer} connected to the provided {@link SnippetManager}. */
+ public JsonRpcServer(Context context) {
+ mSnippetManager = SnippetManager.initSnippetManager(context);
+ mRpcUtil = new RpcUtil();
+ }
+
+ @Override
+ protected void handleRPCConnection(
+ Socket sock, Integer UID, BufferedReader reader, PrintWriter writer) throws Exception {
+ Log.d("UID " + UID);
+ 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");
+
+ // Handle builtin commands
+ if (method.equals(CMD_HELP)) {
+ help(writer, id, mSnippetManager, UID);
+ continue;
+ } else if (method.equals(CMD_CLOSE_SESSION)) {
+ Log.d("Got shutdown signal");
+ synchronized (writer) {
+ // Shut down all RPC receivers.
+ mSnippetManager.shutdown();
+
+ // Shut down this client connection. As soon as this happens, the client will
+ // kill us by triggering the 'stop' action from another instrumentation, so no
+ // other cleanup steps are guaranteed to execute.
+ send(writer, JsonRpcResult.empty(id), UID);
+ reader.close();
+ writer.close();
+ sock.close();
+
+ // Shut down this server.
+ shutdown();
+ }
+ return;
+ }
+ JSONObject returnValue = mRpcUtil.invokeRpc(method, params, id, UID);
+ send(writer, returnValue, UID);
+ }
+ }
+
+ private void help(PrintWriter writer, int id, SnippetManager receiverManager, Integer UID)
+ throws JSONException {
+ // Create a map from class simple name to the methods inside it.
+ Map<String, Set<MethodDescriptor>> methods = new TreeMap<>();
+ for (String method : receiverManager.getMethodNames()) {
+ MethodDescriptor descriptor = receiverManager.getMethodDescriptor(method);
+ String snippetClassName = descriptor.getSnippetClass().getSimpleName();
+ Set<MethodDescriptor> snippetClassMethods = methods.get(snippetClassName);
+ if (snippetClassMethods == null) {
+ // Preserve insertion order (alphabetical)
+ snippetClassMethods = new LinkedHashSet<>();
+ methods.put(snippetClassName, snippetClassMethods);
+ }
+ snippetClassMethods.add(descriptor);
+ }
+ StringBuilder result = new StringBuilder();
+ for (Map.Entry<String, Set<MethodDescriptor>> entry : methods.entrySet()) {
+ result.append("\nRPCs provided by ").append(entry.getKey()).append(":\n");
+ for (MethodDescriptor descriptor : entry.getValue()) {
+ result.append(" ").append(descriptor.getHelp()).append("\n");
+ }
+ }
+ send(writer, JsonRpcResult.result(id, result), UID);
+ }
+
+ 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/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonSerializable.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonSerializable.java
new file mode 100644
index 0000000..5871e01
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/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.google.android.mobly.snippet.rpc;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public interface JsonSerializable {
+ JSONObject toJSON() throws JSONException;
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java
new file mode 100644
index 0000000..b9c8a7a
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import android.content.Intent;
+import android.net.Uri;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.manager.SnippetManager;
+import com.google.android.mobly.snippet.manager.SnippetObjectConverterManager;
+import com.google.android.mobly.snippet.util.AndroidUtil;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** An adapter that wraps {@code Method}. */
+public final class MethodDescriptor {
+ private final Method mMethod;
+ private final Class<? extends Snippet> mClass;
+
+ private MethodDescriptor(Class<? extends Snippet> 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 Snippet> clazz) {
+ List<MethodDescriptor> descriptors = new ArrayList<MethodDescriptor>();
+ for (Method method : clazz.getMethods()) {
+ if (method.isAnnotationPresent(Rpc.class)
+ || method.isAnnotationPresent(AsyncRpc.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 the exception raised from executing the RPC method.
+ */
+ public Object invoke(SnippetManager manager, final JSONArray parameters) throws Throwable {
+ final Type[] parameterTypes = getGenericParameterTypes();
+ final Object[] args = new Object[parameterTypes.length];
+
+ 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 {
+ throw new RpcError("Argument " + (i + 1) + " is not present");
+ }
+ }
+
+ return manager.invoke(mClass, mMethod, args);
+ }
+
+ /** Converts a parameter from JSON into a Java Object. */
+ // 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
+ private static Object convertParameter(final JSONArray parameters, int index, Type type)
+ throws JSONException, RpcError {
+ try {
+ // 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 || type == boolean.class) {
+ try {
+ return parameters.getBoolean(index);
+ } catch (JSONException e) {
+ return parameters.getInt(index) != 0;
+ }
+ } else if (type == Long.class || type == long.class) {
+ return parameters.getLong(index);
+ } else if (type == Double.class || type == double.class) {
+ return parameters.getDouble(index);
+ } else if (type == Integer.class || type == int.class) {
+ return parameters.getInt(index);
+ } else if (type == Intent.class) {
+ return buildIntent(parameters.getJSONObject(index));
+ } else if (type == String.class) {
+ return parameters.getString(index);
+ } else if (type == Integer[].class || type == int[].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 == Long[].class || type == long[].class) {
+ JSONArray list = parameters.getJSONArray(index);
+ Long[] result = new Long[list.length()];
+ for (int i = 0; i < list.length(); i++) {
+ result[i] = list.getLong(i);
+ }
+ return result;
+ } else if (type == Byte.class || type == byte[].class) {
+ JSONArray list = parameters.getJSONArray(index);
+ byte[] result = new byte[list.length()];
+ for (int i = 0; i < list.length(); i++) {
+ result[i] = (byte) 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 if (type == JSONArray.class) {
+ return parameters.getJSONArray(index);
+ } else {
+ // Try any custom converter provided.
+ Object object =
+ SnippetObjectConverterManager.getInstance()
+ .jsonToObject(parameters.getJSONObject(index), type);
+ if (object != null) {
+ return object;
+ }
+ // 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()
+ + ", but is of type "
+ + parameters.get(index).getClass().getSimpleName());
+ }
+ }
+
+ private 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")) {
+ AndroidUtil.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 String getName() {
+ return mMethod.getName();
+ }
+
+ private Type[] getGenericParameterTypes() {
+ return mMethod.getGenericParameterTypes();
+ }
+
+ public boolean isAsync() {
+ return mMethod.isAnnotationPresent(AsyncRpc.class);
+ }
+
+ Class<? extends Snippet> getSnippetClass() {
+ return mClass;
+ }
+
+ private String getAnnotationDescription() {
+ if (isAsync()) {
+ AsyncRpc annotation = mMethod.getAnnotation(AsyncRpc.class);
+ return annotation.description();
+ }
+ Rpc annotation = mMethod.getAnnotation(Rpc.class);
+ return annotation.description();
+ }
+ /**
+ * Returns a human-readable help text for this RPC, based on annotations in the source code.
+ *
+ * @return derived help string
+ */
+ String getHelp() {
+ StringBuilder paramBuilder = new StringBuilder();
+ Class<?>[] parameterTypes = mMethod.getParameterTypes();
+ for (int i = 0; i < parameterTypes.length; i++) {
+ if (i != 0) {
+ paramBuilder.append(", ");
+ }
+ paramBuilder.append(parameterTypes[i].getSimpleName());
+ }
+ return String.format(
+ Locale.US,
+ "%s %s(%s) returns %s // %s",
+ isAsync() ? "@AsyncRpc" : "@Rpc",
+ mMethod.getName(),
+ paramBuilder,
+ mMethod.getReturnType().getSimpleName(),
+ getAnnotationDescription());
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.java
new file mode 100644
index 0000000..af321ba
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/Rpc.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.google.android.mobly.snippet.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();
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java
new file mode 100644
index 0000000..0862673
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.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.google.android.mobly.snippet.rpc;
+
+@SuppressWarnings("serial")
+public class RpcError extends Exception {
+
+ public RpcError(String message) {
+ super(message);
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.java
new file mode 100644
index 0000000..f03fd2a
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcMinSdk.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.google.android.mobly.snippet.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. */
+ int value();
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java
new file mode 100644
index 0000000..cde08f0
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RunOnUiThread.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.mobly.snippet.rpc;
+
+import com.google.android.mobly.snippet.Snippet;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This annotation will cause the RPC to execute on the main app thread.
+ *
+ * <p>This annotation can be applied to:
+ *
+ * <ul>
+ * <li>The constructor of a class implementing the {@link Snippet} interface.
+ * <li>A method annotated with the {@link Rpc} or {@link AsyncRpc} annotation.
+ * <li>The {@link Snippet#shutdown()} method.
+ * </ul>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RunOnUiThread {}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java
new file mode 100644
index 0000000..db7255a
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/SimpleServer.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.rpc;
+
+import com.google.android.mobly.snippet.util.Log;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** A simple server. */
+public abstract class SimpleServer {
+ private static int threadIndex = 0;
+ private final ConcurrentHashMap<Integer, ConnectionThread> mConnectionThreads =
+ new ConcurrentHashMap<>();
+ private final List<SimpleServerObserver> mObservers = new ArrayList<>();
+ private volatile boolean mStopServer = false;
+ private ServerSocket mServer;
+ private Thread mServerThread;
+
+ public interface SimpleServerObserver {
+ void onConnect();
+
+ 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);
+ }
+ }
+ }
+ }
+
+ private 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.
+ }
+
+ /**
+ * Starts the RPC server bound to the localhost address.
+ *
+ * @param port the port to bind to or 0 to pick any unused port
+ * @throws IOException
+ */
+ public void startLocal(int port) throws IOException {
+ InetAddress address = getPrivateInetAddress();
+ mServer = new ServerSocket(port, 5 /* backlog */, address);
+ start();
+ }
+
+ public int getPort() {
+ return mServer.getLocalPort();
+ }
+
+ private void 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());
+ }
+
+ 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() throws Exception {
+ // 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/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/schedulerpc/ScheduleRpcSnippet.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/schedulerpc/ScheduleRpcSnippet.java
new file mode 100644
index 0000000..2042b7f
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/schedulerpc/ScheduleRpcSnippet.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.schedulerpc;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.AsyncRpc;
+import com.google.android.mobly.snippet.util.RpcUtil;
+import org.json.JSONArray;
+
+/** Snippet that provides {@link AsyncRpc} to schedule other RPCs. */
+public class ScheduleRpcSnippet implements Snippet {
+
+ private final RpcUtil mRpcUtil;
+
+ public ScheduleRpcSnippet() {
+ mRpcUtil = new RpcUtil();
+ }
+
+ @AsyncRpc(description = "Delay the given RPC by provided milli-seconds.")
+ public void scheduleRpc(
+ String callbackId, String methodName, long delayTimerMs, JSONArray params)
+ throws Throwable {
+ mRpcUtil.scheduleRpc(callbackId, methodName, delayTimerMs, params);
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java
new file mode 100644
index 0000000..46c4940
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.util;
+
+import android.content.Intent;
+import android.os.Bundle;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public final class AndroidUtil {
+ private AndroidUtil() {}
+
+ // 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);
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/EmptyTestClass.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/EmptyTestClass.java
new file mode 100644
index 0000000..ac1920f
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/EmptyTestClass.java
@@ -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.
+ */
+
+package com.google.android.mobly.snippet.util;
+
+import org.junit.Ignore;
+
+/**
+ * A stub JUnit class with no tests.
+ *
+ * <p>Used for 'safely' calling AndroidJUnitRunner methods on snippets that happen to have tests
+ * defined, to avoid actually calling those tests.
+ */
+@Ignore
+public class EmptyTestClass {}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java
new file mode 100644
index 0000000..64f03e9
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/Log.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.util;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+
+public final class Log {
+ public static volatile String apkLogTag = null;
+
+ private static final String MY_CLASS_NAME = Log.class.getName();
+ private static final String ANDROID_LOG_CLASS_NAME = android.util.Log.class.getName();
+
+ // Skip the first two entries in stack trace when trying to infer the caller.
+ // The first two entries are:
+ // - dalvik.system.VMStack.getThreadStackTrace(Native Method)
+ // - java.lang.Thread.getStackTrace(Thread.java:580)
+ // The {@code getStackTrace()} function returns the stack trace at where the trace is collected
+ // (inisde the JNI function {@code getThreadStackTrace()} instead of at where the {@code
+ // getStackTrace()} is called (althrought this is the natual expectation).
+ private static final int STACK_TRACE_WALK_START_INDEX = 2;
+
+ private Log() {}
+
+ public static synchronized void initLogTag(Context context) {
+ if (apkLogTag != null) {
+ throw new IllegalStateException("Logger should not be re-initialized");
+ }
+ String packageName = context.getPackageName();
+ PackageManager packageManager = context.getPackageManager();
+ ApplicationInfo appInfo;
+ try {
+ appInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
+ } catch (NameNotFoundException e) {
+ throw new IllegalStateException(
+ "Failed to find ApplicationInfo with package name: " + packageName);
+ }
+ Bundle bundle = appInfo.metaData;
+ apkLogTag = bundle.getString("mobly-log-tag");
+ if (apkLogTag == null) {
+ apkLogTag = packageName;
+ w(
+ "AndroidManifest.xml does not contain metadata field named \"mobly-log-tag\". "
+ + "Using package name for logging instead.");
+ }
+ }
+
+ private static String getTag() {
+ String logTag = apkLogTag;
+ if (logTag == null) {
+ throw new IllegalStateException("Logging called before initLogTag()");
+ }
+ StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
+
+ boolean isCallerClassNameFound = false;
+ String fullClassName = null;
+ int lineNumber = 0;
+ // Walk up the stack and look for the first class name that is neither us nor
+ // android.util.Log: that's the caller.
+ // Do not used hard-coded stack depth: that does not work all the time because of proguard
+ // inline optimization.
+ for (int i = STACK_TRACE_WALK_START_INDEX; i < stackTraceElements.length; i++) {
+ StackTraceElement element = stackTraceElements[i];
+ fullClassName = element.getClassName();
+ if (!fullClassName.equals(MY_CLASS_NAME)
+ && !fullClassName.equals(ANDROID_LOG_CLASS_NAME)) {
+ lineNumber = element.getLineNumber();
+ isCallerClassNameFound = true;
+ break;
+ }
+ }
+
+ if (!isCallerClassNameFound) {
+ // Failed to determine caller's class name, fall back the the minimal one.
+ return logTag;
+ } else {
+ String className = fullClassName.substring(fullClassName.lastIndexOf(".") + 1);
+ return logTag + "." + className + ":" + lineNumber;
+ }
+ }
+
+ 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 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 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 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 i(String message) {
+ android.util.Log.i(getTag(), message);
+ }
+
+ public static void i(String message, Throwable e) {
+ android.util.Log.i(getTag(), message, e);
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java
new file mode 100644
index 0000000..0e4ece5
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/MainThread.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.util;
+
+import android.os.Handler;
+import android.os.Looper;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+
+public class MainThread {
+ /**
+ * Wraps a {@link Callable} in a {@link Runnable} that has a way to get the return value and
+ * exception after the fact.
+ */
+ private static class CallableWrapper<T> implements Runnable {
+ private final Callable<T> mCallable;
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private T mReturnValue;
+ private Throwable mException;
+
+ public CallableWrapper(Callable<T> callable) {
+ mCallable = callable;
+ }
+
+ @Override
+ public final void run() {
+ try {
+ mReturnValue = mCallable.call();
+ } catch (Throwable t) {
+ mException = t;
+ } finally {
+ mLatch.countDown();
+ }
+ }
+
+ public void awaitTermination() throws InterruptedException {
+ mLatch.await();
+ }
+
+ public T getReturnValue() {
+ return mReturnValue;
+ }
+
+ public Throwable getException() {
+ return mException;
+ }
+ }
+
+ private static final Handler sMainThreadHandler = new Handler(Looper.getMainLooper());
+
+ private MainThread() {
+ // Utility class.
+ }
+
+ /** Executed in the main thread. Returns the result of an execution or any exception thrown. */
+ public static <T> T run(final Callable<T> task) throws Exception {
+ CallableWrapper<T> wrapper = new CallableWrapper<>(task);
+ return runCallableWrapper(wrapper);
+ }
+
+ private static <T> T runCallableWrapper(CallableWrapper<T> wrapper) throws Exception {
+ sMainThreadHandler.post(wrapper);
+ wrapper.awaitTermination();
+ Throwable exception = wrapper.getException();
+ if (exception != null) {
+ if (exception instanceof RuntimeException) {
+ throw (RuntimeException) exception;
+ }
+ if (exception instanceof Error) {
+ throw (Error) exception;
+ }
+ throw (Exception) exception;
+ }
+ return wrapper.getReturnValue();
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.java
new file mode 100644
index 0000000..39cea5e
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/NotificationIdFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.util;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Creates unique ids to identify the notifications created by the android scripting service and the
+ * trigger service.
+ */
+public final class NotificationIdFactory {
+ private static final AtomicInteger mNextId = new AtomicInteger(0);
+
+ public static int create() {
+ return mNextId.incrementAndGet();
+ }
+
+ private NotificationIdFactory() {}
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/RpcUtil.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/RpcUtil.java
new file mode 100644
index 0000000..4601e93
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/RpcUtil.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.google.android.mobly.snippet.util;
+
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.manager.SnippetManager;
+import com.google.android.mobly.snippet.rpc.JsonRpcResult;
+import com.google.android.mobly.snippet.rpc.MethodDescriptor;
+import com.google.android.mobly.snippet.rpc.RpcError;
+import java.util.Locale;
+import java.util.Timer;
+import java.util.TimerTask;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Class that implements APIs to schedule other RPCs.
+ *
+ * <p>If a device is required to be disconnected (e.g., USB power off), no RPCs can be made while
+ * device is offline.
+ *
+ * <p>However, We still need snippet continue to run and execute previously scheduled RPCs
+ *
+ * <p>The return value of the scheduled RPC is cached in {@link EventCache} and can be retrieved
+ * later after device is back online.
+ */
+public class RpcUtil {
+ // RPC ID is used for reporting responses back to the client. However, the results of
+ // scheduled RPCs are reported back to the client via events instead of through synchronous
+ // responses, so the RPC ID is unused. We pass an arbitrary value of 0.
+ private static final int DEFAULT_ID = 0;
+ private final SnippetManager mReceiverManager;
+ private final EventCache mEventCache = EventCache.getInstance();
+
+ public RpcUtil() {
+ mReceiverManager = SnippetManager.getInstance();
+ }
+
+ /**
+ * Schedule given RPC with some delay.
+ *
+ * @param callbackId The callback ID used to cache RPC results.
+ * @param methodName The RPC name to be scheduled.
+ * @param delayMs The delay in ms
+ * @param params Array of the parameters to the RPC
+ */
+ public void scheduleRpc(
+ final String callbackId,
+ final String methodName,
+ final long delayMs,
+ final JSONArray params)
+ throws Throwable {
+ Timer timer = new Timer();
+ TimerTask task =
+ new TimerTask() {
+ @Override
+ public void run() {
+ SnippetEvent event = new SnippetEvent(callbackId, methodName);
+ try {
+ JSONObject obj = invokeRpc(methodName, params, DEFAULT_ID, callbackId);
+ // Cache RPC method return value.
+ for (int i = 0; i < obj.names().length(); i++) {
+ String key = obj.names().getString(i);
+ event.getData().putString(key, obj.get(key).toString());
+ }
+ } catch (JSONException e) {
+ String stackTrace = JsonRpcResult.getStackTrace(e);
+ event.getData().putString("error", stackTrace);
+ } finally {
+ mEventCache.postEvent(event);
+ }
+ }
+ };
+ timer.schedule(task, delayMs);
+ }
+
+ /**
+ * Invoke the RPC.
+ *
+ * @param methodName The RPC name to be invoked.
+ * @param params Array of the parameters to the RPC
+ * @param id The ID that identifies an RPC
+ * @param UID Globally unique session ID.
+ */
+ public JSONObject invokeRpc(String methodName, JSONArray params, int id, Integer UID)
+ throws JSONException {
+ return invokeRpc(methodName, params, id, String.format(Locale.US, "%d-%d", UID, id));
+ }
+
+ /**
+ * Invoke the RPC.
+ *
+ * @param methodName The RPC name to be invoked.
+ * @param params Array of the parameters to the RPC
+ * @param id The ID that identifies an RPC
+ * @param callbackId The callback ID used to cache RPC results.
+ */
+ public JSONObject invokeRpc(String methodName, JSONArray params, int id, String callbackId)
+ throws JSONException {
+ MethodDescriptor rpc = mReceiverManager.getMethodDescriptor(methodName);
+ if (rpc == null) {
+ return JsonRpcResult.error(id, new RpcError("Unknown RPC: " + methodName));
+ }
+ try {
+ JSONArray newParams = new JSONArray();
+ /** If calling an {@link AsyncRpc}, put the message ID as the first param. */
+ if (rpc.isAsync()) {
+ newParams.put(callbackId);
+ for (int i = 0; i < params.length(); i++) {
+ newParams.put(params.get(i));
+ }
+ Object returnValue = rpc.invoke(mReceiverManager, newParams);
+ return JsonRpcResult.callback(id, returnValue, callbackId);
+ } else {
+ Object returnValue = rpc.invoke(mReceiverManager, params);
+ return JsonRpcResult.result(id, returnValue);
+ }
+ } catch (Throwable t) {
+ Log.e("Invocation error.", t);
+ return JsonRpcResult.error(id, t);
+ }
+ }
+}
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java
new file mode 100644
index 0000000..e6f5af7
--- /dev/null
+++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/SnippetLibException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.util;
+
+@SuppressWarnings("serial")
+public class SnippetLibException extends Exception {
+
+ public SnippetLibException(Exception e) {
+ super(e);
+ }
+
+ public SnippetLibException(String message) {
+ super(message);
+ }
+
+ public SnippetLibException(String message, Exception e) {
+ super(message, e);
+ }
+}