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 17 package com.android.server.uwb; 18 19 import static android.uwb.RangingSession.Callback.REASON_LOCAL_REQUEST; 20 21 import static com.android.server.uwb.UwbShellCommand.DEFAULT_CCC_OPEN_RANGING_PARAMS; 22 import static com.android.server.uwb.UwbShellCommand.DEFAULT_FIRA_OPEN_SESSION_PARAMS; 23 import static com.android.server.uwb.UwbShellCommand.DEFAULT_RADAR_OPEN_SESSION_PARAMS; 24 25 import static com.google.common.truth.Truth.assertThat; 26 import static com.google.uwb.support.fira.FiraParams.RangeDataNtfConfigCapabilityFlag.HAS_RANGE_DATA_NTF_CONFIG_DISABLE; 27 import static com.google.uwb.support.fira.FiraParams.RangeDataNtfConfigCapabilityFlag.HAS_RANGE_DATA_NTF_CONFIG_ENABLE; 28 29 import static org.mockito.ArgumentMatchers.anyInt; 30 import static org.mockito.Mockito.any; 31 import static org.mockito.Mockito.clearInvocations; 32 import static org.mockito.Mockito.doAnswer; 33 import static org.mockito.Mockito.eq; 34 import static org.mockito.Mockito.mock; 35 import static org.mockito.Mockito.never; 36 import static org.mockito.Mockito.validateMockitoUsage; 37 import static org.mockito.Mockito.verify; 38 import static org.mockito.Mockito.when; 39 40 import android.annotation.NonNull; 41 import android.annotation.Nullable; 42 import android.content.AttributionSource; 43 import android.content.Context; 44 import android.os.Binder; 45 import android.os.PersistableBundle; 46 import android.os.Process; 47 import android.util.Pair; 48 import android.uwb.IUwbRangingCallbacks; 49 import android.uwb.RangingMeasurement; 50 import android.uwb.RangingReport; 51 import android.uwb.SessionHandle; 52 import android.uwb.UwbManager; 53 import android.uwb.UwbTestUtils; 54 55 import androidx.test.filters.SmallTest; 56 import androidx.test.runner.AndroidJUnit4; 57 58 import com.google.uwb.support.base.Params; 59 import com.google.uwb.support.ccc.CccOpenRangingParams; 60 import com.google.uwb.support.ccc.CccSpecificationParams; 61 import com.google.uwb.support.ccc.CccStartRangingParams; 62 import com.google.uwb.support.fira.FiraOpenSessionParams; 63 import com.google.uwb.support.fira.FiraSpecificationParams; 64 import com.google.uwb.support.generic.GenericSpecificationParams; 65 import com.google.uwb.support.radar.RadarOpenSessionParams; 66 67 import org.junit.After; 68 import org.junit.Before; 69 import org.junit.Test; 70 import org.junit.runner.RunWith; 71 import org.mockito.ArgumentCaptor; 72 import org.mockito.Mock; 73 import org.mockito.MockitoAnnotations; 74 75 import java.io.FileDescriptor; 76 import java.util.EnumSet; 77 import java.util.List; 78 import java.util.concurrent.FutureTask; 79 80 /** 81 * Unit tests for {@link com.android.server.uwb.UwbShellCommand}. 82 */ 83 @RunWith(AndroidJUnit4.class) 84 @SmallTest 85 public class UwbShellCommandTest { 86 private static final String TEST_PACKAGE = "com.android.test"; 87 88 @Mock UwbInjector mUwbInjector; 89 @Mock UwbServiceImpl mUwbService; 90 @Mock UwbCountryCode mUwbCountryCode; 91 @Mock Context mContext; 92 @Mock UwbServiceCore mUwbServiceCore; 93 94 UwbShellCommand mUwbShellCommand; 95 96 @Before setUp()97 public void setUp() throws Exception { 98 MockitoAnnotations.initMocks(this); 99 100 when(mUwbInjector.getUwbCountryCode()).thenReturn(mUwbCountryCode); 101 when(mUwbInjector.getUwbServiceCore()).thenReturn(mUwbServiceCore); 102 doAnswer(invocation -> { 103 FutureTask t = invocation.getArgument(0); 104 t.run(); 105 return t.get(); 106 }).when(mUwbInjector).runTaskOnSingleThreadExecutor(any(FutureTask.class), anyInt()); 107 GenericSpecificationParams params = new GenericSpecificationParams.Builder() 108 .setCccSpecificationParams(mock(CccSpecificationParams.class)) 109 .setFiraSpecificationParams( 110 new FiraSpecificationParams.Builder() 111 .setSupportedChannels(List.of(9)) 112 .setRangeDataNtfConfigCapabilities( 113 EnumSet.of( 114 HAS_RANGE_DATA_NTF_CONFIG_DISABLE, 115 HAS_RANGE_DATA_NTF_CONFIG_ENABLE)) 116 .build()) 117 .build(); 118 when(mUwbServiceCore.getCachedSpecificationParams(any())).thenReturn(params); 119 120 mUwbShellCommand = new UwbShellCommand(mUwbInjector, mUwbService, mContext); 121 122 // by default emulate shell uid. 123 BinderUtil.setUid(Process.SHELL_UID); 124 } 125 126 @After tearDown()127 public void tearDown() throws Exception { 128 mUwbShellCommand.reset(); 129 validateMockitoUsage(); 130 } 131 132 @Test testStatus()133 public void testStatus() throws Exception { 134 when(mUwbService.getAdapterState()) 135 .thenReturn(UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE); 136 137 // unrooted shell. 138 mUwbShellCommand.exec( 139 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 140 new String[]{"status"}); 141 verify(mUwbService).getAdapterState(); 142 } 143 144 @Test testForceSetCountryCode()145 public void testForceSetCountryCode() throws Exception { 146 // not allowed for unrooted shell. 147 mUwbShellCommand.exec( 148 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 149 new String[]{"force-country-code", "enabled", "US"}); 150 verify(mUwbCountryCode, never()).setOverrideCountryCode(any()); 151 assertThat(mUwbShellCommand.getErrPrintWriter().toString().isEmpty()).isFalse(); 152 153 BinderUtil.setUid(Process.ROOT_UID); 154 155 // rooted shell. 156 mUwbShellCommand.exec( 157 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 158 new String[]{"force-country-code", "enabled", "US"}); 159 verify(mUwbCountryCode).setOverrideCountryCode(any()); 160 161 } 162 163 @Test testForceClearCountryCode()164 public void testForceClearCountryCode() throws Exception { 165 // not allowed for unrooted shell. 166 mUwbShellCommand.exec( 167 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 168 new String[]{"force-country-code", "disabled"}); 169 verify(mUwbCountryCode, never()).setOverrideCountryCode(any()); 170 assertThat(mUwbShellCommand.getErrPrintWriter().toString().isEmpty()).isFalse(); 171 172 BinderUtil.setUid(Process.ROOT_UID); 173 174 // rooted shell. 175 mUwbShellCommand.exec( 176 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 177 new String[]{"force-country-code", "disabled"}); 178 verify(mUwbCountryCode).clearOverrideCountryCode(); 179 } 180 181 @Test testGetCountryCode()182 public void testGetCountryCode() throws Exception { 183 mUwbShellCommand.exec( 184 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 185 new String[]{"get-country-code"}); 186 verify(mUwbCountryCode).getCountryCode(); 187 } 188 189 @Test testEnableUwb()190 public void testEnableUwb() throws Exception { 191 mUwbShellCommand.exec( 192 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 193 new String[]{"enable-uwb"}); 194 verify(mUwbService).setEnabled(true); 195 } 196 197 @Test testDisableUwb()198 public void testDisableUwb() throws Exception { 199 mUwbShellCommand.exec( 200 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 201 new String[]{"disable-uwb"}); 202 verify(mUwbService).setEnabled(false); 203 } 204 205 private static class MutableCb { 206 @Nullable public IUwbRangingCallbacks cb; 207 } 208 triggerAndVerifySessionStart( String[] sessionStartCmd, @NonNull Params openSessionParams)209 private Pair<IUwbRangingCallbacks, SessionHandle> triggerAndVerifySessionStart( 210 String[] sessionStartCmd, @NonNull Params openSessionParams) throws Exception { 211 return triggerAndVerifySessionStart(sessionStartCmd, openSessionParams, null); 212 } 213 triggerAndVerifySessionStart( String[] sessionStartCmd, @NonNull Params openSessionParams, @Nullable Params startSessionParams)214 private Pair<IUwbRangingCallbacks, SessionHandle> triggerAndVerifySessionStart( 215 String[] sessionStartCmd, @NonNull Params openSessionParams, @Nullable Params 216 startSessionParams) throws Exception { 217 final MutableCb cbCaptor = new MutableCb(); 218 doAnswer(invocation -> { 219 cbCaptor.cb = invocation.getArgument(2); 220 cbCaptor.cb.onRangingOpened(invocation.getArgument(1)); 221 return true; 222 }).when(mUwbService).openRanging(any(), any(), any(), any(), any()); 223 doAnswer(invocation -> { 224 cbCaptor.cb.onRangingStarted(invocation.getArgument(0), new PersistableBundle()); 225 return true; 226 }).when(mUwbService).startRanging(any(), any()); 227 228 mUwbShellCommand.exec( 229 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 230 sessionStartCmd); 231 232 ArgumentCaptor<SessionHandle> sessionHandleCaptor = 233 ArgumentCaptor.forClass(SessionHandle.class); 234 ArgumentCaptor<PersistableBundle> paramsCaptor = 235 ArgumentCaptor.forClass(PersistableBundle.class); 236 237 verify(mUwbService).openRanging( 238 eq(new AttributionSource.Builder(Process.SHELL_UID) 239 .setPackageName(UwbShellCommand.SHELL_PACKAGE_NAME) 240 .build()), 241 sessionHandleCaptor.capture(), any(), paramsCaptor.capture(), any()); 242 // PersistableBundle does not implement equals, so use toString equals. 243 assertThat(paramsCaptor.getValue().toString()) 244 .isEqualTo(openSessionParams.toBundle().toString()); 245 246 verify(mUwbService).startRanging( 247 eq(sessionHandleCaptor.getValue()), paramsCaptor.capture()); 248 assertThat(paramsCaptor.getValue().toString()) 249 .isEqualTo(startSessionParams != null 250 ? startSessionParams.toBundle().toString() 251 : new PersistableBundle().toString()); 252 253 return Pair.create(cbCaptor.cb, sessionHandleCaptor.getValue()); 254 } 255 triggerAndVerifySessionStop( String[] sessionStopCmd, IUwbRangingCallbacks cb, SessionHandle sessionHandle)256 private void triggerAndVerifySessionStop( 257 String[] sessionStopCmd, IUwbRangingCallbacks cb, SessionHandle sessionHandle) 258 throws Exception { 259 doAnswer(invocation -> { 260 cb.onRangingStopped(sessionHandle, REASON_LOCAL_REQUEST, new PersistableBundle()); 261 return true; 262 }).when(mUwbService).stopRanging(any()); 263 doAnswer(invocation -> { 264 cb.onRangingClosed( 265 sessionHandle, REASON_LOCAL_REQUEST, 266 new PersistableBundle()); 267 return true; 268 }).when(mUwbService).closeRanging(any()); 269 270 mUwbShellCommand.exec( 271 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 272 sessionStopCmd); 273 274 verify(mUwbService).stopRanging(sessionHandle); 275 verify(mUwbService).closeRanging(sessionHandle); 276 } 277 getCccStartRangingParamsFromOpenRangingParams( @onNull CccOpenRangingParams openSessionParams)278 private CccStartRangingParams getCccStartRangingParamsFromOpenRangingParams( 279 @NonNull CccOpenRangingParams openSessionParams) { 280 return new CccStartRangingParams.Builder() 281 .setSessionId(openSessionParams.getSessionId()) 282 .setRanMultiplier(openSessionParams.getRanMultiplier()) 283 .build(); 284 } 285 286 @Test testStartFiraRanging()287 public void testStartFiraRanging() throws Exception { 288 triggerAndVerifySessionStart( 289 new String[]{"start-fira-ranging-session"}, 290 DEFAULT_FIRA_OPEN_SESSION_PARAMS.build()); 291 } 292 293 @Test testStartFiraRangingUsesUniqueSessionHandle()294 public void testStartFiraRangingUsesUniqueSessionHandle() throws Exception { 295 FiraOpenSessionParams.Builder openSessionParamsBuilder = 296 new FiraOpenSessionParams.Builder(DEFAULT_FIRA_OPEN_SESSION_PARAMS); 297 298 openSessionParamsBuilder.setSessionId(1); 299 Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle1 = 300 triggerAndVerifySessionStart( 301 new String[]{"start-fira-ranging-session", "-i", "1"}, 302 openSessionParamsBuilder.build()); 303 clearInvocations(mUwbService); 304 305 openSessionParamsBuilder.setSessionId(2); 306 Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle2 = 307 triggerAndVerifySessionStart( 308 new String[]{"start-fira-ranging-session", "-i", "2"}, 309 openSessionParamsBuilder.build()); 310 assertThat(cbAndSessionHandle1.second).isNotEqualTo(cbAndSessionHandle2.second); 311 } 312 313 @Test testStartFiraRangingWithNonDefaultParams()314 public void testStartFiraRangingWithNonDefaultParams() throws Exception { 315 FiraOpenSessionParams.Builder openSessionParamsBuilder = 316 new FiraOpenSessionParams.Builder(DEFAULT_FIRA_OPEN_SESSION_PARAMS); 317 openSessionParamsBuilder.setSessionId(5); 318 triggerAndVerifySessionStart( 319 new String[]{"start-fira-ranging-session", "-i", "5"}, 320 openSessionParamsBuilder.build()); 321 } 322 323 @Test testStartFiraRangingWithBothInterleavingAndAoaResultReq()324 public void testStartFiraRangingWithBothInterleavingAndAoaResultReq() throws Exception { 325 // Both AOA result req and interleaving are not allowed in the same command. 326 assertThat(mUwbShellCommand.exec( 327 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 328 new String[]{"start-fira-ranging-session", "-i", "5", "-z", "4,5,6", "-e", 329 "enabled"})).isEqualTo(-1); 330 } 331 getRangingMeasurement()332 private RangingMeasurement getRangingMeasurement() { 333 return new RangingMeasurement.Builder() 334 .setStatus(RangingMeasurement.RANGING_STATUS_SUCCESS) 335 .setElapsedRealtimeNanos(67) 336 .setDistanceMeasurement(UwbTestUtils.getDistanceMeasurement()) 337 .setAngleOfArrivalMeasurement(UwbTestUtils.getAngleOfArrivalMeasurement()) 338 .setRemoteDeviceAddress(UwbTestUtils.getUwbAddress(true)) 339 .build(); 340 } 341 342 @Test testRangingReportFiraRanging()343 public void testRangingReportFiraRanging() throws Exception { 344 Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle = 345 triggerAndVerifySessionStart( 346 new String[]{"start-fira-ranging-session"}, 347 DEFAULT_FIRA_OPEN_SESSION_PARAMS.build()); 348 int sessionId = DEFAULT_FIRA_OPEN_SESSION_PARAMS.build().getSessionId(); 349 cbAndSessionHandle.first.onRangingResult( 350 cbAndSessionHandle.second, 351 new RangingReport.Builder().addMeasurement(getRangingMeasurement()).build()); 352 mUwbShellCommand.exec( 353 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 354 new String[]{"get-ranging-session-reports", String.valueOf(sessionId)}); 355 } 356 357 @Test testRangingReportAllFiraRanging()358 public void testRangingReportAllFiraRanging() throws Exception { 359 Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle = 360 triggerAndVerifySessionStart( 361 new String[]{"start-fira-ranging-session"}, 362 DEFAULT_FIRA_OPEN_SESSION_PARAMS.build()); 363 cbAndSessionHandle.first.onRangingResult( 364 cbAndSessionHandle.second, 365 new RangingReport.Builder().addMeasurement(getRangingMeasurement()).build()); 366 mUwbShellCommand.exec( 367 new Binder(), new FileDescriptor(), new FileDescriptor(), new FileDescriptor(), 368 new String[]{"get-all-ranging-session-reports"}); 369 } 370 371 @Test testStopFiraRanging()372 public void testStopFiraRanging() throws Exception { 373 Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle = 374 triggerAndVerifySessionStart( 375 new String[]{"start-fira-ranging-session"}, 376 DEFAULT_FIRA_OPEN_SESSION_PARAMS.build()); 377 int sessionId = DEFAULT_FIRA_OPEN_SESSION_PARAMS.build().getSessionId(); 378 triggerAndVerifySessionStop( 379 new String[]{"stop-ranging-session", String.valueOf(sessionId)}, 380 cbAndSessionHandle.first, cbAndSessionHandle.second); 381 } 382 383 @Test testStartCccRanging()384 public void testStartCccRanging() throws Exception { 385 CccOpenRangingParams openSessionParams = DEFAULT_CCC_OPEN_RANGING_PARAMS.build(); 386 triggerAndVerifySessionStart( 387 new String[]{"start-ccc-ranging-session"}, 388 openSessionParams, 389 getCccStartRangingParamsFromOpenRangingParams(openSessionParams)); 390 } 391 392 @Test testStartCccRangingWithNonDefaultParams()393 public void testStartCccRangingWithNonDefaultParams() throws Exception { 394 CccOpenRangingParams.Builder openSessionParamsBuilder = 395 new CccOpenRangingParams.Builder(DEFAULT_CCC_OPEN_RANGING_PARAMS); 396 openSessionParamsBuilder.setSessionId(5); 397 CccOpenRangingParams openSessionParams = openSessionParamsBuilder.build(); 398 triggerAndVerifySessionStart( 399 new String[]{"start-ccc-ranging-session", "-i", "5"}, 400 openSessionParams, 401 getCccStartRangingParamsFromOpenRangingParams(openSessionParams)); 402 } 403 404 @Test testStopCccRanging()405 public void testStopCccRanging() throws Exception { 406 CccOpenRangingParams openSessionParams = DEFAULT_CCC_OPEN_RANGING_PARAMS.build(); 407 Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle = 408 triggerAndVerifySessionStart( 409 new String[]{"start-ccc-ranging-session"}, 410 openSessionParams, 411 getCccStartRangingParamsFromOpenRangingParams(openSessionParams)); 412 int sessionId = openSessionParams.getSessionId(); 413 triggerAndVerifySessionStop( 414 new String[]{"stop-ranging-session", String.valueOf(sessionId)}, 415 cbAndSessionHandle.first, cbAndSessionHandle.second); 416 } 417 418 @Test testStopAllRanging()419 public void testStopAllRanging() throws Exception { 420 CccOpenRangingParams openSessionParams = DEFAULT_CCC_OPEN_RANGING_PARAMS.build(); 421 Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle = 422 triggerAndVerifySessionStart( 423 new String[]{"start-ccc-ranging-session"}, 424 openSessionParams, 425 getCccStartRangingParamsFromOpenRangingParams(openSessionParams)); 426 triggerAndVerifySessionStop( 427 new String[]{"stop-all-ranging-sessions"}, 428 cbAndSessionHandle.first, cbAndSessionHandle.second); 429 } 430 431 @Test testStartRadarSession()432 public void testStartRadarSession() throws Exception { 433 RadarOpenSessionParams openSessionParams = DEFAULT_RADAR_OPEN_SESSION_PARAMS.build(); 434 triggerAndVerifySessionStart( 435 new String[]{"start-radar-session"}, 436 openSessionParams); 437 } 438 439 @Test testStopRadarSession()440 public void testStopRadarSession() throws Exception { 441 RadarOpenSessionParams openSessionParams = DEFAULT_RADAR_OPEN_SESSION_PARAMS.build(); 442 Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle = 443 triggerAndVerifySessionStart( 444 new String[]{"start-radar-session"}, 445 openSessionParams); 446 int sessionId = openSessionParams.getSessionId(); 447 triggerAndVerifySessionStop( 448 new String[]{"stop-radar-session", String.valueOf(sessionId)}, 449 cbAndSessionHandle.first, cbAndSessionHandle.second); 450 } 451 452 @Test testStopAllRadarSessions()453 public void testStopAllRadarSessions() throws Exception { 454 RadarOpenSessionParams openSessionParams = DEFAULT_RADAR_OPEN_SESSION_PARAMS.build(); 455 Pair<IUwbRangingCallbacks, SessionHandle> cbAndSessionHandle = 456 triggerAndVerifySessionStart( 457 new String[]{"start-radar-session"}, 458 openSessionParams); 459 triggerAndVerifySessionStop( 460 new String[]{"stop-all-radar-sessions"}, 461 cbAndSessionHandle.first, cbAndSessionHandle.second); 462 } 463 } 464