• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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