aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfrankfeng <frankfeng@google.com>2022-01-05 14:52:25 -0800
committerfrankfeng <frankfeng@google.com>2022-01-05 14:56:10 -0800
commit22b92531fc067819abdcb7733ac7696a879ea4e5 (patch)
treedaa905c2cab4f307d9342908fc85ec54ab73bab3
parenta18b21585728971688495dbe384c61b0332c8ce1 (diff)
parent52cfcf2fb5af0d7f7f650bea0fb298371687e019 (diff)
downloadmobly-snippet-lib-22b92531fc067819abdcb7733ac7696a879ea4e5.tar.gz
Merge remote-tracking branch 'aosp/upstream-master' into merge
Also: add meta files Bug: 213332338 Test: TH Change-Id: Ib83f22bb728cf459fbebf5af7f8bfdcb42387b70
-rw-r--r--.gitignore9
-rw-r--r--CHANGELOG31
-rw-r--r--CONTRIBUTING27
-rw-r--r--LICENSE202
-rw-r--r--METADATA17
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--OWNERS3
-rw-r--r--README.md66
-rw-r--r--build.gradle28
-rw-r--r--examples/ex1_standalone_app/README.md108
-rw-r--r--examples/ex1_standalone_app/build.gradle27
-rw-r--r--examples/ex1_standalone_app/src/main/AndroidManifest.xml27
-rw-r--r--examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet.java30
-rw-r--r--examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java55
-rw-r--r--examples/ex2_espresso/README.md128
-rw-r--r--examples/ex2_espresso/build.gradle56
-rw-r--r--examples/ex2_espresso/src/androidTest/AndroidManifest.xml11
-rw-r--r--examples/ex2_espresso/src/androidTest/java/com/google/android/mobly/snippet/example2/EspressoTest.java48
-rw-r--r--examples/ex2_espresso/src/main/AndroidManifest.xml14
-rw-r--r--examples/ex2_espresso/src/main/java/com/google/android/mobly/snippet/example2/MainActivity.java50
-rw-r--r--examples/ex2_espresso/src/main/res/layout/activity_main.xml23
-rw-r--r--examples/ex2_espresso/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3418 bytes
-rw-r--r--examples/ex2_espresso/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2206 bytes
-rw-r--r--examples/ex2_espresso/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4842 bytes
-rw-r--r--examples/ex2_espresso/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 7718 bytes
-rw-r--r--examples/ex2_espresso/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 10486 bytes
-rw-r--r--examples/ex2_espresso/src/main/res/values-w820dp/dimens.xml6
-rw-r--r--examples/ex2_espresso/src/main/res/values/colors.xml6
-rw-r--r--examples/ex2_espresso/src/main/res/values/dimens.xml5
-rw-r--r--examples/ex2_espresso/src/main/res/values/strings.xml3
-rw-r--r--examples/ex2_espresso/src/main/res/values/styles.xml11
-rw-r--r--examples/ex2_espresso/src/snippet/AndroidManifest.xml14
-rw-r--r--examples/ex2_espresso/src/snippet/java/com/google/android/mobly/snippet/example2/EspressoSnippet.java55
-rw-r--r--examples/ex3_async_event/README.md41
-rw-r--r--examples/ex3_async_event/build.gradle26
-rw-r--r--examples/ex3_async_event/src/main/AndroidManifest.xml16
-rw-r--r--examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java98
-rw-r--r--examples/ex4_uiautomator/README.md45
-rw-r--r--examples/ex4_uiautomator/build.gradle32
-rw-r--r--examples/ex4_uiautomator/src/main/AndroidManifest.xml19
-rw-r--r--examples/ex4_uiautomator/src/main/java/com/google/android/mobly/snippet/example4/UiAutomatorSnippet.java111
-rw-r--r--examples/ex5_schedule_rpc/README.md62
-rw-r--r--examples/ex5_schedule_rpc/build.gradle27
-rw-r--r--examples/ex5_schedule_rpc/src/main/AndroidManifest.xml16
-rw-r--r--examples/ex5_schedule_rpc/src/main/java/com/google/android/mobly/snippet/example5/ExampleScheduleRpcSnippet.java116
-rw-r--r--examples/ex6_complex_type_conversion/README.md108
-rw-r--r--examples/ex6_complex_type_conversion/build.gradle27
-rw-r--r--examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml23
-rw-r--r--examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java21
-rw-r--r--examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java34
-rw-r--r--examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java57
-rw-r--r--gradle.properties29
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 53636 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew160
-rw-r--r--gradlew.bat90
-rw-r--r--settings.gradle9
-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
91 files changed, 5424 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..91f681f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.DS_Store
+.gradle/
+.idea/
+build/
+
+*.iml
+*.pro
+bintray.properties
+local.properties
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..489a2f1
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,31 @@
+1.3.1:
+ - Migrate from android.support to androidx library
+ - Add a default close() method
+ - Support for long arrays as parameters
+ - Use newer Notification methods for Android O and above
+ - Various minor fixes
+
+1.3.0:
+ - Support for RPC scheduling
+ - Support for custom converters of non-primitive types
+ - Support for JSONArray parsing of parameters
+ - Various minor fixes and cleanups
+
+1.2.0:
+ - New startup protocol:
+ - Snippet protocol is now versionated and reported to instrumentation output
+ - Snippet startup progress can now be monitored via output
+ - Server port can now be allocated on device and reported via output
+ - Improvements to help() output
+ - Allow close() methods to throw exceptions
+
+1.1.0:
+ - Support for asynchronous RPCs
+ - Add an optional annotation to cause RPCs to be invoked on the main thread
+ - Log tags are now configurable in the manifest
+
+1.0.1:
+ - Ignore regular JUnit tests linked into the snippet lib
+ - Improved exception handling and reporting
+
+1.0.0: Initial release of Mobly Snippet Lib with some examples
diff --git a/CONTRIBUTING b/CONTRIBUTING
new file mode 100644
index 0000000..1a09deb
--- /dev/null
+++ b/CONTRIBUTING
@@ -0,0 +1,27 @@
+Want to contribute? Great! First, read this page (including the small print at the end).
+
+### Before you contribute
+Before we can use your code, you must sign the
+[Google Individual Contributor License Agreement]
+(https://cla.developers.google.com/about/google-individual)
+(CLA), which you can do online. The CLA is necessary mainly because you own the
+copyright to your changes, even after your contribution becomes part of our
+codebase, so we need your permission to use and distribute your code. We also
+need to be sure of various other things—for instance that you'll tell us if you
+know that your code infringes on other people's patents. You don't have to sign
+the CLA until after you've submitted your code for review and a member has
+approved it, but you must do it before we can put your code into our codebase.
+Before you start working on a larger contribution, you should get in touch with
+us first through the issue tracker with your idea so that we can help out and
+possibly guide you. Coordinating up front makes it much easier to avoid
+frustration later on.
+
+### Code reviews
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose.
+
+### The small print
+Contributions made by corporations are covered by a different agreement than
+the one above, the
+[Software Grant and Corporate Contributor License Agreement]
+(https://cla.developers.google.com/about/google-corporate).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ 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:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) 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
+
+ (d) 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.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..ee44373
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,17 @@
+name: "mobly-snippet-lib"
+description:
+ "Mobly Snippet Lib is a library for triggering device-side code from host-side Mobly tests."
+
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://github.com/google/mobly-snippet-lib"
+ }
+ url {
+ type: GIT
+ value: "https://github.com/google/mobly-snippet-lib"
+ }
+ version: "1.3.1"
+ last_upgrade_date { year: 2022 month: 1 day: 4 }
+ license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..0854447
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,3 @@
+jdesprez@google.com
+frankfeng@google.com
+murj@google.com
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f4447c3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,66 @@
+# Getting Started with Snippets for Mobly
+
+Mobly Snippet Lib is a library for triggering device-side code from host-side
+[Mobly](http://github.com/google/mobly) tests. This tutorial teaches you how to
+use the snippet lib to trigger custom device-side actions.
+
+Note: Mobly and the snippet lib are not official Google products.
+
+
+## Prerequisites
+
+- These examples and tutorials assume basic familiarity with the Mobly
+ framework, so please follow the
+ [Mobly tutorial](http://github.com/google/mobly) before doing this one.
+- You should know how to create an Android app and build it with gradle. If
+ not, follow the
+ [Android app tutorial](https://developer.android.com/training/basics/firstapp/index.html).
+
+
+## Overview
+
+The Mobly Snippet Lib allows you to write Java methods that run on Android
+devices, and trigger the methods from inside a Mobly test case. The Java methods
+invoked this way are called `snippets`.
+
+The `snippet` code can either be written in its own standalone apk, or as a
+[product flavor](https://developer.android.com/studio/build/build-variants.html#product-flavors)
+of an existing apk. This allows you to write snippets that instrument or
+automate another app.
+
+
+## Under The Hood
+
+A snippet is launched by an `am instrument` call. Snippets use a custom
+`InstrumentationTestRunner` derived from `AndroidJUnitRunner`. This allows
+for snippets that interact with a main app's classes, such as Espresso snippets,
+and allows you to get either the test app's or the main app's context from
+`InstrumentationRegistry`.
+
+Once started, the special runner starts a web server which listens for requests
+to trigger snippets. The server's handler locates the corresponding methods by
+reflection, runs them, and returns results over the TCP socket. All common
+built-in variable types are supported as arguments.
+
+
+## Usage
+
+The [examples/](examples/) folder contains examples of how to use the
+mobly snippet lib along with detailed tutorials.
+
+* [ex1_standalone_app](examples/ex1_standalone_app): Basic example of a
+ snippet which is compiled into its own standalone apk.
+* [ex2_espresso](examples/ex2_espresso): Example of a snippet which
+ instruments a primary app to drive its UI using
+ [Espresso](https://google.github.io/android-testing-support-library/docs/espresso/).
+* [ex3_async_event](examples/ex3_async_event): Example of how to use the
+ @AsyncRpc annotation to handle asynchronous callbacks.
+* [ex4_uiautomator](examples/ex4_uiautomator): Example of how to create
+ snippets that automate the UI actions using UIAutomator. Unlike Espresso
+ UIAutomator works even without access to app source code.
+* [ex5_schedule_rpc](examples/ex5_schedule_rpc): Example of how to use the
+ 'scheduleRpc' RPC to execute another RPC at a later time, potentially after
+ device disconnection.
+* [ex6_complex_type_conversion](examples/ex6_complex_type_conversion): Example of how to pass a
+ non-primitive type to the Rpc methods and return non-primitive type from Rpc methods by
+ supplying a type converter.
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..dcd6e86
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,28 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+// For android studio 2.3 and plugin 2.3.3
+
+buildscript {
+ repositories {
+ mavenCentral()
+ google()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.0.2'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ mavenCentral()
+ google()
+ }
+ gradle.projectsEvaluated {
+ tasks.withType(JavaCompile) {
+ options.compilerArgs << "-Xlint:all"
+ }
+ }
+}
+
diff --git a/examples/ex1_standalone_app/README.md b/examples/ex1_standalone_app/README.md
new file mode 100644
index 0000000..5d68e34
--- /dev/null
+++ b/examples/ex1_standalone_app/README.md
@@ -0,0 +1,108 @@
+# Standalone Snippet App Example
+
+This tutorial shows you how to create a standalone Mobly snippet app. To create
+a snippet app that controls (instruments) another app under test, please see
+[Example 2](../ex2_espresso/README.md).
+
+## Tutorial
+
+1. Use Android Studio to create a new app project.
+
+1. Link against Mobly Snippet Lib in your `build.gradle` file
+
+ ```
+ dependencies {
+ implementation 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ }
+ ```
+
+1. Write a Java class implementing `Snippet` and add methods to trigger the
+ behaviour that you want. Annotate them with `@Rpc`
+
+ ```java
+ package com.my.app;
+ ...
+ public class ExampleSnippet implements Snippet {
+ @Rpc(description='Returns a string containing the given number.')
+ public String getFoo(Integer input) {
+ return "foo " + input;
+ }
+
+ @Override
+ public void shutdown() {}
+ }
+ ```
+
+1. Add any classes that implement the `Snippet` interface in your
+ `AndroidManifest.xml` application section as `meta-data`
+
+ ```xml
+ <manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.my.app">
+ <application>
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.my.app.test.MySnippet1,
+ com.my.app.test.MySnippet2" />
+ ...
+ ```
+
+
+1. Add an `instrumentation` tag to your `AndroidManifest.xml` so that the
+ framework can launch your server through an `instrument` command.
+
+ ```xml
+ <manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.my.app">
+ <application>...</application>
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.ServerRunner"
+ android:targetPackage="com.my.app" />
+ </manifest>
+ ```
+
+1. Build your apk and install it on your phone
+
+1. In your Mobly python test, connect to your snippet .apk in `setup_class`
+
+ ```python
+ class HelloWorldTest(base_test.BaseTestClass):
+ def setup_class(self):
+ self.ads = self.register_controller(android_device)
+ self.dut1 = self.ads[0]
+ self.dut1.load_snippet(name='snippet', package='com.my.app.test')
+ ```
+
+6. Invoke your needed functionality within your test
+
+ ```python
+ def test_get_foo(self):
+ actual_foo = self.dut1.snippet.getFoo(5)
+ asserts.assert_equal("foo 5", actual_foo)
+ ```
+
+## Running the example code
+
+This folder contains a fully working example of a standalone snippet apk.
+
+1. Compile the example
+
+ ./gradlew examples:ex1_standalone_app:assembleDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex1_standalone_app/build/outputs/apk/debug/ex1_standalone_app-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `getFoo()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example1
+
+ >>> print(s.help())
+ Known methods:
+ getBar(String) returns String // Returns the given string with the prefix "bar"
+ getFoo(Integer) returns String // Returns the given integer with the prefix "foo"
+
+ >>> s.getFoo(5)
+ u'foo 5'
diff --git a/examples/ex1_standalone_app/build.gradle b/examples/ex1_standalone_app/build.gradle
new file mode 100644
index 0000000..ee60dc1
--- /dev/null
+++ b/examples/ex1_standalone_app/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example1"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.1"
+ }
+ lintOptions {
+ abortOnError false
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardwareIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+}
+
+dependencies {
+ // The 'implementation project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular
+ // 'implementation' dep instead:
+ //implementation 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ implementation project(':mobly-snippet-lib')
+}
diff --git a/examples/ex1_standalone_app/src/main/AndroidManifest.xml b/examples/ex1_standalone_app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..90ebdea
--- /dev/null
+++ b/examples/ex1_standalone_app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example1">
+
+ <application android:allowBackup="false">
+ <!-- Required: list of all classes with @Rpc methods. -->
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example1.ExampleSnippet,
+ com.google.android.mobly.snippet.example1.ExampleSnippet2" />
+
+ <!-- Optional: tag which will be used for logs through the snippet lib's logger.
+ If specified, log lines will look like this:
+ MoblySnippetLibExample1.JsonRpcServer:84: Got shutdown signal
+
+ If not specified, log lines will look like this
+ com.google.android.mobly.snippet.example1.JsonRpcServer:84: Got shutdown signal -->
+ <meta-data
+ android:name="mobly-log-tag"
+ android:value="MoblySnippetLibExample1" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example1" />
+</manifest>
diff --git a/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet.java b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet.java
new file mode 100644
index 0000000..46c55ba
--- /dev/null
+++ b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.example1;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+
+public class ExampleSnippet implements Snippet {
+ @Rpc(description = "Returns the given integer with the prefix \"foo\"")
+ public String getFoo(Integer input) {
+ return "foo " + input;
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java
new file mode 100644
index 0000000..61c3e0a
--- /dev/null
+++ b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example1;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+
+import com.google.android.mobly.snippet.rpc.RunOnUiThread;
+
+import org.json.JSONArray;
+
+import java.io.IOException;
+
+public class ExampleSnippet2 implements Snippet {
+ @Rpc(description = "Returns the given string with the prefix \"bar\"")
+ public String getBar(String input) {
+ return "bar " + input;
+ }
+
+ @Rpc(description = "Returns the given JSON array with the prefix \"bar\"")
+ public String getJSONArray(JSONArray input) {
+ return "bar " + input;
+ }
+
+ @Rpc(description = "Throws an exception")
+ public String throwSomething() throws IOException {
+ throw new IOException("Example exception from throwSomething()");
+ }
+
+ @Rpc(description = "Throws an exception from the main thread")
+ // @RunOnUiThread makes this method execute on the main thread, but only has effect when
+ // invoked as an RPC. It does not affect how this method executes if invoked directly in Java.
+ // This annotation can also be applied to the constructor and the shutdown() method.
+ @RunOnUiThread
+ public String throwSomethingFromMainThread() throws IOException {
+ throw new IOException("Example exception from throwSomethingFromMainThread()");
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/examples/ex2_espresso/README.md b/examples/ex2_espresso/README.md
new file mode 100644
index 0000000..c9422a5
--- /dev/null
+++ b/examples/ex2_espresso/README.md
@@ -0,0 +1,128 @@
+# Espresso Snippet Example
+
+This tutorial shows you how to create snippets that automate the UI of another
+app using Espresso.
+
+The same approach can be used to create any snippet app that needs to access
+the classes or resources of any other single app.
+
+## Overview
+
+To build a snippet that instruments another app, you have to create a new
+[product flavor](https://developer.android.com/studio/build/build-variants.html#product-flavors)
+of your existing app with the snippet code built in.
+
+The snippet code cannot run from a regular test apk because it requires a custom
+`testInstrumentationRunner`.
+
+## Tutorial
+
+1. In the `build.gradle` file of your existing app, create a new product flavor called `snippet`.
+
+ ```
+ android {
+ defaultConfig { ... }
+ productFlavors {
+ main {}
+ snippet {}
+ }
+ }
+ ```
+
+1. Link against Mobly Snippet Lib in your `build.gradle` file
+
+ ```
+ dependencies {
+ snippetCompile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ }
+ ```
+
+1. Create a new source tree called `src/snippet` where you will place the
+ snippet code.
+
+1. In Android Studio, use the `Build Variants` tab in the left hand pane to
+ switch to the snippetDebug build variant. This will let you edit code in the
+ new tree.
+
+1. Write your snippet code in a new class under `src/snippet/java`
+
+ ```java
+ package com.my.app;
+ ...
+ public class EspressoSnippet implements Snippet {
+ @Rpc(description="Pushes the main app button.")
+ public void pushMainButton() {
+ onView(withId(R.id.main_button)).perform(click());
+ }
+
+ @Override
+ public void shutdown() {}
+ }
+ ```
+
+1. Create `src/snippet/AndroidManifest.xml` containing an `<instrument>` block
+ and any classes that implement the `Snippet` interface in `meta-data`
+
+ ```xml
+ <?xml version="1.0" encoding="utf-8"?>
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.my.app.EspressoSnippet" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.my.app" />
+ </manifest>
+ ```
+
+1. Build your apk by invoking the new `assembleSnippetDebug` target.
+
+1. Install the apk on your phone. You do not need to install the main app's
+ apk; the snippet-enabled apk is a complete replacement for your app.
+
+1. In your Mobly python test, connect to your snippet .apk in `setup_class`
+
+ ```python
+ class HelloWorldTest(base_test.BaseTestClass):
+ def setup_class(self):
+ self.ads = self.register_controller(android_device)
+ self.dut1 = self.ads[0]
+ self.dut1.load_snippet(name='snippet', package='com.my.app')
+ ```
+
+6. Invoke your needed functionality within your test
+
+ ```python
+ def test_click_button(self):
+ self.dut1.snippet.pushMainButton()
+ ```
+
+## Running the example code
+
+This folder contains a fully working example of a snippet apk that uses espresso
+to automate a simple app.
+
+1. Compile the example
+
+ ./gradlew examples:ex2_espresso:assembleSnippetDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex2_espresso/build/outputs/apk/snippet/debug/ex2_espresso-snippet-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `pushMainButton()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example2
+
+ >>> print(s.help())
+ Known methods:
+ pushMainButton(boolean) returns void // Pushes the main app button, and checks the label if this is the first time.
+ startMainActivity() returns void // Opens the main activity of the app
+
+ >>> s.startMainActivity()
+ >>> s.pushMainButton(True)
+
+1. Press ctrl+d to exit the shell and terminate the app.
diff --git a/examples/ex2_espresso/build.gradle b/examples/ex2_espresso/build.gradle
new file mode 100644
index 0000000..4479e1b
--- /dev/null
+++ b/examples/ex2_espresso/build.gradle
@@ -0,0 +1,56 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 31
+ flavorDimensions "examples"
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example2"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.1"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ productFlavors {
+ original {
+ dimension "examples"
+ }
+ snippet {
+ testApplicationId "com.google.android.mobly.snippet.example2.snippet"
+ dimension "examples"
+ }
+ }
+
+ lintOptions {
+ abortOnError true
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardcodedText', 'UnusedIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.4.0-beta01'
+ implementation 'androidx.test:runner:1.4.0'
+
+ // The androidTest package is not for snippet support; it shows an example
+ // of an instrumentation test coexisting with a snippet in the same
+ // codebase.
+ androidTestImplementation 'androidx.annotation:annotation:1.2.0'
+ androidTestImplementation 'androidx.test:runner:1.4.0'
+ androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+
+ // The 'snippetCompile project' dep is to compile against the snippet lib
+ // source in this repo. For your own snippets, you'll want to use the
+ // regular 'snippetCompile' dep instead:
+ //snippetCompile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ snippetImplementation project(':mobly-snippet-lib')
+
+ snippetImplementation 'androidx.annotation:annotation:1.2.0'
+ snippetImplementation 'androidx.test:rules:1.4.0'
+ snippetImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
diff --git a/examples/ex2_espresso/src/androidTest/AndroidManifest.xml b/examples/ex2_espresso/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..0dc87bc
--- /dev/null
+++ b/examples/ex2_espresso/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example2.test">
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example2" />
+</manifest>
diff --git a/examples/ex2_espresso/src/androidTest/java/com/google/android/mobly/snippet/example2/EspressoTest.java b/examples/ex2_espresso/src/androidTest/java/com/google/android/mobly/snippet/example2/EspressoTest.java
new file mode 100644
index 0000000..f41374e
--- /dev/null
+++ b/examples/ex2_espresso/src/androidTest/java/com/google/android/mobly/snippet/example2/EspressoTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example2;
+
+import androidx.test.espresso.action.ViewActions;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+/**
+ * This test is not part of the snippet code. It's a regular espresso instrumentation test which
+ * shows how regular tests can coexist with snippets in the source tree.
+ */
+@RunWith(AndroidJUnit4.class)
+public class EspressoTest {
+ @Rule
+ public ActivityTestRule<MainActivity> mActivityRule =
+ new ActivityTestRule<>(MainActivity.class);
+
+ @Test
+ public void espressoTest() {
+ onView(withId(R.id.main_text_view)).check(matches(withText("Hello World!")));
+ onView(withId(R.id.main_button)).perform(ViewActions.click());
+ onView(withId(R.id.main_text_view)).check(matches(withText("Button pressed 1 times.")));
+ }
+}
diff --git a/examples/ex2_espresso/src/main/AndroidManifest.xml b/examples/ex2_espresso/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..33f853f
--- /dev/null
+++ b/examples/ex2_espresso/src/main/AndroidManifest.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example2">
+
+ <application android:allowBackup="true" android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme">
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/examples/ex2_espresso/src/main/java/com/google/android/mobly/snippet/example2/MainActivity.java b/examples/ex2_espresso/src/main/java/com/google/android/mobly/snippet/example2/MainActivity.java
new file mode 100644
index 0000000..0cc0b54
--- /dev/null
+++ b/examples/ex2_espresso/src/main/java/com/google/android/mobly/snippet/example2/MainActivity.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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.example2;
+
+import java.util.Locale;
+import androidx.appcompat.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+public class MainActivity extends AppCompatActivity {
+ private TextView mTextView;
+ private Button mButton;
+ private int mNumPressed;
+
+ /**
+ * Attaches a simple listener that increments the text in the textbox whenever the button is
+ * pressed.
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ mTextView = (TextView) findViewById(R.id.main_text_view);
+ mButton = (Button) findViewById(R.id.main_button);
+ mButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mNumPressed++;
+ mTextView.setText(String.format(Locale.ROOT, "Button pressed %d times.", mNumPressed));
+ }
+ });
+ }
+}
diff --git a/examples/ex2_espresso/src/main/res/layout/activity_main.xml b/examples/ex2_espresso/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..e190302
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/layout/activity_main.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main"
+ android:layout_width="match_parent" android:layout_height="match_parent"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ tools:context="com.google.android.mobly.snippet.example2.MainActivity">
+
+<TextView
+ android:id="@+id/main_text_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:text="Hello World!" />
+
+<Button
+ android:id="@+id/main_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Push the button!" />
+</RelativeLayout>
diff --git a/examples/ex2_espresso/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/ex2_espresso/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/examples/ex2_espresso/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/ex2_espresso/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/examples/ex2_espresso/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/ex2_espresso/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/ex2_espresso/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/ex2_espresso/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/ex2_espresso/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/ex2_espresso/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/ex2_espresso/src/main/res/values-w820dp/dimens.xml b/examples/ex2_espresso/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+ (such as screen margins) for screens with more than 820dp of available width. This
+ would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+ <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/examples/ex2_espresso/src/main/res/values/colors.xml b/examples/ex2_espresso/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/examples/ex2_espresso/src/main/res/values/dimens.xml b/examples/ex2_espresso/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..47c8224
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/examples/ex2_espresso/src/main/res/values/strings.xml b/examples/ex2_espresso/src/main/res/values/strings.xml
new file mode 100644
index 0000000..c46b9d8
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">Mobly Snippet Espresso Example</string>
+</resources>
diff --git a/examples/ex2_espresso/src/main/res/values/styles.xml b/examples/ex2_espresso/src/main/res/values/styles.xml
new file mode 100644
index 0000000..daa2a5c
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+</resources>
diff --git a/examples/ex2_espresso/src/snippet/AndroidManifest.xml b/examples/ex2_espresso/src/snippet/AndroidManifest.xml
new file mode 100644
index 0000000..85797ce
--- /dev/null
+++ b/examples/ex2_espresso/src/snippet/AndroidManifest.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example2.EspressoSnippet" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example2" />
+
+</manifest>
diff --git a/examples/ex2_espresso/src/snippet/java/com/google/android/mobly/snippet/example2/EspressoSnippet.java b/examples/ex2_espresso/src/snippet/java/com/google/android/mobly/snippet/example2/EspressoSnippet.java
new file mode 100644
index 0000000..8ea7c21
--- /dev/null
+++ b/examples/ex2_espresso/src/snippet/java/com/google/android/mobly/snippet/example2/EspressoSnippet.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example2;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import androidx.test.espresso.action.ViewActions;
+import androidx.test.rule.ActivityTestRule;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import org.junit.Rule;
+
+public class EspressoSnippet implements Snippet {
+ @Rule
+ public ActivityTestRule<MainActivity> mActivityRule =
+ new ActivityTestRule<>(MainActivity.class);
+
+ @Rpc(description="Opens the main activity of the app")
+ public void startMainActivity() {
+ mActivityRule.launchActivity(null /* startIntent */);
+ }
+
+ @Rpc(description="Pushes the main app button, and checks the label if this is the first time.")
+ public void pushMainButton(boolean checkFirstRun) {
+ if (checkFirstRun) {
+ onView(withId(R.id.main_text_view)).check(matches(withText("Hello World!")));
+ }
+ onView(withId(R.id.main_button)).perform(ViewActions.click());
+ if (checkFirstRun) {
+ onView(withId(R.id.main_text_view)).check(matches(withText("Button pressed 1 times.")));
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ mActivityRule.getActivity().finish();
+ }
+}
diff --git a/examples/ex3_async_event/README.md b/examples/ex3_async_event/README.md
new file mode 100644
index 0000000..edb4fbd
--- /dev/null
+++ b/examples/ex3_async_event/README.md
@@ -0,0 +1,41 @@
+# Async Event RPC Example
+
+This example shows you how to use the @AsyncRpc annotation of Mobly snippet lib
+to handle asynchronous callbacks.
+
+See the source code in `ExampleAsyncSnippet.java` for details.
+
+## Running the example code
+
+This folder contains a fully working example of a standalone snippet apk.
+
+1. Compile the example
+
+ ./gradlew examples:ex3_async_event:assembleDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex3_async_event/build/outputs/apk/debug/ex3_async_event-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `tryEvent()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example3
+
+ >>> handler = s.tryEvent(42)
+ >>> print("Not blocked, can do stuff here")
+ >>> event = handler.waitAndGet('AsyncTaskResult') # Blocks until the event is received
+
+ Now let's see the content of the event
+
+ >>> import pprint
+ >>> pprint.pprint(event)
+ {
+ 'callbackId': '2-1',
+ 'name': 'AsyncTaskResult',
+ 'time': 20460228696,
+ 'data': {
+ 'exampleData': "Here's a simple event.",
+ 'successful': True,
+ 'secretNumber': 12
+ }
+ }
diff --git a/examples/ex3_async_event/build.gradle b/examples/ex3_async_event/build.gradle
new file mode 100644
index 0000000..7327fc4
--- /dev/null
+++ b/examples/ex3_async_event/build.gradle
@@ -0,0 +1,26 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example3"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.1"
+ }
+ lintOptions {
+ abortOnError false
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardwareIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+}
+
+dependencies {
+ // The 'compile project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular 'compile' dep instead:
+ // compile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ implementation project(':mobly-snippet-lib')
+}
diff --git a/examples/ex3_async_event/src/main/AndroidManifest.xml b/examples/ex3_async_event/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..dcc724b
--- /dev/null
+++ b/examples/ex3_async_event/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example3">
+
+ <application android:allowBackup="false">
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example3.ExampleAsyncSnippet" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example3" />
+
+</manifest>
diff --git a/examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java b/examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java
new file mode 100644
index 0000000..a2b22fa
--- /dev/null
+++ b/examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java
@@ -0,0 +1,98 @@
+/*
+ * 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.example3;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.rpc.AsyncRpc;
+import com.google.android.mobly.snippet.util.Log;
+
+public class ExampleAsyncSnippet implements Snippet {
+
+ private final EventCache mEventCache = EventCache.getInstance();
+
+ /**
+ * This is a sample asynchronous task.
+ *
+ * In real world use cases, it can be a {@link android.content.BroadcastReceiver}, a Listener,
+ * or any other kind asynchronous callback class.
+ */
+ public class AsyncTask implements Runnable {
+
+ private final String mCallbackId;
+ private final int mSecretNumber;
+
+ public AsyncTask(String callbackId, int secreteNumber) {
+ this.mCallbackId = callbackId;
+ this.mSecretNumber = secreteNumber;
+ }
+
+ /**
+ * Sleeps for 10s then post a {@link SnippetEvent} with some data.
+ *
+ * If the sleep is interrupted, a {@link SnippetEvent} signaling failure will be posted instead.
+ */
+ public void run() {
+ Log.d("Sleeping for 10s before posting an event.");
+ SnippetEvent event = new SnippetEvent(mCallbackId, "AsyncTaskResult");
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ event.getData().putBoolean("successful", false);
+ event.getData().putString("reason", "Sleep was interrupted.");
+ mEventCache.postEvent(event);
+ }
+ event.getData().putBoolean("successful", true);
+ event.getData().putString("exampleData", "Here's a simple event.");
+ event.getData().putInt("secretNumber", mSecretNumber);
+ mEventCache.postEvent(event);
+ }
+ }
+
+ /**
+ * An Rpc method demonstrating the async event mechanism.
+ *
+ * This call returns immediately, but starts a task in a separate thread which will post an
+ * event 10s after the task was started.
+ *
+ * Expect to see an event on the client side that looks like:
+ *
+ * {
+ * 'callbackId': '2-1',
+ * 'name': 'AsyncTaskResult',
+ * 'time': 20460228696,
+ * 'data': {
+ * 'exampleData': "Here's a simple event.",
+ * 'successful': True,
+ * 'secretNumber': 12
+ * }
+ * }
+ *
+ * @param callbackId The ID that should be used to tag {@link SnippetEvent} objects triggered by
+ * this method.
+ * @throws InterruptedException
+ */
+ @AsyncRpc(description = "This triggers an async event and returns.")
+ public void tryEvent(String callbackId, int secretNumber) throws InterruptedException {
+ Runnable asyncTask = new AsyncTask(callbackId, secretNumber);
+ Thread thread = new Thread(asyncTask);
+ thread.start();
+ }
+ @Override
+ public void shutdown() {}
+}
diff --git a/examples/ex4_uiautomator/README.md b/examples/ex4_uiautomator/README.md
new file mode 100644
index 0000000..10fb144
--- /dev/null
+++ b/examples/ex4_uiautomator/README.md
@@ -0,0 +1,45 @@
+# UIAutomator Snippet Example
+
+This example shows you how to create snippets that control the UI of a device
+across system and multiple app views using UIAutomator. Unlike Espresso-based
+UI automation, it does not require access to app source code.
+
+This snippet is written as a [standalone snippet](../ex1_standalone_app/README.md)
+and does not target another app. In particular, it doesn't need to target the
+app under test, so it doesn't need its classpath or to be signed with the same
+key.
+
+See the [Espresso snippet tutorial](../ex2_espresso/README.md) for more
+information about the app this example automates.
+
+## Running the example code
+
+This folder contains a fully working example of a snippet apk that uses
+UIAutomator to automate a simple app.
+
+1. Compile the main app and automation. The main app of ex2 (espresso) is used
+ as the app to automate. Unlike espresso, the uiautomator test does not
+ depend on this apk and does not use its source or classpath, so you must
+ compile and install the app separately.
+
+ ./gradlew examples:ex2_espresso:assembleDebug examples:ex4_uiautomator:assembleDebug
+
+1. Install the apks on your phone
+
+ adb install -r ./examples/ex2_espresso/build/outputs/apk/debug/ex2_espresso-main-debug.apk
+ adb install -r ./examples/ex4_uiautomator/build/outputs/apk/debug/ex4_uiautomator-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `pushMainButton()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example4
+
+ >>> print(s.help())
+ Known methods:
+ pushMainButton(boolean) returns void // Pushes the main app button, and checks the label if this is the first time.
+ startMainActivity() returns void // Opens the main activity of the app
+ uiautomatorDump() returns String // Perform a UIAutomator dump
+
+ >>> s.startMainActivity()
+ >>> s.pushMainButton(True)
+
+1. Press ctrl+d to exit the shell and terminate the app.
diff --git a/examples/ex4_uiautomator/build.gradle b/examples/ex4_uiautomator/build.gradle
new file mode 100644
index 0000000..b071e1b
--- /dev/null
+++ b/examples/ex4_uiautomator/build.gradle
@@ -0,0 +1,32 @@
+apply plugin: 'com.android.application'
+
+android {
+ // This has to match what the appcompat dep expects.
+ compileSdkVersion 31
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example4"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.2"
+ }
+ lintOptions {
+ abortOnError false
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardwareIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+}
+
+dependencies {
+ // The 'compile project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular
+ // 'compile' dep instead:
+ //compile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ implementation project(':mobly-snippet-lib')
+ implementation 'junit:junit:4.13.2'
+ implementation 'androidx.test:runner:1.4.0'
+ implementation 'androidx.appcompat:appcompat:1.4.0-beta01'
+ implementation 'androidx.test.uiautomator:uiautomator:2.2.0'
+}
diff --git a/examples/ex4_uiautomator/src/main/AndroidManifest.xml b/examples/ex4_uiautomator/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..89d5276
--- /dev/null
+++ b/examples/ex4_uiautomator/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example4">
+
+ <application android:allowBackup="false">
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example4.UiAutomatorSnippet" />
+ </application>
+
+ <!-- This snippet does NOT target ex2 (which is the main app the code
+ automates). The instrumentation target is itself which creates a
+ standalone snippet. -->
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example4" />
+
+</manifest>
diff --git a/examples/ex4_uiautomator/src/main/java/com/google/android/mobly/snippet/example4/UiAutomatorSnippet.java b/examples/ex4_uiautomator/src/main/java/com/google/android/mobly/snippet/example4/UiAutomatorSnippet.java
new file mode 100644
index 0000000..9fc01b2
--- /dev/null
+++ b/examples/ex4_uiautomator/src/main/java/com/google/android/mobly/snippet/example4/UiAutomatorSnippet.java
@@ -0,0 +1,111 @@
+/*
+ * 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.example4;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+/**
+ * Demonstrates how to drive an app using UIAutomator without access to the app's source code or
+ * classpath.
+ *
+ * <p>Drives the Espresso example app from ex2 without instrumenting it.
+ */
+public class UiAutomatorSnippet implements Snippet {
+ private static final class UiAutomatorSnippetException extends Exception {
+ private static final long serialVersionUID = 1;
+
+ public UiAutomatorSnippetException(String message) {
+ super(message);
+ }
+ }
+
+ private static final String MAIN_PACKAGE = "com.google.android.mobly.snippet.example2";
+ private static final int LAUNCH_TIMEOUT = 5000;
+
+ private final Context mContext;
+ private final UiDevice mDevice;
+
+ public UiAutomatorSnippet() {
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ }
+
+ @Rpc(description="Opens the main activity of the app")
+ public void startMainActivity() throws UiAutomatorSnippetException {
+ // Send the launch intent
+ Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(MAIN_PACKAGE);
+ if (intent == null) {
+ throw new UiAutomatorSnippetException(
+ "Unable to create launch intent for " + MAIN_PACKAGE + "; is the app installed?");
+ }
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ mContext.startActivity(intent);
+
+ // Wait for the app to appear
+ mDevice.wait(Until.hasObject(By.pkg(MAIN_PACKAGE).depth(0)), LAUNCH_TIMEOUT);
+ }
+
+ @Rpc(description="Pushes the main app button, and checks the label if this is the first time.")
+ public void pushMainButton(boolean checkFirstRun) {
+ if (checkFirstRun) {
+ assertEquals(
+ "Hello World!",
+ // Example of finding object by id.
+ mDevice.findObject(By.res(MAIN_PACKAGE, "main_text_view")).getText());
+ }
+ // Example of finding a button by text. Finding by ID is also possible, as above.
+ UiObject2 button = mDevice.findObject(By.text("PUSH THE BUTTON!"));
+ button.click();
+ if (checkFirstRun) {
+ assertEquals(
+ "Button pressed 1 times",
+ mDevice.findObject(By.res(MAIN_PACKAGE, "main_text_view")).getText());
+ }
+ }
+
+ @Rpc(description="Perform a UIAutomator dump")
+ public String uiautomatorDump() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ mDevice.dumpWindowHierarchy(baos);
+ byte[] dumpBytes = baos.toByteArray();
+ String dumpStr = new String(dumpBytes, Charset.forName("UTF-8"));
+ return dumpStr;
+ } finally {
+ baos.close();
+ }
+ }
+
+ @Override
+ public void shutdown() throws IOException {
+ mDevice.executeShellCommand("am force-stop " + MAIN_PACKAGE);
+ }
+}
diff --git a/examples/ex5_schedule_rpc/README.md b/examples/ex5_schedule_rpc/README.md
new file mode 100644
index 0000000..4ccd10f
--- /dev/null
+++ b/examples/ex5_schedule_rpc/README.md
@@ -0,0 +1,62 @@
+# Scheduling RPCs Example
+
+This example shows you how to use `scheduleRpc` which is built into
+Mobly snippet lib to handle RPC scheduling.
+
+## Why this is needed?
+
+Some tests may need a snippet RPC to execute when the snippet client is unable
+to reach the device, e.g., performing test actions while USB is disconnected.
+For example, for battery testing (with Monsoon devices), we may want to measure
+power consumed during certain test actions (e.g., phone calls). However
+a Monsoon device turns off USB during battery data measurement, and a regular
+snippet RPC won't work when the client is not connected to the device.
+Therefore, prior to starting the Monsoon measurement we need to schedule a phone
+call RPC prior to soccur during the measurement period.
+
+In this scenario, the test steps would be:
+
+1. Schedule the `makePhoneCall('123456')` to execute after (e.g., 10 seconds):
+
+ s.scheduleRpc('makePhoneCall', 10000, ['123456'])
+
+2. Start a Monsoon device to collect battery data, while simultaneously USB is
+ turned off.
+3. After 10 seconds, the phone call starts while USB is off.
+4. Finally, after the phone call is finished, Monsoon data collection completes
+ and USB is re-enabled.
+5. The test retrieves any cached events or data from the device.
+
+
+
+See the source code ExampleScheduleRpcSnippet.java for details.
+
+## Running the example code
+
+This folder contains a fully working example of a standalone snippet apk.
+
+1. Compile the example
+
+ ./gradlew examples:ex5_schedule_rpc:assembleDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex5_schedule_rpc/build/outputs/apk/debug/ex5_schedule_rpc-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `tryEvent()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example5
+
+ >>> callback = s.scheduleRpc('makeToast', 5000, ['message'])
+
+ Wait for the message to show up on the screen (sync RPC call)
+
+ >>> callback.waitAndGet('makeToast').data
+ {u'callback': u'null', u'error': u'null', u'result': u'OK', u'id': u'0'}
+
+ >>> callback = s.scheduleRpc('asyncMakeToast', 5000, ['message'])
+
+ Wait for the message to show up on the screen (async RPC call)
+
+ >>> callback.waitAndGet('asyncMakeToast').data
+ {u'callback': u'1-1', u'error': u'null', u'result': u'null', u'id': u'0'}
diff --git a/examples/ex5_schedule_rpc/build.gradle b/examples/ex5_schedule_rpc/build.gradle
new file mode 100644
index 0000000..f5aa15c
--- /dev/null
+++ b/examples/ex5_schedule_rpc/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example5"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.1"
+ }
+ lintOptions {
+ abortOnError false
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardwareIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+}
+
+dependencies {
+ // The 'compile project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular 'compile' dep instead:
+ // compile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ implementation project(':mobly-snippet-lib')
+ implementation 'androidx.test:runner:1.4.0'
+}
diff --git a/examples/ex5_schedule_rpc/src/main/AndroidManifest.xml b/examples/ex5_schedule_rpc/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9a3271c
--- /dev/null
+++ b/examples/ex5_schedule_rpc/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example5">
+
+ <application android:allowBackup="false">
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example5.ExampleScheduleRpcSnippet" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example5" />
+
+</manifest>
diff --git a/examples/ex5_schedule_rpc/src/main/java/com/google/android/mobly/snippet/example5/ExampleScheduleRpcSnippet.java b/examples/ex5_schedule_rpc/src/main/java/com/google/android/mobly/snippet/example5/ExampleScheduleRpcSnippet.java
new file mode 100644
index 0000000..a94d68a
--- /dev/null
+++ b/examples/ex5_schedule_rpc/src/main/java/com/google/android/mobly/snippet/example5/ExampleScheduleRpcSnippet.java
@@ -0,0 +1,116 @@
+/*
+ * 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.example5;
+
+import android.content.Context;
+import android.os.Handler;
+import androidx.test.InstrumentationRegistry;
+import android.widget.Toast;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.rpc.AsyncRpc;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.util.Log;
+
+/**
+ * Demonstrates how to schedule an RPC.
+ */
+public class ExampleScheduleRpcSnippet implements Snippet {
+
+ /**
+ * This is a sample asynchronous task.
+ *
+ * In real world use cases, it can be a {@link android.content.BroadcastReceiver}, a Listener,
+ * or any other kind asynchronous callback class.
+ */
+ public class AsyncTask implements Runnable {
+
+ private final String mCallbackId;
+ private final String mMessage;
+
+ public AsyncTask(String callbackId, String message) {
+ this.mCallbackId = callbackId;
+ this.mMessage = message;
+ }
+
+ /**
+ * Sleeps for 10s then make toast and post a {@link SnippetEvent} with some data.
+ *
+ * <p>If the sleep is interrupted, a {@link SnippetEvent} signaling failure will be posted
+ * instead.
+ */
+ @Override
+ public void run() {
+ Log.d("Sleeping for 10s before posting an event.");
+ SnippetEvent event = new SnippetEvent(mCallbackId, mMessage);
+ try {
+ Thread.sleep(10000);
+ showToast(mMessage);
+ } catch (InterruptedException e) {
+ event.getData().putBoolean("successful", false);
+ event.getData().putString("reason", "Sleep was interrupted.");
+ mEventCache.postEvent(event);
+ }
+ event.getData().putBoolean("successful", true);
+ event.getData().putString("eventName", mMessage);
+ mEventCache.postEvent(event);
+ }
+ }
+
+ private final Context mContext;
+ private final EventCache mEventCache = EventCache.getInstance();
+
+ /**
+ * Since the APIs here deal with UI, most of them have to be called in a thread that has called
+ * looper.
+ */
+ private final Handler mHandler;
+
+ public ExampleScheduleRpcSnippet() {
+ mContext = InstrumentationRegistry.getContext();
+ mHandler = new Handler(mContext.getMainLooper());
+ }
+
+ @Rpc(description = "Make a toast on screen.")
+ public String makeToast(String message) throws InterruptedException {
+ showToast(message);
+ return "OK";
+ }
+
+ @AsyncRpc(description = "Make a toast on screen after some time.")
+ public void asyncMakeToast(String callbackId, String message)
+ throws Throwable {
+ Runnable asyncTask = new AsyncTask(callbackId, "asyncMakeToast");
+ Thread thread = new Thread(asyncTask);
+ thread.start();
+ }
+
+ @Override
+ public void shutdown() {}
+
+ private void showToast(final String message) {
+ mHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+}
+
diff --git a/examples/ex6_complex_type_conversion/README.md b/examples/ex6_complex_type_conversion/README.md
new file mode 100644
index 0000000..104d91b
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/README.md
@@ -0,0 +1,108 @@
+# Complex Type Conversion in Snippet Example
+
+This tutorial shows you how to use a custom object in Snippet Lib.
+
+This example assumes basic familiarity with Snippet Lib as demonstrated in
+[Example 1](../ex1_standalone_app/README.md).
+
+## Tutorial
+
+1. Use Android Studio to create a new app project, similar to
+ [Example 1](../ex1_standalone_app/README.md).
+
+1. Create a complex type in Java:
+ ```java
+ public class CustomType {
+ private String myValue;
+ CustomType(String value) {
+ myValue = value;
+ }
+
+ String getMyValue() {
+ return myValue;
+ }
+ public void setMyValue(String newValue) {
+ myValue = newValue;
+ }
+ }
+ ```
+1. Create a Java class implementing `SnippetObjectConverter`, which defines how the complex type
+ should be converted against `JSONObject`:
+ ```java
+ public class ExampleObjectConverter implements SnippetObjectConverter {
+ @Override
+ public JSONObject serialize(Object object) throws JSONException {
+ JSONObject result = new JSONObject();
+ if (object instanceof CustomType) {
+ CustomType input = (CustomType) object;
+ result.put("Value", input.getMyValue());
+ return result;
+ }
+ return null;
+ }
+
+ @Override
+ public Object deserialize(JSONObject jsonObject, Type type) throws JSONException {
+ if (type == CustomType.class) {
+ return new CustomType(jsonObject.getString("Value"));
+ }
+ return null;
+ }
+ }
+ ```
+1. Write a Java class implementing `Snippet` and add Rpc methods that takes your complex type as
+ a parameter and another Rpc method that returns the complext type directly.
+
+ ```java
+ package com.my.app;
+ ...
+ public class ExampleSnippet implements Snippet {
+ @Rpc(description = "Pass a complex type as a snippet parameter.")
+ public String passComplexTypeToSnippet(CustomType input) {
+ Log.i("Old value is: " + input.getMyValue());
+ return "The value in CustomType is: " + input.getMyValue();
+ }
+
+ @Rpc(description = "Returns a complex type from snippet.")
+ public CustomType returnComplexTypeFromSnippet(String value) {
+ return new CustomType(value);
+ }
+ @Override
+ public void shutdown() {}
+ }
+ ```
+
+1. In `AndroidManifest.xml`, specify the converter class as a `meta-data` named
+ `mobly-object-converter`
+
+ ```xml
+ <manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.my.app">
+ <application>
+ <meta-data
+ android:name="mobly-object-converter"
+ android:value="com.my.app.ExampleObjectConverter" />
+ ...
+ ```
+
+## Running the example code
+
+This folder contains a fully working example of a standalone snippet apk.
+
+1. Compile the example
+
+ ./gradlew examples:ex6_complex_type_conversion:assembleDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex6_complex_type_conversion/build/outputs/apk/debug/ex6_complex_type_conversion-debug.apk
+
+1. Use Mobly's `snippet_shell` from mobly to trigger the Rpc methods:
+
+ snippet_shell.py com.google.android.mobly.snippet.example6
+
+ >>> s.passComplexTypeToSnippet({'Value': 'Hello'})
+ 'The value in CustomType is: Hello'
+ >>> s.returnComplexTypeFromSnippet('Bye')
+ {'Value': 'Bye'}
diff --git a/examples/ex6_complex_type_conversion/build.gradle b/examples/ex6_complex_type_conversion/build.gradle
new file mode 100644
index 0000000..b6039b0
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 31
+ buildToolsVersion '31.0.0'
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example6"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.1"
+ }
+ lintOptions {
+ abortOnError true
+ checkAllWarnings true
+ warningsAsErrors true
+ }
+}
+
+dependencies {
+ // The 'implementation project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular
+ // 'implementation' dep instead:
+ //implementation 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ implementation project(':mobly-snippet-lib')
+}
diff --git a/examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml b/examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c1548e7
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example6">
+
+ <application>
+ <!-- Required: list of all classes with @Rpc methods. -->
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example6.ExampleSnippet" />
+ <!-- Optional: a class used for converting Java objects to/from JSON. -->
+ <meta-data
+ android:name="mobly-object-converter"
+ android:value="com.google.android.mobly.snippet.example6.ExampleObjectConverter" />
+ <meta-data
+ android:name="mobly-log-tag"
+ android:value="MoblySnippetLibExample6" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example6" />
+</manifest>
diff --git a/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java
new file mode 100644
index 0000000..223b63e
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java
@@ -0,0 +1,21 @@
+package com.google.android.mobly.snippet.example6;
+
+/**
+ * A data class that defines a non-primitive type.
+ *
+ * This type is used to demonstrate serialization and de-serialization of complex type objects in
+ * Mobly Snippet Lib for Android.
+ */
+public class CustomType {
+ private String myValue;
+ CustomType(String value) {
+ myValue = value;
+ }
+
+ String getMyValue() {
+ return myValue;
+ }
+ public void setMyValue(String newValue) {
+ myValue = newValue;
+ }
+}
diff --git a/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java
new file mode 100644
index 0000000..eea8831
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java
@@ -0,0 +1,34 @@
+package com.google.android.mobly.snippet.example6;
+
+import com.google.android.mobly.snippet.SnippetObjectConverter;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.reflect.Type;
+
+
+/**
+ * Example showing how to supply custom object converter to Mobly Snippet Lib.
+ */
+
+public class ExampleObjectConverter implements SnippetObjectConverter {
+ @Override
+ public JSONObject serialize(Object object) throws JSONException {
+ JSONObject result = new JSONObject();
+ if (object instanceof CustomType) {
+ CustomType input = (CustomType) object;
+ result.put("Value", input.getMyValue());
+ return result;
+ }
+ return null;
+ }
+
+ @Override
+ public Object deserialize(JSONObject jsonObject, Type type) throws JSONException {
+ if (type == CustomType.class) {
+ return new CustomType(jsonObject.getString("Value"));
+ }
+ return null;
+ }
+}
diff --git a/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java
new file mode 100644
index 0000000..3306f85
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java
@@ -0,0 +1,57 @@
+/*
+ * 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.example6;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Example snippet showing converting complex type objects using custom logic.
+ *
+ * For complex types in Java, one can supply a custom object converter to Snippet Lib to specify how
+ * each complex type should be serialized/de-serialized. With this, users don't have to explicitly
+ * call a serializer or de-serializer in every single Rpc method, which simplifies code.
+ */
+public class ExampleSnippet implements Snippet {
+ @Rpc(description = "Pass a complex type as a snippet parameter.")
+ public String passComplexTypeToSnippet(CustomType input) {
+ Log.i("Old value is: " + input.getMyValue());
+ return "The value in CustomType is: " + input.getMyValue();
+ }
+
+ @Rpc(description = "Returns a complex type from snippet.")
+ public CustomType returnComplexTypeFromSnippet(String value) {
+ return new CustomType(value);
+ }
+
+ /**
+ * Demonstrates serialization/de-serialization of a collection of custom type objects.
+ */
+ @Rpc(description = "Update values for multiple CustomType objects.")
+ public ArrayList<CustomType> updateValues(ArrayList<CustomType> objects, String newValue) {
+ for (CustomType obj : objects) {
+ obj.setMyValue(newValue);
+ }
+ return objects;
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..ec6a751
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,29 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+android.useAndroidX=true
+android.enableJetifier=true
+
+# Key for signing the release
+signing.keyId=<your-key-id>
+signing.password=<your-key-password>
+signing.secretKeyRingFile=<path-to-your-secret-key-ring-file>
+
+# Credentials for OSSRH
+ossrhUsername=<your-jira-username>
+ossrhPassword=<your-jira-password>
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..cc8de8e
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Oct 05 19:50:22 PDT 2021
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..08025db
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,9 @@
+include (
+ ':mobly-snippet-lib',
+ ':examples:ex1_standalone_app',
+ ':examples:ex2_espresso',
+ ':examples:ex3_async_event',
+ ':examples:ex4_uiautomator',
+ ':examples:ex5_schedule_rpc',
+ ':examples:ex6_complex_type_conversion')
+project(":mobly-snippet-lib").projectDir = file('third_party/sl4a')
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);
+ }
+}