1 /* 2 * Copyright (C) 2023 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 17 package android.car.cts.app; 18 19 import static android.app.NotificationManager.IMPORTANCE_DEFAULT; 20 21 import android.app.Notification; 22 import android.app.NotificationChannel; 23 import android.app.NotificationManager; 24 import android.app.Service; 25 import android.car.Car; 26 import android.car.hardware.power.CarPowerManager; 27 import android.car.test.mocks.JavaMockitoHelper; 28 import android.content.Intent; 29 import android.os.Bundle; 30 import android.os.IBinder; 31 import android.text.TextUtils; 32 import android.util.ArrayMap; 33 import android.util.Log; 34 35 import java.io.FileDescriptor; 36 import java.io.PrintWriter; 37 import java.io.StringWriter; 38 import java.util.ArrayList; 39 import java.util.List; 40 import java.util.Set; 41 import java.util.concurrent.CountDownLatch; 42 43 import javax.annotation.concurrent.GuardedBy; 44 45 /** 46 * To test car power: 47 * 48 * <pre class="prettyprint"> 49 * adb shell am start -n android.car.cts.app/.CarPowerTestService / 50 * --es power [action] 51 * action: 52 * set-listener,[with-completion|without-completion],[s2r|s2d] 53 * get-listener-states-results,[with-completion|without-completion], 54 * [s2r|s2d] 55 * clear-listener 56 * </pre> 57 */ 58 public final class CarPowerTestService extends Service { 59 private static final long WAIT_TIMEOUT_MS = 5_000; 60 private static final int RESULT_LOG_SIZE = 4096; 61 private static final String TAG = CarPowerTestService.class.getSimpleName(); 62 private static final String CMD_IDENTIFIER = "power"; 63 private static final String CMD_SET_LISTENER = "set-listener"; 64 private static final String CMD_GET_LISTENER_STATES_RESULTS = "get-listener-states-results"; 65 private static final String CMD_CLEAR_LISTENER = "clear-listener"; 66 private static final List<Integer> EXPECTED_STATES_S2R = List.of( 67 CarPowerManager.STATE_PRE_SHUTDOWN_PREPARE, 68 CarPowerManager.STATE_SHUTDOWN_PREPARE, 69 CarPowerManager.STATE_SUSPEND_ENTER, 70 CarPowerManager.STATE_POST_SUSPEND_ENTER 71 ); 72 private static final List<Integer> EXPECTED_STATES_S2D = List.of( 73 CarPowerManager.STATE_PRE_SHUTDOWN_PREPARE, 74 CarPowerManager.STATE_SHUTDOWN_PREPARE, 75 CarPowerManager.STATE_HIBERNATION_ENTER, 76 CarPowerManager.STATE_POST_HIBERNATION_ENTER 77 ); 78 private static final Set<Integer> FUTURE_ALLOWING_STATES = Set.of( 79 CarPowerManager.STATE_PRE_SHUTDOWN_PREPARE, 80 CarPowerManager.STATE_SHUTDOWN_PREPARE, 81 CarPowerManager.STATE_SHUTDOWN_ENTER, 82 CarPowerManager.STATE_SUSPEND_ENTER, 83 CarPowerManager.STATE_HIBERNATION_ENTER, 84 CarPowerManager.STATE_POST_SHUTDOWN_ENTER, 85 CarPowerManager.STATE_POST_SUSPEND_ENTER, 86 CarPowerManager.STATE_POST_HIBERNATION_ENTER 87 ); 88 89 // Foreground service requirements 90 private static final String NOTIFICATION_CHANNEL_ID = TAG; 91 private static final String NOTIFICATION_CHANNEL_NAME = TAG; 92 private final int mCarPowerTestServiceNotificationId = this.hashCode(); 93 94 private final Object mLock = new Object(); 95 96 private final StringWriter mResultBuf = new StringWriter(RESULT_LOG_SIZE); 97 98 private Car mCarApi; 99 @GuardedBy("mLock") 100 private WaitablePowerStateListener mListener = new WaitablePowerStateListener(0); 101 @GuardedBy("mLock") 102 private CarPowerManager mCarPowerManager; 103 104 @Override onBind(Intent intent)105 public IBinder onBind(Intent intent) { 106 return null; 107 } 108 initManagers(Car car, boolean ready)109 private void initManagers(Car car, boolean ready) { 110 synchronized (mLock) { 111 if (ready) { 112 mCarPowerManager = (CarPowerManager) car.getCarManager( 113 Car.POWER_SERVICE); 114 Log.i(TAG, "initManagers() completed"); 115 } else { 116 mCarPowerManager = null; 117 Log.wtf(TAG, "initManagers() set to be null"); 118 } 119 } 120 } 121 initCarApi()122 private void initCarApi() { 123 if (mCarApi != null && mCarApi.isConnected()) { 124 mCarApi.disconnect(); 125 } 126 mCarApi = Car.createCar(/* context= */ this, /* handler= */ null, 127 /* waitTimeoutMs= */ Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 128 /* statusChangeListener= */ (Car car, boolean ready) -> { 129 initManagers(car, ready); 130 }); 131 } 132 133 @Override onCreate()134 public void onCreate() { 135 Log.i(TAG, "onCreate"); 136 super.onCreate(); 137 initCarApi(); 138 } 139 140 // Make CarPowerTestService run in the foreground so that it won't be killed during the test startForeground()141 void startForeground() { 142 NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, 143 NOTIFICATION_CHANNEL_NAME, IMPORTANCE_DEFAULT); 144 NotificationManager manager = getSystemService(NotificationManager.class); 145 manager.createNotificationChannel(notificationChannel); 146 147 Notification notification = 148 new Notification.Builder(this, NOTIFICATION_CHANNEL_ID) 149 .setSmallIcon(android.R.drawable.checkbox_on_background) 150 .setContentTitle(TAG) 151 .setContentText(TAG) 152 .setOngoing(true) 153 .build(); 154 startForeground(mCarPowerTestServiceNotificationId, notification); 155 } 156 157 @Override onStartCommand(Intent intent, int flags, int startId)158 public int onStartCommand(Intent intent, int flags, int startId) { 159 Log.i(TAG, "onStartCommand"); 160 super.onStartCommand(intent, flags, startId); 161 Bundle extras = intent.getExtras(); 162 if (extras == null) { 163 Log.i(TAG, "onStartCommand(): empty extras"); 164 return START_NOT_STICKY; 165 } 166 167 try { 168 parseCommandAndExecute(extras); 169 } catch (Exception e) { 170 Log.e(TAG, "onStartCommand(): failed to handle cmd", e); 171 } 172 173 startForeground(); 174 175 return START_NOT_STICKY; 176 } 177 178 @Override onDestroy()179 public void onDestroy() { 180 Log.i(TAG, "onDestroy"); 181 if (mCarApi != null) { 182 mCarApi.disconnect(); 183 } 184 super.onDestroy(); 185 } 186 187 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)188 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 189 Log.i(TAG, "Dumping CarPowerTestService"); 190 writer.println("*CarPowerTestService*"); 191 writer.printf("mResultBuf: %s\n", mResultBuf); 192 synchronized (mLock) { 193 writer.printf("mListener set: %b\n", mListener != null); 194 } 195 } 196 197 @GuardedBy("mLock") setListenerWithoutCompletionLocked(int expectedStatesSize)198 private void setListenerWithoutCompletionLocked(int expectedStatesSize) { 199 WaitablePowerStateListenerWithoutCompletion listener = 200 new WaitablePowerStateListenerWithoutCompletion(expectedStatesSize); 201 mListener = listener; 202 } 203 204 @GuardedBy("mLock") setListenerWithCompletionLocked(int expectedStatesSize)205 private void setListenerWithCompletionLocked(int expectedStatesSize) { 206 WaitablePowerStateListenerWithCompletion listener = 207 new WaitablePowerStateListenerWithCompletion( 208 expectedStatesSize, FUTURE_ALLOWING_STATES); 209 mListener = listener; 210 } 211 listenerStatesMatchExpected(WaitablePowerStateListener listener, List<Integer> expectedStates)212 private boolean listenerStatesMatchExpected(WaitablePowerStateListener listener, 213 List<Integer> expectedStates) throws InterruptedException { 214 List<Integer> observedStates = listener.await(); 215 Log.i(TAG, "observedStates: \n" + observedStates); 216 return observedStates.equals(expectedStates); 217 } 218 isListenerWithCompletion(String completionType)219 private boolean isListenerWithCompletion(String completionType) throws 220 IllegalArgumentException { 221 if (completionType.equals("with-completion")) { 222 return true; 223 } else if (completionType.equals("without-completion")) { 224 return false; 225 } 226 throw new IllegalArgumentException("Completion type parameter must be 'with-completion' or " 227 + "'without-completion'"); 228 } 229 getListenerExpectedStates(String suspendType)230 private List<Integer> getListenerExpectedStates(String suspendType) throws 231 IllegalArgumentException { 232 if (suspendType.equals("s2r")) { 233 return EXPECTED_STATES_S2R; 234 } else if (suspendType.equals("s2d")) { 235 return EXPECTED_STATES_S2D; 236 } 237 throw new IllegalArgumentException("Suspend type parameter must be 's2r' or 's2d'"); 238 } 239 parseCommandAndExecute(Bundle extras)240 private void parseCommandAndExecute(Bundle extras) { 241 String commandString = extras.getString(CMD_IDENTIFIER); 242 if (TextUtils.isEmpty(commandString)) { 243 Log.i(TAG, "empty power test command"); 244 return; 245 } 246 Log.i(TAG, "parseCommandAndExecute with: " + commandString); 247 248 String[] tokens = commandString.split(","); 249 switch(tokens[0]) { 250 case CMD_SET_LISTENER: 251 if (tokens.length != 3) { 252 Log.i(TAG, "incorrect set-listener command format: " + commandString 253 + ", should be set-listener,[with-completion|without-completion]," 254 + "[s2r|s2d]"); 255 break; 256 } 257 258 String completionType = tokens[1]; 259 Log.i(TAG, "Set listener command completion type: " + completionType); 260 boolean withCompletion; 261 try { 262 withCompletion = isListenerWithCompletion(completionType); 263 } catch (IllegalArgumentException e) { 264 Log.i(TAG, e.getMessage()); 265 break; 266 } 267 268 String suspendType = tokens[2]; 269 Log.i(TAG, "Set listener command suspend type: " + suspendType); 270 int expectedStatesSize; 271 try { 272 expectedStatesSize = getListenerExpectedStates(suspendType).size(); 273 } catch (IllegalArgumentException e) { 274 Log.i(TAG, e.getMessage()); 275 break; 276 } 277 278 synchronized (mLock) { 279 if (withCompletion) { 280 setListenerWithCompletionLocked(expectedStatesSize); 281 } else { 282 setListenerWithoutCompletionLocked(expectedStatesSize); 283 } 284 } 285 Log.i(TAG, "Listener set"); 286 break; 287 case CMD_GET_LISTENER_STATES_RESULTS: 288 if (tokens.length != 3) { 289 Log.i(TAG, "incorrect get-listener-states-results command format: " 290 + commandString + ", should be get-listener-states-results," 291 + "[with-completion|without-completion],[s2r|s2d]"); 292 break; 293 } 294 295 WaitablePowerStateListener listener; 296 synchronized (mLock) { 297 if (mListener == null) { 298 Log.i(TAG, "There is no listener registered"); 299 break; 300 } 301 listener = mListener; 302 } 303 304 completionType = tokens[1]; 305 Log.i(TAG, "Get listener command completion type: " + completionType); 306 try { 307 withCompletion = isListenerWithCompletion(completionType); 308 } catch (IllegalArgumentException e) { 309 Log.i(TAG, e.getMessage()); 310 break; 311 } 312 313 suspendType = tokens[2]; 314 Log.i(TAG, "Get listener command suspend type: " + suspendType); 315 List<Integer> expectedStates; 316 try { 317 expectedStates = getListenerExpectedStates(suspendType); 318 } catch (IllegalArgumentException e) { 319 Log.i(TAG, e.getMessage()); 320 break; 321 } 322 Log.i(TAG, "expectedStates: " + expectedStates); 323 324 try { 325 boolean statesMatchExpected = listenerStatesMatchExpected(listener, 326 expectedStates); 327 if (withCompletion) { 328 WaitablePowerStateListenerWithCompletion listenerWithCompletion = 329 ((WaitablePowerStateListenerWithCompletion) listener); 330 boolean futureIsValid = 331 listenerWithCompletion.completablePowerStateChangeFutureIsValid(); 332 statesMatchExpected = statesMatchExpected && futureIsValid; 333 } 334 Log.i(TAG, "statesMatchExpected: " + statesMatchExpected); 335 mResultBuf.write(String.valueOf(statesMatchExpected)); 336 } catch (InterruptedException e) { 337 Log.i(TAG, "Getting listener states timed out"); 338 mResultBuf.write("false"); 339 break; 340 } 341 break; 342 case CMD_CLEAR_LISTENER: 343 synchronized (mLock) { 344 mCarPowerManager.clearListener(); 345 mListener = null; 346 } 347 Log.i(TAG, "Listener cleared"); 348 break; 349 default: 350 throw new IllegalArgumentException("invalid power test command: " + commandString); 351 } 352 } 353 354 private class WaitablePowerStateListener { 355 private final int mInitialCount; 356 protected final CountDownLatch mLatch; 357 protected final CarPowerManager mPowerManager; 358 protected List<Integer> mReceivedStates = new ArrayList<Integer>(); 359 WaitablePowerStateListener(int initialCount)360 WaitablePowerStateListener(int initialCount) { 361 mLatch = new CountDownLatch(initialCount); 362 mInitialCount = initialCount; 363 synchronized (mLock) { 364 mPowerManager = mCarPowerManager; 365 } 366 } 367 await()368 List<Integer> await() throws InterruptedException { 369 JavaMockitoHelper.await(mLatch, WAIT_TIMEOUT_MS); 370 return mReceivedStates.subList(0, mInitialCount); 371 } 372 } 373 374 private final class WaitablePowerStateListenerWithoutCompletion extends 375 WaitablePowerStateListener{ WaitablePowerStateListenerWithoutCompletion(int initialCount)376 WaitablePowerStateListenerWithoutCompletion(int initialCount) { 377 super(initialCount); 378 mPowerManager.setListener(getMainExecutor(), 379 (state) -> { 380 mReceivedStates.add(state); 381 mLatch.countDown(); 382 Log.i(TAG, "Listener without completion observed state: " + state 383 + ", received states: " + mReceivedStates + ", mLatch count:" 384 + mLatch.getCount()); 385 }); 386 Log.i(TAG, "Listener without completion set"); 387 } 388 } 389 390 private final class WaitablePowerStateListenerWithCompletion extends 391 WaitablePowerStateListener { 392 private final ArrayMap<Integer, String> mInvalidFutureMap = new ArrayMap<>(); 393 private final Set<Integer> mFutureAllowingStates; 394 WaitablePowerStateListenerWithCompletion(int initialCount, Set<Integer> futureAllowingStates)395 WaitablePowerStateListenerWithCompletion(int initialCount, 396 Set<Integer> futureAllowingStates) { 397 super(initialCount); 398 mFutureAllowingStates = futureAllowingStates; 399 mPowerManager.setListenerWithCompletion(getMainExecutor(), 400 (state, future) -> { 401 mReceivedStates.add(state); 402 if (mFutureAllowingStates.contains(state)) { 403 if (future == null) { 404 mInvalidFutureMap.put(state, "CompletablePowerStateChangeFuture for" 405 + " state(" + state + ") must not be null"); 406 } else { 407 future.complete(); 408 } 409 } else { 410 if (future != null) { 411 mInvalidFutureMap.put(state, "CompletablePowerStateChangeFuture for" 412 + " state(" + state + ") must be null"); 413 } 414 } 415 mLatch.countDown(); 416 Log.i(TAG, "Listener with completion observed state: " + state 417 + ", received states: " + mReceivedStates + ", mLatch count:" 418 + mLatch.getCount()); 419 }); 420 Log.i(TAG, "Listener with completion set"); 421 } 422 completablePowerStateChangeFutureIsValid()423 boolean completablePowerStateChangeFutureIsValid() { 424 if (!mInvalidFutureMap.isEmpty()) { 425 Log.i(TAG, "Wrong CompletablePowerStateChangeFuture(s) is(are) passed to the " 426 + "listener: " + mInvalidFutureMap); 427 return false; 428 } 429 return true; 430 } 431 } 432 } 433