diff options
8 files changed, 497 insertions, 33 deletions
@@ -1,7 +1,9 @@ Mobly Bundled Snippets is a set of Snippets to allow Mobly tests to control -Android devices. +Android devices by exposing a simplified verison of the public Android API +suitable for testing. -They expose a simplified verison of the public Android API suitable for testing. +We are adding more APIs as we go. If you have specific needs for certain groups +of APIs, feel free to file a request in [Issues](https://github.com/google/mobly-bundled-snippets/issues). Note: this is not an official Google product. @@ -11,25 +13,29 @@ Note: this is not an official Google product. 1. Compile and install the bundled snippets ./gradlew assembleDebug - adb install -d -r ./build/outputs/apk/mobly-bundled-snippets-debug.apk + adb install -d -r -g ./build/outputs/apk/mobly-bundled-snippets-debug.apk 1. Use the Mobly snippet shell to interact with the bundled snippets snippet_shell.py com.google.android.mobly.snippet.bundled >>> print(s.help()) Known methods: - bluetoothDisable() returns void // Enable bluetooth + bluetoothDisable() returns void // Disable bluetooth with a 30s timeout. + ... + wifiDisable() returns void // Turns off Wi-Fi with a 30s timeout. + wifiEnable() returns void // Turns on Wi-Fi with a 30s timeout. ... -1. To use these snippets within Mobly tests, create a snippet client like so: +1. To use these snippets within Mobly tests, load it on your AndroidDevice objects + after registering android_device module: ```python def setup_class(self): self.ad = self.register_controllers(android_device, min_number=1)[0] self.ad.load_snippet('api', 'com.google.android.mobly.snippet.bundled') - def test_enable_bluetooth(self): - self.ad.api.bluetoothEnable() + def test_enable_wifi(self): + self.ad.api.wifiEnable() ``` diff --git a/build.gradle b/build.gradle index ff69ba3..3c61868 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,10 @@ buildscript { } } +plugins { + id "com.github.sherter.google-java-format" version "0.6" +} + repositories { jcenter() } @@ -26,10 +30,22 @@ android { versionCode 1 versionName "0.0.1" setProperty("archivesBaseName", "mobly-bundled-snippets") + jackOptions { + enabled true + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { compile 'com.google.android.mobly:mobly-snippet-lib:1.0.0' compile 'com.android.support.test:runner:0.5' + compile 'com.google.code.gson:gson:2.6.2' +} + +googleJavaFormat { + options style: 'AOSP' } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 4fe73a9..94ea89a 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -5,10 +5,15 @@ <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> + <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <application> <meta-data android:name="mobly-snippets" - android:value="com.google.android.mobly.snippet.bundled.BluetoothAdapterSnippet" /> + android:value="com.google.android.mobly.snippet.bundled.BluetoothAdapterSnippet, + com.google.android.mobly.snippet.bundled.WifiManagerSnippet" /> </application> <instrumentation diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java index a830ad9..79268da 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java @@ -19,13 +19,14 @@ package com.google.android.mobly.snippet.bundled; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.support.test.InstrumentationRegistry; - import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.bundled.utils.Utils; import com.google.android.mobly.snippet.rpc.Rpc; +/** Snippet class exposing Android APIs in BluetoothAdapter. */ public class BluetoothAdapterSnippet implements Snippet { - private static class BluetoothException extends Exception { - public BluetoothException(String msg) { + private static class BluetoothAdapterSnippetException extends Exception { + public BluetoothAdapterSnippetException(String msg) { super(msg); } } @@ -38,37 +39,26 @@ public class BluetoothAdapterSnippet implements Snippet { mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); } - @Rpc(description = "Enable bluetooth") - public void bluetoothEnable() throws BluetoothException, InterruptedException { + @Rpc(description = "Enable bluetooth with a 30s timeout.") + public void bluetoothEnable() throws BluetoothAdapterSnippetException, InterruptedException { if (!mBluetoothAdapter.enable()) { - throw new BluetoothException("Failed to start enabling bluetooth"); - } - int timeout = 30; - while (!mBluetoothAdapter.isEnabled() && timeout >= 0) { - Thread.sleep(1000); - timeout -= 1; + throw new BluetoothAdapterSnippetException("Failed to start enabling bluetooth"); } - if (!mBluetoothAdapter.isEnabled()) { - throw new BluetoothException("Bluetooth did not turn on before timeout"); + if (!Utils.waitUntil(() -> mBluetoothAdapter.isEnabled(), 30)) { + throw new BluetoothAdapterSnippetException("Bluetooth did not turn on within 30s."); } } - @Rpc(description = "Disable bluetooth") - public void bluetoothDisable() throws BluetoothException, InterruptedException { + @Rpc(description = "Disable bluetooth with a 30s timeout.") + public void bluetoothDisable() throws BluetoothAdapterSnippetException, InterruptedException { if (!mBluetoothAdapter.disable()) { - throw new BluetoothException("Failed to start disabling bluetooth"); + throw new BluetoothAdapterSnippetException("Failed to start disabling bluetooth"); } - int timeout = 30; - while (mBluetoothAdapter.isEnabled() && timeout >= 0) { - Thread.sleep(1000); - timeout -= 1; - } - if (mBluetoothAdapter.isEnabled()) { - throw new BluetoothException("Bluetooth did not turn off before timeout"); + if (!Utils.waitUntil(() -> !mBluetoothAdapter.isEnabled(), 30)) { + throw new BluetoothAdapterSnippetException("Bluetooth did not turn off within 30s."); } } @Override - public void shutdown() { - } + public void shutdown() {} } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java new file mode 100644 index 0000000..1dca048 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.mobly.snippet.bundled; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.support.annotation.Nullable; +import android.support.test.InstrumentationRegistry; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer; +import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; +import com.google.android.mobly.snippet.bundled.utils.Utils; +import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.util.Log; +import java.util.ArrayList; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** Snippet class exposing Android APIs in WifiManager. */ +public class WifiManagerSnippet implements Snippet { + private static class WifiManagerSnippetException extends Exception { + public WifiManagerSnippetException(String msg) { + super(msg); + } + } + + private final WifiManager mWifiManager; + private final Context mContext; + private static final String TAG = "WifiManagerSnippet"; + private final JsonSerializer mJsonSerializer = new JsonSerializer(); + private volatile boolean mIsScanning = false; + private volatile boolean mIsScanResultAvailable = false; + + public WifiManagerSnippet() { + mContext = InstrumentationRegistry.getContext(); + mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + } + + @Rpc(description = "Turns on Wi-Fi with a 30s timeout.") + public void wifiEnable() throws InterruptedException, WifiManagerSnippetException { + if (!mWifiManager.setWifiEnabled(true)) { + throw new WifiManagerSnippetException("Failed to initiate enabling Wi-Fi."); + } + if (!Utils.waitUntil( + () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED, 30)) { + throw new WifiManagerSnippetException("Failed to enable Wi-Fi after 30s, timeout!"); + } + } + + @Rpc(description = "Turns off Wi-Fi with a 30s timeout.") + public void wifiDisable() throws InterruptedException, WifiManagerSnippetException { + if (!mWifiManager.setWifiEnabled(false)) { + throw new WifiManagerSnippetException("Failed to initiate disabling Wi-Fi."); + } + if (!Utils.waitUntil( + () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED, 30)) { + throw new WifiManagerSnippetException("Failed to disable Wi-Fi after 30s, timeout!"); + } + } + + @Rpc(description = "Trigger Wi-Fi scan.") + public void wifiStartScan() throws WifiManagerSnippetException { + if (!mWifiManager.startScan()) { + throw new WifiManagerSnippetException("Failed to initiate Wi-Fi scan."); + } + } + + @Rpc( + description = + "Get Wi-Fi scan results, which is a list of serialized WifiScanResult objects." + ) + public JSONArray wifiGetCachedScanResults() throws JSONException { + JSONArray results = new JSONArray(); + for (ScanResult result : mWifiManager.getScanResults()) { + results.put(mJsonSerializer.toJson(result)); + } + return results; + } + + @Rpc( + description = + "Start scan, wait for scan to complete, and return results, which is a list of " + + "serialized WifiScanResult objects." + ) + public JSONArray wifiScanAndGetResults() + throws InterruptedException, JSONException, WifiManagerSnippetException { + mContext.registerReceiver( + new WifiScanReceiver(), + new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + wifiStartScan(); + mIsScanResultAvailable = false; + mIsScanning = true; + if (!Utils.waitUntil(() -> mIsScanResultAvailable, 2 * 60)) { + throw new WifiManagerSnippetException( + "Failed to get scan results after 2min, timeout!"); + } + return wifiGetCachedScanResults(); + } + + @Rpc( + description = + "Connects to a Wi-Fi network. This covers the common network types like open and " + + "WPA2." + ) + public void wifiConnectSimple(String ssid, @Nullable String password) + throws InterruptedException, JSONException, WifiManagerSnippetException { + JSONObject config = new JSONObject(); + config.put("SSID", ssid); + if (password != null) { + config.put("password", password); + } + wifiConnect(config); + } + + /** + * Connect to a Wi-Fi network. + * + * @param wifiNetworkConfig A JSON object that contains the info required to connect to a Wi-Fi + * network. It follows the fields of WifiConfiguration type, e.g. {"SSID": "myWifi", + * "password": "12345678"}. + * @throws InterruptedException + * @throws JSONException + * @throws WifiManagerSnippetException + */ + @Rpc(description = "Connects to a Wi-Fi network.") + public void wifiConnect(JSONObject wifiNetworkConfig) + throws InterruptedException, JSONException, WifiManagerSnippetException { + Log.d("Got network config: " + wifiNetworkConfig); + WifiConfiguration wifiConfig = JsonDeserializer.jsonToWifiConfig(wifiNetworkConfig); + int networkId = mWifiManager.addNetwork(wifiConfig); + mWifiManager.disconnect(); + if (!mWifiManager.enableNetwork(networkId, true)) { + throw new WifiManagerSnippetException( + "Failed to enable Wi-Fi network of ID: " + networkId); + } + if (!mWifiManager.reconnect()) { + throw new WifiManagerSnippetException( + "Failed to reconnect to Wi-Fi network of ID: " + networkId); + } + if (!Utils.waitUntil( + () -> mWifiManager.getConnectionInfo().getSSID().equals(wifiConfig.SSID), 90)) { + throw new WifiManagerSnippetException( + "Failed to connect to Wi-Fi network " + + wifiNetworkConfig.toString() + + ", timeout!"); + } + Log.d( + "Connected to network '" + + mWifiManager.getConnectionInfo().getSSID() + + "' with ID " + + mWifiManager.getConnectionInfo().getNetworkId()); + } + + @Rpc( + description = + "Forget a configured Wi-Fi network by its network ID, which is part of the" + + " WifiConfiguration." + ) + public void wifiRemoveNetwork(Integer networkId) throws WifiManagerSnippetException { + if (!mWifiManager.removeNetwork(networkId)) { + throw new WifiManagerSnippetException("Failed to remove network of ID: " + networkId); + } + } + + @Rpc( + description = + "Get the list of configured Wi-Fi networks, each is a serialized " + + "WifiConfiguration object." + ) + public ArrayList<JSONObject> wifiGetConfiguredNetworks() throws JSONException { + ArrayList<JSONObject> networks = new ArrayList<>(); + for (WifiConfiguration config : mWifiManager.getConfiguredNetworks()) { + networks.add(mJsonSerializer.toJson(config)); + } + return networks; + } + + @Rpc( + description = + "Get the information about the active Wi-Fi connection, which is a serialized " + + "WifiInfo object." + ) + public JSONObject wifiGetConnectionInfo() throws JSONException { + return mJsonSerializer.toJson(mWifiManager.getConnectionInfo()); + } + + @Rpc( + description = + "Get the info from last successful DHCP request, which is a serialized DhcpInfo " + + "object." + ) + public JSONObject wifiGetDhcpInfo() throws JSONException { + return mJsonSerializer.toJson(mWifiManager.getDhcpInfo()); + } + + @Override + public void shutdown() {} + + private class WifiScanReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context c, Intent intent) { + String action = intent.getAction(); + if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { + mIsScanning = false; + mIsScanResultAvailable = true; + } + } + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java new file mode 100644 index 0000000..4fc6b82 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.mobly.snippet.bundled.utils; + +import android.net.wifi.WifiConfiguration; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * A collection of methods used to deserialize JSON strings into data objects defined in Android + * API. + */ +public class JsonDeserializer { + + private JsonDeserializer() {} + + public static WifiConfiguration jsonToWifiConfig(JSONObject jsonObject) throws JSONException { + WifiConfiguration config = new WifiConfiguration(); + config.SSID = "\"" + jsonObject.getString("SSID") + "\""; + config.hiddenSSID = jsonObject.optBoolean("hiddenSSID", false); + if (jsonObject.has("password")) { + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); + config.preSharedKey = "\"" + jsonObject.getString("password") + "\""; + } else { + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); + } + return config; + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java new file mode 100644 index 0000000..58f8bac --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.mobly.snippet.bundled.utils; + +import android.net.DhcpInfo; +import android.net.wifi.SupplicantState; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.lang.reflect.Modifier; +import java.net.InetAddress; +import java.net.UnknownHostException; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * A collection of methods used to serialize data types defined in Android API into JSON strings. + */ +public class JsonSerializer { + private static Gson mGson; + + public JsonSerializer() { + GsonBuilder builder = new GsonBuilder(); + mGson = + builder.serializeNulls() + .excludeFieldsWithModifiers(Modifier.STATIC) + .enableComplexMapKeySerialization() + .disableInnerClassSerialization() + .create(); + } + + /** + * Remove the extra quotation marks from the beginning and the end of a string. + * + * <p>This is useful for strings like the SSID field of Android's Wi-Fi configuration. + * + * @param originalString + */ + private String trimQuotationMarks(String originalString) { + String result = originalString; + if (originalString.charAt(0) == '"' + && originalString.charAt(originalString.length() - 1) == '"') { + result = originalString.substring(1, originalString.length() - 1); + } + return result; + } + + public JSONObject toJson(Object object) throws JSONException { + if (object instanceof DhcpInfo) { + return serializeDhcpInfo((DhcpInfo) object); + } else if (object instanceof WifiConfiguration) { + return serializeWifiConfiguration((WifiConfiguration) object); + } else if (object instanceof WifiInfo) { + return serializeWifiInfo((WifiInfo) object); + } + return defaultSerialization(object); + } + + /** + * By default, we rely on Gson to do the right job. + * + * @param data An object to serialize + * @return A JSONObject that has the info of the serialized data object. + * @throws JSONException + */ + private JSONObject defaultSerialization(Object data) throws JSONException { + return new JSONObject(mGson.toJson(data)); + } + + private JSONObject serializeDhcpInfo(DhcpInfo data) throws JSONException { + JSONObject result = new JSONObject(mGson.toJson(data)); + int ipAddress = data.ipAddress; + byte[] addressBytes = { + (byte) (0xff & ipAddress), + (byte) (0xff & (ipAddress >> 8)), + (byte) (0xff & (ipAddress >> 16)), + (byte) (0xff & (ipAddress >> 24)) + }; + try { + String addressString = InetAddress.getByAddress(addressBytes).toString(); + result.put("IpAddress", addressString); + } catch (UnknownHostException e) { + result.put("IpAddress", ipAddress); + } + return result; + } + + private JSONObject serializeWifiConfiguration(WifiConfiguration data) throws JSONException { + JSONObject result = new JSONObject(mGson.toJson(data)); + result.put("Status", WifiConfiguration.Status.strings[data.status]); + result.put("SSID", trimQuotationMarks(data.SSID)); + return result; + } + + private JSONObject serializeWifiInfo(WifiInfo data) throws JSONException { + JSONObject result = new JSONObject(mGson.toJson(data)); + result.put("SSID", trimQuotationMarks(data.getSSID())); + for (SupplicantState state : SupplicantState.values()) { + if (data.getSupplicantState().equals(state)) { + result.put("SupplicantState", state.name()); + } + } + return result; + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java new file mode 100644 index 0000000..00bfbd2 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.mobly.snippet.bundled.utils; + +public final class Utils { + private Utils() {} + + /** + * Waits util a condition is met. + * + * <p>This is often used to wait for asynchronous operations to finish and the system to reach a + * desired state. + * + * @param predicate A lambda function that specifies the condition to wait for. This function + * should return true when the desired state has been reached. + * @param timeout The number of seconds to wait for before giving up. + * @return true if the operation finished before timeout, false otherwise. + * @throws InterruptedException + */ + public static boolean waitUntil(Utils.Predicate predicate, int timeout) + throws InterruptedException { + timeout *= 10; + while (!predicate.waitCondition() && timeout >= 0) { + Thread.sleep(100); + timeout -= 1; + } + if (predicate.waitCondition()) { + return true; + } + return false; + } + + /** + * A function interface that is used by lambda functions signaling an async operation is still + * going on. + */ + public interface Predicate { + boolean waitCondition(); + } +} |