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());
+    }
+}