xref: /aosp_15_r20/cts/tests/controls/src/android/controls/cts/CtsControlsService.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.controls.cts;
17 
18 import static org.junit.Assert.assertEquals;
19 import static org.junit.Assert.assertNotEquals;
20 
21 import android.app.PendingIntent;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.res.ColorStateList;
25 import android.graphics.drawable.Icon;
26 import android.service.controls.Control;
27 import android.service.controls.ControlsProviderService;
28 import android.service.controls.DeviceTypes;
29 import android.service.controls.actions.BooleanAction;
30 import android.service.controls.actions.CommandAction;
31 import android.service.controls.actions.ControlAction;
32 import android.service.controls.actions.FloatAction;
33 import android.service.controls.actions.ModeAction;
34 import android.service.controls.templates.ControlButton;
35 import android.service.controls.templates.ControlTemplate;
36 import android.service.controls.templates.RangeTemplate;
37 import android.service.controls.templates.StatelessTemplate;
38 import android.service.controls.templates.TemperatureControlTemplate;
39 import android.service.controls.templates.ThumbnailTemplate;
40 import android.service.controls.templates.ToggleRangeTemplate;
41 import android.service.controls.templates.ToggleTemplate;
42 
43 import androidx.test.InstrumentationRegistry;
44 
45 import java.util.ArrayList;
46 import java.util.HashMap;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.concurrent.Flow.Publisher;
50 import java.util.function.Consumer;
51 import java.util.stream.Collectors;
52 
53 /**
54   * CTS Controls Service to send known controls for testing.
55   */
56 public class CtsControlsService extends ControlsProviderService {
57 
58     private CtsControlsPublisher mUpdatePublisher;
59     private final List<Control> mAllControls = new ArrayList<>();
60     private final Map<String, Control> mControlsById = new HashMap<>();
61     private final Context mContext;
62     private final PendingIntent mPendingIntent;
63     private ColorStateList mColorStateList;
64     private Icon mIcon;
65 
CtsControlsService()66     public CtsControlsService() {
67         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
68         mPendingIntent = PendingIntent.getActivity(mContext, 1,
69                 new Intent().setPackage(mContext.getPackageName()),
70             PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
71         mIcon = Icon.createWithResource(mContext, R.drawable.ic_device_unknown);
72         mColorStateList = mContext.getResources().getColorStateList(R.color.custom_mower, null);
73 
74         mAllControls.add(buildLight(false /* isOn */, 0.0f /* intensity */));
75         mAllControls.add(buildLock(false /* isLocked */));
76         mAllControls.add(buildRoutine());
77         mAllControls.add(buildThermostat(TemperatureControlTemplate.MODE_OFF));
78         mAllControls.add(buildMower(false /* isStarted */));
79         mAllControls.add(buildSwitch(false /* isOn */));
80         mAllControls.add(buildGate(false /* isLocked */));
81         mAllControls.add(buildCamera(true /* isActive */));
82 
83         for (Control c : mAllControls) {
84             mControlsById.put(c.getControlId(), c);
85         }
86     }
87 
buildLight(boolean isOn, float intensity)88     public Control buildLight(boolean isOn, float intensity) {
89         RangeTemplate rt = new RangeTemplate("range", 0.0f, 100.0f, intensity, 1.0f, null);
90         ControlTemplate template =
91                 new ToggleRangeTemplate("toggleRange", isOn, isOn ? "On" : "Off", rt);
92         return new Control.StatefulBuilder("light", mPendingIntent)
93             .setTitle("Light Title")
94             .setSubtitle("Light Subtitle")
95             .setStatus(Control.STATUS_OK)
96             .setStatusText(isOn ? "On" : "Off")
97             .setDeviceType(DeviceTypes.TYPE_LIGHT)
98             .setStructure("Home")
99             .setControlTemplate(template)
100             .setAuthRequired(false)
101             .build();
102     }
103 
buildSwitch(boolean isOn)104     public Control buildSwitch(boolean isOn) {
105         ControlButton button = new ControlButton(isOn, isOn ? "On" : "Off");
106         ControlTemplate template = new ToggleTemplate("toggle", button);
107         return new Control.StatefulBuilder("switch", mPendingIntent)
108             .setTitle("Switch Title")
109             .setSubtitle("Switch Subtitle")
110             .setStatus(Control.STATUS_OK)
111             .setStatusText(isOn ? "On" : "Off")
112             .setDeviceType(DeviceTypes.TYPE_SWITCH)
113             .setStructure("Home")
114             .setControlTemplate(template)
115             .build();
116     }
117 
118 
buildMower(boolean isStarted)119     public Control buildMower(boolean isStarted) {
120         String desc = isStarted ? "Started" : "Stopped";
121         ControlButton button = new ControlButton(isStarted, desc);
122         ControlTemplate template = new ToggleTemplate("toggle", button);
123         return new Control.StatefulBuilder("mower", mPendingIntent)
124             .setTitle("Mower Title")
125             .setSubtitle("Mower Subtitle")
126             .setStatus(Control.STATUS_OK)
127             .setStatusText(desc)
128             .setDeviceType(DeviceTypes.TYPE_MOWER)
129             .setStructure("Vacation")
130             .setZone("Outside")
131             .setControlTemplate(template)
132             .setCustomIcon(mIcon)
133             .setCustomColor(mColorStateList)
134             .build();
135     }
136 
buildLock(boolean isLocked)137     public Control buildLock(boolean isLocked) {
138         String desc = isLocked ? "Locked" : "Unlocked";
139         ControlButton button = new ControlButton(isLocked, desc);
140         ControlTemplate template = new ToggleTemplate("toggle", button);
141         return new Control.StatefulBuilder("lock", mPendingIntent)
142             .setTitle("Lock Title")
143             .setSubtitle("Lock Subtitle")
144             .setStatus(Control.STATUS_OK)
145             .setStatusText(desc)
146             .setDeviceType(DeviceTypes.TYPE_LOCK)
147             .setControlTemplate(template)
148             .build();
149     }
150 
buildGate(boolean isLocked)151     public Control buildGate(boolean isLocked) {
152         String desc = isLocked ? "Locked" : "Unlocked";
153         ControlButton button = new ControlButton(isLocked, desc);
154         ControlTemplate template = new ToggleTemplate("toggle", button);
155         return new Control.StatefulBuilder("gate", mPendingIntent)
156             .setTitle("Gate Title")
157             .setSubtitle("Gate Subtitle")
158             .setStatus(Control.STATUS_OK)
159             .setStatusText(desc)
160             .setDeviceType(DeviceTypes.TYPE_GATE)
161             .setControlTemplate(template)
162             .setStructure("Other home")
163             .build();
164     }
165 
buildThermostat(int mode)166     public Control buildThermostat(int mode) {
167         ControlTemplate template = new TemperatureControlTemplate("temperature",
168                     ControlTemplate.getNoTemplateObject(),
169                     mode,
170                     TemperatureControlTemplate.MODE_OFF,
171                     TemperatureControlTemplate.FLAG_MODE_HEAT
172                     | TemperatureControlTemplate.FLAG_MODE_COOL
173                     | TemperatureControlTemplate.FLAG_MODE_OFF
174                     | TemperatureControlTemplate.FLAG_MODE_ECO);
175 
176         return new Control.StatefulBuilder("thermostat", mPendingIntent)
177             .setTitle("Thermostat Title")
178             .setSubtitle("Thermostat Subtitle")
179             .setStatus(Control.STATUS_OK)
180             .setStatusText("Off")
181             .setDeviceType(DeviceTypes.TYPE_THERMOSTAT)
182             .setControlTemplate(template)
183             .build();
184     }
185 
buildRoutine()186     public Control buildRoutine() {
187         ControlTemplate template = new StatelessTemplate("stateless");
188         return new Control.StatefulBuilder("routine", mPendingIntent)
189             .setTitle("Routine Title")
190             .setSubtitle("Routine Subtitle")
191             .setStatus(Control.STATUS_OK)
192             .setStatusText("Good Morning")
193             .setDeviceType(DeviceTypes.TYPE_ROUTINE)
194             .setControlTemplate(template)
195             .build();
196     }
197 
buildCamera(boolean active)198     public Control buildCamera(boolean active) {
199         String description = active ? "Live" : "Not live";
200         ControlTemplate template = new ThumbnailTemplate("thumbnail", active, mIcon, description);
201         return new Control.StatefulBuilder("camera", mPendingIntent)
202                 .setTitle("Camera Title")
203                 .setTitle("Camera Subtitle")
204                 .setStatus(Control.STATUS_OK)
205                 .setStatusText(description)
206                 .setDeviceType(DeviceTypes.TYPE_CAMERA)
207                 .setControlTemplate(template)
208                 .build();
209     }
210 
211     @Override
createPublisherForAllAvailable()212     public Publisher<Control> createPublisherForAllAvailable() {
213         return new CtsControlsPublisher(mAllControls.stream()
214             .map(c -> new Control.StatelessBuilder(c).build())
215             .collect(Collectors.toList()));
216     }
217 
218     @Override
createPublisherForSuggested()219     public Publisher<Control> createPublisherForSuggested() {
220         return new CtsControlsPublisher(mAllControls.stream()
221             .map(c -> new Control.StatelessBuilder(c).build())
222             .collect(Collectors.toList()));
223     }
224 
225     @Override
createPublisherFor(List<String> controlIds)226     public Publisher<Control> createPublisherFor(List<String> controlIds) {
227         mUpdatePublisher = new CtsControlsPublisher(null);
228 
229         for (String id : controlIds) {
230             Control control = mControlsById.get(id);
231             if (control == null) continue;
232 
233             mUpdatePublisher.onNext(control);
234         }
235 
236         return mUpdatePublisher;
237     }
238 
239     @Override
performControlAction(String controlId, ControlAction action, Consumer<Integer> consumer)240     public void performControlAction(String controlId, ControlAction action,
241             Consumer<Integer> consumer) {
242         Control c = mControlsById.get(controlId);
243         if (c == null) return;
244 
245         // all values are hardcoded for this test
246         assertEquals(action.getTemplateId(), "action");
247         assertNotEquals(action, ControlAction.getErrorAction());
248 
249         Control.StatefulBuilder builder = controlToBuilder(c);
250 
251         // Modify the builder in order to update the Control to have predefined, verifiable behavior
252         if (action instanceof BooleanAction) {
253             BooleanAction b = (BooleanAction) action;
254 
255             if (c.getDeviceType() == DeviceTypes.TYPE_LIGHT) {
256                 RangeTemplate rt = new RangeTemplate("range",
257                         0.0f /* minValue */,
258                         100.0f /* maxValue */,
259                         50.0f /* currentValue */,
260                         1.0f /* step */, null);
261                 String desc = b.getNewState() ? "On" : "Off";
262 
263                 builder.setStatusText(desc);
264                 builder.setControlTemplate(new ToggleRangeTemplate("toggleRange", b.getNewState(),
265                         desc, rt));
266             } else if (c.getDeviceType() == DeviceTypes.TYPE_ROUTINE) {
267                 builder.setStatusText("Running");
268                 builder.setControlTemplate(new StatelessTemplate("stateless"));
269             } else if (c.getDeviceType() == DeviceTypes.TYPE_SWITCH) {
270                 String desc = b.getNewState() ? "On" : "Off";
271                 builder.setStatusText(desc);
272                 ControlButton button = new ControlButton(b.getNewState(), desc);
273                 builder.setControlTemplate(new ToggleTemplate("toggle", button));
274             } else if (c.getDeviceType() == DeviceTypes.TYPE_LOCK) {
275                 String value = action.getChallengeValue();
276                 if (value != null && value.equals("1234")) {
277                     String desc = b.getNewState() ? "Locked" : "Unlocked";
278                     ControlButton button = new ControlButton(b.getNewState(), desc);
279                     builder.setStatusText(desc);
280                     builder.setControlTemplate(new ToggleTemplate("toggle", button));
281                 } else {
282                     consumer.accept(ControlAction.RESPONSE_CHALLENGE_PIN);
283                     return;
284                 }
285             } else if (c.getDeviceType() == DeviceTypes.TYPE_GATE) {
286                 String value = action.getChallengeValue();
287                 if (value != null && value.equals("abc123")) {
288                     String desc = b.getNewState() ? "Locked" : "Unlocked";
289                     ControlButton button = new ControlButton(b.getNewState(), desc);
290                     builder.setStatusText(desc);
291                     builder.setControlTemplate(new ToggleTemplate("toggle", button));
292                 } else {
293                     consumer.accept(ControlAction.RESPONSE_CHALLENGE_PASSPHRASE);
294                     return;
295                 }
296             } else if (c.getDeviceType() == DeviceTypes.TYPE_MOWER) {
297                 String value = action.getChallengeValue();
298                 if (value != null && value.equals("true")) {
299                     String desc = b.getNewState() ? "Started" : "Stopped";
300                     ControlButton button = new ControlButton(b.getNewState(), desc);
301                     builder.setStatusText(desc);
302                     builder.setControlTemplate(new ToggleTemplate("toggle", button));
303                 } else {
304                     consumer.accept(ControlAction.RESPONSE_CHALLENGE_ACK);
305                     return;
306                 }
307             }
308         } else if (action instanceof FloatAction) {
309             FloatAction f = (FloatAction) action;
310             if (c.getDeviceType() == DeviceTypes.TYPE_LIGHT) {
311                 RangeTemplate rt = new RangeTemplate("range", 0.0f, 100.0f, f.getNewValue(), 1.0f,
312                         null);
313 
314                 ToggleRangeTemplate trt = (ToggleRangeTemplate) c.getControlTemplate();
315                 String desc = trt.getActionDescription().toString();
316                 boolean state = trt.isChecked();
317 
318                 builder.setStatusText(desc);
319                 builder.setControlTemplate(new ToggleRangeTemplate("toggleRange", state, desc, rt));
320             }
321         } else if (action instanceof ModeAction) {
322             ModeAction m = (ModeAction) action;
323             if (c.getDeviceType() == DeviceTypes.TYPE_THERMOSTAT) {
324                 ControlTemplate template = new TemperatureControlTemplate("temperature",
325                         ControlTemplate.getNoTemplateObject(),
326                         m.getNewMode(),
327                         TemperatureControlTemplate.MODE_OFF,
328                         TemperatureControlTemplate.FLAG_MODE_HEAT
329                         | TemperatureControlTemplate.FLAG_MODE_COOL
330                         | TemperatureControlTemplate.FLAG_MODE_OFF
331                         | TemperatureControlTemplate.FLAG_MODE_ECO);
332 
333                 builder.setControlTemplate(template);
334             }
335         } else if (action instanceof CommandAction) {
336             builder.setControlTemplate(new StatelessTemplate("stateless"));
337         }
338 
339         // Finally build and send the default OK status
340         Control updatedControl = builder.build();
341         mControlsById.put(controlId, updatedControl);
342         mUpdatePublisher.onNext(updatedControl);
343         consumer.accept(ControlAction.RESPONSE_OK);
344     }
345 
controlToBuilder(Control c)346     private Control.StatefulBuilder controlToBuilder(Control c) {
347         return new Control.StatefulBuilder(c.getControlId(), c.getAppIntent())
348             .setTitle(c.getTitle())
349             .setSubtitle(c.getSubtitle())
350             .setStructure(c.getStructure())
351             .setDeviceType(c.getDeviceType())
352             .setZone(c.getZone())
353             .setCustomIcon(c.getCustomIcon())
354             .setCustomColor(c.getCustomColor())
355             .setStatus(c.getStatus())
356             .setStatusText(c.getStatusText())
357             .setAuthRequired(c.isAuthRequired());
358     }
359 }
360