From 876ea66681369935addbd7d4700a1e7fbc9f9e1f Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Tue, 10 Jan 2017 17:42:05 -0800 Subject: Basic bundled snippet to control bluetooth. --- .../snippet/bundled/BluetoothAdapterSnippet.java | 74 ++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 new file mode 100644 index 0000000..a830ad9 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java @@ -0,0 +1,74 @@ +/* + * 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.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.support.test.InstrumentationRegistry; + +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; + +public class BluetoothAdapterSnippet implements Snippet { + private static class BluetoothException extends Exception { + public BluetoothException(String msg) { + super(msg); + } + } + + private final Context mContext; + private final BluetoothAdapter mBluetoothAdapter; + + public BluetoothAdapterSnippet() { + mContext = InstrumentationRegistry.getContext(); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + } + + @Rpc(description = "Enable bluetooth") + public void bluetoothEnable() throws BluetoothException, 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; + } + if (!mBluetoothAdapter.isEnabled()) { + throw new BluetoothException("Bluetooth did not turn on before timeout"); + } + } + + @Rpc(description = "Disable bluetooth") + public void bluetoothDisable() throws BluetoothException, InterruptedException { + if (!mBluetoothAdapter.disable()) { + throw new BluetoothException("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"); + } + } + + @Override + public void shutdown() { + } +} -- cgit v1.2.3 From 0eb024bbab6f81d26a48e6df2acb729bca38c1a6 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Mon, 23 Jan 2017 15:46:10 -0800 Subject: Utilities, conventions, serialize/deserialize mechanism, and basic Wi-Fi APIs * Add basic Wi-Fi API support. * Make bluetooth snippet style consistent with Wi-Fi. * Add the mechanism to serialize and deserialize data types. * Create a util for waiting on asynch operations. * Enable Java8 for the project. * Add formatter for the project. Fixes #1. --- .../snippet/bundled/BluetoothAdapterSnippet.java | 40 ++-- .../mobly/snippet/bundled/WifiManagerSnippet.java | 230 +++++++++++++++++++++ .../snippet/bundled/utils/JsonDeserializer.java | 43 ++++ .../snippet/bundled/utils/JsonSerializer.java | 120 +++++++++++ .../android/mobly/snippet/bundled/utils/Utils.java | 54 +++++ 5 files changed, 462 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 wifiGetConfiguredNetworks() throws JSONException { + ArrayList 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. + * + *

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. + * + *

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(); + } +} -- cgit v1.2.3 From 9896303f43e140c467d34d87148b545ac237bcc9 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Thu, 9 Feb 2017 12:25:52 -0800 Subject: Add makeToast. --- .../mobly/snippet/bundled/NotificationSnippet.java | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java new file mode 100644 index 0000000..11e6187 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java @@ -0,0 +1,48 @@ +/* + * 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.Context; +import android.os.Handler; +import android.support.test.InstrumentationRegistry; +import android.widget.Toast; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; + +/** Snippet class exposing Android APIs related to creating notification on screen. */ +public class NotificationSnippet implements Snippet { + + private final Context mContext; + /** + * Since the APIs here deal with UI, most of them have to be called in a thread that has called + * looper. + */ + private final Handler mHandler; + + public NotificationSnippet() { + mContext = InstrumentationRegistry.getContext(); + mHandler = new Handler(mContext.getMainLooper()); + } + + @Rpc(description = "Make a toast on screen.") + public void makeToast(String message) { + mHandler.post(() -> Toast.makeText(mContext, message, Toast.LENGTH_LONG).show()); + } + + @Override + public void shutdown() {} +} -- cgit v1.2.3 From c82a5a36f1b616b0d69488512da6d9054340b8da Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Mon, 13 Feb 2017 19:02:21 -0800 Subject: Support adding and listing Google accounts. (#16) --- .../mobly/snippet/bundled/AccountSnippet.java | 156 +++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java new file mode 100644 index 0000000..48b70fc --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -0,0 +1,156 @@ +/* + * 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.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AccountsException; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SyncAdapterType; +import android.content.SyncStatusObserver; +import android.os.Bundle; +import android.os.Handler; +import android.support.test.InstrumentationRegistry; +import android.widget.Toast; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.util.Log; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * Snippet class exposing Android APIs related to management of device accounts. + * + *

Android devices can have accounts of any type added and synced. New types can be created by + * apps by implementing a {@link android.content.ContentProvider} for a particular account type. + * + *

Google (gmail) accounts are of type "com.google" and their handling is managed by the + * operating system. This class allows you to add and remove Google accounts from a device. + * */ +public class AccountSnippet implements Snippet { + private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; + private static final String AUTH_TOKEN_TYPE = "mail"; + + private static class AccountSnippetException extends Exception { + public AccountSnippetException(String msg) { + super(msg); + } + } + + private final AccountManager mAccountManager; + private final List mSyncStatusObserverHandles; + + public AccountSnippet() { + Context context = InstrumentationRegistry.getContext(); + mAccountManager = AccountManager.get(context); + mSyncStatusObserverHandles = new LinkedList<>(); + } + + /** + * Adds a Google account to the device. + * + *

TODO(adorokhine): Support adding accounts of other types with an optional 'type' kwarg. + *

TODO(adorokhine): Allow users to choose whether to enable/disable sync with a kwarg. + * + * @param username Username of the account to add (including @gmail.com). + * @param password Password of the account to add. + */ + @Rpc(description = + "Add a Google (GMail) account to the device, with account data sync disabled.") + public void addAccount(String username, String password) + throws AccountSnippetException, AccountsException, IOException { + // Check for existing account. If we try to re-add an existing account, Android throws an + // exception that says "Account does not exist or not visible. Maybe change pwd?" which is + // a little hard to understand. + if (listAccounts().contains(username)) { + throw new AccountSnippetException( + "Account " + username + " already exists on the device"); + } + Bundle addAccountOptions = new Bundle(); + addAccountOptions.putString("username", username); + addAccountOptions.putString("password", password); + AccountManagerFuture future = + mAccountManager.addAccount( + GOOGLE_ACCOUNT_TYPE, + AUTH_TOKEN_TYPE, + null /* requiredFeatures */, + addAccountOptions, + null /* activity */, + null /* authCallback */, + null /* handler */); + Bundle result = future.getResult(); + if (result.containsKey(AccountManager.KEY_ERROR_CODE)) { + throw new AccountSnippetException( + String.format("Failed to add account due to code %d: %s", + result.getInt(AccountManager.KEY_ERROR_CODE), + result.getString(AccountManager.KEY_ERROR_MESSAGE))); + } + + // Disable sync to avoid test flakiness as accounts fetch additional data. + // It takes a while for all sync adapters to be populated, so register for broadcasts when + // sync is starting and disable them there. + // NOTE: this listener is NOT unregistered because several sync requests for the new account + // will come in over time. + Account account = new Account(username, GOOGLE_ACCOUNT_TYPE); + Object handle = ContentResolver.addStatusChangeListener( + ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE | ContentResolver.SYNC_OBSERVER_TYPE_PENDING, + which -> { + Log.i("Attempt to sync account " + username + " detected! Disabling."); + for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) { + if (!adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE)) { + continue; + } + ContentResolver + .setSyncAutomatically(account, adapter.authority, false /* sync */); + ContentResolver.cancelSync(account, adapter.authority); + } + }); + mSyncStatusObserverHandles.add(handle); + } + + /** + * Returns a list of all Google accounts on the device. + * + *

TODO(adorokhine): Support accounts of other types with an optional 'type' kwarg. + */ + @Rpc(description = "List all Google (GMail) accounts on the device.") + public Set listAccounts() throws SecurityException { + Account[] accounts = mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); + Set usernames = new TreeSet<>(); + for (Account account : accounts) { + usernames.add(account.name); + } + return usernames; + } + + @Override + public void shutdown() { + for (Object handle : mSyncStatusObserverHandles) { + ContentResolver.removeStatusChangeListener(handle); + } + } +} -- cgit v1.2.3 From 99816155feec8667ec76edf15fbb3db1f4aa1401 Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Tue, 14 Feb 2017 14:08:12 -0800 Subject: Minor fixes to Mobly Bundled Snippets. (#19) * Remove unused imports * Explicitly add the INTERNET permission, which is required for snippet to run (it came in implicitly through manfiest merge with mobly-snippet-lib but it's goot to be explicit). --- .../com/google/android/mobly/snippet/bundled/AccountSnippet.java | 8 -------- 1 file changed, 8 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index 48b70fc..8e8d56b 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -18,25 +18,17 @@ package com.google.android.mobly.snippet.bundled; import android.accounts.Account; import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; import android.accounts.AccountsException; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.content.ContentResolver; import android.content.Context; import android.content.SyncAdapterType; -import android.content.SyncStatusObserver; import android.os.Bundle; -import android.os.Handler; import android.support.test.InstrumentationRegistry; -import android.widget.Toast; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; import com.google.android.mobly.snippet.util.Log; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; -- cgit v1.2.3 From 75c9e61de79b29ab2f38b1a04623a5b4b95cffce Mon Sep 17 00:00:00 2001 From: "David T.H. Kao" Date: Wed, 15 Feb 2017 12:17:15 -0800 Subject: Add some simple telephony and audio controls to MBS. (#14) * Add some simple telephony and audio controls to MBS. --- .../mobly/snippet/bundled/AccountSnippet.java | 63 +++++++------- .../mobly/snippet/bundled/AudioSnippet.java | 98 ++++++++++++++++++++++ .../mobly/snippet/bundled/TelephonySnippet.java | 56 +++++++++++++ 3 files changed, 188 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index 8e8d56b..375c774 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -42,7 +42,7 @@ import java.util.TreeSet; * *

Google (gmail) accounts are of type "com.google" and their handling is managed by the * operating system. This class allows you to add and remove Google accounts from a device. - * */ + */ public class AccountSnippet implements Snippet { private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; private static final String AUTH_TOKEN_TYPE = "mail"; @@ -66,40 +66,43 @@ public class AccountSnippet implements Snippet { * Adds a Google account to the device. * *

TODO(adorokhine): Support adding accounts of other types with an optional 'type' kwarg. + * *

TODO(adorokhine): Allow users to choose whether to enable/disable sync with a kwarg. * * @param username Username of the account to add (including @gmail.com). * @param password Password of the account to add. */ - @Rpc(description = - "Add a Google (GMail) account to the device, with account data sync disabled.") + @Rpc( + description = "Add a Google (GMail) account to the device, with account data sync disabled." + ) public void addAccount(String username, String password) - throws AccountSnippetException, AccountsException, IOException { + throws AccountSnippetException, AccountsException, IOException { // Check for existing account. If we try to re-add an existing account, Android throws an // exception that says "Account does not exist or not visible. Maybe change pwd?" which is // a little hard to understand. if (listAccounts().contains(username)) { throw new AccountSnippetException( - "Account " + username + " already exists on the device"); + "Account " + username + " already exists on the device"); } Bundle addAccountOptions = new Bundle(); addAccountOptions.putString("username", username); addAccountOptions.putString("password", password); AccountManagerFuture future = - mAccountManager.addAccount( - GOOGLE_ACCOUNT_TYPE, - AUTH_TOKEN_TYPE, - null /* requiredFeatures */, - addAccountOptions, - null /* activity */, - null /* authCallback */, - null /* handler */); + mAccountManager.addAccount( + GOOGLE_ACCOUNT_TYPE, + AUTH_TOKEN_TYPE, + null /* requiredFeatures */, + addAccountOptions, + null /* activity */, + null /* authCallback */, + null /* handler */); Bundle result = future.getResult(); if (result.containsKey(AccountManager.KEY_ERROR_CODE)) { throw new AccountSnippetException( - String.format("Failed to add account due to code %d: %s", - result.getInt(AccountManager.KEY_ERROR_CODE), - result.getString(AccountManager.KEY_ERROR_MESSAGE))); + String.format( + "Failed to add account due to code %d: %s", + result.getInt(AccountManager.KEY_ERROR_CODE), + result.getString(AccountManager.KEY_ERROR_MESSAGE))); } // Disable sync to avoid test flakiness as accounts fetch additional data. @@ -108,19 +111,21 @@ public class AccountSnippet implements Snippet { // NOTE: this listener is NOT unregistered because several sync requests for the new account // will come in over time. Account account = new Account(username, GOOGLE_ACCOUNT_TYPE); - Object handle = ContentResolver.addStatusChangeListener( - ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE | ContentResolver.SYNC_OBSERVER_TYPE_PENDING, - which -> { - Log.i("Attempt to sync account " + username + " detected! Disabling."); - for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) { - if (!adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE)) { - continue; - } - ContentResolver - .setSyncAutomatically(account, adapter.authority, false /* sync */); - ContentResolver.cancelSync(account, adapter.authority); - } - }); + Object handle = + ContentResolver.addStatusChangeListener( + ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE + | ContentResolver.SYNC_OBSERVER_TYPE_PENDING, + which -> { + Log.i("Attempt to sync account " + username + " detected! Disabling."); + for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) { + if (!adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE)) { + continue; + } + ContentResolver.setSyncAutomatically( + account, adapter.authority, false /* sync */); + ContentResolver.cancelSync(account, adapter.authority); + } + }); mSyncStatusObserverHandles.add(handle); } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java new file mode 100644 index 0000000..44f2d69 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.mobly.snippet.bundled; + +import android.content.Context; +import android.media.AudioManager; +import android.support.test.InstrumentationRegistry; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; + +/* Snippet class to control audio */ +public class AudioSnippet implements Snippet { + + private final AudioManager mAudioManager; + + public AudioSnippet() { + Context context = InstrumentationRegistry.getContext(); + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + } + + @Rpc(description = "Gets the music stream volume.") + public int getMusicVolume() { + return mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + } + + @Rpc(description = "Gets the maximum music stream volume value.") + public int getMusicMaxVolume() { + return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + } + + @Rpc( + description = + "Sets the music stream volume. The minimum value is 0. Use getMusicMaxVolume" + + "to determine the maximum." + ) + public void setMusicVolume(Integer value) { + mAudioManager.setStreamVolume( + AudioManager.STREAM_MUSIC, value, 0 /* flags, 0 = no flags */); + } + + @Rpc(description = "Gets the ringer volume.") + public int getRingVolume() { + return mAudioManager.getStreamVolume(AudioManager.STREAM_RING); + } + + @Rpc(description = "Gets the maximum ringer volume value.") + public int getRingMaxVolume() { + return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING); + } + + @Rpc( + description = + "Sets the ringer stream volume. The minimum value is 0. Use getRingMaxVolume" + + "to determine the maximum." + ) + public void setRingVolume(Integer value) { + mAudioManager.setStreamVolume(AudioManager.STREAM_RING, value, 0 /* flags, 0 = no flags */); + } + + @Rpc(description = "Silences all audio streams.") + public void muteAll() { + // TODO: NUM_STREAMS is deprecated. Find a different solution. + for (int i = 0; i < AudioManager.NUM_STREAMS; i++) { + mAudioManager.setStreamVolume(i /* audio stream */, 0 /* value */, 0 /* flags */); + } + } + + @Rpc( + description = + "Puts the ringer volume at the lowest setting, but does not set it to " + + "DO NOT DISTURB; the phone will vibrate when receiving a call." + ) + public void muteRing() { + setRingVolume(0); + } + + @Rpc(description = "Mute music stream.") + public void muteMusic() { + setMusicVolume(0); + } + + @Override + public void shutdown() {} +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java new file mode 100644 index 0000000..431456d --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java @@ -0,0 +1,56 @@ +/* + * 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.Context; +import android.support.test.InstrumentationRegistry; +import android.telephony.TelephonyManager; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; + +/** Snippet class for telephony RPCs. */ +public class TelephonySnippet implements Snippet { + + private final TelephonyManager mTelephonyManager; + + public TelephonySnippet() { + Context context = InstrumentationRegistry.getContext(); + mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + } + + @Rpc(description = "Gets the line 1 phone number.") + public String getLine1Number() { + return mTelephonyManager.getLine1Number(); + } + + @Rpc(description = "Returns the unique subscriber ID, for example, the IMSI for a GSM phone.") + public String getSubscriberId() { + return mTelephonyManager.getSubscriberId(); + } + + @Rpc( + description = + "Gets the call state for the default subscription. Call state values are" + + "0: IDLE, 1: RINGING, 2: OFFHOOK" + ) + public int getTelephonyCallState() { + return mTelephonyManager.getCallState(); + } + + @Override + public void shutdown() {} +} -- cgit v1.2.3 From dd918fe5932beca0c5202706ec2969c15620439d Mon Sep 17 00:00:00 2001 From: "David T.H. Kao" Date: Wed, 22 Feb 2017 13:54:57 -0800 Subject: Get the number of streams for muting through reflection. (#22) * Get the number of streams for muting through reflection. --- .../google/android/mobly/snippet/bundled/AudioSnippet.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java index 44f2d69..9275fd1 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -21,6 +21,9 @@ import android.media.AudioManager; import android.support.test.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.util.Log; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; /* Snippet class to control audio */ public class AudioSnippet implements Snippet { @@ -72,9 +75,13 @@ public class AudioSnippet implements Snippet { } @Rpc(description = "Silences all audio streams.") - public void muteAll() { - // TODO: NUM_STREAMS is deprecated. Find a different solution. - for (int i = 0; i < AudioManager.NUM_STREAMS; i++) { + public void muteAll() throws Exception { + /* Get numStreams from AudioSystem through reflection. If for some reason this fails, + calling muteAll will throw. */ + Class audioSystem = Class.forName("android.media.AudioSystem"); + Method getNumStreamTypes = audioSystem.getDeclaredMethod("getNumStreamTypes"); + int numStreams = (int) getNumStreamTypes.invoke(null); + for (int i = 0; i < numStreams; i++) { mAudioManager.setStreamVolume(i /* audio stream */, 0 /* value */, 0 /* flags */); } } -- cgit v1.2.3 From 1b53366265d1c553adf105c5814a728d1eeca44f Mon Sep 17 00:00:00 2001 From: "David T.H. Kao" Date: Thu, 23 Feb 2017 09:43:30 -0800 Subject: remove unused imports in AudioSnippet (#23) --- .../java/com/google/android/mobly/snippet/bundled/AudioSnippet.java | 2 -- 1 file changed, 2 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java index 9275fd1..43cdfd2 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -21,8 +21,6 @@ import android.media.AudioManager; import android.support.test.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; -import com.google.android.mobly.snippet.util.Log; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /* Snippet class to control audio */ -- cgit v1.2.3 From ee617f3eba60f30d86be03ee59a391b5a8453d91 Mon Sep 17 00:00:00 2001 From: "David T.H. Kao" Date: Sun, 5 Mar 2017 21:57:40 -0800 Subject: Adding content sync controls to AccountSnippet. (#25) * Adding content sync controls to AccountSnippet. --- .../mobly/snippet/bundled/AccountSnippet.java | 154 ++++++++++++++++++++- .../mobly/snippet/bundled/AudioSnippet.java | 2 +- 2 files changed, 151 insertions(+), 5 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index 375c774..68d6efd 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -29,10 +29,15 @@ import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; import com.google.android.mobly.snippet.util.Log; import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Snippet class exposing Android APIs related to management of device accounts. @@ -56,10 +61,15 @@ public class AccountSnippet implements Snippet { private final AccountManager mAccountManager; private final List mSyncStatusObserverHandles; + private final Map> mSyncWhitelist; + private final ReentrantReadWriteLock mLock; + public AccountSnippet() { Context context = InstrumentationRegistry.getContext(); mAccountManager = AccountManager.get(context); mSyncStatusObserverHandles = new LinkedList<>(); + mSyncWhitelist = new HashMap<>(); + mLock = new ReentrantReadWriteLock(); } /** @@ -116,19 +126,155 @@ public class AccountSnippet implements Snippet { ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE | ContentResolver.SYNC_OBSERVER_TYPE_PENDING, which -> { - Log.i("Attempt to sync account " + username + " detected! Disabling."); for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) { + // Ignore non-Google account types. if (!adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE)) { continue; } - ContentResolver.setSyncAutomatically( - account, adapter.authority, false /* sync */); - ContentResolver.cancelSync(account, adapter.authority); + // If a content provider is not whitelisted, then disable it. + // Because startSync and stopSync synchronously update the whitelist + // and sync settings, writelock both the whitelist check and the + // call to sync together. + mLock.writeLock().lock(); + try { + if (!isAdapterWhitelisted(username, adapter.authority)) { + updateSync(account, adapter.authority, false /* sync */); + } + } finally { + mLock.writeLock().unlock(); + } } }); mSyncStatusObserverHandles.add(handle); } + /** + * Checks to see if the SyncAdapter is whitelisted. + * + *

AccountSnippet disables syncing by default when adding an account, except for whitelisted + * SyncAdapters. This function checks the whitelist for a specific account-authority pair. + * + * @param username Username of the account (including @gmail.com). + * @param authority The authority of a content provider that should be checked. + */ + private boolean isAdapterWhitelisted(String username, String authority) { + boolean result = false; + mLock.readLock().lock(); + try { + Set whitelistedProviders = mSyncWhitelist.get(username); + if (whitelistedProviders != null) { + result = whitelistedProviders.contains(authority); + } + } finally { + mLock.readLock().unlock(); + } + return result; + } + + /** + * Updates ContentResolver sync settings for an Account's specified SyncAdapter. + * + *

Sets an accounts SyncAdapter (selected based on authority) to sync/not-sync automatically + * and immediately requests/cancels a sync. + * + *

updateSync should always be called under {@link AccountSnippet#mLock} write lock to avoid + * flapping between the getSyncAutomatically and setSyncAutomatically calls. + * + * @param account A Google Account. + * @param authority The authority of a content provider that should (not) be synced. + * @param sync Whether or not the account's content provider should be synced. + */ + private void updateSync(Account account, String authority, boolean sync) { + if (ContentResolver.getSyncAutomatically(account, authority) != sync) { + ContentResolver.setSyncAutomatically(account, authority, sync); + if (sync) { + ContentResolver.requestSync(account, authority, new Bundle()); + } else { + ContentResolver.cancelSync(account, authority); + } + Log.i( + "Set sync to " + + sync + + " for account " + + account + + ", adapter " + + authority + + "."); + } + } + + /** + * Enables syncing of a SyncAdapter for a given content provider. + * + *

Adds the authority to a whitelist, and immediately requests a sync. + * + * @param username Username of the account (including @gmail.com). + * @param authority The authority of a content provider that should be synced. + */ + @Rpc(description = "Enables syncing of a SyncAdapter for a content provider.") + public void startSync(String username, String authority) throws AccountSnippetException { + if (!listAccounts().contains(username)) { + throw new AccountSnippetException("Account " + username + " is not on the device"); + } + // Add to the whitelist + mLock.writeLock().lock(); + try { + if (mSyncWhitelist.containsKey(username)) { + mSyncWhitelist.get(username).add(authority); + } else { + mSyncWhitelist.put(username, new HashSet(Arrays.asList(authority))); + } + // Update the Sync settings + for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) { + // Find the Google account content provider. + if (adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE) + && adapter.authority.equals(authority)) { + Account account = new Account(username, GOOGLE_ACCOUNT_TYPE); + updateSync(account, authority, true); + } + } + } finally { + mLock.writeLock().unlock(); + } + } + + /** + * Disables syncing of a SyncAdapter for a given content provider. + * + *

Removes the content provider authority from a whitelist. + * + * @param username Username of the account (including @gmail.com). + * @param authority The authority of a content provider that should not be synced. + */ + @Rpc(description = "Disables syncing of a SyncAdapter for a content provider.") + public void stopSync(String username, String authority) throws AccountSnippetException { + if (!listAccounts().contains(username)) { + throw new AccountSnippetException("Account " + username + " is not on the device"); + } + // Remove from whitelist + mLock.writeLock().lock(); + try { + if (mSyncWhitelist.containsKey(username)) { + Set whitelistedProviders = mSyncWhitelist.get(username); + whitelistedProviders.remove(authority); + if (whitelistedProviders.isEmpty()) { + mSyncWhitelist.remove(username); + } + } + // Update the Sync settings + for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) { + // Find the Google account content provider. + if (adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE) + && adapter.authority.equals(authority)) { + Account account = new Account(username, GOOGLE_ACCOUNT_TYPE); + updateSync(account, authority, false); + } + } + } finally { + mLock.writeLock().unlock(); + } + } + /** * Returns a list of all Google accounts on the device. * diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java index 43cdfd2..1ca96d0 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -75,7 +75,7 @@ public class AudioSnippet implements Snippet { @Rpc(description = "Silences all audio streams.") public void muteAll() throws Exception { /* Get numStreams from AudioSystem through reflection. If for some reason this fails, - calling muteAll will throw. */ + * calling muteAll will throw. */ Class audioSystem = Class.forName("android.media.AudioSystem"); Method getNumStreamTypes = audioSystem.getDeclaredMethod("getNumStreamTypes"); int numStreams = (int) getNumStreamTypes.invoke(null); -- cgit v1.2.3 From 272e22a0ac799f6e54a32b2ad4a93bed9885ce91 Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Tue, 14 Mar 2017 15:15:11 -0700 Subject: Run googleJavaFormat. (#30) Fixes invalid formatting in AccountSnippet. --- .../android/mobly/snippet/bundled/AccountSnippet.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index 68d6efd..baef605 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -193,13 +193,13 @@ public class AccountSnippet implements Snippet { ContentResolver.cancelSync(account, authority); } Log.i( - "Set sync to " - + sync - + " for account " - + account - + ", adapter " - + authority - + "."); + "Set sync to " + + sync + + " for account " + + account + + ", adapter " + + authority + + "."); } } -- cgit v1.2.3 From d10e2367d72e2a244f5b1160d9f0a6576abb0f5d Mon Sep 17 00:00:00 2001 From: Ang Li Date: Tue, 14 Mar 2017 15:59:03 -0700 Subject: Allow calling wifiConnect with the connected network. (#28) --- .../mobly/snippet/bundled/WifiManagerSnippet.java | 28 ++++++++++++++++++++++ .../snippet/bundled/utils/JsonSerializer.java | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 1dca048..617028f 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -132,6 +132,22 @@ public class WifiManagerSnippet implements Snippet { wifiConnect(config); } + /** + * Gets the {@link WifiConfiguration} of a Wi-Fi network that has already been configured. + * + *

If the network has not been configured, returns null. + * + *

A network is configured if a WifiConfiguration was created for it and added with {@link + * WifiManager#addNetwork(WifiConfiguration)}. + */ + private WifiConfiguration getExistingConfiguredNetwork(String ssid) { + for (WifiConfiguration config : mWifiManager.getConfiguredNetworks()) { + if (config.SSID.equals(ssid)) { + return config; + } + } + return null; + } /** * Connect to a Wi-Fi network. * @@ -147,6 +163,18 @@ public class WifiManagerSnippet implements Snippet { throws InterruptedException, JSONException, WifiManagerSnippetException { Log.d("Got network config: " + wifiNetworkConfig); WifiConfiguration wifiConfig = JsonDeserializer.jsonToWifiConfig(wifiNetworkConfig); + // Return directly if network is already connected. + String connectedSsid = mWifiManager.getConnectionInfo().getSSID(); + if (connectedSsid.equals(wifiConfig.SSID)) { + Log.d("Network " + connectedSsid + " is already connected."); + return; + } + // If the network is already added but not connected, update the configuration first. + WifiConfiguration existingConfig = getExistingConfiguredNetwork(wifiConfig.SSID); + if (existingConfig != null) { + Log.d("Update the configuration of network " + existingConfig.SSID + "."); + mWifiManager.removeNetwork(existingConfig.networkId); + } int networkId = mWifiManager.addNetwork(wifiConfig); mWifiManager.disconnect(); if (!mWifiManager.enableNetwork(networkId, true)) { 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 index 58f8bac..c1d4e81 100644 --- 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 @@ -51,7 +51,7 @@ public class JsonSerializer { * * @param originalString */ - private String trimQuotationMarks(String originalString) { + private static String trimQuotationMarks(String originalString) { String result = originalString; if (originalString.charAt(0) == '"' && originalString.charAt(originalString.length() - 1) == '"') { -- cgit v1.2.3 From 6607e9486fb6f55f2cf03e8e5886121b5995655d Mon Sep 17 00:00:00 2001 From: "David T.H. Kao" Date: Tue, 14 Mar 2017 23:15:37 -0700 Subject: Remove unused var mIsScanning from WifiManagerSnippet (#32) --- .../com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java | 3 --- 1 file changed, 3 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 617028f..6662511 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -48,7 +48,6 @@ public class WifiManagerSnippet implements Snippet { 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() { @@ -109,7 +108,6 @@ public class WifiManagerSnippet implements Snippet { 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!"); @@ -250,7 +248,6 @@ public class WifiManagerSnippet implements Snippet { public void onReceive(Context c, Intent intent) { String action = intent.getAction(); if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { - mIsScanning = false; mIsScanResultAvailable = true; } } -- cgit v1.2.3 From 5b20052d9aac1fcd5c166f725ef5bc47bb6e59e8 Mon Sep 17 00:00:00 2001 From: ygw11223 Date: Fri, 7 Apr 2017 11:13:58 -0700 Subject: adding methords for BluetoothAdapterSnippet with serializer (#26) * Add methods for bt discovery and listing bt devices * Update JsonSerializer.java --- .../snippet/bundled/BluetoothAdapterSnippet.java | 87 +++++++++++++++++++++- .../snippet/bundled/utils/JsonSerializer.java | 51 ++++++++++++- 2 files changed, 135 insertions(+), 3 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 79268da..91e0564 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 @@ -17,11 +17,19 @@ package com.google.android.mobly.snippet.bundled; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.support.test.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; +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 java.util.ArrayList; +import org.json.JSONArray; +import org.json.JSONException; /** Snippet class exposing Android APIs in BluetoothAdapter. */ public class BluetoothAdapterSnippet implements Snippet { @@ -33,6 +41,9 @@ public class BluetoothAdapterSnippet implements Snippet { private final Context mContext; private final BluetoothAdapter mBluetoothAdapter; + private final JsonSerializer mJsonSerializer = new JsonSerializer(); + private final ArrayList mDiscoveryResults = new ArrayList<>(); + private volatile boolean mIsScanResultAvailable = false; public BluetoothAdapterSnippet() { mContext = InstrumentationRegistry.getContext(); @@ -40,7 +51,7 @@ public class BluetoothAdapterSnippet implements Snippet { } @Rpc(description = "Enable bluetooth with a 30s timeout.") - public void bluetoothEnable() throws BluetoothAdapterSnippetException, InterruptedException { + public void btEnable() throws BluetoothAdapterSnippetException, InterruptedException { if (!mBluetoothAdapter.enable()) { throw new BluetoothAdapterSnippetException("Failed to start enabling bluetooth"); } @@ -50,7 +61,7 @@ public class BluetoothAdapterSnippet implements Snippet { } @Rpc(description = "Disable bluetooth with a 30s timeout.") - public void bluetoothDisable() throws BluetoothAdapterSnippetException, InterruptedException { + public void btDisable() throws BluetoothAdapterSnippetException, InterruptedException { if (!mBluetoothAdapter.disable()) { throw new BluetoothAdapterSnippetException("Failed to start disabling bluetooth"); } @@ -59,6 +70,78 @@ public class BluetoothAdapterSnippet implements Snippet { } } + @Rpc( + description = + "Get bluetooth discovery results, which is a list of serialized BluetoothDevice objects." + ) + public JSONArray btGetCachedScanResults() throws JSONException { + JSONArray results = new JSONArray(); + for (BluetoothDevice result : mDiscoveryResults) { + results.put(mJsonSerializer.toJson(result)); + } + return results; + } + + @Rpc( + description = + "Start discovery, wait for discovery to complete, and return results, which is a list of " + + "serialized BluetoothDevice objects." + ) + public JSONArray btDiscoveryAndGetResults() + throws InterruptedException, JSONException, BluetoothAdapterSnippetException { + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); + filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + if (mBluetoothAdapter.isDiscovering()) { + mBluetoothAdapter.cancelDiscovery(); + } + mDiscoveryResults.clear(); + mIsScanResultAvailable = false; + BroadcastReceiver receiver = new BluetoothScanReceiver(); + mContext.registerReceiver(receiver, filter); + try { + if (!mBluetoothAdapter.startDiscovery()) { + throw new BluetoothAdapterSnippetException( + "Failed to initiate Bluetooth Discovery."); + } + if (!Utils.waitUntil(() -> mIsScanResultAvailable, 60)) { + throw new BluetoothAdapterSnippetException( + "Failed to get discovery results after 1 min, timeout!"); + } + } finally { + mContext.unregisterReceiver(receiver); + } + return btGetCachedScanResults(); + } + + @Rpc(description = "Get the list of paired bluetooth devices") + public JSONArray btGetPairedDevices() + throws BluetoothAdapterSnippetException, InterruptedException, JSONException { + JSONArray pairedDevices = new JSONArray(); + for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) { + pairedDevices.put(mJsonSerializer.toJson(device)); + } + return pairedDevices; + } + @Override public void shutdown() {} + + private class BluetoothScanReceiver extends BroadcastReceiver { + + /** + * The receiver gets an ACTION_FOUND intent whenever a new device is found. + * ACTION_DISCOVERY_FINISHED intent is received when the discovery process ends. + */ + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { + mIsScanResultAvailable = true; + } else if (BluetoothDevice.ACTION_FOUND.equals(action)) { + BluetoothDevice device = + (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + mDiscoveryResults.add(device); + } + } + } } 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 index c1d4e81..0c1b7a3 100644 --- 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 @@ -16,15 +16,19 @@ package com.google.android.mobly.snippet.bundled.utils; +import android.bluetooth.BluetoothDevice; import android.net.DhcpInfo; import android.net.wifi.SupplicantState; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; +import android.os.ParcelUuid; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.lang.reflect.Modifier; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.ArrayList; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -61,7 +65,9 @@ public class JsonSerializer { } public JSONObject toJson(Object object) throws JSONException { - if (object instanceof DhcpInfo) { + if (object instanceof BluetoothDevice) { + return serializeBluetoothDevice((BluetoothDevice) object); + } else if (object instanceof DhcpInfo) { return serializeDhcpInfo((DhcpInfo) object); } else if (object instanceof WifiConfiguration) { return serializeWifiConfiguration((WifiConfiguration) object); @@ -117,4 +123,47 @@ public class JsonSerializer { } return result; } + + private JSONObject serializeBluetoothDevice(BluetoothDevice data) throws JSONException { + JSONObject result = new JSONObject(); + result.put("Address", data.getAddress()); + switch (data.getBondState()) { + case 10: + result.put("BondState", "None"); + break; + case 11: + result.put("BondState", "Bonding"); + break; + case 12: + result.put("BondState", "Bonded"); + } + if (data.getName() == null) { + result.put("Name", "Null"); + } else { + result.put("Name", data.getName()); + } + switch (data.getType()) { + case 0: + result.put("Type", "Unknown"); + break; + case 1: + result.put("Type", "Classic"); + break; + case 2: + result.put("Type", "Le"); + break; + case 3: + result.put("Type", "Dual"); + } + if (data.getUuids() == null) { + result.put("UUIDs", "Null"); + } else { + ArrayList uuids = new ArrayList(); + for (ParcelUuid uuid : data.getUuids()) { + uuids.add(uuid.toString()); + } + result.put("UUIDs", new JSONArray(uuids)); + } + return result; + } } -- cgit v1.2.3 From 5485b184bc0823a7f42033b498baaeaac94270c2 Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Wed, 19 Apr 2017 21:05:13 -0700 Subject: Squash "serializable class has no definition of serialVersionUID" warnings. (#37) Fixes #36. --- .../java/com/google/android/mobly/snippet/bundled/AccountSnippet.java | 1 + .../google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java | 1 + .../com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java | 1 + 3 files changed, 3 insertions(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index baef605..dc1fac1 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -53,6 +53,7 @@ public class AccountSnippet implements Snippet { private static final String AUTH_TOKEN_TYPE = "mail"; private static class AccountSnippetException extends Exception { + private static final long serialVersionUID = 1; public AccountSnippetException(String msg) { super(msg); } 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 91e0564..8f87948 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 @@ -34,6 +34,7 @@ import org.json.JSONException; /** Snippet class exposing Android APIs in BluetoothAdapter. */ public class BluetoothAdapterSnippet implements Snippet { private static class BluetoothAdapterSnippetException extends Exception { + private static final long serialVersionUID = 1; public BluetoothAdapterSnippetException(String msg) { super(msg); } 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 index 6662511..f4fa0c0 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -39,6 +39,7 @@ import org.json.JSONObject; /** Snippet class exposing Android APIs in WifiManager. */ public class WifiManagerSnippet implements Snippet { private static class WifiManagerSnippetException extends Exception { + private static final long serialVersionUID = 1; public WifiManagerSnippetException(String msg) { super(msg); } -- cgit v1.2.3 From 3c4740c21204e35f4ff4ae2d3e120dc26bb1b038 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Tue, 25 Apr 2017 10:56:15 -0700 Subject: Add Rpcs for enabling/disabling bt snoop log. (#39) * Add Rpcs for enabling/disabling bt snoop log. --- .../snippet/bundled/BluetoothAdapterSnippet.java | 46 ++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 8f87948..90ba707 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 @@ -22,11 +22,14 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Build; import android.support.test.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; 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.rpc.RpcMinSdk; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import org.json.JSONArray; import org.json.JSONException; @@ -35,6 +38,7 @@ import org.json.JSONException; public class BluetoothAdapterSnippet implements Snippet { private static class BluetoothAdapterSnippetException extends Exception { private static final long serialVersionUID = 1; + public BluetoothAdapterSnippetException(String msg) { super(msg); } @@ -102,11 +106,11 @@ public class BluetoothAdapterSnippet implements Snippet { try { if (!mBluetoothAdapter.startDiscovery()) { throw new BluetoothAdapterSnippetException( - "Failed to initiate Bluetooth Discovery."); + "Failed to initiate Bluetooth Discovery."); } if (!Utils.waitUntil(() -> mIsScanResultAvailable, 60)) { throw new BluetoothAdapterSnippetException( - "Failed to get discovery results after 1 min, timeout!"); + "Failed to get discovery results after 1 min, timeout!"); } } finally { mContext.unregisterReceiver(receiver); @@ -114,7 +118,7 @@ public class BluetoothAdapterSnippet implements Snippet { return btGetCachedScanResults(); } - @Rpc(description = "Get the list of paired bluetooth devices") + @Rpc(description = "Get the list of paired bluetooth devices.") public JSONArray btGetPairedDevices() throws BluetoothAdapterSnippetException, InterruptedException, JSONException { JSONArray pairedDevices = new JSONArray(); @@ -124,6 +128,42 @@ public class BluetoothAdapterSnippet implements Snippet { return pairedDevices; } + /** + * Enable Bluetooth HCI snoop log collection. + * + *

The file can be pulled from `/sdcard/btsnoop_hci.log`. + * + * @return false if enabling the snoop log failed, true otherwise. + * @throws Throwable + */ + @RpcMinSdk(Build.VERSION_CODES.KITKAT) + @Rpc(description = "Enable Bluetooth HCI snoop log for debugging.") + public boolean btEnableHciSnoopLog() throws Throwable { + try { + return (boolean) + mBluetoothAdapter + .getClass() + .getDeclaredMethod("configHciSnoopLog", boolean.class) + .invoke(mBluetoothAdapter, true); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + @RpcMinSdk(Build.VERSION_CODES.KITKAT) + @Rpc(description = "Disable Bluetooth HCI snoop log.") + public boolean btDisableHciSnoopLog() throws Throwable { + try { + return (boolean) + mBluetoothAdapter + .getClass() + .getDeclaredMethod("configHciSnoopLog", boolean.class) + .invoke(mBluetoothAdapter, false); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + @Override public void shutdown() {} -- cgit v1.2.3 From 4c0c5d68410dda9845e457a37fbb2098e743b550 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Wed, 26 Apr 2017 15:21:10 -0700 Subject: Add Wi-Fi soft ap related Rpc methods. (#35) * Add Wi-Fi soft ap related Rpc methods. * Change wait util to accomodate lambda functions that throw exceptions, which interrupt the wait. --- .../mobly/snippet/bundled/WifiManagerSnippet.java | 103 +++++++++++++++++++++ .../snippet/bundled/utils/ApiVersionException.java | 8 ++ .../snippet/bundled/utils/JsonSerializer.java | 2 +- .../android/mobly/snippet/bundled/utils/Utils.java | 25 +++-- 4 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/utils/ApiVersionException.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index f4fa0c0..ba9569c 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -23,14 +23,17 @@ import android.content.IntentFilter; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; +import android.os.Build; 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.ApiVersionException; 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.lang.reflect.InvocationTargetException; import java.util.ArrayList; import org.json.JSONArray; import org.json.JSONException; @@ -240,6 +243,106 @@ public class WifiManagerSnippet implements Snippet { return mJsonSerializer.toJson(mWifiManager.getDhcpInfo()); } + private void verifyApiVersionForSoftAp() throws ApiVersionException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + throw new ApiVersionException( + "Soft AP APIs are not supported in Android versions >= N."); + } + } + + @Rpc(description = "Check whether Wi-Fi Soft AP (hotspot) is enabled.") + public boolean wifiIsApEnabled() throws Throwable { + verifyApiVersionForSoftAp(); + try { + return (boolean) + mWifiManager + .getClass() + .getDeclaredMethod("isWifiApEnabled") + .invoke(mWifiManager); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + /** + * Enable Wi-Fi Soft AP (hotspot). + * + *

Does not work for release N. + * + * @param configuration The same format as the param wifiNetworkConfig param for wifiConnect. + * @throws Throwable + */ + @Rpc(description = "Enable Wi-Fi Soft AP (hotspot).") + public void wifiEnableSoftAp(@Nullable JSONObject configuration) throws Throwable { + verifyApiVersionForSoftAp(); + // If no configuration is provided, the existing configuration would be used. + WifiConfiguration wifiConfiguration = null; + if (configuration != null) { + wifiConfiguration = JsonDeserializer.jsonToWifiConfig(configuration); + // Have to trim off the extra quotation marks since Soft AP logic interprets + // WifiConfiguration.SSID literally, unlike the WifiManager connection logic. + wifiConfiguration.SSID = JsonSerializer.trimQuotationMarks(wifiConfiguration.SSID); + } + boolean success; + try { + success = + (boolean) + mWifiManager + .getClass() + .getDeclaredMethod( + "setWifiApEnabled", + WifiConfiguration.class, + boolean.class) + .invoke(mWifiManager, wifiConfiguration, true); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + if (!success) { + throw new WifiManagerSnippetException("Failed to initiate turning on Wi-Fi Soft AP."); + } + if (!Utils.waitUntil(() -> wifiIsApEnabled() == true, 60)) { + throw new WifiManagerSnippetException( + "Timed out after 60s waiting for Wi-Fi Soft AP state to turn on with configuration: " + + configuration); + } + } + + /** + * Disables Wi-Fi Soft AP (hotspot). + * + *

Does not work for release N. + * + * @throws Throwable + */ + @Rpc(description = "Disable Wi-Fi Soft AP (hotspot).") + public void wifiDisableSoftAp() throws Throwable { + verifyApiVersionForSoftAp(); + boolean success; + try { + success = + (boolean) + mWifiManager + .getClass() + .getDeclaredMethod( + "setWifiApEnabled", + WifiConfiguration.class, + boolean.class) + .invoke( + mWifiManager, + null, /* No configuration needed for disabling */ + false); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + if (!success) { + throw new WifiManagerSnippetException("Failed to initiate turning off Wi-Fi Soft AP."); + } + if (!Utils.waitUntil(() -> wifiIsApEnabled() == false, 60)) { + throw new WifiManagerSnippetException( + "Timed out after 60s waiting for Wi-Fi Soft AP state to turn off."); + } + } + @Override public void shutdown() {} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/ApiVersionException.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/ApiVersionException.java new file mode 100644 index 0000000..68f2a55 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/ApiVersionException.java @@ -0,0 +1,8 @@ +package com.google.android.mobly.snippet.bundled.utils; + +/** Raised for when an Rpc call is not supported by the Android version used. */ +public class ApiVersionException extends Exception { + public ApiVersionException(String message) { + super(message); + } +} 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 index 0c1b7a3..45af4a4 100644 --- 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 @@ -55,7 +55,7 @@ public class JsonSerializer { * * @param originalString */ - private static String trimQuotationMarks(String originalString) { + public static String trimQuotationMarks(String originalString) { String result = originalString; if (originalString.charAt(0) == '"' && originalString.charAt(originalString.length() - 1) == '"') { 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 index 00bfbd2..0ea5313 100644 --- 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 @@ -25,21 +25,26 @@ public final class Utils { *

This is often used to wait for asynchronous operations to finish and the system to reach a * desired state. * + *

If the predicate function throws an exception and interrupts the waiting, the exception + * will be wrapped in an {@link RuntimeException}. + * * @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 { + public static boolean waitUntil(Utils.Predicate predicate, int timeout) { timeout *= 10; - while (!predicate.waitCondition() && timeout >= 0) { - Thread.sleep(100); - timeout -= 1; - } - if (predicate.waitCondition()) { - return true; + try { + while (!predicate.waitCondition() && timeout >= 0) { + Thread.sleep(100); + timeout -= 1; + } + if (predicate.waitCondition()) { + return true; + } + } catch (Throwable e) { + throw new RuntimeException(e); } return false; } @@ -49,6 +54,6 @@ public final class Utils { * going on. */ public interface Predicate { - boolean waitCondition(); + boolean waitCondition() throws Throwable; } } -- cgit v1.2.3 From dcae801dceef228c4c9be81f5ba61660ce90369f Mon Sep 17 00:00:00 2001 From: Ang Li Date: Thu, 27 Apr 2017 16:03:45 -0700 Subject: Fixes in `serializeBluetoothDevice`. (#34) --- .../mobly/snippet/bundled/AccountSnippet.java | 1 + .../mobly/snippet/bundled/WifiManagerSnippet.java | 1 + .../snippet/bundled/utils/JsonSerializer.java | 63 +++++++++++----------- 3 files changed, 34 insertions(+), 31 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index dc1fac1..627d1b4 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -54,6 +54,7 @@ public class AccountSnippet implements Snippet { private static class AccountSnippetException extends Exception { private static final long serialVersionUID = 1; + public AccountSnippetException(String msg) { super(msg); } 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 index ba9569c..ea75b5a 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -43,6 +43,7 @@ import org.json.JSONObject; public class WifiManagerSnippet implements Snippet { private static class WifiManagerSnippetException extends Exception { private static final long serialVersionUID = 1; + public WifiManagerSnippetException(String msg) { super(msg); } 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 index 45af4a4..fc7d17b 100644 --- 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 @@ -21,6 +21,7 @@ import android.net.DhcpInfo; import android.net.wifi.SupplicantState; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; +import android.os.Build; import android.os.ParcelUuid; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -28,7 +29,6 @@ import java.lang.reflect.Modifier; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -127,42 +127,43 @@ public class JsonSerializer { private JSONObject serializeBluetoothDevice(BluetoothDevice data) throws JSONException { JSONObject result = new JSONObject(); result.put("Address", data.getAddress()); + final String bondStateFieldName = "BondState"; switch (data.getBondState()) { - case 10: - result.put("BondState", "None"); + case BluetoothDevice.BOND_NONE: + result.put(bondStateFieldName, "BOND_NONE"); break; - case 11: - result.put("BondState", "Bonding"); + case BluetoothDevice.BOND_BONDING: + result.put(bondStateFieldName, "BOND_BONDING"); break; - case 12: - result.put("BondState", "Bonded"); - } - if (data.getName() == null) { - result.put("Name", "Null"); - } else { - result.put("Name", data.getName()); - } - switch (data.getType()) { - case 0: - result.put("Type", "Unknown"); - break; - case 1: - result.put("Type", "Classic"); + case BluetoothDevice.BOND_BONDED: + result.put(bondStateFieldName, "BOND_BONDED"); break; - case 2: - result.put("Type", "Le"); - break; - case 3: - result.put("Type", "Dual"); } - if (data.getUuids() == null) { - result.put("UUIDs", "Null"); - } else { - ArrayList uuids = new ArrayList(); - for (ParcelUuid uuid : data.getUuids()) { - uuids.add(uuid.toString()); + result.put("Name", data.getName()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + final String deviceTypeFieldName = "DeviceType"; + switch (data.getType()) { + case BluetoothDevice.DEVICE_TYPE_CLASSIC: + result.put(deviceTypeFieldName, "DEVICE_TYPE_CLASSIC"); + break; + case BluetoothDevice.DEVICE_TYPE_LE: + result.put(deviceTypeFieldName, "DEVICE_TYPE_LE"); + break; + case BluetoothDevice.DEVICE_TYPE_DUAL: + result.put(deviceTypeFieldName, "DEVICE_TYPE_DUAL"); + break; + case BluetoothDevice.DEVICE_TYPE_UNKNOWN: + result.put(deviceTypeFieldName, "DEVICE_TYPE_UNKNOWN"); + break; + } + ParcelUuid[] parcelUuids = data.getUuids(); + if (parcelUuids != null) { + ArrayList uuidStrings = new ArrayList<>(parcelUuids.length); + for (ParcelUuid parcelUuid : parcelUuids) { + uuidStrings.add(parcelUuid.getUuid().toString()); + } + result.put("UUIDs", uuidStrings); } - result.put("UUIDs", new JSONArray(uuids)); } return result; } -- cgit v1.2.3 From 6f3d76cabe9d284f1400e7ee2657b7406b8bf779 Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Mon, 1 May 2017 19:58:54 -0700 Subject: Throw exceptions in case of failure to enable or disable HCI snoop log. (#42) Returning boolean is not consistent with our style and is not done for any other method in MBS. --- .../snippet/bundled/BluetoothAdapterSnippet.java | 35 ++++++++++++++-------- 1 file changed, 22 insertions(+), 13 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 90ba707..a6e6dbb 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 @@ -133,35 +133,44 @@ public class BluetoothAdapterSnippet implements Snippet { * *

The file can be pulled from `/sdcard/btsnoop_hci.log`. * - * @return false if enabling the snoop log failed, true otherwise. * @throws Throwable */ @RpcMinSdk(Build.VERSION_CODES.KITKAT) @Rpc(description = "Enable Bluetooth HCI snoop log for debugging.") - public boolean btEnableHciSnoopLog() throws Throwable { + public void btEnableHciSnoopLog() throws Throwable { + boolean success; try { - return (boolean) - mBluetoothAdapter - .getClass() - .getDeclaredMethod("configHciSnoopLog", boolean.class) - .invoke(mBluetoothAdapter, true); + success = + (boolean) + mBluetoothAdapter + .getClass() + .getDeclaredMethod("configHciSnoopLog", boolean.class) + .invoke(mBluetoothAdapter, true); } catch (InvocationTargetException e) { throw e.getCause(); } + if (!success) { + throw new BluetoothAdapterSnippetException("Failed to enable HCI snoop log."); + } } @RpcMinSdk(Build.VERSION_CODES.KITKAT) @Rpc(description = "Disable Bluetooth HCI snoop log.") - public boolean btDisableHciSnoopLog() throws Throwable { + public void btDisableHciSnoopLog() throws Throwable { + boolean success; try { - return (boolean) - mBluetoothAdapter - .getClass() - .getDeclaredMethod("configHciSnoopLog", boolean.class) - .invoke(mBluetoothAdapter, false); + success = + (boolean) + mBluetoothAdapter + .getClass() + .getDeclaredMethod("configHciSnoopLog", boolean.class) + .invoke(mBluetoothAdapter, false); } catch (InvocationTargetException e) { throw e.getCause(); } + if (!success) { + throw new BluetoothAdapterSnippetException("Failed to disable HCI snoop log."); + } } @Override -- cgit v1.2.3 From 5858a6ece2d8368f6074cd05759490ed008bd4e9 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Mon, 1 May 2017 21:05:41 -0700 Subject: Add Bluetooth Rpcs needed to support making the device discoverable. (#41) * Add Bluetooth Rpcs needed to support making the device discoverable. * Fix a bug in serializer; fix a typo in an Rpc name. Related to #8 --- .../snippet/bundled/BluetoothAdapterSnippet.java | 70 +++++++++++++++++++++- .../snippet/bundled/utils/JsonSerializer.java | 32 ++++++++-- 2 files changed, 97 insertions(+), 5 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 a6e6dbb..7a0a15e 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 @@ -75,6 +75,11 @@ public class BluetoothAdapterSnippet implements Snippet { } } + @Rpc(description = "Return true if Bluetooth is enabled, false otherwise.") + public boolean btIsEnabled() { + return mBluetoothAdapter.isEnabled(); + } + @Rpc( description = "Get bluetooth discovery results, which is a list of serialized BluetoothDevice objects." @@ -87,12 +92,29 @@ public class BluetoothAdapterSnippet implements Snippet { return results; } + @Rpc(description = "Set the friendly Bluetooth name of the local Bluetooth adapter.") + public void btSetName(String name) throws BluetoothAdapterSnippetException { + if (!btIsEnabled()) { + throw new BluetoothAdapterSnippetException( + "Bluetooth is not enabled, cannot set Bluetooth name."); + } + if (!mBluetoothAdapter.setName(name)) { + throw new BluetoothAdapterSnippetException( + "Failed to set local Bluetooth name to " + name); + } + } + + @Rpc(description = "Get the friendly Bluetooth name of the local Bluetooth adapter.") + public String btGetName() { + return mBluetoothAdapter.getName(); + } + @Rpc( description = "Start discovery, wait for discovery to complete, and return results, which is a list of " + "serialized BluetoothDevice objects." ) - public JSONArray btDiscoveryAndGetResults() + public JSONArray btDiscoverAndGetResults() throws InterruptedException, JSONException, BluetoothAdapterSnippetException { IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); @@ -118,6 +140,52 @@ public class BluetoothAdapterSnippet implements Snippet { return btGetCachedScanResults(); } + @Rpc(description = "Become discoverable in Bluetooth.") + public void btBecomeDiscoverable(Integer duration) throws Throwable { + if (!btIsEnabled()) { + throw new BluetoothAdapterSnippetException( + "Bluetooth is not enabled, cannot become discoverable."); + } + boolean success; + try { + success = + (boolean) + mBluetoothAdapter + .getClass() + .getDeclaredMethod("setScanMode", int.class, int.class) + .invoke( + mBluetoothAdapter, + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, + duration); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + if (!success) { + throw new BluetoothAdapterSnippetException("Failed to become discoverable."); + } + } + + @Rpc(description = "Stop being discoverable in Bluetooth.") + public void btStopBeingDiscoverable() throws Throwable { + boolean success; + try { + success = + (boolean) + mBluetoothAdapter + .getClass() + .getDeclaredMethod("setScanMode", int.class, int.class) + .invoke( + mBluetoothAdapter, + BluetoothAdapter.SCAN_MODE_NONE, + 0 /* duration is not used for this */); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + if (!success) { + throw new BluetoothAdapterSnippetException("Failed to stop being discoverable."); + } + } + @Rpc(description = "Get the list of paired bluetooth devices.") public JSONArray btGetPairedDevices() throws BluetoothAdapterSnippetException, InterruptedException, JSONException { 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 index fc7d17b..bd00f45 100644 --- 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 @@ -109,13 +109,13 @@ public class JsonSerializer { 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)); + guaranteedPut(result, "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())); + guaranteedPut(result, "SSID", trimQuotationMarks(data.getSSID())); for (SupplicantState state : SupplicantState.values()) { if (data.getSupplicantState().equals(state)) { result.put("SupplicantState", state.name()); @@ -126,7 +126,7 @@ public class JsonSerializer { private JSONObject serializeBluetoothDevice(BluetoothDevice data) throws JSONException { JSONObject result = new JSONObject(); - result.put("Address", data.getAddress()); + guaranteedPut(result, "Address", data.getAddress()); final String bondStateFieldName = "BondState"; switch (data.getBondState()) { case BluetoothDevice.BOND_NONE: @@ -139,7 +139,7 @@ public class JsonSerializer { result.put(bondStateFieldName, "BOND_BONDED"); break; } - result.put("Name", data.getName()); + guaranteedPut(result, "Name", data.getName()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { final String deviceTypeFieldName = "DeviceType"; switch (data.getType()) { @@ -167,4 +167,28 @@ public class JsonSerializer { } return result; } + + /** + * Guarantees a field is put into a JSONObject even if it's null. + * + *

By default, if the object of {@link JSONObject#put(String, Object)} is null, the `put` + * method would either remove the field or do nothing, causing serialized objects to have + * inconsistent fields. + * + *

Use this method to put objects that may be null into the serialized JSONObject so the + * serialized objects have a consistent set of critical fields, like the SSID field in + * serialized WifiConfiguration objects. + * + * @param data + * @param name + * @param object + * @throws JSONException + */ + private void guaranteedPut(JSONObject data, String name, Object object) throws JSONException { + if (object == null) { + data.put(name, JSONObject.NULL); + } else { + data.put(name, object); + } + } } -- cgit v1.2.3 From fec0d88ea2860ef5b595d6f04481cec963350a1d Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Tue, 2 May 2017 18:54:35 -0700 Subject: Fix issues related to WifiManagerSnippet. (#44) * Removed version code check; it was incorrect and doesn't seem to be necessary. * Missing permission needed to be able to call setWifiApEnabled. --- .../mobly/snippet/bundled/WifiManagerSnippet.java | 23 +--------------------- .../snippet/bundled/utils/ApiVersionException.java | 8 -------- 2 files changed, 1 insertion(+), 30 deletions(-) delete mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/utils/ApiVersionException.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index ea75b5a..bc57ab0 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -23,11 +23,9 @@ import android.content.IntentFilter; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; -import android.os.Build; 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.ApiVersionException; 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; @@ -51,7 +49,6 @@ public class WifiManagerSnippet implements Snippet { private final WifiManager mWifiManager; private final Context mContext; - private static final String TAG = "WifiManagerSnippet"; private final JsonSerializer mJsonSerializer = new JsonSerializer(); private volatile boolean mIsScanResultAvailable = false; @@ -244,16 +241,8 @@ public class WifiManagerSnippet implements Snippet { return mJsonSerializer.toJson(mWifiManager.getDhcpInfo()); } - private void verifyApiVersionForSoftAp() throws ApiVersionException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - throw new ApiVersionException( - "Soft AP APIs are not supported in Android versions >= N."); - } - } - @Rpc(description = "Check whether Wi-Fi Soft AP (hotspot) is enabled.") public boolean wifiIsApEnabled() throws Throwable { - verifyApiVersionForSoftAp(); try { return (boolean) mWifiManager @@ -268,14 +257,11 @@ public class WifiManagerSnippet implements Snippet { /** * Enable Wi-Fi Soft AP (hotspot). * - *

Does not work for release N. - * * @param configuration The same format as the param wifiNetworkConfig param for wifiConnect. * @throws Throwable */ @Rpc(description = "Enable Wi-Fi Soft AP (hotspot).") public void wifiEnableSoftAp(@Nullable JSONObject configuration) throws Throwable { - verifyApiVersionForSoftAp(); // If no configuration is provided, the existing configuration would be used. WifiConfiguration wifiConfiguration = null; if (configuration != null) { @@ -308,16 +294,9 @@ public class WifiManagerSnippet implements Snippet { } } - /** - * Disables Wi-Fi Soft AP (hotspot). - * - *

Does not work for release N. - * - * @throws Throwable - */ + /** Disables Wi-Fi Soft AP (hotspot). */ @Rpc(description = "Disable Wi-Fi Soft AP (hotspot).") public void wifiDisableSoftAp() throws Throwable { - verifyApiVersionForSoftAp(); boolean success; try { success = diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/ApiVersionException.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/ApiVersionException.java deleted file mode 100644 index 68f2a55..0000000 --- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/ApiVersionException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.google.android.mobly.snippet.bundled.utils; - -/** Raised for when an Rpc call is not supported by the Android version used. */ -public class ApiVersionException extends Exception { - public ApiVersionException(String message) { - super(message); - } -} -- cgit v1.2.3 From 796a97541ca681478b707c0b9e535e0d201496ac Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Tue, 2 May 2017 19:34:59 -0700 Subject: Simplify API required to call methods by reflection. (#45) * Implement a cleaner way to call methods by reflection. * Port all callers to the new reflection API. --- .../mobly/snippet/bundled/AudioSnippet.java | 2 +- .../snippet/bundled/BluetoothAdapterSnippet.java | 69 ++++------------- .../mobly/snippet/bundled/WifiManagerSnippet.java | 53 +++---------- .../android/mobly/snippet/bundled/utils/Utils.java | 90 ++++++++++++++++++++++ 4 files changed, 115 insertions(+), 99 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java index 1ca96d0..4f6b0ae 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -78,7 +78,7 @@ public class AudioSnippet implements Snippet { * calling muteAll will throw. */ Class audioSystem = Class.forName("android.media.AudioSystem"); Method getNumStreamTypes = audioSystem.getDeclaredMethod("getNumStreamTypes"); - int numStreams = (int) getNumStreamTypes.invoke(null); + int numStreams = (int) getNumStreamTypes.invoke(null /* instance */); for (int i = 0; i < numStreams; i++) { mAudioManager.setStreamVolume(i /* audio stream */, 0 /* value */, 0 /* flags */); } 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 7a0a15e..6946216 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 @@ -29,7 +29,6 @@ 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.rpc.RpcMinSdk; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import org.json.JSONArray; import org.json.JSONException; @@ -146,42 +145,24 @@ public class BluetoothAdapterSnippet implements Snippet { throw new BluetoothAdapterSnippetException( "Bluetooth is not enabled, cannot become discoverable."); } - boolean success; - try { - success = - (boolean) - mBluetoothAdapter - .getClass() - .getDeclaredMethod("setScanMode", int.class, int.class) - .invoke( - mBluetoothAdapter, - BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, - duration); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, + duration)) { throw new BluetoothAdapterSnippetException("Failed to become discoverable."); } } @Rpc(description = "Stop being discoverable in Bluetooth.") public void btStopBeingDiscoverable() throws Throwable { - boolean success; - try { - success = - (boolean) - mBluetoothAdapter - .getClass() - .getDeclaredMethod("setScanMode", int.class, int.class) - .invoke( - mBluetoothAdapter, - BluetoothAdapter.SCAN_MODE_NONE, - 0 /* duration is not used for this */); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_NONE, + 0 /* duration is not used for this */)) { throw new BluetoothAdapterSnippetException("Failed to stop being discoverable."); } } @@ -206,18 +187,7 @@ public class BluetoothAdapterSnippet implements Snippet { @RpcMinSdk(Build.VERSION_CODES.KITKAT) @Rpc(description = "Enable Bluetooth HCI snoop log for debugging.") public void btEnableHciSnoopLog() throws Throwable { - boolean success; - try { - success = - (boolean) - mBluetoothAdapter - .getClass() - .getDeclaredMethod("configHciSnoopLog", boolean.class) - .invoke(mBluetoothAdapter, true); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", true)) { throw new BluetoothAdapterSnippetException("Failed to enable HCI snoop log."); } } @@ -225,18 +195,7 @@ public class BluetoothAdapterSnippet implements Snippet { @RpcMinSdk(Build.VERSION_CODES.KITKAT) @Rpc(description = "Disable Bluetooth HCI snoop log.") public void btDisableHciSnoopLog() throws Throwable { - boolean success; - try { - success = - (boolean) - mBluetoothAdapter - .getClass() - .getDeclaredMethod("configHciSnoopLog", boolean.class) - .invoke(mBluetoothAdapter, false); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", false)) { throw new BluetoothAdapterSnippetException("Failed to disable HCI snoop log."); } } 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 index bc57ab0..f2ac9eb 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -31,7 +31,6 @@ 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.lang.reflect.InvocationTargetException; import java.util.ArrayList; import org.json.JSONArray; import org.json.JSONException; @@ -243,15 +242,7 @@ public class WifiManagerSnippet implements Snippet { @Rpc(description = "Check whether Wi-Fi Soft AP (hotspot) is enabled.") public boolean wifiIsApEnabled() throws Throwable { - try { - return (boolean) - mWifiManager - .getClass() - .getDeclaredMethod("isWifiApEnabled") - .invoke(mWifiManager); - } catch (InvocationTargetException e) { - throw e.getCause(); - } + return (boolean) Utils.invokeByReflection(mWifiManager, "isWifiApEnabled"); } /** @@ -270,21 +261,9 @@ public class WifiManagerSnippet implements Snippet { // WifiConfiguration.SSID literally, unlike the WifiManager connection logic. wifiConfiguration.SSID = JsonSerializer.trimQuotationMarks(wifiConfiguration.SSID); } - boolean success; - try { - success = - (boolean) - mWifiManager - .getClass() - .getDeclaredMethod( - "setWifiApEnabled", - WifiConfiguration.class, - boolean.class) - .invoke(mWifiManager, wifiConfiguration, true); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) + Utils.invokeByReflection( + mWifiManager, "setWifiApEnabled", wifiConfiguration, true)) { throw new WifiManagerSnippetException("Failed to initiate turning on Wi-Fi Soft AP."); } if (!Utils.waitUntil(() -> wifiIsApEnabled() == true, 60)) { @@ -297,24 +276,12 @@ public class WifiManagerSnippet implements Snippet { /** Disables Wi-Fi Soft AP (hotspot). */ @Rpc(description = "Disable Wi-Fi Soft AP (hotspot).") public void wifiDisableSoftAp() throws Throwable { - boolean success; - try { - success = - (boolean) - mWifiManager - .getClass() - .getDeclaredMethod( - "setWifiApEnabled", - WifiConfiguration.class, - boolean.class) - .invoke( - mWifiManager, - null, /* No configuration needed for disabling */ - false); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - if (!success) { + if (!(boolean) + Utils.invokeByReflection( + mWifiManager, + "setWifiApEnabled", + null /* No configuration needed for disabling */, + false)) { throw new WifiManagerSnippetException("Failed to initiate turning off Wi-Fi Soft AP."); } if (!Utils.waitUntil(() -> wifiIsApEnabled() == false, 60)) { 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 index 0ea5313..8736d85 100644 --- 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 @@ -16,6 +16,11 @@ package com.google.android.mobly.snippet.bundled.utils; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + public final class Utils { private Utils() {} @@ -56,4 +61,89 @@ public final class Utils { public interface Predicate { boolean waitCondition() throws Throwable; } + + /** + * Simplified API to invoke an instance method by reflection. + * + *

Sample usage: + * + *

+     *   boolean result = (boolean) Utils.invokeByReflection(
+     *           mWifiManager,
+     *           "setWifiApEnabled", null /* wifiConfiguration * /, true /* enabled * /);
+     * 
+ * + * @param instance Instance of object defining the method to call. + * @param methodName Name of the method to call. Can be inherited. + * @param args Variadic array of arguments to supply to the method. Their types will be used to + * locate a suitable method to call. Subtypes, primitive types, boxed types, and {@code + * null} arguments are properly handled. + * @return The return value of the method, or {@code null} if no return value. + * @throws NoSuchMethodException If no suitable method could be found. + * @throws Throwable The exception raised by the method, if any. + */ + public static Object invokeByReflection(Object instance, String methodName, Object... args) + throws Throwable { + // Java doesn't know if invokeByReflection(instance, name, null) means that the array is + // null or that it's a non-null array containing a single null element. We mean the latter. + // Silly Java. + if (args == null) { + args = new Object[] {null}; + } + // Can't use Class#getMethod(Class...) because it expects that the passed in classes + // exactly match the parameters of the method, and doesn't handle superclasses. + Method method = null; + METHOD_SEARCHER: + for (Method candidateMethod : instance.getClass().getMethods()) { + // getMethods() returns only public methods, so we don't need to worry about checking + // whether the method is accessible. + if (!candidateMethod.getName().equals(methodName)) { + continue; + } + Class[] declaredParams = candidateMethod.getParameterTypes(); + if (declaredParams.length != args.length) { + continue; + } + for (int i = 0; i < declaredParams.length; i++) { + if (args[i] == null) { + // Null is assignable to anything except primitives. + if (declaredParams[i].isPrimitive()) { + continue METHOD_SEARCHER; + } + } else { + // Allow autoboxing during reflection by wrapping primitives. + Class declaredClass = Primitives.wrap(declaredParams[i]); + Class actualClass = Primitives.wrap(args[i].getClass()); + TypeToken declaredParamType = TypeToken.of(declaredClass); + TypeToken actualParamType = TypeToken.of(actualClass); + if (!declaredParamType.isSupertypeOf(actualParamType)) { + continue METHOD_SEARCHER; + } + } + } + method = candidateMethod; + break; + } + if (method == null) { + StringBuilder methodString = + new StringBuilder(instance.getClass().getName()) + .append('#') + .append(methodName) + .append('('); + for (int i = 0; i < args.length - 1; i++) { + methodString.append(args[i].getClass().getSimpleName()).append(", "); + } + if (args.length > 0) { + methodString.append(args[args.length - 1].getClass().getSimpleName()); + } + methodString.append(')'); + throw new NoSuchMethodException(methodString.toString()); + } + try { + Object result = method.invoke(instance, args); + return result; + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } } -- cgit v1.2.3 From 119881856e36e8f6e5f98977e99f141d3e7071d4 Mon Sep 17 00:00:00 2001 From: whutchin5 Date: Wed, 3 May 2017 14:43:19 -0700 Subject: Added Rpc to enable/disable wifi-verbose logging on API 21+ (#46) --- .../google/android/mobly/snippet/bundled/WifiManagerSnippet.java | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index f2ac9eb..5c25bc6 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -23,6 +23,7 @@ import android.content.IntentFilter; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; +import android.os.Build; import android.support.annotation.Nullable; import android.support.test.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; @@ -30,6 +31,7 @@ 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.rpc.RpcMinSdk; import com.google.android.mobly.snippet.util.Log; import java.util.ArrayList; import org.json.JSONArray; @@ -222,6 +224,12 @@ public class WifiManagerSnippet implements Snippet { return networks; } + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP) + @Rpc(description = "Enable or disable wifi verbose logging.") + public void wifiSetVerboseLogging(boolean enable) throws Throwable { + Utils.invokeByReflection(mWifiManager, "enableVerboseLogging", enable ? 1 : 0); + } + @Rpc( description = "Get the information about the active Wi-Fi connection, which is a serialized " -- cgit v1.2.3 From a9afffd1211e1f1147a66a420bf28a1e309b9a95 Mon Sep 17 00:00:00 2001 From: chok Date: Mon, 15 May 2017 14:54:50 -0700 Subject: Increasing bt discovery timeout from 1 to 2 minutes for btDiscoverAndGetResults() (#49) --- .../google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 6946216..4b8e4f1 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 @@ -129,9 +129,9 @@ public class BluetoothAdapterSnippet implements Snippet { throw new BluetoothAdapterSnippetException( "Failed to initiate Bluetooth Discovery."); } - if (!Utils.waitUntil(() -> mIsScanResultAvailable, 60)) { + if (!Utils.waitUntil(() -> mIsScanResultAvailable, 120)) { throw new BluetoothAdapterSnippetException( - "Failed to get discovery results after 1 min, timeout!"); + "Failed to get discovery results after 2 mins, timeout!"); } } finally { mContext.unregisterReceiver(receiver); -- cgit v1.2.3 From e9c9480e0e44e223e302d61f3905e1e3aed7bd2d Mon Sep 17 00:00:00 2001 From: chok Date: Tue, 16 May 2017 17:18:28 -0700 Subject: Adding BT RPC getAddress (#50) --- .../android/mobly/snippet/bundled/BluetoothAdapterSnippet.java | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 4b8e4f1..67df97b 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 @@ -108,6 +108,11 @@ public class BluetoothAdapterSnippet implements Snippet { return mBluetoothAdapter.getName(); } + @Rpc(description = "Returns the hardware address of the local Bluetooth adapter.") + public String btGetAddress() { + return mBluetoothAdapter.getAddress(); + } + @Rpc( description = "Start discovery, wait for discovery to complete, and return results, which is a list of " -- cgit v1.2.3 From 8a7fe9abd925d8ee471424fcd901a4767aaabf90 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Tue, 23 May 2017 16:24:46 -0700 Subject: Add basic Bluetooth LE advertising and scan support. (#47) * Add basic Bluetooth LE advertising and scan support. * Introduce `RpcEnum`, a standard way to handle String-int enum conversion. --- .../snippet/bundled/BluetoothAdapterSnippet.java | 7 +- .../bundled/BluetoothLeAdvertiserSnippet.java | 178 +++++++++++++++++++++ .../snippet/bundled/BluetoothLeScannerSnippet.java | 138 ++++++++++++++++ .../snippet/bundled/utils/JsonDeserializer.java | 61 +++++++ .../snippet/bundled/utils/JsonSerializer.java | 110 ++++++------- .../mobly/snippet/bundled/utils/MbsEnums.java | 91 +++++++++++ .../mobly/snippet/bundled/utils/RpcEnum.java | 89 +++++++++++ .../android/mobly/snippet/bundled/utils/Utils.java | 1 + 8 files changed, 618 insertions(+), 57 deletions(-) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 67df97b..0f4d3e6 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 @@ -23,6 +23,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; +import android.os.Bundle; import android.support.test.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; @@ -173,11 +174,11 @@ public class BluetoothAdapterSnippet implements Snippet { } @Rpc(description = "Get the list of paired bluetooth devices.") - public JSONArray btGetPairedDevices() + public ArrayList btGetPairedDevices() throws BluetoothAdapterSnippetException, InterruptedException, JSONException { - JSONArray pairedDevices = new JSONArray(); + ArrayList pairedDevices = new ArrayList<>(); for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) { - pairedDevices.put(mJsonSerializer.toJson(device)); + pairedDevices.add(mJsonSerializer.serializeBluetoothDevice(device)); } return pairedDevices; } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java new file mode 100644 index 0000000..e161a5b --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java @@ -0,0 +1,178 @@ +/* + * 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.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.le.AdvertiseCallback; +import android.bluetooth.le.AdvertiseData; +import android.bluetooth.le.AdvertiseSettings; +import android.bluetooth.le.BluetoothLeAdvertiser; +import android.os.Build; +import android.os.Bundle; +import android.os.ParcelUuid; +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.RpcEnum; +import com.google.android.mobly.snippet.event.EventCache; +import com.google.android.mobly.snippet.event.SnippetEvent; +import com.google.android.mobly.snippet.rpc.AsyncRpc; +import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.rpc.RpcMinSdk; +import com.google.android.mobly.snippet.util.Log; +import java.util.HashMap; +import org.json.JSONException; +import org.json.JSONObject; + +/** Snippet class exposing Android APIs in WifiManager. */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) +public class BluetoothLeAdvertiserSnippet implements Snippet { + private static class BluetoothLeAdvertiserSnippetException extends Exception { + private static final long serialVersionUID = 1; + + public BluetoothLeAdvertiserSnippetException(String msg) { + super(msg); + } + } + + private final BluetoothLeAdvertiser mAdvertiser; + private static final EventCache sEventCache = EventCache.getInstance(); + + private final HashMap mAdvertiseCallbacks = new HashMap<>(); + + public BluetoothLeAdvertiserSnippet() { + mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser(); + } + + /** + * Start Bluetooth LE advertising. + * + *

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

+     *          {
+     *            "AdvertiseMode": "ADVERTISE_MODE_BALANCED",
+     *            "Timeout": (int, milliseconds),
+     *            "Connectable": (bool),
+     *            "TxPowerLevel": "ADVERTISE_TX_POWER_LOW"
+     *          }
+     *     
+ * + * @param advertiseData A JSONObject representing a {@link AdvertiseData} object. E.g. + *
+     *          {
+     *            "IncludeDeviceName": (bool),
+     *            # JSON list, each element representing a set of service data, which is composed of
+     *            # a UUID, and an optional string.
+     *            "ServiceData": [
+     *                      {
+     *                        "UUID": (A string representation of {@link ParcelUuid}),
+     *                        "Data": (Optional, The string representation of what you want to
+     *                                 advertise, base64 encoded)
+     *                        # If you want to add a UUID without data, simply omit the "Data"
+     *                        # field.
+     *                      }
+     *                ]
+     *          }
+     *     
+ * + * @throws BluetoothLeAdvertiserSnippetException + * @throws JSONException + */ + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) + @AsyncRpc(description = "Start BLE advertising.") + public void bleStartAdvertising( + String callbackId, JSONObject advertiseSettings, JSONObject advertiseData) + throws BluetoothLeAdvertiserSnippetException, JSONException { + if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { + throw new BluetoothLeAdvertiserSnippetException( + "Bluetooth is disabled, cannot start BLE advertising."); + } + AdvertiseSettings settings = JsonDeserializer.jsonToBleAdvertiseSettings(advertiseSettings); + AdvertiseData data = JsonDeserializer.jsonToBleAdvertiseData(advertiseData); + AdvertiseCallback advertiseCallback = new DefaultAdvertiseCallback(callbackId); + mAdvertiser.startAdvertising(settings, data, advertiseCallback); + mAdvertiseCallbacks.put(callbackId, advertiseCallback); + } + + /** + * Stop a BLE advertising. + * + * @param callbackId The callbackId corresponding to the {@link + * BluetoothLeAdvertiserSnippet#bleStartAdvertising} call that started the advertising. + * @throws BluetoothLeScannerSnippet.BluetoothLeScanSnippetException + */ + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) + @Rpc(description = "Stop BLE advertising.") + public void bleStopAdvertising(String callbackId) throws BluetoothLeAdvertiserSnippetException { + AdvertiseCallback callback = mAdvertiseCallbacks.remove(callbackId); + if (callback == null) { + throw new BluetoothLeAdvertiserSnippetException( + "No advertising session found for ID " + callbackId); + } + mAdvertiser.stopAdvertising(callback); + } + + private static class DefaultAdvertiseCallback extends AdvertiseCallback { + private final String mCallbackId; + public static RpcEnum ADVERTISE_FAILURE_ERROR_CODE = + new RpcEnum.Builder() + .add("ADVERTISE_FAILED_ALREADY_STARTED", ADVERTISE_FAILED_ALREADY_STARTED) + .add("ADVERTISE_FAILED_DATA_TOO_LARGE", ADVERTISE_FAILED_DATA_TOO_LARGE) + .add( + "ADVERTISE_FAILED_FEATURE_UNSUPPORTED", + ADVERTISE_FAILED_FEATURE_UNSUPPORTED) + .add("ADVERTISE_FAILED_INTERNAL_ERROR", ADVERTISE_FAILED_INTERNAL_ERROR) + .add( + "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS", + ADVERTISE_FAILED_TOO_MANY_ADVERTISERS) + .build(); + + public DefaultAdvertiseCallback(String callbackId) { + mCallbackId = callbackId; + } + + public void onStartSuccess(AdvertiseSettings settingsInEffect) { + Log.e("Bluetooth LE advertising started with settings: " + settingsInEffect.toString()); + SnippetEvent event = new SnippetEvent(mCallbackId, "onStartSuccess"); + Bundle advertiseSettings = + JsonSerializer.serializeBleAdvertisingSettings(settingsInEffect); + event.getData().putBundle("SettingsInEffect", advertiseSettings); + sEventCache.postEvent(event); + } + + public void onStartFailure(int errorCode) { + Log.e("Bluetooth LE advertising failed to start with error code: " + errorCode); + SnippetEvent event = new SnippetEvent(mCallbackId, "onStartFailure"); + final String errorCodeString = ADVERTISE_FAILURE_ERROR_CODE.getString(errorCode); + event.getData().putString("ErrorCode", errorCodeString); + sEventCache.postEvent(event); + } + } + + @Override + public void shutdown() { + for (AdvertiseCallback callback : mAdvertiseCallbacks.values()) { + mAdvertiser.stopAdvertising(callback); + } + mAdvertiseCallbacks.clear(); + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java new file mode 100644 index 0000000..7e133d1 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java @@ -0,0 +1,138 @@ +/* + * 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.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanResult; +import android.os.Build; +import android.os.Bundle; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; +import com.google.android.mobly.snippet.bundled.utils.MbsEnums; +import com.google.android.mobly.snippet.event.EventCache; +import com.google.android.mobly.snippet.event.SnippetEvent; +import com.google.android.mobly.snippet.rpc.AsyncRpc; +import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.rpc.RpcMinSdk; +import com.google.android.mobly.snippet.util.Log; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** Snippet class exposing Android APIs in WifiManager. */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) +public class BluetoothLeScannerSnippet implements Snippet { + private static class BluetoothLeScanSnippetException extends Exception { + private static final long serialVersionUID = 1; + + public BluetoothLeScanSnippetException(String msg) { + super(msg); + } + } + + private final BluetoothLeScanner mScanner; + private final EventCache mEventCache = EventCache.getInstance(); + private final HashMap mScanCallbacks = new HashMap<>(); + private final JsonSerializer mJsonSerializer = new JsonSerializer(); + + public BluetoothLeScannerSnippet() { + mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner(); + } + + /** + * Start a BLE scan. + * + * @param callbackId + * @throws BluetoothLeScanSnippetException + */ + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) + @AsyncRpc(description = "Start BLE scan.") + public void bleStartScan(String callbackId) throws BluetoothLeScanSnippetException { + if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { + throw new BluetoothLeScanSnippetException( + "Bluetooth is disabled, cannot start BLE scan."); + } + DefaultScanCallback callback = new DefaultScanCallback(callbackId); + mScanner.startScan(callback); + mScanCallbacks.put(callbackId, callback); + } + + /** + * Stop a BLE scan. + * + * @param callbackId The callbackId corresponding to the {@link + * BluetoothLeScannerSnippet#bleStartScan} call that started the scan. + * @throws BluetoothLeScanSnippetException + */ + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) + @Rpc(description = "Stop a BLE scan.") + public void bleStopScan(String callbackId) throws BluetoothLeScanSnippetException { + ScanCallback callback = mScanCallbacks.remove(callbackId); + if (callback == null) { + throw new BluetoothLeScanSnippetException("No ongoing scan with ID: " + callbackId); + } + mScanner.stopScan(callback); + } + + @Override + public void shutdown() { + for (ScanCallback callback : mScanCallbacks.values()) { + mScanner.stopScan(callback); + } + mScanCallbacks.clear(); + } + + private class DefaultScanCallback extends ScanCallback { + private final String mCallbackId; + + public DefaultScanCallback(String callbackId) { + mCallbackId = callbackId; + } + + public void onScanResult(int callbackType, ScanResult result) { + Log.i("Got Bluetooth LE scan result."); + SnippetEvent event = new SnippetEvent(mCallbackId, "onScanResult"); + String callbackTypeString = + MbsEnums.BLE_SCAN_RESULT_CALLBACK_TYPE.getString(callbackType); + event.getData().putString("CallbackType", callbackTypeString); + event.getData().putBundle("result", mJsonSerializer.serializeBleScanResult(result)); + mEventCache.postEvent(event); + } + + public void onBatchScanResults(List results) { + Log.i("Got Bluetooth LE batch scan results."); + SnippetEvent event = new SnippetEvent(mCallbackId, "onBatchScanResult"); + ArrayList resultList = new ArrayList<>(results.size()); + for (ScanResult result : results) { + resultList.add(mJsonSerializer.serializeBleScanResult(result)); + } + event.getData().putParcelableArrayList("results", resultList); + mEventCache.postEvent(event); + } + + public void onScanFailed(int errorCode) { + Log.e("Bluetooth LE scan failed with error code: " + errorCode); + SnippetEvent event = new SnippetEvent(mCallbackId, "onScanFailed"); + String errorCodeString = MbsEnums.BLE_SCAN_FAILED_ERROR_CODE.getString(errorCode); + event.getData().putString("ErrorCode", errorCodeString); + mEventCache.postEvent(event); + } + } +} 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 index 4fc6b82..2f943e0 100644 --- 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 @@ -16,7 +16,14 @@ package com.google.android.mobly.snippet.bundled.utils; +import android.annotation.TargetApi; +import android.bluetooth.le.AdvertiseData; +import android.bluetooth.le.AdvertiseSettings; import android.net.wifi.WifiConfiguration; +import android.os.Build; +import android.os.ParcelUuid; +import android.util.Base64; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -40,4 +47,58 @@ public class JsonDeserializer { } return config; } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static AdvertiseSettings jsonToBleAdvertiseSettings(JSONObject jsonObject) + throws JSONException { + AdvertiseSettings.Builder builder = new AdvertiseSettings.Builder(); + if (jsonObject.has("AdvertiseMode")) { + int mode = MbsEnums.BLE_ADVERTISE_MODE.getInt(jsonObject.getString("AdvertiseMode")); + builder.setAdvertiseMode(mode); + } + // Timeout in milliseconds. + if (jsonObject.has("Timeout")) { + builder.setTimeout(jsonObject.getInt("Timeout")); + } + if (jsonObject.has("Connectable")) { + builder.setConnectable(jsonObject.getBoolean("Connectable")); + } + if (jsonObject.has("TxPowerLevel")) { + int txPowerLevel = + MbsEnums.BLE_ADVERTISE_TX_POWER.getInt(jsonObject.getString("TxPowerLevel")); + builder.setTxPowerLevel(txPowerLevel); + } + return builder.build(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static AdvertiseData jsonToBleAdvertiseData(JSONObject jsonObject) throws JSONException { + AdvertiseData.Builder builder = new AdvertiseData.Builder(); + if (jsonObject.has("IncludeDeviceName")) { + builder.setIncludeDeviceName(jsonObject.getBoolean("IncludeDeviceName")); + } + if (jsonObject.has("IncludeTxPowerLevel")) { + builder.setIncludeTxPowerLevel(jsonObject.getBoolean("IncludeTxPowerLevel")); + } + if (jsonObject.has("ServiceData")) { + JSONArray serviceData = jsonObject.getJSONArray("ServiceData"); + for (int i = 0; i < serviceData.length(); i++) { + JSONObject dataSet = serviceData.getJSONObject(i); + ParcelUuid parcelUuid = ParcelUuid.fromString(dataSet.getString("UUID")); + builder.addServiceUuid(parcelUuid); + if (dataSet.has("Data")) { + byte[] data = Base64.decode(dataSet.getString("Data"), Base64.DEFAULT); + builder.addServiceData(parcelUuid, data); + } + } + } + if (jsonObject.has("ManufacturerData")) { + JSONObject manufacturerData = jsonObject.getJSONObject("ManufacturerData"); + int manufacturerId = manufacturerData.getInt("ManufacturerId"); + byte[] manufacturerSpecificData = + Base64.decode(jsonObject.getString("ManufacturerSpecificData"), Base64.DEFAULT); + builder.addManufacturerData(manufacturerId, manufacturerSpecificData); + } + return builder.build(); + } } 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 index bd00f45..c555b92 100644 --- 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 @@ -16,12 +16,16 @@ package com.google.android.mobly.snippet.bundled.utils; +import android.annotation.TargetApi; import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.AdvertiseSettings; +import android.bluetooth.le.ScanRecord; import android.net.DhcpInfo; import android.net.wifi.SupplicantState; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.os.Build; +import android.os.Bundle; import android.os.ParcelUuid; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -65,9 +69,7 @@ public class JsonSerializer { } public JSONObject toJson(Object object) throws JSONException { - if (object instanceof BluetoothDevice) { - return serializeBluetoothDevice((BluetoothDevice) object); - } else if (object instanceof DhcpInfo) { + if (object instanceof DhcpInfo) { return serializeDhcpInfo((DhcpInfo) object); } else if (object instanceof WifiConfiguration) { return serializeWifiConfiguration((WifiConfiguration) object); @@ -109,13 +111,13 @@ public class JsonSerializer { private JSONObject serializeWifiConfiguration(WifiConfiguration data) throws JSONException { JSONObject result = new JSONObject(mGson.toJson(data)); result.put("Status", WifiConfiguration.Status.strings[data.status]); - guaranteedPut(result, "SSID", trimQuotationMarks(data.SSID)); + result.put("SSID", trimQuotationMarks(data.SSID)); return result; } private JSONObject serializeWifiInfo(WifiInfo data) throws JSONException { JSONObject result = new JSONObject(mGson.toJson(data)); - guaranteedPut(result, "SSID", trimQuotationMarks(data.getSSID())); + result.put("SSID", trimQuotationMarks(data.getSSID())); for (SupplicantState state : SupplicantState.values()) { if (data.getSupplicantState().equals(state)) { result.put("SupplicantState", state.name()); @@ -124,71 +126,71 @@ public class JsonSerializer { return result; } - private JSONObject serializeBluetoothDevice(BluetoothDevice data) throws JSONException { - JSONObject result = new JSONObject(); - guaranteedPut(result, "Address", data.getAddress()); - final String bondStateFieldName = "BondState"; - switch (data.getBondState()) { - case BluetoothDevice.BOND_NONE: - result.put(bondStateFieldName, "BOND_NONE"); - break; - case BluetoothDevice.BOND_BONDING: - result.put(bondStateFieldName, "BOND_BONDING"); - break; - case BluetoothDevice.BOND_BONDED: - result.put(bondStateFieldName, "BOND_BONDED"); - break; - } - guaranteedPut(result, "Name", data.getName()); + public Bundle serializeBluetoothDevice(BluetoothDevice data) { + Bundle result = new Bundle(); + result.putString("Address", data.getAddress()); + final String bondState = + MbsEnums.BLUETOOTH_DEVICE_BOND_STATE.getString(data.getBondState()); + result.putString("BondState", bondState); + result.putString("Name", data.getName()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - final String deviceTypeFieldName = "DeviceType"; - switch (data.getType()) { - case BluetoothDevice.DEVICE_TYPE_CLASSIC: - result.put(deviceTypeFieldName, "DEVICE_TYPE_CLASSIC"); - break; - case BluetoothDevice.DEVICE_TYPE_LE: - result.put(deviceTypeFieldName, "DEVICE_TYPE_LE"); - break; - case BluetoothDevice.DEVICE_TYPE_DUAL: - result.put(deviceTypeFieldName, "DEVICE_TYPE_DUAL"); - break; - case BluetoothDevice.DEVICE_TYPE_UNKNOWN: - result.put(deviceTypeFieldName, "DEVICE_TYPE_UNKNOWN"); - break; - } + String deviceType = MbsEnums.BLUETOOTH_DEVICE_TYPE.getString(data.getType()); + result.putString("DeviceType", deviceType); ParcelUuid[] parcelUuids = data.getUuids(); if (parcelUuids != null) { ArrayList uuidStrings = new ArrayList<>(parcelUuids.length); for (ParcelUuid parcelUuid : parcelUuids) { uuidStrings.add(parcelUuid.getUuid().toString()); } - result.put("UUIDs", uuidStrings); + result.putStringArrayList("UUIDs", uuidStrings); } } return result; } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Bundle serializeBleScanResult(android.bluetooth.le.ScanResult scanResult) { + Bundle result = new Bundle(); + result.putBundle("Device", serializeBluetoothDevice(scanResult.getDevice())); + result.putInt("Rssi", scanResult.getRssi()); + result.putBundle("ScanRecord", serializeBleScanRecord(scanResult.getScanRecord())); + result.putLong("TimestampNanos", scanResult.getTimestampNanos()); + return result; + } + /** - * Guarantees a field is put into a JSONObject even if it's null. + * Serialize ScanRecord for Bluetooth LE. * - *

By default, if the object of {@link JSONObject#put(String, Object)} is null, the `put` - * method would either remove the field or do nothing, causing serialized objects to have - * inconsistent fields. + *

Not all fields are serialized here. Will add more as we need. * - *

Use this method to put objects that may be null into the serialized JSONObject so the - * serialized objects have a consistent set of critical fields, like the SSID field in - * serialized WifiConfiguration objects. + *

The returned {@link Bundle} has the following info:
+     *          "DeviceName", String
+     *          "TxPowerLevel", String
+     * 
* - * @param data - * @param name - * @param object - * @throws JSONException + * @param record A {@link ScanRecord} object. + * @return A {@link Bundle} object. */ - private void guaranteedPut(JSONObject data, String name, Object object) throws JSONException { - if (object == null) { - data.put(name, JSONObject.NULL); - } else { - data.put(name, object); - } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private Bundle serializeBleScanRecord(ScanRecord record) { + Bundle result = new Bundle(); + result.putString("DeviceName", record.getDeviceName()); + result.putString( + "TxPowerLevel", + MbsEnums.BLE_ADVERTISE_TX_POWER.getString(record.getTxPowerLevel())); + return result; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static Bundle serializeBleAdvertisingSettings(AdvertiseSettings advertiseSettings) { + Bundle result = new Bundle(); + result.putString( + "TxPowerLevel", + MbsEnums.BLE_ADVERTISE_TX_POWER.getString(advertiseSettings.getTxPowerLevel())); + result.putString( + "Mode", MbsEnums.BLE_ADVERTISE_MODE.getString(advertiseSettings.getMode())); + result.putInt("Timeout", advertiseSettings.getTimeout()); + result.putBoolean("IsConnectable", advertiseSettings.isConnectable()); + return result; } } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java new file mode 100644 index 0000000..33c425c --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java @@ -0,0 +1,91 @@ +package com.google.android.mobly.snippet.bundled.utils; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.AdvertiseSettings; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanSettings; +import android.os.Build; + +/** Mobly Bundled Snippets (MBS)'s {@link RpcEnum} objects representing enums in Android APIs. */ +public class MbsEnums { + static final RpcEnum BLE_ADVERTISE_MODE = buildBleAdvertiseModeEnum(); + static final RpcEnum BLE_ADVERTISE_TX_POWER = buildBleAdvertiseTxPowerEnum(); + public static final RpcEnum BLE_SCAN_FAILED_ERROR_CODE = buildBleScanFailedErrorCodeEnum(); + public static final RpcEnum BLE_SCAN_RESULT_CALLBACK_TYPE = + buildBleScanResultCallbackTypeEnum(); + static final RpcEnum BLUETOOTH_DEVICE_BOND_STATE = buildBluetoothDeviceBondState(); + static final RpcEnum BLUETOOTH_DEVICE_TYPE = buildBluetoothDeviceTypeEnum(); + + private static RpcEnum buildBluetoothDeviceBondState() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + return builder.add("BOND_NONE", BluetoothDevice.BOND_NONE) + .add("BOND_BONDING", BluetoothDevice.BOND_BONDING) + .add("BOND_BONDED", BluetoothDevice.BOND_BONDED) + .build(); + } + + private static RpcEnum buildBluetoothDeviceTypeEnum() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + return builder.build(); + } + return builder.add("DEVICE_TYPE_CLASSIC", BluetoothDevice.DEVICE_TYPE_CLASSIC) + .add("DEVICE_TYPE_LE", BluetoothDevice.DEVICE_TYPE_LE) + .add("DEVICE_TYPE_DUAL", BluetoothDevice.DEVICE_TYPE_DUAL) + .build(); + } + + private static RpcEnum buildBleAdvertiseTxPowerEnum() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return builder.build(); + } + return builder.add( + "ADVERTISE_TX_POWER_ULTRA_LOW", + AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW) + .add("ADVERTISE_TX_POWER_LOW", AdvertiseSettings.ADVERTISE_TX_POWER_LOW) + .add("ADVERTISE_TX_POWER_MEDIUM", AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) + .add("ADVERTISE_TX_POWER_HIGH", AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) + .build(); + } + + private static RpcEnum buildBleAdvertiseModeEnum() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return builder.build(); + } + return builder.add("ADVERTISE_MODE_BALANCED", AdvertiseSettings.ADVERTISE_MODE_BALANCED) + .add("ADVERTISE_MODE_LOW_LATENCY", AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) + .add("ADVERTISE_MODE_LOW_POWER", AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) + .build(); + } + + private static RpcEnum buildBleScanFailedErrorCodeEnum() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return builder.build(); + } + return builder.add("SCAN_FAILED_ALREADY_STARTED", ScanCallback.SCAN_FAILED_ALREADY_STARTED) + .add( + "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED", + ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED) + .add( + "SCAN_FAILED_FEATURE_UNSUPPORTED", + ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED) + .add("SCAN_FAILED_INTERNAL_ERROR", ScanCallback.SCAN_FAILED_INTERNAL_ERROR) + .build(); + } + + private static RpcEnum buildBleScanResultCallbackTypeEnum() { + RpcEnum.Builder builder = new RpcEnum.Builder(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return builder.build(); + } + builder.add("CALLBACK_TYPE_ALL_MATCHES", ScanSettings.CALLBACK_TYPE_ALL_MATCHES); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder.add("CALLBACK_TYPE_FIRST_MATCH", ScanSettings.CALLBACK_TYPE_FIRST_MATCH); + builder.add("CALLBACK_TYPE_MATCH_LOST", ScanSettings.CALLBACK_TYPE_MATCH_LOST); + } + return builder.build(); + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java new file mode 100644 index 0000000..d3d95ae --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java @@ -0,0 +1,89 @@ +/* + * 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 com.google.common.collect.ImmutableBiMap; + +/** + * A container type for handling String-Integer enum conversion in Rpc protocol. + * + *

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

Once built, an RpcEnum object is immutable. + */ +public class RpcEnum { + private final ImmutableBiMap mEnums; + + private RpcEnum(ImmutableBiMap.Builder builder, int minSdk) { + mEnums = builder.build(); + } + + /** + * Get the int value of an enum based on its String value. + * + * @param enumString + * @return + */ + public int getInt(String enumString) { + Integer result = mEnums.get(enumString); + if (result == null) { + throw new NoSuchFieldError("No int value found for: " + enumString); + } + return result; + } + + /** + * Get the String value of an enum based on its int value. + * + * @param enumInt + * @return + */ + public String getString(int enumInt) { + String result = mEnums.inverse().get(enumInt); + if (result == null) { + throw new NoSuchFieldError("No String value found for: " + enumInt); + } + return result; + } + + /** Builder for RpcEnum. */ + public static class Builder { + private final ImmutableBiMap.Builder builder; + public int minSdk = 0; + + public Builder() { + builder = new ImmutableBiMap.Builder<>(); + } + + /** + * Add an enum String-Integer pair. + * + * @param enumString + * @param enumInt + * @return + */ + public Builder add(String enumString, int enumInt) { + builder.put(enumString, enumInt); + return this; + } + + public RpcEnum build() { + return new RpcEnum(builder, minSdk); + } + } +} 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 index 8736d85..8f4f7d6 100644 --- 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 @@ -22,6 +22,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public final class Utils { + private Utils() {} /** -- cgit v1.2.3 From 2af13eb9da1bde081bd43329b08d92fff5251b68 Mon Sep 17 00:00:00 2001 From: "David T.H. Kao" Date: Fri, 2 Jun 2017 11:10:46 -0700 Subject: Microphone mute RPCs (#53) * Add microphone muting to AudioManager controls * Fix return types --- .../com/google/android/mobly/snippet/bundled/AudioSnippet.java | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java index 4f6b0ae..cf5331f 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -33,6 +33,16 @@ public class AudioSnippet implements Snippet { mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); } + @Rpc(description = "Sets the microphone mute state: True = Muted, False = not muted.") + public void setMicrophoneMute(boolean state) { + mAudioManager.setMicrophoneMute(state); + } + + @Rpc(description = "Returns whether or not the microphone is muted.") + public boolean isMicrophoneMute() { + return mAudioManager.isMicrophoneMute(); + } + @Rpc(description = "Gets the music stream volume.") public int getMusicVolume() { return mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); -- cgit v1.2.3 From ec29dcc0819d1e31af39f3c1102f5349fa29ed88 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Mon, 5 Jun 2017 16:23:57 -0700 Subject: Add Rpcs for basic Android logging. (#52) --- .../android/mobly/snippet/bundled/LogSnippet.java | 64 ++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/LogSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/LogSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/LogSnippet.java new file mode 100644 index 0000000..9f889e4 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/LogSnippet.java @@ -0,0 +1,64 @@ +/* + * 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.util.Log; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; + +/** Snippet class exposing Android APIs related to logging. */ +public class LogSnippet implements Snippet { + private String mTag = "MoblyTestLog"; + + @Rpc(description = "Set the tag to use for logX Rpcs. Default is 'MoblyTestLog'.") + public void logSetTag(String tag) { + mTag = tag; + } + + @Rpc(description = "Log at info level.") + public void logI(String message) { + Log.i(mTag, message); + } + + @Rpc(description = "Log at debug level.") + public void logD(String message) { + Log.d(mTag, message); + } + + @Rpc(description = "Log at error level.") + public void logE(String message) { + Log.e(mTag, message); + } + + @Rpc(description = "Log at warning level.") + public void logW(String message) { + Log.w(mTag, message); + } + + @Rpc(description = "Log at verbose level.") + public void logV(String message) { + Log.v(mTag, message); + } + + @Rpc(description = "Log at WTF level.") + public void logWtf(String message) { + Log.wtf(mTag, message); + } + + @Override + public void shutdown() {} +} -- cgit v1.2.3 From 4e01d97997994bd3fdae1448ebbbba626d7fe50f Mon Sep 17 00:00:00 2001 From: Ang Li Date: Mon, 12 Jun 2017 10:39:47 -0700 Subject: Add basic Bluetooth pairing and A2DP connection features. (#54) * Add support for pairing/unpairing a device. * Add support for connecting with A2DP profile. * Reorganize Bluetooth related snippets for future expansion of profile support. * Use Bundle instead of JSONObject for BluetoothDevice serialization. --- .../snippet/bundled/BluetoothAdapterSnippet.java | 230 ---------------- .../bundled/bluetooth/BluetoothAdapterSnippet.java | 298 +++++++++++++++++++++ .../bluetooth/PairingBroadcastReceiver.java | 30 +++ .../bluetooth/profiles/BluetoothA2dpSnippet.java | 113 ++++++++ .../snippet/bundled/utils/JsonSerializer.java | 10 + 5 files changed, 451 insertions(+), 230 deletions(-) delete mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 deleted file mode 100644 index 0f4d3e6..0000000 --- a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothAdapterSnippet.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * 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.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; -import android.os.Bundle; -import android.support.test.InstrumentationRegistry; -import com.google.android.mobly.snippet.Snippet; -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.rpc.RpcMinSdk; -import java.util.ArrayList; -import org.json.JSONArray; -import org.json.JSONException; - -/** Snippet class exposing Android APIs in BluetoothAdapter. */ -public class BluetoothAdapterSnippet implements Snippet { - private static class BluetoothAdapterSnippetException extends Exception { - private static final long serialVersionUID = 1; - - public BluetoothAdapterSnippetException(String msg) { - super(msg); - } - } - - private final Context mContext; - private final BluetoothAdapter mBluetoothAdapter; - private final JsonSerializer mJsonSerializer = new JsonSerializer(); - private final ArrayList mDiscoveryResults = new ArrayList<>(); - private volatile boolean mIsScanResultAvailable = false; - - public BluetoothAdapterSnippet() { - mContext = InstrumentationRegistry.getContext(); - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - } - - @Rpc(description = "Enable bluetooth with a 30s timeout.") - public void btEnable() throws BluetoothAdapterSnippetException, InterruptedException { - if (!mBluetoothAdapter.enable()) { - throw new BluetoothAdapterSnippetException("Failed to start enabling bluetooth"); - } - if (!Utils.waitUntil(() -> mBluetoothAdapter.isEnabled(), 30)) { - throw new BluetoothAdapterSnippetException("Bluetooth did not turn on within 30s."); - } - } - - @Rpc(description = "Disable bluetooth with a 30s timeout.") - public void btDisable() throws BluetoothAdapterSnippetException, InterruptedException { - if (!mBluetoothAdapter.disable()) { - throw new BluetoothAdapterSnippetException("Failed to start disabling bluetooth"); - } - if (!Utils.waitUntil(() -> !mBluetoothAdapter.isEnabled(), 30)) { - throw new BluetoothAdapterSnippetException("Bluetooth did not turn off within 30s."); - } - } - - @Rpc(description = "Return true if Bluetooth is enabled, false otherwise.") - public boolean btIsEnabled() { - return mBluetoothAdapter.isEnabled(); - } - - @Rpc( - description = - "Get bluetooth discovery results, which is a list of serialized BluetoothDevice objects." - ) - public JSONArray btGetCachedScanResults() throws JSONException { - JSONArray results = new JSONArray(); - for (BluetoothDevice result : mDiscoveryResults) { - results.put(mJsonSerializer.toJson(result)); - } - return results; - } - - @Rpc(description = "Set the friendly Bluetooth name of the local Bluetooth adapter.") - public void btSetName(String name) throws BluetoothAdapterSnippetException { - if (!btIsEnabled()) { - throw new BluetoothAdapterSnippetException( - "Bluetooth is not enabled, cannot set Bluetooth name."); - } - if (!mBluetoothAdapter.setName(name)) { - throw new BluetoothAdapterSnippetException( - "Failed to set local Bluetooth name to " + name); - } - } - - @Rpc(description = "Get the friendly Bluetooth name of the local Bluetooth adapter.") - public String btGetName() { - return mBluetoothAdapter.getName(); - } - - @Rpc(description = "Returns the hardware address of the local Bluetooth adapter.") - public String btGetAddress() { - return mBluetoothAdapter.getAddress(); - } - - @Rpc( - description = - "Start discovery, wait for discovery to complete, and return results, which is a list of " - + "serialized BluetoothDevice objects." - ) - public JSONArray btDiscoverAndGetResults() - throws InterruptedException, JSONException, BluetoothAdapterSnippetException { - IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); - filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); - if (mBluetoothAdapter.isDiscovering()) { - mBluetoothAdapter.cancelDiscovery(); - } - mDiscoveryResults.clear(); - mIsScanResultAvailable = false; - BroadcastReceiver receiver = new BluetoothScanReceiver(); - mContext.registerReceiver(receiver, filter); - try { - if (!mBluetoothAdapter.startDiscovery()) { - throw new BluetoothAdapterSnippetException( - "Failed to initiate Bluetooth Discovery."); - } - if (!Utils.waitUntil(() -> mIsScanResultAvailable, 120)) { - throw new BluetoothAdapterSnippetException( - "Failed to get discovery results after 2 mins, timeout!"); - } - } finally { - mContext.unregisterReceiver(receiver); - } - return btGetCachedScanResults(); - } - - @Rpc(description = "Become discoverable in Bluetooth.") - public void btBecomeDiscoverable(Integer duration) throws Throwable { - if (!btIsEnabled()) { - throw new BluetoothAdapterSnippetException( - "Bluetooth is not enabled, cannot become discoverable."); - } - if (!(boolean) - Utils.invokeByReflection( - mBluetoothAdapter, - "setScanMode", - BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, - duration)) { - throw new BluetoothAdapterSnippetException("Failed to become discoverable."); - } - } - - @Rpc(description = "Stop being discoverable in Bluetooth.") - public void btStopBeingDiscoverable() throws Throwable { - if (!(boolean) - Utils.invokeByReflection( - mBluetoothAdapter, - "setScanMode", - BluetoothAdapter.SCAN_MODE_NONE, - 0 /* duration is not used for this */)) { - throw new BluetoothAdapterSnippetException("Failed to stop being discoverable."); - } - } - - @Rpc(description = "Get the list of paired bluetooth devices.") - public ArrayList btGetPairedDevices() - throws BluetoothAdapterSnippetException, InterruptedException, JSONException { - ArrayList pairedDevices = new ArrayList<>(); - for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) { - pairedDevices.add(mJsonSerializer.serializeBluetoothDevice(device)); - } - return pairedDevices; - } - - /** - * Enable Bluetooth HCI snoop log collection. - * - *

The file can be pulled from `/sdcard/btsnoop_hci.log`. - * - * @throws Throwable - */ - @RpcMinSdk(Build.VERSION_CODES.KITKAT) - @Rpc(description = "Enable Bluetooth HCI snoop log for debugging.") - public void btEnableHciSnoopLog() throws Throwable { - if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", true)) { - throw new BluetoothAdapterSnippetException("Failed to enable HCI snoop log."); - } - } - - @RpcMinSdk(Build.VERSION_CODES.KITKAT) - @Rpc(description = "Disable Bluetooth HCI snoop log.") - public void btDisableHciSnoopLog() throws Throwable { - if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", false)) { - throw new BluetoothAdapterSnippetException("Failed to disable HCI snoop log."); - } - } - - @Override - public void shutdown() {} - - private class BluetoothScanReceiver extends BroadcastReceiver { - - /** - * The receiver gets an ACTION_FOUND intent whenever a new device is found. - * ACTION_DISCOVERY_FINISHED intent is received when the discovery process ends. - */ - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { - mIsScanResultAvailable = true; - } else if (BluetoothDevice.ACTION_FOUND.equals(action)) { - BluetoothDevice device = - (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - mDiscoveryResults.add(device); - } - } - } -} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java new file mode 100644 index 0000000..5971517 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java @@ -0,0 +1,298 @@ +/* + * 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.bluetooth; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Bundle; +import android.support.test.InstrumentationRegistry; +import com.google.android.mobly.snippet.Snippet; +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.rpc.RpcMinSdk; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; +import org.json.JSONException; + +/** Snippet class exposing Android APIs in BluetoothAdapter. */ +public class BluetoothAdapterSnippet implements Snippet { + private static class BluetoothAdapterSnippetException extends Exception { + private static final long serialVersionUID = 1; + + public BluetoothAdapterSnippetException(String msg) { + super(msg); + } + } + + private final Context mContext; + private static final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + private final JsonSerializer mJsonSerializer = new JsonSerializer(); + private static final ConcurrentHashMap mDiscoveryResults = + new ConcurrentHashMap<>(); + private volatile boolean mIsScanResultAvailable = false; + + public BluetoothAdapterSnippet() { + mContext = InstrumentationRegistry.getContext(); + } + + /** + * Gets a {@link BluetoothDevice} that has either been paired or discovered. + * + * @param deviceAddress + * @return + */ + public static BluetoothDevice getKnownDeviceByAddress(String deviceAddress) { + BluetoothDevice pairedDevice = getPairedDeviceByAddress(deviceAddress); + if (pairedDevice != null) { + return pairedDevice; + } + BluetoothDevice discoveredDevice = mDiscoveryResults.get(deviceAddress); + if (discoveredDevice != null) { + return discoveredDevice; + } + throw new NoSuchElementException( + "No device with address " + + deviceAddress + + " is paired or has been discovered. Cannot proceed."); + } + + private static BluetoothDevice getPairedDeviceByAddress(String deviceAddress) { + for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) { + if (device.getAddress().equalsIgnoreCase(deviceAddress)) { + return device; + } + } + return null; + } + + @Rpc(description = "Enable bluetooth with a 30s timeout.") + public void btEnable() throws BluetoothAdapterSnippetException, InterruptedException { + if (!mBluetoothAdapter.enable()) { + throw new BluetoothAdapterSnippetException("Failed to start enabling bluetooth"); + } + if (!Utils.waitUntil(() -> mBluetoothAdapter.isEnabled(), 30)) { + throw new BluetoothAdapterSnippetException("Bluetooth did not turn on within 30s."); + } + } + + @Rpc(description = "Disable bluetooth with a 30s timeout.") + public void btDisable() throws BluetoothAdapterSnippetException, InterruptedException { + if (!mBluetoothAdapter.disable()) { + throw new BluetoothAdapterSnippetException("Failed to start disabling bluetooth"); + } + if (!Utils.waitUntil(() -> !mBluetoothAdapter.isEnabled(), 30)) { + throw new BluetoothAdapterSnippetException("Bluetooth did not turn off within 30s."); + } + } + + @Rpc(description = "Return true if Bluetooth is enabled, false otherwise.") + public boolean btIsEnabled() { + return mBluetoothAdapter.isEnabled(); + } + + @Rpc( + description = + "Get bluetooth discovery results, which is a list of serialized BluetoothDevice objects." + ) + public ArrayList btGetCachedScanResults() { + return mJsonSerializer.serializeBluetoothDeviceList(mDiscoveryResults.values()); + } + + @Rpc(description = "Set the friendly Bluetooth name of the local Bluetooth adapter.") + public void btSetName(String name) throws BluetoothAdapterSnippetException { + if (!btIsEnabled()) { + throw new BluetoothAdapterSnippetException( + "Bluetooth is not enabled, cannot set Bluetooth name."); + } + if (!mBluetoothAdapter.setName(name)) { + throw new BluetoothAdapterSnippetException( + "Failed to set local Bluetooth name to " + name); + } + } + + @Rpc(description = "Get the friendly Bluetooth name of the local Bluetooth adapter.") + public String btGetName() { + return mBluetoothAdapter.getName(); + } + + @Rpc(description = "Returns the hardware address of the local Bluetooth adapter.") + public String btGetAddress() { + return mBluetoothAdapter.getAddress(); + } + + @Rpc( + description = + "Start discovery, wait for discovery to complete, and return results, which is a list of " + + "serialized BluetoothDevice objects." + ) + public List btDiscoverAndGetResults() + throws InterruptedException, BluetoothAdapterSnippetException { + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); + filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + if (mBluetoothAdapter.isDiscovering()) { + mBluetoothAdapter.cancelDiscovery(); + } + mDiscoveryResults.clear(); + mIsScanResultAvailable = false; + BroadcastReceiver receiver = new BluetoothScanReceiver(); + mContext.registerReceiver(receiver, filter); + try { + if (!mBluetoothAdapter.startDiscovery()) { + throw new BluetoothAdapterSnippetException( + "Failed to initiate Bluetooth Discovery."); + } + if (!Utils.waitUntil(() -> mIsScanResultAvailable, 120)) { + throw new BluetoothAdapterSnippetException( + "Failed to get discovery results after 2 mins, timeout!"); + } + } finally { + mContext.unregisterReceiver(receiver); + } + return btGetCachedScanResults(); + } + + @Rpc(description = "Become discoverable in Bluetooth.") + public void btBecomeDiscoverable(Integer duration) throws Throwable { + if (!btIsEnabled()) { + throw new BluetoothAdapterSnippetException( + "Bluetooth is not enabled, cannot become discoverable."); + } + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, + duration)) { + throw new BluetoothAdapterSnippetException("Failed to become discoverable."); + } + } + + @Rpc(description = "Stop being discoverable in Bluetooth.") + public void btStopBeingDiscoverable() throws Throwable { + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_NONE, + 0 /* duration is not used for this */)) { + throw new BluetoothAdapterSnippetException("Failed to stop being discoverable."); + } + } + + @Rpc(description = "Get the list of paired bluetooth devices.") + public List btGetPairedDevices() + throws BluetoothAdapterSnippetException, InterruptedException, JSONException { + ArrayList pairedDevices = new ArrayList<>(); + for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) { + pairedDevices.add(mJsonSerializer.serializeBluetoothDevice(device)); + } + return pairedDevices; + } + + @Rpc(description = "Pair with a bluetooth device.") + public void btPairDevice(String deviceAddress) throws Throwable { + BluetoothDevice device = mDiscoveryResults.get(deviceAddress); + if (device == null) { + throw new NoSuchElementException( + "No device with address " + + deviceAddress + + " has been discovered. Cannot proceed."); + } + mContext.registerReceiver( + new PairingBroadcastReceiver(mContext), PairingBroadcastReceiver.filter); + if (!(boolean) Utils.invokeByReflection(device, "createBond")) { + throw new BluetoothAdapterSnippetException( + "Failed to initiate the pairing process to device: " + deviceAddress); + } + if (!Utils.waitUntil(() -> device.getBondState() == BluetoothDevice.BOND_BONDED, 120)) { + throw new BluetoothAdapterSnippetException( + "Failed to pair with device " + deviceAddress + " after 2min."); + } + } + + @Rpc(description = "Un-pair a bluetooth device.") + public void btUnpairDevice(String deviceAddress) throws Throwable { + for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) { + if (device.getAddress().equalsIgnoreCase(deviceAddress)) { + if (!(boolean) Utils.invokeByReflection(device, "removeBond")) { + throw new BluetoothAdapterSnippetException( + "Failed to initiate the un-pairing process for device: " + + deviceAddress); + } + if (!Utils.waitUntil( + () -> device.getBondState() == BluetoothDevice.BOND_NONE, 30)) { + throw new BluetoothAdapterSnippetException( + "Failed to un-pair device " + deviceAddress + " after 30s."); + } + } + } + throw new NoSuchElementException("No device wih address " + deviceAddress + " is paired."); + } + + /** + * Enable Bluetooth HCI snoop log collection. + * + *

The file can be pulled from `/sdcard/btsnoop_hci.log`. + * + * @throws Throwable + */ + @RpcMinSdk(Build.VERSION_CODES.KITKAT) + @Rpc(description = "Enable Bluetooth HCI snoop log for debugging.") + public void btEnableHciSnoopLog() throws Throwable { + if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", true)) { + throw new BluetoothAdapterSnippetException("Failed to enable HCI snoop log."); + } + } + + @RpcMinSdk(Build.VERSION_CODES.KITKAT) + @Rpc(description = "Disable Bluetooth HCI snoop log.") + public void btDisableHciSnoopLog() throws Throwable { + if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", false)) { + throw new BluetoothAdapterSnippetException("Failed to disable HCI snoop log."); + } + } + + @Override + public void shutdown() {} + + private class BluetoothScanReceiver extends BroadcastReceiver { + + /** + * The receiver gets an ACTION_FOUND intent whenever a new device is found. + * ACTION_DISCOVERY_FINISHED intent is received when the discovery process ends. + */ + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { + mIsScanResultAvailable = true; + } else if (BluetoothDevice.ACTION_FOUND.equals(action)) { + BluetoothDevice device = + (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + mDiscoveryResults.put(device.getAddress(), device); + } + } + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java new file mode 100644 index 0000000..0cfd362 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java @@ -0,0 +1,30 @@ +package com.google.android.mobly.snippet.bundled.bluetooth; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import com.google.android.mobly.snippet.util.Log; + +@TargetApi(Build.VERSION_CODES.KITKAT) +public class PairingBroadcastReceiver extends BroadcastReceiver { + private final Context mContext; + public static IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); + + public PairingBroadcastReceiver(Context context) { + mContext = context; + } + + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothDevice.ACTION_PAIRING_REQUEST)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + Log.d("Confirming pairing with device: " + device.getAddress()); + device.setPairingConfirmation(true); + mContext.unregisterReceiver(this); + } + } +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java new file mode 100644 index 0000000..b218723 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java @@ -0,0 +1,113 @@ +package com.google.android.mobly.snippet.bundled.bluetooth.profiles; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Bundle; +import android.support.test.InstrumentationRegistry; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.bundled.bluetooth.BluetoothAdapterSnippet; +import com.google.android.mobly.snippet.bundled.bluetooth.PairingBroadcastReceiver; +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.rpc.RpcMinSdk; +import java.util.ArrayList; + +public class BluetoothA2dpSnippet implements Snippet { + private static class BluetoothA2dpSnippetException extends Exception { + private static final long serialVersionUID = 1; + + BluetoothA2dpSnippetException(String msg) { + super(msg); + } + } + + private Context mContext; + private static boolean sIsA2dpProfileReady = false; + private static BluetoothA2dp sA2dpProfile; + private final JsonSerializer mJsonSerializer = new JsonSerializer(); + + public BluetoothA2dpSnippet() { + mContext = InstrumentationRegistry.getContext(); + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + bluetoothAdapter.getProfileProxy( + mContext, new A2dpServiceListener(), BluetoothProfile.A2DP); + Utils.waitUntil(() -> sIsA2dpProfileReady, 60); + } + + private static class A2dpServiceListener implements BluetoothProfile.ServiceListener { + public void onServiceConnected(int var1, BluetoothProfile profile) { + sA2dpProfile = (BluetoothA2dp) profile; + sIsA2dpProfileReady = true; + } + + public void onServiceDisconnected(int var1) { + sIsA2dpProfileReady = false; + } + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + @RpcMinSdk(Build.VERSION_CODES.KITKAT) + @Rpc( + description = + "Connects to a paired or discovered device with A2DP profile." + + "If a device has been discovered but not paired, this will pair it." + ) + public void btA2dpConnect(String deviceAddress) throws Throwable { + BluetoothDevice device = BluetoothAdapterSnippet.getKnownDeviceByAddress(deviceAddress); + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); + mContext.registerReceiver(new PairingBroadcastReceiver(mContext), filter); + Utils.invokeByReflection(sA2dpProfile, "connect", device); + if (!Utils.waitUntil( + () -> sA2dpProfile.getConnectionState(device) == BluetoothA2dp.STATE_CONNECTED, + 120)) { + throw new BluetoothA2dpSnippetException( + "Failed to connect to device " + + device.getName() + + "|" + + device.getAddress() + + " with A2DP profile within 2min."); + } + } + + @Rpc(description = "Disconnects a device from A2DP profile.") + public void btA2dpDisconnect(String deviceAddress) throws Throwable { + BluetoothDevice device = getConnectedBluetoothDevice(deviceAddress); + Utils.invokeByReflection(sA2dpProfile, "disconnect", device); + if (!Utils.waitUntil( + () -> sA2dpProfile.getConnectionState(device) == BluetoothA2dp.STATE_DISCONNECTED, + 120)) { + throw new BluetoothA2dpSnippetException( + "Failed to disconnect device " + + device.getName() + + "|" + + device.getAddress() + + " from A2DP profile within 2min."); + } + } + + @Rpc(description = "Gets all the devices currently connected via A2DP profile.") + public ArrayList btA2dpGetConnectedDevices() { + return mJsonSerializer.serializeBluetoothDeviceList(sA2dpProfile.getConnectedDevices()); + } + + private BluetoothDevice getConnectedBluetoothDevice(String deviceAddress) + throws BluetoothA2dpSnippetException { + for (BluetoothDevice device : sA2dpProfile.getConnectedDevices()) { + if (device.getAddress().equalsIgnoreCase(deviceAddress)) { + return device; + } + } + throw new BluetoothA2dpSnippetException( + "No device with address " + deviceAddress + " is connected via A2DP."); + } + + @Override + public void shutdown() {} +} 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 index c555b92..7e5ca5b 100644 --- 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 @@ -33,6 +33,7 @@ import java.lang.reflect.Modifier; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; +import java.util.Collection; import org.json.JSONException; import org.json.JSONObject; @@ -148,6 +149,15 @@ public class JsonSerializer { return result; } + public ArrayList serializeBluetoothDeviceList( + Collection bluetoothDevices) { + ArrayList results = new ArrayList<>(); + for (BluetoothDevice device : bluetoothDevices) { + results.add(serializeBluetoothDevice(device)); + } + return results; + } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) public Bundle serializeBleScanResult(android.bluetooth.le.ScanResult scanResult) { Bundle result = new Bundle(); -- cgit v1.2.3 From 1888b5d2ba38ad60ada2c00c03469d05c7cc4ef8 Mon Sep 17 00:00:00 2001 From: "David T.H. Kao" Date: Tue, 13 Jun 2017 14:18:27 -0700 Subject: Add method to clear configured networks (#55) * Add method to clear configured networks * throw after trying to clear all with list of failed ssids * weird spacing * config not ssid * remove a log * missing diamond * address the NPE that can happen when wifi is disabled and document issues with permissions * fix log --- .../mobly/snippet/bundled/WifiManagerSnippet.java | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 5c25bc6..1911b9f 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -34,6 +34,7 @@ import com.google.android.mobly.snippet.rpc.Rpc; import com.google.android.mobly.snippet.rpc.RpcMinSdk; import com.google.android.mobly.snippet.util.Log; import java.util.ArrayList; +import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -58,6 +59,25 @@ public class WifiManagerSnippet implements Snippet { mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); } + @Rpc(description = "Clears all configured networks. This will only work if all configured " + + "networks were added through this MBS instance") + public void wifiClearConfiguredNetworks() throws WifiManagerSnippetException { + List unremovedConfigs = mWifiManager.getConfiguredNetworks(); + List failedConfigs = new ArrayList<>(); + if (unremovedConfigs == null) { + throw new WifiManagerSnippetException( + "Failed to get a list of configured networks. Is wifi disabled?"); + } + for (WifiConfiguration config : unremovedConfigs) { + if (!mWifiManager.removeNetwork(config.networkId)) { + failedConfigs.add(config); + } + } + if (!failedConfigs.isEmpty()) { + throw new WifiManagerSnippetException("Failed to remove networks: " + failedConfigs); + } + } + @Rpc(description = "Turns on Wi-Fi with a 30s timeout.") public void wifiEnable() throws InterruptedException, WifiManagerSnippetException { if (!mWifiManager.setWifiEnabled(true)) { @@ -216,8 +236,8 @@ public class WifiManagerSnippet implements Snippet { "Get the list of configured Wi-Fi networks, each is a serialized " + "WifiConfiguration object." ) - public ArrayList wifiGetConfiguredNetworks() throws JSONException { - ArrayList networks = new ArrayList<>(); + public List wifiGetConfiguredNetworks() throws JSONException { + List networks = new ArrayList<>(); for (WifiConfiguration config : mWifiManager.getConfiguredNetworks()) { networks.add(mJsonSerializer.toJson(config)); } -- cgit v1.2.3 From 947441aeb9be8d049c220b1c02956cac0da5f829 Mon Sep 17 00:00:00 2001 From: chok Date: Tue, 13 Jun 2017 20:31:39 -0700 Subject: Minor bug fix to btUnpairedDevice() (#61) * Fixed btUnpairDevice() bug where an exception is thrown even in the success case --- .../android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java | 1 + 1 file changed, 1 insertion(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java index 5971517..81ffd2d 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java @@ -246,6 +246,7 @@ public class BluetoothAdapterSnippet implements Snippet { throw new BluetoothAdapterSnippetException( "Failed to un-pair device " + deviceAddress + " after 30s."); } + return; } } throw new NoSuchElementException("No device wih address " + deviceAddress + " is paired."); -- cgit v1.2.3 From 1627da09afbdb3d275728cd13a7e29358e47a5f3 Mon Sep 17 00:00:00 2001 From: chok Date: Fri, 16 Jun 2017 19:46:09 -0700 Subject: Added support for Unknown BT device type (#63) --- .../java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java | 1 + 1 file changed, 1 insertion(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java index 33c425c..08163b4 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java @@ -32,6 +32,7 @@ public class MbsEnums { return builder.add("DEVICE_TYPE_CLASSIC", BluetoothDevice.DEVICE_TYPE_CLASSIC) .add("DEVICE_TYPE_LE", BluetoothDevice.DEVICE_TYPE_LE) .add("DEVICE_TYPE_DUAL", BluetoothDevice.DEVICE_TYPE_DUAL) + .add("DEVICE_TYPE_UNKNOWN", BluetoothDevice.DEVICE_TYPE_UNKNOWN) .build(); } -- cgit v1.2.3 From 4e41427eb8120f8fbc125b89d4a01d6d009af77c Mon Sep 17 00:00:00 2001 From: "David T.H. Kao" Date: Tue, 20 Jun 2017 10:13:21 -0700 Subject: Initial audio playback functionality (#62) * Initial audio playback functionality * cleanup descriptions * whitespace cleanup --- .../mobly/snippet/bundled/MediaSnippet.java | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/MediaSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/MediaSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/MediaSnippet.java new file mode 100644 index 0000000..ee2a214 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/MediaSnippet.java @@ -0,0 +1,68 @@ +/* + * 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.media.AudioAttributes; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; +import java.io.IOException; + +/* Snippet class to control media playback. */ +public class MediaSnippet implements Snippet { + + private final MediaPlayer mPlayer; + + public MediaSnippet() { + mPlayer = new MediaPlayer(); + } + + @Rpc(description = "Resets snippet media player to an idle state, regardless of current state.") + public void mediaReset() { + mPlayer.reset(); + } + + @Rpc(description = "Play an audio file stored at a specified file path in external storage.") + public void mediaPlayAudioFile(String mediaFilePath) throws IOException { + mediaReset(); + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + mPlayer.setAudioAttributes( + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + ); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + } + mPlayer.setDataSource(mediaFilePath); + mPlayer.prepare(); // Synchronous call blocks until the player is ready for playback. + mPlayer.start(); + } + + @Rpc(description = "Stops media playback.") + public void mediaStop() throws IOException { + mPlayer.stop(); + } + + @Override + public void shutdown() {} +} -- cgit v1.2.3 From fff9f69353017d72c8ff9ea84986bcdd4d95f300 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Fri, 7 Jul 2017 23:46:44 -0700 Subject: Add an rpc to check if wifi is enabled. (#65) --- .../android/mobly/snippet/bundled/WifiManagerSnippet.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 1911b9f..13a39cd 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -59,14 +59,17 @@ public class WifiManagerSnippet implements Snippet { mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); } - @Rpc(description = "Clears all configured networks. This will only work if all configured " - + "networks were added through this MBS instance") + @Rpc( + description = + "Clears all configured networks. This will only work if all configured " + + "networks were added through this MBS instance" + ) public void wifiClearConfiguredNetworks() throws WifiManagerSnippetException { List unremovedConfigs = mWifiManager.getConfiguredNetworks(); List failedConfigs = new ArrayList<>(); if (unremovedConfigs == null) { throw new WifiManagerSnippetException( - "Failed to get a list of configured networks. Is wifi disabled?"); + "Failed to get a list of configured networks. Is wifi disabled?"); } for (WifiConfiguration config : unremovedConfigs) { if (!mWifiManager.removeNetwork(config.networkId)) { @@ -100,6 +103,11 @@ public class WifiManagerSnippet implements Snippet { } } + @Rpc(description = "Checks if Wi-Fi is enabled.") + public boolean wifiIsEnabled() { + return mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED; + } + @Rpc(description = "Trigger Wi-Fi scan.") public void wifiStartScan() throws WifiManagerSnippetException { if (!mWifiManager.startScan()) { -- cgit v1.2.3 From fd5d5cc78ac7814a1e986ee29e8769db277fbc94 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Tue, 11 Jul 2017 09:56:30 -0700 Subject: Remove bt snoop log APIs. (#66) The official story is that only Settings running as system UID can call them. --- .../bundled/bluetooth/BluetoothAdapterSnippet.java | 25 ---------------------- 1 file changed, 25 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java index 81ffd2d..0685923 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java @@ -22,14 +22,12 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.os.Build; import android.os.Bundle; import android.support.test.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; 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.rpc.RpcMinSdk; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; @@ -252,29 +250,6 @@ public class BluetoothAdapterSnippet implements Snippet { throw new NoSuchElementException("No device wih address " + deviceAddress + " is paired."); } - /** - * Enable Bluetooth HCI snoop log collection. - * - *

The file can be pulled from `/sdcard/btsnoop_hci.log`. - * - * @throws Throwable - */ - @RpcMinSdk(Build.VERSION_CODES.KITKAT) - @Rpc(description = "Enable Bluetooth HCI snoop log for debugging.") - public void btEnableHciSnoopLog() throws Throwable { - if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", true)) { - throw new BluetoothAdapterSnippetException("Failed to enable HCI snoop log."); - } - } - - @RpcMinSdk(Build.VERSION_CODES.KITKAT) - @Rpc(description = "Disable Bluetooth HCI snoop log.") - public void btDisableHciSnoopLog() throws Throwable { - if (!(boolean) Utils.invokeByReflection(mBluetoothAdapter, "configHciSnoopLog", false)) { - throw new BluetoothAdapterSnippetException("Failed to disable HCI snoop log."); - } - } - @Override public void shutdown() {} -- cgit v1.2.3 From 80d6246f4e0f5dc75fc3d9fdf3db1d4c203ccd85 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Fri, 14 Jul 2017 09:53:33 -0700 Subject: Run linter on MediaSnippet. (#67) --- .../mobly/snippet/bundled/MediaSnippet.java | 60 +++++++++++----------- 1 file changed, 29 insertions(+), 31 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/MediaSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/MediaSnippet.java index ee2a214..58b38ac 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/MediaSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/MediaSnippet.java @@ -16,7 +16,6 @@ package com.google.android.mobly.snippet.bundled; - import android.media.AudioAttributes; import android.media.AudioManager; import android.media.MediaPlayer; @@ -29,40 +28,39 @@ import java.io.IOException; /* Snippet class to control media playback. */ public class MediaSnippet implements Snippet { - private final MediaPlayer mPlayer; + private final MediaPlayer mPlayer; - public MediaSnippet() { - mPlayer = new MediaPlayer(); - } + public MediaSnippet() { + mPlayer = new MediaPlayer(); + } - @Rpc(description = "Resets snippet media player to an idle state, regardless of current state.") - public void mediaReset() { - mPlayer.reset(); - } + @Rpc(description = "Resets snippet media player to an idle state, regardless of current state.") + public void mediaReset() { + mPlayer.reset(); + } - @Rpc(description = "Play an audio file stored at a specified file path in external storage.") - public void mediaPlayAudioFile(String mediaFilePath) throws IOException { - mediaReset(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - mPlayer.setAudioAttributes( - new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .setUsage(AudioAttributes.USAGE_MEDIA) - .build() - ); - } else { - mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + @Rpc(description = "Play an audio file stored at a specified file path in external storage.") + public void mediaPlayAudioFile(String mediaFilePath) throws IOException { + mediaReset(); + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + mPlayer.setAudioAttributes( + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build()); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + } + mPlayer.setDataSource(mediaFilePath); + mPlayer.prepare(); // Synchronous call blocks until the player is ready for playback. + mPlayer.start(); } - mPlayer.setDataSource(mediaFilePath); - mPlayer.prepare(); // Synchronous call blocks until the player is ready for playback. - mPlayer.start(); - } - @Rpc(description = "Stops media playback.") - public void mediaStop() throws IOException { - mPlayer.stop(); - } + @Rpc(description = "Stops media playback.") + public void mediaStop() throws IOException { + mPlayer.stop(); + } - @Override - public void shutdown() {} + @Override + public void shutdown() {} } -- cgit v1.2.3 From 5d45a95261c0152059dfdb7105307aebe2e2f10c Mon Sep 17 00:00:00 2001 From: "David T.H. Kao" Date: Mon, 17 Jul 2017 14:26:43 -0700 Subject: Check for current network activity before deciding it is already connected. (#70) --- .../google/android/mobly/snippet/bundled/WifiManagerSnippet.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 13a39cd..32f6a8d 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build; import android.support.annotation.Nullable; @@ -193,9 +194,10 @@ public class WifiManagerSnippet implements Snippet { Log.d("Got network config: " + wifiNetworkConfig); WifiConfiguration wifiConfig = JsonDeserializer.jsonToWifiConfig(wifiNetworkConfig); // Return directly if network is already connected. - String connectedSsid = mWifiManager.getConnectionInfo().getSSID(); - if (connectedSsid.equals(wifiConfig.SSID)) { - Log.d("Network " + connectedSsid + " is already connected."); + WifiInfo connectionInfo = mWifiManager.getConnectionInfo(); + if (connectionInfo.getNetworkId() != -1 && connectionInfo.getSSID().equals(wifiConfig.SSID)) + { + Log.d("Network " + connectionInfo.getSSID() + " is already connected."); return; } // If the network is already added but not connected, update the configuration first. -- cgit v1.2.3 From 3b48f2df00a2204e9ce3cba7ec538d4097e42018 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Tue, 18 Jul 2017 11:59:21 -0700 Subject: Do not attempt to modify a network in wifiConnect. (#68) --- .../mobly/snippet/bundled/WifiManagerSnippet.java | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 32f6a8d..6614904 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -193,6 +193,7 @@ public class WifiManagerSnippet implements Snippet { throws InterruptedException, JSONException, WifiManagerSnippetException { Log.d("Got network config: " + wifiNetworkConfig); WifiConfiguration wifiConfig = JsonDeserializer.jsonToWifiConfig(wifiNetworkConfig); + String SSID = wifiConfig.SSID; // Return directly if network is already connected. WifiInfo connectionInfo = mWifiManager.getConnectionInfo(); if (connectionInfo.getNetworkId() != -1 && connectionInfo.getSSID().equals(wifiConfig.SSID)) @@ -200,13 +201,24 @@ public class WifiManagerSnippet implements Snippet { Log.d("Network " + connectionInfo.getSSID() + " is already connected."); return; } - // If the network is already added but not connected, update the configuration first. + int networkId; + // If this is a network with a known SSID, connect with the existing config. + // We have to do this because in N+, network configs can only be modified by the UID that + // created the network. So any attempt to modify a network config that does not belong to us + // would result in error. WifiConfiguration existingConfig = getExistingConfiguredNetwork(wifiConfig.SSID); if (existingConfig != null) { - Log.d("Update the configuration of network " + existingConfig.SSID + "."); - mWifiManager.removeNetwork(existingConfig.networkId); + Log.w( + "Connecting to network \"" + + existingConfig.SSID + + "\" with its existing configuration: " + + existingConfig.toString()); + wifiConfig = existingConfig; + networkId = wifiConfig.networkId; + } else { + // If this is a network with a new SSID, add the network. + networkId = mWifiManager.addNetwork(wifiConfig); } - int networkId = mWifiManager.addNetwork(wifiConfig); mWifiManager.disconnect(); if (!mWifiManager.enableNetwork(networkId, true)) { throw new WifiManagerSnippetException( @@ -216,8 +228,7 @@ public class WifiManagerSnippet implements Snippet { throw new WifiManagerSnippetException( "Failed to reconnect to Wi-Fi network of ID: " + networkId); } - if (!Utils.waitUntil( - () -> mWifiManager.getConnectionInfo().getSSID().equals(wifiConfig.SSID), 90)) { + if (!Utils.waitUntil(() -> mWifiManager.getConnectionInfo().getSSID().equals(SSID), 90)) { throw new WifiManagerSnippetException( "Failed to connect to Wi-Fi network " + wifiNetworkConfig.toString() -- cgit v1.2.3 From 4dbbe064143465c11a5bd0e78386bf6f06532934 Mon Sep 17 00:00:00 2001 From: arammelk Date: Mon, 24 Jul 2017 18:52:36 -0700 Subject: Add SMS snippet (#69) * add sms snippet * Remove unnecessary sleep * Add missing copyright to IncomingSmsBroadcastReceiver * Add sent confirmation for sendSms, this makes it an AsyncRpc. - Added OutboundSmsReceiver to post events when SMS are send action is done. Posts error back if action is not successful. Also wait for all parts of a multipart message to be sent before posting event. Example usage now: event = s.sendSms('+15555678912', 'message message message').waitAndGet('SentSms') - Renamed IncomingSmsBroardcastReceiver to SmsReceiver and make it a private class of SmsSnippet. Converted sendSms to AsyncRpc so it will post event back when message is sent * Add sent confirmation for sendSms, this makes it an AsyncRpc. - Added OutboundSmsReceiver to post events when SMS are send action is done. Posts error back if action is not successful. Also wait for all parts of a multipart message to be sent before posting event. Example usage now: event = s.sendSms('+15555678912', 'message message message').waitAndGet('SentSms') - Renamed IncomingSmsBroardcastReceiver to SmsReceiver and make it a private class of SmsSnippet. Converted sendSms to AsyncRpc so it will post event back when message is sent * Cleanup naming * Align event key names with Java object property names. * Use EventCache + built in mobly SnippetEvents to wait for SMS sent confirmation. This shares code with EventSnippet in Mobly Snippet Lib. I'm not sure what the accepted practice is for sharing code across github projects? Should I refactor some of the event code in Mobly Bundle Lib into EventUtils, then use that in Mobly Bundled Snippets to avoid duplicating this? I still want to check this is in as is, to have a working version earlier. * Return data from event instead of fulll SnippetEvent. This way it works like so: >>> result = s.sendSms("15551234567", "Hello world") >>> pprint.pprint(result) {u'sent': True} >>> * throw exception on error instead and some javadocs. * cleanup --- .../android/mobly/snippet/bundled/SmsSnippet.java | 212 +++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java new file mode 100644 index 0000000..1fdb409 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java @@ -0,0 +1,212 @@ +/* + * 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.annotation.TargetApi; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Bundle; +import android.provider.Telephony.Sms.Intents; +import android.support.test.InstrumentationRegistry; +import android.telephony.SmsManager; +import android.telephony.SmsMessage; + +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.event.EventCache; +import com.google.android.mobly.snippet.event.SnippetEvent; +import com.google.android.mobly.snippet.rpc.AsyncRpc; +import com.google.android.mobly.snippet.rpc.JsonBuilder; +import com.google.android.mobly.snippet.rpc.Rpc; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +/** Snippet class for SMS RPCs. */ +public class SmsSnippet implements Snippet { + + private static class SmsSnippetException extends Exception { + private static final long serialVersionUID = 1L; + + public SmsSnippetException(String msg) { + super(msg); + } + } + + private static final int MAX_CHAR_COUNT_PER_SMS = 160; + private static final String SMS_SENT_ACTION = ".SMS_SENT"; + private static final int DEFAULT_TIMEOUT_MILLISECOND = 60 * 1000; + private static final String SMS_RECEIVED_EVENT_NAME = "ReceivedSms"; + private static final String SMS_SENT_EVENT_NAME = "SentSms"; + private static final String SMS_CALLBACK_ID_PREFIX = "sendSms-"; + + private static int mCallbackCounter = 0; + + private final Context mContext; + private final SmsManager mSmsManager; + + public SmsSnippet() { + this.mContext = InstrumentationRegistry.getContext(); + this.mSmsManager = SmsManager.getDefault(); + } + + /** + * Send SMS and return after waiting for send confirmation (with a timeout of 60 seconds). + * + * @param phoneNumber A String representing phone number with country code. + * @param message A String representing the message to send. + * @throws InterruptedException + * @throws SmsSnippetException + * @throws JSONException + */ + @Rpc(description = "Send SMS to a specified phone number.") + public void sendSms(String phoneNumber, String message) + throws InterruptedException, SmsSnippetException, JSONException { + String callbackId = new StringBuilder().append(SMS_CALLBACK_ID_PREFIX) + .append(++mCallbackCounter).toString(); + OutboundSmsReceiver receiver = new OutboundSmsReceiver(mContext, callbackId); + + if (message.length() > MAX_CHAR_COUNT_PER_SMS) { + ArrayList parts = mSmsManager.divideMessage(message); + ArrayList sIntents = new ArrayList<>(); + for (String part : parts) { + sIntents.add(PendingIntent.getBroadcast( + mContext, 0, new Intent(SMS_SENT_ACTION), 0)); + } + receiver.setExpectedMessageCount(parts.size()); + mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION)); + mSmsManager.sendMultipartTextMessage(phoneNumber, null, parts, sIntents, null); + } else { + PendingIntent sentIntent = PendingIntent.getBroadcast( + mContext, 0, new Intent(SMS_SENT_ACTION), 0); + receiver.setExpectedMessageCount(1); + mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION)); + mSmsManager.sendTextMessage(phoneNumber, null, message, sentIntent, null); + } + + String qId = EventCache.getQueueId(callbackId, SMS_SENT_EVENT_NAME); + LinkedBlockingDeque q = EventCache.getInstance().getEventDeque(qId); + SnippetEvent result = q.pollFirst(DEFAULT_TIMEOUT_MILLISECOND, TimeUnit.MILLISECONDS); + if (result == null) { + throw new SmsSnippetException("Timed out waiting for SMS sent confirmation."); + } else if (result.getData().containsKey("error")) { + throw new SmsSnippetException( + "Failed to send SMS, error code: " + result.getData().getInt("error")); + } + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + @AsyncRpc(description = "Async wait for incoming SMS message.") + public void asyncWaitForSms(String callbackId) { + SmsReceiver receiver = new SmsReceiver(mContext, callbackId); + mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION)); + } + + @Override + public void shutdown() {} + + private class OutboundSmsReceiver extends BroadcastReceiver { + private final String mCallbackId; + private Context mContext; + private final EventCache mEventCache; + private int mExpectedMessageCount; + + public OutboundSmsReceiver(Context context, String callbackId) { + this.mCallbackId = callbackId; + this.mContext = context; + this.mEventCache = EventCache.getInstance(); + mExpectedMessageCount = 0; + } + + public void setExpectedMessageCount(int count) { mExpectedMessageCount = count; } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + if (SMS_SENT_ACTION.equals(action)) { + SnippetEvent event = new SnippetEvent(mCallbackId, SMS_SENT_EVENT_NAME); + switch(getResultCode()) { + case Activity.RESULT_OK: + if (mExpectedMessageCount == 1) { + event.getData().putBoolean("sent", true); + mEventCache.postEvent(event); + mContext.unregisterReceiver(this); + } + + if (mExpectedMessageCount > 0 ) { + mExpectedMessageCount--; + } + break; + case SmsManager.RESULT_ERROR_GENERIC_FAILURE: + case SmsManager.RESULT_ERROR_NO_SERVICE: + case SmsManager.RESULT_ERROR_NULL_PDU: + case SmsManager.RESULT_ERROR_RADIO_OFF: + event.getData().putBoolean("sent", false); + event.getData().putInt("error_code", getResultCode()); + mEventCache.postEvent(event); + mContext.unregisterReceiver(this); + break; + } + } + } + } + + private class SmsReceiver extends BroadcastReceiver { + private final String mCallbackId; + private Context mContext; + private final EventCache mEventCache; + + public SmsReceiver(Context context, String callbackId) { + this.mCallbackId = callbackId; + this.mContext = context; + this.mEventCache = EventCache.getInstance(); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + @Override + public void onReceive(Context receivedContext, Intent intent) { + if (Intents.SMS_RECEIVED_ACTION.equals(intent.getAction())) { + SnippetEvent event = new SnippetEvent(mCallbackId, SMS_RECEIVED_EVENT_NAME); + Bundle extras = intent.getExtras(); + if (extras != null) { + SmsMessage[] msgs = Intents.getMessagesFromIntent(intent); + StringBuilder smsMsg = new StringBuilder(); + + SmsMessage sms = msgs[0]; + String sender = sms.getOriginatingAddress(); + event.getData().putString("OriginatingAddress", sender); + + for (SmsMessage msg : msgs) { + smsMsg.append(msg.getMessageBody()); + } + event.getData().putString("MessageBody", smsMsg.toString()); + mEventCache.postEvent(event); + mContext.unregisterReceiver(this); + } + } + } + } +} -- cgit v1.2.3 From 084eb9326917b953900fd6dc61b61d13cf6175b1 Mon Sep 17 00:00:00 2001 From: arammelk Date: Mon, 31 Jul 2017 18:29:31 -0700 Subject: SmsSnippet cleanup (#72) * Cleanups in SmsSnippet * Wrap InterruptedException from polling for SMS sent confirmation event. Now throw SmsException with message that we didn't get the confirmation. * Don't use StringBuilder. --- .../android/mobly/snippet/bundled/SmsSnippet.java | 36 +++++++++++++--------- 1 file changed, 21 insertions(+), 15 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java index 1fdb409..2eb8f38 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java @@ -34,12 +34,8 @@ import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.event.EventCache; import com.google.android.mobly.snippet.event.SnippetEvent; import com.google.android.mobly.snippet.rpc.AsyncRpc; -import com.google.android.mobly.snippet.rpc.JsonBuilder; import com.google.android.mobly.snippet.rpc.Rpc; -import org.json.JSONException; -import org.json.JSONObject; - import java.util.ArrayList; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; @@ -77,21 +73,18 @@ public class SmsSnippet implements Snippet { * * @param phoneNumber A String representing phone number with country code. * @param message A String representing the message to send. - * @throws InterruptedException - * @throws SmsSnippetException - * @throws JSONException + * @throws SmsSnippetException on SMS send error. */ @Rpc(description = "Send SMS to a specified phone number.") public void sendSms(String phoneNumber, String message) - throws InterruptedException, SmsSnippetException, JSONException { - String callbackId = new StringBuilder().append(SMS_CALLBACK_ID_PREFIX) - .append(++mCallbackCounter).toString(); + throws SmsSnippetException { + String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter); OutboundSmsReceiver receiver = new OutboundSmsReceiver(mContext, callbackId); if (message.length() > MAX_CHAR_COUNT_PER_SMS) { ArrayList parts = mSmsManager.divideMessage(message); ArrayList sIntents = new ArrayList<>(); - for (String part : parts) { + for (int i = 0; i < parts.size(); i++) { sIntents.add(PendingIntent.getBroadcast( mContext, 0, new Intent(SMS_SENT_ACTION), 0)); } @@ -108,9 +101,16 @@ public class SmsSnippet implements Snippet { String qId = EventCache.getQueueId(callbackId, SMS_SENT_EVENT_NAME); LinkedBlockingDeque q = EventCache.getInstance().getEventDeque(qId); - SnippetEvent result = q.pollFirst(DEFAULT_TIMEOUT_MILLISECOND, TimeUnit.MILLISECONDS); + SnippetEvent result; + + try { + result = q.pollFirst(DEFAULT_TIMEOUT_MILLISECOND, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new SmsSnippetException("Did not receive SMS sent confirmation event."); + } + if (result == null) { - throw new SmsSnippetException("Timed out waiting for SMS sent confirmation."); + throw new SmsSnippetException("Timed out waiting for SMS sent confirmation event."); } else if (result.getData().containsKey("error")) { throw new SmsSnippetException( "Failed to send SMS, error code: " + result.getData().getInt("error")); @@ -127,7 +127,7 @@ public class SmsSnippet implements Snippet { @Override public void shutdown() {} - private class OutboundSmsReceiver extends BroadcastReceiver { + private static class OutboundSmsReceiver extends BroadcastReceiver { private final String mCallbackId; private Context mContext; private final EventCache mEventCache; @@ -169,12 +169,18 @@ public class SmsSnippet implements Snippet { mEventCache.postEvent(event); mContext.unregisterReceiver(this); break; + default: + event.getData().putBoolean("sent", false); + event.getData().putInt("error_code", -1 /* Unknown */); + mEventCache.postEvent(event); + mContext.unregisterReceiver(this); + break; } } } } - private class SmsReceiver extends BroadcastReceiver { + private static class SmsReceiver extends BroadcastReceiver { private final String mCallbackId; private Context mContext; private final EventCache mEventCache; -- cgit v1.2.3 From ccedca0a4907591d994f7412adba2555c138bd0d Mon Sep 17 00:00:00 2001 From: arammelk Date: Thu, 24 Aug 2017 12:37:19 -0700 Subject: Add synchronous waitForSms (#76) --- .../android/mobly/snippet/bundled/SmsSnippet.java | 58 +++++++++++----------- .../android/mobly/snippet/bundled/utils/Utils.java | 43 ++++++++++++++++ 2 files changed, 73 insertions(+), 28 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java index 2eb8f38..4a38798 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java @@ -29,16 +29,14 @@ import android.provider.Telephony.Sms.Intents; import android.support.test.InstrumentationRegistry; import android.telephony.SmsManager; import android.telephony.SmsMessage; - import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.bundled.utils.Utils; import com.google.android.mobly.snippet.event.EventCache; import com.google.android.mobly.snippet.event.SnippetEvent; import com.google.android.mobly.snippet.rpc.AsyncRpc; import com.google.android.mobly.snippet.rpc.Rpc; - import java.util.ArrayList; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.TimeUnit; +import org.json.JSONObject; /** Snippet class for SMS RPCs. */ public class SmsSnippet implements Snippet { @@ -46,12 +44,12 @@ public class SmsSnippet implements Snippet { private static class SmsSnippetException extends Exception { private static final long serialVersionUID = 1L; - public SmsSnippetException(String msg) { + SmsSnippetException(String msg) { super(msg); } } - private static final int MAX_CHAR_COUNT_PER_SMS = 160; + private static final int MAX_CHAR_COUNT_PER_SMS = 160; private static final String SMS_SENT_ACTION = ".SMS_SENT"; private static final int DEFAULT_TIMEOUT_MILLISECOND = 60 * 1000; private static final String SMS_RECEIVED_EVENT_NAME = "ReceivedSms"; @@ -71,13 +69,12 @@ public class SmsSnippet implements Snippet { /** * Send SMS and return after waiting for send confirmation (with a timeout of 60 seconds). * - * @param phoneNumber A String representing phone number with country code. + * @param phoneNumber A String representing phone number with country code. * @param message A String representing the message to send. * @throws SmsSnippetException on SMS send error. */ @Rpc(description = "Send SMS to a specified phone number.") - public void sendSms(String phoneNumber, String message) - throws SmsSnippetException { + public void sendSms(String phoneNumber, String message) throws Throwable { String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter); OutboundSmsReceiver receiver = new OutboundSmsReceiver(mContext, callbackId); @@ -85,33 +82,25 @@ public class SmsSnippet implements Snippet { ArrayList parts = mSmsManager.divideMessage(message); ArrayList sIntents = new ArrayList<>(); for (int i = 0; i < parts.size(); i++) { - sIntents.add(PendingIntent.getBroadcast( - mContext, 0, new Intent(SMS_SENT_ACTION), 0)); + sIntents.add( + PendingIntent.getBroadcast(mContext, 0, new Intent(SMS_SENT_ACTION), 0)); } receiver.setExpectedMessageCount(parts.size()); mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION)); mSmsManager.sendMultipartTextMessage(phoneNumber, null, parts, sIntents, null); } else { - PendingIntent sentIntent = PendingIntent.getBroadcast( - mContext, 0, new Intent(SMS_SENT_ACTION), 0); + PendingIntent sentIntent = + PendingIntent.getBroadcast(mContext, 0, new Intent(SMS_SENT_ACTION), 0); receiver.setExpectedMessageCount(1); mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION)); mSmsManager.sendTextMessage(phoneNumber, null, message, sentIntent, null); } - String qId = EventCache.getQueueId(callbackId, SMS_SENT_EVENT_NAME); - LinkedBlockingDeque q = EventCache.getInstance().getEventDeque(qId); - SnippetEvent result; - - try { - result = q.pollFirst(DEFAULT_TIMEOUT_MILLISECOND, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - throw new SmsSnippetException("Did not receive SMS sent confirmation event."); - } + SnippetEvent result = + Utils.waitForSnippetEvent( + callbackId, SMS_SENT_EVENT_NAME, DEFAULT_TIMEOUT_MILLISECOND); - if (result == null) { - throw new SmsSnippetException("Timed out waiting for SMS sent confirmation event."); - } else if (result.getData().containsKey("error")) { + if (result.getData().containsKey("error")) { throw new SmsSnippetException( "Failed to send SMS, error code: " + result.getData().getInt("error")); } @@ -124,6 +113,17 @@ public class SmsSnippet implements Snippet { mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION)); } + @TargetApi(Build.VERSION_CODES.KITKAT) + @Rpc(description = "Wait for incoming SMS message.") + public JSONObject waitForSms() throws Throwable { + String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter); + SmsReceiver receiver = new SmsReceiver(mContext, callbackId); + mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION)); + return Utils.waitForSnippetEvent( + callbackId, SMS_RECEIVED_EVENT_NAME, DEFAULT_TIMEOUT_MILLISECOND) + .toJson(); + } + @Override public void shutdown() {} @@ -140,7 +140,9 @@ public class SmsSnippet implements Snippet { mExpectedMessageCount = 0; } - public void setExpectedMessageCount(int count) { mExpectedMessageCount = count; } + public void setExpectedMessageCount(int count) { + mExpectedMessageCount = count; + } @Override public void onReceive(Context context, Intent intent) { @@ -148,7 +150,7 @@ public class SmsSnippet implements Snippet { if (SMS_SENT_ACTION.equals(action)) { SnippetEvent event = new SnippetEvent(mCallbackId, SMS_SENT_EVENT_NAME); - switch(getResultCode()) { + switch (getResultCode()) { case Activity.RESULT_OK: if (mExpectedMessageCount == 1) { event.getData().putBoolean("sent", true); @@ -156,7 +158,7 @@ public class SmsSnippet implements Snippet { mContext.unregisterReceiver(this); } - if (mExpectedMessageCount > 0 ) { + if (mExpectedMessageCount > 0) { mExpectedMessageCount--; } break; 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 index 8f4f7d6..c68ae5a 100644 --- 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 @@ -16,10 +16,17 @@ package com.google.android.mobly.snippet.bundled.utils; +import com.google.android.mobly.snippet.bundled.SmsSnippet; +import com.google.android.mobly.snippet.event.EventCache; +import com.google.android.mobly.snippet.event.SnippetEvent; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Locale; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; public final class Utils { @@ -55,6 +62,42 @@ public final class Utils { return false; } + /** + * Wait on a specific snippet event. + * + *

This allows a snippet to wait on another SnippetEvent as long as they know the name and + * callback id. Commonly used to make async calls synchronous, see {@link + * SmsSnippet#waitForSms()} waitForSms} for example usage. + * + * @param callbackId String callbackId that we want to wait on. + * @param eventName String event name that we are waiting on. + * @param timeout int timeout in milliseconds for how long it will wait for the event. + * @return SnippetEvent if one was received. + * @throws Throwable if interrupted while polling for event completion. Throws TimeoutException + * if no snippet event is received. + */ + public static SnippetEvent waitForSnippetEvent( + String callbackId, String eventName, Integer timeout) throws Throwable { + String qId = EventCache.getQueueId(callbackId, eventName); + LinkedBlockingDeque q = EventCache.getInstance().getEventDeque(qId); + SnippetEvent result; + try { + result = q.pollFirst(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw e.getCause(); + } + + if (result == null) { + throw new TimeoutException( + String.format( + Locale.ROOT, + "Timed out waiting(%d millis) for SnippetEvent: %s", + timeout, + callbackId)); + } + return result; + } + /** * A function interface that is used by lambda functions signaling an async operation is still * going on. -- cgit v1.2.3 From 7f7798eb4b720054f40e0f507c0b2824cabbb405 Mon Sep 17 00:00:00 2001 From: Keith Dart Date: Thu, 14 Sep 2017 11:10:06 -0700 Subject: Add a networking snippet module. (#78) Has one method for now, Connectable, that implicitly checks TCP connectivity to a host and port. --- .../mobly/snippet/bundled/NetworkingSnippet.java | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java new file mode 100644 index 0000000..d72205a --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java @@ -0,0 +1,52 @@ +/* + * 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 java.net.InetAddress; +import java.net.Socket; +import java.io.IOException; +import java.net.UnknownHostException; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.util.Log; + +/** Snippet class for networking RPCs. */ +public class NetworkingSnippet implements Snippet { + + @Rpc(description = "Check if a host and port are connectable using a TCP connection attempt.") + public boolean networkIsTcpConnectable(String host, int port) { + InetAddress addr; + try { + addr = InetAddress.getByName(host); + } catch (UnknownHostException uherr) { + Log.d("Host name lookup failure: " + uherr.getMessage()); + return false; + } + + try { + Socket sock = new Socket(addr, port); + sock.close(); + } catch (IOException ioerr) { + Log.d("Did not make connection to host: " + ioerr.getMessage()); + return false; + } + return true; + } + + @Override + public void shutdown() {} +} -- cgit v1.2.3 From 2797e51535fd0906326ba8785159d6d2f96f1e7d Mon Sep 17 00:00:00 2001 From: Keith Dart Date: Thu, 28 Sep 2017 17:54:57 -0700 Subject: Add Rpcs needed to download files via HTTP. (#80) * Add an Rpc to perform an HTTP download using DownloadManager. * Add file operation Rpcs. --- .../android/mobly/snippet/bundled/FileSnippet.java | 75 +++++++++++++++++++ .../mobly/snippet/bundled/NetworkingSnippet.java | 87 +++++++++++++++++++++- .../android/mobly/snippet/bundled/utils/Utils.java | 22 ++++++ 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java new file mode 100644 index 0000000..571d57f --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java @@ -0,0 +1,75 @@ +/* + * 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 java.io.IOException; +import java.security.MessageDigest; +import java.security.DigestInputStream; +import java.security.NoSuchAlgorithmException; +import android.content.Context; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +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 for File and abstract storage URI operation RPCs. */ +public class FileSnippet implements Snippet { + + private final Context mContext; + + public FileSnippet() { + mContext = InstrumentationRegistry.getContext(); + } + + private static class FileSnippetException extends Exception { + + private static final long serialVersionUID = 8081L; + + public FileSnippetException(String msg) { + super(msg); + } + } + + @Rpc(description = "Compute MD5 hash on a content URI. Return the MD5 has has a hex string.") + public String fileMd5Hash(String uri) throws IOException, NoSuchAlgorithmException { + Uri uri_ = Uri.parse(uri); + ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(uri_, "r"); + MessageDigest md = MessageDigest.getInstance("MD5"); + int length = (int) pfd.getStatSize(); + byte[] buf = new byte[length]; + ParcelFileDescriptor.AutoCloseInputStream stream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); + DigestInputStream dis = new DigestInputStream(stream, md); + try { + dis.read(buf, 0, length); + return Utils.bytesToHexString(md.digest()); + } finally { + dis.close(); + stream.close(); + } + } + + @Rpc(description = "Remove a file pointed to by the content URI.") + public void fileDeleteContent(String uri) { + Uri uri_ = Uri.parse(uri); + mContext.getContentResolver().delete(uri_, null, null); + } + + @Override + public void shutdown() {} +} diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java index d72205a..f175fdd 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java @@ -16,17 +16,47 @@ package com.google.android.mobly.snippet.bundled; +import java.util.List; import java.net.InetAddress; import java.net.Socket; import java.io.IOException; import java.net.UnknownHostException; +import android.content.Intent; +import android.content.Context; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.net.Uri; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.app.DownloadManager; +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; import com.google.android.mobly.snippet.util.Log; /** Snippet class for networking RPCs. */ public class NetworkingSnippet implements Snippet { + private final Context mContext; + private final DownloadManager mDownloadManager; + private volatile boolean mIsDownloadComplete = false; + private volatile long mReqid = 0; + + public NetworkingSnippet() { + mContext = InstrumentationRegistry.getContext(); + mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); + } + + private static class NetworkingSnippetException extends Exception { + + private static final long serialVersionUID = 8080L; + + public NetworkingSnippetException(String msg) { + super(msg); + } + } + @Rpc(description = "Check if a host and port are connectable using a TCP connection attempt.") public boolean networkIsTcpConnectable(String host, int port) { InetAddress addr; @@ -47,6 +77,61 @@ public class NetworkingSnippet implements Snippet { return true; } + @Rpc(description = "Download a file using HTTP. Return content Uri (file remains on device). " + + "The Uri should be treated as an opaque handle for further operations.") + public String networkHttpDownload(String url) throws IllegalArgumentException, NetworkingSnippetException { + + Uri uri = Uri.parse(url); + List pathsegments = uri.getPathSegments(); + if (pathsegments.size() < 1) { + throw new IllegalArgumentException(String.format("The Uri %s does not have a path.", uri.toString())); + } + DownloadManager.Request request = new DownloadManager.Request(uri); + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, + pathsegments.get(pathsegments.size() - 1)); + mIsDownloadComplete = false; + mReqid = 0; + IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + BroadcastReceiver receiver = new DownloadReceiver(); + mContext.registerReceiver(receiver, filter); + try { + mReqid = mDownloadManager.enqueue(request); + Log.d(String.format("networkHttpDownload download of %s with id %d", url, mReqid)); + if (!Utils.waitUntil(() -> mIsDownloadComplete, 30)) { + Log.d(String.format("networkHttpDownload timed out waiting for completion")); + throw new NetworkingSnippetException("networkHttpDownload timed out."); + } + } finally { + mContext.unregisterReceiver(receiver); + } + Uri resp = mDownloadManager.getUriForDownloadedFile(mReqid); + if (resp != null) { + Log.d(String.format("networkHttpDownload completed to %s", resp.toString())); + mReqid = 0; + return resp.toString(); + } else { + Log.d(String.format("networkHttpDownload Failed to download %s", uri.toString())); + throw new NetworkingSnippetException("networkHttpDownload didn't get downloaded file."); + } + } + + private class DownloadReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + long gotid = (long) intent.getExtras().get("extra_download_id"); + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action) + && gotid == mReqid) { + mIsDownloadComplete = true; + } + } + } + @Override - public void shutdown() {} + public void shutdown() { + if (mReqid != 0) { + mDownloadManager.remove(mReqid); + } + } } 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 index c68ae5a..e4c0dbd 100644 --- 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 @@ -30,6 +30,8 @@ import java.util.concurrent.TimeoutException; public final class Utils { + private final static char[] hexArray = "0123456789abcdef".toCharArray(); + private Utils() {} /** @@ -190,4 +192,24 @@ public final class Utils { throw e.getCause(); } } + + /** + * Convert a byte array (binary data) to a hexadecimal string (ASCII) + * representation. + + * [\x01\x02] -> "0102" + * + * @param bytes The array of byte to convert. + * @return a String with the ASCII hex representation. + */ + public static String bytesToHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + } -- cgit v1.2.3 From 37402446cd74da1a346469c03e09900d99673b8a Mon Sep 17 00:00:00 2001 From: Keith Dart Date: Mon, 2 Oct 2017 19:49:41 -0700 Subject: Make linter happy. (#81) * Add `presubmit` action to run formatter and lint. --- .../mobly/snippet/bundled/AccountSnippet.java | 2 + .../android/mobly/snippet/bundled/FileSnippet.java | 13 ++--- .../mobly/snippet/bundled/NetworkingSnippet.java | 57 ++++++++++++++-------- .../mobly/snippet/bundled/WifiManagerSnippet.java | 8 +-- .../android/mobly/snippet/bundled/utils/Utils.java | 12 ++--- 5 files changed, 55 insertions(+), 37 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index 627d1b4..f8499b4 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -34,6 +34,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -112,6 +113,7 @@ public class AccountSnippet implements Snippet { if (result.containsKey(AccountManager.KEY_ERROR_CODE)) { throw new AccountSnippetException( String.format( + Locale.US, "Failed to add account due to code %d: %s", result.getInt(AccountManager.KEY_ERROR_CODE), result.getString(AccountManager.KEY_ERROR_MESSAGE))); diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java index 571d57f..f6b6918 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java @@ -16,10 +16,6 @@ package com.google.android.mobly.snippet.bundled; -import java.io.IOException; -import java.security.MessageDigest; -import java.security.DigestInputStream; -import java.security.NoSuchAlgorithmException; import android.content.Context; import android.net.Uri; import android.os.ParcelFileDescriptor; @@ -27,6 +23,10 @@ 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; +import java.io.IOException; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; /** Snippet class for File and abstract storage URI operation RPCs. */ public class FileSnippet implements Snippet { @@ -47,13 +47,14 @@ public class FileSnippet implements Snippet { } @Rpc(description = "Compute MD5 hash on a content URI. Return the MD5 has has a hex string.") - public String fileMd5Hash(String uri) throws IOException, NoSuchAlgorithmException { + public String fileMd5Hash(String uri) throws IOException, NoSuchAlgorithmException { Uri uri_ = Uri.parse(uri); ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(uri_, "r"); MessageDigest md = MessageDigest.getInstance("MD5"); int length = (int) pfd.getStatSize(); byte[] buf = new byte[length]; - ParcelFileDescriptor.AutoCloseInputStream stream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); + ParcelFileDescriptor.AutoCloseInputStream stream = + new ParcelFileDescriptor.AutoCloseInputStream(pfd); DigestInputStream dis = new DigestInputStream(stream, md); try { dis.read(buf, 0, length); diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java index f175fdd..e89fc12 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java @@ -16,24 +16,24 @@ package com.google.android.mobly.snippet.bundled; -import java.util.List; -import java.net.InetAddress; -import java.net.Socket; -import java.io.IOException; -import java.net.UnknownHostException; -import android.content.Intent; +import android.app.DownloadManager; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; import android.content.IntentFilter; -import android.content.BroadcastReceiver; import android.net.Uri; import android.os.Environment; -import android.os.ParcelFileDescriptor; -import android.app.DownloadManager; 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; import com.google.android.mobly.snippet.util.Log; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.List; +import java.util.Locale; /** Snippet class for networking RPCs. */ public class NetworkingSnippet implements Snippet { @@ -77,18 +77,23 @@ public class NetworkingSnippet implements Snippet { return true; } - @Rpc(description = "Download a file using HTTP. Return content Uri (file remains on device). " - + "The Uri should be treated as an opaque handle for further operations.") - public String networkHttpDownload(String url) throws IllegalArgumentException, NetworkingSnippetException { + @Rpc( + description = + "Download a file using HTTP. Return content Uri (file remains on device). " + + "The Uri should be treated as an opaque handle for further operations." + ) + public String networkHttpDownload(String url) + throws IllegalArgumentException, NetworkingSnippetException { Uri uri = Uri.parse(url); List pathsegments = uri.getPathSegments(); if (pathsegments.size() < 1) { - throw new IllegalArgumentException(String.format("The Uri %s does not have a path.", uri.toString())); + throw new IllegalArgumentException( + String.format(Locale.US, "The Uri %s does not have a path.", uri.toString())); } DownloadManager.Request request = new DownloadManager.Request(uri); - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, - pathsegments.get(pathsegments.size() - 1)); + request.setDestinationInExternalPublicDir( + Environment.DIRECTORY_DOWNLOADS, pathsegments.get(pathsegments.size() - 1)); mIsDownloadComplete = false; mReqid = 0; IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); @@ -96,9 +101,16 @@ public class NetworkingSnippet implements Snippet { mContext.registerReceiver(receiver, filter); try { mReqid = mDownloadManager.enqueue(request); - Log.d(String.format("networkHttpDownload download of %s with id %d", url, mReqid)); + Log.d( + String.format( + Locale.US, + "networkHttpDownload download of %s with id %d", + url, + mReqid)); if (!Utils.waitUntil(() -> mIsDownloadComplete, 30)) { - Log.d(String.format("networkHttpDownload timed out waiting for completion")); + Log.d( + String.format( + Locale.US, "networkHttpDownload timed out waiting for completion")); throw new NetworkingSnippetException("networkHttpDownload timed out."); } } finally { @@ -106,11 +118,15 @@ public class NetworkingSnippet implements Snippet { } Uri resp = mDownloadManager.getUriForDownloadedFile(mReqid); if (resp != null) { - Log.d(String.format("networkHttpDownload completed to %s", resp.toString())); + Log.d(String.format(Locale.US, "networkHttpDownload completed to %s", resp.toString())); mReqid = 0; return resp.toString(); } else { - Log.d(String.format("networkHttpDownload Failed to download %s", uri.toString())); + Log.d( + String.format( + Locale.US, + "networkHttpDownload Failed to download %s", + uri.toString())); throw new NetworkingSnippetException("networkHttpDownload didn't get downloaded file."); } } @@ -121,8 +137,7 @@ public class NetworkingSnippet implements Snippet { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); long gotid = (long) intent.getExtras().get("extra_download_id"); - if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action) - && gotid == mReqid) { + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action) && gotid == mReqid) { mIsDownloadComplete = true; } } 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 index 6614904..83a42f6 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -57,7 +57,9 @@ public class WifiManagerSnippet implements Snippet { public WifiManagerSnippet() { mContext = InstrumentationRegistry.getContext(); - mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + mWifiManager = + (WifiManager) + mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); } @Rpc( @@ -196,8 +198,8 @@ public class WifiManagerSnippet implements Snippet { String SSID = wifiConfig.SSID; // Return directly if network is already connected. WifiInfo connectionInfo = mWifiManager.getConnectionInfo(); - if (connectionInfo.getNetworkId() != -1 && connectionInfo.getSSID().equals(wifiConfig.SSID)) - { + if (connectionInfo.getNetworkId() != -1 + && connectionInfo.getSSID().equals(wifiConfig.SSID)) { Log.d("Network " + connectionInfo.getSSID() + " is already connected."); return; } 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 index e4c0dbd..376bcb5 100644 --- 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 @@ -30,7 +30,7 @@ import java.util.concurrent.TimeoutException; public final class Utils { - private final static char[] hexArray = "0123456789abcdef".toCharArray(); + private static final char[] hexArray = "0123456789abcdef".toCharArray(); private Utils() {} @@ -194,22 +194,20 @@ public final class Utils { } /** - * Convert a byte array (binary data) to a hexadecimal string (ASCII) - * representation. - - * [\x01\x02] -> "0102" + * Convert a byte array (binary data) to a hexadecimal string (ASCII) representation. + * + *

[\x01\x02] -> "0102" * * @param bytes The array of byte to convert. * @return a String with the ASCII hex representation. */ public static String bytesToHexString(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; - for ( int j = 0; j < bytes.length; j++ ) { + for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); } - } -- cgit v1.2.3 From 212c2df7eb4a4938069be9061c88170b160058e8 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Wed, 4 Oct 2017 13:43:36 -0700 Subject: Revert "Make linter happy. (#81)" (#85) This reverts commit 37402446cd74da1a346469c03e09900d99673b8a. --- .../mobly/snippet/bundled/AccountSnippet.java | 2 - .../android/mobly/snippet/bundled/FileSnippet.java | 13 +++-- .../mobly/snippet/bundled/NetworkingSnippet.java | 57 ++++++++-------------- .../mobly/snippet/bundled/WifiManagerSnippet.java | 8 ++- .../android/mobly/snippet/bundled/utils/Utils.java | 12 +++-- 5 files changed, 37 insertions(+), 55 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index f8499b4..627d1b4 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -34,7 +34,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -113,7 +112,6 @@ public class AccountSnippet implements Snippet { if (result.containsKey(AccountManager.KEY_ERROR_CODE)) { throw new AccountSnippetException( String.format( - Locale.US, "Failed to add account due to code %d: %s", result.getInt(AccountManager.KEY_ERROR_CODE), result.getString(AccountManager.KEY_ERROR_MESSAGE))); diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java index f6b6918..571d57f 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java @@ -16,6 +16,10 @@ package com.google.android.mobly.snippet.bundled; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.DigestInputStream; +import java.security.NoSuchAlgorithmException; import android.content.Context; import android.net.Uri; import android.os.ParcelFileDescriptor; @@ -23,10 +27,6 @@ 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; -import java.io.IOException; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; /** Snippet class for File and abstract storage URI operation RPCs. */ public class FileSnippet implements Snippet { @@ -47,14 +47,13 @@ public class FileSnippet implements Snippet { } @Rpc(description = "Compute MD5 hash on a content URI. Return the MD5 has has a hex string.") - public String fileMd5Hash(String uri) throws IOException, NoSuchAlgorithmException { + public String fileMd5Hash(String uri) throws IOException, NoSuchAlgorithmException { Uri uri_ = Uri.parse(uri); ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(uri_, "r"); MessageDigest md = MessageDigest.getInstance("MD5"); int length = (int) pfd.getStatSize(); byte[] buf = new byte[length]; - ParcelFileDescriptor.AutoCloseInputStream stream = - new ParcelFileDescriptor.AutoCloseInputStream(pfd); + ParcelFileDescriptor.AutoCloseInputStream stream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); DigestInputStream dis = new DigestInputStream(stream, md); try { dis.read(buf, 0, length); diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java index e89fc12..f175fdd 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java @@ -16,24 +16,24 @@ package com.google.android.mobly.snippet.bundled; -import android.app.DownloadManager; -import android.content.BroadcastReceiver; -import android.content.Context; +import java.util.List; +import java.net.InetAddress; +import java.net.Socket; +import java.io.IOException; +import java.net.UnknownHostException; import android.content.Intent; +import android.content.Context; import android.content.IntentFilter; +import android.content.BroadcastReceiver; import android.net.Uri; import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.app.DownloadManager; 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; import com.google.android.mobly.snippet.util.Log; -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.util.List; -import java.util.Locale; /** Snippet class for networking RPCs. */ public class NetworkingSnippet implements Snippet { @@ -77,23 +77,18 @@ public class NetworkingSnippet implements Snippet { return true; } - @Rpc( - description = - "Download a file using HTTP. Return content Uri (file remains on device). " - + "The Uri should be treated as an opaque handle for further operations." - ) - public String networkHttpDownload(String url) - throws IllegalArgumentException, NetworkingSnippetException { + @Rpc(description = "Download a file using HTTP. Return content Uri (file remains on device). " + + "The Uri should be treated as an opaque handle for further operations.") + public String networkHttpDownload(String url) throws IllegalArgumentException, NetworkingSnippetException { Uri uri = Uri.parse(url); List pathsegments = uri.getPathSegments(); if (pathsegments.size() < 1) { - throw new IllegalArgumentException( - String.format(Locale.US, "The Uri %s does not have a path.", uri.toString())); + throw new IllegalArgumentException(String.format("The Uri %s does not have a path.", uri.toString())); } DownloadManager.Request request = new DownloadManager.Request(uri); - request.setDestinationInExternalPublicDir( - Environment.DIRECTORY_DOWNLOADS, pathsegments.get(pathsegments.size() - 1)); + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, + pathsegments.get(pathsegments.size() - 1)); mIsDownloadComplete = false; mReqid = 0; IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); @@ -101,16 +96,9 @@ public class NetworkingSnippet implements Snippet { mContext.registerReceiver(receiver, filter); try { mReqid = mDownloadManager.enqueue(request); - Log.d( - String.format( - Locale.US, - "networkHttpDownload download of %s with id %d", - url, - mReqid)); + Log.d(String.format("networkHttpDownload download of %s with id %d", url, mReqid)); if (!Utils.waitUntil(() -> mIsDownloadComplete, 30)) { - Log.d( - String.format( - Locale.US, "networkHttpDownload timed out waiting for completion")); + Log.d(String.format("networkHttpDownload timed out waiting for completion")); throw new NetworkingSnippetException("networkHttpDownload timed out."); } } finally { @@ -118,15 +106,11 @@ public class NetworkingSnippet implements Snippet { } Uri resp = mDownloadManager.getUriForDownloadedFile(mReqid); if (resp != null) { - Log.d(String.format(Locale.US, "networkHttpDownload completed to %s", resp.toString())); + Log.d(String.format("networkHttpDownload completed to %s", resp.toString())); mReqid = 0; return resp.toString(); } else { - Log.d( - String.format( - Locale.US, - "networkHttpDownload Failed to download %s", - uri.toString())); + Log.d(String.format("networkHttpDownload Failed to download %s", uri.toString())); throw new NetworkingSnippetException("networkHttpDownload didn't get downloaded file."); } } @@ -137,7 +121,8 @@ public class NetworkingSnippet implements Snippet { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); long gotid = (long) intent.getExtras().get("extra_download_id"); - if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action) && gotid == mReqid) { + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action) + && gotid == mReqid) { mIsDownloadComplete = true; } } 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 index 83a42f6..6614904 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -57,9 +57,7 @@ public class WifiManagerSnippet implements Snippet { public WifiManagerSnippet() { mContext = InstrumentationRegistry.getContext(); - mWifiManager = - (WifiManager) - mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); } @Rpc( @@ -198,8 +196,8 @@ public class WifiManagerSnippet implements Snippet { String SSID = wifiConfig.SSID; // Return directly if network is already connected. WifiInfo connectionInfo = mWifiManager.getConnectionInfo(); - if (connectionInfo.getNetworkId() != -1 - && connectionInfo.getSSID().equals(wifiConfig.SSID)) { + if (connectionInfo.getNetworkId() != -1 && connectionInfo.getSSID().equals(wifiConfig.SSID)) + { Log.d("Network " + connectionInfo.getSSID() + " is already connected."); return; } 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 index 376bcb5..e4c0dbd 100644 --- 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 @@ -30,7 +30,7 @@ import java.util.concurrent.TimeoutException; public final class Utils { - private static final char[] hexArray = "0123456789abcdef".toCharArray(); + private final static char[] hexArray = "0123456789abcdef".toCharArray(); private Utils() {} @@ -194,20 +194,22 @@ public final class Utils { } /** - * Convert a byte array (binary data) to a hexadecimal string (ASCII) representation. - * - *

[\x01\x02] -> "0102" + * Convert a byte array (binary data) to a hexadecimal string (ASCII) + * representation. + + * [\x01\x02] -> "0102" * * @param bytes The array of byte to convert. * @return a String with the ASCII hex representation. */ public static String bytesToHexString(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { + for ( int j = 0; j < bytes.length; j++ ) { int v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); } + } -- cgit v1.2.3 From 953365323e977ebc0c459c6efde4bfc5a327356b Mon Sep 17 00:00:00 2001 From: Ang Li Date: Thu, 5 Oct 2017 10:10:12 -0700 Subject: Fix a misuse of enum in Ble scan record. (#86) * Don't use advertising enum for scan record. --- .../google/android/mobly/snippet/bundled/utils/JsonSerializer.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 7e5ca5b..ef7f7d7 100644 --- 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 @@ -185,9 +185,7 @@ public class JsonSerializer { private Bundle serializeBleScanRecord(ScanRecord record) { Bundle result = new Bundle(); result.putString("DeviceName", record.getDeviceName()); - result.putString( - "TxPowerLevel", - MbsEnums.BLE_ADVERTISE_TX_POWER.getString(record.getTxPowerLevel())); + result.putInt("TxPowerLevel", record.getTxPowerLevel()); return result; } -- cgit v1.2.3 From 1bcd42813b8b60ac030d036bda2bffeeff87cf49 Mon Sep 17 00:00:00 2001 From: Keith Dart Date: Thu, 5 Oct 2017 12:14:42 -0700 Subject: Fix version warnings for gradle builds (#87) * Add a `presubmit` target to run formatter and lint. * Add dev process to README * Gradle config improvements * Changes made by linter --- .../mobly/snippet/bundled/AccountSnippet.java | 2 + .../android/mobly/snippet/bundled/FileSnippet.java | 13 ++--- .../mobly/snippet/bundled/NetworkingSnippet.java | 57 ++++++++++++++-------- .../mobly/snippet/bundled/WifiManagerSnippet.java | 8 +-- .../android/mobly/snippet/bundled/utils/Utils.java | 12 ++--- 5 files changed, 55 insertions(+), 37 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index 627d1b4..f8499b4 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -34,6 +34,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -112,6 +113,7 @@ public class AccountSnippet implements Snippet { if (result.containsKey(AccountManager.KEY_ERROR_CODE)) { throw new AccountSnippetException( String.format( + Locale.US, "Failed to add account due to code %d: %s", result.getInt(AccountManager.KEY_ERROR_CODE), result.getString(AccountManager.KEY_ERROR_MESSAGE))); diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java index 571d57f..f6b6918 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java @@ -16,10 +16,6 @@ package com.google.android.mobly.snippet.bundled; -import java.io.IOException; -import java.security.MessageDigest; -import java.security.DigestInputStream; -import java.security.NoSuchAlgorithmException; import android.content.Context; import android.net.Uri; import android.os.ParcelFileDescriptor; @@ -27,6 +23,10 @@ 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; +import java.io.IOException; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; /** Snippet class for File and abstract storage URI operation RPCs. */ public class FileSnippet implements Snippet { @@ -47,13 +47,14 @@ public class FileSnippet implements Snippet { } @Rpc(description = "Compute MD5 hash on a content URI. Return the MD5 has has a hex string.") - public String fileMd5Hash(String uri) throws IOException, NoSuchAlgorithmException { + public String fileMd5Hash(String uri) throws IOException, NoSuchAlgorithmException { Uri uri_ = Uri.parse(uri); ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(uri_, "r"); MessageDigest md = MessageDigest.getInstance("MD5"); int length = (int) pfd.getStatSize(); byte[] buf = new byte[length]; - ParcelFileDescriptor.AutoCloseInputStream stream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); + ParcelFileDescriptor.AutoCloseInputStream stream = + new ParcelFileDescriptor.AutoCloseInputStream(pfd); DigestInputStream dis = new DigestInputStream(stream, md); try { dis.read(buf, 0, length); diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java index f175fdd..e89fc12 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java @@ -16,24 +16,24 @@ package com.google.android.mobly.snippet.bundled; -import java.util.List; -import java.net.InetAddress; -import java.net.Socket; -import java.io.IOException; -import java.net.UnknownHostException; -import android.content.Intent; +import android.app.DownloadManager; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; import android.content.IntentFilter; -import android.content.BroadcastReceiver; import android.net.Uri; import android.os.Environment; -import android.os.ParcelFileDescriptor; -import android.app.DownloadManager; 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; import com.google.android.mobly.snippet.util.Log; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.List; +import java.util.Locale; /** Snippet class for networking RPCs. */ public class NetworkingSnippet implements Snippet { @@ -77,18 +77,23 @@ public class NetworkingSnippet implements Snippet { return true; } - @Rpc(description = "Download a file using HTTP. Return content Uri (file remains on device). " - + "The Uri should be treated as an opaque handle for further operations.") - public String networkHttpDownload(String url) throws IllegalArgumentException, NetworkingSnippetException { + @Rpc( + description = + "Download a file using HTTP. Return content Uri (file remains on device). " + + "The Uri should be treated as an opaque handle for further operations." + ) + public String networkHttpDownload(String url) + throws IllegalArgumentException, NetworkingSnippetException { Uri uri = Uri.parse(url); List pathsegments = uri.getPathSegments(); if (pathsegments.size() < 1) { - throw new IllegalArgumentException(String.format("The Uri %s does not have a path.", uri.toString())); + throw new IllegalArgumentException( + String.format(Locale.US, "The Uri %s does not have a path.", uri.toString())); } DownloadManager.Request request = new DownloadManager.Request(uri); - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, - pathsegments.get(pathsegments.size() - 1)); + request.setDestinationInExternalPublicDir( + Environment.DIRECTORY_DOWNLOADS, pathsegments.get(pathsegments.size() - 1)); mIsDownloadComplete = false; mReqid = 0; IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); @@ -96,9 +101,16 @@ public class NetworkingSnippet implements Snippet { mContext.registerReceiver(receiver, filter); try { mReqid = mDownloadManager.enqueue(request); - Log.d(String.format("networkHttpDownload download of %s with id %d", url, mReqid)); + Log.d( + String.format( + Locale.US, + "networkHttpDownload download of %s with id %d", + url, + mReqid)); if (!Utils.waitUntil(() -> mIsDownloadComplete, 30)) { - Log.d(String.format("networkHttpDownload timed out waiting for completion")); + Log.d( + String.format( + Locale.US, "networkHttpDownload timed out waiting for completion")); throw new NetworkingSnippetException("networkHttpDownload timed out."); } } finally { @@ -106,11 +118,15 @@ public class NetworkingSnippet implements Snippet { } Uri resp = mDownloadManager.getUriForDownloadedFile(mReqid); if (resp != null) { - Log.d(String.format("networkHttpDownload completed to %s", resp.toString())); + Log.d(String.format(Locale.US, "networkHttpDownload completed to %s", resp.toString())); mReqid = 0; return resp.toString(); } else { - Log.d(String.format("networkHttpDownload Failed to download %s", uri.toString())); + Log.d( + String.format( + Locale.US, + "networkHttpDownload Failed to download %s", + uri.toString())); throw new NetworkingSnippetException("networkHttpDownload didn't get downloaded file."); } } @@ -121,8 +137,7 @@ public class NetworkingSnippet implements Snippet { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); long gotid = (long) intent.getExtras().get("extra_download_id"); - if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action) - && gotid == mReqid) { + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action) && gotid == mReqid) { mIsDownloadComplete = true; } } 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 index 6614904..83a42f6 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -57,7 +57,9 @@ public class WifiManagerSnippet implements Snippet { public WifiManagerSnippet() { mContext = InstrumentationRegistry.getContext(); - mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + mWifiManager = + (WifiManager) + mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); } @Rpc( @@ -196,8 +198,8 @@ public class WifiManagerSnippet implements Snippet { String SSID = wifiConfig.SSID; // Return directly if network is already connected. WifiInfo connectionInfo = mWifiManager.getConnectionInfo(); - if (connectionInfo.getNetworkId() != -1 && connectionInfo.getSSID().equals(wifiConfig.SSID)) - { + if (connectionInfo.getNetworkId() != -1 + && connectionInfo.getSSID().equals(wifiConfig.SSID)) { Log.d("Network " + connectionInfo.getSSID() + " is already connected."); return; } 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 index e4c0dbd..376bcb5 100644 --- 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 @@ -30,7 +30,7 @@ import java.util.concurrent.TimeoutException; public final class Utils { - private final static char[] hexArray = "0123456789abcdef".toCharArray(); + private static final char[] hexArray = "0123456789abcdef".toCharArray(); private Utils() {} @@ -194,22 +194,20 @@ public final class Utils { } /** - * Convert a byte array (binary data) to a hexadecimal string (ASCII) - * representation. - - * [\x01\x02] -> "0102" + * Convert a byte array (binary data) to a hexadecimal string (ASCII) representation. + * + *

[\x01\x02] -> "0102" * * @param bytes The array of byte to convert. * @return a String with the ASCII hex representation. */ public static String bytesToHexString(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; - for ( int j = 0; j < bytes.length; j++ ) { + for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); } - } -- cgit v1.2.3 From e741403b6199bdfefd2ec5c27548a8ebf7a7f148 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Fri, 15 Dec 2017 13:14:10 -0800 Subject: Handle a possible race condition in wifi/bt toggle. (#91) E.g. if BT was in the process of turning off when we called `btEnable`, BT would finish turning off before turning on again, but the `btEnable` call would timeout before that. Same situation for `btDisable` and wifi toggles. --- .../mobly/snippet/bundled/WifiManagerSnippet.java | 37 +++++++++++++++++++--- .../bundled/bluetooth/BluetoothAdapterSnippet.java | 35 +++++++++++++++++--- 2 files changed, 64 insertions(+), 8 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 83a42f6..f6de71e 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -50,6 +50,7 @@ public class WifiManagerSnippet implements Snippet { } } + private static final int TIMEOUT_TOGGLE_STATE = 30; private final WifiManager mWifiManager; private final Context mContext; private final JsonSerializer mJsonSerializer = new JsonSerializer(); @@ -86,23 +87,51 @@ public class WifiManagerSnippet implements Snippet { @Rpc(description = "Turns on Wi-Fi with a 30s timeout.") public void wifiEnable() throws InterruptedException, WifiManagerSnippetException { + if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED) { + return; + } + // If Wi-Fi is trying to turn off, wait for that to complete before continuing. + if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLING) { + if (!Utils.waitUntil( + () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED, + TIMEOUT_TOGGLE_STATE)) { + Log.e(String.format("Wi-Fi failed to stabilize after %ss.", TIMEOUT_TOGGLE_STATE)); + } + } 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!"); + () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED, + TIMEOUT_TOGGLE_STATE)) { + throw new WifiManagerSnippetException( + String.format( + "Failed to enable Wi-Fi after %ss, timeout!", TIMEOUT_TOGGLE_STATE)); } } @Rpc(description = "Turns off Wi-Fi with a 30s timeout.") public void wifiDisable() throws InterruptedException, WifiManagerSnippetException { + if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED) { + return; + } + // If Wi-Fi is trying to turn on, wait for that to complete before continuing. + if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLING) { + if (!Utils.waitUntil( + () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED, + TIMEOUT_TOGGLE_STATE)) { + Log.e(String.format("Wi-Fi failed to stabilize after %ss.", TIMEOUT_TOGGLE_STATE)); + } + } 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!"); + () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED, + TIMEOUT_TOGGLE_STATE)) { + throw new WifiManagerSnippetException( + String.format( + "Failed to disable Wi-Fi after %ss, timeout!", TIMEOUT_TOGGLE_STATE)); } } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java index 0685923..ffaf1af 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java @@ -28,6 +28,7 @@ import com.google.android.mobly.snippet.Snippet; 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 java.util.List; import java.util.NoSuchElementException; @@ -44,6 +45,8 @@ public class BluetoothAdapterSnippet implements Snippet { } } + // Default timeout in seconds. + private static final int TIMEOUT_TOGGLE_STATE = 30; private final Context mContext; private static final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); private final JsonSerializer mJsonSerializer = new JsonSerializer(); @@ -87,21 +90,45 @@ public class BluetoothAdapterSnippet implements Snippet { @Rpc(description = "Enable bluetooth with a 30s timeout.") public void btEnable() throws BluetoothAdapterSnippetException, InterruptedException { + if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { + return; + } + // If bt is trying to turn off, wait for that to finish before continuing. + if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF) { + if (!Utils.waitUntil( + () -> mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF, + TIMEOUT_TOGGLE_STATE)) { + Log.e(String.format("BT failed to stabilize after %ss.", TIMEOUT_TOGGLE_STATE)); + } + } if (!mBluetoothAdapter.enable()) { throw new BluetoothAdapterSnippetException("Failed to start enabling bluetooth"); } - if (!Utils.waitUntil(() -> mBluetoothAdapter.isEnabled(), 30)) { - throw new BluetoothAdapterSnippetException("Bluetooth did not turn on within 30s."); + if (!Utils.waitUntil(() -> mBluetoothAdapter.isEnabled(), TIMEOUT_TOGGLE_STATE)) { + throw new BluetoothAdapterSnippetException( + String.format("Bluetooth did not turn on within %ss.", TIMEOUT_TOGGLE_STATE)); } } @Rpc(description = "Disable bluetooth with a 30s timeout.") public void btDisable() throws BluetoothAdapterSnippetException, InterruptedException { + if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { + return; + } + // If bt is trying to turn on, wait for that to finish before continuing. + if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_TURNING_ON) { + if (!Utils.waitUntil( + () -> mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON, + TIMEOUT_TOGGLE_STATE)) { + Log.e(String.format("BT failed to stabilize after %ss.", TIMEOUT_TOGGLE_STATE)); + } + } if (!mBluetoothAdapter.disable()) { throw new BluetoothAdapterSnippetException("Failed to start disabling bluetooth"); } - if (!Utils.waitUntil(() -> !mBluetoothAdapter.isEnabled(), 30)) { - throw new BluetoothAdapterSnippetException("Bluetooth did not turn off within 30s."); + if (!Utils.waitUntil(() -> !mBluetoothAdapter.isEnabled(), TIMEOUT_TOGGLE_STATE)) { + throw new BluetoothAdapterSnippetException( + String.format("Bluetooth did not turn off within %ss.", TIMEOUT_TOGGLE_STATE)); } } -- cgit v1.2.3 From 31c3b90be561736ba99d201e3a2d8735f0c9e9c9 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Mon, 26 Feb 2018 13:53:20 -0800 Subject: Fail `wifiConnect` if the network specified does not exist. (#92) --- .../google/android/mobly/snippet/bundled/WifiManagerSnippet.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index f6de71e..15f8029 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -259,7 +259,11 @@ public class WifiManagerSnippet implements Snippet { throw new WifiManagerSnippetException( "Failed to reconnect to Wi-Fi network of ID: " + networkId); } - if (!Utils.waitUntil(() -> mWifiManager.getConnectionInfo().getSSID().equals(SSID), 90)) { + if (!Utils.waitUntil( + () -> + mWifiManager.getConnectionInfo().getSSID().equals(SSID) + && connectionInfo.getNetworkId() != -1, + 90)) { throw new WifiManagerSnippetException( "Failed to connect to Wi-Fi network " + wifiNetworkConfig.toString() -- cgit v1.2.3 From f42e4c4a93e93c8bd9c2c143d4bf7f90c04dd350 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Mon, 26 Feb 2018 15:59:49 -0800 Subject: Use annotation instead of executor for `makeToast`. (#93) --- .../mobly/snippet/bundled/NotificationSnippet.java | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java index 11e6187..a07b231 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java @@ -16,31 +16,19 @@ package com.google.android.mobly.snippet.bundled; -import android.content.Context; -import android.os.Handler; import android.support.test.InstrumentationRegistry; import android.widget.Toast; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.rpc.RunOnUiThread; /** Snippet class exposing Android APIs related to creating notification on screen. */ public class NotificationSnippet implements Snippet { - private final Context mContext; - /** - * Since the APIs here deal with UI, most of them have to be called in a thread that has called - * looper. - */ - private final Handler mHandler; - - public NotificationSnippet() { - mContext = InstrumentationRegistry.getContext(); - mHandler = new Handler(mContext.getMainLooper()); - } - + @RunOnUiThread @Rpc(description = "Make a toast on screen.") public void makeToast(String message) { - mHandler.post(() -> Toast.makeText(mContext, message, Toast.LENGTH_LONG).show()); + Toast.makeText(InstrumentationRegistry.getContext(), message, Toast.LENGTH_LONG).show(); } @Override -- cgit v1.2.3 From 7fa78b2b68e79bd48464e46d137557f01c4f1d8a Mon Sep 17 00:00:00 2001 From: Ang Li Date: Tue, 27 Feb 2018 11:58:59 -0800 Subject: Fix typo in wifiConnect. (#94) --- .../com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 15f8029..768fb4c 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -262,7 +262,7 @@ public class WifiManagerSnippet implements Snippet { if (!Utils.waitUntil( () -> mWifiManager.getConnectionInfo().getSSID().equals(SSID) - && connectionInfo.getNetworkId() != -1, + && mWifiManager.getConnectionInfo().getNetworkId() != -1, 90)) { throw new WifiManagerSnippetException( "Failed to connect to Wi-Fi network " -- cgit v1.2.3 From 770ccb9262f4b38db60c9157678331ea6c41e831 Mon Sep 17 00:00:00 2001 From: "David T.H. Kao" Date: Wed, 9 May 2018 15:52:11 -0700 Subject: Add functionality to specify timeout for waitForSms. (#95) --- .../java/com/google/android/mobly/snippet/bundled/SmsSnippet.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java index 4a38798..ec6d470 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java @@ -115,12 +115,11 @@ public class SmsSnippet implements Snippet { @TargetApi(Build.VERSION_CODES.KITKAT) @Rpc(description = "Wait for incoming SMS message.") - public JSONObject waitForSms() throws Throwable { + public JSONObject waitForSms(int timeoutMillis) throws Throwable { String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter); SmsReceiver receiver = new SmsReceiver(mContext, callbackId); mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION)); - return Utils.waitForSnippetEvent( - callbackId, SMS_RECEIVED_EVENT_NAME, DEFAULT_TIMEOUT_MILLISECOND) + return Utils.waitForSnippetEvent(callbackId, SMS_RECEIVED_EVENT_NAME, timeoutMillis) .toJson(); } -- cgit v1.2.3 From aad933736764c6eafb942524214266d50c123453 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Mon, 21 May 2018 19:47:16 -0700 Subject: Add bt cancel discovery. (#98) `btCancelDiscovery` synchronous Rpc. --- .../bundled/bluetooth/BluetoothAdapterSnippet.java | 32 +++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java index ffaf1af..37e7c5f 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java @@ -52,7 +52,7 @@ public class BluetoothAdapterSnippet implements Snippet { private final JsonSerializer mJsonSerializer = new JsonSerializer(); private static final ConcurrentHashMap mDiscoveryResults = new ConcurrentHashMap<>(); - private volatile boolean mIsScanResultAvailable = false; + private volatile boolean mIsDiscoveryFinished = false; public BluetoothAdapterSnippet() { mContext = InstrumentationRegistry.getContext(); @@ -180,7 +180,7 @@ public class BluetoothAdapterSnippet implements Snippet { mBluetoothAdapter.cancelDiscovery(); } mDiscoveryResults.clear(); - mIsScanResultAvailable = false; + mIsDiscoveryFinished = false; BroadcastReceiver receiver = new BluetoothScanReceiver(); mContext.registerReceiver(receiver, filter); try { @@ -188,7 +188,7 @@ public class BluetoothAdapterSnippet implements Snippet { throw new BluetoothAdapterSnippetException( "Failed to initiate Bluetooth Discovery."); } - if (!Utils.waitUntil(() -> mIsScanResultAvailable, 120)) { + if (!Utils.waitUntil(() -> mIsDiscoveryFinished, 120)) { throw new BluetoothAdapterSnippetException( "Failed to get discovery results after 2 mins, timeout!"); } @@ -214,6 +214,30 @@ public class BluetoothAdapterSnippet implements Snippet { } } + @Rpc(description = "Cancel ongoing bluetooth discovery.") + public void btCancelDiscovery() throws BluetoothAdapterSnippetException { + if (!mBluetoothAdapter.isDiscovering()) { + Log.d("No ongoing bluetooth discovery."); + return; + } + IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + mIsDiscoveryFinished = false; + BroadcastReceiver receiver = new BluetoothScanReceiver(); + mContext.registerReceiver(receiver, filter); + try { + if (!mBluetoothAdapter.cancelDiscovery()) { + throw new BluetoothAdapterSnippetException( + "Failed to initiate to cancel bluetooth discovery."); + } + if (!Utils.waitUntil(() -> mIsDiscoveryFinished, 120)) { + throw new BluetoothAdapterSnippetException( + "Failed to get discovery results after 2 mins, timeout!"); + } + } finally { + mContext.unregisterReceiver(receiver); + } + } + @Rpc(description = "Stop being discoverable in Bluetooth.") public void btStopBeingDiscoverable() throws Throwable { if (!(boolean) @@ -290,7 +314,7 @@ public class BluetoothAdapterSnippet implements Snippet { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { - mIsScanResultAvailable = true; + mIsDiscoveryFinished = true; } else if (BluetoothDevice.ACTION_FOUND.equals(action)) { BluetoothDevice device = (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); -- cgit v1.2.3 From 2e3f116cecba6649979aa916436d90dbace32671 Mon Sep 17 00:00:00 2001 From: sceller Date: Tue, 11 Dec 2018 11:11:22 -0800 Subject: Add RPCs to get/set voice call stream volume (#102) --- .../android/mobly/snippet/bundled/AudioSnippet.java | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java index cf5331f..9871c4b 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -81,6 +81,26 @@ public class AudioSnippet implements Snippet { public void setRingVolume(Integer value) { mAudioManager.setStreamVolume(AudioManager.STREAM_RING, value, 0 /* flags, 0 = no flags */); } + + @Rpc(description = "Gets the voice call volume.") + public int getVoiceCallVolume() { + return mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL); + } + + @Rpc(description = "Gets the maximum voice call volume value.") + public int getVoiceCallMaxVolume() { + return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL); + } + + @Rpc( + description = + "Sets the voice call stream volume. The minimum value is 0. Use getVoiceCallMaxVolume" + + "to determine the maximum." + ) + public void setVoiceCallVolume(Integer value) { + mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, + value, 0 /* flags, 0 = no flags */); + } @Rpc(description = "Silences all audio streams.") public void muteAll() throws Exception { -- cgit v1.2.3 From 0413539e267935009220b19014f8c1185d481c64 Mon Sep 17 00:00:00 2001 From: sceller Date: Tue, 11 Dec 2018 14:23:29 -0800 Subject: Bring line 97 under 100 characters (#103) --- .../google/android/mobly/snippet/bundled/AudioSnippet.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java index 9871c4b..e435a72 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -55,8 +55,8 @@ public class AudioSnippet implements Snippet { @Rpc( description = - "Sets the music stream volume. The minimum value is 0. Use getMusicMaxVolume" - + "to determine the maximum." + "Sets the music stream volume. The minimum value is 0. Use 'getMusicMaxVolume'" + + " to determine the maximum." ) public void setMusicVolume(Integer value) { mAudioManager.setStreamVolume( @@ -75,8 +75,8 @@ public class AudioSnippet implements Snippet { @Rpc( description = - "Sets the ringer stream volume. The minimum value is 0. Use getRingMaxVolume" - + "to determine the maximum." + "Sets the ringer stream volume. The minimum value is 0. Use 'getRingMaxVolume'" + + " to determine the maximum." ) public void setRingVolume(Integer value) { mAudioManager.setStreamVolume(AudioManager.STREAM_RING, value, 0 /* flags, 0 = no flags */); @@ -94,8 +94,8 @@ public class AudioSnippet implements Snippet { @Rpc( description = - "Sets the voice call stream volume. The minimum value is 0. Use getVoiceCallMaxVolume" - + "to determine the maximum." + "Sets the voice call stream volume. The minimum value is 0. Use" + + " 'getVoiceCallMaxVolume' to determine the maximum." ) public void setVoiceCallVolume(Integer value) { mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, -- cgit v1.2.3 From 2fa52efb68751e9729ac733548687889e8cfb884 Mon Sep 17 00:00:00 2001 From: Adam Van Hine <45979414+vanhine@users.noreply.github.com> Date: Tue, 8 Jan 2019 10:55:31 -0800 Subject: Adding APIs to get the External Storage Directory. (#104) --- .../mobly/snippet/bundled/StorageSnippet.java | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/StorageSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/StorageSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/StorageSnippet.java new file mode 100644 index 0000000..23048d4 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/StorageSnippet.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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.os.Environment; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; + +public class StorageSnippet implements Snippet { + + @Rpc(description = "Return the primary shared/external storage directory.") + public String storageGetExternalStorageDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath(); + } + + @Rpc(description = "Return the root of the \"system\" directory.") + public String storageGetRootDirectory() { + return Environment.getRootDirectory().getAbsolutePath(); + } + + @Override + public void shutdown() {} +} -- cgit v1.2.3 From 35e1bfad098f65db344d4d6fd7abb5830d8aa7a2 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Fri, 11 Jan 2019 12:51:57 -0800 Subject: Migrate to androidx packages (#105) --- .../mobly/snippet/bundled/AccountSnippet.java | 8 +-- .../mobly/snippet/bundled/AudioSnippet.java | 44 ++++++++-------- .../android/mobly/snippet/bundled/FileSnippet.java | 4 +- .../mobly/snippet/bundled/NetworkingSnippet.java | 11 ++-- .../mobly/snippet/bundled/NotificationSnippet.java | 8 ++- .../android/mobly/snippet/bundled/SmsSnippet.java | 4 +- .../mobly/snippet/bundled/TelephonySnippet.java | 11 ++-- .../mobly/snippet/bundled/WifiManagerSnippet.java | 60 ++++++++++------------ .../bundled/bluetooth/BluetoothAdapterSnippet.java | 16 +++--- .../bluetooth/profiles/BluetoothA2dpSnippet.java | 11 ++-- 10 files changed, 82 insertions(+), 95 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index f8499b4..c7f576b 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -24,7 +24,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.SyncAdapterType; import android.os.Bundle; -import android.support.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; import com.google.android.mobly.snippet.util.Log; @@ -68,7 +68,7 @@ public class AccountSnippet implements Snippet { private final ReentrantReadWriteLock mLock; public AccountSnippet() { - Context context = InstrumentationRegistry.getContext(); + Context context = InstrumentationRegistry.getInstrumentation().getContext(); mAccountManager = AccountManager.get(context); mSyncStatusObserverHandles = new LinkedList<>(); mSyncWhitelist = new HashMap<>(); @@ -86,8 +86,8 @@ public class AccountSnippet implements Snippet { * @param password Password of the account to add. */ @Rpc( - description = "Add a Google (GMail) account to the device, with account data sync disabled." - ) + description = + "Add a Google (GMail) account to the device, with account data sync disabled.") public void addAccount(String username, String password) throws AccountSnippetException, AccountsException, IOException { // Check for existing account. If we try to re-add an existing account, Android throws an diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java index e435a72..fbdb5d0 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -18,7 +18,7 @@ package com.google.android.mobly.snippet.bundled; import android.content.Context; import android.media.AudioManager; -import android.support.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; import java.lang.reflect.Method; @@ -29,7 +29,7 @@ public class AudioSnippet implements Snippet { private final AudioManager mAudioManager; public AudioSnippet() { - Context context = InstrumentationRegistry.getContext(); + Context context = InstrumentationRegistry.getInstrumentation().getContext(); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); } @@ -44,7 +44,7 @@ public class AudioSnippet implements Snippet { } @Rpc(description = "Gets the music stream volume.") - public int getMusicVolume() { + public Integer getMusicVolume() { return mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); } @@ -54,17 +54,16 @@ public class AudioSnippet implements Snippet { } @Rpc( - description = - "Sets the music stream volume. The minimum value is 0. Use 'getMusicMaxVolume'" - + " to determine the maximum." - ) + description = + "Sets the music stream volume. The minimum value is 0. Use 'getMusicMaxVolume'" + + " to determine the maximum.") public void setMusicVolume(Integer value) { mAudioManager.setStreamVolume( AudioManager.STREAM_MUSIC, value, 0 /* flags, 0 = no flags */); } @Rpc(description = "Gets the ringer volume.") - public int getRingVolume() { + public Integer getRingVolume() { return mAudioManager.getStreamVolume(AudioManager.STREAM_RING); } @@ -74,16 +73,15 @@ public class AudioSnippet implements Snippet { } @Rpc( - description = - "Sets the ringer stream volume. The minimum value is 0. Use 'getRingMaxVolume'" - + " to determine the maximum." - ) + description = + "Sets the ringer stream volume. The minimum value is 0. Use 'getRingMaxVolume'" + + " to determine the maximum.") public void setRingVolume(Integer value) { mAudioManager.setStreamVolume(AudioManager.STREAM_RING, value, 0 /* flags, 0 = no flags */); } - + @Rpc(description = "Gets the voice call volume.") - public int getVoiceCallVolume() { + public Integer getVoiceCallVolume() { return mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL); } @@ -93,13 +91,12 @@ public class AudioSnippet implements Snippet { } @Rpc( - description = - "Sets the voice call stream volume. The minimum value is 0. Use" - + " 'getVoiceCallMaxVolume' to determine the maximum." - ) + description = + "Sets the voice call stream volume. The minimum value is 0. Use" + + " 'getVoiceCallMaxVolume' to determine the maximum.") public void setVoiceCallVolume(Integer value) { - mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, - value, 0 /* flags, 0 = no flags */); + mAudioManager.setStreamVolume( + AudioManager.STREAM_VOICE_CALL, value, 0 /* flags, 0 = no flags */); } @Rpc(description = "Silences all audio streams.") @@ -115,10 +112,9 @@ public class AudioSnippet implements Snippet { } @Rpc( - description = - "Puts the ringer volume at the lowest setting, but does not set it to " - + "DO NOT DISTURB; the phone will vibrate when receiving a call." - ) + description = + "Puts the ringer volume at the lowest setting, but does not set it to " + + "DO NOT DISTURB; the phone will vibrate when receiving a call.") public void muteRing() { setRingVolume(0); } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java index f6b6918..a1e9d83 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java @@ -19,7 +19,7 @@ package com.google.android.mobly.snippet.bundled; import android.content.Context; import android.net.Uri; import android.os.ParcelFileDescriptor; -import android.support.test.InstrumentationRegistry; +import androidx.test.platform.app.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; @@ -34,7 +34,7 @@ public class FileSnippet implements Snippet { private final Context mContext; public FileSnippet() { - mContext = InstrumentationRegistry.getContext(); + mContext = InstrumentationRegistry.getInstrumentation().getContext(); } private static class FileSnippetException extends Exception { diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java index e89fc12..636c0fd 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/NetworkingSnippet.java @@ -23,7 +23,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Environment; -import android.support.test.InstrumentationRegistry; +import androidx.test.platform.app.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; @@ -44,7 +44,7 @@ public class NetworkingSnippet implements Snippet { private volatile long mReqid = 0; public NetworkingSnippet() { - mContext = InstrumentationRegistry.getContext(); + mContext = InstrumentationRegistry.getInstrumentation().getContext(); mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); } @@ -78,10 +78,9 @@ public class NetworkingSnippet implements Snippet { } @Rpc( - description = - "Download a file using HTTP. Return content Uri (file remains on device). " - + "The Uri should be treated as an opaque handle for further operations." - ) + description = + "Download a file using HTTP. Return content Uri (file remains on device). " + + "The Uri should be treated as an opaque handle for further operations.") public String networkHttpDownload(String url) throws IllegalArgumentException, NetworkingSnippetException { diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java index a07b231..1c34264 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/NotificationSnippet.java @@ -16,8 +16,8 @@ package com.google.android.mobly.snippet.bundled; -import android.support.test.InstrumentationRegistry; import android.widget.Toast; +import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; import com.google.android.mobly.snippet.rpc.RunOnUiThread; @@ -28,7 +28,11 @@ public class NotificationSnippet implements Snippet { @RunOnUiThread @Rpc(description = "Make a toast on screen.") public void makeToast(String message) { - Toast.makeText(InstrumentationRegistry.getContext(), message, Toast.LENGTH_LONG).show(); + Toast.makeText( + InstrumentationRegistry.getInstrumentation().getContext(), + message, + Toast.LENGTH_LONG) + .show(); } @Override diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java index ec6d470..be41e9e 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java @@ -26,9 +26,9 @@ import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; import android.provider.Telephony.Sms.Intents; -import android.support.test.InstrumentationRegistry; import android.telephony.SmsManager; import android.telephony.SmsMessage; +import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.bundled.utils.Utils; import com.google.android.mobly.snippet.event.EventCache; @@ -62,7 +62,7 @@ public class SmsSnippet implements Snippet { private final SmsManager mSmsManager; public SmsSnippet() { - this.mContext = InstrumentationRegistry.getContext(); + this.mContext = InstrumentationRegistry.getInstrumentation().getContext(); this.mSmsManager = SmsManager.getDefault(); } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java index 431456d..fbb3041 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java @@ -17,8 +17,8 @@ package com.google.android.mobly.snippet.bundled; import android.content.Context; -import android.support.test.InstrumentationRegistry; import android.telephony.TelephonyManager; +import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; @@ -28,7 +28,7 @@ public class TelephonySnippet implements Snippet { private final TelephonyManager mTelephonyManager; public TelephonySnippet() { - Context context = InstrumentationRegistry.getContext(); + Context context = InstrumentationRegistry.getInstrumentation().getContext(); mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); } @@ -43,10 +43,9 @@ public class TelephonySnippet implements Snippet { } @Rpc( - description = - "Gets the call state for the default subscription. Call state values are" - + "0: IDLE, 1: RINGING, 2: OFFHOOK" - ) + description = + "Gets the call state for the default subscription. Call state values are" + + "0: IDLE, 1: RINGING, 2: OFFHOOK") public int getTelephonyCallState() { return mTelephonyManager.getCallState(); } 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 index 768fb4c..3d96e4d 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -25,8 +25,8 @@ import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build; -import android.support.annotation.Nullable; -import android.support.test.InstrumentationRegistry; +import androidx.annotation.Nullable; +import androidx.test.platform.app.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; @@ -57,17 +57,16 @@ public class WifiManagerSnippet implements Snippet { private volatile boolean mIsScanResultAvailable = false; public WifiManagerSnippet() { - mContext = InstrumentationRegistry.getContext(); + mContext = InstrumentationRegistry.getInstrumentation().getContext(); mWifiManager = (WifiManager) mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); } @Rpc( - description = - "Clears all configured networks. This will only work if all configured " - + "networks were added through this MBS instance" - ) + description = + "Clears all configured networks. This will only work if all configured " + + "networks were added through this MBS instance") public void wifiClearConfiguredNetworks() throws WifiManagerSnippetException { List unremovedConfigs = mWifiManager.getConfiguredNetworks(); List failedConfigs = new ArrayList<>(); @@ -148,9 +147,8 @@ public class WifiManagerSnippet implements Snippet { } @Rpc( - description = - "Get Wi-Fi scan results, which is a list of serialized WifiScanResult objects." - ) + 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()) { @@ -160,10 +158,9 @@ public class WifiManagerSnippet implements Snippet { } @Rpc( - description = - "Start scan, wait for scan to complete, and return results, which is a list of " - + "serialized WifiScanResult objects." - ) + 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( @@ -179,10 +176,9 @@ public class WifiManagerSnippet implements Snippet { } @Rpc( - description = - "Connects to a Wi-Fi network. This covers the common network types like open and " - + "WPA2." - ) + 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(); @@ -277,10 +273,9 @@ public class WifiManagerSnippet implements Snippet { } @Rpc( - description = - "Forget a configured Wi-Fi network by its network ID, which is part of the" - + " WifiConfiguration." - ) + 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); @@ -288,10 +283,9 @@ public class WifiManagerSnippet implements Snippet { } @Rpc( - description = - "Get the list of configured Wi-Fi networks, each is a serialized " - + "WifiConfiguration object." - ) + description = + "Get the list of configured Wi-Fi networks, each is a serialized " + + "WifiConfiguration object.") public List wifiGetConfiguredNetworks() throws JSONException { List networks = new ArrayList<>(); for (WifiConfiguration config : mWifiManager.getConfiguredNetworks()) { @@ -307,19 +301,17 @@ public class WifiManagerSnippet implements Snippet { } @Rpc( - description = - "Get the information about the active Wi-Fi connection, which is a serialized " - + "WifiInfo object." - ) + 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." - ) + 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()); } diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java index 37e7c5f..14393be 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java @@ -23,7 +23,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; -import android.support.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; import com.google.android.mobly.snippet.bundled.utils.Utils; @@ -55,7 +55,7 @@ public class BluetoothAdapterSnippet implements Snippet { private volatile boolean mIsDiscoveryFinished = false; public BluetoothAdapterSnippet() { - mContext = InstrumentationRegistry.getContext(); + mContext = InstrumentationRegistry.getInstrumentation().getContext(); } /** @@ -138,9 +138,8 @@ public class BluetoothAdapterSnippet implements Snippet { } @Rpc( - description = - "Get bluetooth discovery results, which is a list of serialized BluetoothDevice objects." - ) + description = + "Get bluetooth discovery results, which is a list of serialized BluetoothDevice objects.") public ArrayList btGetCachedScanResults() { return mJsonSerializer.serializeBluetoothDeviceList(mDiscoveryResults.values()); } @@ -168,10 +167,9 @@ public class BluetoothAdapterSnippet implements Snippet { } @Rpc( - description = - "Start discovery, wait for discovery to complete, and return results, which is a list of " - + "serialized BluetoothDevice objects." - ) + description = + "Start discovery, wait for discovery to complete, and return results, which is a list of " + + "serialized BluetoothDevice objects.") public List btDiscoverAndGetResults() throws InterruptedException, BluetoothAdapterSnippetException { IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java index b218723..ec148ca 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java @@ -9,7 +9,7 @@ import android.content.Context; import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; -import android.support.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.bundled.bluetooth.BluetoothAdapterSnippet; import com.google.android.mobly.snippet.bundled.bluetooth.PairingBroadcastReceiver; @@ -34,7 +34,7 @@ public class BluetoothA2dpSnippet implements Snippet { private final JsonSerializer mJsonSerializer = new JsonSerializer(); public BluetoothA2dpSnippet() { - mContext = InstrumentationRegistry.getContext(); + mContext = InstrumentationRegistry.getInstrumentation().getContext(); BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); bluetoothAdapter.getProfileProxy( mContext, new A2dpServiceListener(), BluetoothProfile.A2DP); @@ -55,10 +55,9 @@ public class BluetoothA2dpSnippet implements Snippet { @TargetApi(Build.VERSION_CODES.KITKAT) @RpcMinSdk(Build.VERSION_CODES.KITKAT) @Rpc( - description = - "Connects to a paired or discovered device with A2DP profile." - + "If a device has been discovered but not paired, this will pair it." - ) + description = + "Connects to a paired or discovered device with A2DP profile." + + "If a device has been discovered but not paired, this will pair it.") public void btA2dpConnect(String deviceAddress) throws Throwable { BluetoothDevice device = BluetoothAdapterSnippet.getKnownDeviceByAddress(deviceAddress); IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); -- cgit v1.2.3 From 62f5ef20b55af02c80b5e71fc884d9244f1a3ff7 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Mon, 28 Jan 2019 17:41:17 -0800 Subject: Remove an unused inner class. (#106) * Remove an unused inner class. * Restore the correct application path. --- .../com/google/android/mobly/snippet/bundled/FileSnippet.java | 9 --------- 1 file changed, 9 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java index a1e9d83..b6d6ca5 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/FileSnippet.java @@ -37,15 +37,6 @@ public class FileSnippet implements Snippet { mContext = InstrumentationRegistry.getInstrumentation().getContext(); } - private static class FileSnippetException extends Exception { - - private static final long serialVersionUID = 8081L; - - public FileSnippetException(String msg) { - super(msg); - } - } - @Rpc(description = "Compute MD5 hash on a content URI. Return the MD5 has has a hex string.") public String fileMd5Hash(String uri) throws IOException, NoSuchAlgorithmException { Uri uri_ = Uri.parse(uri); -- cgit v1.2.3 From a727fbc5ee92c632fca4475d65dad2bdbf2d4833 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Tue, 29 Jan 2019 17:50:09 -0800 Subject: Add an API to check if 5G Wi-Fi is supported. (#107) --- .../android/mobly/snippet/bundled/WifiManagerSnippet.java | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 3d96e4d..d848eb9 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -26,6 +26,7 @@ import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer; @@ -321,6 +322,16 @@ public class WifiManagerSnippet implements Snippet { return (boolean) Utils.invokeByReflection(mWifiManager, "isWifiApEnabled"); } + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP) + @Rpc( + description = + "Check whether this device supports 5 GHz band Wi-Fi. " + + "Turn on Wi-Fi before calling.") + public boolean wifiIs5GHzBandSupported() { + return mWifiManager.is5GHzBandSupported(); + } + /** * Enable Wi-Fi Soft AP (hotspot). * -- cgit v1.2.3 From 244d747c29066e1caf9c99693e6ad356931b231d Mon Sep 17 00:00:00 2001 From: Ang Li Date: Tue, 12 Feb 2019 19:03:32 -0800 Subject: Add api to remove account. (#108) --- .../mobly/snippet/bundled/AccountSnippet.java | 42 +++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index c7f576b..aac543a 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -23,7 +23,9 @@ import android.accounts.AccountsException; import android.content.ContentResolver; import android.content.Context; import android.content.SyncAdapterType; +import android.os.Build; import android.os.Bundle; +import androidx.annotation.RequiresApi; import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.rpc.Rpc; @@ -78,10 +80,6 @@ public class AccountSnippet implements Snippet { /** * Adds a Google account to the device. * - *

TODO(adorokhine): Support adding accounts of other types with an optional 'type' kwarg. - * - *

TODO(adorokhine): Allow users to choose whether to enable/disable sync with a kwarg. - * * @param username Username of the account to add (including @gmail.com). * @param password Password of the account to add. */ @@ -152,6 +150,42 @@ public class AccountSnippet implements Snippet { mSyncStatusObserverHandles.add(handle); } + /** + * Removes an account from the device. + * + *

The account has to be Google account. + * + * @param username the username of the account to remove. + * @throws AccountSnippetException if removing the account failed. + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) + @Rpc(description = "Remove a Google account.") + public void removeAccount(String username) throws AccountSnippetException { + if (!mAccountManager.removeAccountExplicitly(getAccountByName(username))) { + throw new AccountSnippetException("Failed to remove account '" + username + "'."); + } + } + + /** + * Get an existing account by its username. + * + *

Google account only. + * + * @param username the username of the account to remove. + * @return tHe account with the username. + * @throws AccountSnippetException if no account has the given username. + */ + private Account getAccountByName(String username) throws AccountSnippetException { + Account[] accounts = mAccountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE); + for (Account account : accounts) { + if (account.name.equals(username)) { + return account; + } + } + throw new AccountSnippetException( + "Account '" + username + "' does not exist on the device."); + } + /** * Checks to see if the SyncAdapter is whitelisted. * -- cgit v1.2.3 From 2ce5ca97caf3c377311aac0c33da0248daed8e27 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Tue, 9 Apr 2019 19:45:49 -0700 Subject: Fix a potentialy NPE in wifiConnect. (#109) Also improve a log line. --- .../android/mobly/snippet/bundled/WifiManagerSnippet.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index d848eb9..f7cd049 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -199,7 +199,11 @@ public class WifiManagerSnippet implements Snippet { * WifiManager#addNetwork(WifiConfiguration)}. */ private WifiConfiguration getExistingConfiguredNetwork(String ssid) { - for (WifiConfiguration config : mWifiManager.getConfiguredNetworks()) { + List wifiConfigs = mWifiManager.getConfiguredNetworks(); + if (wifiConfigs == null) { + return null; + } + for (WifiConfiguration config : wifiConfigs) { if (config.SSID.equals(ssid)) { return config; } @@ -262,9 +266,10 @@ public class WifiManagerSnippet implements Snippet { && mWifiManager.getConnectionInfo().getNetworkId() != -1, 90)) { throw new WifiManagerSnippetException( - "Failed to connect to Wi-Fi network " - + wifiNetworkConfig.toString() - + ", timeout!"); + String.format( + "Failed to connect to '%s', timeout! Current connection: '%s'", + wifiNetworkConfig.toString(), + mWifiManager.getConnectionInfo().getSSID())); } Log.d( "Connected to network '" -- cgit v1.2.3 From c2841cf932e00b17dba87ad122546ea1325d2567 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Thu, 11 Apr 2019 05:29:36 -0700 Subject: Remove unnecessary `toString`. (#110) --- .../com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index f7cd049..0008c13 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -268,8 +268,7 @@ public class WifiManagerSnippet implements Snippet { throw new WifiManagerSnippetException( String.format( "Failed to connect to '%s', timeout! Current connection: '%s'", - wifiNetworkConfig.toString(), - mWifiManager.getConnectionInfo().getSSID())); + wifiNetworkConfig, mWifiManager.getConnectionInfo().getSSID())); } Log.d( "Connected to network '" -- cgit v1.2.3 From b05854810a19da0b698b1c9fa6a9531acdcb8ffb Mon Sep 17 00:00:00 2001 From: Ang Li Date: Wed, 12 Jun 2019 17:34:15 -0700 Subject: Handle Wi-Fi permission requirements in Q. (#112) * Adopt shell permission in Wi-Fi snippet when it's Q. * Fix ClassNotFound error caused by androidx Tested locally and confirmed that the change works. --- .../com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 0008c13..3c46708 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -62,6 +62,11 @@ public class WifiManagerSnippet implements Snippet { mWifiManager = (WifiManager) mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + if (Build.VERSION.SDK_INT >= 29) { + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + } } @Rpc( -- cgit v1.2.3 From 431ae9a53b85ac40b92cc36e2a8e583b1fd00c81 Mon Sep 17 00:00:00 2001 From: Peter Fong Date: Mon, 24 Jun 2019 12:56:40 -0700 Subject: Cleanup UiAutomator connection so it won't block other from using since UiAutomator only support a single connection at a time. --- .../mobly/snippet/bundled/WifiManagerSnippet.java | 24 ++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 3c46708..1cb1680 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -16,6 +16,7 @@ package com.google.android.mobly.snippet.bundled; +import android.app.UiAutomation; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -35,6 +36,8 @@ import com.google.android.mobly.snippet.bundled.utils.Utils; import com.google.android.mobly.snippet.rpc.Rpc; import com.google.android.mobly.snippet.rpc.RpcMinSdk; import com.google.android.mobly.snippet.util.Log; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import org.json.JSONArray; @@ -49,6 +52,10 @@ public class WifiManagerSnippet implements Snippet { public WifiManagerSnippetException(String msg) { super(msg); } + + public WifiManagerSnippetException(String msg, Throwable err) { + super(msg, err); + } } private static final int TIMEOUT_TOGGLE_STATE = 30; @@ -57,15 +64,24 @@ public class WifiManagerSnippet implements Snippet { private final JsonSerializer mJsonSerializer = new JsonSerializer(); private volatile boolean mIsScanResultAvailable = false; - public WifiManagerSnippet() { + public WifiManagerSnippet() throws WifiManagerSnippetException { mContext = InstrumentationRegistry.getInstrumentation().getContext(); mWifiManager = (WifiManager) mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); if (Build.VERSION.SDK_INT >= 29) { - InstrumentationRegistry.getInstrumentation() - .getUiAutomation() - .adoptShellPermissionIdentity(); + UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); + uia.adoptShellPermissionIdentity(); + try { + Class cls = Class.forName("android.app.UiAutomation"); + Method destroyMethod = cls.getDeclaredMethod("destroy"); + destroyMethod.invoke(uia); + } catch (NoSuchMethodException + | IllegalAccessException + | ClassNotFoundException + | InvocationTargetException e) { + throw new WifiManagerSnippetException("Failed to cleaup Ui Automation", e); + } } } -- cgit v1.2.3 From a45ca74497012a1c0c0ae4531983e42cecfe8092 Mon Sep 17 00:00:00 2001 From: Peter Fong Date: Wed, 26 Jun 2019 09:19:09 -0700 Subject: updated to use target sdk version to check if shell permission is needed --- .../android/mobly/snippet/bundled/WifiManagerSnippet.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 1cb1680..f14f379 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -69,7 +69,7 @@ public class WifiManagerSnippet implements Snippet { mWifiManager = (WifiManager) mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - if (Build.VERSION.SDK_INT >= 29) { + if (mContext.getApplicationContext().getApplicationInfo().targetSdkVersion >= 29) { UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); uia.adoptShellPermissionIdentity(); try { @@ -77,10 +77,10 @@ public class WifiManagerSnippet implements Snippet { Method destroyMethod = cls.getDeclaredMethod("destroy"); destroyMethod.invoke(uia); } catch (NoSuchMethodException - | IllegalAccessException - | ClassNotFoundException - | InvocationTargetException e) { - throw new WifiManagerSnippetException("Failed to cleaup Ui Automation", e); + | IllegalAccessException + | ClassNotFoundException + | InvocationTargetException e) { + throw new WifiManagerSnippetException("Failed to cleaup Ui Automation", e); } } } -- cgit v1.2.3 From 9da2670f6df85bce85ea0e3e4fa4de5a594d4966 Mon Sep 17 00:00:00 2001 From: Peter Fong Date: Wed, 26 Jun 2019 13:24:37 -0700 Subject: Refactored out shell permission adoption routine --- .../mobly/snippet/bundled/WifiManagerSnippet.java | 39 ++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index f14f379..442db0a 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -64,24 +64,17 @@ public class WifiManagerSnippet implements Snippet { private final JsonSerializer mJsonSerializer = new JsonSerializer(); private volatile boolean mIsScanResultAvailable = false; - public WifiManagerSnippet() throws WifiManagerSnippetException { + public WifiManagerSnippet() throws Throwable { mContext = InstrumentationRegistry.getInstrumentation().getContext(); mWifiManager = (WifiManager) mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + + // Starting in Android Q (29), additional restrictions are added for wifi operation. See + // below Android Q privacy changes for additional details. + // https://developer.android.com/preview/privacy/camera-connectivity if (mContext.getApplicationContext().getApplicationInfo().targetSdkVersion >= 29) { - UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); - uia.adoptShellPermissionIdentity(); - try { - Class cls = Class.forName("android.app.UiAutomation"); - Method destroyMethod = cls.getDeclaredMethod("destroy"); - destroyMethod.invoke(uia); - } catch (NoSuchMethodException - | IllegalAccessException - | ClassNotFoundException - | InvocationTargetException e) { - throw new WifiManagerSnippetException("Failed to cleaup Ui Automation", e); - } + adoptShellPermissionIdentity(); } } @@ -405,6 +398,26 @@ public class WifiManagerSnippet implements Snippet { @Override public void shutdown() {} + /** + * Elevate permission by adopting shell's identity. + * + * @throws Throwable + */ + private void adoptShellPermissionIdentity() throws Throwable { + UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); + uia.adoptShellPermissionIdentity(); + try { + Class cls = Class.forName("android.app.UiAutomation"); + Method destroyMethod = cls.getDeclaredMethod("destroy"); + destroyMethod.invoke(uia); + } catch (NoSuchMethodException + | IllegalAccessException + | ClassNotFoundException + | InvocationTargetException e) { + throw new WifiManagerSnippetException("Failed to cleaup Ui Automation", e); + } + } + private class WifiScanReceiver extends BroadcastReceiver { @Override -- cgit v1.2.3 From e62de2e63ff6ff41cb1f36ca84e4fba03626f3bd Mon Sep 17 00:00:00 2001 From: Peter Fong Date: Thu, 27 Jun 2019 09:49:42 -0700 Subject: Renamed and embedded sdk check in method --- .../mobly/snippet/bundled/WifiManagerSnippet.java | 41 +++++++++++----------- 1 file changed, 21 insertions(+), 20 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 442db0a..3ad85a2 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -69,13 +69,7 @@ public class WifiManagerSnippet implements Snippet { mWifiManager = (WifiManager) mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - - // Starting in Android Q (29), additional restrictions are added for wifi operation. See - // below Android Q privacy changes for additional details. - // https://developer.android.com/preview/privacy/camera-connectivity - if (mContext.getApplicationContext().getApplicationInfo().targetSdkVersion >= 29) { - adoptShellPermissionIdentity(); - } + elevatePermissionIfRequired(); } @Rpc( @@ -399,22 +393,29 @@ public class WifiManagerSnippet implements Snippet { public void shutdown() {} /** - * Elevate permission by adopting shell's identity. + * Elevates permission as require for proper wifi controls. * * @throws Throwable */ - private void adoptShellPermissionIdentity() throws Throwable { - UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); - uia.adoptShellPermissionIdentity(); - try { - Class cls = Class.forName("android.app.UiAutomation"); - Method destroyMethod = cls.getDeclaredMethod("destroy"); - destroyMethod.invoke(uia); - } catch (NoSuchMethodException - | IllegalAccessException - | ClassNotFoundException - | InvocationTargetException e) { - throw new WifiManagerSnippetException("Failed to cleaup Ui Automation", e); + private void elevatePermissionIfRequired() throws Throwable { + // Starting in Android Q (29), additional restrictions are added for wifi operation. See + // below Android Q privacy changes for additional details. + // https://developer.android.com/preview/privacy/camera-connectivity + if (mContext.getApplicationContext().getApplicationInfo().targetSdkVersion >= 29 + && Build.VERSION.SDK_INT >= 29) { + Log.d("Elevating permission require to enable support for wifi operation in Android Q+"); + UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); + uia.adoptShellPermissionIdentity(); + try { + Class cls = Class.forName("android.app.UiAutomation"); + Method destroyMethod = cls.getDeclaredMethod("destroy"); + destroyMethod.invoke(uia); + } catch (NoSuchMethodException + | IllegalAccessException + | ClassNotFoundException + | InvocationTargetException e) { + throw new WifiManagerSnippetException("Failed to cleaup Ui Automation", e); + } } } -- cgit v1.2.3 From 58d8b2c889389e098253f574144098fef46f21e6 Mon Sep 17 00:00:00 2001 From: Peter Fong Date: Mon, 1 Jul 2019 11:37:07 -0700 Subject: Minor review comment fixes --- .../android/mobly/snippet/bundled/WifiManagerSnippet.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 3ad85a2..8d8f19a 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -69,7 +69,7 @@ public class WifiManagerSnippet implements Snippet { mWifiManager = (WifiManager) mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - elevatePermissionIfRequired(); + adaptShellPermissionIfRequired(); } @Rpc( @@ -395,12 +395,13 @@ public class WifiManagerSnippet implements Snippet { /** * Elevates permission as require for proper wifi controls. * + * Starting in Android Q (29), additional restrictions are added for wifi operation. See + * below Android Q privacy changes for additional details. + * https://developer.android.com/preview/privacy/camera-connectivity + * * @throws Throwable */ - private void elevatePermissionIfRequired() throws Throwable { - // Starting in Android Q (29), additional restrictions are added for wifi operation. See - // below Android Q privacy changes for additional details. - // https://developer.android.com/preview/privacy/camera-connectivity + private void adaptShellPermissionIfRequired() throws Throwable { if (mContext.getApplicationContext().getApplicationInfo().targetSdkVersion >= 29 && Build.VERSION.SDK_INT >= 29) { Log.d("Elevating permission require to enable support for wifi operation in Android Q+"); -- cgit v1.2.3 From fa182fd946eacdd4b9ca7aae4449535a01d04696 Mon Sep 17 00:00:00 2001 From: Peter Fong Date: Tue, 2 Jul 2019 10:28:24 -0700 Subject: fixed lint error --- .../com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 8d8f19a..5158af0 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -399,7 +399,7 @@ public class WifiManagerSnippet implements Snippet { * below Android Q privacy changes for additional details. * https://developer.android.com/preview/privacy/camera-connectivity * - * @throws Throwable + * @throws Throwable if failed to cleanup connection with UiAutomation */ private void adaptShellPermissionIfRequired() throws Throwable { if (mContext.getApplicationContext().getApplicationInfo().targetSdkVersion >= 29 @@ -408,7 +408,7 @@ public class WifiManagerSnippet implements Snippet { UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); uia.adoptShellPermissionIdentity(); try { - Class cls = Class.forName("android.app.UiAutomation"); + Class cls = Class.forName("android.app.UiAutomation"); Method destroyMethod = cls.getDeclaredMethod("destroy"); destroyMethod.invoke(uia); } catch (NoSuchMethodException -- cgit v1.2.3 From 430271fb37c3faa0e4f58aa0876da62432ad40d4 Mon Sep 17 00:00:00 2001 From: chuanhsiao <49941275+chuanhsiao@users.noreply.github.com> Date: Thu, 22 Aug 2019 14:04:08 +0800 Subject: Create BluetoothHearingAidSnippet.java (#119) --- .../profiles/BluetoothHearingAidSnippet.java | 113 +++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothHearingAidSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothHearingAidSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothHearingAidSnippet.java new file mode 100644 index 0000000..3a95de1 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothHearingAidSnippet.java @@ -0,0 +1,113 @@ +package com.google.android.mobly.snippet.bundled.bluetooth.profiles; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHearingAid; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Bundle; +import androidx.test.platform.app.InstrumentationRegistry; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.bundled.bluetooth.BluetoothAdapterSnippet; +import com.google.android.mobly.snippet.bundled.bluetooth.PairingBroadcastReceiver; +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.rpc.RpcMinSdk; +import java.util.ArrayList; + +public class BluetoothHearingAidSnippet implements Snippet { + private static class BluetoothHearingAidSnippetException extends Exception { + private static final long serialVersionUID = 1; + + BluetoothHearingAidSnippetException(String msg) { + super(msg); + } + } + + private static final int TIMEOUT_SEC = 60; + + private Context context; + private static boolean isHearingAidProfileReady = false; + private static BluetoothHearingAid hearingAidProfile; + private final JsonSerializer jsonSerializer = new JsonSerializer(); + + @TargetApi(Build.VERSION_CODES.Q) + public BluetoothHearingAidSnippet() { + context = InstrumentationRegistry.getInstrumentation().getContext(); + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + bluetoothAdapter.getProfileProxy( + context, new HearingAidServiceListener(), BluetoothProfile.HEARING_AID); + Utils.waitUntil(() -> isHearingAidProfileReady, TIMEOUT_SEC); + } + + @TargetApi(Build.VERSION_CODES.Q) + private static class HearingAidServiceListener implements BluetoothProfile.ServiceListener { + public void onServiceConnected(int var1, BluetoothProfile profile) { + hearingAidProfile = (BluetoothHearingAid) profile; + isHearingAidProfileReady = true; + } + + public void onServiceDisconnected(int var1) { + isHearingAidProfileReady = false; + } + } + + @TargetApi(Build.VERSION_CODES.Q) + @RpcMinSdk(Build.VERSION_CODES.Q) + @Rpc(description = "Connects to a paired or discovered device with HA profile.") + public void btHearingAidConnect(String deviceAddress) throws Throwable { + BluetoothDevice device = BluetoothAdapterSnippet.getKnownDeviceByAddress(deviceAddress); + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); + context.registerReceiver(new PairingBroadcastReceiver(context), filter); + Utils.invokeByReflection(hearingAidProfile, "connect", device); + if (!Utils.waitUntil( + () -> + hearingAidProfile.getConnectionState(device) + == BluetoothHearingAid.STATE_CONNECTED, + TIMEOUT_SEC)) { + throw new BluetoothHearingAidSnippetException( + String.format( + "Failed to connect to device %s|%s with HA profile within %d sec.", + device.getName(), device.getAddress(), TIMEOUT_SEC)); + } + } + + @Rpc(description = "Disconnects a device from HA profile.") + public void btHearingAidDisconnect(String deviceAddress) throws Throwable { + BluetoothDevice device = getConnectedBluetoothDevice(deviceAddress); + Utils.invokeByReflection(hearingAidProfile, "disconnect", device); + if (!Utils.waitUntil( + () -> + hearingAidProfile.getConnectionState(device) + == BluetoothHearingAid.STATE_DISCONNECTED, + TIMEOUT_SEC)) { + throw new BluetoothHearingAidSnippetException( + String.format( + "Failed to disconnect to device %s|%s with HA profile within %d sec.", + device.getName(), device.getAddress(), TIMEOUT_SEC)); + } + } + + @Rpc(description = "Gets all the devices currently connected via HA profile.") + public ArrayList btHearingAidGetConnectedDevices() { + return jsonSerializer.serializeBluetoothDeviceList(hearingAidProfile.getConnectedDevices()); + } + + private BluetoothDevice getConnectedBluetoothDevice(String deviceAddress) + throws BluetoothHearingAidSnippetException { + for (BluetoothDevice device : hearingAidProfile.getConnectedDevices()) { + if (device.getAddress().equalsIgnoreCase(deviceAddress)) { + return device; + } + } + throw new BluetoothHearingAidSnippetException(String.format( + "No device with address %s is connected via HA Profile.", deviceAddress)); + } + + @Override + public void shutdown() {} +} -- cgit v1.2.3 From 13af002cd91ac7e06d064bb3a81ca2d00010c313 Mon Sep 17 00:00:00 2001 From: chuanhsiao <49941275+chuanhsiao@users.noreply.github.com> Date: Fri, 23 Aug 2019 09:32:38 +0800 Subject: Fix "JavaCodeClarity" problem for BluetoothHearingAidSnippet (#122) --- .../bluetooth/profiles/BluetoothHearingAidSnippet.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothHearingAidSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothHearingAidSnippet.java index 3a95de1..7243857 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothHearingAidSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothHearingAidSnippet.java @@ -17,6 +17,7 @@ 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.rpc.RpcMinSdk; +import com.google.common.base.Ascii; import java.util.ArrayList; public class BluetoothHearingAidSnippet implements Snippet { @@ -29,8 +30,8 @@ public class BluetoothHearingAidSnippet implements Snippet { } private static final int TIMEOUT_SEC = 60; - - private Context context; + + private final Context context; private static boolean isHearingAidProfileReady = false; private static BluetoothHearingAid hearingAidProfile; private final JsonSerializer jsonSerializer = new JsonSerializer(); @@ -46,11 +47,13 @@ public class BluetoothHearingAidSnippet implements Snippet { @TargetApi(Build.VERSION_CODES.Q) private static class HearingAidServiceListener implements BluetoothProfile.ServiceListener { + @Override public void onServiceConnected(int var1, BluetoothProfile profile) { hearingAidProfile = (BluetoothHearingAid) profile; isHearingAidProfileReady = true; } + @Override public void onServiceDisconnected(int var1) { isHearingAidProfileReady = false; } @@ -97,10 +100,10 @@ public class BluetoothHearingAidSnippet implements Snippet { return jsonSerializer.serializeBluetoothDeviceList(hearingAidProfile.getConnectedDevices()); } - private BluetoothDevice getConnectedBluetoothDevice(String deviceAddress) + private static BluetoothDevice getConnectedBluetoothDevice(String deviceAddress) throws BluetoothHearingAidSnippetException { for (BluetoothDevice device : hearingAidProfile.getConnectedDevices()) { - if (device.getAddress().equalsIgnoreCase(deviceAddress)) { + if (Ascii.equalsIgnoreCase(device.getAddress(), deviceAddress)) { return device; } } -- cgit v1.2.3 From 58820984f8e79b250bc63d6b47b5f2910876a631 Mon Sep 17 00:00:00 2001 From: ryancllin <59638643+ryancllin@users.noreply.github.com> Date: Fri, 10 Jan 2020 01:05:55 +0800 Subject: Fix exception in "trimQuotationMarks()" method (#125) Add a string length check on the given param to avoid StringIndexOutOfBoundsException. --- .../com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index ef7f7d7..82e1e4f 100644 --- 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 @@ -62,7 +62,8 @@ public class JsonSerializer { */ public static String trimQuotationMarks(String originalString) { String result = originalString; - if (originalString.charAt(0) == '"' + if (originalString.length() > 2 + && originalString.charAt(0) == '"' && originalString.charAt(originalString.length() - 1) == '"') { result = originalString.substring(1, originalString.length() - 1); } -- cgit v1.2.3 From 8c035ba57fe232146004b737e41a83f477a62bd3 Mon Sep 17 00:00:00 2001 From: chuanhsiao <49941275+chuanhsiao@users.noreply.github.com> Date: Wed, 15 Jan 2020 11:38:58 +0800 Subject: Add a Rpc "btIsA2dpPlaying" to BluetoothA2dpSnippet (#128) A RPC method is used to check A2DP audio path is enabled or not. --- .../snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java index ec148ca..60ed1ec 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/profiles/BluetoothA2dpSnippet.java @@ -96,6 +96,12 @@ public class BluetoothA2dpSnippet implements Snippet { return mJsonSerializer.serializeBluetoothDeviceList(sA2dpProfile.getConnectedDevices()); } + @Rpc(description = "Checks if a device is streaming audio via A2DP profile.") + public boolean btIsA2dpPlaying(String deviceAddress) throws Throwable { + BluetoothDevice device = getConnectedBluetoothDevice(deviceAddress); + return sA2dpProfile.isA2dpPlaying(device); + } + private BluetoothDevice getConnectedBluetoothDevice(String deviceAddress) throws BluetoothA2dpSnippetException { for (BluetoothDevice device : sA2dpProfile.getConnectedDevices()) { -- cgit v1.2.3 From ecc2ee7bd4e3425bb7b4f4cb9cdd6696a2be674e Mon Sep 17 00:00:00 2001 From: jwang1013 <37950852+jwang1013@users.noreply.github.com> Date: Wed, 12 Feb 2020 14:09:01 -0800 Subject: update btBecomeDiscoverable to handle api level (#129) Android SDK beyond 29 changed the BluetoothAdapter.setScanMode signature to (Integer, Long). change btBecomeDiscoverable to handle different api level. --- .../bundled/bluetooth/BluetoothAdapterSnippet.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java index 14393be..f2ff677 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java @@ -22,6 +22,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Build; import android.os.Bundle; import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.mobly.snippet.Snippet; @@ -202,14 +203,26 @@ public class BluetoothAdapterSnippet implements Snippet { throw new BluetoothAdapterSnippetException( "Bluetooth is not enabled, cannot become discoverable."); } - if (!(boolean) + // TODO(jwang1013): change it to SDK version for R after R is released. + if (Build.VERSION.CODENAME.equals("R") || Build.VERSION.SDK_INT > 29) { + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, + (long) duration * 1000)) { + throw new BluetoothAdapterSnippetException("Failed to become discoverable."); + } else { + if (!(boolean) Utils.invokeByReflection( mBluetoothAdapter, "setScanMode", BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, duration)) { throw new BluetoothAdapterSnippetException("Failed to become discoverable."); + } } + } } @Rpc(description = "Cancel ongoing bluetooth discovery.") -- cgit v1.2.3 From c5093aad6df3096498cb396da299793d8b13db56 Mon Sep 17 00:00:00 2001 From: Syaoran Kuo Date: Mon, 2 Nov 2020 11:28:43 +0800 Subject: Add snippet for UI hierarchy dump. (#132) --- .../mobly/snippet/bundled/UiautomatorSnippet.java | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java new file mode 100644 index 0000000..06b8b0c --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 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 androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.rpc.Rpc; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Snippet class exposing Android APIs in Uiautomator. + */ +public class UiautomatorSnippet implements Snippet { + + private static final UiDevice device = + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + + @Rpc(description = "Dumps UI hierarchy XML and return as string.") + public String uiautomatorDumpWindowHierarchy() throws IOException { + String res = ""; + OutputStream outStream = new ByteArrayOutputStream(); + device.dumpWindowHierarchy(outStream); + res = outStream.toString(); + outStream.close(); + return res; + } + + @Override + public void shutdown() { + } +} -- cgit v1.2.3 From faa3ca3d693ea0314e442247be8713f953c582fb Mon Sep 17 00:00:00 2001 From: Syaoran Kuo Date: Mon, 2 Nov 2020 16:54:40 +0800 Subject: Merge unnecessary lines in UiautomatorSnippet (#135) --- .../com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java index 06b8b0c..d71bff1 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java @@ -34,10 +34,9 @@ public class UiautomatorSnippet implements Snippet { @Rpc(description = "Dumps UI hierarchy XML and return as string.") public String uiautomatorDumpWindowHierarchy() throws IOException { - String res = ""; OutputStream outStream = new ByteArrayOutputStream(); device.dumpWindowHierarchy(outStream); - res = outStream.toString(); + String res = outStream.toString(); outStream.close(); return res; } -- cgit v1.2.3 From 05ceaf206c46e2360328edd0cc02b30d0b916164 Mon Sep 17 00:00:00 2001 From: tprotopopov-dev <73544592+tprotopopov-dev@users.noreply.github.com> Date: Tue, 3 Nov 2020 18:51:16 -0600 Subject: Improve the reliability of bluetooth state change Rpcs. (#134) --- .../bundled/bluetooth/BluetoothAdapterSnippet.java | 109 +++++++++++++-------- 1 file changed, 67 insertions(+), 42 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java index f2ff677..6e66e43 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java @@ -38,7 +38,9 @@ import org.json.JSONException; /** Snippet class exposing Android APIs in BluetoothAdapter. */ public class BluetoothAdapterSnippet implements Snippet { + private static class BluetoothAdapterSnippetException extends Exception { + private static final long serialVersionUID = 1; public BluetoothAdapterSnippetException(String msg) { @@ -46,8 +48,10 @@ public class BluetoothAdapterSnippet implements Snippet { } } + // Timeout to measure consistent BT state. + private static final int BT_MATCHING_STATE_INTERVAL_SEC = 5; // Default timeout in seconds. - private static final int TIMEOUT_TOGGLE_STATE = 30; + private static final int TIMEOUT_TOGGLE_STATE_SEC = 30; private final Context mContext; private static final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); private final JsonSerializer mJsonSerializer = new JsonSerializer(); @@ -94,20 +98,17 @@ public class BluetoothAdapterSnippet implements Snippet { if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { return; } - // If bt is trying to turn off, wait for that to finish before continuing. - if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF) { - if (!Utils.waitUntil( - () -> mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF, - TIMEOUT_TOGGLE_STATE)) { - Log.e(String.format("BT failed to stabilize after %ss.", TIMEOUT_TOGGLE_STATE)); - } - } + waitForStableBtState(); + if (!mBluetoothAdapter.enable()) { - throw new BluetoothAdapterSnippetException("Failed to start enabling bluetooth"); + throw new BluetoothAdapterSnippetException("Failed to start enabling bluetooth."); } - if (!Utils.waitUntil(() -> mBluetoothAdapter.isEnabled(), TIMEOUT_TOGGLE_STATE)) { + if (!Utils.waitUntil( + () -> mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON, + TIMEOUT_TOGGLE_STATE_SEC)) { throw new BluetoothAdapterSnippetException( - String.format("Bluetooth did not turn on within %ss.", TIMEOUT_TOGGLE_STATE)); + String.format( + "Bluetooth did not turn on within %ss.", TIMEOUT_TOGGLE_STATE_SEC)); } } @@ -116,20 +117,16 @@ public class BluetoothAdapterSnippet implements Snippet { if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { return; } - // If bt is trying to turn on, wait for that to finish before continuing. - if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_TURNING_ON) { - if (!Utils.waitUntil( - () -> mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON, - TIMEOUT_TOGGLE_STATE)) { - Log.e(String.format("BT failed to stabilize after %ss.", TIMEOUT_TOGGLE_STATE)); - } - } + waitForStableBtState(); if (!mBluetoothAdapter.disable()) { - throw new BluetoothAdapterSnippetException("Failed to start disabling bluetooth"); + throw new BluetoothAdapterSnippetException("Failed to start disabling bluetooth."); } - if (!Utils.waitUntil(() -> !mBluetoothAdapter.isEnabled(), TIMEOUT_TOGGLE_STATE)) { + if (!Utils.waitUntil( + () -> mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF, + TIMEOUT_TOGGLE_STATE_SEC)) { throw new BluetoothAdapterSnippetException( - String.format("Bluetooth did not turn off within %ss.", TIMEOUT_TOGGLE_STATE)); + String.format( + "Bluetooth did not turn off within %ss.", TIMEOUT_TOGGLE_STATE_SEC)); } } @@ -203,26 +200,25 @@ public class BluetoothAdapterSnippet implements Snippet { throw new BluetoothAdapterSnippetException( "Bluetooth is not enabled, cannot become discoverable."); } - // TODO(jwang1013): change it to SDK version for R after R is released. - if (Build.VERSION.CODENAME.equals("R") || Build.VERSION.SDK_INT > 29) { - if (!(boolean) - Utils.invokeByReflection( - mBluetoothAdapter, - "setScanMode", - BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, - (long) duration * 1000)) { - throw new BluetoothAdapterSnippetException("Failed to become discoverable."); - } else { - if (!(boolean) - Utils.invokeByReflection( - mBluetoothAdapter, - "setScanMode", - BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, - duration)) { - throw new BluetoothAdapterSnippetException("Failed to become discoverable."); - } + if (Build.VERSION.SDK_INT > 29) { + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, + (long) duration * 1000)) { + throw new BluetoothAdapterSnippetException("Failed to become discoverable."); + } else { + if (!(boolean) + Utils.invokeByReflection( + mBluetoothAdapter, + "setScanMode", + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, + duration)) { + throw new BluetoothAdapterSnippetException("Failed to become discoverable."); + } + } } - } } @Rpc(description = "Cancel ongoing bluetooth discovery.") @@ -333,4 +329,33 @@ public class BluetoothAdapterSnippet implements Snippet { } } } + + /** + * Waits until the bluetooth adapter state has stabilized. We consider BT state stabilized if it + * hasn't changed within 5 sec. + */ + private static void waitForStableBtState() throws BluetoothAdapterSnippetException { + long timeoutMs = System.currentTimeMillis() + TIMEOUT_TOGGLE_STATE_SEC * 1000; + long continuousStateIntervalMs = + System.currentTimeMillis() + BT_MATCHING_STATE_INTERVAL_SEC * 1000; + int prevState = mBluetoothAdapter.getState(); + while (System.currentTimeMillis() < timeoutMs) { + // Delay. + Utils.waitUntil(() -> false, /* timeout= */ 1); + + int currentState = mBluetoothAdapter.getState(); + if (currentState != prevState) { + continuousStateIntervalMs = + System.currentTimeMillis() + BT_MATCHING_STATE_INTERVAL_SEC * 1000; + } + if (continuousStateIntervalMs <= System.currentTimeMillis()) { + return; + } + prevState = currentState; + } + throw new BluetoothAdapterSnippetException( + String.format( + "Failed to reach a stable Bluetooth state within %d s", + TIMEOUT_TOGGLE_STATE_SEC)); + } } -- cgit v1.2.3 From fe521427722eff1994aa156d846afb609c8cba64 Mon Sep 17 00:00:00 2001 From: tprotopopov-dev <73544592+tprotopopov-dev@users.noreply.github.com> Date: Wed, 16 Dec 2020 03:17:37 -0600 Subject: Revert c5093aad6df3096498cb396da299793d8b13db56 due to import incompatibility (#136) --- .../mobly/snippet/bundled/UiautomatorSnippet.java | 47 ---------------------- 1 file changed, 47 deletions(-) delete mode 100644 src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java deleted file mode 100644 index d71bff1..0000000 --- a/src/main/java/com/google/android/mobly/snippet/bundled/UiautomatorSnippet.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2020 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 androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.uiautomator.UiDevice; -import com.google.android.mobly.snippet.Snippet; -import com.google.android.mobly.snippet.rpc.Rpc; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * Snippet class exposing Android APIs in Uiautomator. - */ -public class UiautomatorSnippet implements Snippet { - - private static final UiDevice device = - UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); - - @Rpc(description = "Dumps UI hierarchy XML and return as string.") - public String uiautomatorDumpWindowHierarchy() throws IOException { - OutputStream outStream = new ByteArrayOutputStream(); - device.dumpWindowHierarchy(outStream); - String res = outStream.toString(); - outStream.close(); - return res; - } - - @Override - public void shutdown() { - } -} -- cgit v1.2.3 From 9190cc3a9d64c1067f332a4dc6daa499a8162904 Mon Sep 17 00:00:00 2001 From: tprotopopov-dev <73544592+tprotopopov-dev@users.noreply.github.com> Date: Tue, 12 Jan 2021 19:58:44 -0600 Subject: Additional condition to allow wifi to fully connect. (#138) --- .../mobly/snippet/bundled/WifiManagerSnippet.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') 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 index 5158af0..cf577c3 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -43,7 +43,7 @@ import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; - +import android.net.wifi.SupplicantState; /** Snippet class exposing Android APIs in WifiManager. */ public class WifiManagerSnippet implements Snippet { private static class WifiManagerSnippetException extends Exception { @@ -269,14 +269,15 @@ public class WifiManagerSnippet implements Snippet { "Failed to reconnect to Wi-Fi network of ID: " + networkId); } if (!Utils.waitUntil( - () -> - mWifiManager.getConnectionInfo().getSSID().equals(SSID) - && mWifiManager.getConnectionInfo().getNetworkId() != -1, - 90)) { + () -> + mWifiManager.getConnectionInfo().getSSID().equals(SSID) + && mWifiManager.getConnectionInfo().getNetworkId() != -1 && mWifiManager + .getConnectionInfo().getSupplicantState().equals(SupplicantState.COMPLETED), + 90)) { throw new WifiManagerSnippetException( - String.format( - "Failed to connect to '%s', timeout! Current connection: '%s'", - wifiNetworkConfig, mWifiManager.getConnectionInfo().getSSID())); + String.format( + "Failed to connect to '%s', timeout! Current connection: '%s'", + wifiNetworkConfig, mWifiManager.getConnectionInfo().getSSID())); } Log.d( "Connected to network '" -- cgit v1.2.3 From 721de46e35d5a87674adae2e6525f0af2c4ebb2e Mon Sep 17 00:00:00 2001 From: "Eric Lin (Tzu Hsiang Lin)" Date: Thu, 18 Feb 2021 12:04:44 +0800 Subject: Respectful code cleanup. (#140) Replaced "whitelist" with a more respectful term: allowlist. https://developers.google.com/style/word-list?hl=zh-tw#blacklist --- .../mobly/snippet/bundled/AccountSnippet.java | 50 +++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java index aac543a..3c21dbf 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AccountSnippet.java @@ -66,14 +66,14 @@ public class AccountSnippet implements Snippet { private final AccountManager mAccountManager; private final List mSyncStatusObserverHandles; - private final Map> mSyncWhitelist; + private final Map> mSyncAllowList; private final ReentrantReadWriteLock mLock; public AccountSnippet() { Context context = InstrumentationRegistry.getInstrumentation().getContext(); mAccountManager = AccountManager.get(context); mSyncStatusObserverHandles = new LinkedList<>(); - mSyncWhitelist = new HashMap<>(); + mSyncAllowList = new HashMap<>(); mLock = new ReentrantReadWriteLock(); } @@ -133,13 +133,13 @@ public class AccountSnippet implements Snippet { if (!adapter.accountType.equals(GOOGLE_ACCOUNT_TYPE)) { continue; } - // If a content provider is not whitelisted, then disable it. - // Because startSync and stopSync synchronously update the whitelist - // and sync settings, writelock both the whitelist check and the + // If a content provider is not allowListed, then disable it. + // Because startSync and stopSync synchronously update the allowList + // and sync settings, writelock both the allowList check and the // call to sync together. mLock.writeLock().lock(); try { - if (!isAdapterWhitelisted(username, adapter.authority)) { + if (!isAdapterAllowListed(username, adapter.authority)) { updateSync(account, adapter.authority, false /* sync */); } } finally { @@ -187,21 +187,21 @@ public class AccountSnippet implements Snippet { } /** - * Checks to see if the SyncAdapter is whitelisted. + * Checks to see if the SyncAdapter is allowListed. * - *

AccountSnippet disables syncing by default when adding an account, except for whitelisted - * SyncAdapters. This function checks the whitelist for a specific account-authority pair. + *

AccountSnippet disables syncing by default when adding an account, except for allowListed + * SyncAdapters. This function checks the allowList for a specific account-authority pair. * * @param username Username of the account (including @gmail.com). * @param authority The authority of a content provider that should be checked. */ - private boolean isAdapterWhitelisted(String username, String authority) { + private boolean isAdapterAllowListed(String username, String authority) { boolean result = false; mLock.readLock().lock(); try { - Set whitelistedProviders = mSyncWhitelist.get(username); - if (whitelistedProviders != null) { - result = whitelistedProviders.contains(authority); + Set allowListedProviders = mSyncAllowList.get(username); + if (allowListedProviders != null) { + result = allowListedProviders.contains(authority); } } finally { mLock.readLock().unlock(); @@ -244,7 +244,7 @@ public class AccountSnippet implements Snippet { /** * Enables syncing of a SyncAdapter for a given content provider. * - *

Adds the authority to a whitelist, and immediately requests a sync. + *

Adds the authority to a allowList, and immediately requests a sync. * * @param username Username of the account (including @gmail.com). * @param authority The authority of a content provider that should be synced. @@ -254,13 +254,13 @@ public class AccountSnippet implements Snippet { if (!listAccounts().contains(username)) { throw new AccountSnippetException("Account " + username + " is not on the device"); } - // Add to the whitelist + // Add to the allowList mLock.writeLock().lock(); try { - if (mSyncWhitelist.containsKey(username)) { - mSyncWhitelist.get(username).add(authority); + if (mSyncAllowList.containsKey(username)) { + mSyncAllowList.get(username).add(authority); } else { - mSyncWhitelist.put(username, new HashSet(Arrays.asList(authority))); + mSyncAllowList.put(username, new HashSet(Arrays.asList(authority))); } // Update the Sync settings for (SyncAdapterType adapter : ContentResolver.getSyncAdapterTypes()) { @@ -279,7 +279,7 @@ public class AccountSnippet implements Snippet { /** * Disables syncing of a SyncAdapter for a given content provider. * - *

Removes the content provider authority from a whitelist. + *

Removes the content provider authority from a allowList. * * @param username Username of the account (including @gmail.com). * @param authority The authority of a content provider that should not be synced. @@ -289,14 +289,14 @@ public class AccountSnippet implements Snippet { if (!listAccounts().contains(username)) { throw new AccountSnippetException("Account " + username + " is not on the device"); } - // Remove from whitelist + // Remove from allowList mLock.writeLock().lock(); try { - if (mSyncWhitelist.containsKey(username)) { - Set whitelistedProviders = mSyncWhitelist.get(username); - whitelistedProviders.remove(authority); - if (whitelistedProviders.isEmpty()) { - mSyncWhitelist.remove(username); + if (mSyncAllowList.containsKey(username)) { + Set allowListedProviders = mSyncAllowList.get(username); + allowListedProviders.remove(authority); + if (allowListedProviders.isEmpty()) { + mSyncAllowList.remove(username); } } // Update the Sync settings -- cgit v1.2.3 From 978af86e283956acb2d2e3e91d93ad58f1f3baf7 Mon Sep 17 00:00:00 2001 From: troub1emaker-sys <51954037+troub1emaker-sys@users.noreply.github.com> Date: Sat, 5 Jun 2021 09:08:29 +0800 Subject: Gets data and voice network type from TelephonyManager. (#141) --- .../android/mobly/snippet/bundled/TelephonySnippet.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java index fbb3041..ad0ac80 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java @@ -50,6 +50,21 @@ public class TelephonySnippet implements Snippet { return mTelephonyManager.getCallState(); } + @Rpc( + description = + "Returns a constant indicating the radio technology (network type) currently" + + "in use on the device for data transmission.") + public int getDataNetworkType() { + return mTelephonyManager.getDataNetworkType(); + } + + @Rpc( + description = + "Returns a constant indicating the radio technology (network type) currently" + + "in use on the device for voice transmission.") + public int getVoiceNetworkType() { + return mTelephonyManager.getVoiceNetworkType(); + } @Override public void shutdown() {} } -- cgit v1.2.3 From 9d70d22c8bc1db1771777e979c5f95a773ab18ef Mon Sep 17 00:00:00 2001 From: troub1emaker-sys <51954037+troub1emaker-sys@users.noreply.github.com> Date: Fri, 25 Jun 2021 23:59:46 -0700 Subject: Remove trailing spaces. (#143) --- .../com/google/android/mobly/snippet/bundled/TelephonySnippet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java index ad0ac80..21c5d1e 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/TelephonySnippet.java @@ -51,7 +51,7 @@ public class TelephonySnippet implements Snippet { } @Rpc( - description = + description = "Returns a constant indicating the radio technology (network type) currently" + "in use on the device for data transmission.") public int getDataNetworkType() { @@ -59,7 +59,7 @@ public class TelephonySnippet implements Snippet { } @Rpc( - description = + description = "Returns a constant indicating the radio technology (network type) currently" + "in use on the device for voice transmission.") public int getVoiceNetworkType() { -- cgit v1.2.3 From 1ff2867fb8645c5792656bd4b822d70bbce44ec2 Mon Sep 17 00:00:00 2001 From: troub1emaker-sys <51954037+troub1emaker-sys@users.noreply.github.com> Date: Fri, 30 Jul 2021 08:07:47 -0700 Subject: Add the Rpc for `AudioManager#isMusicActive` (#144) --- .../java/com/google/android/mobly/snippet/bundled/AudioSnippet.java | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/main/java/com/google/android/mobly/snippet/bundled') diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java index fbdb5d0..9b4874f 100644 --- a/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java +++ b/src/main/java/com/google/android/mobly/snippet/bundled/AudioSnippet.java @@ -43,6 +43,11 @@ public class AudioSnippet implements Snippet { return mAudioManager.isMicrophoneMute(); } + @Rpc(description = "Returns whether or not any music is active.") + public boolean isMusicActive() { + return mAudioManager.isMusicActive(); + } + @Rpc(description = "Gets the music stream volume.") public Integer getMusicVolume() { return mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); -- cgit v1.2.3