| /* |
| * Copyright (C) 2022 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.nearby.provider; |
| |
| import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE; |
| |
| import static com.android.server.nearby.NearbyService.TAG; |
| |
| import android.annotation.Nullable; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.hardware.location.NanoAppMessage; |
| import android.nearby.DataElement; |
| import android.nearby.NearbyDevice; |
| import android.nearby.NearbyDeviceParcelable; |
| import android.nearby.PresenceDevice; |
| import android.nearby.PresenceScanFilter; |
| import android.nearby.PublicCredential; |
| import android.nearby.ScanFilter; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.server.nearby.NearbyConfiguration; |
| |
| import com.google.protobuf.ByteString; |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.Executor; |
| |
| import service.proto.Blefilter; |
| |
| /** Discovery provider that uses CHRE Nearby Nanoapp to do scanning. */ |
| public class ChreDiscoveryProvider extends AbstractDiscoveryProvider { |
| // Nanoapp ID reserved for Nearby Presence. |
| /** @hide */ |
| @VisibleForTesting |
| public static final long NANOAPP_ID = 0x476f6f676c001031L; |
| /** @hide */ |
| @VisibleForTesting |
| public static final int NANOAPP_MESSAGE_TYPE_FILTER = 3; |
| /** @hide */ |
| @VisibleForTesting |
| public static final int NANOAPP_MESSAGE_TYPE_FILTER_RESULT = 4; |
| /** @hide */ |
| @VisibleForTesting |
| public static final int NANOAPP_MESSAGE_TYPE_CONFIG = 5; |
| |
| private static final int FP_ACCOUNT_KEY_LENGTH = 16; |
| |
| private final ChreCommunication mChreCommunication; |
| private final ChreCallback mChreCallback; |
| private final Object mLock = new Object(); |
| |
| private boolean mChreStarted = false; |
| private Blefilter.BleFilters mFilters = null; |
| private Context mContext; |
| private NearbyConfiguration mNearbyConfiguration; |
| private final IntentFilter mIntentFilter; |
| // Null when the filters are never set |
| @GuardedBy("mLock") |
| @Nullable |
| private List<ScanFilter> mScanFilters; |
| |
| private final BroadcastReceiver mScreenBroadcastReceiver = |
| new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| Boolean screenOn = intent.getAction().equals(Intent.ACTION_SCREEN_ON) |
| || intent.getAction().equals(Intent.ACTION_USER_PRESENT); |
| Log.d(TAG, String.format( |
| "[ChreDiscoveryProvider] update nanoapp screen status: %B", screenOn)); |
| sendScreenUpdate(screenOn); |
| } |
| }; |
| |
| public ChreDiscoveryProvider( |
| Context context, ChreCommunication chreCommunication, Executor executor) { |
| super(context, executor); |
| mContext = context; |
| mChreCommunication = chreCommunication; |
| mChreCallback = new ChreCallback(); |
| mIntentFilter = new IntentFilter(); |
| } |
| |
| /** Initialize the CHRE discovery provider. */ |
| public void init() { |
| mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID)); |
| mNearbyConfiguration = new NearbyConfiguration(); |
| } |
| |
| @Override |
| protected void onStart() { |
| Log.d(TAG, "Start CHRE scan"); |
| synchronized (mLock) { |
| updateFiltersLocked(); |
| } |
| } |
| |
| @Override |
| protected void onStop() { |
| Log.d(TAG, "Stop CHRE scan"); |
| synchronized (mLock) { |
| if (mScanFilters != null) { |
| mScanFilters = null; |
| } |
| updateFiltersLocked(); |
| } |
| } |
| |
| @Override |
| protected void onSetScanFilters(List<ScanFilter> filters) { |
| synchronized (mLock) { |
| mScanFilters = filters == null ? null : List.copyOf(filters); |
| updateFiltersLocked(); |
| } |
| } |
| |
| /** |
| * @return {@code true} if CHRE is available and {@code null} when CHRE availability result |
| * has not been returned |
| */ |
| @Nullable |
| public Boolean available() { |
| return mChreCommunication.available(); |
| } |
| |
| @VisibleForTesting |
| List<ScanFilter> getFiltersLocked() { |
| synchronized (mLock) { |
| return mScanFilters == null ? null : List.copyOf(mScanFilters); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void updateFiltersLocked() { |
| if (mScanFilters == null) { |
| Log.e(TAG, "ScanFilters not set."); |
| return; |
| } |
| Blefilter.BleFilters.Builder filtersBuilder = Blefilter.BleFilters.newBuilder(); |
| for (ScanFilter scanFilter : mScanFilters) { |
| PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter; |
| Blefilter.BleFilter.Builder filterBuilder = Blefilter.BleFilter.newBuilder(); |
| for (PublicCredential credential : presenceScanFilter.getCredentials()) { |
| filterBuilder.addCertificate(toProtoPublicCredential(credential)); |
| } |
| for (DataElement dataElement : presenceScanFilter.getExtendedProperties()) { |
| if (dataElement.getKey() == DataElement.DataType.ACCOUNT_KEY_DATA) { |
| filterBuilder.addDataElement(toProtoDataElement(dataElement)); |
| } else if (mNearbyConfiguration.isTestAppSupported() |
| && DataElement.isTestDeType(dataElement.getKey())) { |
| filterBuilder.addDataElement(toProtoDataElement(dataElement)); |
| } |
| } |
| if (!presenceScanFilter.getPresenceActions().isEmpty()) { |
| filterBuilder.setIntent(presenceScanFilter.getPresenceActions().get(0)); |
| } |
| filtersBuilder.addFilter(filterBuilder.build()); |
| } |
| if (mChreStarted) { |
| sendFilters(filtersBuilder.build()); |
| mFilters = null; |
| } |
| } |
| |
| private Blefilter.PublicateCertificate toProtoPublicCredential(PublicCredential credential) { |
| Log.d(TAG, String.format("Returns a PublicCertificate with authenticity key size %d and" |
| + " encrypted metadata key tag size %d", |
| credential.getAuthenticityKey().length, |
| credential.getEncryptedMetadataKeyTag().length)); |
| return Blefilter.PublicateCertificate.newBuilder() |
| .setAuthenticityKey(ByteString.copyFrom(credential.getAuthenticityKey())) |
| .setMetadataEncryptionKeyTag( |
| ByteString.copyFrom(credential.getEncryptedMetadataKeyTag())) |
| .build(); |
| } |
| |
| private Blefilter.DataElement toProtoDataElement(DataElement dataElement) { |
| return Blefilter.DataElement.newBuilder() |
| .setKey(Arrays.stream(Blefilter.DataElement.ElementType.values()) |
| .filter(p -> p.getNumber() == dataElement.getKey()) |
| .findFirst() |
| .get()) |
| .setValue(ByteString.copyFrom(dataElement.getValue())) |
| .setValueLength(dataElement.getValue().length) |
| .build(); |
| } |
| |
| private void sendFilters(Blefilter.BleFilters filters) { |
| NanoAppMessage message = |
| NanoAppMessage.createMessageToNanoApp( |
| NANOAPP_ID, NANOAPP_MESSAGE_TYPE_FILTER, filters.toByteArray()); |
| if (!mChreCommunication.sendMessageToNanoApp(message)) { |
| Log.e(TAG, "Failed to send filters to CHRE."); |
| } |
| } |
| |
| private void sendScreenUpdate(Boolean screenOn) { |
| Blefilter.BleConfig config = Blefilter.BleConfig.newBuilder().setScreenOn(screenOn).build(); |
| NanoAppMessage message = |
| NanoAppMessage.createMessageToNanoApp( |
| NANOAPP_ID, NANOAPP_MESSAGE_TYPE_CONFIG, config.toByteArray()); |
| if (!mChreCommunication.sendMessageToNanoApp(message)) { |
| Log.e(TAG, "Failed to send config to CHRE."); |
| } |
| } |
| |
| private class ChreCallback implements ChreCommunication.ContextHubCommsCallback { |
| |
| @Override |
| public void started(boolean success) { |
| if (success) { |
| synchronized (ChreDiscoveryProvider.this) { |
| Log.i(TAG, "CHRE communication started"); |
| mIntentFilter.addAction(Intent.ACTION_SCREEN_ON); |
| mIntentFilter.addAction(Intent.ACTION_USER_PRESENT); |
| mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF); |
| mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter); |
| mChreStarted = true; |
| if (mFilters != null) { |
| sendFilters(mFilters); |
| mFilters = null; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void onHubReset() { |
| // TODO(b/221082271): hooked with upper level codes. |
| Log.i(TAG, "CHRE reset."); |
| } |
| |
| @Override |
| public void onNanoAppRestart(long nanoAppId) { |
| // TODO(b/221082271): hooked with upper level codes. |
| Log.i(TAG, String.format("CHRE NanoApp %d restart.", nanoAppId)); |
| } |
| |
| @Override |
| public void onMessageFromNanoApp(NanoAppMessage message) { |
| if (message.getNanoAppId() != NANOAPP_ID) { |
| Log.e(TAG, "Received message from unknown nano app."); |
| return; |
| } |
| if (mListener == null) { |
| Log.e(TAG, "the listener is not set in ChreDiscoveryProvider."); |
| return; |
| } |
| if (message.getMessageType() == NANOAPP_MESSAGE_TYPE_FILTER_RESULT) { |
| try { |
| Blefilter.BleFilterResults results = |
| Blefilter.BleFilterResults.parseFrom(message.getMessageBody()); |
| for (Blefilter.BleFilterResult filterResult : results.getResultList()) { |
| // TODO(b/234653356): There are some duplicate fields set both in |
| // PresenceDevice and NearbyDeviceParcelable, cleanup is needed. |
| byte[] salt = {1}; |
| byte[] secretId = {1}; |
| byte[] authenticityKey = {1}; |
| byte[] publicKey = {1}; |
| byte[] encryptedMetaData = {1}; |
| byte[] encryptedMetaDataTag = {1}; |
| if (filterResult.hasPublicCredential()) { |
| Blefilter.PublicCredential credential = |
| filterResult.getPublicCredential(); |
| secretId = credential.getSecretId().toByteArray(); |
| authenticityKey = credential.getAuthenticityKey().toByteArray(); |
| publicKey = credential.getPublicKey().toByteArray(); |
| encryptedMetaData = credential.getEncryptedMetadata().toByteArray(); |
| encryptedMetaDataTag = |
| credential.getEncryptedMetadataTag().toByteArray(); |
| } |
| PresenceDevice.Builder presenceDeviceBuilder = |
| new PresenceDevice.Builder( |
| String.valueOf(filterResult.hashCode()), |
| salt, |
| secretId, |
| encryptedMetaData) |
| .setRssi(filterResult.getRssi()) |
| .addMedium(NearbyDevice.Medium.BLE); |
| // Data Elements reported from nanoapp added to Data Elements. |
| // i.e. Fast Pair account keys, connection status and battery |
| for (Blefilter.DataElement element : filterResult.getDataElementList()) { |
| addDataElementsToPresenceDevice(element, presenceDeviceBuilder); |
| } |
| // BlE address appended to Data Element. |
| if (filterResult.hasBluetoothAddress()) { |
| presenceDeviceBuilder.addExtendedProperty( |
| new DataElement( |
| DataElement.DataType.BLE_ADDRESS, |
| filterResult.getBluetoothAddress().toByteArray())); |
| } |
| // BlE TX Power appended to Data Element. |
| if (filterResult.hasTxPower()) { |
| presenceDeviceBuilder.addExtendedProperty( |
| new DataElement( |
| DataElement.DataType.TX_POWER, |
| new byte[]{(byte) filterResult.getTxPower()})); |
| } |
| // BLE Service data appended to Data Elements. |
| if (filterResult.hasBleServiceData()) { |
| // Retrieves the length of the service data from the first byte, |
| // and then skips the first byte and returns data[1 .. dataLength) |
| // as the DataElement value. |
| int dataLength = Byte.toUnsignedInt( |
| filterResult.getBleServiceData().byteAt(0)); |
| presenceDeviceBuilder.addExtendedProperty( |
| new DataElement( |
| DataElement.DataType.BLE_SERVICE_DATA, |
| filterResult.getBleServiceData() |
| .substring(1, 1 + dataLength).toByteArray())); |
| } |
| // Add action |
| if (filterResult.hasIntent()) { |
| presenceDeviceBuilder.addExtendedProperty( |
| new DataElement( |
| DataElement.DataType.ACTION, |
| new byte[]{(byte) filterResult.getIntent()})); |
| } |
| |
| PublicCredential publicCredential = |
| new PublicCredential.Builder( |
| secretId, |
| authenticityKey, |
| publicKey, |
| encryptedMetaData, |
| encryptedMetaDataTag) |
| .build(); |
| |
| NearbyDeviceParcelable device = |
| new NearbyDeviceParcelable.Builder() |
| .setScanType(SCAN_TYPE_NEARBY_PRESENCE) |
| .setMedium(NearbyDevice.Medium.BLE) |
| .setTxPower(filterResult.getTxPower()) |
| .setRssi(filterResult.getRssi()) |
| .setAction(filterResult.getIntent()) |
| .setPublicCredential(publicCredential) |
| .setPresenceDevice(presenceDeviceBuilder.build()) |
| .setEncryptionKeyTag(encryptedMetaDataTag) |
| .build(); |
| mExecutor.execute(() -> mListener.onNearbyDeviceDiscovered(device)); |
| } |
| } catch (Exception e) { |
| Log.e(TAG, String.format("Failed to decode the filter result %s", e)); |
| } |
| } |
| } |
| |
| private void addDataElementsToPresenceDevice(Blefilter.DataElement element, |
| PresenceDevice.Builder presenceDeviceBuilder) { |
| int endIndex = element.hasValueLength() ? element.getValueLength() : |
| element.getValue().size(); |
| switch (element.getKey()) { |
| case DE_FAST_PAIR_ACCOUNT_KEY: |
| presenceDeviceBuilder.addExtendedProperty( |
| new DataElement(DataElement.DataType.ACCOUNT_KEY_DATA, |
| element.getValue().substring(0, endIndex).toByteArray())); |
| break; |
| case DE_CONNECTION_STATUS: |
| presenceDeviceBuilder.addExtendedProperty( |
| new DataElement(DataElement.DataType.CONNECTION_STATUS, |
| element.getValue().substring(0, endIndex).toByteArray())); |
| break; |
| case DE_BATTERY_STATUS: |
| presenceDeviceBuilder.addExtendedProperty( |
| new DataElement(DataElement.DataType.BATTERY, |
| element.getValue().substring(0, endIndex).toByteArray())); |
| break; |
| default: |
| if (mNearbyConfiguration.isTestAppSupported() |
| && DataElement.isTestDeType(element.getKey().getNumber())) { |
| presenceDeviceBuilder.addExtendedProperty( |
| new DataElement(Arrays.stream( |
| Blefilter.DataElement.ElementType.values()) |
| .filter(p -> p.getNumber() == element.getKey().getNumber()) |
| .findFirst() |
| .get().getNumber(), |
| element.getValue().substring(0, endIndex).toByteArray())); |
| } |
| break; |
| } |
| } |
| } |
| } |