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, new Intent(),
69             PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
70         mIcon = Icon.createWithResource(mContext, R.drawable.ic_device_unknown);
71         mColorStateList = mContext.getResources().getColorStateList(R.color.custom_mower, null);
72 
73         mAllControls.add(buildLight(false /* isOn */, 0.0f /* intensity */));
74         mAllControls.add(buildLock(false /* isLocked */));
75         mAllControls.add(buildRoutine());
76         mAllControls.add(buildThermostat(TemperatureControlTemplate.MODE_OFF));
77         mAllControls.add(buildMower(false /* isStarted */));
78         mAllControls.add(buildSwitch(false /* isOn */));
79         mAllControls.add(buildGate(false /* isLocked */));
80         mAllControls.add(buildCamera(true /* isActive */));
81 
82         for (Control c : mAllControls) {
83             mControlsById.put(c.getControlId(), c);
84         }
85     }
86 
buildLight(boolean isOn, float intensity)87     public Control buildLight(boolean isOn, float intensity) {
88         RangeTemplate rt = new RangeTemplate("range", 0.0f, 100.0f, intensity, 1.0f, null);
89         ControlTemplate template =
90                 new ToggleRangeTemplate("toggleRange", isOn, isOn ? "On" : "Off", rt);
91         return new Control.StatefulBuilder("light", mPendingIntent)
92             .setTitle("Light Title")
93             .setSubtitle("Light Subtitle")
94             .setStatus(Control.STATUS_OK)
95             .setStatusText(isOn ? "On" : "Off")
96             .setDeviceType(DeviceTypes.TYPE_LIGHT)
97             .setStructure("Home")
98             .setControlTemplate(template)
99             .build();
100     }
101 
buildSwitch(boolean isOn)102     public Control buildSwitch(boolean isOn) {
103         ControlButton button = new ControlButton(isOn, isOn ? "On" : "Off");
104         ControlTemplate template = new ToggleTemplate("toggle", button);
105         return new Control.StatefulBuilder("switch", mPendingIntent)
106             .setTitle("Switch Title")
107             .setSubtitle("Switch Subtitle")
108             .setStatus(Control.STATUS_OK)
109             .setStatusText(isOn ? "On" : "Off")
110             .setDeviceType(DeviceTypes.TYPE_SWITCH)
111             .setStructure("Home")
112             .setControlTemplate(template)
113             .build();
114     }
115 
116 
buildMower(boolean isStarted)117     public Control buildMower(boolean isStarted) {
118         String desc = isStarted ? "Started" : "Stopped";
119         ControlButton button = new ControlButton(isStarted, desc);
120         ControlTemplate template = new ToggleTemplate("toggle", button);
121         return new Control.StatefulBuilder("mower", mPendingIntent)
122             .setTitle("Mower Title")
123             .setSubtitle("Mower Subtitle")
124             .setStatus(Control.STATUS_OK)
125             .setStatusText(desc)
126             .setDeviceType(DeviceTypes.TYPE_MOWER)
127             .setStructure("Vacation")
128             .setZone("Outside")
129             .setControlTemplate(template)
130             .setCustomIcon(mIcon)
131             .setCustomColor(mColorStateList)
132             .build();
133     }
134 
buildLock(boolean isLocked)135     public Control buildLock(boolean isLocked) {
136         String desc = isLocked ? "Locked" : "Unlocked";
137         ControlButton button = new ControlButton(isLocked, desc);
138         ControlTemplate template = new ToggleTemplate("toggle", button);
139         return new Control.StatefulBuilder("lock", mPendingIntent)
140             .setTitle("Lock Title")
141             .setSubtitle("Lock Subtitle")
142             .setStatus(Control.STATUS_OK)
143             .setStatusText(desc)
144             .setDeviceType(DeviceTypes.TYPE_LOCK)
145             .setControlTemplate(template)
146             .build();
147     }
148 
buildGate(boolean isLocked)149     public Control buildGate(boolean isLocked) {
150         String desc = isLocked ? "Locked" : "Unlocked";
151         ControlButton button = new ControlButton(isLocked, desc);
152         ControlTemplate template = new ToggleTemplate("toggle", button);
153         return new Control.StatefulBuilder("gate", mPendingIntent)
154             .setTitle("Gate Title")
155             .setSubtitle("Gate Subtitle")
156             .setStatus(Control.STATUS_OK)
157             .setStatusText(desc)
158             .setDeviceType(DeviceTypes.TYPE_GATE)
159             .setControlTemplate(template)
160             .setStructure("Other home")
161             .build();
162     }
163 
buildThermostat(int mode)164     public Control buildThermostat(int mode) {
165         ControlTemplate template = new TemperatureControlTemplate("temperature",
166                     ControlTemplate.getNoTemplateObject(),
167                     mode,
168                     TemperatureControlTemplate.MODE_OFF,
169                     TemperatureControlTemplate.FLAG_MODE_HEAT
170                     | TemperatureControlTemplate.FLAG_MODE_COOL
171                     | TemperatureControlTemplate.FLAG_MODE_OFF
172                     | TemperatureControlTemplate.FLAG_MODE_ECO);
173 
174         return new Control.StatefulBuilder("thermostat", mPendingIntent)
175             .setTitle("Thermostat Title")
176             .setSubtitle("Thermostat Subtitle")
177             .setStatus(Control.STATUS_OK)
178             .setStatusText("Off")
179             .setDeviceType(DeviceTypes.TYPE_THERMOSTAT)
180             .setControlTemplate(template)
181             .build();
182     }
183 
buildRoutine()184     public Control buildRoutine() {
185         ControlTemplate template = new StatelessTemplate("stateless");
186         return new Control.StatefulBuilder("routine", mPendingIntent)
187             .setTitle("Routine Title")
188             .setSubtitle("Routine Subtitle")
189             .setStatus(Control.STATUS_OK)
190             .setStatusText("Good Morning")
191             .setDeviceType(DeviceTypes.TYPE_ROUTINE)
192             .setControlTemplate(template)
193             .build();
194     }
195 
buildCamera(boolean active)196     public Control buildCamera(boolean active) {
197         String description = active ? "Live" : "Not live";
198         ControlTemplate template = new ThumbnailTemplate("thumbnail", active, mIcon, description);
199         return new Control.StatefulBuilder("camera", mPendingIntent)
200                 .setTitle("Camera Title")
201                 .setTitle("Camera Subtitle")
202                 .setStatus(Control.STATUS_OK)
203                 .setStatusText(description)
204                 .setDeviceType(DeviceTypes.TYPE_CAMERA)
205                 .setControlTemplate(template)
206                 .build();
207     }
208 
209     @Override
createPublisherForAllAvailable()210     public Publisher<Control> createPublisherForAllAvailable() {
211         return new CtsControlsPublisher(mAllControls.stream()
212             .map(c -> new Control.StatelessBuilder(c).build())
213             .collect(Collectors.toList()));
214     }
215 
216     @Override
createPublisherForSuggested()217     public Publisher<Control> createPublisherForSuggested() {
218         return new CtsControlsPublisher(mAllControls.stream()
219             .map(c -> new Control.StatelessBuilder(c).build())
220             .collect(Collectors.toList()));
221     }
222 
223     @Override
createPublisherFor(List<String> controlIds)224     public Publisher<Control> createPublisherFor(List<String> controlIds) {
225         mUpdatePublisher = new CtsControlsPublisher(null);
226 
227         for (String id : controlIds) {
228             Control control = mControlsById.get(id);
229             if (control == null) continue;
230 
231             mUpdatePublisher.onNext(control);
232         }
233 
234         return mUpdatePublisher;
235     }
236 
237     @Override
performControlAction(String controlId, ControlAction action, Consumer<Integer> consumer)238     public void performControlAction(String controlId, ControlAction action,
239             Consumer<Integer> consumer) {
240         Control c = mControlsById.get(controlId);
241         if (c == null) return;
242 
243         // all values are hardcoded for this test
244         assertEquals(action.getTemplateId(), "action");
245         assertNotEquals(action, ControlAction.getErrorAction());
246 
247         Control.StatefulBuilder builder = controlToBuilder(c);
248 
249         // Modify the builder in order to update the Control to have predefined, verifiable behavior
250         if (action instanceof BooleanAction) {
251             BooleanAction b = (BooleanAction) action;
252 
253             if (c.getDeviceType() == DeviceTypes.TYPE_LIGHT) {
254                 RangeTemplate rt = new RangeTemplate("range",
255                         0.0f /* minValue */,
256                         100.0f /* maxValue */,
257                         50.0f /* currentValue */,
258                         1.0f /* step */, null);
259                 String desc = b.getNewState() ? "On" : "Off";
260 
261                 builder.setStatusText(desc);
262                 builder.setControlTemplate(new ToggleRangeTemplate("toggleRange", b.getNewState(),
263                         desc, rt));
264             } else if (c.getDeviceType() == DeviceTypes.TYPE_ROUTINE) {
265                 builder.setStatusText("Running");
266                 builder.setControlTemplate(new StatelessTemplate("stateless"));
267             } else if (c.getDeviceType() == DeviceTypes.TYPE_SWITCH) {
268                 String desc = b.getNewState() ? "On" : "Off";
269                 builder.setStatusText(desc);
270                 ControlButton button = new ControlButton(b.getNewState(), desc);
271                 builder.setControlTemplate(new ToggleTemplate("toggle", button));
272             } else if (c.getDeviceType() == DeviceTypes.TYPE_LOCK) {
273                 String value = action.getChallengeValue();
274                 if (value != null && value.equals("1234")) {
275                     String desc = b.getNewState() ? "Locked" : "Unlocked";
276                     ControlButton button = new ControlButton(b.getNewState(), desc);
277                     builder.setStatusText(desc);
278                     builder.setControlTemplate(new ToggleTemplate("toggle", button));
279                 } else {
280                     consumer.accept(ControlAction.RESPONSE_CHALLENGE_PIN);
281                     return;
282                 }
283             } else if (c.getDeviceType() == DeviceTypes.TYPE_GATE) {
284                 String value = action.getChallengeValue();
285                 if (value != null && value.equals("abc123")) {
286                     String desc = b.getNewState() ? "Locked" : "Unlocked";
287                     ControlButton button = new ControlButton(b.getNewState(), desc);
288                     builder.setStatusText(desc);
289                     builder.setControlTemplate(new ToggleTemplate("toggle", button));
290                 } else {
291                     consumer.accept(ControlAction.RESPONSE_CHALLENGE_PASSPHRASE);
292                     return;
293                 }
294             } else if (c.getDeviceType() == DeviceTypes.TYPE_MOWER) {
295                 String value = action.getChallengeValue();
296                 if (value != null && value.equals("true")) {
297                     String desc = b.getNewState() ? "Started" : "Stopped";
298                     ControlButton button = new ControlButton(b.getNewState(), desc);
299                     builder.setStatusText(desc);
300                     builder.setControlTemplate(new ToggleTemplate("toggle", button));
301                 } else {
302                     consumer.accept(ControlAction.RESPONSE_CHALLENGE_ACK);
303                     return;
304                 }
305             }
306         } else if (action instanceof FloatAction) {
307             FloatAction f = (FloatAction) action;
308             if (c.getDeviceType() == DeviceTypes.TYPE_LIGHT) {
309                 RangeTemplate rt = new RangeTemplate("range", 0.0f, 100.0f, f.getNewValue(), 1.0f,
310                         null);
311 
312                 ToggleRangeTemplate trt = (ToggleRangeTemplate) c.getControlTemplate();
313                 String desc = trt.getActionDescription().toString();
314                 boolean state = trt.isChecked();
315 
316                 builder.setStatusText(desc);
317                 builder.setControlTemplate(new ToggleRangeTemplate("toggleRange", state, desc, rt));
318             }
319         } else if (action instanceof ModeAction) {
320             ModeAction m = (ModeAction) action;
321             if (c.getDeviceType() == DeviceTypes.TYPE_THERMOSTAT) {
322                 ControlTemplate template = new TemperatureControlTemplate("temperature",
323                         ControlTemplate.getNoTemplateObject(),
324                         m.getNewMode(),
325                         TemperatureControlTemplate.MODE_OFF,
326                         TemperatureControlTemplate.FLAG_MODE_HEAT
327                         | TemperatureControlTemplate.FLAG_MODE_COOL
328                         | TemperatureControlTemplate.FLAG_MODE_OFF
329                         | TemperatureControlTemplate.FLAG_MODE_ECO);
330 
331                 builder.setControlTemplate(template);
332             }
333         } else if (action instanceof CommandAction) {
334             builder.setControlTemplate(new StatelessTemplate("stateless"));
335         }
336 
337         // Finally build and send the default OK status
338         Control updatedControl = builder.build();
339         mControlsById.put(controlId, updatedControl);
340         mUpdatePublisher.onNext(updatedControl);
341         consumer.accept(ControlAction.RESPONSE_OK);
342     }
343 
controlToBuilder(Control c)344     private Control.StatefulBuilder controlToBuilder(Control c) {
345         return new Control.StatefulBuilder(c.getControlId(), c.getAppIntent())
346             .setTitle(c.getTitle())
347             .setSubtitle(c.getSubtitle())
348             .setStructure(c.getStructure())
349             .setDeviceType(c.getDeviceType())
350             .setZone(c.getZone())
351             .setCustomIcon(c.getCustomIcon())
352             .setCustomColor(c.getCustomColor())
353             .setStatus(c.getStatus())
354             .setStatusText(c.getStatusText());
355     }
356 }
357