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 com.google.android.chre.test.chqts;
17 
18 import android.app.Instrumentation;
19 import android.bluetooth.BluetoothAdapter;
20 import android.content.BroadcastReceiver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.hardware.location.ContextHubClient;
25 import android.hardware.location.ContextHubClientCallback;
26 import android.hardware.location.ContextHubInfo;
27 import android.hardware.location.ContextHubManager;
28 import android.hardware.location.ContextHubTransaction;
29 import android.hardware.location.NanoAppBinary;
30 import android.hardware.location.NanoAppMessage;
31 import android.util.Log;
32 
33 import com.google.android.utils.chre.ChreTestUtil;
34 import com.google.android.utils.chre.SettingsUtil;
35 
36 import org.junit.Assert;
37 
38 import java.nio.ByteBuffer;
39 import java.nio.ByteOrder;
40 import java.nio.charset.Charset;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.HashSet;
44 import java.util.List;
45 import java.util.Set;
46 import java.util.concurrent.CountDownLatch;
47 import java.util.concurrent.TimeUnit;
48 import java.util.concurrent.atomic.AtomicReference;
49 
50 /**
51  * A class that can execute the CHQTS "general" tests. Nanoapps using this "general" test framework
52  * have the name "general_test".
53  *
54  * A test successfully passes in one of two ways:
55  * - MessageType.SUCCESS received from the Nanoapp by this infrastructure.
56  * - A call to ContextHubGeneralTestExecutor.pass() by the test code.
57  *
58  * NOTE: A test must extend this class and define the handleNanoappMessage() function to handle
59  * specific messages for the test.
60  *
61  * TODO: Refactor this class to be able to be invoked for < P builds.
62  */
63 public abstract class ContextHubGeneralTestExecutor extends ContextHubClientCallback {
64     public static final String TAG = "ContextHubGeneralTestExecutor";
65 
66     private final List<GeneralTestNanoApp> mGeneralTestNanoAppList;
67 
68     private final Set<Long> mNanoAppIdSet;
69 
70     private ContextHubClient mContextHubClient;
71 
72     private final ContextHubManager mContextHubManager;
73 
74     private final ContextHubInfo mContextHubInfo;
75 
76     private CountDownLatch mCountDownLatch;
77 
78     private boolean mInitialized = false;
79 
80     private AtomicReference<String> mErrorString = new AtomicReference<>(null);
81 
82     private long mThreadId;
83 
84     private final Instrumentation mInstrumentation =
85             androidx.test.platform.app.InstrumentationRegistry.getInstrumentation();
86 
87     private final Context mContext = mInstrumentation.getTargetContext();
88 
89     private final SettingsUtil mSettingsUtil;
90 
91     private boolean mInitialBluetoothEnabled = false;
92 
93     public static class BluetoothUpdateListener {
94         public CountDownLatch mBluetoothLatch = new CountDownLatch(1);
95 
96         public BroadcastReceiver mBluetoothUpdateReceiver = new BroadcastReceiver() {
97             @Override
98             public void onReceive(Context context, Intent intent) {
99                 if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
100                     mBluetoothLatch.countDown();
101                 }
102             }
103         };
104     }
105 
106     /**
107      * A container class to describe a general_test nanoapp.
108      */
109     public static class GeneralTestNanoApp {
110         private final NanoAppBinary mNanoAppBinary;
111         private final ContextHubTestConstants.TestNames mTestName;
112 
113         // Set to false if the nanoapp should not be loaded at init. An example of why this may be
114         // needed are for nanoapps that are loaded in the middle of the test execution, but still
115         // needs to be included in this test executor (e.g. deliver messages from it).
116         private final boolean mLoadAtInit;
117 
118         // Set to false if the nanoapp should not send a start message at init. An example of where
119         // this is not needed is for test nanoapps that use the general_test protocol, but do not
120         // require a start message (e.g. starts on load like the busy_startup nanoapp).
121         private final boolean mSendStartMessage;
122 
GeneralTestNanoApp(NanoAppBinary nanoAppBinary, ContextHubTestConstants.TestNames testName)123         public GeneralTestNanoApp(NanoAppBinary nanoAppBinary,
124                 ContextHubTestConstants.TestNames testName) {
125             mTestName = testName;
126             mNanoAppBinary = nanoAppBinary;
127             mLoadAtInit = true;
128             mSendStartMessage = true;
129         }
130 
GeneralTestNanoApp(NanoAppBinary nanoAppBinary, ContextHubTestConstants.TestNames testName, boolean loadAtInit)131         public GeneralTestNanoApp(NanoAppBinary nanoAppBinary,
132                 ContextHubTestConstants.TestNames testName, boolean loadAtInit) {
133             mTestName = testName;
134             mNanoAppBinary = nanoAppBinary;
135             mLoadAtInit = loadAtInit;
136             mSendStartMessage = true;
137         }
138 
GeneralTestNanoApp(NanoAppBinary nanoAppBinary, ContextHubTestConstants.TestNames testName, boolean loadAtInit, boolean sendStartMessage)139         public GeneralTestNanoApp(NanoAppBinary nanoAppBinary,
140                 ContextHubTestConstants.TestNames testName,
141                 boolean loadAtInit, boolean sendStartMessage) {
142             mTestName = testName;
143             mNanoAppBinary = nanoAppBinary;
144             mLoadAtInit = loadAtInit;
145             mSendStartMessage = sendStartMessage;
146         }
147 
getNanoAppBinary()148         public NanoAppBinary getNanoAppBinary() {
149             return mNanoAppBinary;
150         }
151 
getTestName()152         public ContextHubTestConstants.TestNames getTestName() {
153             return mTestName;
154         }
155 
loadAtInit()156         public boolean loadAtInit() {
157             return mLoadAtInit;
158         }
159 
sendStartMessage()160         public boolean sendStartMessage() {
161             return mSendStartMessage;
162         }
163     }
164 
165     /**
166      * Note that this constructor accepts multiple general_test nanoapps to test.
167      */
ContextHubGeneralTestExecutor(ContextHubManager manager, ContextHubInfo info, GeneralTestNanoApp... tests)168     public ContextHubGeneralTestExecutor(ContextHubManager manager, ContextHubInfo info,
169             GeneralTestNanoApp... tests) {
170         mContextHubManager = manager;
171         mContextHubInfo = info;
172         mGeneralTestNanoAppList = new ArrayList<>(Arrays.asList(tests));
173         mNanoAppIdSet = new HashSet<>();
174         for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
175             mNanoAppIdSet.add(test.getNanoAppBinary().getNanoAppId());
176         }
177         mSettingsUtil = new SettingsUtil(mContext);
178     }
179 
180     @Override
onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message)181     public void onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message) {
182         if (mNanoAppIdSet.contains(message.getNanoAppId())) {
183             NanoAppMessage realMessage = hackMessageFromNanoApp(message);
184 
185             int messageType = realMessage.getMessageType();
186             ContextHubTestConstants.MessageType messageEnum =
187                     ContextHubTestConstants.MessageType.fromInt(messageType, "");
188             byte[] data = realMessage.getMessageBody();
189 
190             switch (messageEnum) {
191                 case INVALID_MESSAGE_TYPE:  // fall-through
192                 case FAILURE:  // fall-through
193                 case INTERNAL_FAILURE:
194                     // These are univeral failure conditions for all tests.
195                     // If they have data, it's expected to be an ASCII string.
196                     String errorString = new String(data, Charset.forName("US-ASCII"));
197                     fail(errorString);
198                     break;
199 
200                 case SKIPPED:
201                     // TODO: Use junit Assume
202                     String reason = new String(data, Charset.forName("US-ASCII"));
203                     Log.w(TAG, "SKIPPED " + ":" + reason);
204                     pass();
205                     break;
206 
207                 case SUCCESS:
208                     // This is a universal success for the test.  We ignore
209                     // 'data'.
210                     pass();
211                     break;
212 
213                 default:
214                     handleMessageFromNanoApp(message.getNanoAppId(), messageEnum, data);
215             }
216         }
217     }
218 
219     /**
220      * Should be invoked before run() is invoked to set up the test, e.g. in a @Before method.
221      */
init()222     public void init() {
223         Assert.assertFalse("init() must not be invoked when already initialized", mInitialized);
224 
225         mInitialized = true;
226 
227         // Initialize the CountDownLatch before run() since some nanoapps will start on load.
228         mCountDownLatch = new CountDownLatch(1);
229 
230         mContextHubClient = mContextHubManager.createClient(mContextHubInfo, this);
231 
232         for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
233             if (test.getTestName() == ContextHubTestConstants.TestNames.BASIC_BLE_TEST) {
234                 handleBleTestSetup();
235             }
236             if (test.loadAtInit()) {
237                 ChreTestUtil.loadNanoAppAssertSuccess(mContextHubManager, mContextHubInfo,
238                         test.getNanoAppBinary());
239             }
240         }
241 
242         mErrorString.set(null);
243     }
244 
handleBleTestSetup()245     private void handleBleTestSetup() {
246         mInitialBluetoothEnabled = mSettingsUtil.isBluetoothEnabled();
247         Log.i(TAG, "Initial bluetooth setting enabled: " + mInitialBluetoothEnabled);
248         if (mInitialBluetoothEnabled) {
249             return;
250         }
251         BluetoothUpdateListener bluetoothUpdateListener = new BluetoothUpdateListener();
252         IntentFilter filter = new IntentFilter();
253         filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
254         mContext.registerReceiver(bluetoothUpdateListener.mBluetoothUpdateReceiver, filter);
255         mSettingsUtil.setBluetooth(true /* enable */);
256         try {
257             bluetoothUpdateListener.mBluetoothLatch.await(10, TimeUnit.SECONDS);
258             Assert.assertTrue(mSettingsUtil.isBluetoothEnabled());
259             Log.i(TAG, "Bluetooth enabled successfully");
260             // Wait a few seconds to ensure setting is propagated to CHRE path
261             // TODO(b/302018530): Remove Thread.sleep calls for CHRE settings propagation
262             Thread.sleep(2000);
263         } catch (InterruptedException e) {
264             Assert.fail(e.getMessage());
265         }
266     }
267 
268     /**
269      * Run the test.
270      */
run(long timeoutSeconds)271     public void run(long timeoutSeconds) throws InterruptedException {
272         mThreadId = Thread.currentThread().getId();
273 
274         for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
275             if (test.loadAtInit() && test.sendStartMessage()) {
276                 sendMessageToNanoAppOrFail(test.getNanoAppBinary().getNanoAppId(),
277                         test.getTestName().asInt(), new byte[0] /* data */);
278             }
279         }
280 
281         boolean success = mCountDownLatch.await(timeoutSeconds, TimeUnit.SECONDS);
282         Assert.assertTrue("Test timed out", success);
283     }
284 
285     /**
286      * Invoke to indicate that the test has passed.
287      */
pass()288     public void pass() {
289         mCountDownLatch.countDown();
290     }
291 
292     /**
293      * Cleans up the test, should be invoked in e.g. @After method.
294      */
deinit()295     public void deinit() {
296         Assert.assertTrue("deinit() must be invoked after init()", mInitialized);
297 
298         // TODO: If the nanoapp aborted (i.e. test failed), wait for CHRE reset or nanoapp abort
299         // callback, and otherwise assert unload success.
300         for (GeneralTestNanoApp test : mGeneralTestNanoAppList) {
301             if (test.getTestName() == ContextHubTestConstants.TestNames.BASIC_BLE_TEST
302                     && !mInitialBluetoothEnabled) {
303                 mSettingsUtil.setBluetooth(mInitialBluetoothEnabled);
304             }
305             ChreTestUtil.unloadNanoApp(mContextHubManager, mContextHubInfo,
306                     test.getNanoAppBinary().getNanoAppId());
307         }
308 
309         mContextHubClient.close();
310         mContextHubClient = null;
311 
312         mInitialized = false;
313 
314         if (mErrorString.get() != null) {
315             Assert.fail(mErrorString.get());
316         }
317     }
318 
319     /**
320      * Sends a message to the test nanoapp.
321      *
322      * @param nanoAppId The 64-bit ID of the nanoapp to send the message to.
323      * @param type      The message type.
324      * @param data      The message payload.
325      */
sendMessageToNanoAppOrFail(long nanoAppId, int type, byte[] data)326     protected void sendMessageToNanoAppOrFail(long nanoAppId, int type, byte[] data) {
327         NanoAppMessage message = NanoAppMessage.createMessageToNanoApp(
328                 nanoAppId, type, data);
329 
330         int result = mContextHubClient.sendMessageToNanoApp(hackMessageToNanoApp(message));
331         if (result != ContextHubTransaction.RESULT_SUCCESS) {
332             fail("Failed to send message: result = " + result);
333         }
334     }
335 
336     /**
337      * @param errorMessage The error message to display
338      */
fail(String errorMessage)339     protected void fail(String errorMessage) {
340         assertTrue(errorMessage, false /* condition */);
341     }
342 
343     /**
344      * Semantics the same as Assert.assertEquals.
345      */
assertEquals(String errorMessage, T expected, T actual)346     protected <T> void assertEquals(String errorMessage, T expected, T actual) {
347         if (Thread.currentThread().getId() == mThreadId) {
348             Assert.assertEquals(errorMessage, expected, actual);
349         } else if ((expected == null && actual != null) || (expected != null && !expected.equals(
350                 actual))) {
351             mErrorString.set(errorMessage + ": " + expected + " != " + actual);
352             mCountDownLatch.countDown();
353         }
354     }
355 
356     /**
357      * Semantics the same as Assert.assertTrue.
358      */
assertTrue(String errorMessage, boolean condition)359     protected void assertTrue(String errorMessage, boolean condition) {
360         if (Thread.currentThread().getId() == mThreadId) {
361             Assert.assertTrue(errorMessage, condition);
362         } else if (!condition) {
363             mErrorString.set(errorMessage);
364             mCountDownLatch.countDown();
365         }
366     }
367 
368     /**
369      * Semantics are the same as Assert.assertFalse.
370      */
assertFalse(String errorMessage, boolean condition)371     protected void assertFalse(String errorMessage, boolean condition) {
372         assertTrue(errorMessage, !condition);
373     }
374 
getContextHubManager()375     protected ContextHubManager getContextHubManager() {
376         return mContextHubManager;
377     }
378 
getContextHubInfo()379     protected ContextHubInfo getContextHubInfo() {
380         return mContextHubInfo;
381     }
382 
383     /**
384      * Handles a message specific for a test.
385      *
386      * @param nanoAppId The 64-bit ID of the nanoapp sending the message.
387      * @param type      The message type.
388      * @param data      The message body.
389      */
handleMessageFromNanoApp( long nanoAppId, ContextHubTestConstants.MessageType type, byte[] data)390     protected abstract void handleMessageFromNanoApp(
391             long nanoAppId, ContextHubTestConstants.MessageType type, byte[] data);
392 
393     // TODO: Remove this hack
hackMessageToNanoApp(NanoAppMessage message)394     protected NanoAppMessage hackMessageToNanoApp(NanoAppMessage message) {
395         // For NYC, we are not able to assume that the messageType correctly
396         // makes it to the nanoapp.  So we put it, in little endian, as the
397         // first four bytes of the message.
398         byte[] origData = message.getMessageBody();
399         ByteBuffer newData = ByteBuffer.allocate(4 + origData.length);
400         newData.order(ByteOrder.LITTLE_ENDIAN);
401         newData.putInt(message.getMessageType());
402         newData.put(origData);
403         return NanoAppMessage.createMessageToNanoApp(
404                 message.getNanoAppId(), message.getMessageType(), newData.array());
405     }
406 
407     // TODO: Remove this hack
hackMessageFromNanoApp(NanoAppMessage message)408     protected NanoAppMessage hackMessageFromNanoApp(NanoAppMessage message) {
409         // For now, our nanohub HAL and JNI code end up not sending across the
410         // message type of the user correctly.  So our testing protocol hacks
411         // around this by putting the message type in the first four bytes of
412         // the data payload, in little endian.
413         ByteBuffer origData = ByteBuffer.wrap(message.getMessageBody());
414         origData.order(ByteOrder.LITTLE_ENDIAN);
415         int newMessageType = origData.getInt();
416         // The new data is the remainder of this array (which could be empty).
417         byte[] newData = new byte[origData.remaining()];
418         origData.get(newData);
419         return NanoAppMessage.createMessageFromNanoApp(
420                 message.getNanoAppId(), newMessageType, newData,
421                 message.isBroadcastMessage());
422     }
423 }
424