diff options
author | frankfeng <frankfeng@google.com> | 2022-01-05 14:52:25 -0800 |
---|---|---|
committer | frankfeng <frankfeng@google.com> | 2022-01-05 14:56:10 -0800 |
commit | 22b92531fc067819abdcb7733ac7696a879ea4e5 (patch) | |
tree | daa905c2cab4f307d9342908fc85ec54ab73bab3 | |
parent | a18b21585728971688495dbe384c61b0332c8ce1 (diff) | |
parent | 52cfcf2fb5af0d7f7f650bea0fb298371687e019 (diff) | |
download | mobly-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
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). @@ -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 @@ -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 Binary files differnew file mode 100644 index 0000000..cde69bc --- /dev/null +++ b/examples/ex2_espresso/src/main/res/mipmap-hdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000..c133a0c --- /dev/null +++ b/examples/ex2_espresso/src/main/res/mipmap-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000..bfa42f0 --- /dev/null +++ b/examples/ex2_espresso/src/main/res/mipmap-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000..324e72c --- /dev/null +++ b/examples/ex2_espresso/src/main/res/mipmap-xxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000..aee44e1 --- /dev/null +++ b/examples/ex2_espresso/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000..13372ae --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar 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 @@ -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 <major> <minor>" upon snippet start + * <li>"SNIPPET SERVING, PORT <port>" 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); + } +} |