From 27e1a87a4869e345a103c6b93c79b156e1f2836f Mon Sep 17 00:00:00 2001 From: Roshan Pius Date: Wed, 31 Mar 2021 12:07:17 -0700 Subject: [PATCH 1/3] Uwb: Create a new Uwb system service This thin AOSP service layer will trampoline all API calls to the vendor UWB service. In follow up CL's, i. The AOSP service will perform all the permission checks necessary for this API surface before forwarding the call to the vendor UWB service. ii. Similarly, it will perform necessary permission checks + noteOp before forwarding the ranging callbacks from the vendor service back to the apps. Bug: 183904955 Test: atest android.uwb.cts.UwbManagerTest Test: atest com.android.server.uwb Change-Id: I2b367d1b6accc2f4e075cacb5c8e3c51f1faf5db --- core/java/android/uwb/UwbManager.java | 4 +- services/Android.bp | 2 + .../java/com/android/server/SystemServer.java | 7 + services/tests/servicestests/Android.bp | 1 + .../server/uwb/UwbServiceImplTest.java | 156 ++++++++++++++++++ services/uwb/Android.bp | 26 +++ .../com/android/server/uwb/UwbInjector.java | 48 ++++++ .../com/android/server/uwb/UwbService.java | 42 +++++ .../android/server/uwb/UwbServiceImpl.java | 107 ++++++++++++ 9 files changed, 390 insertions(+), 3 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java create mode 100644 services/uwb/Android.bp create mode 100644 services/uwb/java/com/android/server/uwb/UwbInjector.java create mode 100644 services/uwb/java/com/android/server/uwb/UwbService.java create mode 100644 services/uwb/java/com/android/server/uwb/UwbServiceImpl.java diff --git a/core/java/android/uwb/UwbManager.java b/core/java/android/uwb/UwbManager.java index bed77e6643372..95024b38575be 100644 --- a/core/java/android/uwb/UwbManager.java +++ b/core/java/android/uwb/UwbManager.java @@ -24,9 +24,7 @@ import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; -import android.content.AttributionSource; import android.content.Context; -import android.content.ContextParams; import android.os.CancellationSignal; import android.os.IBinder; import android.os.PersistableBundle; @@ -49,7 +47,7 @@ import java.util.concurrent.Executor; @SystemApi @SystemService(Context.UWB_SERVICE) public final class UwbManager { - private static final String SERVICE_NAME = "uwb"; + private static final String SERVICE_NAME = Context.UWB_SERVICE; private final Context mContext; private final IUwbAdapter mUwbAdapter; diff --git a/services/Android.bp b/services/Android.bp index ad1406c73c2fd..20b89de7f2a60 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -65,6 +65,7 @@ filegroup { ":services.texttospeech-sources", ":services.usage-sources", ":services.usb-sources", + ":services.uwb-sources", ":services.voiceinteraction-sources", ":services.wifi-sources", ], @@ -129,6 +130,7 @@ java_library { "services.texttospeech", "services.usage", "services.usb", + "services.uwb", "services.voiceinteraction", "services.wifi", "service-blobstore", diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 912b8cad952c8..4c4c5821a2dbb 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -384,6 +384,7 @@ public final class SystemServer implements Dumpable { private static final String ROLE_SERVICE_CLASS = "com.android.role.RoleService"; private static final String GAME_MANAGER_SERVICE_CLASS = "com.android.server.app.GameManagerService$Lifecycle"; + private static final String UWB_SERVICE_CLASS = "com.android.server.uwb.UwbService"; private static final String TETHERING_CONNECTOR_CLASS = "android.net.ITetheringConnector"; @@ -2637,6 +2638,12 @@ public final class SystemServer implements Dumpable { LocalManagerRegistry.addManager(ArtManagerLocal.class, new ArtManagerLocal()); t.traceEnd(); + if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB)) { + t.traceBegin("UwbService"); + mSystemServiceManager.startService(UWB_SERVICE_CLASS); + t.traceEnd(); + } + t.traceBegin("StartBootPhaseDeviceSpecificServicesReady"); mSystemServiceManager.startBootPhase(t, SystemService.PHASE_DEVICE_SPECIFIC_SERVICES_READY); t.traceEnd(); diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index d7fbd4913b2c0..9d055e0d431fb 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -37,6 +37,7 @@ android_test { "services.net", "services.people", "services.usage", + "services.uwb", "guava", "androidx.test.core", "androidx.test.ext.truth", diff --git a/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java b/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java new file mode 100644 index 0000000000000..a8121a6341812 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.uwb; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.PersistableBundle; +import android.platform.test.annotations.Presubmit; +import android.test.suitebuilder.annotation.SmallTest; +import android.uwb.IUwbAdapter; +import android.uwb.IUwbAdapterStateCallbacks; +import android.uwb.IUwbRangingCallbacks; +import android.uwb.SessionHandle; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link UwbServiceImpl}. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class UwbServiceImplTest { + @Mock private IUwbAdapter mVendorService; + @Mock private Context mContext; + @Mock private UwbInjector mUwbInjector; + + private UwbServiceImpl mUwbServiceImpl; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mUwbInjector.getVendorService()).thenReturn(mVendorService); + mUwbServiceImpl = new UwbServiceImpl(mContext, mUwbInjector); + } + + @Test + public void testApiCallThrowsIllegalStateExceptionIfVendorServiceNotFound() throws Exception { + when(mUwbInjector.getVendorService()).thenReturn(null); + + final IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class); + try { + mUwbServiceImpl.registerAdapterStateCallbacks(cb); + fail(); + } catch (IllegalStateException e) { /* pass */ } + } + + @Test + public void testRegisterAdapterStateCallbacks() throws Exception { + final IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class); + mUwbServiceImpl.registerAdapterStateCallbacks(cb); + + verify(mVendorService).registerAdapterStateCallbacks(cb); + } + + @Test + public void testUnregisterAdapterStateCallbacks() throws Exception { + final IUwbAdapterStateCallbacks cb = mock(IUwbAdapterStateCallbacks.class); + mUwbServiceImpl.unregisterAdapterStateCallbacks(cb); + + verify(mVendorService).unregisterAdapterStateCallbacks(cb); + } + + @Test + public void testGetTimestampResolutionNanos() throws Exception { + final long timestamp = 34L; + when(mVendorService.getTimestampResolutionNanos()).thenReturn(timestamp); + assertThat(mUwbServiceImpl.getTimestampResolutionNanos()).isEqualTo(timestamp); + + verify(mVendorService).getTimestampResolutionNanos(); + } + + @Test + public void testGetSpecificationInfo() throws Exception { + final PersistableBundle specification = new PersistableBundle(); + when(mVendorService.getSpecificationInfo()).thenReturn(specification); + assertThat(mUwbServiceImpl.getSpecificationInfo()).isEqualTo(specification); + + verify(mVendorService).getSpecificationInfo(); + } + + @Test + public void testOpenRanging() throws Exception { + final SessionHandle sessionHandle = new SessionHandle(5); + final IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class); + final PersistableBundle parameters = new PersistableBundle(); + + mUwbServiceImpl.openRanging(sessionHandle, cb, parameters); + + verify(mVendorService).openRanging(sessionHandle, cb, parameters); + } + + @Test + public void testStartRanging() throws Exception { + final SessionHandle sessionHandle = new SessionHandle(5); + final PersistableBundle parameters = new PersistableBundle(); + + mUwbServiceImpl.startRanging(sessionHandle, parameters); + + verify(mVendorService).startRanging(sessionHandle, parameters); + } + + @Test + public void testReconfigureRanging() throws Exception { + final SessionHandle sessionHandle = new SessionHandle(5); + final PersistableBundle parameters = new PersistableBundle(); + + mUwbServiceImpl.reconfigureRanging(sessionHandle, parameters); + + verify(mVendorService).reconfigureRanging(sessionHandle, parameters); + } + + @Test + public void testStopRanging() throws Exception { + final SessionHandle sessionHandle = new SessionHandle(5); + + mUwbServiceImpl.stopRanging(sessionHandle); + + verify(mVendorService).stopRanging(sessionHandle); + } + + @Test + public void testCloseRanging() throws Exception { + final SessionHandle sessionHandle = new SessionHandle(5); + + mUwbServiceImpl.closeRanging(sessionHandle); + + verify(mVendorService).closeRanging(sessionHandle); + } +} diff --git a/services/uwb/Android.bp b/services/uwb/Android.bp new file mode 100644 index 0000000000000..da30d43a45369 --- /dev/null +++ b/services/uwb/Android.bp @@ -0,0 +1,26 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +filegroup { + name: "services.uwb-sources", + srcs: ["java/**/*.java"], + path: "java", + visibility: ["//frameworks/base/services"], +} + +java_library_static { + name: "services.uwb", + defaults: ["platform_service_defaults"], + srcs: [ + ":services.uwb-sources", + ], + libs: [ + "services.core", + ], +} diff --git a/services/uwb/java/com/android/server/uwb/UwbInjector.java b/services/uwb/java/com/android/server/uwb/UwbInjector.java new file mode 100644 index 0000000000000..00c0acabcb3bc --- /dev/null +++ b/services/uwb/java/com/android/server/uwb/UwbInjector.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.uwb; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.IBinder; +import android.os.ServiceManager; +import android.uwb.IUwbAdapter; + + +/** + * To be used for dependency injection (especially helps mocking static dependencies). + */ +public class UwbInjector { + private static final String TAG = "UwbInjector"; + + private static final String VENDOR_SERVICE_NAME = "uwb_vendor"; + + private final Context mContext; + + public UwbInjector(@NonNull Context context) { + mContext = context; + } + + /** + * @return Returns the vendor service handle. + */ + public IUwbAdapter getVendorService() { + IBinder b = ServiceManager.getService(VENDOR_SERVICE_NAME); + if (b == null) return null; + return IUwbAdapter.Stub.asInterface(b); + } +} diff --git a/services/uwb/java/com/android/server/uwb/UwbService.java b/services/uwb/java/com/android/server/uwb/UwbService.java new file mode 100644 index 0000000000000..4bb280f75ed12 --- /dev/null +++ b/services/uwb/java/com/android/server/uwb/UwbService.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.uwb; + +import android.content.Context; +import android.util.Log; + +import com.android.server.SystemService; + +/** + * Uwb System service. + */ +public class UwbService extends SystemService { + private static final String TAG = "UwbService"; + + private final UwbServiceImpl mImpl; + + public UwbService(Context context) { + super(context); + mImpl = new UwbServiceImpl(context, new UwbInjector(context)); + } + + @Override + public void onStart() { + Log.i(TAG, "Registering " + Context.UWB_SERVICE); + publishBinderService(Context.UWB_SERVICE, mImpl); + } +} diff --git a/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java b/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java new file mode 100644 index 0000000000000..332402c927ca5 --- /dev/null +++ b/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.uwb; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.PersistableBundle; +import android.os.RemoteException; +import android.util.Log; +import android.uwb.IUwbAdapter; +import android.uwb.IUwbAdapterStateCallbacks; +import android.uwb.IUwbRangingCallbacks; +import android.uwb.SessionHandle; + +/** + * Implementation of {@link android.uwb.IUwbAdapter} binder service. + */ +public class UwbServiceImpl extends IUwbAdapter.Stub { + private static final String TAG = "UwbServiceImpl"; + + private final Context mContext; + private final UwbInjector mUwbInjector; + + /** + * Used for caching the vendor implementation of {@link IUwbAdapter} interface. + */ + private IUwbAdapter mVendorUwbAdapter; + + private IUwbAdapter getVendorUwbAdapter() throws IllegalStateException { + if (mVendorUwbAdapter != null) return mVendorUwbAdapter; + mVendorUwbAdapter = mUwbInjector.getVendorService(); + if (mVendorUwbAdapter == null) { + throw new IllegalStateException("No vendor service found!"); + } + Log.i(TAG, "Retrieved vendor service"); + return mVendorUwbAdapter; + } + + public UwbServiceImpl(@NonNull Context context, @NonNull UwbInjector uwbInjector) { + mContext = context; + mUwbInjector = uwbInjector; + } + + @Override + public void registerAdapterStateCallbacks(IUwbAdapterStateCallbacks adapterStateCallbacks) + throws RemoteException { + getVendorUwbAdapter().registerAdapterStateCallbacks(adapterStateCallbacks); + } + + @Override + public void unregisterAdapterStateCallbacks(IUwbAdapterStateCallbacks adapterStateCallbacks) + throws RemoteException { + getVendorUwbAdapter().unregisterAdapterStateCallbacks(adapterStateCallbacks); + } + + @Override + public long getTimestampResolutionNanos() throws RemoteException { + return getVendorUwbAdapter().getTimestampResolutionNanos(); + } + + @Override + public PersistableBundle getSpecificationInfo() throws RemoteException { + return getVendorUwbAdapter().getSpecificationInfo(); + } + + @Override + public void openRanging(SessionHandle sessionHandle, IUwbRangingCallbacks rangingCallbacks, + PersistableBundle parameters) throws RemoteException { + getVendorUwbAdapter().openRanging(sessionHandle, rangingCallbacks, parameters); + } + + @Override + public void startRanging(SessionHandle sessionHandle, PersistableBundle parameters) + throws RemoteException { + getVendorUwbAdapter().startRanging(sessionHandle, parameters); + } + + @Override + public void reconfigureRanging(SessionHandle sessionHandle, PersistableBundle parameters) + throws RemoteException { + getVendorUwbAdapter().reconfigureRanging(sessionHandle, parameters); + } + + @Override + public void stopRanging(SessionHandle sessionHandle) throws RemoteException { + getVendorUwbAdapter().stopRanging(sessionHandle); + } + + @Override + public void closeRanging(SessionHandle sessionHandle) throws RemoteException { + getVendorUwbAdapter().closeRanging(sessionHandle); + } +} From e4fd3ed60bb1cc958f06a06b113f6241dff2dde0 Mon Sep 17 00:00:00 2001 From: Roshan Pius Date: Wed, 31 Mar 2021 18:08:33 -0700 Subject: [PATCH 2/3] UwbService: Create a UwbRanging callback wrapper This wrapper will be used for performing permission checks before sending the results to the corresponding app. Bug: 183904955 Test: atest android.uwb.cts.UwbManagerTest Change-Id: I562951d9f28f1a55391f723445c30fa5ff8aea47 --- .../server/uwb/UwbServiceImplTest.java | 106 ++++++++++++++ .../android/server/uwb/UwbServiceImpl.java | 134 +++++++++++++++++- 2 files changed, 238 insertions(+), 2 deletions(-) diff --git a/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java b/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java index a8121a6341812..867f68415b6b8 100644 --- a/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java @@ -19,17 +19,25 @@ package com.android.server.uwb; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.os.IBinder; import android.os.PersistableBundle; import android.platform.test.annotations.Presubmit; import android.test.suitebuilder.annotation.SmallTest; import android.uwb.IUwbAdapter; import android.uwb.IUwbAdapterStateCallbacks; import android.uwb.IUwbRangingCallbacks; +import android.uwb.RangingReport; +import android.uwb.RangingSession; import android.uwb.SessionHandle; import androidx.test.runner.AndroidJUnit4; @@ -37,6 +45,8 @@ import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -50,6 +60,9 @@ public class UwbServiceImplTest { @Mock private IUwbAdapter mVendorService; @Mock private Context mContext; @Mock private UwbInjector mUwbInjector; + @Captor private ArgumentCaptor mRangingCbCaptor; + @Captor private ArgumentCaptor mClientDeathCaptor; + @Captor private ArgumentCaptor mVendorServiceDeathCaptor; private UwbServiceImpl mUwbServiceImpl; @@ -153,4 +166,97 @@ public class UwbServiceImplTest { verify(mVendorService).closeRanging(sessionHandle); } + + @Test + public void testRangingCallbacks() throws Exception { + final SessionHandle sessionHandle = new SessionHandle(5); + final IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class); + final PersistableBundle parameters = new PersistableBundle(); + final IBinder cbBinder = mock(IBinder.class); + when(cb.asBinder()).thenReturn(cbBinder); + + mUwbServiceImpl.openRanging(sessionHandle, cb, parameters); + + verify(mVendorService).openRanging( + eq(sessionHandle), mRangingCbCaptor.capture(), eq(parameters)); + assertThat(mRangingCbCaptor.getValue()).isNotNull(); + + // Invoke vendor service callbacks and ensure that the corresponding app callback is + // invoked. + mRangingCbCaptor.getValue().onRangingOpened(sessionHandle); + verify(cb).onRangingOpened(sessionHandle); + + mRangingCbCaptor.getValue().onRangingOpenFailed( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + verify(cb).onRangingOpenFailed( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + + mRangingCbCaptor.getValue().onRangingStarted(sessionHandle, parameters); + verify(cb).onRangingStarted(sessionHandle, parameters); + + mRangingCbCaptor.getValue().onRangingStartFailed( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + verify(cb).onRangingStartFailed( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + + mRangingCbCaptor.getValue().onRangingReconfigured(sessionHandle, parameters); + verify(cb).onRangingReconfigured(sessionHandle, parameters); + + mRangingCbCaptor.getValue().onRangingReconfigureFailed( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + verify(cb).onRangingReconfigureFailed( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + + mRangingCbCaptor.getValue().onRangingStopped( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + verify(cb).onRangingStopped( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + + mRangingCbCaptor.getValue().onRangingStopFailed( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + verify(cb).onRangingStopFailed( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + + final RangingReport rangingReport = new RangingReport.Builder().build(); + mRangingCbCaptor.getValue().onRangingResult(sessionHandle, rangingReport); + verify(cb).onRangingResult(sessionHandle, rangingReport); + + mRangingCbCaptor.getValue().onRangingClosed( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + verify(cb).onRangingClosed( + sessionHandle, RangingSession.Callback.REASON_GENERIC_ERROR, parameters); + } + + @Test + public void testHandleClientDeath() throws Exception { + final SessionHandle sessionHandle = new SessionHandle(5); + final IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class); + final PersistableBundle parameters = new PersistableBundle(); + final IBinder cbBinder = mock(IBinder.class); + when(cb.asBinder()).thenReturn(cbBinder); + + mUwbServiceImpl.openRanging(sessionHandle, cb, parameters); + + verify(mVendorService).openRanging( + eq(sessionHandle), mRangingCbCaptor.capture(), eq(parameters)); + assertThat(mRangingCbCaptor.getValue()).isNotNull(); + + verify(cbBinder).linkToDeath(mClientDeathCaptor.capture(), anyInt()); + assertThat(mClientDeathCaptor.getValue()).isNotNull(); + + clearInvocations(cb); + + // Invoke cb, ensure it reaches the client. + mRangingCbCaptor.getValue().onRangingOpened(sessionHandle); + verify(cb).onRangingOpened(sessionHandle); + + // Trigger client death and ensure the session is stopped. + mClientDeathCaptor.getValue().binderDied(); + verify(mVendorService).stopRanging(sessionHandle); + verify(mVendorService).closeRanging(sessionHandle); + + // Invoke cb, it should be ignored. + mRangingCbCaptor.getValue().onRangingStarted(sessionHandle, parameters); + verify(cb, never()).onRangingStarted(any(), any()); + } } diff --git a/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java b/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java index 332402c927ca5..ce39ff65c5e2e 100644 --- a/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java +++ b/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java @@ -18,14 +18,21 @@ package com.android.server.uwb; import android.annotation.NonNull; import android.content.Context; +import android.os.IBinder; import android.os.PersistableBundle; import android.os.RemoteException; +import android.util.ArrayMap; import android.util.Log; import android.uwb.IUwbAdapter; import android.uwb.IUwbAdapterStateCallbacks; import android.uwb.IUwbRangingCallbacks; +import android.uwb.RangingReport; import android.uwb.SessionHandle; +import com.android.internal.annotations.GuardedBy; + +import java.util.Map; + /** * Implementation of {@link android.uwb.IUwbAdapter} binder service. */ @@ -34,12 +41,129 @@ public class UwbServiceImpl extends IUwbAdapter.Stub { private final Context mContext; private final UwbInjector mUwbInjector; + /** + * Map for storing the callbacks wrapper for each session. + */ + @GuardedBy("mCallbacksMap") + private final Map mCallbacksMap = new ArrayMap<>(); /** * Used for caching the vendor implementation of {@link IUwbAdapter} interface. */ private IUwbAdapter mVendorUwbAdapter; + /** + * Wrapper for callback registered with vendor service. This wrapper is needed for performing + * permission check before sending the callback to the external app. + */ + private class UwbRangingCallbacksWrapper extends IUwbRangingCallbacks.Stub + implements IBinder.DeathRecipient{ + private final SessionHandle mSessionHandle; + private final IUwbRangingCallbacks mExternalCb; + + UwbRangingCallbacksWrapper(@NonNull SessionHandle sessionHandle, + @NonNull IUwbRangingCallbacks externalCb) { + mSessionHandle = sessionHandle; + mExternalCb = externalCb; + + // Link to death for external callback. + linkToDeath(); + } + + private void linkToDeath() { + IBinder binder = mExternalCb.asBinder(); + try { + binder.linkToDeath(this, 0); + } catch (RemoteException e) { + Log.e(TAG, "Unable to link to client death event."); + } + } + + private void removeClientAndUnlinkToDeath() { + // Remove from the map. + synchronized (mCallbacksMap) { + mCallbacksMap.remove(mSessionHandle); + } + IBinder binder = mExternalCb.asBinder(); + binder.unlinkToDeath(this, 0); + } + + + @Override + public void onRangingOpened(SessionHandle sessionHandle) throws RemoteException { + mExternalCb.onRangingOpened(sessionHandle); + } + + @Override + public void onRangingOpenFailed(SessionHandle sessionHandle, + int reason, PersistableBundle parameters) throws RemoteException { + mExternalCb.onRangingOpenFailed(sessionHandle, reason, parameters); + } + + @Override + public void onRangingStarted(SessionHandle sessionHandle, PersistableBundle parameters) + throws RemoteException { + mExternalCb.onRangingStarted(sessionHandle, parameters); + } + + @Override + public void onRangingStartFailed(SessionHandle sessionHandle, + int reason, PersistableBundle parameters) throws RemoteException { + mExternalCb.onRangingStartFailed(sessionHandle, reason, parameters); + } + + @Override + public void onRangingReconfigured(SessionHandle sessionHandle, PersistableBundle parameters) + throws RemoteException { + mExternalCb.onRangingReconfigured(sessionHandle, parameters); + } + + @Override + public void onRangingReconfigureFailed(SessionHandle sessionHandle, + int reason, PersistableBundle parameters) throws RemoteException { + mExternalCb.onRangingReconfigureFailed(sessionHandle, reason, parameters); + } + + @Override + public void onRangingStopped(SessionHandle sessionHandle, int reason, + PersistableBundle parameters) + throws RemoteException { + mExternalCb.onRangingStopped(sessionHandle, reason, parameters); + } + + @Override + public void onRangingStopFailed(SessionHandle sessionHandle, int reason, + PersistableBundle parameters) throws RemoteException { + mExternalCb.onRangingStopFailed(sessionHandle, reason, parameters); + } + + @Override + public void onRangingClosed(SessionHandle sessionHandle, int reason, + PersistableBundle parameters) throws RemoteException { + mExternalCb.onRangingClosed(sessionHandle, reason, parameters); + removeClientAndUnlinkToDeath(); + } + + @Override + public void onRangingResult(SessionHandle sessionHandle, RangingReport rangingReport) + throws RemoteException { + // TODO: Perform permission checks and noteOp. + mExternalCb.onRangingResult(sessionHandle, rangingReport); + } + + @Override + public void binderDied() { + Log.i(TAG, "Client died: ending session: " + mSessionHandle); + try { + stopRanging(mSessionHandle); + closeRanging(mSessionHandle); + } catch (RemoteException execption) { + Log.w(TAG, "Remote exception while handling client death"); + removeClientAndUnlinkToDeath(); + } + } + } + private IUwbAdapter getVendorUwbAdapter() throws IllegalStateException { if (mVendorUwbAdapter != null) return mVendorUwbAdapter; mVendorUwbAdapter = mUwbInjector.getVendorService(); @@ -50,7 +174,7 @@ public class UwbServiceImpl extends IUwbAdapter.Stub { return mVendorUwbAdapter; } - public UwbServiceImpl(@NonNull Context context, @NonNull UwbInjector uwbInjector) { + UwbServiceImpl(@NonNull Context context, @NonNull UwbInjector uwbInjector) { mContext = context; mUwbInjector = uwbInjector; } @@ -80,12 +204,18 @@ public class UwbServiceImpl extends IUwbAdapter.Stub { @Override public void openRanging(SessionHandle sessionHandle, IUwbRangingCallbacks rangingCallbacks, PersistableBundle parameters) throws RemoteException { - getVendorUwbAdapter().openRanging(sessionHandle, rangingCallbacks, parameters); + UwbRangingCallbacksWrapper wrapperCb = + new UwbRangingCallbacksWrapper(sessionHandle, rangingCallbacks); + synchronized (mCallbacksMap) { + mCallbacksMap.put(sessionHandle, wrapperCb); + } + getVendorUwbAdapter().openRanging(sessionHandle, wrapperCb, parameters); } @Override public void startRanging(SessionHandle sessionHandle, PersistableBundle parameters) throws RemoteException { + // TODO: Perform permission checks. getVendorUwbAdapter().startRanging(sessionHandle, parameters); } From 5681a4818d54f5c0bb3384d8f36cd57bf56eedb3 Mon Sep 17 00:00:00 2001 From: Roshan Pius Date: Thu, 8 Apr 2021 12:55:11 -0700 Subject: [PATCH 3/3] UwbService: Handle vendor service death Needs to trigger session close callbacks for all the clients and clear the client map. Bug: 183904955 Test: atest android.uwb.cts.UwbManagerTest Change-Id: Ib6da490ba535e692daba418c3d10abf6c0ad3ab8 --- .../server/uwb/UwbServiceImplTest.java | 39 ++++++++- .../android/server/uwb/UwbServiceImpl.java | 85 +++++++++++++++---- 2 files changed, 106 insertions(+), 18 deletions(-) diff --git a/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java b/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java index 867f68415b6b8..2f5a5cc971174 100644 --- a/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/uwb/UwbServiceImplTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; @@ -58,6 +59,7 @@ import org.mockito.MockitoAnnotations; @Presubmit public class UwbServiceImplTest { @Mock private IUwbAdapter mVendorService; + @Mock private IBinder mVendorServiceBinder; @Mock private Context mContext; @Mock private UwbInjector mUwbInjector; @Captor private ArgumentCaptor mRangingCbCaptor; @@ -70,6 +72,7 @@ public class UwbServiceImplTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(mUwbInjector.getVendorService()).thenReturn(mVendorService); + when(mVendorService.asBinder()).thenReturn(mVendorServiceBinder); mUwbServiceImpl = new UwbServiceImpl(mContext, mUwbInjector); } @@ -123,10 +126,14 @@ public class UwbServiceImplTest { final SessionHandle sessionHandle = new SessionHandle(5); final IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class); final PersistableBundle parameters = new PersistableBundle(); + final IBinder cbBinder = mock(IBinder.class); + when(cb.asBinder()).thenReturn(cbBinder); mUwbServiceImpl.openRanging(sessionHandle, cb, parameters); - verify(mVendorService).openRanging(sessionHandle, cb, parameters); + verify(mVendorService).openRanging( + eq(sessionHandle), mRangingCbCaptor.capture(), eq(parameters)); + assertThat(mRangingCbCaptor.getValue()).isNotNull(); } @Test @@ -259,4 +266,34 @@ public class UwbServiceImplTest { mRangingCbCaptor.getValue().onRangingStarted(sessionHandle, parameters); verify(cb, never()).onRangingStarted(any(), any()); } + + @Test + public void testHandleVendorServiceDeath() throws Exception { + final SessionHandle sessionHandle = new SessionHandle(5); + final IUwbRangingCallbacks cb = mock(IUwbRangingCallbacks.class); + final PersistableBundle parameters = new PersistableBundle(); + final IBinder cbBinder = mock(IBinder.class); + when(cb.asBinder()).thenReturn(cbBinder); + + mUwbServiceImpl.openRanging(sessionHandle, cb, parameters); + + verify(mVendorServiceBinder).linkToDeath(mVendorServiceDeathCaptor.capture(), anyInt()); + assertThat(mVendorServiceDeathCaptor.getValue()).isNotNull(); + + verify(mVendorService).openRanging( + eq(sessionHandle), mRangingCbCaptor.capture(), eq(parameters)); + assertThat(mRangingCbCaptor.getValue()).isNotNull(); + + clearInvocations(cb); + + // Invoke cb, ensure it reaches the client. + mRangingCbCaptor.getValue().onRangingOpened(sessionHandle); + verify(cb).onRangingOpened(sessionHandle); + + // Trigger vendor service death and ensure that the client is informed of session end. + mVendorServiceDeathCaptor.getValue().binderDied(); + verify(cb).onRangingClosed( + eq(sessionHandle), eq(RangingSession.Callback.REASON_UNKNOWN), + argThat((p) -> p.isEmpty())); + } } diff --git a/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java b/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java index ce39ff65c5e2e..70bd20e0014e5 100644 --- a/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java +++ b/services/uwb/java/com/android/server/uwb/UwbServiceImpl.java @@ -27,6 +27,7 @@ import android.uwb.IUwbAdapter; import android.uwb.IUwbAdapterStateCallbacks; import android.uwb.IUwbRangingCallbacks; import android.uwb.RangingReport; +import android.uwb.RangingSession; import android.uwb.SessionHandle; import com.android.internal.annotations.GuardedBy; @@ -36,7 +37,7 @@ import java.util.Map; /** * Implementation of {@link android.uwb.IUwbAdapter} binder service. */ -public class UwbServiceImpl extends IUwbAdapter.Stub { +public class UwbServiceImpl extends IUwbAdapter.Stub implements IBinder.DeathRecipient{ private static final String TAG = "UwbServiceImpl"; private final Context mContext; @@ -55,16 +56,20 @@ public class UwbServiceImpl extends IUwbAdapter.Stub { /** * Wrapper for callback registered with vendor service. This wrapper is needed for performing * permission check before sending the callback to the external app. + * + * Access to these callbacks are synchronized. */ private class UwbRangingCallbacksWrapper extends IUwbRangingCallbacks.Stub implements IBinder.DeathRecipient{ private final SessionHandle mSessionHandle; private final IUwbRangingCallbacks mExternalCb; + private boolean mIsValid; UwbRangingCallbacksWrapper(@NonNull SessionHandle sessionHandle, @NonNull IUwbRangingCallbacks externalCb) { mSessionHandle = sessionHandle; mExternalCb = externalCb; + mIsValid = true; // Link to death for external callback. linkToDeath(); @@ -75,7 +80,7 @@ public class UwbServiceImpl extends IUwbAdapter.Stub { try { binder.linkToDeath(this, 0); } catch (RemoteException e) { - Log.e(TAG, "Unable to link to client death event."); + Log.e(TAG, "Unable to link to client death event.", e); } } @@ -86,91 +91,136 @@ public class UwbServiceImpl extends IUwbAdapter.Stub { } IBinder binder = mExternalCb.asBinder(); binder.unlinkToDeath(this, 0); + mIsValid = false; } @Override - public void onRangingOpened(SessionHandle sessionHandle) throws RemoteException { + public synchronized void onRangingOpened(SessionHandle sessionHandle) + throws RemoteException { + if (!mIsValid) return; mExternalCb.onRangingOpened(sessionHandle); } @Override - public void onRangingOpenFailed(SessionHandle sessionHandle, + public synchronized void onRangingOpenFailed(SessionHandle sessionHandle, int reason, PersistableBundle parameters) throws RemoteException { + if (!mIsValid) return; mExternalCb.onRangingOpenFailed(sessionHandle, reason, parameters); } @Override - public void onRangingStarted(SessionHandle sessionHandle, PersistableBundle parameters) + public synchronized void onRangingStarted(SessionHandle sessionHandle, + PersistableBundle parameters) throws RemoteException { + if (!mIsValid) return; mExternalCb.onRangingStarted(sessionHandle, parameters); } @Override - public void onRangingStartFailed(SessionHandle sessionHandle, + public synchronized void onRangingStartFailed(SessionHandle sessionHandle, int reason, PersistableBundle parameters) throws RemoteException { + if (!mIsValid) return; mExternalCb.onRangingStartFailed(sessionHandle, reason, parameters); } @Override - public void onRangingReconfigured(SessionHandle sessionHandle, PersistableBundle parameters) + public synchronized void onRangingReconfigured(SessionHandle sessionHandle, + PersistableBundle parameters) throws RemoteException { + if (!mIsValid) return; mExternalCb.onRangingReconfigured(sessionHandle, parameters); } @Override - public void onRangingReconfigureFailed(SessionHandle sessionHandle, + public synchronized void onRangingReconfigureFailed(SessionHandle sessionHandle, int reason, PersistableBundle parameters) throws RemoteException { + if (!mIsValid) return; mExternalCb.onRangingReconfigureFailed(sessionHandle, reason, parameters); } @Override - public void onRangingStopped(SessionHandle sessionHandle, int reason, + public synchronized void onRangingStopped(SessionHandle sessionHandle, int reason, PersistableBundle parameters) throws RemoteException { + if (!mIsValid) return; mExternalCb.onRangingStopped(sessionHandle, reason, parameters); } @Override - public void onRangingStopFailed(SessionHandle sessionHandle, int reason, + public synchronized void onRangingStopFailed(SessionHandle sessionHandle, int reason, PersistableBundle parameters) throws RemoteException { + if (!mIsValid) return; mExternalCb.onRangingStopFailed(sessionHandle, reason, parameters); } @Override - public void onRangingClosed(SessionHandle sessionHandle, int reason, + public synchronized void onRangingClosed(SessionHandle sessionHandle, int reason, PersistableBundle parameters) throws RemoteException { + if (!mIsValid) return; mExternalCb.onRangingClosed(sessionHandle, reason, parameters); removeClientAndUnlinkToDeath(); } @Override - public void onRangingResult(SessionHandle sessionHandle, RangingReport rangingReport) + public synchronized void onRangingResult(SessionHandle sessionHandle, + RangingReport rangingReport) throws RemoteException { + if (!mIsValid) return; // TODO: Perform permission checks and noteOp. mExternalCb.onRangingResult(sessionHandle, rangingReport); } @Override - public void binderDied() { + public synchronized void binderDied() { + if (!mIsValid) return; Log.i(TAG, "Client died: ending session: " + mSessionHandle); try { + removeClientAndUnlinkToDeath(); stopRanging(mSessionHandle); closeRanging(mSessionHandle); - } catch (RemoteException execption) { - Log.w(TAG, "Remote exception while handling client death"); - removeClientAndUnlinkToDeath(); + } catch (RemoteException e) { + Log.e(TAG, "Remote exception while handling client death", e); } } } - private IUwbAdapter getVendorUwbAdapter() throws IllegalStateException { + private void linkToVendorServiceDeath() { + IBinder binder = mVendorUwbAdapter.asBinder(); + try { + binder.linkToDeath(this, 0); + } catch (RemoteException e) { + Log.e(TAG, "Unable to link to vendor service death event.", e); + } + } + + @Override + public void binderDied() { + Log.i(TAG, "Vendor service died: sending session close callbacks"); + synchronized (mCallbacksMap) { + for (Map.Entry e : mCallbacksMap.entrySet()) { + try { + e.getValue().mExternalCb.onRangingClosed( + e.getKey(), RangingSession.Callback.REASON_UNKNOWN, + new PersistableBundle()); + } catch (RemoteException ex) { + Log.e(TAG, "Failed to send session close callback " + e.getKey(), ex); + } + } + // Clear all sessions. + mCallbacksMap.clear(); + } + mVendorUwbAdapter = null; + } + + private synchronized IUwbAdapter getVendorUwbAdapter() throws IllegalStateException { if (mVendorUwbAdapter != null) return mVendorUwbAdapter; mVendorUwbAdapter = mUwbInjector.getVendorService(); if (mVendorUwbAdapter == null) { throw new IllegalStateException("No vendor service found!"); } Log.i(TAG, "Retrieved vendor service"); + linkToVendorServiceDeath(); return mVendorUwbAdapter; } @@ -234,4 +284,5 @@ public class UwbServiceImpl extends IUwbAdapter.Stub { public void closeRanging(SessionHandle sessionHandle) throws RemoteException { getVendorUwbAdapter().closeRanging(sessionHandle); } + }