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