/* * Copyright (C) 2020 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 android.car.vms; import static android.system.OsConstants.PROT_READ; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.car.Car; import android.car.vms.VmsClientManager.VmsClientCallback; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.os.SharedMemory; import android.system.ErrnoException; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.BiConsumer; import java.util.function.Consumer; /** * API implementation for use by Vehicle Map Service clients. * * This API can be obtained by registering a callback with {@link VmsClientManager}. * * @hide */ @SystemApi public final class VmsClient { private static final boolean DBG = false; private static final String TAG = VmsClient.class.getSimpleName(); private static final VmsAvailableLayers DEFAULT_AVAILABLE_LAYERS = new VmsAvailableLayers(Collections.emptySet(), 0); private static final VmsSubscriptionState DEFAULT_SUBSCRIPTIONS = new VmsSubscriptionState(0, Collections.emptySet(), Collections.emptySet()); private static final int LARGE_PACKET_THRESHOLD = 16 * 1024; // 16 KB private final IVmsBrokerService mService; private final Executor mExecutor; private final VmsClientCallback mCallback; private final boolean mLegacyClient; private final IVmsClientCallback mClientCallback; private final Consumer mExceptionHandler; private final IBinder mClientToken; private final Object mLock = new Object(); @GuardedBy("mLock") private VmsAvailableLayers mAvailableLayers = DEFAULT_AVAILABLE_LAYERS; @GuardedBy("mLock") private VmsSubscriptionState mSubscriptionState = DEFAULT_SUBSCRIPTIONS; @GuardedBy("mLock") private boolean mMonitoringEnabled; /** * @hide */ public VmsClient(IVmsBrokerService service, Executor executor, VmsClientCallback callback, boolean legacyClient, boolean autoCloseMemory, Consumer exceptionHandler) { mService = service; mExecutor = executor; mCallback = callback; mLegacyClient = legacyClient; mClientCallback = new IVmsClientCallbackImpl(this, autoCloseMemory); mExceptionHandler = exceptionHandler; mClientToken = new Binder(); } /** * Retrieves registered information about a Vehicle Map Service data provider. * *

Data layers may be associated with multiple providers in updates received via * {@link VmsClientCallback#onLayerAvailabilityChanged(VmsAvailableLayers)}, and clients can use * this query to determine a provider or subset of providers to subscribe to. * * @param providerId Provider ID * @return Provider's registration information, or null if unavailable */ @Nullable @RequiresPermission(anyOf = {Car.PERMISSION_VMS_PUBLISHER, Car.PERMISSION_VMS_SUBSCRIBER}) public byte[] getProviderDescription(int providerId) { if (DBG) Slog.d(TAG, "Getting provider information for " + providerId); try { return mService.getProviderInfo(mClientToken, providerId).getDescription(); } catch (RemoteException e) { Slog.e(TAG, "While getting publisher information for " + providerId, e); mExceptionHandler.accept(e); return null; } } /** * Sets this client's data layer subscriptions * *

Existing subscriptions for the client will be replaced. * * @param layers Data layers to be subscribed */ @RequiresPermission(Car.PERMISSION_VMS_SUBSCRIBER) public void setSubscriptions(@NonNull Set layers) { if (DBG) Slog.d(TAG, "Setting subscriptions to " + layers); try { mService.setSubscriptions(mClientToken, new ArrayList<>(layers)); } catch (RemoteException e) { Slog.e(TAG, "While setting subscriptions", e); mExceptionHandler.accept(e); } } /** * Enables the monitoring of Vehicle Map Service packets by this client. * *

If monitoring is enabled, the client will receive all packets, regardless of any * subscriptions. Enabling monitoring does not affect the client's existing subscriptions. */ @RequiresPermission(Car.PERMISSION_VMS_SUBSCRIBER) public void setMonitoringEnabled(boolean enabled) { if (DBG) Slog.d(TAG, "Setting monitoring state to " + enabled); try { mService.setMonitoringEnabled(mClientToken, enabled); synchronized (mLock) { mMonitoringEnabled = enabled; } } catch (RemoteException e) { Slog.e(TAG, "While setting monitoring state to " + enabled, e); mExceptionHandler.accept(e); } } /** * Returns the current monitoring state of the client. */ @RequiresPermission(Car.PERMISSION_VMS_SUBSCRIBER) public boolean isMonitoringEnabled() { synchronized (mLock) { return mMonitoringEnabled; } } /** * Returns the most recently received data layer availability. */ @NonNull @RequiresPermission(anyOf = {Car.PERMISSION_VMS_PUBLISHER, Car.PERMISSION_VMS_SUBSCRIBER}) public VmsAvailableLayers getAvailableLayers() { synchronized (mLock) { return mAvailableLayers; } } /** * Registers a data provider with the Vehicle Map Service. * * @param providerDescription Identifying information about the data provider * @return Provider ID to use for setting offerings and publishing packets, or -1 on * connection error */ @RequiresPermission(Car.PERMISSION_VMS_PUBLISHER) public int registerProvider(@NonNull byte[] providerDescription) { if (DBG) Slog.d(TAG, "Registering provider"); Objects.requireNonNull(providerDescription, "providerDescription cannot be null"); try { return mService.registerProvider(mClientToken, new VmsProviderInfo(providerDescription)); } catch (RemoteException e) { Slog.e(TAG, "While registering provider", e); mExceptionHandler.accept(e); return -1; } } /** * Unregisters a data provider with the Vehicle Map Service. * * @param providerId Provider ID */ @RequiresPermission(Car.PERMISSION_VMS_PUBLISHER) public void unregisterProvider(int providerId) { if (DBG) Slog.d(TAG, "Unregistering provider"); try { setProviderOfferings(providerId, Collections.emptySet()); } catch (IllegalArgumentException e) { Slog.e(TAG, "While unregistering provider " + providerId, e); } } /** * Sets the data layer offerings for a provider registered through * {@link #registerProvider(byte[])}. * *

Existing offerings for the provider ID will be replaced. * * @param providerId Provider ID * @param offerings Data layer offerings * @throws IllegalArgumentException if the client has not registered the provider */ @RequiresPermission(Car.PERMISSION_VMS_PUBLISHER) public void setProviderOfferings(int providerId, @NonNull Set offerings) { if (DBG) Slog.d(TAG, "Setting provider offerings for " + providerId); Objects.requireNonNull(offerings, "offerings cannot be null"); try { mService.setProviderOfferings(mClientToken, providerId, new ArrayList<>(offerings)); } catch (RemoteException e) { Slog.e(TAG, "While setting provider offerings for " + providerId, e); mExceptionHandler.accept(e); } } /** * Publishes a Vehicle Maps Service packet. * * @param providerId Packet provider * @param layer Packet layer * @param packet Packet data * @throws IllegalArgumentException if the client does not offer the layer as the provider */ @RequiresPermission(Car.PERMISSION_VMS_PUBLISHER) public void publishPacket(int providerId, @NonNull VmsLayer layer, @NonNull byte[] packet) { Objects.requireNonNull(layer, "layer cannot be null"); Objects.requireNonNull(packet, "packet cannot be null"); if (DBG) { Slog.d(TAG, "Publishing packet as " + providerId + " (" + packet.length + " bytes)"); } try { if (packet.length < LARGE_PACKET_THRESHOLD) { mService.publishPacket(mClientToken, providerId, layer, packet); } else { try (SharedMemory largePacket = packetToSharedMemory(packet)) { mService.publishLargePacket(mClientToken, providerId, layer, largePacket); } } } catch (RemoteException e) { Slog.e(TAG, "While publishing packet as " + providerId); mExceptionHandler.accept(e); } } /** * Returns the most recently received data layer subscription state. */ @NonNull @RequiresPermission(anyOf = {Car.PERMISSION_VMS_PUBLISHER, Car.PERMISSION_VMS_SUBSCRIBER}) public VmsSubscriptionState getSubscriptionState() { synchronized (mLock) { return mSubscriptionState; } } /** * Registers this client with the Vehicle Map Service. * * @hide */ public void register() throws RemoteException { VmsRegistrationInfo registrationInfo = mService.registerClient( mClientToken, mClientCallback, mLegacyClient); synchronized (mLock) { mAvailableLayers = registrationInfo.getAvailableLayers(); mSubscriptionState = registrationInfo.getSubscriptionState(); } } /** * Unregisters this client from the Vehicle Map Service. * * @hide */ public void unregister() throws RemoteException { mService.unregisterClient(mClientToken); } private static class IVmsClientCallbackImpl extends IVmsClientCallback.Stub { private final WeakReference mClient; private final boolean mAutoCloseMemory; private IVmsClientCallbackImpl(VmsClient client, boolean autoCloseMemory) { mClient = new WeakReference<>(client); mAutoCloseMemory = autoCloseMemory; } @Override public void onLayerAvailabilityChanged(VmsAvailableLayers availableLayers) { if (DBG) Slog.d(TAG, "Received new layer availability: " + availableLayers); executeCallback((client, callback) -> { synchronized (client.mLock) { client.mAvailableLayers = availableLayers; } callback.onLayerAvailabilityChanged(availableLayers); }); } @Override public void onSubscriptionStateChanged(VmsSubscriptionState subscriptionState) { if (DBG) Slog.d(TAG, "Received new subscription state: " + subscriptionState); executeCallback((client, callback) -> { synchronized (client.mLock) { client.mSubscriptionState = subscriptionState; } callback.onSubscriptionStateChanged(subscriptionState); }); } @Override public void onPacketReceived(int providerId, VmsLayer layer, byte[] packet) { if (DBG) { Slog.d(TAG, "Received packet from " + providerId + " for: " + layer + " (" + packet.length + " bytes)"); } executeCallback((client, callback) -> callback.onPacketReceived(providerId, layer, packet)); } @Override public void onLargePacketReceived(int providerId, VmsLayer layer, SharedMemory packet) { if (DBG) { Slog.d(TAG, "Received large packet from " + providerId + " for: " + layer + " (" + packet.getSize() + " bytes)"); } byte[] largePacket; if (mAutoCloseMemory) { try (SharedMemory autoClosedPacket = packet) { largePacket = sharedMemoryToPacket(autoClosedPacket); } } else { largePacket = sharedMemoryToPacket(packet); } executeCallback((client, callback) -> callback.onPacketReceived(providerId, layer, largePacket)); } private void executeCallback(BiConsumer callbackOperation) { final VmsClient client = mClient.get(); if (client == null) { Slog.w(TAG, "VmsClient unavailable"); return; } long token = Binder.clearCallingIdentity(); try { client.mExecutor.execute(() -> callbackOperation.accept(client, client.mCallback)); } finally { Binder.restoreCallingIdentity(token); } } } private static SharedMemory packetToSharedMemory(byte[] packet) { SharedMemory shm; try { shm = SharedMemory.create("VmsClient", packet.length); } catch (ErrnoException e) { throw new IllegalStateException("Failed to allocate shared memory", e); } ByteBuffer buffer = null; try { buffer = shm.mapReadWrite(); buffer.put(packet); } catch (ErrnoException e) { shm.close(); throw new IllegalStateException("Failed to create write buffer", e); } finally { if (buffer != null) { SharedMemory.unmap(buffer); } } if (!shm.setProtect(PROT_READ)) { shm.close(); throw new SecurityException("Failed to set read-only protection on shared memory"); } return shm; } private static byte[] sharedMemoryToPacket(SharedMemory shm) { ByteBuffer buffer; try { buffer = shm.mapReadOnly(); } catch (ErrnoException e) { throw new IllegalStateException("Failed to create read buffer", e); } byte[] packet; try { packet = new byte[buffer.capacity()]; buffer.get(packet); } finally { SharedMemory.unmap(buffer); } return packet; } }