aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorfrankfeng <frankfeng@google.com>2022-01-05 23:53:39 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2022-01-05 23:53:39 +0000
commit7151acd88deae8709cb212b20494a4c38ecc8263 (patch)
treedaa905c2cab4f307d9342908fc85ec54ab73bab3 /examples
parenta18b21585728971688495dbe384c61b0332c8ce1 (diff)
parenta3c4e39314251b98b0ee70da2bb63dbc52137ac7 (diff)
downloadmobly-snippet-lib-7151acd88deae8709cb212b20494a4c38ecc8263.tar.gz
Merge remote-tracking branch 'aosp/upstream-master' into merge am: 22b92531fc am: 7902e5747e am: a3c4e39314
Original change: https://android-review.googlesource.com/c/platform/external/mobly-snippet-lib/+/1932020 Change-Id: Id8702566f1ba34c44a867e3f61f4af94b7b4006d
Diffstat (limited to 'examples')
-rw-r--r--examples/ex1_standalone_app/README.md108
-rw-r--r--examples/ex1_standalone_app/build.gradle27
-rw-r--r--examples/ex1_standalone_app/src/main/AndroidManifest.xml27
-rw-r--r--examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet.java30
-rw-r--r--examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java55
-rw-r--r--examples/ex2_espresso/README.md128
-rw-r--r--examples/ex2_espresso/build.gradle56
-rw-r--r--examples/ex2_espresso/src/androidTest/AndroidManifest.xml11
-rw-r--r--examples/ex2_espresso/src/androidTest/java/com/google/android/mobly/snippet/example2/EspressoTest.java48
-rw-r--r--examples/ex2_espresso/src/main/AndroidManifest.xml14
-rw-r--r--examples/ex2_espresso/src/main/java/com/google/android/mobly/snippet/example2/MainActivity.java50
-rw-r--r--examples/ex2_espresso/src/main/res/layout/activity_main.xml23
-rw-r--r--examples/ex2_espresso/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3418 bytes
-rw-r--r--examples/ex2_espresso/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2206 bytes
-rw-r--r--examples/ex2_espresso/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4842 bytes
-rw-r--r--examples/ex2_espresso/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 7718 bytes
-rw-r--r--examples/ex2_espresso/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 10486 bytes
-rw-r--r--examples/ex2_espresso/src/main/res/values-w820dp/dimens.xml6
-rw-r--r--examples/ex2_espresso/src/main/res/values/colors.xml6
-rw-r--r--examples/ex2_espresso/src/main/res/values/dimens.xml5
-rw-r--r--examples/ex2_espresso/src/main/res/values/strings.xml3
-rw-r--r--examples/ex2_espresso/src/main/res/values/styles.xml11
-rw-r--r--examples/ex2_espresso/src/snippet/AndroidManifest.xml14
-rw-r--r--examples/ex2_espresso/src/snippet/java/com/google/android/mobly/snippet/example2/EspressoSnippet.java55
-rw-r--r--examples/ex3_async_event/README.md41
-rw-r--r--examples/ex3_async_event/build.gradle26
-rw-r--r--examples/ex3_async_event/src/main/AndroidManifest.xml16
-rw-r--r--examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java98
-rw-r--r--examples/ex4_uiautomator/README.md45
-rw-r--r--examples/ex4_uiautomator/build.gradle32
-rw-r--r--examples/ex4_uiautomator/src/main/AndroidManifest.xml19
-rw-r--r--examples/ex4_uiautomator/src/main/java/com/google/android/mobly/snippet/example4/UiAutomatorSnippet.java111
-rw-r--r--examples/ex5_schedule_rpc/README.md62
-rw-r--r--examples/ex5_schedule_rpc/build.gradle27
-rw-r--r--examples/ex5_schedule_rpc/src/main/AndroidManifest.xml16
-rw-r--r--examples/ex5_schedule_rpc/src/main/java/com/google/android/mobly/snippet/example5/ExampleScheduleRpcSnippet.java116
-rw-r--r--examples/ex6_complex_type_conversion/README.md108
-rw-r--r--examples/ex6_complex_type_conversion/build.gradle27
-rw-r--r--examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml23
-rw-r--r--examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java21
-rw-r--r--examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java34
-rw-r--r--examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java57
42 files changed, 1556 insertions, 0 deletions
diff --git a/examples/ex1_standalone_app/README.md b/examples/ex1_standalone_app/README.md
new file mode 100644
index 0000000..5d68e34
--- /dev/null
+++ b/examples/ex1_standalone_app/README.md
@@ -0,0 +1,108 @@
+# Standalone Snippet App Example
+
+This tutorial shows you how to create a standalone Mobly snippet app. To create
+a snippet app that controls (instruments) another app under test, please see
+[Example 2](../ex2_espresso/README.md).
+
+## Tutorial
+
+1. Use Android Studio to create a new app project.
+
+1. Link against Mobly Snippet Lib in your `build.gradle` file
+
+ ```
+ dependencies {
+ implementation 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ }
+ ```
+
+1. Write a Java class implementing `Snippet` and add methods to trigger the
+ behaviour that you want. Annotate them with `@Rpc`
+
+ ```java
+ package com.my.app;
+ ...
+ public class ExampleSnippet implements Snippet {
+ @Rpc(description='Returns a string containing the given number.')
+ public String getFoo(Integer input) {
+ return "foo " + input;
+ }
+
+ @Override
+ public void shutdown() {}
+ }
+ ```
+
+1. Add any classes that implement the `Snippet` interface in your
+ `AndroidManifest.xml` application section as `meta-data`
+
+ ```xml
+ <manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.my.app">
+ <application>
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.my.app.test.MySnippet1,
+ com.my.app.test.MySnippet2" />
+ ...
+ ```
+
+
+1. Add an `instrumentation` tag to your `AndroidManifest.xml` so that the
+ framework can launch your server through an `instrument` command.
+
+ ```xml
+ <manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.my.app">
+ <application>...</application>
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.ServerRunner"
+ android:targetPackage="com.my.app" />
+ </manifest>
+ ```
+
+1. Build your apk and install it on your phone
+
+1. In your Mobly python test, connect to your snippet .apk in `setup_class`
+
+ ```python
+ class HelloWorldTest(base_test.BaseTestClass):
+ def setup_class(self):
+ self.ads = self.register_controller(android_device)
+ self.dut1 = self.ads[0]
+ self.dut1.load_snippet(name='snippet', package='com.my.app.test')
+ ```
+
+6. Invoke your needed functionality within your test
+
+ ```python
+ def test_get_foo(self):
+ actual_foo = self.dut1.snippet.getFoo(5)
+ asserts.assert_equal("foo 5", actual_foo)
+ ```
+
+## Running the example code
+
+This folder contains a fully working example of a standalone snippet apk.
+
+1. Compile the example
+
+ ./gradlew examples:ex1_standalone_app:assembleDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex1_standalone_app/build/outputs/apk/debug/ex1_standalone_app-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `getFoo()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example1
+
+ >>> print(s.help())
+ Known methods:
+ getBar(String) returns String // Returns the given string with the prefix "bar"
+ getFoo(Integer) returns String // Returns the given integer with the prefix "foo"
+
+ >>> s.getFoo(5)
+ u'foo 5'
diff --git a/examples/ex1_standalone_app/build.gradle b/examples/ex1_standalone_app/build.gradle
new file mode 100644
index 0000000..ee60dc1
--- /dev/null
+++ b/examples/ex1_standalone_app/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example1"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.1"
+ }
+ lintOptions {
+ abortOnError false
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardwareIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+}
+
+dependencies {
+ // The 'implementation project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular
+ // 'implementation' dep instead:
+ //implementation 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ implementation project(':mobly-snippet-lib')
+}
diff --git a/examples/ex1_standalone_app/src/main/AndroidManifest.xml b/examples/ex1_standalone_app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..90ebdea
--- /dev/null
+++ b/examples/ex1_standalone_app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example1">
+
+ <application android:allowBackup="false">
+ <!-- Required: list of all classes with @Rpc methods. -->
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example1.ExampleSnippet,
+ com.google.android.mobly.snippet.example1.ExampleSnippet2" />
+
+ <!-- Optional: tag which will be used for logs through the snippet lib's logger.
+ If specified, log lines will look like this:
+ MoblySnippetLibExample1.JsonRpcServer:84: Got shutdown signal
+
+ If not specified, log lines will look like this
+ com.google.android.mobly.snippet.example1.JsonRpcServer:84: Got shutdown signal -->
+ <meta-data
+ android:name="mobly-log-tag"
+ android:value="MoblySnippetLibExample1" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example1" />
+</manifest>
diff --git a/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet.java b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet.java
new file mode 100644
index 0000000..46c55ba
--- /dev/null
+++ b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example1;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+
+public class ExampleSnippet implements Snippet {
+ @Rpc(description = "Returns the given integer with the prefix \"foo\"")
+ public String getFoo(Integer input) {
+ return "foo " + input;
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java
new file mode 100644
index 0000000..61c3e0a
--- /dev/null
+++ b/examples/ex1_standalone_app/src/main/java/com/google/android/mobly/snippet/example1/ExampleSnippet2.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example1;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+
+import com.google.android.mobly.snippet.rpc.RunOnUiThread;
+
+import org.json.JSONArray;
+
+import java.io.IOException;
+
+public class ExampleSnippet2 implements Snippet {
+ @Rpc(description = "Returns the given string with the prefix \"bar\"")
+ public String getBar(String input) {
+ return "bar " + input;
+ }
+
+ @Rpc(description = "Returns the given JSON array with the prefix \"bar\"")
+ public String getJSONArray(JSONArray input) {
+ return "bar " + input;
+ }
+
+ @Rpc(description = "Throws an exception")
+ public String throwSomething() throws IOException {
+ throw new IOException("Example exception from throwSomething()");
+ }
+
+ @Rpc(description = "Throws an exception from the main thread")
+ // @RunOnUiThread makes this method execute on the main thread, but only has effect when
+ // invoked as an RPC. It does not affect how this method executes if invoked directly in Java.
+ // This annotation can also be applied to the constructor and the shutdown() method.
+ @RunOnUiThread
+ public String throwSomethingFromMainThread() throws IOException {
+ throw new IOException("Example exception from throwSomethingFromMainThread()");
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/examples/ex2_espresso/README.md b/examples/ex2_espresso/README.md
new file mode 100644
index 0000000..c9422a5
--- /dev/null
+++ b/examples/ex2_espresso/README.md
@@ -0,0 +1,128 @@
+# Espresso Snippet Example
+
+This tutorial shows you how to create snippets that automate the UI of another
+app using Espresso.
+
+The same approach can be used to create any snippet app that needs to access
+the classes or resources of any other single app.
+
+## Overview
+
+To build a snippet that instruments another app, you have to create a new
+[product flavor](https://developer.android.com/studio/build/build-variants.html#product-flavors)
+of your existing app with the snippet code built in.
+
+The snippet code cannot run from a regular test apk because it requires a custom
+`testInstrumentationRunner`.
+
+## Tutorial
+
+1. In the `build.gradle` file of your existing app, create a new product flavor called `snippet`.
+
+ ```
+ android {
+ defaultConfig { ... }
+ productFlavors {
+ main {}
+ snippet {}
+ }
+ }
+ ```
+
+1. Link against Mobly Snippet Lib in your `build.gradle` file
+
+ ```
+ dependencies {
+ snippetCompile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ }
+ ```
+
+1. Create a new source tree called `src/snippet` where you will place the
+ snippet code.
+
+1. In Android Studio, use the `Build Variants` tab in the left hand pane to
+ switch to the snippetDebug build variant. This will let you edit code in the
+ new tree.
+
+1. Write your snippet code in a new class under `src/snippet/java`
+
+ ```java
+ package com.my.app;
+ ...
+ public class EspressoSnippet implements Snippet {
+ @Rpc(description="Pushes the main app button.")
+ public void pushMainButton() {
+ onView(withId(R.id.main_button)).perform(click());
+ }
+
+ @Override
+ public void shutdown() {}
+ }
+ ```
+
+1. Create `src/snippet/AndroidManifest.xml` containing an `<instrument>` block
+ and any classes that implement the `Snippet` interface in `meta-data`
+
+ ```xml
+ <?xml version="1.0" encoding="utf-8"?>
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.my.app.EspressoSnippet" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.my.app" />
+ </manifest>
+ ```
+
+1. Build your apk by invoking the new `assembleSnippetDebug` target.
+
+1. Install the apk on your phone. You do not need to install the main app's
+ apk; the snippet-enabled apk is a complete replacement for your app.
+
+1. In your Mobly python test, connect to your snippet .apk in `setup_class`
+
+ ```python
+ class HelloWorldTest(base_test.BaseTestClass):
+ def setup_class(self):
+ self.ads = self.register_controller(android_device)
+ self.dut1 = self.ads[0]
+ self.dut1.load_snippet(name='snippet', package='com.my.app')
+ ```
+
+6. Invoke your needed functionality within your test
+
+ ```python
+ def test_click_button(self):
+ self.dut1.snippet.pushMainButton()
+ ```
+
+## Running the example code
+
+This folder contains a fully working example of a snippet apk that uses espresso
+to automate a simple app.
+
+1. Compile the example
+
+ ./gradlew examples:ex2_espresso:assembleSnippetDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex2_espresso/build/outputs/apk/snippet/debug/ex2_espresso-snippet-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `pushMainButton()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example2
+
+ >>> print(s.help())
+ Known methods:
+ pushMainButton(boolean) returns void // Pushes the main app button, and checks the label if this is the first time.
+ startMainActivity() returns void // Opens the main activity of the app
+
+ >>> s.startMainActivity()
+ >>> s.pushMainButton(True)
+
+1. Press ctrl+d to exit the shell and terminate the app.
diff --git a/examples/ex2_espresso/build.gradle b/examples/ex2_espresso/build.gradle
new file mode 100644
index 0000000..4479e1b
--- /dev/null
+++ b/examples/ex2_espresso/build.gradle
@@ -0,0 +1,56 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 31
+ flavorDimensions "examples"
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example2"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.1"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ productFlavors {
+ original {
+ dimension "examples"
+ }
+ snippet {
+ testApplicationId "com.google.android.mobly.snippet.example2.snippet"
+ dimension "examples"
+ }
+ }
+
+ lintOptions {
+ abortOnError true
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardcodedText', 'UnusedIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.4.0-beta01'
+ implementation 'androidx.test:runner:1.4.0'
+
+ // The androidTest package is not for snippet support; it shows an example
+ // of an instrumentation test coexisting with a snippet in the same
+ // codebase.
+ androidTestImplementation 'androidx.annotation:annotation:1.2.0'
+ androidTestImplementation 'androidx.test:runner:1.4.0'
+ androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+
+ // The 'snippetCompile project' dep is to compile against the snippet lib
+ // source in this repo. For your own snippets, you'll want to use the
+ // regular 'snippetCompile' dep instead:
+ //snippetCompile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ snippetImplementation project(':mobly-snippet-lib')
+
+ snippetImplementation 'androidx.annotation:annotation:1.2.0'
+ snippetImplementation 'androidx.test:rules:1.4.0'
+ snippetImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
diff --git a/examples/ex2_espresso/src/androidTest/AndroidManifest.xml b/examples/ex2_espresso/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..0dc87bc
--- /dev/null
+++ b/examples/ex2_espresso/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example2.test">
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example2" />
+</manifest>
diff --git a/examples/ex2_espresso/src/androidTest/java/com/google/android/mobly/snippet/example2/EspressoTest.java b/examples/ex2_espresso/src/androidTest/java/com/google/android/mobly/snippet/example2/EspressoTest.java
new file mode 100644
index 0000000..f41374e
--- /dev/null
+++ b/examples/ex2_espresso/src/androidTest/java/com/google/android/mobly/snippet/example2/EspressoTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example2;
+
+import androidx.test.espresso.action.ViewActions;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+/**
+ * This test is not part of the snippet code. It's a regular espresso instrumentation test which
+ * shows how regular tests can coexist with snippets in the source tree.
+ */
+@RunWith(AndroidJUnit4.class)
+public class EspressoTest {
+ @Rule
+ public ActivityTestRule<MainActivity> mActivityRule =
+ new ActivityTestRule<>(MainActivity.class);
+
+ @Test
+ public void espressoTest() {
+ onView(withId(R.id.main_text_view)).check(matches(withText("Hello World!")));
+ onView(withId(R.id.main_button)).perform(ViewActions.click());
+ onView(withId(R.id.main_text_view)).check(matches(withText("Button pressed 1 times.")));
+ }
+}
diff --git a/examples/ex2_espresso/src/main/AndroidManifest.xml b/examples/ex2_espresso/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..33f853f
--- /dev/null
+++ b/examples/ex2_espresso/src/main/AndroidManifest.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example2">
+
+ <application android:allowBackup="true" android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme">
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/examples/ex2_espresso/src/main/java/com/google/android/mobly/snippet/example2/MainActivity.java b/examples/ex2_espresso/src/main/java/com/google/android/mobly/snippet/example2/MainActivity.java
new file mode 100644
index 0000000..0cc0b54
--- /dev/null
+++ b/examples/ex2_espresso/src/main/java/com/google/android/mobly/snippet/example2/MainActivity.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example2;
+
+import java.util.Locale;
+import androidx.appcompat.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+public class MainActivity extends AppCompatActivity {
+ private TextView mTextView;
+ private Button mButton;
+ private int mNumPressed;
+
+ /**
+ * Attaches a simple listener that increments the text in the textbox whenever the button is
+ * pressed.
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ mTextView = (TextView) findViewById(R.id.main_text_view);
+ mButton = (Button) findViewById(R.id.main_button);
+ mButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mNumPressed++;
+ mTextView.setText(String.format(Locale.ROOT, "Button pressed %d times.", mNumPressed));
+ }
+ });
+ }
+}
diff --git a/examples/ex2_espresso/src/main/res/layout/activity_main.xml b/examples/ex2_espresso/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..e190302
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/layout/activity_main.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main"
+ android:layout_width="match_parent" android:layout_height="match_parent"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ tools:context="com.google.android.mobly.snippet.example2.MainActivity">
+
+<TextView
+ android:id="@+id/main_text_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:text="Hello World!" />
+
+<Button
+ android:id="@+id/main_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Push the button!" />
+</RelativeLayout>
diff --git a/examples/ex2_espresso/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/ex2_espresso/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/examples/ex2_espresso/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/ex2_espresso/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/examples/ex2_espresso/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/ex2_espresso/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/ex2_espresso/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/ex2_espresso/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/ex2_espresso/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/ex2_espresso/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/ex2_espresso/src/main/res/values-w820dp/dimens.xml b/examples/ex2_espresso/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+ (such as screen margins) for screens with more than 820dp of available width. This
+ would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+ <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/examples/ex2_espresso/src/main/res/values/colors.xml b/examples/ex2_espresso/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/examples/ex2_espresso/src/main/res/values/dimens.xml b/examples/ex2_espresso/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..47c8224
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/examples/ex2_espresso/src/main/res/values/strings.xml b/examples/ex2_espresso/src/main/res/values/strings.xml
new file mode 100644
index 0000000..c46b9d8
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">Mobly Snippet Espresso Example</string>
+</resources>
diff --git a/examples/ex2_espresso/src/main/res/values/styles.xml b/examples/ex2_espresso/src/main/res/values/styles.xml
new file mode 100644
index 0000000..daa2a5c
--- /dev/null
+++ b/examples/ex2_espresso/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+</resources>
diff --git a/examples/ex2_espresso/src/snippet/AndroidManifest.xml b/examples/ex2_espresso/src/snippet/AndroidManifest.xml
new file mode 100644
index 0000000..85797ce
--- /dev/null
+++ b/examples/ex2_espresso/src/snippet/AndroidManifest.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example2.EspressoSnippet" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example2" />
+
+</manifest>
diff --git a/examples/ex2_espresso/src/snippet/java/com/google/android/mobly/snippet/example2/EspressoSnippet.java b/examples/ex2_espresso/src/snippet/java/com/google/android/mobly/snippet/example2/EspressoSnippet.java
new file mode 100644
index 0000000..8ea7c21
--- /dev/null
+++ b/examples/ex2_espresso/src/snippet/java/com/google/android/mobly/snippet/example2/EspressoSnippet.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example2;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import androidx.test.espresso.action.ViewActions;
+import androidx.test.rule.ActivityTestRule;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import org.junit.Rule;
+
+public class EspressoSnippet implements Snippet {
+ @Rule
+ public ActivityTestRule<MainActivity> mActivityRule =
+ new ActivityTestRule<>(MainActivity.class);
+
+ @Rpc(description="Opens the main activity of the app")
+ public void startMainActivity() {
+ mActivityRule.launchActivity(null /* startIntent */);
+ }
+
+ @Rpc(description="Pushes the main app button, and checks the label if this is the first time.")
+ public void pushMainButton(boolean checkFirstRun) {
+ if (checkFirstRun) {
+ onView(withId(R.id.main_text_view)).check(matches(withText("Hello World!")));
+ }
+ onView(withId(R.id.main_button)).perform(ViewActions.click());
+ if (checkFirstRun) {
+ onView(withId(R.id.main_text_view)).check(matches(withText("Button pressed 1 times.")));
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ mActivityRule.getActivity().finish();
+ }
+}
diff --git a/examples/ex3_async_event/README.md b/examples/ex3_async_event/README.md
new file mode 100644
index 0000000..edb4fbd
--- /dev/null
+++ b/examples/ex3_async_event/README.md
@@ -0,0 +1,41 @@
+# Async Event RPC Example
+
+This example shows you how to use the @AsyncRpc annotation of Mobly snippet lib
+to handle asynchronous callbacks.
+
+See the source code in `ExampleAsyncSnippet.java` for details.
+
+## Running the example code
+
+This folder contains a fully working example of a standalone snippet apk.
+
+1. Compile the example
+
+ ./gradlew examples:ex3_async_event:assembleDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex3_async_event/build/outputs/apk/debug/ex3_async_event-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `tryEvent()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example3
+
+ >>> handler = s.tryEvent(42)
+ >>> print("Not blocked, can do stuff here")
+ >>> event = handler.waitAndGet('AsyncTaskResult') # Blocks until the event is received
+
+ Now let's see the content of the event
+
+ >>> import pprint
+ >>> pprint.pprint(event)
+ {
+ 'callbackId': '2-1',
+ 'name': 'AsyncTaskResult',
+ 'time': 20460228696,
+ 'data': {
+ 'exampleData': "Here's a simple event.",
+ 'successful': True,
+ 'secretNumber': 12
+ }
+ }
diff --git a/examples/ex3_async_event/build.gradle b/examples/ex3_async_event/build.gradle
new file mode 100644
index 0000000..7327fc4
--- /dev/null
+++ b/examples/ex3_async_event/build.gradle
@@ -0,0 +1,26 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example3"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.1"
+ }
+ lintOptions {
+ abortOnError false
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardwareIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+}
+
+dependencies {
+ // The 'compile project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular 'compile' dep instead:
+ // compile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ implementation project(':mobly-snippet-lib')
+}
diff --git a/examples/ex3_async_event/src/main/AndroidManifest.xml b/examples/ex3_async_event/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..dcc724b
--- /dev/null
+++ b/examples/ex3_async_event/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example3">
+
+ <application android:allowBackup="false">
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example3.ExampleAsyncSnippet" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example3" />
+
+</manifest>
diff --git a/examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java b/examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java
new file mode 100644
index 0000000..a2b22fa
--- /dev/null
+++ b/examples/ex3_async_event/src/main/java/com/google/android/mobly/snippet/example3/ExampleAsyncSnippet.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example3;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.rpc.AsyncRpc;
+import com.google.android.mobly.snippet.util.Log;
+
+public class ExampleAsyncSnippet implements Snippet {
+
+ private final EventCache mEventCache = EventCache.getInstance();
+
+ /**
+ * This is a sample asynchronous task.
+ *
+ * In real world use cases, it can be a {@link android.content.BroadcastReceiver}, a Listener,
+ * or any other kind asynchronous callback class.
+ */
+ public class AsyncTask implements Runnable {
+
+ private final String mCallbackId;
+ private final int mSecretNumber;
+
+ public AsyncTask(String callbackId, int secreteNumber) {
+ this.mCallbackId = callbackId;
+ this.mSecretNumber = secreteNumber;
+ }
+
+ /**
+ * Sleeps for 10s then post a {@link SnippetEvent} with some data.
+ *
+ * If the sleep is interrupted, a {@link SnippetEvent} signaling failure will be posted instead.
+ */
+ public void run() {
+ Log.d("Sleeping for 10s before posting an event.");
+ SnippetEvent event = new SnippetEvent(mCallbackId, "AsyncTaskResult");
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ event.getData().putBoolean("successful", false);
+ event.getData().putString("reason", "Sleep was interrupted.");
+ mEventCache.postEvent(event);
+ }
+ event.getData().putBoolean("successful", true);
+ event.getData().putString("exampleData", "Here's a simple event.");
+ event.getData().putInt("secretNumber", mSecretNumber);
+ mEventCache.postEvent(event);
+ }
+ }
+
+ /**
+ * An Rpc method demonstrating the async event mechanism.
+ *
+ * This call returns immediately, but starts a task in a separate thread which will post an
+ * event 10s after the task was started.
+ *
+ * Expect to see an event on the client side that looks like:
+ *
+ * {
+ * 'callbackId': '2-1',
+ * 'name': 'AsyncTaskResult',
+ * 'time': 20460228696,
+ * 'data': {
+ * 'exampleData': "Here's a simple event.",
+ * 'successful': True,
+ * 'secretNumber': 12
+ * }
+ * }
+ *
+ * @param callbackId The ID that should be used to tag {@link SnippetEvent} objects triggered by
+ * this method.
+ * @throws InterruptedException
+ */
+ @AsyncRpc(description = "This triggers an async event and returns.")
+ public void tryEvent(String callbackId, int secretNumber) throws InterruptedException {
+ Runnable asyncTask = new AsyncTask(callbackId, secretNumber);
+ Thread thread = new Thread(asyncTask);
+ thread.start();
+ }
+ @Override
+ public void shutdown() {}
+}
diff --git a/examples/ex4_uiautomator/README.md b/examples/ex4_uiautomator/README.md
new file mode 100644
index 0000000..10fb144
--- /dev/null
+++ b/examples/ex4_uiautomator/README.md
@@ -0,0 +1,45 @@
+# UIAutomator Snippet Example
+
+This example shows you how to create snippets that control the UI of a device
+across system and multiple app views using UIAutomator. Unlike Espresso-based
+UI automation, it does not require access to app source code.
+
+This snippet is written as a [standalone snippet](../ex1_standalone_app/README.md)
+and does not target another app. In particular, it doesn't need to target the
+app under test, so it doesn't need its classpath or to be signed with the same
+key.
+
+See the [Espresso snippet tutorial](../ex2_espresso/README.md) for more
+information about the app this example automates.
+
+## Running the example code
+
+This folder contains a fully working example of a snippet apk that uses
+UIAutomator to automate a simple app.
+
+1. Compile the main app and automation. The main app of ex2 (espresso) is used
+ as the app to automate. Unlike espresso, the uiautomator test does not
+ depend on this apk and does not use its source or classpath, so you must
+ compile and install the app separately.
+
+ ./gradlew examples:ex2_espresso:assembleDebug examples:ex4_uiautomator:assembleDebug
+
+1. Install the apks on your phone
+
+ adb install -r ./examples/ex2_espresso/build/outputs/apk/debug/ex2_espresso-main-debug.apk
+ adb install -r ./examples/ex4_uiautomator/build/outputs/apk/debug/ex4_uiautomator-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `pushMainButton()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example4
+
+ >>> print(s.help())
+ Known methods:
+ pushMainButton(boolean) returns void // Pushes the main app button, and checks the label if this is the first time.
+ startMainActivity() returns void // Opens the main activity of the app
+ uiautomatorDump() returns String // Perform a UIAutomator dump
+
+ >>> s.startMainActivity()
+ >>> s.pushMainButton(True)
+
+1. Press ctrl+d to exit the shell and terminate the app.
diff --git a/examples/ex4_uiautomator/build.gradle b/examples/ex4_uiautomator/build.gradle
new file mode 100644
index 0000000..b071e1b
--- /dev/null
+++ b/examples/ex4_uiautomator/build.gradle
@@ -0,0 +1,32 @@
+apply plugin: 'com.android.application'
+
+android {
+ // This has to match what the appcompat dep expects.
+ compileSdkVersion 31
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example4"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.2"
+ }
+ lintOptions {
+ abortOnError false
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardwareIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+}
+
+dependencies {
+ // The 'compile project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular
+ // 'compile' dep instead:
+ //compile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ implementation project(':mobly-snippet-lib')
+ implementation 'junit:junit:4.13.2'
+ implementation 'androidx.test:runner:1.4.0'
+ implementation 'androidx.appcompat:appcompat:1.4.0-beta01'
+ implementation 'androidx.test.uiautomator:uiautomator:2.2.0'
+}
diff --git a/examples/ex4_uiautomator/src/main/AndroidManifest.xml b/examples/ex4_uiautomator/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..89d5276
--- /dev/null
+++ b/examples/ex4_uiautomator/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example4">
+
+ <application android:allowBackup="false">
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example4.UiAutomatorSnippet" />
+ </application>
+
+ <!-- This snippet does NOT target ex2 (which is the main app the code
+ automates). The instrumentation target is itself which creates a
+ standalone snippet. -->
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example4" />
+
+</manifest>
diff --git a/examples/ex4_uiautomator/src/main/java/com/google/android/mobly/snippet/example4/UiAutomatorSnippet.java b/examples/ex4_uiautomator/src/main/java/com/google/android/mobly/snippet/example4/UiAutomatorSnippet.java
new file mode 100644
index 0000000..9fc01b2
--- /dev/null
+++ b/examples/ex4_uiautomator/src/main/java/com/google/android/mobly/snippet/example4/UiAutomatorSnippet.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example4;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+/**
+ * Demonstrates how to drive an app using UIAutomator without access to the app's source code or
+ * classpath.
+ *
+ * <p>Drives the Espresso example app from ex2 without instrumenting it.
+ */
+public class UiAutomatorSnippet implements Snippet {
+ private static final class UiAutomatorSnippetException extends Exception {
+ private static final long serialVersionUID = 1;
+
+ public UiAutomatorSnippetException(String message) {
+ super(message);
+ }
+ }
+
+ private static final String MAIN_PACKAGE = "com.google.android.mobly.snippet.example2";
+ private static final int LAUNCH_TIMEOUT = 5000;
+
+ private final Context mContext;
+ private final UiDevice mDevice;
+
+ public UiAutomatorSnippet() {
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ }
+
+ @Rpc(description="Opens the main activity of the app")
+ public void startMainActivity() throws UiAutomatorSnippetException {
+ // Send the launch intent
+ Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(MAIN_PACKAGE);
+ if (intent == null) {
+ throw new UiAutomatorSnippetException(
+ "Unable to create launch intent for " + MAIN_PACKAGE + "; is the app installed?");
+ }
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ mContext.startActivity(intent);
+
+ // Wait for the app to appear
+ mDevice.wait(Until.hasObject(By.pkg(MAIN_PACKAGE).depth(0)), LAUNCH_TIMEOUT);
+ }
+
+ @Rpc(description="Pushes the main app button, and checks the label if this is the first time.")
+ public void pushMainButton(boolean checkFirstRun) {
+ if (checkFirstRun) {
+ assertEquals(
+ "Hello World!",
+ // Example of finding object by id.
+ mDevice.findObject(By.res(MAIN_PACKAGE, "main_text_view")).getText());
+ }
+ // Example of finding a button by text. Finding by ID is also possible, as above.
+ UiObject2 button = mDevice.findObject(By.text("PUSH THE BUTTON!"));
+ button.click();
+ if (checkFirstRun) {
+ assertEquals(
+ "Button pressed 1 times",
+ mDevice.findObject(By.res(MAIN_PACKAGE, "main_text_view")).getText());
+ }
+ }
+
+ @Rpc(description="Perform a UIAutomator dump")
+ public String uiautomatorDump() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ mDevice.dumpWindowHierarchy(baos);
+ byte[] dumpBytes = baos.toByteArray();
+ String dumpStr = new String(dumpBytes, Charset.forName("UTF-8"));
+ return dumpStr;
+ } finally {
+ baos.close();
+ }
+ }
+
+ @Override
+ public void shutdown() throws IOException {
+ mDevice.executeShellCommand("am force-stop " + MAIN_PACKAGE);
+ }
+}
diff --git a/examples/ex5_schedule_rpc/README.md b/examples/ex5_schedule_rpc/README.md
new file mode 100644
index 0000000..4ccd10f
--- /dev/null
+++ b/examples/ex5_schedule_rpc/README.md
@@ -0,0 +1,62 @@
+# Scheduling RPCs Example
+
+This example shows you how to use `scheduleRpc` which is built into
+Mobly snippet lib to handle RPC scheduling.
+
+## Why this is needed?
+
+Some tests may need a snippet RPC to execute when the snippet client is unable
+to reach the device, e.g., performing test actions while USB is disconnected.
+For example, for battery testing (with Monsoon devices), we may want to measure
+power consumed during certain test actions (e.g., phone calls). However
+a Monsoon device turns off USB during battery data measurement, and a regular
+snippet RPC won't work when the client is not connected to the device.
+Therefore, prior to starting the Monsoon measurement we need to schedule a phone
+call RPC prior to soccur during the measurement period.
+
+In this scenario, the test steps would be:
+
+1. Schedule the `makePhoneCall('123456')` to execute after (e.g., 10 seconds):
+
+ s.scheduleRpc('makePhoneCall', 10000, ['123456'])
+
+2. Start a Monsoon device to collect battery data, while simultaneously USB is
+ turned off.
+3. After 10 seconds, the phone call starts while USB is off.
+4. Finally, after the phone call is finished, Monsoon data collection completes
+ and USB is re-enabled.
+5. The test retrieves any cached events or data from the device.
+
+
+
+See the source code ExampleScheduleRpcSnippet.java for details.
+
+## Running the example code
+
+This folder contains a fully working example of a standalone snippet apk.
+
+1. Compile the example
+
+ ./gradlew examples:ex5_schedule_rpc:assembleDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex5_schedule_rpc/build/outputs/apk/debug/ex5_schedule_rpc-debug.apk
+
+1. Use `snippet_shell` from mobly to trigger `tryEvent()`:
+
+ snippet_shell.py com.google.android.mobly.snippet.example5
+
+ >>> callback = s.scheduleRpc('makeToast', 5000, ['message'])
+
+ Wait for the message to show up on the screen (sync RPC call)
+
+ >>> callback.waitAndGet('makeToast').data
+ {u'callback': u'null', u'error': u'null', u'result': u'OK', u'id': u'0'}
+
+ >>> callback = s.scheduleRpc('asyncMakeToast', 5000, ['message'])
+
+ Wait for the message to show up on the screen (async RPC call)
+
+ >>> callback.waitAndGet('asyncMakeToast').data
+ {u'callback': u'1-1', u'error': u'null', u'result': u'null', u'id': u'0'}
diff --git a/examples/ex5_schedule_rpc/build.gradle b/examples/ex5_schedule_rpc/build.gradle
new file mode 100644
index 0000000..f5aa15c
--- /dev/null
+++ b/examples/ex5_schedule_rpc/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example5"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.1"
+ }
+ lintOptions {
+ abortOnError false
+ checkAllWarnings true
+ warningsAsErrors true
+ disable 'HardwareIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi'
+ }
+}
+
+dependencies {
+ // The 'compile project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular 'compile' dep instead:
+ // compile 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ implementation project(':mobly-snippet-lib')
+ implementation 'androidx.test:runner:1.4.0'
+}
diff --git a/examples/ex5_schedule_rpc/src/main/AndroidManifest.xml b/examples/ex5_schedule_rpc/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9a3271c
--- /dev/null
+++ b/examples/ex5_schedule_rpc/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example5">
+
+ <application android:allowBackup="false">
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example5.ExampleScheduleRpcSnippet" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example5" />
+
+</manifest>
diff --git a/examples/ex5_schedule_rpc/src/main/java/com/google/android/mobly/snippet/example5/ExampleScheduleRpcSnippet.java b/examples/ex5_schedule_rpc/src/main/java/com/google/android/mobly/snippet/example5/ExampleScheduleRpcSnippet.java
new file mode 100644
index 0000000..a94d68a
--- /dev/null
+++ b/examples/ex5_schedule_rpc/src/main/java/com/google/android/mobly/snippet/example5/ExampleScheduleRpcSnippet.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example5;
+
+import android.content.Context;
+import android.os.Handler;
+import androidx.test.InstrumentationRegistry;
+import android.widget.Toast;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.rpc.AsyncRpc;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.util.Log;
+
+/**
+ * Demonstrates how to schedule an RPC.
+ */
+public class ExampleScheduleRpcSnippet implements Snippet {
+
+ /**
+ * This is a sample asynchronous task.
+ *
+ * In real world use cases, it can be a {@link android.content.BroadcastReceiver}, a Listener,
+ * or any other kind asynchronous callback class.
+ */
+ public class AsyncTask implements Runnable {
+
+ private final String mCallbackId;
+ private final String mMessage;
+
+ public AsyncTask(String callbackId, String message) {
+ this.mCallbackId = callbackId;
+ this.mMessage = message;
+ }
+
+ /**
+ * Sleeps for 10s then make toast and post a {@link SnippetEvent} with some data.
+ *
+ * <p>If the sleep is interrupted, a {@link SnippetEvent} signaling failure will be posted
+ * instead.
+ */
+ @Override
+ public void run() {
+ Log.d("Sleeping for 10s before posting an event.");
+ SnippetEvent event = new SnippetEvent(mCallbackId, mMessage);
+ try {
+ Thread.sleep(10000);
+ showToast(mMessage);
+ } catch (InterruptedException e) {
+ event.getData().putBoolean("successful", false);
+ event.getData().putString("reason", "Sleep was interrupted.");
+ mEventCache.postEvent(event);
+ }
+ event.getData().putBoolean("successful", true);
+ event.getData().putString("eventName", mMessage);
+ mEventCache.postEvent(event);
+ }
+ }
+
+ private final Context mContext;
+ private final EventCache mEventCache = EventCache.getInstance();
+
+ /**
+ * Since the APIs here deal with UI, most of them have to be called in a thread that has called
+ * looper.
+ */
+ private final Handler mHandler;
+
+ public ExampleScheduleRpcSnippet() {
+ mContext = InstrumentationRegistry.getContext();
+ mHandler = new Handler(mContext.getMainLooper());
+ }
+
+ @Rpc(description = "Make a toast on screen.")
+ public String makeToast(String message) throws InterruptedException {
+ showToast(message);
+ return "OK";
+ }
+
+ @AsyncRpc(description = "Make a toast on screen after some time.")
+ public void asyncMakeToast(String callbackId, String message)
+ throws Throwable {
+ Runnable asyncTask = new AsyncTask(callbackId, "asyncMakeToast");
+ Thread thread = new Thread(asyncTask);
+ thread.start();
+ }
+
+ @Override
+ public void shutdown() {}
+
+ private void showToast(final String message) {
+ mHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+}
+
diff --git a/examples/ex6_complex_type_conversion/README.md b/examples/ex6_complex_type_conversion/README.md
new file mode 100644
index 0000000..104d91b
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/README.md
@@ -0,0 +1,108 @@
+# Complex Type Conversion in Snippet Example
+
+This tutorial shows you how to use a custom object in Snippet Lib.
+
+This example assumes basic familiarity with Snippet Lib as demonstrated in
+[Example 1](../ex1_standalone_app/README.md).
+
+## Tutorial
+
+1. Use Android Studio to create a new app project, similar to
+ [Example 1](../ex1_standalone_app/README.md).
+
+1. Create a complex type in Java:
+ ```java
+ public class CustomType {
+ private String myValue;
+ CustomType(String value) {
+ myValue = value;
+ }
+
+ String getMyValue() {
+ return myValue;
+ }
+ public void setMyValue(String newValue) {
+ myValue = newValue;
+ }
+ }
+ ```
+1. Create a Java class implementing `SnippetObjectConverter`, which defines how the complex type
+ should be converted against `JSONObject`:
+ ```java
+ public class ExampleObjectConverter implements SnippetObjectConverter {
+ @Override
+ public JSONObject serialize(Object object) throws JSONException {
+ JSONObject result = new JSONObject();
+ if (object instanceof CustomType) {
+ CustomType input = (CustomType) object;
+ result.put("Value", input.getMyValue());
+ return result;
+ }
+ return null;
+ }
+
+ @Override
+ public Object deserialize(JSONObject jsonObject, Type type) throws JSONException {
+ if (type == CustomType.class) {
+ return new CustomType(jsonObject.getString("Value"));
+ }
+ return null;
+ }
+ }
+ ```
+1. Write a Java class implementing `Snippet` and add Rpc methods that takes your complex type as
+ a parameter and another Rpc method that returns the complext type directly.
+
+ ```java
+ package com.my.app;
+ ...
+ public class ExampleSnippet implements Snippet {
+ @Rpc(description = "Pass a complex type as a snippet parameter.")
+ public String passComplexTypeToSnippet(CustomType input) {
+ Log.i("Old value is: " + input.getMyValue());
+ return "The value in CustomType is: " + input.getMyValue();
+ }
+
+ @Rpc(description = "Returns a complex type from snippet.")
+ public CustomType returnComplexTypeFromSnippet(String value) {
+ return new CustomType(value);
+ }
+ @Override
+ public void shutdown() {}
+ }
+ ```
+
+1. In `AndroidManifest.xml`, specify the converter class as a `meta-data` named
+ `mobly-object-converter`
+
+ ```xml
+ <manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.my.app">
+ <application>
+ <meta-data
+ android:name="mobly-object-converter"
+ android:value="com.my.app.ExampleObjectConverter" />
+ ...
+ ```
+
+## Running the example code
+
+This folder contains a fully working example of a standalone snippet apk.
+
+1. Compile the example
+
+ ./gradlew examples:ex6_complex_type_conversion:assembleDebug
+
+1. Install the apk on your phone
+
+ adb install -r ./examples/ex6_complex_type_conversion/build/outputs/apk/debug/ex6_complex_type_conversion-debug.apk
+
+1. Use Mobly's `snippet_shell` from mobly to trigger the Rpc methods:
+
+ snippet_shell.py com.google.android.mobly.snippet.example6
+
+ >>> s.passComplexTypeToSnippet({'Value': 'Hello'})
+ 'The value in CustomType is: Hello'
+ >>> s.returnComplexTypeFromSnippet('Bye')
+ {'Value': 'Bye'}
diff --git a/examples/ex6_complex_type_conversion/build.gradle b/examples/ex6_complex_type_conversion/build.gradle
new file mode 100644
index 0000000..b6039b0
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 31
+ buildToolsVersion '31.0.0'
+
+ defaultConfig {
+ applicationId "com.google.android.mobly.snippet.example6"
+ minSdkVersion 26
+ targetSdkVersion 31
+ versionCode 1
+ versionName "0.0.1"
+ }
+ lintOptions {
+ abortOnError true
+ checkAllWarnings true
+ warningsAsErrors true
+ }
+}
+
+dependencies {
+ // The 'implementation project' dep is to compile against the snippet lib source in
+ // this repo. For your own snippets, you'll want to use the regular
+ // 'implementation' dep instead:
+ //implementation 'com.google.android.mobly:mobly-snippet-lib:1.3.1'
+ implementation project(':mobly-snippet-lib')
+}
diff --git a/examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml b/examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c1548e7
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.mobly.snippet.example6">
+
+ <application>
+ <!-- Required: list of all classes with @Rpc methods. -->
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.android.mobly.snippet.example6.ExampleSnippet" />
+ <!-- Optional: a class used for converting Java objects to/from JSON. -->
+ <meta-data
+ android:name="mobly-object-converter"
+ android:value="com.google.android.mobly.snippet.example6.ExampleObjectConverter" />
+ <meta-data
+ android:name="mobly-log-tag"
+ android:value="MoblySnippetLibExample6" />
+ </application>
+
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.android.mobly.snippet.example6" />
+</manifest>
diff --git a/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java
new file mode 100644
index 0000000..223b63e
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/CustomType.java
@@ -0,0 +1,21 @@
+package com.google.android.mobly.snippet.example6;
+
+/**
+ * A data class that defines a non-primitive type.
+ *
+ * This type is used to demonstrate serialization and de-serialization of complex type objects in
+ * Mobly Snippet Lib for Android.
+ */
+public class CustomType {
+ private String myValue;
+ CustomType(String value) {
+ myValue = value;
+ }
+
+ String getMyValue() {
+ return myValue;
+ }
+ public void setMyValue(String newValue) {
+ myValue = newValue;
+ }
+}
diff --git a/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java
new file mode 100644
index 0000000..eea8831
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleObjectConverter.java
@@ -0,0 +1,34 @@
+package com.google.android.mobly.snippet.example6;
+
+import com.google.android.mobly.snippet.SnippetObjectConverter;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.reflect.Type;
+
+
+/**
+ * Example showing how to supply custom object converter to Mobly Snippet Lib.
+ */
+
+public class ExampleObjectConverter implements SnippetObjectConverter {
+ @Override
+ public JSONObject serialize(Object object) throws JSONException {
+ JSONObject result = new JSONObject();
+ if (object instanceof CustomType) {
+ CustomType input = (CustomType) object;
+ result.put("Value", input.getMyValue());
+ return result;
+ }
+ return null;
+ }
+
+ @Override
+ public Object deserialize(JSONObject jsonObject, Type type) throws JSONException {
+ if (type == CustomType.class) {
+ return new CustomType(jsonObject.getString("Value"));
+ }
+ return null;
+ }
+}
diff --git a/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java
new file mode 100644
index 0000000..3306f85
--- /dev/null
+++ b/examples/ex6_complex_type_conversion/src/main/java/com/google/android/mobly/snippet/example6/ExampleSnippet.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.example6;
+
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Example snippet showing converting complex type objects using custom logic.
+ *
+ * For complex types in Java, one can supply a custom object converter to Snippet Lib to specify how
+ * each complex type should be serialized/de-serialized. With this, users don't have to explicitly
+ * call a serializer or de-serializer in every single Rpc method, which simplifies code.
+ */
+public class ExampleSnippet implements Snippet {
+ @Rpc(description = "Pass a complex type as a snippet parameter.")
+ public String passComplexTypeToSnippet(CustomType input) {
+ Log.i("Old value is: " + input.getMyValue());
+ return "The value in CustomType is: " + input.getMyValue();
+ }
+
+ @Rpc(description = "Returns a complex type from snippet.")
+ public CustomType returnComplexTypeFromSnippet(String value) {
+ return new CustomType(value);
+ }
+
+ /**
+ * Demonstrates serialization/de-serialization of a collection of custom type objects.
+ */
+ @Rpc(description = "Update values for multiple CustomType objects.")
+ public ArrayList<CustomType> updateValues(ArrayList<CustomType> objects, String newValue) {
+ for (CustomType obj : objects) {
+ obj.setMyValue(newValue);
+ }
+ return objects;
+ }
+
+ @Override
+ public void shutdown() {}
+}