Controls CTS - All public controls apis
Migrate existing ControlTemplate tests to CTS. Create initial controls
CTS project. Fully exercise all templates, actions, builders, and
service provider.
Bug: 152394902
Test: atest CtsControlsDeviceTestCases
Test: cts-tradefed run singleCommand cts -m CtsControlsDeviceTestCases
Change-Id: Ia7980bda0f24db44ea1fe72be7acc30b42dfac72
diff --git a/tests/controls/Android.bp b/tests/controls/Android.bp
new file mode 100644
index 0000000..b0346be
--- /dev/null
+++ b/tests/controls/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// 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.
+
+android_test {
+ name: "CtsControlsDeviceTestCases",
+ defaults: ["cts_defaults"],
+
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+
+ static_libs: [
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "androidx.test.uiautomator_uiautomator",
+ "compatibility-device-util-axt",
+ ],
+
+ libs: [
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ ],
+
+ srcs: ["src/**/*.java"],
+ sdk_version: "test_current",
+}
\ No newline at end of file
diff --git a/tests/controls/AndroidManifest.xml b/tests/controls/AndroidManifest.xml
new file mode 100644
index 0000000..4ce024e
--- /dev/null
+++ b/tests/controls/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.controls.cts">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ <activity android:name="CtsControlsDeviceActivity" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+ <!-- self-instrumenting test package. -->
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.controls.cts" >
+ </instrumentation>
+</manifest>
diff --git a/tests/controls/AndroidTest.xml b/tests/controls/AndroidTest.xml
new file mode 100644
index 0000000..7141031
--- /dev/null
+++ b/tests/controls/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ 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.
+-->
+
+<configuration description="Config for CtsControlsDeviceTestCases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="framework" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="CtsControlsDeviceTestCases.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.controls.cts" />
+ </test>
+</configuration>
diff --git a/tests/controls/OWNERS b/tests/controls/OWNERS
new file mode 100644
index 0000000..813d05f
--- /dev/null
+++ b/tests/controls/OWNERS
@@ -0,0 +1,6 @@
+# Bug component: 802986
+mpietal@google.com
+kozynski@google.com
+nesciosquid@google.com
+ethibodeau@google.com
+dupin@google.com
\ No newline at end of file
diff --git a/tests/controls/src/android/controls/cts/CtsControlTemplateTest.java b/tests/controls/src/android/controls/cts/CtsControlTemplateTest.java
new file mode 100644
index 0000000..2c0da02
--- /dev/null
+++ b/tests/controls/src/android/controls/cts/CtsControlTemplateTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 android.controls.cts;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Parcel;
+import android.service.controls.Control;
+import android.service.controls.templates.ControlButton;
+import android.service.controls.templates.ControlTemplate;
+import android.service.controls.templates.RangeTemplate;
+import android.service.controls.templates.StatelessTemplate;
+import android.service.controls.templates.TemperatureControlTemplate;
+import android.service.controls.templates.ToggleRangeTemplate;
+import android.service.controls.templates.ToggleTemplate;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CtsControlTemplateTest {
+
+ private static final String TEST_ID = "TEST_ID";
+ private static final CharSequence TEST_ACTION_DESCRIPTION = "TEST_ACTION_DESCRIPTION";
+ private ControlButton mControlButton;
+
+ private PendingIntent mPendingIntent;
+
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ mControlButton = new ControlButton(true, TEST_ACTION_DESCRIPTION);
+ mPendingIntent = PendingIntent.getActivity(context, 1, new Intent(), 0);
+ }
+
+ @Test
+ public void testUnparcelingCorrectClass_none() {
+ ControlTemplate toParcel = ControlTemplate.getNoTemplateObject();
+
+ ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
+
+ assertEquals(ControlTemplate.getNoTemplateObject(), fromParcel);
+ }
+
+ @Test
+ public void testUnparcelingCorrectClass_toggle() {
+ ControlTemplate toParcel = new ToggleTemplate(TEST_ID, mControlButton);
+
+ ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
+
+ assertEquals(ControlTemplate.TYPE_TOGGLE, fromParcel.getTemplateType());
+ assertTrue(fromParcel instanceof ToggleTemplate);
+ }
+
+ @Test
+ public void testUnparcelingCorrectClass_range() {
+ ControlTemplate toParcel = new RangeTemplate(TEST_ID, 0, 2, 1, 1, "%f");
+
+ ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
+
+ assertEquals(ControlTemplate.TYPE_RANGE, fromParcel.getTemplateType());
+ assertTrue(fromParcel instanceof RangeTemplate);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRangeParameters_minMax() {
+ new RangeTemplate(TEST_ID, 2, 0, 1, 1, "%f");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRangeParameters_minCurrent() {
+ new RangeTemplate(TEST_ID, 0, 2, -1, 1, "%f");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRangeParameters_maxCurrent() {
+ new RangeTemplate(TEST_ID, 0, 2, 3, 1, "%f");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRangeParameters_negativeStep() {
+ new RangeTemplate(TEST_ID, 0, 2, 1, -1, "%f");
+ }
+
+ @Test
+ public void testUnparcelingCorrectClass_toggleRange() {
+ ControlTemplate toParcel = new ToggleRangeTemplate(TEST_ID, mControlButton,
+ new RangeTemplate(TEST_ID, 0, 2, 1, 1, "%f"));
+
+ ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
+
+ assertEquals(ControlTemplate.TYPE_TOGGLE_RANGE, fromParcel.getTemplateType());
+ assertTrue(fromParcel instanceof ToggleRangeTemplate);
+ }
+
+ @Test
+ public void testUnparcelingCorrectClass_stateless() {
+ ControlTemplate toParcel = new StatelessTemplate(TEST_ID);
+
+ ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
+
+ assertEquals(ControlTemplate.TYPE_STATELESS, fromParcel.getTemplateType());
+ assertTrue(fromParcel instanceof StatelessTemplate);
+ }
+
+ @Test
+ public void testUnparcelingCorrectClass_thermostat() {
+ ControlTemplate toParcel = new TemperatureControlTemplate(
+ TEST_ID,
+ new ToggleTemplate("", mControlButton),
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
+
+ ControlTemplate fromParcel = parcelAndUnparcel(toParcel);
+
+ assertEquals(ControlTemplate.TYPE_TEMPERATURE, fromParcel.getTemplateType());
+ assertTrue(fromParcel instanceof TemperatureControlTemplate);
+ }
+
+ @Test
+ public void testThermostatParams_wrongMode() {
+ TemperatureControlTemplate thermostat = new TemperatureControlTemplate(
+ TEST_ID,
+ ControlTemplate.getNoTemplateObject(),
+ -1,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
+ assertEquals(TemperatureControlTemplate.MODE_UNKNOWN, thermostat.getCurrentMode());
+
+ thermostat = new TemperatureControlTemplate(
+ TEST_ID,
+ ControlTemplate.getNoTemplateObject(),
+ 100,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
+ assertEquals(TemperatureControlTemplate.MODE_UNKNOWN, thermostat.getCurrentMode());
+ }
+
+ @Test
+ public void testThermostatParams_wrongActiveMode() {
+ TemperatureControlTemplate thermostat = new TemperatureControlTemplate(
+ TEST_ID,
+ ControlTemplate.getNoTemplateObject(),
+ TemperatureControlTemplate.MODE_OFF,
+ -1,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
+ assertEquals(TemperatureControlTemplate.MODE_UNKNOWN, thermostat.getCurrentActiveMode());
+
+ thermostat = new TemperatureControlTemplate(
+ TEST_ID,
+ ControlTemplate.getNoTemplateObject(),
+ TemperatureControlTemplate.MODE_OFF,
+ 100,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
+ assertEquals(TemperatureControlTemplate.MODE_UNKNOWN, thermostat.getCurrentActiveMode());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testThermostatParams_wrongFlags_currentMode() {
+ new TemperatureControlTemplate(
+ TEST_ID,
+ ControlTemplate.getNoTemplateObject(),
+ TemperatureControlTemplate.MODE_HEAT,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_OFF);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testThermostatParams_wrongFlags_currentActiveMode() {
+ new TemperatureControlTemplate(TEST_ID,
+ ControlTemplate.getNoTemplateObject(),
+ TemperatureControlTemplate.MODE_HEAT,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_HEAT);
+ }
+
+ private ControlTemplate parcelAndUnparcel(ControlTemplate toParcel) {
+ Parcel parcel = Parcel.obtain();
+ assertNotNull(parcel);
+
+ parcel.setDataPosition(0);
+ Control control = new Control.StatefulBuilder("1", mPendingIntent)
+ .setControlTemplate(toParcel)
+ .build();
+ control.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ return Control.CREATOR.createFromParcel(parcel).getControlTemplate();
+ }
+}
diff --git a/tests/controls/src/android/controls/cts/CtsControlsDeviceActivity.java b/tests/controls/src/android/controls/cts/CtsControlsDeviceActivity.java
new file mode 100644
index 0000000..dcf7c32
--- /dev/null
+++ b/tests/controls/src/android/controls/cts/CtsControlsDeviceActivity.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 android.controls.cts;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class CtsControlsDeviceActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ finish();
+ }
+}
diff --git a/tests/controls/src/android/controls/cts/CtsControlsPublisher.java b/tests/controls/src/android/controls/cts/CtsControlsPublisher.java
new file mode 100644
index 0000000..643d80e
--- /dev/null
+++ b/tests/controls/src/android/controls/cts/CtsControlsPublisher.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 android.controls.cts;
+
+import android.service.controls.Control;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Flow.Publisher;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.Flow.Subscription;
+
+/**
+ * Simplified Publisher for use with CTS testing only. Assumes all Controls are added
+ * to the Publisher ahead of a subscribe request. Assumes only one request() call.
+ */
+public class CtsControlsPublisher implements Publisher<Control> {
+
+ private final List<Control> mControls = new ArrayList<>();
+ private Subscriber<? super Control> mSubscriber;
+
+ public CtsControlsPublisher(List<Control> controls) {
+ if (controls != null) {
+ mControls.addAll(controls);
+ }
+ }
+
+ public void subscribe​(Subscriber<? super Control> subscriber) {
+ mSubscriber = subscriber;
+ mSubscriber.onSubscribe(new Subscription() {
+ public void request(long n) {
+ int i = 0;
+ while (i < n && i < mControls.size()) {
+ subscriber.onNext(mControls.get(i));
+ i++;
+ }
+
+ if (i == mControls.size()) {
+ subscriber.onComplete();
+ }
+ }
+
+ public void cancel() {
+
+ }
+ });
+ }
+
+ public void onNext(Control c) {
+ if (mSubscriber == null) {
+ mControls.add(c);
+ } else {
+ mSubscriber.onNext(c);
+ }
+ }
+}
diff --git a/tests/controls/src/android/controls/cts/CtsControlsService.java b/tests/controls/src/android/controls/cts/CtsControlsService.java
new file mode 100644
index 0000000..8890664
--- /dev/null
+++ b/tests/controls/src/android/controls/cts/CtsControlsService.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 android.controls.cts;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.service.controls.Control;
+import android.service.controls.ControlsProviderService;
+import android.service.controls.DeviceTypes;
+import android.service.controls.actions.BooleanAction;
+import android.service.controls.actions.CommandAction;
+import android.service.controls.actions.ControlAction;
+import android.service.controls.actions.FloatAction;
+import android.service.controls.actions.ModeAction;
+import android.service.controls.templates.ControlButton;
+import android.service.controls.templates.ControlTemplate;
+import android.service.controls.templates.RangeTemplate;
+import android.service.controls.templates.StatelessTemplate;
+import android.service.controls.templates.TemperatureControlTemplate;
+import android.service.controls.templates.ToggleRangeTemplate;
+import android.service.controls.templates.ToggleTemplate;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Flow.Publisher;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * CTS Controls Service to send known controls for testing.
+ */
+public class CtsControlsService extends ControlsProviderService {
+
+ private CtsControlsPublisher mUpdatePublisher;
+ private final List<Control> mAllControls = new ArrayList<>();
+ private final Map<String, Control> mControlsById = new HashMap<>();
+ private final Context mContext;
+ private final PendingIntent mPendingIntent;
+
+ public CtsControlsService() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ mPendingIntent = PendingIntent.getActivity(mContext, 1, new Intent(),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ mAllControls.add(buildLight(false /* isOn */, 0.0f /* intensity */));
+ mAllControls.add(buildLock(false /* isLocked */));
+ mAllControls.add(buildRoutine());
+ mAllControls.add(buildThermostat(TemperatureControlTemplate.MODE_OFF));
+ mAllControls.add(buildMower(false /* isStarted */));
+ mAllControls.add(buildSwitch(false /* isOn */));
+ mAllControls.add(buildGate(false /* isLocked */));
+
+ for (Control c : mAllControls) {
+ mControlsById.put(c.getControlId(), c);
+ }
+ }
+
+ public Control buildLight(boolean isOn, float intensity) {
+ RangeTemplate rt = new RangeTemplate("range", 0.0f, 100.0f, intensity, 1.0f, null);
+ ControlTemplate template =
+ new ToggleRangeTemplate("toggleRange", isOn, isOn ? "On" : "Off", rt);
+ return new Control.StatefulBuilder("light", mPendingIntent)
+ .setTitle("Light Title")
+ .setSubtitle("Light Subtitle")
+ .setStatus(Control.STATUS_OK)
+ .setStatusText(isOn ? "On" : "Off")
+ .setDeviceType(DeviceTypes.TYPE_LIGHT)
+ .setStructure("Home")
+ .setControlTemplate(template)
+ .build();
+ }
+
+ public Control buildSwitch(boolean isOn) {
+ ControlButton button = new ControlButton(isOn, isOn ? "On" : "Off");
+ ControlTemplate template = new ToggleTemplate("toggle", button);
+ return new Control.StatefulBuilder("switch", mPendingIntent)
+ .setTitle("Switch Title")
+ .setSubtitle("Switch Subtitle")
+ .setStatus(Control.STATUS_OK)
+ .setStatusText(isOn ? "On" : "Off")
+ .setDeviceType(DeviceTypes.TYPE_SWITCH)
+ .setStructure("Home")
+ .setControlTemplate(template)
+ .build();
+ }
+
+
+ public Control buildMower(boolean isStarted) {
+ String desc = isStarted ? "Started" : "Stopped";
+ ControlButton button = new ControlButton(isStarted, desc);
+ ControlTemplate template = new ToggleTemplate("toggle", button);
+ return new Control.StatefulBuilder("mower", mPendingIntent)
+ .setTitle("Mower Title")
+ .setSubtitle("Mower Subtitle")
+ .setStatus(Control.STATUS_OK)
+ .setStatusText(desc)
+ .setDeviceType(DeviceTypes.TYPE_MOWER)
+ .setStructure("Vacation")
+ .setZone("Outside")
+ .setControlTemplate(template)
+ .build();
+ }
+
+ public Control buildLock(boolean isLocked) {
+ String desc = isLocked ? "Locked" : "Unlocked";
+ ControlButton button = new ControlButton(isLocked, desc);
+ ControlTemplate template = new ToggleTemplate("toggle", button);
+ return new Control.StatefulBuilder("lock", mPendingIntent)
+ .setTitle("Lock Title")
+ .setSubtitle("Lock Subtitle")
+ .setStatus(Control.STATUS_OK)
+ .setStatusText(desc)
+ .setDeviceType(DeviceTypes.TYPE_LOCK)
+ .setControlTemplate(template)
+ .build();
+ }
+
+ public Control buildGate(boolean isLocked) {
+ String desc = isLocked ? "Locked" : "Unlocked";
+ ControlButton button = new ControlButton(isLocked, desc);
+ ControlTemplate template = new ToggleTemplate("toggle", button);
+ return new Control.StatefulBuilder("gate", mPendingIntent)
+ .setTitle("Gate Title")
+ .setSubtitle("Gate Subtitle")
+ .setStatus(Control.STATUS_OK)
+ .setStatusText(desc)
+ .setDeviceType(DeviceTypes.TYPE_GATE)
+ .setControlTemplate(template)
+ .setStructure("Other home")
+ .build();
+ }
+
+ public Control buildThermostat(int mode) {
+ ControlTemplate template = new TemperatureControlTemplate("temperature",
+ ControlTemplate.getNoTemplateObject(),
+ mode,
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_HEAT
+ | TemperatureControlTemplate.FLAG_MODE_COOL
+ | TemperatureControlTemplate.FLAG_MODE_OFF
+ | TemperatureControlTemplate.FLAG_MODE_ECO);
+
+ return new Control.StatefulBuilder("thermostat", mPendingIntent)
+ .setTitle("Thermostat Title")
+ .setSubtitle("Thermostat Subtitle")
+ .setStatus(Control.STATUS_OK)
+ .setStatusText("Off")
+ .setDeviceType(DeviceTypes.TYPE_THERMOSTAT)
+ .setControlTemplate(template)
+ .build();
+ }
+
+ public Control buildRoutine() {
+ ControlTemplate template = new StatelessTemplate("stateless");
+ return new Control.StatefulBuilder("routine", mPendingIntent)
+ .setTitle("Routine Title")
+ .setSubtitle("Routine Subtitle")
+ .setStatus(Control.STATUS_OK)
+ .setStatusText("Good Morning")
+ .setDeviceType(DeviceTypes.TYPE_ROUTINE)
+ .setControlTemplate(template)
+ .build();
+ }
+
+ @Override
+ public Publisher<Control> createPublisherForAllAvailable() {
+ return new CtsControlsPublisher(mAllControls.stream()
+ .map(c -> new Control.StatelessBuilder(c).build())
+ .collect(Collectors.toList()));
+ }
+
+ @Override
+ public Publisher<Control> createPublisherForSuggested() {
+ return new CtsControlsPublisher(mAllControls.stream()
+ .map(c -> new Control.StatelessBuilder(c).build())
+ .collect(Collectors.toList()));
+ }
+
+ @Override
+ public Publisher<Control> createPublisherFor(List<String> controlIds) {
+ mUpdatePublisher = new CtsControlsPublisher(null);
+
+ for (String id : controlIds) {
+ Control control = mControlsById.get(id);
+ if (control == null) continue;
+
+ mUpdatePublisher.onNext(control);
+ }
+
+ return mUpdatePublisher;
+ }
+
+ @Override
+ public void performControlAction(String controlId, ControlAction action,
+ Consumer<Integer> consumer) {
+ Control c = mControlsById.get(controlId);
+ if (c == null) return;
+
+ Control.StatefulBuilder builder = controlToBuilder(c);
+
+ // Modify the builder in order to update the Control to have predefined, verifiable behavior
+ if (action instanceof BooleanAction) {
+ BooleanAction b = (BooleanAction) action;
+
+ if (c.getDeviceType() == DeviceTypes.TYPE_LIGHT) {
+ RangeTemplate rt = new RangeTemplate("range",
+ 0.0f /* minValue */,
+ 100.0f /* maxValue */,
+ 50.0f /* currentValue */,
+ 1.0f /* step */, null);
+ String desc = b.getNewState() ? "On" : "Off";
+
+ builder.setStatusText(desc);
+ builder.setControlTemplate(new ToggleRangeTemplate("toggleRange", b.getNewState(),
+ desc, rt));
+ } else if (c.getDeviceType() == DeviceTypes.TYPE_ROUTINE) {
+ builder.setStatusText("Running");
+ builder.setControlTemplate(new StatelessTemplate("stateless"));
+ } else if (c.getDeviceType() == DeviceTypes.TYPE_SWITCH) {
+ String desc = b.getNewState() ? "On" : "Off";
+ builder.setStatusText(desc);
+ ControlButton button = new ControlButton(b.getNewState(), desc);
+ builder.setControlTemplate(new ToggleTemplate("toggle", button));
+ } else if (c.getDeviceType() == DeviceTypes.TYPE_LOCK) {
+ String value = action.getChallengeValue();
+ if (value != null && value.equals("1234")) {
+ String desc = b.getNewState() ? "Locked" : "Unlocked";
+ ControlButton button = new ControlButton(b.getNewState(), desc);
+ builder.setStatusText(desc);
+ builder.setControlTemplate(new ToggleTemplate("toggle", button));
+ } else {
+ consumer.accept(ControlAction.RESPONSE_CHALLENGE_PIN);
+ return;
+ }
+ } else if (c.getDeviceType() == DeviceTypes.TYPE_GATE) {
+ String value = action.getChallengeValue();
+ if (value != null && value.equals("abc123")) {
+ String desc = b.getNewState() ? "Locked" : "Unlocked";
+ ControlButton button = new ControlButton(b.getNewState(), desc);
+ builder.setStatusText(desc);
+ builder.setControlTemplate(new ToggleTemplate("toggle", button));
+ } else {
+ consumer.accept(ControlAction.RESPONSE_CHALLENGE_PASSPHRASE);
+ return;
+ }
+ } else if (c.getDeviceType() == DeviceTypes.TYPE_MOWER) {
+ String value = action.getChallengeValue();
+ if (value != null && value.equals("true")) {
+ String desc = b.getNewState() ? "Started" : "Stopped";
+ ControlButton button = new ControlButton(b.getNewState(), desc);
+ builder.setStatusText(desc);
+ builder.setControlTemplate(new ToggleTemplate("toggle", button));
+ } else {
+ consumer.accept(ControlAction.RESPONSE_CHALLENGE_ACK);
+ return;
+ }
+ }
+ } else if (action instanceof FloatAction) {
+ FloatAction f = (FloatAction) action;
+ if (c.getDeviceType() == DeviceTypes.TYPE_LIGHT) {
+ RangeTemplate rt = new RangeTemplate("range", 0.0f, 100.0f, f.getNewValue(), 1.0f,
+ null);
+
+ ToggleRangeTemplate trt = (ToggleRangeTemplate) c.getControlTemplate();
+ String desc = trt.getActionDescription().toString();
+ boolean state = trt.isChecked();
+
+ builder.setStatusText(desc);
+ builder.setControlTemplate(new ToggleRangeTemplate("toggleRange", state, desc, rt));
+ }
+ } else if (action instanceof ModeAction) {
+ ModeAction m = (ModeAction) action;
+ if (c.getDeviceType() == DeviceTypes.TYPE_THERMOSTAT) {
+ ControlTemplate template = new TemperatureControlTemplate("temperature",
+ ControlTemplate.getNoTemplateObject(),
+ m.getNewMode(),
+ TemperatureControlTemplate.MODE_OFF,
+ TemperatureControlTemplate.FLAG_MODE_HEAT
+ | TemperatureControlTemplate.FLAG_MODE_COOL
+ | TemperatureControlTemplate.FLAG_MODE_OFF
+ | TemperatureControlTemplate.FLAG_MODE_ECO);
+
+ builder.setControlTemplate(template);
+ }
+ } else if (action instanceof CommandAction) {
+ builder.setControlTemplate(new StatelessTemplate("stateless"));
+ }
+
+ // Finally build and send the default OK status
+ Control updatedControl = builder.build();
+ mControlsById.put(controlId, updatedControl);
+ mUpdatePublisher.onNext(updatedControl);
+ consumer.accept(ControlAction.RESPONSE_OK);
+ }
+
+ private Control.StatefulBuilder controlToBuilder(Control c) {
+ return new Control.StatefulBuilder(c.getControlId(), c.getAppIntent())
+ .setTitle(c.getTitle())
+ .setSubtitle(c.getSubtitle())
+ .setStructure(c.getStructure())
+ .setDeviceType(c.getDeviceType())
+ .setZone(c.getZone())
+ .setStatus(Control.STATUS_OK)
+ .setStatusText("Refreshed");
+ }
+}
diff --git a/tests/controls/src/android/controls/cts/CtsControlsServiceTest.java b/tests/controls/src/android/controls/cts/CtsControlsServiceTest.java
new file mode 100644
index 0000000..97dae17
--- /dev/null
+++ b/tests/controls/src/android/controls/cts/CtsControlsServiceTest.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 android.controls.cts;
+
+import android.service.controls.Control;
+import android.service.controls.actions.BooleanAction;
+import android.service.controls.actions.CommandAction;
+import android.service.controls.actions.ControlAction;
+import android.service.controls.actions.FloatAction;
+import android.service.controls.actions.ModeAction;
+import android.service.controls.templates.ControlTemplate;
+import android.service.controls.templates.RangeTemplate;
+import android.service.controls.templates.TemperatureControlTemplate;
+import android.service.controls.templates.ToggleRangeTemplate;
+import android.service.controls.templates.ToggleTemplate;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Flow.Publisher;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.Flow.Subscription;
+import java.util.function.Consumer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+@RunWith(AndroidJUnit4.class)
+public class CtsControlsServiceTest {
+
+ private CtsControlsService mControlsService;
+
+ @Before
+ public void setUp() {
+ mControlsService = new CtsControlsService();
+ }
+
+ @Test
+ public void testLoadAllAvailable() {
+ Publisher<Control> publisher = mControlsService.createPublisherForAllAvailable();
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 10, loadedControls);
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(new Control.StatelessBuilder(
+ mControlsService.buildLight(false, 0.0f)).build());
+ expectedControls.add(new Control.StatelessBuilder(
+ mControlsService.buildLock(false)).build());
+ expectedControls.add(new Control.StatelessBuilder(
+ mControlsService.buildRoutine()).build());
+ expectedControls.add(new Control.StatelessBuilder(mControlsService.buildThermostat(
+ TemperatureControlTemplate.MODE_OFF)).build());
+ expectedControls.add(new Control.StatelessBuilder(
+ mControlsService.buildMower(false)).build());
+ expectedControls.add(new Control.StatelessBuilder(
+ mControlsService.buildSwitch(false)).build());
+ expectedControls.add(new Control.StatelessBuilder(
+ mControlsService.buildGate(false)).build());
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ @Test
+ public void testLoadSuggested() {
+ Publisher<Control> publisher = mControlsService.createPublisherForSuggested();
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 3, loadedControls);
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(new Control.StatelessBuilder(
+ mControlsService.buildLight(false, 0.0f)).build());
+ expectedControls.add(new Control.StatelessBuilder(
+ mControlsService.buildLock(false)).build());
+ expectedControls.add(new Control.StatelessBuilder(
+ mControlsService.buildRoutine()).build());
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ @Test
+ public void testPublisherForSingleControl() {
+ List<String> idsToLoad = new ArrayList<>();
+ idsToLoad.add("mower");
+
+ Publisher<Control> publisher = mControlsService.createPublisherFor(idsToLoad);
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 10, loadedControls);
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(mControlsService.buildMower(false));
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ @Test
+ public void testPublisherForMultipleControls() {
+ List<String> idsToLoad = new ArrayList<>();
+ idsToLoad.add("lock");
+ idsToLoad.add("light");
+
+ Publisher<Control> publisher = mControlsService.createPublisherFor(idsToLoad);
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 10, loadedControls);
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(mControlsService.buildLock(false));
+ expectedControls.add(mControlsService.buildLight(false, 0.0f));
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ @Test
+ public void testBooleanAction() {
+ List<String> idsToLoad = new ArrayList<>();
+ idsToLoad.add("switch");
+
+ Publisher<Control> publisher = mControlsService.createPublisherFor(idsToLoad);
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 10, loadedControls);
+
+ mControlsService.performControlAction("switch", new BooleanAction("action", true),
+ assertConsumer(ControlAction.RESPONSE_OK));
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(mControlsService.buildSwitch(false));
+ expectedControls.add(mControlsService.buildSwitch(true));
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ @Test
+ public void testFloatAction() {
+ List<String> idsToLoad = new ArrayList<>();
+ idsToLoad.add("light");
+
+ Publisher<Control> publisher = mControlsService.createPublisherFor(idsToLoad);
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 10, loadedControls);
+
+ mControlsService.performControlAction("light", new BooleanAction("action", true),
+ assertConsumer(ControlAction.RESPONSE_OK));
+
+ mControlsService.performControlAction("light", new FloatAction("action", 80.0f),
+ assertConsumer(ControlAction.RESPONSE_OK));
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(mControlsService.buildLight(false, 0.0f));
+ expectedControls.add(mControlsService.buildLight(true, 50.0f));
+ expectedControls.add(mControlsService.buildLight(true, 80.0f));
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ @Test
+ public void testCommandAction() {
+ List<String> idsToLoad = new ArrayList<>();
+ idsToLoad.add("routine");
+
+ Publisher<Control> publisher = mControlsService.createPublisherFor(idsToLoad);
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 10, loadedControls);
+
+ mControlsService.performControlAction("routine", new CommandAction("action"),
+ assertConsumer(ControlAction.RESPONSE_OK));
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(mControlsService.buildRoutine());
+ expectedControls.add(mControlsService.buildRoutine());
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ @Test
+ public void testBooleanActionWithPinChallenge() {
+ List<String> idsToLoad = new ArrayList<>();
+ idsToLoad.add("lock");
+
+ Publisher<Control> publisher = mControlsService.createPublisherFor(idsToLoad);
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 10, loadedControls);
+
+ mControlsService.performControlAction("lock", new BooleanAction("action", true),
+ assertConsumer(ControlAction.RESPONSE_CHALLENGE_PIN));
+
+ mControlsService.performControlAction("lock", new BooleanAction("action", true, "1234"),
+ assertConsumer(ControlAction.RESPONSE_OK));
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(mControlsService.buildLock(false));
+ expectedControls.add(mControlsService.buildLock(true));
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ @Test
+ public void testBooleanActionWithPassphraseChallenge() {
+ List<String> idsToLoad = new ArrayList<>();
+ idsToLoad.add("gate");
+
+ Publisher<Control> publisher = mControlsService.createPublisherFor(idsToLoad);
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 10, loadedControls);
+
+ mControlsService.performControlAction("gate", new BooleanAction("action", true),
+ assertConsumer(ControlAction.RESPONSE_CHALLENGE_PASSPHRASE));
+
+ mControlsService.performControlAction("gate", new BooleanAction("action", true, "abc123"),
+ assertConsumer(ControlAction.RESPONSE_OK));
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(mControlsService.buildGate(false));
+ expectedControls.add(mControlsService.buildGate(true));
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ @Test
+ public void testBooleanActionWithAckChallenge() {
+ List<String> idsToLoad = new ArrayList<>();
+ idsToLoad.add("mower");
+
+ Publisher<Control> publisher = mControlsService.createPublisherFor(idsToLoad);
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 10, loadedControls);
+
+ mControlsService.performControlAction("mower", new BooleanAction("action", true),
+ assertConsumer(ControlAction.RESPONSE_CHALLENGE_ACK));
+
+ mControlsService.performControlAction("mower", new BooleanAction("action", true, "true"),
+ assertConsumer(ControlAction.RESPONSE_OK));
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(mControlsService.buildMower(false));
+ expectedControls.add(mControlsService.buildMower(true));
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ @Test
+ public void testModeAction() {
+ List<String> idsToLoad = new ArrayList<>();
+ idsToLoad.add("thermostat");
+
+ Publisher<Control> publisher = mControlsService.createPublisherFor(idsToLoad);
+ List<Control> loadedControls = new ArrayList<>();
+ subscribe(publisher, 10, loadedControls);
+
+ mControlsService.performControlAction("thermostat",
+ new ModeAction("action", TemperatureControlTemplate.MODE_COOL),
+ assertConsumer(ControlAction.RESPONSE_OK));
+
+ List<Control> expectedControls = new ArrayList<>();
+ expectedControls.add(mControlsService.buildThermostat(TemperatureControlTemplate.MODE_OFF));
+ expectedControls.add(mControlsService.buildThermostat(
+ TemperatureControlTemplate.MODE_COOL));
+
+ assertControlsList(loadedControls, expectedControls);
+ }
+
+ private void assertConsumerOk(int status) {
+ assertEquals(status, ControlAction.RESPONSE_OK);
+ }
+
+ private Consumer<Integer> assertConsumer(int expectedStatus) {
+ return (status) -> {
+ assertEquals((int) status, expectedStatus);
+ };
+ }
+
+ private void subscribe(Publisher<Control> publisher, final int request,
+ final List<Control> addToList) {
+ publisher.subscribe(new Subscriber<Control>() {
+ public void onSubscribe(Subscription s) {
+ s.request(request);
+ }
+
+ public void onNext(Control c) {
+ addToList.add(c);
+ }
+
+ public void onError(Throwable t) {
+ throw new IllegalStateException("onError should not be called here");
+ }
+
+ public void onComplete() {
+
+ }
+ });
+ }
+
+ private void assertControlsList(List<Control> actualControls, List<Control> expectedControls) {
+ assertEquals(actualControls.size(), expectedControls.size());
+
+ for (int i = 0; i < actualControls.size(); i++) {
+ assertControlEquals(actualControls.get(i), expectedControls.get(i));
+ }
+ }
+
+ private void assertControlEquals(Control c1, Control c2) {
+ assertEquals(c1.getTitle(), c2.getTitle());
+ assertEquals(c1.getSubtitle(), c2.getSubtitle());
+ assertEquals(c1.getStructure(), c2.getStructure());
+ assertEquals(c1.getZone(), c2.getZone());
+ assertEquals(c1.getDeviceType(), c2.getDeviceType());
+ assertEquals(c1.getStatus(), c2.getStatus());
+ assertEquals(c1.getControlId(), c2.getControlId());
+
+ assertTemplateEquals(c1.getControlTemplate(), c2.getControlTemplate());
+ }
+
+ private void assertTemplateEquals(ControlTemplate ct1, ControlTemplate ct2) {
+ if (ct1 == null) {
+ assertNull(ct2);
+ return;
+ } else {
+ assertNotNull(ct2);
+ }
+
+ assertEquals(ct1.getTemplateType(), ct2.getTemplateType());
+ assertEquals(ct1.getTemplateId(), ct2.getTemplateId());
+
+ switch (ct1.getTemplateType()) {
+ case ControlTemplate.TYPE_TOGGLE:
+ assertToggleTemplate((ToggleTemplate) ct1, (ToggleTemplate) ct2);
+ break;
+ case ControlTemplate.TYPE_RANGE:
+ assertRangeTemplate((RangeTemplate) ct1, (RangeTemplate) ct2);
+ break;
+ case ControlTemplate.TYPE_TEMPERATURE:
+ assertTemperatureControlTemplate((TemperatureControlTemplate) ct1,
+ (TemperatureControlTemplate) ct2);
+ break;
+ case ControlTemplate.TYPE_TOGGLE_RANGE:
+ assertToggleRangeTemplate((ToggleRangeTemplate) ct1, (ToggleRangeTemplate) ct2);
+ break;
+ }
+ }
+
+ private void assertToggleTemplate(ToggleTemplate t1, ToggleTemplate t2) {
+ assertEquals(t1.isChecked(), t2.isChecked());
+ assertEquals(t1.getContentDescription(), t2.getContentDescription());
+ }
+
+ private void assertRangeTemplate(RangeTemplate t1, RangeTemplate t2) {
+ assertEquals(t1.getMinValue(), t2.getMinValue(), 0.0f);
+ assertEquals(t1.getMaxValue(), t2.getMaxValue(), 0.0f);
+ assertEquals(t1.getCurrentValue(), t2.getCurrentValue(), 0.0f);
+ assertEquals(t1.getStepValue(), t2.getStepValue(), 0.0f);
+ assertEquals(t1.getFormatString(), t2.getFormatString());
+ }
+
+ private void assertTemperatureControlTemplate(TemperatureControlTemplate t1,
+ TemperatureControlTemplate t2) {
+ assertEquals(t1.getCurrentMode(), t2.getCurrentMode());
+ assertEquals(t1.getCurrentActiveMode(), t2.getCurrentActiveMode());
+ assertEquals(t1.getModes(), t2.getModes());
+ assertTemplateEquals(t1.getTemplate(), t2.getTemplate());
+ }
+
+ private void assertToggleRangeTemplate(ToggleRangeTemplate t1, ToggleRangeTemplate t2) {
+ assertEquals(t1.isChecked(), t2.isChecked());
+ assertEquals(t1.getActionDescription(), t2.getActionDescription());
+ assertRangeTemplate(t1.getRange(), t2.getRange());
+ }
+}