diff options
author | frankfeng <frankfeng@google.com> | 2022-01-05 23:53:39 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-01-05 23:53:39 +0000 |
commit | 7151acd88deae8709cb212b20494a4c38ecc8263 (patch) | |
tree | daa905c2cab4f307d9342908fc85ec54ab73bab3 /examples | |
parent | a18b21585728971688495dbe384c61b0332c8ce1 (diff) | |
parent | a3c4e39314251b98b0ee70da2bb63dbc52137ac7 (diff) | |
download | mobly-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')
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 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() {} +} |