├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew └── src ├── main ├── AndroidManifest.xml └── java │ └── com │ └── google │ └── android │ └── mobly │ └── snippet │ └── bundled │ ├── AccountSnippet.java │ ├── AudioSnippet.java │ ├── BluetoothLeAdvertiserSnippet.java │ ├── BluetoothLeScannerSnippet.java │ ├── ContactSnippet.java │ ├── FileSnippet.java │ ├── LogSnippet.java │ ├── MediaSnippet.java │ ├── NetworkingSnippet.java │ ├── NotificationSnippet.java │ ├── SmsSnippet.java │ ├── StorageSnippet.java │ ├── TelephonySnippet.java │ ├── WifiAwareManagerSnippet.java │ ├── WifiManagerSnippet.java │ ├── bluetooth │ ├── BluetoothAdapterSnippet.java │ ├── BluetoothGattClientSnippet.java │ ├── BluetoothGattServerSnippet.java │ ├── PairingBroadcastReceiver.java │ └── profiles │ │ ├── BluetoothA2dpSnippet.java │ │ ├── BluetoothHeadsetSnippet.java │ │ ├── BluetoothHearingAidSnippet.java │ │ └── BluetoothLeAudioSnippet.java │ └── utils │ ├── DataHolder.java │ ├── JsonDeserializer.java │ ├── JsonSerializer.java │ ├── MbsEnums.java │ ├── RpcEnum.java │ └── Utils.java └── test └── java ├── JsonDeserializerTest.java ├── MbsEnumsTest.java └── UtilsTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Mobly Bundled Snippets (MBS) APK Release History 2 | 3 | ## 0.0.1: Initial release 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to any Google project must be accompanied by a Contributor License 9 | Agreement. This is necessary because you own the copyright to your changes, even 10 | after your contribution becomes part of this project. So this agreement simply 11 | gives us permission to use and redistribute your contributions as part of the 12 | project. Head over to to see your current 13 | agreements on file or to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult [GitHub Help] for more 23 | information on using pull requests. 24 | 25 | [GitHub Help]: https://help.github.com/articles/about-pull-requests/ 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mobly Bundled Snippets is a set of Snippets to allow Mobly tests to control 2 | Android devices by exposing a simplified version of the public Android API 3 | suitable for testing. 4 | 5 | We are adding more APIs as we go. If you have specific needs for certain groups 6 | of APIs, feel free to file a request in [Issues](https://github.com/google/mobly-bundled-snippets/issues). 7 | 8 | Note: this is not an official Google product. 9 | 10 | 11 | ## Usage 12 | 13 | 1. Compile and install the bundled snippets 14 | 15 | ./gradlew assembleDebug 16 | adb install -d -r -g ./build/outputs/apk/debug/mobly-bundled-snippets-debug.apk 17 | 18 | 1. Use the Mobly snippet shell to interact with the bundled snippets 19 | 20 | snippet_shell.py com.google.android.mobly.snippet.bundled 21 | >>> print(s.help()) 22 | Known methods: 23 | bluetoothDisable() returns void // Disable bluetooth with a 30s timeout. 24 | ... 25 | wifiDisable() returns void // Turns off Wi-Fi with a 30s timeout. 26 | wifiEnable() returns void // Turns on Wi-Fi with a 30s timeout. 27 | ... 28 | 29 | 1. To use these snippets within Mobly tests, load it on your AndroidDevice objects 30 | after registering android_device module: 31 | 32 | ```python 33 | def setup_class(self): 34 | self.ad = self.register_controllers(android_device, min_number=1)[0] 35 | self.ad.load_snippet('api', 'com.google.android.mobly.snippet.bundled') 36 | 37 | def test_enable_wifi(self): 38 | self.ad.api.wifiEnable() 39 | ``` 40 | 41 | ## Develop 42 | 43 | If you want to contribute, use the usual github method of forking and sending 44 | a pull request. 45 | 46 | Before sending a pull request, run the `presubmit` target to format and run 47 | lint over the code. Fix any issues it indicates. When complete, send the pull 48 | request. 49 | 50 | ```shell 51 | ./gradlew presubmit 52 | ``` 53 | 54 | This target will reformat the code with 55 | [googleJavaFormat](https://github.com/sherter/google-java-format-gradle-plugin) 56 | and run lint. The lint report should open in your default browser. 57 | 58 | Be sure to address *all* off the errors reported by lint. When finished and you 59 | run `presubmit` one last time you should see: 60 | 61 | > No Issues Found 62 | > Congratulations! 63 | 64 | in your browser. 65 | 66 | ## Other resources 67 | 68 | * [Mobly multi-device test framework](http://github.com/google/mobly) 69 | * [Mobly Snippet Lib](http://github.com/google/mobly-snippet-lib) 70 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:8.7.3' 8 | 9 | // NOTE: Do not place your application dependencies here. 10 | } 11 | } 12 | 13 | plugins { 14 | id "com.github.sherter.google-java-format" version "0.9" 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | gradle.projectsEvaluated { 23 | tasks.withType(JavaCompile) { 24 | options.compilerArgs << "-Xlint:all" 25 | } 26 | } 27 | } 28 | 29 | apply plugin: 'com.android.application' 30 | 31 | android { 32 | compileSdk 36 33 | 34 | defaultConfig { 35 | applicationId "com.google.android.mobly.snippet.bundled" 36 | minSdk 26 37 | targetSdk 33 38 | versionCode 1 39 | versionName "0.0.1" 40 | setProperty("archivesBaseName", "mobly-bundled-snippets") 41 | multiDexEnabled true 42 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 43 | } 44 | compileOptions { 45 | sourceCompatibility JavaVersion.VERSION_1_8 46 | targetCompatibility JavaVersion.VERSION_1_8 47 | } 48 | lintOptions { 49 | abortOnError false 50 | checkAllWarnings true 51 | warningsAsErrors true 52 | disable 'HardwareIds','MissingApplicationIcon','GoogleAppIndexingWarning','InvalidPackage','OldTargetApi' 53 | } 54 | namespace 'com.google.android.mobly.snippet.bundled' 55 | } 56 | 57 | // Produces a jar of source files. Needed for compliance reasons. 58 | task sourcesJar(type: Jar) { 59 | from android.sourceSets.main.java.srcDirs 60 | archiveClassifier.set('src') 61 | } 62 | 63 | task javadoc(type: Javadoc) { 64 | source = android.sourceSets.main.java.srcDirs 65 | classpath += project.files( 66 | android.getBootClasspath().join(File.pathSeparator)) 67 | } 68 | 69 | artifacts { 70 | archives sourcesJar 71 | } 72 | 73 | dependencies { 74 | implementation 'androidx.test:runner:1.5.2' 75 | implementation 'com.android.support:multidex:1.0.3' 76 | implementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3' 77 | implementation 'com.google.android.mobly:mobly-snippet-lib:1.4.0' 78 | implementation 'com.google.code.gson:gson:2.8.6' 79 | implementation 'com.google.guava:guava:31.0.1-jre' 80 | implementation 'com.google.errorprone:error_prone_annotations:2.15.0' 81 | implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10' 82 | 83 | testImplementation 'org.robolectric:robolectric:4.14.1' 84 | testImplementation 'com.google.errorprone:error_prone_annotations:2.15.0' 85 | testImplementation 'com.google.guava:guava:31.0.1-jre' 86 | testImplementation 'com.google.truth:truth:1.1.2' 87 | testImplementation 'junit:junit:4.13.2' 88 | } 89 | 90 | googleJavaFormat { 91 | options style: 'AOSP' 92 | } 93 | 94 | // Open lint's HTML report in your default browser or viewer. 95 | task openLintReport(type: Exec) { 96 | def lint_report = "build/reports/lint-results.html" 97 | def cmd = "cat" 98 | def platform = System.getProperty('os.name').toLowerCase(Locale.ROOT) 99 | if (platform.contains("linux")) { 100 | cmd = "xdg-open" 101 | } else if (platform.contains("mac os x")) { 102 | cmd = "open" 103 | } else if (platform.contains("windows")) { 104 | cmd = "launch" 105 | } 106 | commandLine cmd, lint_report 107 | } 108 | 109 | task presubmit { 110 | dependsOn { ['googleJavaFormat', 'lint', 'openLintReport'] } 111 | doLast { 112 | println "Fix any lint issues you see. When it looks good, submit the pull request." 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableD8.desugaring=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | android.defaults.buildfeatures.buildconfig=true 6 | android.nonTransitiveRClass=false 7 | android.nonFinalResIds=false 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/mobly-bundled-snippets/26d0e94a9d949b1458ccc4c11fd63f47b8fb5ed6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 16 18:55:43 PST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 71 | 72 | 73 | 76 | 77 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.content.Context; 20 | import android.media.AudioManager; 21 | import android.media.AudioDeviceInfo; 22 | import androidx.test.platform.app.InstrumentationRegistry; 23 | import com.google.android.mobly.snippet.Snippet; 24 | import com.google.android.mobly.snippet.rpc.Rpc; 25 | import java.lang.reflect.Method; 26 | import java.util.ArrayList; 27 | 28 | /* Snippet class to control audio */ 29 | public class AudioSnippet implements Snippet { 30 | 31 | private final AudioManager mAudioManager; 32 | 33 | public AudioSnippet() { 34 | Context context = InstrumentationRegistry.getInstrumentation().getContext(); 35 | mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 36 | } 37 | 38 | @Rpc(description = "Sets the microphone mute state: True = Muted, False = not muted.") 39 | public void setMicrophoneMute(boolean state) { 40 | mAudioManager.setMicrophoneMute(state); 41 | } 42 | 43 | @Rpc(description = "Returns whether or not the microphone is muted.") 44 | public boolean isMicrophoneMute() { 45 | return mAudioManager.isMicrophoneMute(); 46 | } 47 | 48 | @Rpc(description = "Returns whether or not any music is active.") 49 | public boolean isMusicActive() { 50 | return mAudioManager.isMusicActive(); 51 | } 52 | 53 | @Rpc(description = "Gets the music stream volume.") 54 | public Integer getMusicVolume() { 55 | return mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 56 | } 57 | 58 | @Rpc(description = "Gets the maximum music stream volume value.") 59 | public int getMusicMaxVolume() { 60 | return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 61 | } 62 | 63 | @Rpc( 64 | description = 65 | "Sets the music stream volume. The minimum value is 0. Use 'getMusicMaxVolume'" 66 | + " to determine the maximum.") 67 | public void setMusicVolume(Integer value) { 68 | mAudioManager.setStreamVolume( 69 | AudioManager.STREAM_MUSIC, value, 0 /* flags, 0 = no flags */); 70 | } 71 | 72 | @Rpc(description = "Gets the ringer volume.") 73 | public Integer getRingVolume() { 74 | return mAudioManager.getStreamVolume(AudioManager.STREAM_RING); 75 | } 76 | 77 | @Rpc(description = "Gets the maximum ringer volume value.") 78 | public int getRingMaxVolume() { 79 | return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING); 80 | } 81 | 82 | @Rpc( 83 | description = 84 | "Sets the ringer stream volume. The minimum value is 0. Use 'getRingMaxVolume'" 85 | + " to determine the maximum.") 86 | public void setRingVolume(Integer value) { 87 | mAudioManager.setStreamVolume(AudioManager.STREAM_RING, value, 0 /* flags, 0 = no flags */); 88 | } 89 | 90 | @Rpc(description = "Gets the voice call volume.") 91 | public Integer getVoiceCallVolume() { 92 | return mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL); 93 | } 94 | 95 | @Rpc(description = "Gets the maximum voice call volume value.") 96 | public int getVoiceCallMaxVolume() { 97 | return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL); 98 | } 99 | 100 | @Rpc( 101 | description = 102 | "Sets the voice call stream volume. The minimum value is 0. Use" 103 | + " 'getVoiceCallMaxVolume' to determine the maximum.") 104 | public void setVoiceCallVolume(Integer value) { 105 | mAudioManager.setStreamVolume( 106 | AudioManager.STREAM_VOICE_CALL, value, 0 /* flags, 0 = no flags */); 107 | } 108 | 109 | @Rpc(description = "Gets the alarm volume.") 110 | public Integer getAlarmVolume() { 111 | return mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM); 112 | } 113 | 114 | @Rpc(description = "Gets the maximum alarm volume value.") 115 | public int getAlarmMaxVolume() { 116 | return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM); 117 | } 118 | 119 | @Rpc( 120 | description = 121 | "Sets the alarm stream volume. The minimum value is 0. Use 'getAlarmMaxVolume'" 122 | + " to determine the maximum.") 123 | public void setAlarmVolume(Integer value) { 124 | mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, value, 0 /* flags, 0 = no flags */); 125 | } 126 | 127 | @Rpc(description = "Silences all audio streams.") 128 | public void muteAll() throws Exception { 129 | /* Get numStreams from AudioSystem through reflection. If for some reason this fails, 130 | * calling muteAll will throw. */ 131 | Class audioSystem = Class.forName("android.media.AudioSystem"); 132 | Method getNumStreamTypes = audioSystem.getDeclaredMethod("getNumStreamTypes"); 133 | int numStreams = (int) getNumStreamTypes.invoke(null /* instance */); 134 | for (int i = 0; i < numStreams; i++) { 135 | mAudioManager.setStreamVolume(i /* audio stream */, 0 /* value */, 0 /* flags */); 136 | } 137 | } 138 | 139 | @Rpc( 140 | description = 141 | "Puts the ringer volume at the lowest setting, but does not set it to " 142 | + "DO NOT DISTURB; the phone will vibrate when receiving a call.") 143 | public void muteRing() { 144 | setRingVolume(0); 145 | } 146 | 147 | @Rpc(description = "Mute music stream.") 148 | public void muteMusic() { 149 | setMusicVolume(0); 150 | } 151 | 152 | @Rpc(description = "Mute alarm stream.") 153 | public void muteAlarm() { setAlarmVolume(0); } 154 | 155 | @Rpc( 156 | description = 157 | "Returns an array of AudioDeviceInfo objects corresponding to the audio devices" 158 | + " currently connected to the system.") 159 | public ArrayList getAudioDeviceTypes() { 160 | ArrayList audioDeviceTypes = new ArrayList<>(); 161 | for (AudioDeviceInfo device : mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { 162 | audioDeviceTypes.add(device.getType()); 163 | } 164 | return audioDeviceTypes; 165 | } 166 | 167 | @Override 168 | public void shutdown() {} 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.annotation.TargetApi; 20 | import android.bluetooth.BluetoothAdapter; 21 | import android.bluetooth.le.AdvertiseCallback; 22 | import android.bluetooth.le.AdvertiseData; 23 | import android.bluetooth.le.AdvertiseSettings; 24 | import android.bluetooth.le.BluetoothLeAdvertiser; 25 | import android.os.Build; 26 | import android.os.Bundle; 27 | import android.os.ParcelUuid; 28 | import com.google.android.mobly.snippet.Snippet; 29 | import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer; 30 | import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 31 | import com.google.android.mobly.snippet.bundled.utils.RpcEnum; 32 | import com.google.android.mobly.snippet.event.EventCache; 33 | import com.google.android.mobly.snippet.event.SnippetEvent; 34 | import com.google.android.mobly.snippet.rpc.AsyncRpc; 35 | import com.google.android.mobly.snippet.rpc.Rpc; 36 | import com.google.android.mobly.snippet.rpc.RpcMinSdk; 37 | import com.google.android.mobly.snippet.rpc.RpcOptional; 38 | import com.google.android.mobly.snippet.util.Log; 39 | import java.util.HashMap; 40 | import org.json.JSONException; 41 | import org.json.JSONObject; 42 | 43 | /** Snippet class exposing Android APIs in WifiManager. */ 44 | @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) 45 | public class BluetoothLeAdvertiserSnippet implements Snippet { 46 | private static class BluetoothLeAdvertiserSnippetException extends Exception { 47 | private static final long serialVersionUID = 1; 48 | 49 | public BluetoothLeAdvertiserSnippetException(String msg) { 50 | super(msg); 51 | } 52 | } 53 | 54 | private final BluetoothLeAdvertiser mAdvertiser; 55 | private static final EventCache sEventCache = EventCache.getInstance(); 56 | 57 | private final HashMap mAdvertiseCallbacks = new HashMap<>(); 58 | 59 | public BluetoothLeAdvertiserSnippet() { 60 | mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser(); 61 | } 62 | 63 | /** 64 | * Start Bluetooth LE advertising. 65 | * 66 | *

This can be called multiple times, and each call is associated with a {@link 67 | * AdvertiseCallback} object, which is used to stop the advertising. 68 | * 69 | * @param callbackId 70 | * @param advertiseSettings A JSONObject representing a {@link AdvertiseSettings object}. E.g. 71 | *

 72 |      *          {
 73 |      *            "AdvertiseMode": "ADVERTISE_MODE_BALANCED",
 74 |      *            "Timeout": (int, milliseconds),
 75 |      *            "Connectable": (bool),
 76 |      *            "TxPowerLevel": "ADVERTISE_TX_POWER_LOW"
 77 |      *          }
 78 |      *     
79 | * 80 | * @param advertiseData A JSONObject representing a {@link AdvertiseData} object will be 81 | * broadcast if the operation succeeds. E.g. 82 | *
 83 |      *          {
 84 |      *            "IncludeDeviceName": (bool),
 85 |      *            # JSON list, each element representing a set of service data, which is composed of
 86 |      *            # a UUID, and an optional string.
 87 |      *            "ServiceData": [
 88 |      *                      {
 89 |      *                        "UUID": (A string representation of {@link ParcelUuid}),
 90 |      *                        "Data": (Optional, The string representation of what you want to
 91 |      *                                 advertise, base64 encoded)
 92 |      *                        # If you want to add a UUID without data, simply omit the "Data"
 93 |      *                        # field.
 94 |      *                      }
 95 |      *                ]
 96 |      *          }
 97 |      *     
98 | * 99 | * @param scanResponse A JSONObject representing a {@link AdvertiseData} object which will 100 | * response the data to the scanning device. E.g. 101 | *
102 |      *          {
103 |      *            "IncludeDeviceName": (bool),
104 |      *            # JSON list, each element representing a set of service data, which is composed of
105 |      *            # a UUID, and an optional string.
106 |      *            "ServiceData": [
107 |      *                      {
108 |      *                        "UUID": (A string representation of {@link ParcelUuid}),
109 |      *                        "Data": (Optional, The string representation of what you want to
110 |      *                                 advertise, base64 encoded)
111 |      *                        # If you want to add a UUID without data, simply omit the "Data"
112 |      *                        # field.
113 |      *                      }
114 |      *                ]
115 |      *          }
116 |      *     
117 | * 118 | * @throws BluetoothLeAdvertiserSnippetException 119 | * @throws JSONException 120 | */ 121 | @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) 122 | @AsyncRpc(description = "Start BLE advertising.") 123 | public void bleStartAdvertising( 124 | String callbackId, 125 | JSONObject advertiseSettings, 126 | JSONObject advertiseData, 127 | @RpcOptional JSONObject scanResponse) 128 | throws BluetoothLeAdvertiserSnippetException, JSONException { 129 | if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { 130 | throw new BluetoothLeAdvertiserSnippetException( 131 | "Bluetooth is disabled, cannot start BLE advertising."); 132 | } 133 | AdvertiseSettings settings = JsonDeserializer.jsonToBleAdvertiseSettings(advertiseSettings); 134 | AdvertiseData data = JsonDeserializer.jsonToBleAdvertiseData(advertiseData); 135 | AdvertiseCallback advertiseCallback = new DefaultAdvertiseCallback(callbackId); 136 | if (scanResponse == null) { 137 | mAdvertiser.startAdvertising(settings, data, advertiseCallback); 138 | } else { 139 | AdvertiseData response = JsonDeserializer.jsonToBleAdvertiseData(scanResponse); 140 | mAdvertiser.startAdvertising(settings, data, response, advertiseCallback); 141 | } 142 | mAdvertiseCallbacks.put(callbackId, advertiseCallback); 143 | } 144 | 145 | /** 146 | * Stop a BLE advertising. 147 | * 148 | * @param callbackId The callbackId corresponding to the {@link 149 | * BluetoothLeAdvertiserSnippet#bleStartAdvertising} call that started the advertising. 150 | * @throws BluetoothLeScannerSnippet.BluetoothLeScanSnippetException 151 | */ 152 | @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) 153 | @Rpc(description = "Stop BLE advertising.") 154 | public void bleStopAdvertising(String callbackId) throws BluetoothLeAdvertiserSnippetException { 155 | AdvertiseCallback callback = mAdvertiseCallbacks.remove(callbackId); 156 | if (callback == null) { 157 | throw new BluetoothLeAdvertiserSnippetException( 158 | "No advertising session found for ID " + callbackId); 159 | } 160 | mAdvertiser.stopAdvertising(callback); 161 | } 162 | 163 | private static class DefaultAdvertiseCallback extends AdvertiseCallback { 164 | private final String mCallbackId; 165 | public static RpcEnum ADVERTISE_FAILURE_ERROR_CODE = 166 | new RpcEnum.Builder() 167 | .add("ADVERTISE_FAILED_ALREADY_STARTED", ADVERTISE_FAILED_ALREADY_STARTED) 168 | .add("ADVERTISE_FAILED_DATA_TOO_LARGE", ADVERTISE_FAILED_DATA_TOO_LARGE) 169 | .add( 170 | "ADVERTISE_FAILED_FEATURE_UNSUPPORTED", 171 | ADVERTISE_FAILED_FEATURE_UNSUPPORTED) 172 | .add("ADVERTISE_FAILED_INTERNAL_ERROR", ADVERTISE_FAILED_INTERNAL_ERROR) 173 | .add( 174 | "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS", 175 | ADVERTISE_FAILED_TOO_MANY_ADVERTISERS) 176 | .build(); 177 | 178 | public DefaultAdvertiseCallback(String callbackId) { 179 | mCallbackId = callbackId; 180 | } 181 | 182 | @Override 183 | public void onStartSuccess(AdvertiseSettings settingsInEffect) { 184 | Log.e("Bluetooth LE advertising started with settings: " + settingsInEffect.toString()); 185 | SnippetEvent event = new SnippetEvent(mCallbackId, "onStartSuccess"); 186 | Bundle advertiseSettings = 187 | JsonSerializer.serializeBleAdvertisingSettings(settingsInEffect); 188 | event.getData().putBundle("SettingsInEffect", advertiseSettings); 189 | sEventCache.postEvent(event); 190 | } 191 | 192 | @Override 193 | public void onStartFailure(int errorCode) { 194 | Log.e("Bluetooth LE advertising failed to start with error code: " + errorCode); 195 | SnippetEvent event = new SnippetEvent(mCallbackId, "onStartFailure"); 196 | final String errorCodeString = ADVERTISE_FAILURE_ERROR_CODE.getString(errorCode); 197 | event.getData().putString("ErrorCode", errorCodeString); 198 | sEventCache.postEvent(event); 199 | } 200 | } 201 | 202 | @Override 203 | public void shutdown() { 204 | for (AdvertiseCallback callback : mAdvertiseCallbacks.values()) { 205 | mAdvertiser.stopAdvertising(callback); 206 | } 207 | mAdvertiseCallbacks.clear(); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.annotation.TargetApi; 20 | import android.bluetooth.BluetoothAdapter; 21 | import android.bluetooth.le.BluetoothLeScanner; 22 | import android.bluetooth.le.ScanCallback; 23 | import android.bluetooth.le.ScanFilter; 24 | import android.bluetooth.le.ScanResult; 25 | import android.bluetooth.le.ScanSettings; 26 | import android.os.Build; 27 | import android.os.Bundle; 28 | import com.google.android.mobly.snippet.Snippet; 29 | import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer; 30 | import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 31 | import com.google.android.mobly.snippet.bundled.utils.MbsEnums; 32 | import com.google.android.mobly.snippet.event.EventCache; 33 | import com.google.android.mobly.snippet.event.SnippetEvent; 34 | import com.google.android.mobly.snippet.rpc.AsyncRpc; 35 | import com.google.android.mobly.snippet.rpc.Rpc; 36 | import com.google.android.mobly.snippet.rpc.RpcMinSdk; 37 | import com.google.android.mobly.snippet.rpc.RpcOptional; 38 | import com.google.android.mobly.snippet.util.Log; 39 | import java.util.ArrayList; 40 | import java.util.HashMap; 41 | import java.util.List; 42 | import org.json.JSONArray; 43 | import org.json.JSONException; 44 | import org.json.JSONObject; 45 | 46 | /** Snippet class exposing Android APIs in WifiManager. */ 47 | @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) 48 | public class BluetoothLeScannerSnippet implements Snippet { 49 | private static class BluetoothLeScanSnippetException extends Exception { 50 | private static final long serialVersionUID = 1; 51 | 52 | public BluetoothLeScanSnippetException(String msg) { 53 | super(msg); 54 | } 55 | } 56 | 57 | private final BluetoothLeScanner mScanner; 58 | private final EventCache mEventCache = EventCache.getInstance(); 59 | private final HashMap mScanCallbacks = new HashMap<>(); 60 | private final JsonSerializer mJsonSerializer = new JsonSerializer(); 61 | private long bleScanStartTime = 0; 62 | 63 | public BluetoothLeScannerSnippet() { 64 | mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner(); 65 | } 66 | 67 | /** 68 | * Start a BLE scan. 69 | * 70 | * @param callbackId 71 | * @param scanFilters A JSONArray representing a list of {@link ScanFilter} object for finding 72 | * exact BLE devices. E.g. 73 | *
 74 |      *          [
 75 |      *            {
 76 |      *              "ServiceUuid": (A string representation of {@link ParcelUuid}),
 77 |      *            },
 78 |      *          ]
 79 |      *     
80 | * 81 | * @param scanSettings A JSONObject representing a {@link ScanSettings} object which is the 82 | * Settings for the scan. E.g. 83 | *
 84 |      *          {
 85 |      *            'ScanMode': 'SCAN_MODE_LOW_LATENCY',
 86 |      *          }
 87 |      *     
88 | * 89 | * @throws BluetoothLeScanSnippetException 90 | */ 91 | @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) 92 | @AsyncRpc(description = "Start BLE scan.") 93 | public void bleStartScan( 94 | String callbackId, 95 | @RpcOptional JSONArray scanFilters, 96 | @RpcOptional JSONObject scanSettings) 97 | throws BluetoothLeScanSnippetException, JSONException { 98 | if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { 99 | throw new BluetoothLeScanSnippetException( 100 | "Bluetooth is disabled, cannot start BLE scan."); 101 | } 102 | DefaultScanCallback callback = new DefaultScanCallback(callbackId); 103 | if (scanFilters == null && scanSettings == null) { 104 | mScanner.startScan(callback); 105 | } else { 106 | ArrayList filters = new ArrayList<>(); 107 | for (int i = 0; i < scanFilters.length(); i++) { 108 | filters.add(JsonDeserializer.jsonToScanFilter(scanFilters.getJSONObject(i))); 109 | } 110 | ScanSettings settings = JsonDeserializer.jsonToScanSettings(scanSettings); 111 | mScanner.startScan(filters, settings, callback); 112 | } 113 | bleScanStartTime = System.currentTimeMillis(); 114 | mScanCallbacks.put(callbackId, callback); 115 | } 116 | 117 | /** 118 | * Stop a BLE scan. 119 | * 120 | * @param callbackId The callbackId corresponding to the {@link 121 | * BluetoothLeScannerSnippet#bleStartScan} call that started the scan. 122 | * @throws BluetoothLeScanSnippetException 123 | */ 124 | @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) 125 | @Rpc(description = "Stop a BLE scan.") 126 | public void bleStopScan(String callbackId) throws BluetoothLeScanSnippetException { 127 | ScanCallback callback = mScanCallbacks.remove(callbackId); 128 | if (callback == null) { 129 | throw new BluetoothLeScanSnippetException("No ongoing scan with ID: " + callbackId); 130 | } 131 | mScanner.stopScan(callback); 132 | } 133 | 134 | @Override 135 | public void shutdown() { 136 | for (ScanCallback callback : mScanCallbacks.values()) { 137 | mScanner.stopScan(callback); 138 | } 139 | mScanCallbacks.clear(); 140 | } 141 | 142 | private class DefaultScanCallback extends ScanCallback { 143 | private final String mCallbackId; 144 | 145 | public DefaultScanCallback(String callbackId) { 146 | mCallbackId = callbackId; 147 | } 148 | 149 | @Override 150 | public void onScanResult(int callbackType, ScanResult result) { 151 | Log.i("Got Bluetooth LE scan result."); 152 | long bleScanOnResultTime = System.currentTimeMillis(); 153 | SnippetEvent event = new SnippetEvent(mCallbackId, "onScanResult"); 154 | String callbackTypeString = 155 | MbsEnums.BLE_SCAN_RESULT_CALLBACK_TYPE.getString(callbackType); 156 | event.getData().putString("CallbackType", callbackTypeString); 157 | event.getData().putBundle("result", mJsonSerializer.serializeBleScanResult(result)); 158 | event.getData() 159 | .putLong("StartToResultTimeDeltaMs", bleScanOnResultTime - bleScanStartTime); 160 | mEventCache.postEvent(event); 161 | } 162 | 163 | @Override 164 | public void onBatchScanResults(List results) { 165 | Log.i("Got Bluetooth LE batch scan results."); 166 | SnippetEvent event = new SnippetEvent(mCallbackId, "onBatchScanResult"); 167 | ArrayList resultList = new ArrayList<>(results.size()); 168 | for (ScanResult result : results) { 169 | resultList.add(mJsonSerializer.serializeBleScanResult(result)); 170 | } 171 | event.getData().putParcelableArrayList("results", resultList); 172 | mEventCache.postEvent(event); 173 | } 174 | 175 | @Override 176 | public void onScanFailed(int errorCode) { 177 | Log.e("Bluetooth LE scan failed with error code: " + errorCode); 178 | SnippetEvent event = new SnippetEvent(mCallbackId, "onScanFailed"); 179 | String errorCodeString = MbsEnums.BLE_SCAN_FAILED_ERROR_CODE.getString(errorCode); 180 | event.getData().putString("ErrorCode", errorCodeString); 181 | mEventCache.postEvent(event); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/ContactSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.accounts.Account; 20 | import android.accounts.AccountManager; 21 | import android.content.ContentProviderOperation; 22 | import android.content.ContentResolver; 23 | import android.content.ContentUris; 24 | import android.content.Context; 25 | import android.content.OperationApplicationException; 26 | import android.database.Cursor; 27 | import android.os.Bundle; 28 | import android.os.RemoteException; 29 | import android.provider.ContactsContract; 30 | import androidx.test.platform.app.InstrumentationRegistry; 31 | import com.google.android.mobly.snippet.Snippet; 32 | import com.google.android.mobly.snippet.rpc.Rpc; 33 | import java.util.ArrayList; 34 | 35 | /* Snippet class for operating contacts. */ 36 | public class ContactSnippet implements Snippet { 37 | 38 | public static class ContactSnippetException extends Exception { 39 | 40 | ContactSnippetException(String msg) { 41 | super(msg); 42 | } 43 | } 44 | 45 | private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; 46 | private final Context context = InstrumentationRegistry.getInstrumentation().getContext(); 47 | private final AccountManager mAccountManager = AccountManager.get(context); 48 | 49 | @Rpc(description = "Add a contact with a given email address to a Google account on the device.") 50 | public void contactAddToGoogleAccountByEmail(String contactEmailAddress, 51 | String accountEmailAddress) 52 | throws ContactSnippetException, OperationApplicationException, RemoteException { 53 | assertAccountExists(accountEmailAddress); 54 | ArrayList contentProviderOperations = new ArrayList<>(); 55 | 56 | // Specify where the new contact should be stored. 57 | contentProviderOperations.add( 58 | ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) 59 | .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, GOOGLE_ACCOUNT_TYPE) 60 | .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, accountEmailAddress).build()); 61 | 62 | // Specify data to associate with the new contact. 63 | contentProviderOperations.add( 64 | ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) 65 | .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) 66 | .withValue(ContactsContract.Data.MIMETYPE, 67 | ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) 68 | .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, contactEmailAddress) 69 | .withValue(ContactsContract.CommonDataKinds.Email.TYPE, 70 | ContactsContract.CommonDataKinds.Email.TYPE_HOME).build()); 71 | 72 | // Apply the operations to the ContentProvider. 73 | context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, contentProviderOperations); 74 | } 75 | 76 | @Rpc(description = "Remove a contact with a given email address from a Google account on the device") 77 | public void contactRemoveFromGoogleAccountByEmail(String contactEmailAddress, 78 | String accountEmailAddress) 79 | throws ContactSnippetException, OperationApplicationException, RemoteException { 80 | assertAccountExists(accountEmailAddress); 81 | 82 | // Specify data to associate with the target contact to remove. 83 | long contactId = getContactIdByEmail(contactEmailAddress, accountEmailAddress); 84 | ArrayList contentProviderOperations = new ArrayList<>(); 85 | contentProviderOperations.add(ContentProviderOperation.newDelete( 86 | ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, contactId)).build()); 87 | 88 | // Apply the operations to the ContentProvider. 89 | context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, contentProviderOperations); 90 | } 91 | 92 | @Rpc(description = "Requests an immediate synchronization of contact data for the specified Google account.") 93 | public void syncGoogleContacts(String accountEmailAddress) { 94 | Bundle settingsBundle = new Bundle(); 95 | settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 96 | settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); 97 | ContentResolver.requestSync(new Account(accountEmailAddress, GOOGLE_ACCOUNT_TYPE), 98 | ContactsContract.AUTHORITY, settingsBundle); 99 | } 100 | 101 | private long getContactIdByEmail(String emailAddress, String accountEmailAddress) 102 | throws OperationApplicationException { 103 | try (Cursor cursor = 104 | context 105 | .getContentResolver() 106 | .query( 107 | ContactsContract.CommonDataKinds.Email.CONTENT_URI, 108 | null, 109 | ContactsContract.CommonDataKinds.Email.ADDRESS + " = ?" 110 | + " AND " 111 | + ContactsContract.RawContacts.ACCOUNT_NAME + " = ?", 112 | new String[]{emailAddress, accountEmailAddress}, 113 | null)) { 114 | if (cursor != null && cursor.moveToFirst()) { 115 | return cursor.getLong( 116 | cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.CONTACT_ID)); 117 | } 118 | throw new OperationApplicationException( 119 | "The contact " + emailAddress + " doesn't appear to be saved on " + accountEmailAddress); 120 | } 121 | } 122 | 123 | private void assertAccountExists(String emailAddress) throws ContactSnippetException { 124 | Account[] accounts = mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); 125 | for (Account account : accounts) { 126 | if (account.name.equals(emailAddress)) { 127 | return; 128 | } 129 | } 130 | throw new ContactSnippetException( 131 | "The account " + emailAddress + " doesn't appear to be login on the device"); 132 | } 133 | 134 | @Override 135 | public void shutdown() { 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.content.Context; 20 | import android.net.Uri; 21 | import android.os.ParcelFileDescriptor; 22 | import androidx.test.platform.app.InstrumentationRegistry; 23 | import com.google.android.mobly.snippet.Snippet; 24 | import com.google.android.mobly.snippet.bundled.utils.Utils; 25 | import com.google.android.mobly.snippet.rpc.Rpc; 26 | import java.io.IOException; 27 | import java.security.DigestInputStream; 28 | import java.security.MessageDigest; 29 | import java.security.NoSuchAlgorithmException; 30 | 31 | /** Snippet class for File and abstract storage URI operation RPCs. */ 32 | public class FileSnippet implements Snippet { 33 | 34 | private final Context mContext; 35 | 36 | public FileSnippet() { 37 | mContext = InstrumentationRegistry.getInstrumentation().getContext(); 38 | } 39 | 40 | @Rpc(description = "Compute MD5 hash on a content URI. Return the MD5 has has a hex string.") 41 | public String fileMd5Hash(String uri) throws IOException, NoSuchAlgorithmException { 42 | Uri uri_ = Uri.parse(uri); 43 | ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(uri_, "r"); 44 | MessageDigest md = MessageDigest.getInstance("MD5"); 45 | int length = (int) pfd.getStatSize(); 46 | byte[] buf = new byte[length]; 47 | ParcelFileDescriptor.AutoCloseInputStream stream = 48 | new ParcelFileDescriptor.AutoCloseInputStream(pfd); 49 | DigestInputStream dis = new DigestInputStream(stream, md); 50 | try { 51 | dis.read(buf, 0, length); 52 | return Utils.bytesToHexString(md.digest()); 53 | } finally { 54 | dis.close(); 55 | stream.close(); 56 | } 57 | } 58 | 59 | @Rpc(description = "Remove a file pointed to by the content URI.") 60 | public void fileDeleteContent(String uri) { 61 | Uri uri_ = Uri.parse(uri); 62 | mContext.getContentResolver().delete(uri_, null, null); 63 | } 64 | 65 | @Override 66 | public void shutdown() {} 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/LogSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.util.Log; 20 | import com.google.android.mobly.snippet.Snippet; 21 | import com.google.android.mobly.snippet.rpc.Rpc; 22 | 23 | /** Snippet class exposing Android APIs related to logging. */ 24 | public class LogSnippet implements Snippet { 25 | private String mTag = "MoblyTestLog"; 26 | 27 | @Rpc(description = "Set the tag to use for logX Rpcs. Default is 'MoblyTestLog'.") 28 | public void logSetTag(String tag) { 29 | mTag = tag; 30 | } 31 | 32 | @Rpc(description = "Log at info level.") 33 | public void logI(String message) { 34 | Log.i(mTag, message); 35 | } 36 | 37 | @Rpc(description = "Log at debug level.") 38 | public void logD(String message) { 39 | Log.d(mTag, message); 40 | } 41 | 42 | @Rpc(description = "Log at error level.") 43 | public void logE(String message) { 44 | Log.e(mTag, message); 45 | } 46 | 47 | @Rpc(description = "Log at warning level.") 48 | public void logW(String message) { 49 | Log.w(mTag, message); 50 | } 51 | 52 | @Rpc(description = "Log at verbose level.") 53 | public void logV(String message) { 54 | Log.v(mTag, message); 55 | } 56 | 57 | @Rpc(description = "Log at WTF level.") 58 | public void logWtf(String message) { 59 | Log.wtf(mTag, message); 60 | } 61 | 62 | @Override 63 | public void shutdown() {} 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/MediaSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.content.Context; 20 | import android.media.AudioAttributes; 21 | import android.media.AudioManager; 22 | import android.media.MediaPlayer; 23 | import android.media.MediaRouter; 24 | import android.os.Build; 25 | import android.os.Build.VERSION_CODES; 26 | import androidx.test.platform.app.InstrumentationRegistry; 27 | import com.google.android.mobly.snippet.Snippet; 28 | import com.google.android.mobly.snippet.rpc.Rpc; 29 | import java.io.IOException; 30 | 31 | /* Snippet class to control media playback. */ 32 | public class MediaSnippet implements Snippet { 33 | 34 | private final Context mContext; 35 | private final MediaPlayer mPlayer; 36 | private final MediaRouter mMediaRouter; 37 | 38 | public MediaSnippet() { 39 | mContext = InstrumentationRegistry.getInstrumentation().getContext(); 40 | mPlayer = new MediaPlayer(); 41 | mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE); 42 | } 43 | 44 | @Rpc(description = "Resets snippet media player to an idle state, regardless of current state.") 45 | public void mediaReset() { 46 | mPlayer.reset(); 47 | } 48 | 49 | @Rpc(description = "Play an audio file stored at a specified file path in external storage.") 50 | public void mediaPlayAudioFile(String mediaFilePath) throws IOException { 51 | mediaReset(); 52 | if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 53 | mPlayer.setAudioAttributes( 54 | new AudioAttributes.Builder() 55 | .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) 56 | .setUsage(AudioAttributes.USAGE_MEDIA) 57 | .build()); 58 | } else { 59 | mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 60 | } 61 | mPlayer.setDataSource(mediaFilePath); 62 | mPlayer.prepare(); // Synchronous call blocks until the player is ready for playback. 63 | mPlayer.start(); 64 | } 65 | 66 | @Rpc(description = "Stops media playback.") 67 | public void mediaStop() throws IOException { 68 | mPlayer.stop(); 69 | } 70 | 71 | @Rpc( 72 | description = 73 | "Returns the type of the receiver device associated with the live audio route.") 74 | public int mediaGetLiveAudioRouteType() { 75 | return mMediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO).getDeviceType(); 76 | } 77 | 78 | @Rpc(description = "Returns the user-visible name of the live audio route.") 79 | public String mediaGetLiveAudioRouteName() { 80 | return mMediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO).getName().toString(); 81 | } 82 | 83 | @Override 84 | public void shutdown() {} 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.app.DownloadManager; 20 | import android.content.BroadcastReceiver; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.content.IntentFilter; 24 | import android.net.Uri; 25 | import android.os.Environment; 26 | import androidx.test.platform.app.InstrumentationRegistry; 27 | import com.google.android.mobly.snippet.Snippet; 28 | import com.google.android.mobly.snippet.bundled.utils.Utils; 29 | import com.google.android.mobly.snippet.rpc.Rpc; 30 | import com.google.android.mobly.snippet.util.Log; 31 | import java.io.IOException; 32 | import java.net.InetAddress; 33 | import java.net.Socket; 34 | import java.net.UnknownHostException; 35 | import java.util.List; 36 | import java.util.Locale; 37 | 38 | /** Snippet class for networking RPCs. */ 39 | public class NetworkingSnippet implements Snippet { 40 | 41 | private final Context mContext; 42 | private final DownloadManager mDownloadManager; 43 | private volatile boolean mIsDownloadComplete = false; 44 | private volatile long mReqid = 0; 45 | 46 | public NetworkingSnippet() { 47 | mContext = InstrumentationRegistry.getInstrumentation().getContext(); 48 | mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); 49 | } 50 | 51 | private static class NetworkingSnippetException extends Exception { 52 | 53 | private static final long serialVersionUID = 8080L; 54 | 55 | public NetworkingSnippetException(String msg) { 56 | super(msg); 57 | } 58 | } 59 | 60 | @Rpc(description = "Check if a host and port are connectable using a TCP connection attempt.") 61 | public boolean networkIsTcpConnectable(String host, int port) { 62 | InetAddress addr; 63 | try { 64 | addr = InetAddress.getByName(host); 65 | } catch (UnknownHostException uherr) { 66 | Log.d("Host name lookup failure: " + uherr.getMessage()); 67 | return false; 68 | } 69 | 70 | try { 71 | Socket sock = new Socket(addr, port); 72 | sock.close(); 73 | } catch (IOException ioerr) { 74 | Log.d("Did not make connection to host: " + ioerr.getMessage()); 75 | return false; 76 | } 77 | return true; 78 | } 79 | 80 | @Rpc( 81 | description = 82 | "Download a file using HTTP. Return content Uri (file remains on device). " 83 | + "The Uri should be treated as an opaque handle for further operations.") 84 | public String networkHttpDownload(String url) 85 | throws IllegalArgumentException, NetworkingSnippetException { 86 | 87 | Uri uri = Uri.parse(url); 88 | List pathsegments = uri.getPathSegments(); 89 | if (pathsegments.size() < 1) { 90 | throw new IllegalArgumentException( 91 | String.format(Locale.US, "The Uri %s does not have a path.", uri.toString())); 92 | } 93 | DownloadManager.Request request = new DownloadManager.Request(uri); 94 | request.setDestinationInExternalPublicDir( 95 | Environment.DIRECTORY_DOWNLOADS, pathsegments.get(pathsegments.size() - 1)); 96 | mIsDownloadComplete = false; 97 | mReqid = 0; 98 | IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); 99 | BroadcastReceiver receiver = new DownloadReceiver(); 100 | mContext.registerReceiver(receiver, filter); 101 | try { 102 | mReqid = mDownloadManager.enqueue(request); 103 | Log.d( 104 | String.format( 105 | Locale.US, 106 | "networkHttpDownload download of %s with id %d", 107 | url, 108 | mReqid)); 109 | if (!Utils.waitUntil(() -> mIsDownloadComplete, 30)) { 110 | Log.d( 111 | String.format( 112 | Locale.US, "networkHttpDownload timed out waiting for completion")); 113 | throw new NetworkingSnippetException("networkHttpDownload timed out."); 114 | } 115 | } finally { 116 | mContext.unregisterReceiver(receiver); 117 | } 118 | Uri resp = mDownloadManager.getUriForDownloadedFile(mReqid); 119 | if (resp != null) { 120 | Log.d(String.format(Locale.US, "networkHttpDownload completed to %s", resp.toString())); 121 | mReqid = 0; 122 | return resp.toString(); 123 | } else { 124 | Log.d( 125 | String.format( 126 | Locale.US, 127 | "networkHttpDownload Failed to download %s", 128 | uri.toString())); 129 | throw new NetworkingSnippetException("networkHttpDownload didn't get downloaded file."); 130 | } 131 | } 132 | 133 | private class DownloadReceiver extends BroadcastReceiver { 134 | 135 | @Override 136 | public void onReceive(Context context, Intent intent) { 137 | String action = intent.getAction(); 138 | long gotid = (long) intent.getExtras().get("extra_download_id"); 139 | if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action) && gotid == mReqid) { 140 | mIsDownloadComplete = true; 141 | } 142 | } 143 | } 144 | 145 | @Override 146 | public void shutdown() { 147 | if (mReqid != 0) { 148 | mDownloadManager.remove(mReqid); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.widget.Toast; 20 | import androidx.test.platform.app.InstrumentationRegistry; 21 | import com.google.android.mobly.snippet.Snippet; 22 | import com.google.android.mobly.snippet.rpc.Rpc; 23 | import com.google.android.mobly.snippet.rpc.RunOnUiThread; 24 | 25 | /** Snippet class exposing Android APIs related to creating notification on screen. */ 26 | public class NotificationSnippet implements Snippet { 27 | 28 | @RunOnUiThread 29 | @Rpc(description = "Make a toast on screen.") 30 | public void makeToast(String message) { 31 | Toast.makeText( 32 | InstrumentationRegistry.getInstrumentation().getContext(), 33 | message, 34 | Toast.LENGTH_LONG) 35 | .show(); 36 | } 37 | 38 | @Override 39 | public void shutdown() {} 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import static java.util.stream.Collectors.toCollection; 20 | 21 | import android.annotation.TargetApi; 22 | import android.app.Activity; 23 | import android.app.PendingIntent; 24 | import android.content.BroadcastReceiver; 25 | import android.content.Context; 26 | import android.content.Intent; 27 | import android.content.IntentFilter; 28 | import android.os.Build; 29 | import android.os.Bundle; 30 | import android.provider.Telephony.Sms.Intents; 31 | import android.telephony.SmsManager; 32 | import android.telephony.SmsMessage; 33 | import androidx.test.platform.app.InstrumentationRegistry; 34 | import com.google.android.mobly.snippet.Snippet; 35 | import com.google.android.mobly.snippet.bundled.utils.Utils; 36 | import com.google.android.mobly.snippet.event.EventCache; 37 | import com.google.android.mobly.snippet.event.SnippetEvent; 38 | import com.google.android.mobly.snippet.rpc.AsyncRpc; 39 | import com.google.android.mobly.snippet.rpc.Rpc; 40 | import java.util.ArrayList; 41 | import java.util.stream.IntStream; 42 | import org.json.JSONObject; 43 | 44 | /** Snippet class for SMS RPCs. */ 45 | public class SmsSnippet implements Snippet { 46 | 47 | private static class SmsSnippetException extends Exception { 48 | private static final long serialVersionUID = 1L; 49 | 50 | SmsSnippetException(String msg) { 51 | super(msg); 52 | } 53 | } 54 | 55 | private static final int MAX_CHAR_COUNT_PER_SMS = 160; 56 | private static final String SMS_SENT_ACTION = ".SMS_SENT"; 57 | private static final int DEFAULT_TIMEOUT_MILLISECOND = 60 * 1000; 58 | private static final String SMS_RECEIVED_EVENT_NAME = "ReceivedSms"; 59 | private static final String SMS_SENT_EVENT_NAME = "SentSms"; 60 | private static final String SMS_CALLBACK_ID_PREFIX = "sendSms-"; 61 | 62 | private static int mCallbackCounter = 0; 63 | 64 | private final Context mContext; 65 | private final SmsManager mSmsManager; 66 | 67 | public SmsSnippet() { 68 | this.mContext = InstrumentationRegistry.getInstrumentation().getContext(); 69 | this.mSmsManager = SmsManager.getDefault(); 70 | } 71 | 72 | /** 73 | * Send SMS and return after waiting for send confirmation (with a timeout of 60 seconds). 74 | * 75 | * @param phoneNumber A String representing phone number with country code. 76 | * @param message A String representing the message to send. 77 | * @throws SmsSnippetException on SMS send error. 78 | */ 79 | @Rpc(description = "Send SMS to a specified phone number.") 80 | public void sendSms(String phoneNumber, String message) throws Throwable { 81 | String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter); 82 | OutboundSmsReceiver receiver = new OutboundSmsReceiver(mContext, callbackId); 83 | 84 | if (message.length() > MAX_CHAR_COUNT_PER_SMS) { 85 | ArrayList parts = mSmsManager.divideMessage(message); 86 | receiver.setExpectedMessageCount(parts.size()); 87 | if (Build.VERSION.SDK_INT >= 33) { 88 | mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION), null, 89 | null, 90 | Context.RECEIVER_EXPORTED); 91 | } else { 92 | mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION)); 93 | } 94 | mSmsManager.sendMultipartTextMessage( 95 | /* destinationAddress= */ phoneNumber, 96 | /* scAddress= */ null, 97 | /* parts= */ parts, 98 | /* sentIntents= */ IntStream.range(0, parts.size()) 99 | .mapToObj( 100 | i -> 101 | PendingIntent.getBroadcast( 102 | /* context= */ mContext, 103 | /* requestCode= */ 0, 104 | /* intent= */ new Intent(SMS_SENT_ACTION), 105 | /* flags= */ PendingIntent.FLAG_IMMUTABLE)) 106 | .collect(toCollection(ArrayList::new)), 107 | /* deliveryIntents= */ null); 108 | } else { 109 | PendingIntent sentIntent = 110 | PendingIntent.getBroadcast( 111 | /* context= */ mContext, 112 | /* requestCode= */ 0, 113 | /* intent= */ new Intent(SMS_SENT_ACTION), 114 | /* flags= */ PendingIntent.FLAG_IMMUTABLE); 115 | receiver.setExpectedMessageCount(1); 116 | if (Build.VERSION.SDK_INT >= 33) { 117 | mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION), null, 118 | null, 119 | Context.RECEIVER_EXPORTED); 120 | } else { 121 | mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION)); 122 | } 123 | mSmsManager.sendTextMessage( 124 | /* destinationAddress= */ phoneNumber, 125 | /* scAddress= */ null, 126 | /* text= */ message, 127 | /* sentIntent= */ sentIntent, 128 | /* deliveryIntent= */ null); 129 | } 130 | 131 | SnippetEvent result = 132 | Utils.waitForSnippetEvent( 133 | callbackId, SMS_SENT_EVENT_NAME, DEFAULT_TIMEOUT_MILLISECOND); 134 | 135 | if (result.getData().containsKey("error")) { 136 | throw new SmsSnippetException( 137 | "Failed to send SMS, error code: " + result.getData().getInt("error")); 138 | } 139 | } 140 | 141 | @TargetApi(Build.VERSION_CODES.KITKAT) 142 | @AsyncRpc(description = "Async wait for incoming SMS message.") 143 | public void asyncWaitForSms(String callbackId) { 144 | SmsReceiver receiver = new SmsReceiver(mContext, callbackId); 145 | mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION)); 146 | } 147 | 148 | @TargetApi(Build.VERSION_CODES.KITKAT) 149 | @Rpc(description = "Wait for incoming SMS message.") 150 | public JSONObject waitForSms(int timeoutMillis) throws Throwable { 151 | String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter); 152 | SmsReceiver receiver = new SmsReceiver(mContext, callbackId); 153 | mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION)); 154 | return Utils.waitForSnippetEvent(callbackId, SMS_RECEIVED_EVENT_NAME, timeoutMillis) 155 | .toJson(); 156 | } 157 | 158 | @Override 159 | public void shutdown() {} 160 | 161 | private static class OutboundSmsReceiver extends BroadcastReceiver { 162 | private final String mCallbackId; 163 | private Context mContext; 164 | private final EventCache mEventCache; 165 | private int mExpectedMessageCount; 166 | 167 | public OutboundSmsReceiver(Context context, String callbackId) { 168 | this.mCallbackId = callbackId; 169 | this.mContext = context; 170 | this.mEventCache = EventCache.getInstance(); 171 | mExpectedMessageCount = 0; 172 | } 173 | 174 | public void setExpectedMessageCount(int count) { 175 | mExpectedMessageCount = count; 176 | } 177 | 178 | @Override 179 | public void onReceive(Context context, Intent intent) { 180 | String action = intent.getAction(); 181 | 182 | if (SMS_SENT_ACTION.equals(action)) { 183 | SnippetEvent event = new SnippetEvent(mCallbackId, SMS_SENT_EVENT_NAME); 184 | switch (getResultCode()) { 185 | case Activity.RESULT_OK: 186 | if (mExpectedMessageCount == 1) { 187 | event.getData().putBoolean("sent", true); 188 | mEventCache.postEvent(event); 189 | mContext.unregisterReceiver(this); 190 | } 191 | 192 | if (mExpectedMessageCount > 0) { 193 | mExpectedMessageCount--; 194 | } 195 | break; 196 | case SmsManager.RESULT_ERROR_GENERIC_FAILURE: 197 | case SmsManager.RESULT_ERROR_NO_SERVICE: 198 | case SmsManager.RESULT_ERROR_NULL_PDU: 199 | case SmsManager.RESULT_ERROR_RADIO_OFF: 200 | event.getData().putBoolean("sent", false); 201 | event.getData().putInt("error_code", getResultCode()); 202 | mEventCache.postEvent(event); 203 | mContext.unregisterReceiver(this); 204 | break; 205 | default: 206 | event.getData().putBoolean("sent", false); 207 | event.getData().putInt("error_code", -1 /* Unknown */); 208 | mEventCache.postEvent(event); 209 | mContext.unregisterReceiver(this); 210 | break; 211 | } 212 | } 213 | } 214 | } 215 | 216 | private static class SmsReceiver extends BroadcastReceiver { 217 | private final String mCallbackId; 218 | private Context mContext; 219 | private final EventCache mEventCache; 220 | 221 | public SmsReceiver(Context context, String callbackId) { 222 | this.mCallbackId = callbackId; 223 | this.mContext = context; 224 | this.mEventCache = EventCache.getInstance(); 225 | } 226 | 227 | @TargetApi(Build.VERSION_CODES.KITKAT) 228 | @Override 229 | public void onReceive(Context receivedContext, Intent intent) { 230 | if (Intents.SMS_RECEIVED_ACTION.equals(intent.getAction())) { 231 | SnippetEvent event = new SnippetEvent(mCallbackId, SMS_RECEIVED_EVENT_NAME); 232 | Bundle extras = intent.getExtras(); 233 | if (extras != null) { 234 | SmsMessage[] msgs = Intents.getMessagesFromIntent(intent); 235 | StringBuilder smsMsg = new StringBuilder(); 236 | 237 | SmsMessage sms = msgs[0]; 238 | String sender = sms.getOriginatingAddress(); 239 | event.getData().putString("OriginatingAddress", sender); 240 | 241 | for (SmsMessage msg : msgs) { 242 | smsMsg.append(msg.getMessageBody()); 243 | } 244 | event.getData().putString("MessageBody", smsMsg.toString()); 245 | mEventCache.postEvent(event); 246 | mContext.unregisterReceiver(this); 247 | } 248 | } 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/StorageSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.os.Environment; 20 | import com.google.android.mobly.snippet.Snippet; 21 | import com.google.android.mobly.snippet.rpc.Rpc; 22 | 23 | public class StorageSnippet implements Snippet { 24 | 25 | @Rpc(description = "Return the primary shared/external storage directory.") 26 | public String storageGetExternalStorageDirectory() { 27 | return Environment.getExternalStorageDirectory().getAbsolutePath(); 28 | } 29 | 30 | @Rpc(description = "Return the root of the \"system\" directory.") 31 | public String storageGetRootDirectory() { 32 | return Environment.getRootDirectory().getAbsolutePath(); 33 | } 34 | 35 | @Override 36 | public void shutdown() {} 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled; 18 | 19 | import android.content.Context; 20 | import android.os.Build; 21 | import android.telephony.SubscriptionInfo; 22 | import android.telephony.SubscriptionManager; 23 | import android.telephony.TelephonyManager; 24 | import androidx.test.platform.app.InstrumentationRegistry; 25 | import com.google.android.mobly.snippet.Snippet; 26 | import com.google.android.mobly.snippet.rpc.Rpc; 27 | import com.google.android.mobly.snippet.rpc.RpcDefault; 28 | 29 | /** Snippet class for telephony RPCs. */ 30 | public class TelephonySnippet implements Snippet { 31 | 32 | private final TelephonyManager mTelephonyManager; 33 | private final SubscriptionManager mSubscriptionManager; 34 | 35 | public TelephonySnippet() { 36 | Context context = InstrumentationRegistry.getInstrumentation().getContext(); 37 | mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 38 | mSubscriptionManager = 39 | (SubscriptionManager) 40 | context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE); 41 | } 42 | 43 | @Rpc( 44 | description = 45 | "Gets the line 1 phone number, or optionally get phone number for the " 46 | + "simSlot (slot# start from 0, only valid for API level > 32)") 47 | public String getLine1Number(@RpcDefault("0") Integer simSlot) { 48 | String thisNumber = ""; 49 | 50 | if (Build.VERSION.SDK_INT < 33) { 51 | thisNumber = mTelephonyManager.getLine1Number(); 52 | } else { 53 | SubscriptionInfo mSubscriptionInfo = 54 | mSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex( 55 | simSlot.intValue()); 56 | if (mSubscriptionInfo != null) { 57 | thisNumber = 58 | mSubscriptionManager.getPhoneNumber(mSubscriptionInfo.getSubscriptionId()); 59 | } 60 | } 61 | 62 | return thisNumber; 63 | } 64 | 65 | @Rpc(description = "Returns the unique subscriber ID, for example, the IMSI for a GSM phone.") 66 | public String getSubscriberId() { 67 | return mTelephonyManager.getSubscriberId(); 68 | } 69 | 70 | @Rpc( 71 | description = 72 | "Gets the call state for the default subscription or optionally get the call" 73 | + " state for the simSlot (slot# start from 0, only valid for API" 74 | + " level > 30). Call state values are 0: IDLE, 1: RINGING, 2: OFFHOOK") 75 | public int getTelephonyCallState(@RpcDefault("0") Integer simSlot) { 76 | int thisState = -1; 77 | 78 | if (Build.VERSION.SDK_INT < 31) { 79 | return mTelephonyManager.getCallState(); 80 | } else { 81 | SubscriptionInfo mSubscriptionInfo = 82 | mSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex( 83 | simSlot.intValue()); 84 | if (mSubscriptionInfo != null) { 85 | thisState = 86 | mTelephonyManager 87 | .createForSubscriptionId(mSubscriptionInfo.getSubscriptionId()) 88 | .getCallStateForSubscription(); 89 | } 90 | } 91 | 92 | return thisState; 93 | } 94 | 95 | @Rpc( 96 | description = 97 | "Returns a constant indicating the radio technology (network type) currently" 98 | + "in use on the device for data transmission.") 99 | public int getDataNetworkType() { 100 | if (Build.VERSION.SDK_INT < 30) { 101 | return mTelephonyManager.getNetworkType(); 102 | } else { 103 | return mTelephonyManager.getDataNetworkType(); 104 | } 105 | } 106 | 107 | @Rpc( 108 | description = 109 | "Returns a constant indicating the radio technology (network type) currently" 110 | + "in use on the device for voice transmission.") 111 | public int getVoiceNetworkType() { 112 | return mTelephonyManager.getVoiceNetworkType(); 113 | } 114 | @Override 115 | public void shutdown() {} 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/WifiAwareManagerSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package com.google.android.mobly.snippet.bundled; 17 | 18 | import android.content.Context; 19 | import android.content.pm.PackageManager; 20 | import android.net.wifi.aware.WifiAwareManager; 21 | import androidx.test.platform.app.InstrumentationRegistry; 22 | import com.google.android.mobly.snippet.Snippet; 23 | import com.google.android.mobly.snippet.bundled.utils.Utils; 24 | import com.google.android.mobly.snippet.rpc.Rpc; 25 | 26 | /** Snippet class exposing Android APIs in WifiAwareManager. */ 27 | public class WifiAwareManagerSnippet implements Snippet { 28 | 29 | private static class WifiAwareManagerSnippetException extends Exception { 30 | private static final long serialVersionUID = 1; 31 | 32 | public WifiAwareManagerSnippetException(String msg) { 33 | super(msg); 34 | } 35 | } 36 | private final Context mContext; 37 | private boolean mIsAwareSupported; 38 | WifiAwareManager mWifiAwareManager; 39 | 40 | public WifiAwareManagerSnippet() throws Throwable { 41 | mContext = InstrumentationRegistry.getInstrumentation().getContext(); 42 | mIsAwareSupported = 43 | mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE); 44 | if (mIsAwareSupported) { 45 | mWifiAwareManager = (WifiAwareManager) mContext.getSystemService(Context.WIFI_AWARE_SERVICE); 46 | } 47 | Utils.adaptShellPermissionIfRequired(mContext); 48 | } 49 | 50 | /** Checks if Aware is available. This could return false if WiFi or location is disabled. */ 51 | @Rpc(description = "check if Aware is available.") 52 | public boolean wifiAwareIsAvailable() throws WifiAwareManagerSnippetException { 53 | if (!mIsAwareSupported) { 54 | throw new WifiAwareManagerSnippetException( 55 | "WifiAware is not supported in this device"); 56 | } 57 | return mWifiAwareManager.isAvailable(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothGattClientSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled.bluetooth; 18 | 19 | import android.bluetooth.BluetoothAdapter; 20 | import android.bluetooth.BluetoothDevice; 21 | import android.bluetooth.BluetoothGatt; 22 | import android.bluetooth.BluetoothGattCallback; 23 | import android.bluetooth.BluetoothGattCharacteristic; 24 | import android.bluetooth.BluetoothGattService; 25 | import android.bluetooth.BluetoothProfile; 26 | import android.content.Context; 27 | import android.os.Build.VERSION_CODES; 28 | import android.os.Bundle; 29 | import android.os.SystemClock; 30 | import android.util.Base64; 31 | import androidx.test.platform.app.InstrumentationRegistry; 32 | import com.google.android.mobly.snippet.Snippet; 33 | import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 34 | import com.google.android.mobly.snippet.bundled.utils.MbsEnums; 35 | import com.google.android.mobly.snippet.event.EventCache; 36 | import com.google.android.mobly.snippet.event.SnippetEvent; 37 | import com.google.android.mobly.snippet.rpc.AsyncRpc; 38 | import com.google.android.mobly.snippet.rpc.Rpc; 39 | import com.google.android.mobly.snippet.rpc.RpcMinSdk; 40 | import com.google.android.mobly.snippet.util.Log; 41 | import java.util.ArrayList; 42 | import java.util.HashMap; 43 | import org.json.JSONException; 44 | 45 | /** Snippet class exposing Android APIs in BluetoothGatt. */ 46 | public class BluetoothGattClientSnippet implements Snippet { 47 | private static class BluetoothGattClientSnippetException extends Exception { 48 | private static final long serialVersionUID = 1; 49 | 50 | public BluetoothGattClientSnippetException(String msg) { 51 | super(msg); 52 | } 53 | } 54 | 55 | private final Context context; 56 | private final EventCache eventCache; 57 | private final HashMap> 58 | characteristicHashMap; 59 | 60 | private BluetoothGatt bluetoothGattClient; 61 | 62 | private long connectionStartTime = 0; 63 | private long connectionEndTime = 0; 64 | 65 | public BluetoothGattClientSnippet() { 66 | context = InstrumentationRegistry.getInstrumentation().getContext(); 67 | eventCache = EventCache.getInstance(); 68 | characteristicHashMap = new HashMap<>(); 69 | } 70 | 71 | @RpcMinSdk(VERSION_CODES.LOLLIPOP) 72 | @AsyncRpc(description = "Start BLE client.") 73 | public void bleConnectGatt(String callbackId, String deviceAddress) throws JSONException { 74 | BluetoothDevice remoteDevice = 75 | BluetoothAdapter.getDefaultAdapter().getRemoteDevice(deviceAddress); 76 | BluetoothGattCallback gattCallback = new DefaultBluetoothGattCallback(callbackId); 77 | connectionStartTime = System.currentTimeMillis(); 78 | bluetoothGattClient = remoteDevice.connectGatt(context, false, gattCallback); 79 | Log.d("Connection start time is " + connectionStartTime); 80 | connectionEndTime = 0; 81 | } 82 | 83 | @RpcMinSdk(VERSION_CODES.LOLLIPOP) 84 | @Rpc(description = "Start BLE service discovery") 85 | public long bleDiscoverServices() throws BluetoothGattClientSnippetException { 86 | if (bluetoothGattClient == null) { 87 | throw new BluetoothGattClientSnippetException("BLE client is not initialized."); 88 | } 89 | long discoverServicesStartTime = SystemClock.elapsedRealtimeNanos(); 90 | Log.d("Discover services start time is " + discoverServicesStartTime); 91 | boolean result = bluetoothGattClient.discoverServices(); 92 | if (!result) { 93 | throw new BluetoothGattClientSnippetException("Discover services returned false."); 94 | } 95 | return discoverServicesStartTime; 96 | } 97 | 98 | @RpcMinSdk(VERSION_CODES.LOLLIPOP) 99 | @Rpc(description = "Stop BLE client.") 100 | public void bleDisconnect() throws BluetoothGattClientSnippetException { 101 | if (bluetoothGattClient == null) { 102 | throw new BluetoothGattClientSnippetException("BLE client is not initialized."); 103 | } 104 | bluetoothGattClient.disconnect(); 105 | } 106 | 107 | @RpcMinSdk(VERSION_CODES.LOLLIPOP) 108 | @Rpc(description = "BLE read operation.") 109 | public boolean bleReadOperation(String serviceUuid, String characteristicUuid) 110 | throws JSONException, BluetoothGattClientSnippetException { 111 | if (bluetoothGattClient == null) { 112 | throw new BluetoothGattClientSnippetException("BLE client is not initialized."); 113 | } 114 | boolean result = 115 | bluetoothGattClient.readCharacteristic( 116 | characteristicHashMap.get(serviceUuid).get(characteristicUuid)); 117 | Log.d("Read operation returned result " + result); 118 | return result; 119 | } 120 | 121 | @RpcMinSdk(VERSION_CODES.LOLLIPOP) 122 | @Rpc(description = "BLE write operation.") 123 | public boolean bleWriteOperation(String serviceUuid, String characteristicUuid, String data) 124 | throws JSONException, BluetoothGattClientSnippetException { 125 | if (bluetoothGattClient == null) { 126 | throw new BluetoothGattClientSnippetException("BLE client is not initialized."); 127 | } 128 | BluetoothGattCharacteristic characteristic = 129 | characteristicHashMap.get(serviceUuid).get(characteristicUuid); 130 | characteristic.setValue(Base64.decode(data, Base64.NO_WRAP)); 131 | boolean result = bluetoothGattClient.writeCharacteristic(characteristic); 132 | Log.d("Write operation returned result " + result); 133 | return result; 134 | } 135 | 136 | @RpcMinSdk(VERSION_CODES.LOLLIPOP) 137 | @Rpc(description = "Change MTU.") 138 | public void bleRequestMtu(int mtu) { 139 | bluetoothGattClient.requestMtu(mtu); 140 | } 141 | 142 | private class DefaultBluetoothGattCallback extends BluetoothGattCallback { 143 | private final String callbackId; 144 | 145 | DefaultBluetoothGattCallback(String callbackId) { 146 | this.callbackId = callbackId; 147 | } 148 | 149 | @Override 150 | public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 151 | SnippetEvent event = new SnippetEvent(callbackId, "onConnectionStateChange"); 152 | if (newState == BluetoothProfile.STATE_CONNECTED) { 153 | connectionEndTime = System.currentTimeMillis(); 154 | event.getData().putLong( 155 | "gattConnectionTimeMs", connectionEndTime - connectionStartTime); 156 | Log.d("Connection end time is " + connectionEndTime); 157 | } 158 | event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 159 | event.getData().putString("newState", MbsEnums.BLE_CONNECT_STATUS.getString(newState)); 160 | event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 161 | eventCache.postEvent(event); 162 | } 163 | 164 | @Override 165 | public void onServicesDiscovered(BluetoothGatt gatt, int status) { 166 | long discoverServicesEndTime = SystemClock.elapsedRealtimeNanos(); 167 | Log.d("Discover services end time is " + discoverServicesEndTime); 168 | SnippetEvent event = new SnippetEvent(callbackId, "onServiceDiscovered"); 169 | event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 170 | ArrayList services = new ArrayList<>(); 171 | for (BluetoothGattService service : gatt.getServices()) { 172 | HashMap characteristics = new HashMap<>(); 173 | for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { 174 | characteristics.put(characteristic.getUuid().toString(), characteristic); 175 | } 176 | characteristicHashMap.put(service.getUuid().toString(), characteristics); 177 | services.add(JsonSerializer.serializeBluetoothGattService(service)); 178 | } 179 | // TODO(66740428): Should not return services directly 180 | event.getData().putParcelableArrayList("Services", services); 181 | event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 182 | event.getData().putLong("discoveryServicesEndTime", discoverServicesEndTime); 183 | eventCache.postEvent(event); 184 | } 185 | 186 | @Override 187 | public void onCharacteristicRead( 188 | BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { 189 | SnippetEvent event = new SnippetEvent(callbackId, "onCharacteristicRead"); 190 | event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 191 | // TODO(66740428): Should return the characteristic instead of value 192 | event.getData() 193 | .putString("Data", 194 | Base64.encodeToString(characteristic.getValue(), Base64.NO_WRAP)); 195 | event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 196 | eventCache.postEvent(event); 197 | } 198 | 199 | @Override 200 | public void onCharacteristicWrite( 201 | BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { 202 | SnippetEvent event = new SnippetEvent(callbackId, "onCharacteristicWrite"); 203 | event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 204 | // TODO(66740428): Should return the characteristic instead of value 205 | event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 206 | eventCache.postEvent(event); 207 | } 208 | 209 | @Override 210 | public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { 211 | SnippetEvent event = new SnippetEvent(callbackId, "onReliableWriteCompleted"); 212 | event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 213 | event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 214 | eventCache.postEvent(event); 215 | } 216 | 217 | @Override 218 | public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { 219 | SnippetEvent event = new SnippetEvent(callbackId, "onMtuChanged"); 220 | event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 221 | event.getData().putInt("mtu", mtu); 222 | event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 223 | eventCache.postEvent(event); 224 | } 225 | } 226 | 227 | @Override 228 | public void shutdown() { 229 | if (bluetoothGattClient != null) { 230 | bluetoothGattClient.close(); 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothGattServerSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled.bluetooth; 18 | 19 | import android.bluetooth.BluetoothDevice; 20 | import android.bluetooth.BluetoothGatt; 21 | import android.bluetooth.BluetoothGattCharacteristic; 22 | import android.bluetooth.BluetoothGattServer; 23 | import android.bluetooth.BluetoothGattServerCallback; 24 | import android.bluetooth.BluetoothGattService; 25 | import android.bluetooth.BluetoothManager; 26 | import android.bluetooth.BluetoothProfile; 27 | import android.content.Context; 28 | import android.os.Build.VERSION_CODES; 29 | import android.os.DeadObjectException; 30 | import android.os.SystemClock; 31 | import android.util.Base64; 32 | import androidx.test.platform.app.InstrumentationRegistry; 33 | import com.google.android.mobly.snippet.Snippet; 34 | import com.google.android.mobly.snippet.bundled.utils.DataHolder; 35 | import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer; 36 | import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 37 | import com.google.android.mobly.snippet.bundled.utils.MbsEnums; 38 | import com.google.android.mobly.snippet.event.EventCache; 39 | import com.google.android.mobly.snippet.event.SnippetEvent; 40 | import com.google.android.mobly.snippet.rpc.AsyncRpc; 41 | import com.google.android.mobly.snippet.rpc.Rpc; 42 | import com.google.android.mobly.snippet.rpc.RpcMinSdk; 43 | import com.google.android.mobly.snippet.util.Log; 44 | import java.util.List; 45 | import org.json.JSONArray; 46 | import org.json.JSONException; 47 | import org.json.JSONObject; 48 | import java.util.UUID; 49 | 50 | /** Snippet class exposing Android APIs in BluetoothGattServer. */ 51 | public class BluetoothGattServerSnippet implements Snippet { 52 | private static class BluetoothGattServerSnippetException extends Exception { 53 | private static final long serialVersionUID = 1; 54 | 55 | public BluetoothGattServerSnippetException(String msg) { 56 | super(msg); 57 | } 58 | } 59 | 60 | private final Context context; 61 | private final BluetoothManager bluetoothManager; 62 | private final DataHolder dataHolder; 63 | private final EventCache eventCache; 64 | 65 | private BluetoothGattServer bluetoothGattServer; 66 | 67 | public BluetoothGattServerSnippet() { 68 | context = InstrumentationRegistry.getInstrumentation().getContext(); 69 | bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); 70 | dataHolder = new DataHolder(); 71 | eventCache = EventCache.getInstance(); 72 | } 73 | 74 | private BluetoothDevice getDeviceByAddress(String address) { 75 | List devices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT); 76 | for (BluetoothDevice device : devices) { 77 | if (device.getAddress().equals(address)) { 78 | return device; 79 | } 80 | } 81 | return null; 82 | } 83 | 84 | @RpcMinSdk(VERSION_CODES.LOLLIPOP) 85 | @AsyncRpc(description = "Start BLE server.") 86 | public void bleStartServer(String callbackId, JSONArray services) 87 | throws JSONException, DeadObjectException { 88 | BluetoothGattServerCallback gattServerCallback = 89 | new DefaultBluetoothGattServerCallback(callbackId); 90 | bluetoothGattServer = bluetoothManager.openGattServer(context, gattServerCallback); 91 | addServiceToGattServer(services); 92 | } 93 | 94 | @RpcMinSdk(VERSION_CODES.LOLLIPOP) 95 | @AsyncRpc(description = "Start BLE server with workaround.") 96 | public void bleStartServerWithWorkaround(String callbackId, JSONArray services) 97 | throws JSONException, DeadObjectException { 98 | BluetoothGattServerCallback gattServerCallback = 99 | new DefaultBluetoothGattServerCallback(callbackId); 100 | boolean isGattServerStarted = false; 101 | int count = 0; 102 | while (!isGattServerStarted && count < 5) { 103 | bluetoothGattServer = bluetoothManager.openGattServer(context, gattServerCallback); 104 | if (bluetoothGattServer != null) { 105 | addServiceToGattServer(services); 106 | isGattServerStarted = true; 107 | } else { 108 | SystemClock.sleep(1000); 109 | count++; 110 | } 111 | } 112 | } 113 | 114 | private void addServiceToGattServer(JSONArray services) throws JSONException { 115 | for (int i = 0; i < services.length(); i++) { 116 | JSONObject service = services.getJSONObject(i); 117 | BluetoothGattService bluetoothGattService = 118 | JsonDeserializer.jsonToBluetoothGattService(dataHolder, service); 119 | bluetoothGattServer.addService(bluetoothGattService); 120 | } 121 | } 122 | 123 | @RpcMinSdk(VERSION_CODES.LOLLIPOP) 124 | @Rpc(description = "Stop BLE server.") 125 | public void bleStopServer() throws BluetoothGattServerSnippetException { 126 | if (bluetoothGattServer == null) { 127 | throw new BluetoothGattServerSnippetException("BLE server is not initialized."); 128 | } 129 | bluetoothGattServer.close(); 130 | } 131 | 132 | @RpcMinSdk(VERSION_CODES.LOLLIPOP) 133 | @Rpc(description = "Disconnect a device from the server.") 134 | public void bleCancelConnectionByAddress(String address) throws BluetoothGattServerSnippetException { 135 | if (bluetoothGattServer == null) { 136 | throw new BluetoothGattServerSnippetException("BLE server is not initialized."); 137 | } 138 | BluetoothDevice device = getDeviceByAddress(address); 139 | if (device != null) { 140 | bluetoothGattServer.cancelConnection(device); 141 | } 142 | } 143 | 144 | @RpcMinSdk(VERSION_CODES.TIRAMISU) 145 | @Rpc(description = "Send a notification that a characteristic changed.") 146 | public void bleNotifyCharacteristicChanged( 147 | String address, 148 | String serviceUuid, 149 | String characteristicUuid, 150 | boolean confirm, 151 | String base64Value) 152 | throws BluetoothGattServerSnippetException { 153 | if (bluetoothGattServer == null) { 154 | throw new BluetoothGattServerSnippetException("BLE server is not initialized."); 155 | } 156 | 157 | BluetoothDevice device = getDeviceByAddress(address); 158 | if (device == null) { 159 | throw new BluetoothGattServerSnippetException("Device not found: " + address); 160 | } 161 | 162 | BluetoothGattService service = bluetoothGattServer.getService(UUID.fromString(serviceUuid)); 163 | if (service == null) { 164 | throw new BluetoothGattServerSnippetException("Service not found: " + serviceUuid); 165 | } 166 | BluetoothGattCharacteristic characteristic = 167 | service.getCharacteristic(UUID.fromString(characteristicUuid)); 168 | if (characteristic == null) { 169 | throw new BluetoothGattServerSnippetException( 170 | "Characteristic not found: " + characteristicUuid); 171 | } 172 | byte[] value = Base64.decode(base64Value, Base64.NO_WRAP); 173 | bluetoothGattServer.notifyCharacteristicChanged(device, characteristic, confirm, value); 174 | } 175 | 176 | private class DefaultBluetoothGattServerCallback extends BluetoothGattServerCallback { 177 | private final String callbackId; 178 | 179 | DefaultBluetoothGattServerCallback(String callbackId) { 180 | this.callbackId = callbackId; 181 | } 182 | 183 | @Override 184 | public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { 185 | SnippetEvent event = new SnippetEvent(callbackId, "onConnectionStateChange"); 186 | event.getData().putBundle("device", JsonSerializer.serializeBluetoothDevice(device)); 187 | event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 188 | event.getData().putString("newState", MbsEnums.BLE_CONNECT_STATUS.getString(newState)); 189 | eventCache.postEvent(event); 190 | } 191 | 192 | @Override 193 | public void onServiceAdded(int status, BluetoothGattService service) { 194 | Log.d("Bluetooth Gatt Server service added with status " + status); 195 | SnippetEvent event = new SnippetEvent(callbackId, "onServiceAdded"); 196 | event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 197 | event.getData() 198 | .putParcelable("Service", 199 | JsonSerializer.serializeBluetoothGattService(service)); 200 | eventCache.postEvent(event); 201 | } 202 | 203 | @Override 204 | public void onCharacteristicReadRequest( 205 | BluetoothDevice device, 206 | int requestId, 207 | int offset, 208 | BluetoothGattCharacteristic characteristic) { 209 | Log.d("Bluetooth Gatt Server received a read request"); 210 | if (dataHolder.get(characteristic) != null) { 211 | bluetoothGattServer.sendResponse( 212 | device, 213 | requestId, 214 | BluetoothGatt.GATT_SUCCESS, 215 | offset, 216 | Base64.decode(dataHolder.get(characteristic), Base64.NO_WRAP)); 217 | } else { 218 | bluetoothGattServer.sendResponse( 219 | device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null); 220 | } 221 | } 222 | 223 | @Override 224 | public void onCharacteristicWriteRequest( 225 | BluetoothDevice device, 226 | int requestId, 227 | BluetoothGattCharacteristic characteristic, 228 | boolean preparedWrite, 229 | boolean responseNeeded, 230 | int offset, 231 | byte[] value) { 232 | Log.d("Bluetooth Gatt Server received a write request"); 233 | bluetoothGattServer.sendResponse( 234 | device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null); 235 | SnippetEvent event = new SnippetEvent(callbackId, "onCharacteristicWriteRequest"); 236 | event.getData().putString("Data", Base64.encodeToString(value, Base64.NO_WRAP)); 237 | eventCache.postEvent(event); 238 | } 239 | 240 | @Override 241 | public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) { 242 | Log.d("Bluetooth Gatt Server received an execute write request"); 243 | bluetoothGattServer.sendResponse( 244 | device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null); 245 | } 246 | 247 | @Override 248 | public void onMtuChanged(BluetoothDevice device, int mtu) { 249 | SnippetEvent event = new SnippetEvent(callbackId, "onMtuChanged"); 250 | event.getData().putInt("mtu", mtu); 251 | event.getData().putBundle("device", JsonSerializer.serializeBluetoothDevice(device)); 252 | eventCache.postEvent(event); 253 | } 254 | } 255 | 256 | @Override 257 | public void shutdown() {} 258 | } 259 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | package com.google.android.mobly.snippet.bundled.bluetooth; 2 | 3 | import android.annotation.TargetApi; 4 | import android.bluetooth.BluetoothDevice; 5 | import android.content.BroadcastReceiver; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.IntentFilter; 9 | import android.os.Build; 10 | import com.google.android.mobly.snippet.util.Log; 11 | import com.google.android.mobly.snippet.bundled.utils.Utils; 12 | 13 | @TargetApi(Build.VERSION_CODES.KITKAT) 14 | public class PairingBroadcastReceiver extends BroadcastReceiver { 15 | private final Context mContext; 16 | public static IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); 17 | 18 | public PairingBroadcastReceiver(Context context) throws Throwable { 19 | mContext = context; 20 | Utils.adaptShellPermissionIfRequired(mContext); 21 | } 22 | 23 | @Override 24 | public void onReceive(Context context, Intent intent) { 25 | String action = intent.getAction(); 26 | if (action.equals(BluetoothDevice.ACTION_PAIRING_REQUEST)) { 27 | BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 28 | Log.d("Confirming pairing with device: " + device.getAddress()); 29 | device.setPairingConfirmation(true); 30 | mContext.unregisterReceiver(this); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java: -------------------------------------------------------------------------------- 1 | package com.google.android.mobly.snippet.bundled.bluetooth.profiles; 2 | 3 | import android.annotation.TargetApi; 4 | import android.bluetooth.BluetoothA2dp; 5 | import android.bluetooth.BluetoothAdapter; 6 | import android.bluetooth.BluetoothDevice; 7 | import android.bluetooth.BluetoothProfile; 8 | import android.content.Context; 9 | import android.content.IntentFilter; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import androidx.test.platform.app.InstrumentationRegistry; 13 | import com.google.android.mobly.snippet.Snippet; 14 | import com.google.android.mobly.snippet.bundled.bluetooth.BluetoothAdapterSnippet; 15 | import com.google.android.mobly.snippet.bundled.bluetooth.PairingBroadcastReceiver; 16 | import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 17 | import com.google.android.mobly.snippet.bundled.utils.Utils; 18 | import com.google.android.mobly.snippet.rpc.Rpc; 19 | import com.google.android.mobly.snippet.rpc.RpcMinSdk; 20 | import java.util.ArrayList; 21 | 22 | public class BluetoothA2dpSnippet implements Snippet { 23 | public static class BluetoothA2dpSnippetException extends Exception { 24 | private static final long serialVersionUID = 1; 25 | 26 | BluetoothA2dpSnippetException(String msg) { 27 | super(msg); 28 | } 29 | } 30 | 31 | private Context mContext; 32 | private static boolean sIsA2dpProfileReady = false; 33 | private static BluetoothA2dp sA2dpProfile; 34 | private final JsonSerializer mJsonSerializer = new JsonSerializer(); 35 | 36 | public BluetoothA2dpSnippet() throws BluetoothA2dpSnippetException { 37 | mContext = InstrumentationRegistry.getInstrumentation().getContext(); 38 | BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 39 | boolean isProxyConnectionStarted = bluetoothAdapter.getProfileProxy( 40 | mContext, new A2dpServiceListener(), BluetoothProfile.A2DP); 41 | if (!isProxyConnectionStarted) { 42 | throw new BluetoothA2dpSnippetException( 43 | "Failed to start proxy connection for A2DP profile."); 44 | } 45 | Utils.waitUntil(() -> sIsA2dpProfileReady, 60); 46 | } 47 | 48 | private static class A2dpServiceListener implements BluetoothProfile.ServiceListener { 49 | @Override 50 | public void onServiceConnected(int var1, BluetoothProfile profile) { 51 | sA2dpProfile = (BluetoothA2dp) profile; 52 | sIsA2dpProfileReady = true; 53 | } 54 | 55 | @Override 56 | public void onServiceDisconnected(int var1) { 57 | sIsA2dpProfileReady = false; 58 | } 59 | } 60 | 61 | @TargetApi(Build.VERSION_CODES.KITKAT) 62 | @RpcMinSdk(Build.VERSION_CODES.KITKAT) 63 | @Rpc( 64 | description = 65 | "Connects to a paired or discovered device with A2DP profile." 66 | + "If a device has been discovered but not paired, this will pair it.") 67 | public void btA2dpConnect(String deviceAddress) throws Throwable { 68 | BluetoothDevice device = BluetoothAdapterSnippet.getKnownDeviceByAddress(deviceAddress); 69 | IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); 70 | mContext.registerReceiver(new PairingBroadcastReceiver(mContext), filter); 71 | Utils.invokeByReflection(sA2dpProfile, "connect", device); 72 | if (!Utils.waitUntil( 73 | () -> sA2dpProfile.getConnectionState(device) == BluetoothA2dp.STATE_CONNECTED, 74 | 120)) { 75 | throw new BluetoothA2dpSnippetException( 76 | "Failed to connect to device " 77 | + device.getName() 78 | + "|" 79 | + device.getAddress() 80 | + " with A2DP profile within 2min."); 81 | } 82 | } 83 | 84 | @Rpc(description = "Disconnects a device from A2DP profile.") 85 | public void btA2dpDisconnect(String deviceAddress) throws Throwable { 86 | BluetoothDevice device = getConnectedBluetoothDevice(deviceAddress); 87 | Utils.invokeByReflection(sA2dpProfile, "disconnect", device); 88 | if (!Utils.waitUntil( 89 | () -> sA2dpProfile.getConnectionState(device) == BluetoothA2dp.STATE_DISCONNECTED, 90 | 120)) { 91 | throw new BluetoothA2dpSnippetException( 92 | "Failed to disconnect device " 93 | + device.getName() 94 | + "|" 95 | + device.getAddress() 96 | + " from A2DP profile within 2min."); 97 | } 98 | } 99 | 100 | @Rpc(description = "Gets all the devices currently connected via A2DP profile.") 101 | public ArrayList btA2dpGetConnectedDevices() { 102 | return mJsonSerializer.serializeBluetoothDeviceList(sA2dpProfile.getConnectedDevices()); 103 | } 104 | 105 | @Rpc(description = "Checks if a device is streaming audio via A2DP profile.") 106 | public boolean btIsA2dpPlaying(String deviceAddress) throws Throwable { 107 | BluetoothDevice device = getConnectedBluetoothDevice(deviceAddress); 108 | return sA2dpProfile.isA2dpPlaying(device); 109 | } 110 | 111 | private BluetoothDevice getConnectedBluetoothDevice(String deviceAddress) 112 | throws BluetoothA2dpSnippetException { 113 | for (BluetoothDevice device : sA2dpProfile.getConnectedDevices()) { 114 | if (device.getAddress().equalsIgnoreCase(deviceAddress)) { 115 | return device; 116 | } 117 | } 118 | throw new BluetoothA2dpSnippetException( 119 | "No device with address " + deviceAddress + " is connected via A2DP."); 120 | } 121 | 122 | @Override 123 | public void shutdown() {} 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothHeadsetSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled.bluetooth.profiles; 18 | 19 | import android.annotation.TargetApi; 20 | import android.bluetooth.BluetoothAdapter; 21 | import android.bluetooth.BluetoothDevice; 22 | import android.bluetooth.BluetoothHeadset; 23 | import android.bluetooth.BluetoothProfile; 24 | import android.content.Context; 25 | import android.content.IntentFilter; 26 | import android.os.Build; 27 | import android.os.Bundle; 28 | import androidx.test.platform.app.InstrumentationRegistry; 29 | import com.google.android.mobly.snippet.Snippet; 30 | import com.google.android.mobly.snippet.bundled.bluetooth.BluetoothAdapterSnippet; 31 | import com.google.android.mobly.snippet.bundled.bluetooth.PairingBroadcastReceiver; 32 | import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 33 | import com.google.android.mobly.snippet.bundled.utils.Utils; 34 | import com.google.android.mobly.snippet.rpc.Rpc; 35 | import com.google.android.mobly.snippet.rpc.RpcMinSdk; 36 | import java.util.ArrayList; 37 | import java.util.Set; 38 | 39 | /** 40 | * Custom exception class for handling exceptions within the BluetoothHeadsetSnippet. 41 | * This exception is meant to encapsulate and convey specific error information related to 42 | * BluetoothHeadsetSnippet operations. 43 | */ 44 | public class BluetoothHeadsetSnippet implements Snippet { 45 | 46 | private final JsonSerializer mJsonSerializer = new JsonSerializer(); 47 | private static final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 48 | 49 | private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); 50 | public static class BluetoothHeadsetSnippetException extends Exception { 51 | private static final long serialVersionUID = 1; 52 | 53 | /** 54 | * Constructs a BluetoothHeadsetSnippetException with the specified detail message. 55 | * 56 | * @param msg The detail message providing information about the exception. 57 | */ 58 | BluetoothHeadsetSnippetException(String msg) { 59 | super(msg); 60 | } 61 | } 62 | 63 | private BluetoothHeadset mBluetoothHeadset; 64 | private static final int HEADSET = 1; 65 | 66 | private final BluetoothProfile.ServiceListener mProfileListener = new BluetoothProfile.ServiceListener() { 67 | @Override 68 | public void onServiceConnected(int var1, BluetoothProfile profile) { 69 | if (var1 == HEADSET) { 70 | mBluetoothHeadset = (BluetoothHeadset)profile; 71 | } 72 | } 73 | @Override 74 | public void onServiceDisconnected(int var1) { 75 | if (var1 == HEADSET) { 76 | mBluetoothHeadset = null; 77 | } 78 | } 79 | }; 80 | 81 | public BluetoothHeadsetSnippet() throws Throwable { 82 | IntentFilter filter = new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 83 | filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); 84 | boolean isProxyConnectionStarted = mBluetoothAdapter.getProfileProxy( 85 | mContext, mProfileListener, BluetoothProfile.HEADSET); 86 | if (!isProxyConnectionStarted) { 87 | throw new BluetoothHeadsetSnippetException( 88 | "Failed to start proxy connection for HEADSET profile."); 89 | } 90 | Utils.waitUntil(() -> mBluetoothHeadset != null, 60); 91 | mContext.registerReceiver(new PairingBroadcastReceiver(mContext), filter); 92 | } 93 | 94 | @TargetApi(Build.VERSION_CODES.KITKAT) 95 | @RpcMinSdk(Build.VERSION_CODES.KITKAT) 96 | @Rpc( 97 | description = 98 | "Connects to a paired or discovered device with HEADSET profile." 99 | + "If a device has been discovered but not paired, this will pair it.") 100 | public void btHfpConnect(String deviceAddress) throws Throwable { 101 | BluetoothDevice device = BluetoothAdapterSnippet.getKnownDeviceByAddress(deviceAddress); 102 | IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); 103 | mContext.registerReceiver(new PairingBroadcastReceiver(mContext), filter); 104 | Utils.invokeByReflection(mBluetoothHeadset, "connect", device); 105 | if (!Utils.waitUntil( 106 | () -> mBluetoothHeadset.getConnectionState(device) == BluetoothHeadset.STATE_CONNECTED, 107 | 120)) { 108 | throw new BluetoothHeadsetSnippetException( 109 | "Failed to connect to device " 110 | + device.getName() 111 | + "|" 112 | + device.getAddress() 113 | + " with HEADSET profile within 2min."); 114 | } 115 | } 116 | 117 | @Rpc(description = "Disconnects a device from HEADSET profile.") 118 | public void btHfpDisconnect(String deviceAddress) throws Throwable { 119 | BluetoothDevice device = getConnectedBluetoothDevice(deviceAddress); 120 | Utils.invokeByReflection(mBluetoothHeadset, "disconnect", device); 121 | if (!Utils.waitUntil( 122 | () -> mBluetoothHeadset.getConnectionState(device) == BluetoothHeadset.STATE_DISCONNECTED, 123 | 120)) { 124 | throw new BluetoothHeadsetSnippetException( 125 | "Failed to disconnect device " 126 | + device.getName() 127 | + "|" 128 | + device.getAddress() 129 | + " from HEADSET profile within 2min."); 130 | } 131 | } 132 | 133 | /** 134 | * Returns the connection state for a Bluetooth device with the specified name. 135 | * 136 | * @param deviceAddress The address of the Bluetooth device. 137 | * @return The connection state for the specified device. 138 | * @throws BluetoothHeadsetSnippetException If no device with the specified name is connected via HEADSET. 139 | */ 140 | @Rpc(description = "Returns connection state.") 141 | public int btHfpGetConnectionState(String deviceAddress) throws BluetoothHeadsetSnippetException { 142 | Set pairedDevices = mBluetoothAdapter.getBondedDevices(); 143 | for (BluetoothDevice device : pairedDevices) { 144 | if (device.getAddress().equalsIgnoreCase(deviceAddress)) { 145 | return mBluetoothHeadset.getConnectionState(device); 146 | } 147 | } 148 | throw new BluetoothHeadsetSnippetException("No device with name " + deviceAddress +" is connected via HEADSET."); 149 | } 150 | 151 | /** 152 | * Starts voice recognition for the Bluetooth device with the specified name. 153 | * 154 | * @param deviceAddress The address of the Bluetooth device. 155 | * @return True if voice recognition is successfully started; false otherwise. 156 | * @throws BluetoothHeadsetSnippetException If no device with the specified name is found or if an error 157 | * occurs during the startVoiceRecognition operation. 158 | */ 159 | @Rpc(description = "Starts voice recognition.") 160 | public boolean btHfpStartVoiceRecognition(String deviceAddress) throws BluetoothHeadsetSnippetException{ 161 | Set pairedDevices = mBluetoothAdapter.getBondedDevices(); 162 | for (BluetoothDevice device : pairedDevices) { 163 | if (device.getAddress().equalsIgnoreCase(deviceAddress)) { 164 | return mBluetoothHeadset.startVoiceRecognition(device); 165 | } 166 | } 167 | throw new BluetoothHeadsetSnippetException("No device with name " + deviceAddress +" is connected via HEADSET."); 168 | } 169 | 170 | 171 | /** 172 | * Stops voice recognition for the Bluetooth device with the specified name. 173 | * 174 | * @param deviceAddress The address of the Bluetooth device. 175 | * @return True if voice recognition is successfully started; false otherwise. 176 | * @throws BluetoothHeadsetSnippetException If no device with the specified name is found or if an error 177 | * occurs during the startVoiceRecognition operation. 178 | */ 179 | @Rpc(description = "Stops voice recognition.") 180 | public boolean btHfpStopVoiceRecognition(String deviceAddress) throws BluetoothHeadsetSnippetException { 181 | Set pairedDevices = mBluetoothAdapter.getBondedDevices(); 182 | for (BluetoothDevice device : pairedDevices) { 183 | if (device.getAddress().equalsIgnoreCase(deviceAddress)) { 184 | return mBluetoothHeadset.stopVoiceRecognition(device); 185 | } 186 | } 187 | throw new BluetoothHeadsetSnippetException("No device with name " + deviceAddress +" is connected via HEADSET."); 188 | } 189 | 190 | @Rpc(description = "Checks whether the headset supports voice recognition;") 191 | public boolean btHfpIsVoiceRecognitionSupported(String deviceAddress) throws BluetoothHeadsetSnippetException { 192 | Set pairedDevices = mBluetoothAdapter.getBondedDevices(); 193 | for (BluetoothDevice device : pairedDevices) { 194 | if (device.getAddress().equalsIgnoreCase(deviceAddress)) { 195 | return mBluetoothHeadset.isVoiceRecognitionSupported(device); 196 | } 197 | } 198 | throw new BluetoothHeadsetSnippetException("No device with name " + deviceAddress +" is connected via HEADSET."); 199 | } 200 | @Rpc(description = "Gets all the devices currently connected via HFP profile.") 201 | public ArrayList btHfpGetConnectedDevices() { 202 | return mJsonSerializer.serializeBluetoothDeviceList(mBluetoothHeadset.getConnectedDevices()); 203 | } 204 | 205 | private BluetoothDevice getConnectedBluetoothDevice(String deviceAddress) 206 | throws BluetoothHeadsetSnippetException { 207 | for (BluetoothDevice device : mBluetoothHeadset.getConnectedDevices()) { 208 | if (device.getAddress().equalsIgnoreCase(deviceAddress)) { 209 | return device; 210 | } 211 | } 212 | throw new BluetoothHeadsetSnippetException( 213 | "No device with address " + deviceAddress + " is connected via HEADSET."); 214 | } 215 | 216 | @Override 217 | public void shutdown() { } 218 | } 219 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothHearingAidSnippet.java: -------------------------------------------------------------------------------- 1 | package com.google.android.mobly.snippet.bundled.bluetooth.profiles; 2 | 3 | import android.annotation.TargetApi; 4 | import android.bluetooth.BluetoothAdapter; 5 | import android.bluetooth.BluetoothDevice; 6 | import android.bluetooth.BluetoothHearingAid; 7 | import android.bluetooth.BluetoothProfile; 8 | import android.content.Context; 9 | import android.content.IntentFilter; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import androidx.test.platform.app.InstrumentationRegistry; 13 | import com.google.android.mobly.snippet.Snippet; 14 | import com.google.android.mobly.snippet.bundled.bluetooth.BluetoothAdapterSnippet; 15 | import com.google.android.mobly.snippet.bundled.bluetooth.PairingBroadcastReceiver; 16 | import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 17 | import com.google.android.mobly.snippet.bundled.utils.Utils; 18 | import com.google.android.mobly.snippet.rpc.Rpc; 19 | import com.google.android.mobly.snippet.rpc.RpcMinSdk; 20 | import com.google.common.base.Ascii; 21 | import java.util.ArrayList; 22 | 23 | public class BluetoothHearingAidSnippet implements Snippet { 24 | public static class BluetoothHearingAidSnippetException extends Exception { 25 | private static final long serialVersionUID = 1; 26 | 27 | BluetoothHearingAidSnippetException(String msg) { 28 | super(msg); 29 | } 30 | } 31 | 32 | private static final int TIMEOUT_SEC = 60; 33 | 34 | private final Context context; 35 | private static boolean isHearingAidProfileReady = false; 36 | private static BluetoothHearingAid hearingAidProfile; 37 | private final JsonSerializer jsonSerializer = new JsonSerializer(); 38 | 39 | @TargetApi(Build.VERSION_CODES.Q) 40 | public BluetoothHearingAidSnippet() throws BluetoothHearingAidSnippetException { 41 | context = InstrumentationRegistry.getInstrumentation().getContext(); 42 | BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 43 | boolean isProxyConnectionStarted = bluetoothAdapter.getProfileProxy( 44 | context, new HearingAidServiceListener(), BluetoothProfile.HEARING_AID); 45 | if (!isProxyConnectionStarted) { 46 | throw new BluetoothHearingAidSnippetException( 47 | "Failed to start proxy connection for HEARING AID profile."); 48 | } 49 | Utils.waitUntil(() -> isHearingAidProfileReady, TIMEOUT_SEC); 50 | } 51 | 52 | @TargetApi(Build.VERSION_CODES.Q) 53 | private static class HearingAidServiceListener implements BluetoothProfile.ServiceListener { 54 | @Override 55 | public void onServiceConnected(int var1, BluetoothProfile profile) { 56 | hearingAidProfile = (BluetoothHearingAid) profile; 57 | isHearingAidProfileReady = true; 58 | } 59 | 60 | @Override 61 | public void onServiceDisconnected(int var1) { 62 | isHearingAidProfileReady = false; 63 | } 64 | } 65 | 66 | @TargetApi(Build.VERSION_CODES.Q) 67 | @RpcMinSdk(Build.VERSION_CODES.Q) 68 | @Rpc(description = "Connects to a paired or discovered device with HA profile.") 69 | public void btHearingAidConnect(String deviceAddress) throws Throwable { 70 | BluetoothDevice device = BluetoothAdapterSnippet.getKnownDeviceByAddress(deviceAddress); 71 | IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); 72 | context.registerReceiver(new PairingBroadcastReceiver(context), filter); 73 | Utils.invokeByReflection(hearingAidProfile, "connect", device); 74 | if (!Utils.waitUntil( 75 | () -> 76 | hearingAidProfile.getConnectionState(device) 77 | == BluetoothHearingAid.STATE_CONNECTED, 78 | TIMEOUT_SEC)) { 79 | throw new BluetoothHearingAidSnippetException( 80 | String.format( 81 | "Failed to connect to device %s|%s with HA profile within %d sec.", 82 | device.getName(), device.getAddress(), TIMEOUT_SEC)); 83 | } 84 | } 85 | 86 | @Rpc(description = "Disconnects a device from HA profile.") 87 | public void btHearingAidDisconnect(String deviceAddress) throws Throwable { 88 | BluetoothDevice device = getConnectedBluetoothDevice(deviceAddress); 89 | Utils.invokeByReflection(hearingAidProfile, "disconnect", device); 90 | if (!Utils.waitUntil( 91 | () -> 92 | hearingAidProfile.getConnectionState(device) 93 | == BluetoothHearingAid.STATE_DISCONNECTED, 94 | TIMEOUT_SEC)) { 95 | throw new BluetoothHearingAidSnippetException( 96 | String.format( 97 | "Failed to disconnect to device %s|%s with HA profile within %d sec.", 98 | device.getName(), device.getAddress(), TIMEOUT_SEC)); 99 | } 100 | } 101 | 102 | @Rpc(description = "Gets all the devices currently connected via HA profile.") 103 | public ArrayList btHearingAidGetConnectedDevices() { 104 | return jsonSerializer.serializeBluetoothDeviceList(hearingAidProfile.getConnectedDevices()); 105 | } 106 | 107 | private static BluetoothDevice getConnectedBluetoothDevice(String deviceAddress) 108 | throws BluetoothHearingAidSnippetException { 109 | for (BluetoothDevice device : hearingAidProfile.getConnectedDevices()) { 110 | if (Ascii.equalsIgnoreCase(device.getAddress(), deviceAddress)) { 111 | return device; 112 | } 113 | } 114 | throw new BluetoothHearingAidSnippetException(String.format( 115 | "No device with address %s is connected via HA Profile.", deviceAddress)); 116 | } 117 | 118 | @Override 119 | public void shutdown() {} 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothLeAudioSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled.bluetooth.profiles; 18 | 19 | import android.annotation.TargetApi; 20 | import android.bluetooth.BluetoothAdapter; 21 | import android.bluetooth.BluetoothDevice; 22 | import android.bluetooth.BluetoothLeAudio; 23 | import android.bluetooth.BluetoothProfile; 24 | import android.content.Context; 25 | import android.content.IntentFilter; 26 | import android.os.Build; 27 | import android.os.Bundle; 28 | import androidx.test.platform.app.InstrumentationRegistry; 29 | import com.google.android.mobly.snippet.Snippet; 30 | import com.google.android.mobly.snippet.bundled.bluetooth.BluetoothAdapterSnippet; 31 | import com.google.android.mobly.snippet.bundled.bluetooth.PairingBroadcastReceiver; 32 | import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 33 | import com.google.android.mobly.snippet.bundled.utils.Utils; 34 | import com.google.android.mobly.snippet.rpc.Rpc; 35 | import com.google.android.mobly.snippet.rpc.RpcMinSdk; 36 | import java.util.ArrayList; 37 | 38 | /** Snippet class exposing Bluetooth LE Audio profile. */ 39 | public class BluetoothLeAudioSnippet implements Snippet { 40 | public static class BluetoothLeAudioSnippetException extends Exception { 41 | private static final long serialVersionUID = 1; 42 | 43 | /** 44 | * Constructs a BluetoothLeAudioSnippetException with the specified detail message. 45 | * 46 | * @param msg The detail message providing information about the exception. 47 | */ 48 | BluetoothLeAudioSnippetException(String msg) { 49 | super(msg); 50 | } 51 | } 52 | 53 | private final Context mContext; 54 | private static boolean sIsLeAudioProfileReady = false; 55 | private static BluetoothLeAudio sLeAudioProfile; 56 | private final JsonSerializer mJsonSerializer = new JsonSerializer(); 57 | 58 | public BluetoothLeAudioSnippet() throws BluetoothLeAudioSnippetException { 59 | mContext = InstrumentationRegistry.getInstrumentation().getContext(); 60 | BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 61 | boolean isProxyConnectionStarted = bluetoothAdapter.getProfileProxy( 62 | mContext, new LeAudioServiceListener(), BluetoothProfile.LE_AUDIO); 63 | if (!isProxyConnectionStarted) { 64 | throw new BluetoothLeAudioSnippetException( 65 | "Failed to start proxy connection for LE AUDIO profile."); 66 | } 67 | Utils.waitUntil(() -> sIsLeAudioProfileReady, 60); 68 | } 69 | 70 | @TargetApi(Build.VERSION_CODES.TIRAMISU) 71 | @RpcMinSdk(Build.VERSION_CODES.TIRAMISU) 72 | @Rpc( 73 | description = 74 | "Connects to a paired or discovered device with LE Audio profile." 75 | + "If a device has been discovered but not paired, this will pair it.") 76 | public void btLeAudioConnect(String deviceAddress) throws Throwable { 77 | BluetoothDevice device = BluetoothAdapterSnippet.getKnownDeviceByAddress(deviceAddress); 78 | IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); 79 | mContext.registerReceiver(new PairingBroadcastReceiver(mContext), filter); 80 | Utils.invokeByReflection(sLeAudioProfile, "connect", device); 81 | if (!Utils.waitUntil( 82 | () -> sLeAudioProfile.getConnectionState(device) == BluetoothProfile.STATE_CONNECTED, 83 | 120)) { 84 | throw new BluetoothLeAudioSnippetException( 85 | "Failed to connect to device " 86 | + device.getName() 87 | + "|" 88 | + device.getAddress() 89 | + " with LE Audio profile within 2min."); 90 | } 91 | } 92 | 93 | @TargetApi(Build.VERSION_CODES.TIRAMISU) 94 | @RpcMinSdk(Build.VERSION_CODES.TIRAMISU) 95 | @Rpc(description = "Disconnects a device from LE Audio profile.") 96 | public void btLeAudioDisconnect(String deviceAddress) throws Throwable { 97 | BluetoothDevice device = getConnectedBluetoothDevice(deviceAddress); 98 | Utils.invokeByReflection(sLeAudioProfile, "disconnect", device); 99 | if (!Utils.waitUntil( 100 | () -> sLeAudioProfile.getConnectionState(device) == BluetoothProfile.STATE_DISCONNECTED, 101 | 120)) { 102 | throw new BluetoothLeAudioSnippetException( 103 | "Failed to disconnect device " 104 | + device.getName() 105 | + "|" 106 | + device.getAddress() 107 | + " from LE Audio profile within 2min."); 108 | } 109 | } 110 | 111 | /** Service Listener for {@link BluetoothLeAudio}. */ 112 | private static class LeAudioServiceListener implements BluetoothProfile.ServiceListener { 113 | 114 | @Override 115 | public void onServiceConnected(int profileType, BluetoothProfile profile) { 116 | sLeAudioProfile = (BluetoothLeAudio) profile; 117 | sIsLeAudioProfileReady = true; 118 | } 119 | 120 | @Override 121 | public void onServiceDisconnected(int profileType) { 122 | sIsLeAudioProfileReady = false; 123 | } 124 | } 125 | 126 | @TargetApi(Build.VERSION_CODES.TIRAMISU) 127 | @RpcMinSdk(Build.VERSION_CODES.TIRAMISU) 128 | @Rpc(description = "Gets all the devices currently connected via LE Audio profile.") 129 | public ArrayList btLeAudioGetConnectedDevices() { 130 | return mJsonSerializer.serializeBluetoothDeviceList(sLeAudioProfile.getConnectedDevices()); 131 | } 132 | 133 | private BluetoothDevice getConnectedBluetoothDevice(String deviceAddress) 134 | throws BluetoothLeAudioSnippetException { 135 | for (BluetoothDevice device : sLeAudioProfile.getConnectedDevices()) { 136 | if (device.getAddress().equalsIgnoreCase(deviceAddress)) { 137 | return device; 138 | } 139 | } 140 | throw new BluetoothLeAudioSnippetException( 141 | "No device with address " + deviceAddress + " is connected via LE Audio."); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/utils/DataHolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled.utils; 18 | 19 | import android.bluetooth.BluetoothGattCharacteristic; 20 | import java.util.HashMap; 21 | 22 | /** A holder to hold android objects for snippets. */ 23 | // TODO(ko1in1u): For future extensions between Snippet classes and Utils. 24 | public class DataHolder { 25 | private final HashMap dataToBeRead; 26 | 27 | public DataHolder() { 28 | dataToBeRead = new HashMap<>(); 29 | } 30 | 31 | public String get(BluetoothGattCharacteristic characteristic) { 32 | return dataToBeRead.get(characteristic); 33 | } 34 | 35 | public void insertData(BluetoothGattCharacteristic characteristic, String string) { 36 | dataToBeRead.put(characteristic, string); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled.utils; 18 | 19 | import android.annotation.TargetApi; 20 | import android.bluetooth.BluetoothGattCharacteristic; 21 | import android.bluetooth.BluetoothGattDescriptor; 22 | import android.bluetooth.BluetoothGattService; 23 | import android.bluetooth.le.AdvertiseData; 24 | import android.bluetooth.le.AdvertiseSettings; 25 | import android.bluetooth.le.ScanFilter; 26 | import android.bluetooth.le.ScanSettings; 27 | import android.net.wifi.WifiConfiguration; 28 | import android.os.Build; 29 | import android.os.ParcelUuid; 30 | import android.util.Base64; 31 | import java.util.UUID; 32 | import org.json.JSONArray; 33 | import org.json.JSONException; 34 | import org.json.JSONObject; 35 | 36 | /** 37 | * A collection of methods used to deserialize JSON strings into data objects defined in Android 38 | * API. 39 | */ 40 | public class JsonDeserializer { 41 | 42 | private JsonDeserializer() {} 43 | 44 | public static WifiConfiguration jsonToWifiConfig(JSONObject jsonObject) throws JSONException { 45 | WifiConfiguration config = new WifiConfiguration(); 46 | config.SSID = "\"" + jsonObject.getString("SSID") + "\""; 47 | config.hiddenSSID = jsonObject.optBoolean("hiddenSSID", false); 48 | if (jsonObject.has("password")) { 49 | config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); 50 | config.preSharedKey = "\"" + jsonObject.getString("password") + "\""; 51 | } else { 52 | config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); 53 | } 54 | if (jsonObject.has("BSSID")) { 55 | config.BSSID = jsonObject.getString("BSSID"); 56 | } 57 | return config; 58 | } 59 | 60 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 61 | public static AdvertiseSettings jsonToBleAdvertiseSettings(JSONObject jsonObject) 62 | throws JSONException { 63 | AdvertiseSettings.Builder builder = new AdvertiseSettings.Builder(); 64 | if (jsonObject.has("AdvertiseMode")) { 65 | int mode = MbsEnums.BLE_ADVERTISE_MODE.getInt(jsonObject.getString("AdvertiseMode")); 66 | builder.setAdvertiseMode(mode); 67 | } 68 | // Timeout in milliseconds. 69 | if (jsonObject.has("Timeout")) { 70 | builder.setTimeout(jsonObject.getInt("Timeout")); 71 | } 72 | if (jsonObject.has("Connectable")) { 73 | builder.setConnectable(jsonObject.getBoolean("Connectable")); 74 | } 75 | if (jsonObject.has("TxPowerLevel")) { 76 | int txPowerLevel = 77 | MbsEnums.BLE_ADVERTISE_TX_POWER.getInt(jsonObject.getString("TxPowerLevel")); 78 | builder.setTxPowerLevel(txPowerLevel); 79 | } 80 | return builder.build(); 81 | } 82 | 83 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 84 | public static AdvertiseData jsonToBleAdvertiseData(JSONObject jsonObject) throws JSONException { 85 | AdvertiseData.Builder builder = new AdvertiseData.Builder(); 86 | if (jsonObject.has("IncludeDeviceName")) { 87 | builder.setIncludeDeviceName(jsonObject.getBoolean("IncludeDeviceName")); 88 | } 89 | if (jsonObject.has("IncludeTxPowerLevel")) { 90 | builder.setIncludeTxPowerLevel(jsonObject.getBoolean("IncludeTxPowerLevel")); 91 | } 92 | if (jsonObject.has("ServiceData")) { 93 | JSONArray serviceData = jsonObject.getJSONArray("ServiceData"); 94 | for (int i = 0; i < serviceData.length(); i++) { 95 | JSONObject dataSet = serviceData.getJSONObject(i); 96 | ParcelUuid parcelUuid = ParcelUuid.fromString(dataSet.getString("UUID")); 97 | builder.addServiceUuid(parcelUuid); 98 | if (dataSet.has("Data")) { 99 | byte[] data = Base64.decode(dataSet.getString("Data"), Base64.DEFAULT); 100 | builder.addServiceData(parcelUuid, data); 101 | } 102 | } 103 | } 104 | if (jsonObject.has("ManufacturerData")) { 105 | JSONObject manufacturerData = jsonObject.getJSONObject("ManufacturerData"); 106 | int manufacturerId = manufacturerData.getInt("ManufacturerId"); 107 | byte[] manufacturerSpecificData = 108 | Base64.decode( 109 | manufacturerData.getString("ManufacturerSpecificData"), Base64.DEFAULT); 110 | builder.addManufacturerData(manufacturerId, manufacturerSpecificData); 111 | } 112 | return builder.build(); 113 | } 114 | 115 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 116 | public static BluetoothGattService jsonToBluetoothGattService( 117 | DataHolder dataHolder, JSONObject jsonObject) throws JSONException { 118 | BluetoothGattService service = 119 | new BluetoothGattService( 120 | UUID.fromString(jsonObject.getString("UUID")), 121 | MbsEnums.BLE_SERVICE_TYPE.getInt(jsonObject.getString("Type"))); 122 | JSONArray characteristics = jsonObject.getJSONArray("Characteristics"); 123 | for (int i = 0; i < characteristics.length(); i++) { 124 | BluetoothGattCharacteristic characteristic = 125 | jsonToBluetoothGattCharacteristic(dataHolder, characteristics.getJSONObject(i)); 126 | service.addCharacteristic(characteristic); 127 | } 128 | return service; 129 | } 130 | 131 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 132 | public static BluetoothGattCharacteristic jsonToBluetoothGattCharacteristic( 133 | DataHolder dataHolder, JSONObject jsonObject) throws JSONException { 134 | BluetoothGattCharacteristic characteristic = 135 | new BluetoothGattCharacteristic( 136 | UUID.fromString(jsonObject.getString("UUID")), 137 | // A characteristic can have multiple properties (e.g. PROPERTY_READ and PROPERTY_WRITE). 138 | // Use the BitwiseOr to extract all the properties from the json string. 139 | MbsEnums.BLE_PROPERTY_TYPE.getIntBitwiseOr(jsonObject.getString("Properties")), 140 | // A characteristic can have multiple permissions (e.g. PERMISSION_READ and PERMISSION_WRITE). 141 | // Use the BitwiseOr to extract all the permissions from the json string. 142 | MbsEnums.BLE_PERMISSION_TYPE.getIntBitwiseOr(jsonObject.getString("Permissions"))); 143 | JSONArray descriptors = jsonObject.optJSONArray("Descriptors"); 144 | if (descriptors != null) { 145 | for (int i = 0; i < descriptors.length(); i++) { 146 | BluetoothGattDescriptor descriptor = 147 | jsonToBluetoothGattDescriptor(descriptors.getJSONObject(i)); 148 | characteristic.addDescriptor(descriptor); 149 | } 150 | } 151 | if (jsonObject.has("Data")) { 152 | dataHolder.insertData(characteristic, jsonObject.getString("Data")); 153 | } 154 | return characteristic; 155 | } 156 | 157 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 158 | public static BluetoothGattDescriptor jsonToBluetoothGattDescriptor( 159 | JSONObject jsonObject) throws JSONException { 160 | BluetoothGattDescriptor descriptor = 161 | new BluetoothGattDescriptor( 162 | UUID.fromString(jsonObject.getString("UUID")), 163 | // A descriptor can have multiple permissions (e.g. PERMISSION_READ and PERMISSION_WRITE). 164 | // Use the BitwiseOr to extract all the permissions from the json string. 165 | MbsEnums.BLE_PERMISSION_TYPE.getIntBitwiseOr(jsonObject.getString("Permissions"))); 166 | return descriptor; 167 | } 168 | 169 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 170 | public static ScanFilter jsonToScanFilter(JSONObject jsonObject) throws JSONException { 171 | ScanFilter.Builder builder = new ScanFilter.Builder(); 172 | if (jsonObject.has("ServiceUuid")) { 173 | builder.setServiceUuid(ParcelUuid.fromString(jsonObject.getString("ServiceUuid"))); 174 | } 175 | return builder.build(); 176 | } 177 | 178 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 179 | public static ScanSettings jsonToScanSettings(JSONObject jsonObject) throws JSONException { 180 | ScanSettings.Builder builder = new ScanSettings.Builder(); 181 | if (jsonObject.has("ScanMode")) { 182 | builder.setScanMode(MbsEnums.BLE_SCAN_MODE.getInt(jsonObject.getString("ScanMode"))); 183 | } 184 | return builder.build(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled.utils; 18 | 19 | import static java.nio.charset.StandardCharsets.UTF_8; 20 | 21 | import android.annotation.TargetApi; 22 | import android.bluetooth.BluetoothDevice; 23 | import android.bluetooth.BluetoothGatt; 24 | import android.bluetooth.BluetoothGattCharacteristic; 25 | import android.bluetooth.BluetoothGattService; 26 | import android.bluetooth.le.AdvertiseSettings; 27 | import android.bluetooth.le.ScanRecord; 28 | import android.content.Context; 29 | import android.content.pm.PackageManager; 30 | import android.net.DhcpInfo; 31 | import android.net.wifi.SupplicantState; 32 | import android.net.wifi.WifiConfiguration; 33 | import android.net.wifi.WifiInfo; 34 | import android.os.Build; 35 | import android.os.Bundle; 36 | import android.os.ParcelUuid; 37 | import android.util.Base64; 38 | import android.util.SparseArray; 39 | import androidx.test.platform.app.InstrumentationRegistry; 40 | import com.google.gson.Gson; 41 | import com.google.gson.GsonBuilder; 42 | import java.lang.reflect.Modifier; 43 | import java.net.InetAddress; 44 | import java.net.UnknownHostException; 45 | import java.util.ArrayList; 46 | import java.util.Collection; 47 | import org.json.JSONException; 48 | import org.json.JSONObject; 49 | 50 | /** 51 | * A collection of methods used to serialize data types defined in Android API into JSON strings. 52 | */ 53 | public class JsonSerializer { 54 | private static final String BLUETOOTH_PRIVILEGED_PERMISSION = 55 | "android.permission.BLUETOOTH_PRIVILEGED"; 56 | 57 | private static final Gson gson = 58 | new GsonBuilder() 59 | .serializeNulls() 60 | .excludeFieldsWithModifiers(Modifier.STATIC) 61 | .enableComplexMapKeySerialization() 62 | .disableInnerClassSerialization() 63 | .create(); 64 | 65 | /** 66 | * Remove the extra quotation marks from the beginning and the end of a string. 67 | * 68 | *

This is useful for strings like the SSID field of Android's Wi-Fi configuration. 69 | * 70 | * @param originalString 71 | */ 72 | public static String trimQuotationMarks(String originalString) { 73 | String result = originalString; 74 | if (originalString.length() > 2 75 | && originalString.charAt(0) == '"' 76 | && originalString.charAt(originalString.length() - 1) == '"') { 77 | result = originalString.substring(1, originalString.length() - 1); 78 | } 79 | return result; 80 | } 81 | 82 | public JSONObject toJson(Object object) throws JSONException { 83 | if (object instanceof DhcpInfo) { 84 | return serializeDhcpInfo((DhcpInfo) object); 85 | } else if (object instanceof WifiConfiguration) { 86 | return serializeWifiConfiguration((WifiConfiguration) object); 87 | } else if (object instanceof WifiInfo) { 88 | return serializeWifiInfo((WifiInfo) object); 89 | } 90 | return defaultSerialization(object); 91 | } 92 | 93 | /** 94 | * By default, we rely on Gson to do the right job. 95 | * 96 | * @param data An object to serialize 97 | * @return A JSONObject that has the info of the serialized data object. 98 | * @throws JSONException 99 | */ 100 | private JSONObject defaultSerialization(Object data) throws JSONException { 101 | return new JSONObject(gson.toJson(data)); 102 | } 103 | 104 | private JSONObject serializeDhcpInfo(DhcpInfo data) throws JSONException { 105 | JSONObject result = new JSONObject(gson.toJson(data)); 106 | int ipAddress = data.ipAddress; 107 | byte[] addressBytes = { 108 | (byte) (0xff & ipAddress), 109 | (byte) (0xff & (ipAddress >> 8)), 110 | (byte) (0xff & (ipAddress >> 16)), 111 | (byte) (0xff & (ipAddress >> 24)) 112 | }; 113 | try { 114 | String addressString = InetAddress.getByAddress(addressBytes).toString(); 115 | result.put("IpAddress", addressString); 116 | } catch (UnknownHostException e) { 117 | result.put("IpAddress", ipAddress); 118 | } 119 | return result; 120 | } 121 | 122 | private JSONObject serializeWifiConfiguration(WifiConfiguration data) throws JSONException { 123 | JSONObject result = new JSONObject(gson.toJson(data)); 124 | result.put("Status", WifiConfiguration.Status.strings[data.status]); 125 | result.put("SSID", trimQuotationMarks(data.SSID)); 126 | return result; 127 | } 128 | 129 | private JSONObject serializeWifiInfo(WifiInfo data) throws JSONException { 130 | JSONObject result = new JSONObject(gson.toJson(data)); 131 | result.put("SSID", trimQuotationMarks(data.getSSID())); 132 | for (SupplicantState state : SupplicantState.values()) { 133 | if (data.getSupplicantState().equals(state)) { 134 | result.put("SupplicantState", state.name()); 135 | } 136 | } 137 | return result; 138 | } 139 | 140 | public static Bundle serializeBluetoothDevice(BluetoothDevice data) { 141 | Context context = InstrumentationRegistry.getInstrumentation().getContext(); 142 | Bundle result = new Bundle(); 143 | result.putString("Address", data.getAddress()); 144 | if (Build.VERSION.SDK_INT >= 36 && 145 | context.checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED_PERMISSION) 146 | == PackageManager.PERMISSION_GRANTED) { 147 | result.putString("IdentityAddress", data.getIdentityAddressWithType().getAddress()); 148 | } else { 149 | result.putString("IdentityAddress", null); 150 | } 151 | final String bondState = 152 | MbsEnums.BLUETOOTH_DEVICE_BOND_STATE.getString(data.getBondState()); 153 | result.putString("BondState", bondState); 154 | result.putString("Name", data.getName()); 155 | 156 | String deviceType = MbsEnums.BLUETOOTH_DEVICE_TYPE.getString(data.getType()); 157 | result.putString("DeviceType", deviceType); 158 | ParcelUuid[] parcelUuids = data.getUuids(); 159 | if (parcelUuids != null) { 160 | ArrayList uuidStrings = new ArrayList<>(parcelUuids.length); 161 | for (ParcelUuid parcelUuid : parcelUuids) { 162 | uuidStrings.add(parcelUuid.getUuid().toString()); 163 | } 164 | result.putStringArrayList("UUIDs", uuidStrings); 165 | } 166 | return result; 167 | } 168 | 169 | public ArrayList serializeBluetoothDeviceList( 170 | Collection bluetoothDevices) { 171 | ArrayList results = new ArrayList<>(); 172 | for (BluetoothDevice device : bluetoothDevices) { 173 | results.add(serializeBluetoothDevice(device)); 174 | } 175 | return results; 176 | } 177 | 178 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 179 | public Bundle serializeBleScanResult(android.bluetooth.le.ScanResult scanResult) { 180 | Bundle result = new Bundle(); 181 | result.putBundle("Device", serializeBluetoothDevice(scanResult.getDevice())); 182 | result.putInt("Rssi", scanResult.getRssi()); 183 | result.putBundle("ScanRecord", serializeBleScanRecord(scanResult.getScanRecord())); 184 | result.putLong("TimestampNanos", scanResult.getTimestampNanos()); 185 | return result; 186 | } 187 | 188 | /** 189 | * Serialize ScanRecord for Bluetooth LE. 190 | * 191 | *

Not all fields are serialized here. Will add more as we need. 192 | * 193 | *

The returned {@link Bundle} has the following info:
194 |      *          "DeviceName", String
195 |      *          "TxPowerLevel", String
196 |      * 
197 | * 198 | * @param record A {@link ScanRecord} object. 199 | * @return A {@link Bundle} object. 200 | */ 201 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 202 | private Bundle serializeBleScanRecord(ScanRecord record) { 203 | Bundle result = new Bundle(); 204 | result.putString("DeviceName", record.getDeviceName()); 205 | result.putInt("TxPowerLevel", record.getTxPowerLevel()); 206 | result.putParcelableArrayList("Services", serializeBleScanServices(record)); 207 | result.putBundle( 208 | "manufacturerSpecificData", serializeBleScanManufacturerSpecificData(record)); 209 | return result; 210 | } 211 | 212 | /** Serialize manufacturer specific data from ScanRecord for Bluetooth LE. */ 213 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 214 | private ArrayList serializeBleScanServices(ScanRecord record) { 215 | ArrayList result = new ArrayList<>(); 216 | if (record.getServiceUuids() != null) { 217 | for (ParcelUuid uuid : record.getServiceUuids()) { 218 | Bundle service = new Bundle(); 219 | service.putString("UUID", uuid.getUuid().toString()); 220 | if (record.getServiceData(uuid) != null) { 221 | service.putString( 222 | "Data", 223 | new String(Base64.encode(record.getServiceData(uuid), Base64.NO_WRAP), 224 | UTF_8)); 225 | } else { 226 | service.putString("Data", ""); 227 | } 228 | result.add(service); 229 | } 230 | } 231 | return result; 232 | } 233 | 234 | /** Serialize manufacturer specific data from ScanRecord for Bluetooth LE. */ 235 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 236 | private Bundle serializeBleScanManufacturerSpecificData(ScanRecord record) { 237 | Bundle result = new Bundle(); 238 | SparseArray sparseArray = record.getManufacturerSpecificData(); 239 | for (int i = 0; i < sparseArray.size(); i++) { 240 | int key = sparseArray.keyAt(i); 241 | result.putByteArray(String.valueOf(key), sparseArray.get(key)); 242 | } 243 | return result; 244 | } 245 | 246 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 247 | public static Bundle serializeBleAdvertisingSettings(AdvertiseSettings advertiseSettings) { 248 | Bundle result = new Bundle(); 249 | result.putString( 250 | "TxPowerLevel", 251 | MbsEnums.BLE_ADVERTISE_TX_POWER.getString(advertiseSettings.getTxPowerLevel())); 252 | result.putString( 253 | "Mode", MbsEnums.BLE_ADVERTISE_MODE.getString(advertiseSettings.getMode())); 254 | result.putInt("Timeout", advertiseSettings.getTimeout()); 255 | result.putBoolean("IsConnectable", advertiseSettings.isConnectable()); 256 | return result; 257 | } 258 | 259 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 260 | public static Bundle serializeBluetoothGatt(BluetoothGatt gatt) { 261 | Bundle result = new Bundle(); 262 | ArrayList services = new ArrayList<>(); 263 | for (BluetoothGattService service : gatt.getServices()) { 264 | services.add(JsonSerializer.serializeBluetoothGattService(service)); 265 | } 266 | result.putParcelableArrayList("Services", services); 267 | result.putBundle("Device", JsonSerializer.serializeBluetoothDevice(gatt.getDevice())); 268 | return result; 269 | } 270 | 271 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 272 | public static Bundle serializeBluetoothGattService(BluetoothGattService service) { 273 | Bundle result = new Bundle(); 274 | result.putString("UUID", service.getUuid().toString()); 275 | result.putString("Type", MbsEnums.BLE_SERVICE_TYPE.getString(service.getType())); 276 | ArrayList characteristics = new ArrayList<>(); 277 | for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { 278 | characteristics.add(serializeBluetoothGattCharacteristic(characteristic)); 279 | } 280 | result.putParcelableArrayList("Characteristics", characteristics); 281 | return result; 282 | } 283 | 284 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 285 | public static Bundle serializeBluetoothGattCharacteristic( 286 | BluetoothGattCharacteristic characteristic) { 287 | Bundle result = new Bundle(); 288 | result.putString("UUID", characteristic.getUuid().toString()); 289 | result.putString( 290 | "Property", MbsEnums.BLE_PROPERTY_TYPE.getString(characteristic.getProperties())); 291 | result.putString( 292 | "Permission", 293 | MbsEnums.BLE_PERMISSION_TYPE.getString(characteristic.getPermissions())); 294 | return result; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled.utils; 18 | 19 | import android.bluetooth.BluetoothDevice; 20 | import android.bluetooth.BluetoothGatt; 21 | import android.bluetooth.BluetoothGattCharacteristic; 22 | import android.bluetooth.BluetoothGattService; 23 | import android.bluetooth.BluetoothProfile; 24 | import android.bluetooth.le.AdvertiseCallback; 25 | import android.bluetooth.le.AdvertiseSettings; 26 | import android.bluetooth.le.ScanCallback; 27 | import android.bluetooth.le.ScanSettings; 28 | import android.net.wifi.WifiManager.LocalOnlyHotspotCallback; 29 | import android.os.Build; 30 | 31 | /** Mobly Bundled Snippets (MBS)'s {@link RpcEnum} objects representing enums in Android APIs. */ 32 | public class MbsEnums { 33 | static final RpcEnum BLE_ADVERTISE_MODE = buildBleAdvertiseModeEnum(); 34 | static final RpcEnum BLE_ADVERTISE_TX_POWER = buildBleAdvertiseTxPowerEnum(); 35 | public static final RpcEnum BLE_SCAN_FAILED_ERROR_CODE = buildBleScanFailedErrorCodeEnum(); 36 | public static final RpcEnum BLE_SCAN_RESULT_CALLBACK_TYPE = 37 | buildBleScanResultCallbackTypeEnum(); 38 | static final RpcEnum BLUETOOTH_DEVICE_BOND_STATE = buildBluetoothDeviceBondState(); 39 | static final RpcEnum BLUETOOTH_DEVICE_TYPE = buildBluetoothDeviceTypeEnum(); 40 | static final RpcEnum BLE_SERVICE_TYPE = buildServiceTypeEnum(); 41 | public static final RpcEnum BLE_STATUS_TYPE = buildStatusTypeEnum(); 42 | public static final RpcEnum BLE_CONNECT_STATUS = buildConnectStatusEnum(); 43 | public static final RpcEnum BLE_PROPERTY_TYPE = buildPropertyTypeEnum(); 44 | static final RpcEnum BLE_PERMISSION_TYPE = buildPermissionTypeEnum(); 45 | static final RpcEnum BLE_SCAN_MODE = buildBleScanModeEnum(); 46 | public static final RpcEnum LOCAL_HOTSPOT_FAIL_REASON = buildLocalHotspotFailedReason(); 47 | public static final RpcEnum ADVERTISE_FAILURE_ERROR_CODE = 48 | new RpcEnum.Builder().add("ADVERTISE_FAILED_ALREADY_STARTED", 49 | AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED) 50 | .add("ADVERTISE_FAILED_DATA_TOO_LARGE", 51 | AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE) 52 | .add( 53 | "ADVERTISE_FAILED_FEATURE_UNSUPPORTED", 54 | AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED) 55 | .add("ADVERTISE_FAILED_INTERNAL_ERROR", 56 | AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR) 57 | .add( 58 | "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS", 59 | AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS) 60 | .build(); 61 | 62 | private static RpcEnum buildBluetoothDeviceBondState() { 63 | RpcEnum.Builder builder = new RpcEnum.Builder(); 64 | return builder.add("BOND_NONE", BluetoothDevice.BOND_NONE) 65 | .add("BOND_BONDING", BluetoothDevice.BOND_BONDING) 66 | .add("BOND_BONDED", BluetoothDevice.BOND_BONDED) 67 | .build(); 68 | } 69 | 70 | private static RpcEnum buildBluetoothDeviceTypeEnum() { 71 | RpcEnum.Builder builder = new RpcEnum.Builder(); 72 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { 73 | return builder.build(); 74 | } 75 | return builder.add("DEVICE_TYPE_CLASSIC", BluetoothDevice.DEVICE_TYPE_CLASSIC) 76 | .add("DEVICE_TYPE_LE", BluetoothDevice.DEVICE_TYPE_LE) 77 | .add("DEVICE_TYPE_DUAL", BluetoothDevice.DEVICE_TYPE_DUAL) 78 | .add("DEVICE_TYPE_UNKNOWN", BluetoothDevice.DEVICE_TYPE_UNKNOWN) 79 | .build(); 80 | } 81 | 82 | private static RpcEnum buildBleAdvertiseTxPowerEnum() { 83 | RpcEnum.Builder builder = new RpcEnum.Builder(); 84 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 85 | return builder.build(); 86 | } 87 | return builder.add( 88 | "ADVERTISE_TX_POWER_ULTRA_LOW", 89 | AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW) 90 | .add("ADVERTISE_TX_POWER_LOW", AdvertiseSettings.ADVERTISE_TX_POWER_LOW) 91 | .add("ADVERTISE_TX_POWER_MEDIUM", AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) 92 | .add("ADVERTISE_TX_POWER_HIGH", AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) 93 | .build(); 94 | } 95 | 96 | private static RpcEnum buildBleAdvertiseModeEnum() { 97 | RpcEnum.Builder builder = new RpcEnum.Builder(); 98 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 99 | return builder.build(); 100 | } 101 | return builder.add("ADVERTISE_MODE_BALANCED", AdvertiseSettings.ADVERTISE_MODE_BALANCED) 102 | .add("ADVERTISE_MODE_LOW_LATENCY", AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) 103 | .add("ADVERTISE_MODE_LOW_POWER", AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) 104 | .build(); 105 | } 106 | 107 | private static RpcEnum buildBleScanFailedErrorCodeEnum() { 108 | RpcEnum.Builder builder = new RpcEnum.Builder(); 109 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 110 | return builder.build(); 111 | } 112 | return builder.add("SCAN_FAILED_ALREADY_STARTED", ScanCallback.SCAN_FAILED_ALREADY_STARTED) 113 | .add( 114 | "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED", 115 | ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED) 116 | .add( 117 | "SCAN_FAILED_FEATURE_UNSUPPORTED", 118 | ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED) 119 | .add("SCAN_FAILED_INTERNAL_ERROR", ScanCallback.SCAN_FAILED_INTERNAL_ERROR) 120 | .build(); 121 | } 122 | 123 | private static RpcEnum buildBleScanResultCallbackTypeEnum() { 124 | RpcEnum.Builder builder = new RpcEnum.Builder(); 125 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 126 | return builder.build(); 127 | } 128 | builder.add("CALLBACK_TYPE_ALL_MATCHES", ScanSettings.CALLBACK_TYPE_ALL_MATCHES); 129 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 130 | builder.add("CALLBACK_TYPE_FIRST_MATCH", ScanSettings.CALLBACK_TYPE_FIRST_MATCH); 131 | builder.add("CALLBACK_TYPE_MATCH_LOST", ScanSettings.CALLBACK_TYPE_MATCH_LOST); 132 | } 133 | return builder.build(); 134 | } 135 | 136 | private static RpcEnum buildServiceTypeEnum() { 137 | RpcEnum.Builder builder = new RpcEnum.Builder(); 138 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 139 | return builder.build(); 140 | } 141 | builder.add("SERVICE_TYPE_PRIMARY", BluetoothGattService.SERVICE_TYPE_PRIMARY); 142 | builder.add("SERVICE_TYPE_SECONDARY", BluetoothGattService.SERVICE_TYPE_SECONDARY); 143 | return builder.build(); 144 | } 145 | 146 | private static RpcEnum buildStatusTypeEnum() { 147 | RpcEnum.Builder builder = new RpcEnum.Builder(); 148 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 149 | return builder.build(); 150 | } 151 | builder.add("GATT_SUCCESS", BluetoothGatt.GATT_SUCCESS) 152 | .add("GATT_CONNECTION_CONGESTED", BluetoothGatt.GATT_CONNECTION_CONGESTED) 153 | .add("GATT_FAILURE", BluetoothGatt.GATT_FAILURE) 154 | .add("GATT_INSUFFICIENT_AUTHENTICATION", 155 | BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION) 156 | .add("GATT_INSUFFICIENT_ENCRYPTION", BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION) 157 | .add("GATT_INVALID_ATTRIBUTE_LENGTH", BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH) 158 | .add("GATT_INVALID_OFFSET", BluetoothGatt.GATT_INVALID_OFFSET) 159 | .add("GATT_READ_NOT_PERMITTED", BluetoothGatt.GATT_READ_NOT_PERMITTED) 160 | .add("GATT_REQUEST_NOT_SUPPORTED", BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED) 161 | .add("GATT_WRITE_NOT_PERMITTED", BluetoothGatt.GATT_WRITE_NOT_PERMITTED) 162 | .add("BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION", 0x13) 163 | .add("BLE_HCI_LOCAL_HOST_TERMINATED_CONNECTION", 0x12) 164 | .add("BLE_HCI_STATUS_CODE_LMP_RESPONSE_TIMEOUT", 0x22) 165 | .add("BLE_HCI_CONN_FAILED_TO_BE_ESTABLISHED", 0x3e) 166 | .add("UNEXPECTED_DISCONNECT_NO_ERROR_CODE", 134) 167 | .add("DID_NOT_FIND_OFFLINEP2P_SERVICE", 135) 168 | .add("MISSING_CHARACTERISTIC", 137) 169 | .add("CONNECTION_TIMEOUT", 138) 170 | .add("READ_MALFORMED_VERSION", 139) 171 | .add("READ_WRITE_VERSION_NONSPECIFIC_ERROR", 140) 172 | .add("GATT_0C_err", 0X0C) 173 | .add("GATT_16", 0x16) 174 | .add("GATT_INTERNAL_ERROR", 129) 175 | .add("BLE_HCI_CONNECTION_TIMEOUT", 0x08) 176 | .add("GATT_ERROR", 133); 177 | return builder.build(); 178 | } 179 | 180 | private static RpcEnum buildConnectStatusEnum() { 181 | RpcEnum.Builder builder = new RpcEnum.Builder(); 182 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 183 | return builder.build(); 184 | } 185 | builder.add("STATE_CONNECTED", BluetoothProfile.STATE_CONNECTED) 186 | .add("STATE_CONNECTING", BluetoothProfile.STATE_CONNECTING) 187 | .add("STATE_DISCONNECTED", BluetoothProfile.STATE_DISCONNECTED) 188 | .add("STATE_DISCONNECTING", BluetoothProfile.STATE_DISCONNECTING); 189 | return builder.build(); 190 | } 191 | 192 | private static RpcEnum buildPropertyTypeEnum() { 193 | RpcEnum.Builder builder = new RpcEnum.Builder(); 194 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 195 | return builder.build(); 196 | } 197 | builder 198 | .add("PROPERTY_NONE", 0) 199 | .add("PROPERTY_BROADCAST", BluetoothGattCharacteristic.PROPERTY_BROADCAST) 200 | .add("PROPERTY_EXTENDED_PROPS", BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS) 201 | .add("PROPERTY_INDICATE", BluetoothGattCharacteristic.PROPERTY_INDICATE) 202 | .add("PROPERTY_NOTIFY", BluetoothGattCharacteristic.PROPERTY_NOTIFY) 203 | .add("PROPERTY_READ", BluetoothGattCharacteristic.PROPERTY_READ) 204 | .add("PROPERTY_SIGNED_WRITE", BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE) 205 | .add("PROPERTY_WRITE", BluetoothGattCharacteristic.PROPERTY_WRITE) 206 | .add("PROPERTY_WRITE_NO_RESPONSE", 207 | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE); 208 | return builder.build(); 209 | } 210 | 211 | private static RpcEnum buildPermissionTypeEnum() { 212 | RpcEnum.Builder builder = new RpcEnum.Builder(); 213 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 214 | return builder.build(); 215 | } 216 | builder.add("PERMISSION_NONE", 0) 217 | .add("PERMISSION_READ", BluetoothGattCharacteristic.PERMISSION_READ) 218 | .add("PERMISSION_READ_ENCRYPTED", 219 | BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED) 220 | .add("PERMISSION_READ_ENCRYPTED_MITM", 221 | BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED_MITM) 222 | .add("PERMISSION_WRITE", BluetoothGattCharacteristic.PERMISSION_WRITE) 223 | .add("PERMISSION_WRITE_ENCRYPTED", 224 | BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED) 225 | .add("PERMISSION_WRITE_ENCRYPTED_MITM", 226 | BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED_MITM) 227 | .add("PERMISSION_WRITE_SIGNED", BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED) 228 | .add("PERMISSION_WRITE_SIGNED_MITM", 229 | BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED_MITM); 230 | return builder.build(); 231 | } 232 | 233 | private static RpcEnum buildBleScanModeEnum() { 234 | RpcEnum.Builder builder = new RpcEnum.Builder(); 235 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 236 | return builder.build(); 237 | } 238 | builder.add("SCAN_MODE_LOW_POWER", ScanSettings.SCAN_MODE_LOW_POWER) 239 | .add("SCAN_MODE_BALANCED", ScanSettings.SCAN_MODE_BALANCED) 240 | .add("SCAN_MODE_LOW_LATENCY", ScanSettings.SCAN_MODE_LOW_LATENCY); 241 | return builder.build(); 242 | } 243 | 244 | private static RpcEnum buildLocalHotspotFailedReason() { 245 | RpcEnum.Builder builder = new RpcEnum.Builder(); 246 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 247 | return builder.build(); 248 | } 249 | builder.add("ERROR_TETHERING_DISALLOWED", 250 | LocalOnlyHotspotCallback.ERROR_TETHERING_DISALLOWED) 251 | .add("ERROR_INCOMPATIBLE_MODE", LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE) 252 | .add("ERROR_NO_CHANNEL", LocalOnlyHotspotCallback.ERROR_NO_CHANNEL) 253 | .add("ERROR_GENERIC", LocalOnlyHotspotCallback.ERROR_GENERIC); 254 | return builder.build(); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled.utils; 18 | 19 | import com.google.common.base.Splitter; 20 | import com.google.common.collect.ImmutableBiMap; 21 | import com.google.errorprone.annotations.CanIgnoreReturnValue; 22 | 23 | /** 24 | * A container type for handling String-Integer enum conversion in Rpc protocol. 25 | * 26 | *

In Serializing/Deserializing Android API enums, we often need to convert an enum value from 27 | * one form to another. This container class makes it easier to do so. 28 | * 29 | *

Once built, an RpcEnum object is immutable. 30 | */ 31 | public class RpcEnum { 32 | private final ImmutableBiMap enums; 33 | 34 | private RpcEnum(ImmutableBiMap.Builder builder) { 35 | enums = builder.buildOrThrow(); 36 | } 37 | 38 | /** 39 | * Get the int value of an enum based on its String value. 40 | * 41 | * @param enumString 42 | * @return int value 43 | */ 44 | public int getInt(String enumString) { 45 | Integer result = enums.get(enumString); 46 | if (result == null) { 47 | throw new NoSuchFieldError("No int value found for: " + enumString); 48 | } 49 | return result; 50 | } 51 | 52 | /** 53 | * Get the int value of an enum based on its String value. String value contains multiple enums 54 | * separated by '|'. The int value is the bitwise OR of all the enums. 55 | * 56 | * @param enumString 57 | * @return int value 58 | */ 59 | public int getIntBitwiseOr(String enumString) { 60 | Integer result = 0; 61 | Iterable enumList = Splitter.on('|').split(enumString); 62 | for (String enumEntry : enumList) { 63 | result |= getInt(enumEntry); 64 | } 65 | return result; 66 | } 67 | 68 | /** 69 | * Get the String value of an enum based on its int value. 70 | * 71 | * @param enumInt 72 | * @return string value 73 | */ 74 | public String getString(int enumInt) { 75 | String result = enums.inverse().get(enumInt); 76 | if (result == null) { 77 | return String.format("UNKNOWN_VALUE[%s].", enumInt); 78 | } 79 | return result; 80 | } 81 | 82 | /** Builder for RpcEnum. */ 83 | public static class Builder { 84 | private final ImmutableBiMap.Builder builder; 85 | 86 | public Builder() { 87 | builder = new ImmutableBiMap.Builder<>(); 88 | } 89 | 90 | /** 91 | * Add an enum String-Integer pair. 92 | * 93 | * @param enumString 94 | * @param enumInt 95 | * @return 96 | */ 97 | @CanIgnoreReturnValue 98 | public Builder add(String enumString, int enumInt) { 99 | builder.put(enumString, enumInt); 100 | return this; 101 | } 102 | 103 | public RpcEnum build() { 104 | return new RpcEnum(builder); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.mobly.snippet.bundled.utils; 18 | 19 | import android.app.UiAutomation; 20 | import android.os.Build; 21 | import android.content.Context; 22 | import androidx.test.platform.app.InstrumentationRegistry; 23 | import com.google.android.mobly.snippet.bundled.SmsSnippet; 24 | import com.google.android.mobly.snippet.event.EventCache; 25 | import com.google.android.mobly.snippet.event.SnippetEvent; 26 | import com.google.android.mobly.snippet.util.Log; 27 | import com.google.common.primitives.Primitives; 28 | import com.google.common.reflect.TypeToken; 29 | import java.lang.reflect.InvocationTargetException; 30 | import java.lang.reflect.Method; 31 | import java.util.Locale; 32 | import java.util.concurrent.LinkedBlockingDeque; 33 | import java.util.concurrent.TimeUnit; 34 | import java.util.concurrent.TimeoutException; 35 | 36 | public final class Utils { 37 | 38 | private static final char[] hexArray = "0123456789abcdef".toCharArray(); 39 | 40 | private Utils() {} 41 | 42 | /** 43 | * Waits util a condition is met. 44 | * 45 | *

This is often used to wait for asynchronous operations to finish and the system to reach a 46 | * desired state. 47 | * 48 | *

If the predicate function throws an exception and interrupts the waiting, the exception 49 | * will be wrapped in an {@link RuntimeException}. 50 | * 51 | * @param predicate A lambda function that specifies the condition to wait for. This function 52 | * should return true when the desired state has been reached. 53 | * @param timeout The number of seconds to wait for before giving up. 54 | * @return true if the operation finished before timeout, false otherwise. 55 | */ 56 | public static boolean waitUntil(Utils.Predicate predicate, int timeout) { 57 | timeout *= 10; 58 | try { 59 | while (!predicate.waitCondition() && timeout >= 0) { 60 | Thread.sleep(100); 61 | timeout -= 1; 62 | } 63 | if (predicate.waitCondition()) { 64 | return true; 65 | } 66 | } catch (Throwable e) { 67 | throw new RuntimeException(e); 68 | } 69 | return false; 70 | } 71 | 72 | /** 73 | * Wait on a specific snippet event. 74 | * 75 | *

This allows a snippet to wait on another SnippetEvent as long as they know the name and 76 | * callback id. Commonly used to make async calls synchronous, see {@link 77 | * SmsSnippet#waitForSms()} waitForSms} for example usage. 78 | * 79 | * @param callbackId String callbackId that we want to wait on. 80 | * @param eventName String event name that we are waiting on. 81 | * @param timeout int timeout in milliseconds for how long it will wait for the event. 82 | * @return SnippetEvent if one was received. 83 | * @throws Throwable if interrupted while polling for event completion. Throws TimeoutException 84 | * if no snippet event is received. 85 | */ 86 | public static SnippetEvent waitForSnippetEvent( 87 | String callbackId, String eventName, Integer timeout) throws Throwable { 88 | String qId = EventCache.getQueueId(callbackId, eventName); 89 | LinkedBlockingDeque q = EventCache.getInstance().getEventDeque(qId); 90 | SnippetEvent result; 91 | try { 92 | result = q.pollFirst(timeout, TimeUnit.MILLISECONDS); 93 | } catch (InterruptedException e) { 94 | throw e.getCause(); 95 | } 96 | 97 | if (result == null) { 98 | throw new TimeoutException( 99 | String.format( 100 | Locale.ROOT, 101 | "Timed out waiting(%d millis) for SnippetEvent: %s", 102 | timeout, 103 | callbackId)); 104 | } 105 | return result; 106 | } 107 | 108 | /** 109 | * A function interface that is used by lambda functions signaling an async operation is still 110 | * going on. 111 | */ 112 | public interface Predicate { 113 | boolean waitCondition() throws Throwable; 114 | } 115 | 116 | /** 117 | * Simplified API to invoke an instance method by reflection. 118 | * 119 | *

Sample usage: 120 | * 121 | *

122 |      *   boolean result = (boolean) Utils.invokeByReflection(
123 |      *           mWifiManager,
124 |      *           "setWifiApEnabled", null /* wifiConfiguration * /, true /* enabled * /);
125 |      * 
126 | * 127 | * @param instance Instance of object defining the method to call. 128 | * @param methodName Name of the method to call. Can be inherited. 129 | * @param args Variadic array of arguments to supply to the method. Their types will be used to 130 | * locate a suitable method to call. Subtypes, primitive types, boxed types, and {@code 131 | * null} arguments are properly handled. 132 | * @return The return value of the method, or {@code null} if no return value. 133 | * @throws NoSuchMethodException If no suitable method could be found. 134 | * @throws Throwable The exception raised by the method, if any. 135 | */ 136 | public static Object invokeByReflection(Object instance, String methodName, Object... args) 137 | throws Throwable { 138 | // Java doesn't know if invokeByReflection(instance, name, null) means that the array is 139 | // null or that it's a non-null array containing a single null element. We mean the latter. 140 | // Silly Java. 141 | if (args == null) { 142 | args = new Object[] {null}; 143 | } 144 | // Can't use Class#getMethod(Class...) because it expects that the passed in classes 145 | // exactly match the parameters of the method, and doesn't handle superclasses. 146 | Method method = null; 147 | METHOD_SEARCHER: 148 | for (Method candidateMethod : instance.getClass().getMethods()) { 149 | // getMethods() returns only public methods, so we don't need to worry about checking 150 | // whether the method is accessible. 151 | if (!candidateMethod.getName().equals(methodName)) { 152 | continue; 153 | } 154 | Class[] declaredParams = candidateMethod.getParameterTypes(); 155 | if (declaredParams.length != args.length) { 156 | continue; 157 | } 158 | for (int i = 0; i < declaredParams.length; i++) { 159 | if (args[i] == null) { 160 | // Null is assignable to anything except primitives. 161 | if (declaredParams[i].isPrimitive()) { 162 | continue METHOD_SEARCHER; 163 | } 164 | } else { 165 | // Allow autoboxing during reflection by wrapping primitives. 166 | Class declaredClass = Primitives.wrap(declaredParams[i]); 167 | Class actualClass = Primitives.wrap(args[i].getClass()); 168 | TypeToken declaredParamType = TypeToken.of(declaredClass); 169 | TypeToken actualParamType = TypeToken.of(actualClass); 170 | if (!declaredParamType.isSupertypeOf(actualParamType)) { 171 | continue METHOD_SEARCHER; 172 | } 173 | } 174 | } 175 | method = candidateMethod; 176 | break; 177 | } 178 | if (method == null) { 179 | StringBuilder methodString = 180 | new StringBuilder(instance.getClass().getName()) 181 | .append('#') 182 | .append(methodName) 183 | .append('('); 184 | for (int i = 0; i < args.length - 1; i++) { 185 | methodString.append(args[i].getClass().getSimpleName()).append(", "); 186 | } 187 | if (args.length > 0) { 188 | methodString.append(args[args.length - 1].getClass().getSimpleName()); 189 | } 190 | methodString.append(')'); 191 | throw new NoSuchMethodException(methodString.toString()); 192 | } 193 | try { 194 | Object result = method.invoke(instance, args); 195 | return result; 196 | } catch (InvocationTargetException e) { 197 | throw e.getCause(); 198 | } 199 | } 200 | 201 | /** 202 | * Convert a byte array (binary data) to a hexadecimal string (ASCII) representation. 203 | * 204 | *

[\x01\x02] -> "0102" 205 | * 206 | * @param bytes The array of byte to convert. 207 | * @return a String with the ASCII hex representation. 208 | */ 209 | public static String bytesToHexString(byte[] bytes) { 210 | char[] hexChars = new char[bytes.length * 2]; 211 | for (int j = 0; j < bytes.length; j++) { 212 | int v = bytes[j] & 0xFF; 213 | hexChars[j * 2] = hexArray[v >>> 4]; 214 | hexChars[j * 2 + 1] = hexArray[v & 0x0F]; 215 | } 216 | return new String(hexChars); 217 | } 218 | 219 | public static void adaptShellPermissionIfRequired(Context context) throws Throwable { 220 | if (Build.VERSION.SDK_INT >= 29) { 221 | Log.d("Elevating permission require to enable support for privileged operation in Android Q+"); 222 | UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 223 | uia.adoptShellPermissionIdentity(); 224 | try { 225 | Class cls = Class.forName("android.app.UiAutomation"); 226 | Method destroyMethod = cls.getDeclaredMethod("destroy"); 227 | destroyMethod.invoke(uia); 228 | } catch (NoSuchMethodException 229 | | IllegalAccessException 230 | | ClassNotFoundException 231 | | InvocationTargetException e) { 232 | throw new RuntimeException("Failed to cleaup Ui Automation", e); 233 | } 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/test/java/JsonDeserializerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import android.bluetooth.BluetoothGattCharacteristic; 18 | import android.bluetooth.BluetoothGattDescriptor; 19 | 20 | import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer; 21 | import com.google.common.truth.Truth; 22 | import java.util.UUID; 23 | import org.json.JSONObject; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | import org.robolectric.annotation.Config; 27 | import org.robolectric.RobolectricTestRunner; 28 | 29 | @RunWith(RobolectricTestRunner.class) 30 | @Config(minSdk = 33) 31 | public class JsonDeserializerTest { 32 | @Test 33 | public void testCharacteristicWithPropertiesPermissions() throws Throwable { 34 | String uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff"; 35 | 36 | JSONObject json = new JSONObject(); 37 | json.put("UUID", uuid); 38 | json.put("Properties", "PROPERTY_READ"); 39 | json.put("Permissions", "PERMISSION_READ"); 40 | 41 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, json); 42 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString(uuid)); 43 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ); 44 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ); 45 | } 46 | 47 | @Test 48 | public void testCharacteristicWithMultiplePropertiesPermissions() throws Throwable { 49 | String uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff"; 50 | 51 | JSONObject json = new JSONObject(); 52 | json.put("UUID", uuid); 53 | json.put("Properties", "PROPERTY_READ|PROPERTY_WRITE"); 54 | json.put("Permissions", "PERMISSION_READ|PERMISSION_WRITE"); 55 | 56 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, json); 57 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString(uuid)); 58 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE); 59 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ |BluetoothGattCharacteristic.PERMISSION_WRITE); 60 | } 61 | 62 | @Test 63 | public void testDescriptor() throws Throwable { 64 | String jsonString = 65 | "{" + 66 | " \"UUID\": \"ffffffff-ffff-ffff-ffff-ffffffffffff\"," + 67 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"" + 68 | "}"; 69 | 70 | BluetoothGattDescriptor descriptor = JsonDeserializer.jsonToBluetoothGattDescriptor(new JSONObject(jsonString)); 71 | Truth.assertThat(descriptor.getUuid()).isEqualTo(UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff")); 72 | Truth.assertThat(descriptor.getPermissions()).isEqualTo(BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE); 73 | } 74 | 75 | @Test 76 | public void testCharacteristicNoDescriptors() throws Throwable { 77 | String jsonString = 78 | "{" + 79 | " \"UUID\": \"ffffffff-ffff-ffff-ffff-ffffffffffff\"," + 80 | " \"Properties\":\"PROPERTY_READ|PROPERTY_WRITE\"," + 81 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"" + 82 | "}"; 83 | 84 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, new JSONObject(jsonString)); 85 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff")); 86 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE); 87 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE); 88 | Truth.assertThat(characteristic.getDescriptors()).isEmpty(); 89 | } 90 | 91 | @Test 92 | public void testCharacteristicEmptyListDescriptors() throws Throwable { 93 | String jsonString = 94 | "{" + 95 | " \"UUID\": \"ffffffff-ffff-ffff-ffff-ffffffffffff\"," + 96 | " \"Properties\":\"PROPERTY_READ|PROPERTY_WRITE\"," + 97 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"," + 98 | " \"Descriptors\": []" + 99 | "}"; 100 | 101 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, new JSONObject(jsonString)); 102 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff")); 103 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE); 104 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE); 105 | Truth.assertThat(characteristic.getDescriptors()).isEmpty(); 106 | } 107 | 108 | @Test 109 | public void testCharacteristic1Descriptor() throws Throwable { 110 | String jsonString = 111 | "{" + 112 | " \"UUID\": \"ffffffff-ffff-ffff-ffff-ffffffffffff\"," + 113 | " \"Properties\":\"PROPERTY_READ|PROPERTY_WRITE\"," + 114 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"," + 115 | " \"Descriptors\":" + 116 | " [" + 117 | " {" + 118 | " \"UUID\": \"dddddddd-dddd-dddd-dddd-dddddddddddd\"," + 119 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"" + 120 | " }" + 121 | " ]" + 122 | "}"; 123 | 124 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, new JSONObject(jsonString)); 125 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff")); 126 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE); 127 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE); 128 | Truth.assertThat(characteristic.getDescriptors().size()).isEqualTo(1); 129 | Truth.assertThat(characteristic.getDescriptors().get(0).getUuid()).isEqualTo(UUID.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd")); 130 | Truth.assertThat(characteristic.getDescriptors().get(0).getPermissions()).isEqualTo(BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE); 131 | } 132 | @Test 133 | public void testCharacteristic2Descriptors() throws Throwable { 134 | String jsonString = 135 | "{" + 136 | " \"UUID\": \"ffffffff-ffff-ffff-ffff-ffffffffffff\"," + 137 | " \"Properties\":\"PROPERTY_READ|PROPERTY_WRITE\"," + 138 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"," + 139 | " \"Descriptors\":" + 140 | " [" + 141 | " {" + 142 | " \"UUID\": \"dddddddd-dddd-dddd-dddd-dddddddddddd\"," + 143 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"" + 144 | " }," + 145 | " {" + 146 | " \"UUID\": \"eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee\"," + 147 | " \"Permissions\": \"PERMISSION_READ\"" + 148 | " }" + 149 | " ]" + 150 | "}"; 151 | 152 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, new JSONObject(jsonString)); 153 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff")); 154 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE); 155 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE); 156 | Truth.assertThat(characteristic.getDescriptors().size()).isEqualTo(2); 157 | Truth.assertThat(characteristic.getDescriptors().get(0).getUuid()).isEqualTo(UUID.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd")); 158 | Truth.assertThat(characteristic.getDescriptors().get(0).getPermissions()).isEqualTo(BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE); 159 | Truth.assertThat(characteristic.getDescriptors().get(1).getUuid()).isEqualTo(UUID.fromString("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee")); 160 | Truth.assertThat(characteristic.getDescriptors().get(1).getPermissions()).isEqualTo(BluetoothGattDescriptor.PERMISSION_READ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/test/java/MbsEnumsTest.java: -------------------------------------------------------------------------------- 1 | import android.bluetooth.BluetoothGattCharacteristic; 2 | import android.os.Build.VERSION_CODES; 3 | import com.google.android.mobly.snippet.bundled.utils.MbsEnums; 4 | import com.google.common.truth.Truth; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import androidx.test.runner.AndroidJUnitRunner; 8 | import org.junit.runners.JUnit4; 9 | import org.robolectric.annotation.Config; 10 | import org.robolectric.RobolectricTestRunner; 11 | 12 | @RunWith(RobolectricTestRunner.class) 13 | @Config(minSdk = 33) 14 | public class MbsEnumsTest { 15 | @Test 16 | public void testGetIntBitwiseOrValid() throws Throwable { 17 | Truth.assertThat(MbsEnums.BLE_PROPERTY_TYPE.getIntBitwiseOr("PROPERTY_READ|PROPERTY_NOTIFY")).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY); 18 | Truth.assertThat(MbsEnums.BLE_PROPERTY_TYPE.getIntBitwiseOr("PROPERTY_READ")).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ); 19 | } 20 | 21 | @Test 22 | public void testGetIntBitwiseOrInvalid() throws Throwable { 23 | Throwable thrown = null; 24 | try { 25 | MbsEnums.BLE_PROPERTY_TYPE.getIntBitwiseOr("PROPERTY_NOTHING"); 26 | } catch (Throwable t) { 27 | thrown = t; 28 | } 29 | Truth.assertThat(thrown).isInstanceOf(NoSuchFieldError.class); 30 | } 31 | 32 | @Test 33 | public void testGetIntBitwiseOrInvalid2() throws Throwable { 34 | Throwable thrown = null; 35 | try { 36 | MbsEnums.BLE_PROPERTY_TYPE.getIntBitwiseOr("PROPERTY_READ|PROPERTY_NOTHING"); 37 | } catch (Throwable t) { 38 | thrown = t; 39 | } 40 | Truth.assertThat(thrown).isInstanceOf(NoSuchFieldError.class); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/UtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | import static com.google.android.mobly.snippet.bundled.utils.Utils.invokeByReflection; 18 | 19 | import com.google.android.mobly.snippet.bundled.utils.Utils; 20 | import com.google.common.truth.Truth; 21 | import java.io.IOException; 22 | import java.util.Collections; 23 | import java.util.List; 24 | import org.junit.Assert; 25 | import org.junit.Test; 26 | 27 | /** Tests for {@link com.google.android.mobly.snippet.bundled.utils.Utils} */ 28 | public class UtilsTest { 29 | public static final class ReflectionTest_HostClass { 30 | public Object returnSame(List arg) { 31 | return arg; 32 | } 33 | 34 | public Object returnSame(int arg) { 35 | return arg; 36 | } 37 | 38 | public Object multiArgCall(Object arg1, Object arg2, boolean returnArg1) { 39 | if (returnArg1) { 40 | return arg1; 41 | } 42 | return arg2; 43 | } 44 | 45 | public boolean returnTrue() { 46 | return true; 47 | } 48 | 49 | public void throwsException() throws IOException { 50 | throw new IOException("Example exception"); 51 | } 52 | } 53 | 54 | @Test 55 | public void testInvokeByReflection_Obj() throws Throwable { 56 | List sampleList = Collections.singletonList("sampleList"); 57 | ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); 58 | Object ret = invokeByReflection(hostClass, "returnSame", sampleList); 59 | Truth.assertThat(ret).isEqualTo(sampleList); 60 | } 61 | 62 | @Test 63 | public void testInvokeByReflection_Null() throws Throwable { 64 | ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); 65 | Object ret = invokeByReflection(hostClass, "returnSame", (Object) null); 66 | Truth.assertThat(ret).isNull(); 67 | } 68 | 69 | @Test 70 | public void testInvokeByReflection_NoArg() throws Throwable { 71 | ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); 72 | boolean ret = (boolean) invokeByReflection(hostClass, "returnTrue"); 73 | Truth.assertThat(ret).isTrue(); 74 | } 75 | 76 | @Test 77 | public void testInvokeByReflection_Primitive() throws Throwable { 78 | ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); 79 | Object ret = invokeByReflection(hostClass, "returnSame", 5); 80 | Truth.assertThat(ret).isEqualTo(5); 81 | } 82 | 83 | @Test 84 | public void testInvokeByReflection_MultiArg() throws Throwable { 85 | ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); 86 | Object arg1 = new Object(); 87 | Object arg2 = new Object(); 88 | Object ret = 89 | invokeByReflection(hostClass, "multiArgCall", arg1, arg2, true /* returnArg1 */); 90 | Truth.assertThat(ret).isEqualTo(arg1); 91 | ret = 92 | Utils.invokeByReflection( 93 | hostClass, "multiArgCall", arg1, arg2, false /* returnArg1 */); 94 | Truth.assertThat(ret).isEqualTo(arg2); 95 | } 96 | 97 | @Test 98 | public void testInvokeByReflection_NoMatch() throws Throwable { 99 | ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); 100 | Truth.assertThat(List.class.isAssignableFrom(Object.class)).isFalse(); 101 | try { 102 | invokeByReflection(hostClass, "returnSame", new Object()); 103 | Assert.fail(); 104 | } catch (NoSuchMethodException e) { 105 | Truth.assertThat(e.getMessage()) 106 | .contains("UtilsTest$ReflectionTest_HostClass#returnSame(Object)"); 107 | } 108 | } 109 | 110 | @Test 111 | public void testInvokeByReflection_UnwrapException() throws Throwable { 112 | ReflectionTest_HostClass hostClass = new ReflectionTest_HostClass(); 113 | try { 114 | invokeByReflection(hostClass, "throwsException"); 115 | Assert.fail(); 116 | } catch (IOException e) { 117 | Truth.assertThat(e.getMessage()).isEqualTo("Example exception"); 118 | } 119 | } 120 | } 121 | --------------------------------------------------------------------------------