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