From: Ugo Yu Date: Tue, 8 Jan 2019 01:00:09 +0000 (+0800) Subject: Skeleton implementation of Bluetooth metadata APIs X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=4a33b8876121e52c263c52ed2ed1ec30ae9e8eb2;p=android-x86%2Fframeworks-base.git Skeleton implementation of Bluetooth metadata APIs Bug: 121051445 Test: Build pass Change-Id: I5e80210205b37294b1eb8356502ebf242e627ce4 --- diff --git a/api/system-current.txt b/api/system-current.txt index ab45a22730c0..85d795df124f 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -729,19 +729,46 @@ package android.bluetooth { method @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean enableNoAutoConnect(); method public boolean isBleScanAlwaysAvailable(); method public boolean isLeEnabled(); + method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean registerMetadataListener(android.bluetooth.BluetoothDevice, android.bluetooth.BluetoothAdapter.MetadataListener, android.os.Handler); + method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean unregisterMetadataListener(android.bluetooth.BluetoothDevice); field public static final String ACTION_BLE_STATE_CHANGED = "android.bluetooth.adapter.action.BLE_STATE_CHANGED"; field public static final String ACTION_REQUEST_BLE_SCAN_ALWAYS_AVAILABLE = "android.bluetooth.adapter.action.REQUEST_BLE_SCAN_ALWAYS_AVAILABLE"; } + public abstract class BluetoothAdapter.MetadataListener { + ctor public BluetoothAdapter.MetadataListener(); + method public void onMetadataChanged(android.bluetooth.BluetoothDevice, int, String); + } + public final class BluetoothDevice implements android.os.Parcelable { method @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean cancelBondProcess(); + method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public String getMetadata(int); method @RequiresPermission(android.Manifest.permission.BLUETOOTH) public boolean isConnected(); method @RequiresPermission(android.Manifest.permission.BLUETOOTH) public boolean isEncrypted(); method @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean removeBond(); + method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean setMetadata(int, String); method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean setPhonebookAccessPermission(int); field public static final int ACCESS_ALLOWED = 1; // 0x1 field public static final int ACCESS_REJECTED = 2; // 0x2 field public static final int ACCESS_UNKNOWN = 0; // 0x0 + field public static final int METADATA_COMPANION_APP = 4; // 0x4 + field public static final int METADATA_ENHANCED_SETTINGS_UI_URI = 16; // 0x10 + field public static final int METADATA_HARDWARE_VERSION = 3; // 0x3 + field public static final int METADATA_IS_UNTHETHERED_HEADSET = 6; // 0x6 + field public static final int METADATA_MAIN_ICON = 5; // 0x5 + field public static final int METADATA_MANUFACTURER_NAME = 0; // 0x0 + field public static final int METADATA_MAX_LENGTH = 2048; // 0x800 + field public static final int METADATA_MODEL_NAME = 1; // 0x1 + field public static final int METADATA_SOFTWARE_VERSION = 2; // 0x2 + field public static final int METADATA_UNTHETHERED_CASE_BATTERY = 12; // 0xc + field public static final int METADATA_UNTHETHERED_CASE_CHARGING = 15; // 0xf + field public static final int METADATA_UNTHETHERED_CASE_ICON = 9; // 0x9 + field public static final int METADATA_UNTHETHERED_LEFT_BATTERY = 10; // 0xa + field public static final int METADATA_UNTHETHERED_LEFT_CHARGING = 13; // 0xd + field public static final int METADATA_UNTHETHERED_LEFT_ICON = 7; // 0x7 + field public static final int METADATA_UNTHETHERED_RIGHT_BATTERY = 11; // 0xb + field public static final int METADATA_UNTHETHERED_RIGHT_CHARGING = 14; // 0xe + field public static final int METADATA_UNTHETHERED_RIGHT_ICON = 8; // 0x8 } public final class BluetoothHeadset implements android.bluetooth.BluetoothProfile { diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 38245fb2ad24..e04aac49cfb5 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -36,6 +36,7 @@ import android.bluetooth.le.ScanSettings; import android.content.Context; import android.os.BatteryStats; import android.os.Binder; +import android.os.Handler; import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; @@ -648,6 +649,32 @@ public final class BluetoothAdapter { private final Object mLock = new Object(); private final Map mLeScanClients; + private static final Map>> + sMetadataListeners = new HashMap<>(); + + /** + * Bluetooth metadata listener. Overrides the default BluetoothMetadataListener + * implementation. + */ + private static final IBluetoothMetadataListener sBluetoothMetadataListener = + new IBluetoothMetadataListener.Stub() { + @Override + public void onMetadataChanged(BluetoothDevice device, int key, String value) { + synchronized (sMetadataListeners) { + if (sMetadataListeners.containsKey(device)) { + List> list = sMetadataListeners.get(device); + for (Pair pair : list) { + MetadataListener listener = pair.first; + Handler handler = pair.second; + handler.post(() -> { + listener.onMetadataChanged(device, key, value); + }); + } + } + } + return; + } + }; /** * Get a handle to the default local Bluetooth adapter. @@ -2607,6 +2634,16 @@ public final class BluetoothAdapter { } } } + synchronized (sMetadataListeners) { + sMetadataListeners.forEach((device, pair) -> { + try { + mService.registerMetadataListener(sBluetoothMetadataListener, + device); + } catch (RemoteException e) { + Log.e(TAG, "Failed to register metadata listener", e); + } + }); + } } public void onBluetoothServiceDown() { @@ -3090,4 +3127,142 @@ public final class BluetoothAdapter { + "listenUsingInsecureL2capChannel"); return listenUsingInsecureL2capChannel(); } + + /** + * Register a {@link #MetadataListener} to receive update about metadata + * changes for this {@link BluetoothDevice}. + * Registration must be done when Bluetooth is ON and will last until + * {@link #unregisterMetadataListener(BluetoothDevice)} is called, even when Bluetooth + * restarted in the middle. + * All input parameters should not be null or {@link NullPointerException} will be triggered. + * The same {@link BluetoothDevice} and {@link #MetadataListener} pair can only be registered + * once, double registration would cause {@link IllegalArgumentException}. + * + * @param device {@link BluetoothDevice} that will be registered + * @param listener {@link #MetadataListener} that will receive asynchronous callbacks + * @param handler the handler for listener callback + * @return true on success, false on error + * @throws NullPointerException If one of {@code listener}, {@code device} or {@code handler} + * is null. + * @throws IllegalArgumentException The same {@link #MetadataListener} and + * {@link BluetoothDevice} are registered twice. + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) + public boolean registerMetadataListener(BluetoothDevice device, MetadataListener listener, + Handler handler) { + if (DBG) Log.d(TAG, "registerMetdataListener()"); + + final IBluetooth service = mService; + if (service == null) { + Log.e(TAG, "Bluetooth is not enabled. Cannot register metadata listener"); + return false; + } + if (listener == null) { + throw new NullPointerException("listener is null"); + } + if (device == null) { + throw new NullPointerException("device is null"); + } + if (handler == null) { + throw new NullPointerException("handler is null"); + } + + synchronized (sMetadataListeners) { + List> listenerList = sMetadataListeners.get(device); + if (listenerList == null) { + // Create new listener/handler list for registeration + listenerList = new ArrayList<>(); + sMetadataListeners.put(device, listenerList); + } else { + // Check whether this device was already registed by the lisenter + if (listenerList.stream().anyMatch((pair) -> (pair.first.equals(listener)))) { + throw new IllegalArgumentException("listener was already regestered" + + " for the device"); + } + } + + Pair listenerPair = new Pair(listener, handler); + listenerList.add(listenerPair); + + boolean ret = false; + try { + ret = service.registerMetadataListener(sBluetoothMetadataListener, device); + } catch (RemoteException e) { + Log.e(TAG, "registerMetadataListener fail", e); + } finally { + if (!ret) { + // Remove listener registered earlier when fail. + listenerList.remove(listenerPair); + if (listenerList.isEmpty()) { + // Remove the device if its listener list is empty + sMetadataListeners.remove(device); + } + } + } + return ret; + } + } + + /** + * Unregister all {@link MetadataListener} from this {@link BluetoothDevice}. + * Unregistration can be done when Bluetooth is either ON or OFF. + * {@link #registerMetadataListener(MetadataListener, BluetoothDevice, Handler)} must + * be called before unregisteration. + * Unregistering a device that is not regestered would cause {@link IllegalArgumentException}. + * + * @param device {@link BluetoothDevice} that will be unregistered. it + * should not be null or {@link NullPointerException} will be triggered. + * @return true on success, false on error + * @throws NullPointerException If {@code device} is null. + * @throws IllegalArgumentException If {@code device} has not been registered before. + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) + public boolean unregisterMetadataListener(BluetoothDevice device) { + if (DBG) Log.d(TAG, "unregisterMetdataListener()"); + if (device == null) { + throw new NullPointerException("device is null"); + } + + synchronized (sMetadataListeners) { + if (sMetadataListeners.containsKey(device)) { + sMetadataListeners.remove(device); + } else { + throw new IllegalArgumentException("device was not registered"); + } + + final IBluetooth service = mService; + if (service == null) { + // Bluetooth is OFF, do nothing to Bluetooth service. + return true; + } + try { + return service.unregisterMetadataListener(device); + } catch (RemoteException e) { + Log.e(TAG, "unregisterMetadataListener fail", e); + return false; + } + } + } + + /** + * This abstract class is used to implement {@link BluetoothAdapter} metadata listener. + * @hide + */ + @SystemApi + public abstract class MetadataListener { + /** + * Callback triggered if the metadata of {@link BluetoothDevice} registered in + * {@link #registerMetadataListener}. + * + * @param device changed {@link BluetoothDevice}. + * @param key changed metadata key, one of BluetoothDevice.METADATA_*. + * @param value the new value of metadata. + */ + public void onMetadataChanged(BluetoothDevice device, int key, String value) { + } + } } diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index b2b02850e2e2..7a29c273dae3 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -341,6 +341,137 @@ public final class BluetoothDevice implements Parcelable { "android.bluetooth.device.action.SDP_RECORD"; /** + * Maximum length of a metadata entry, this is to avoid exploding Bluetooth + * disk usage + * @hide + */ + @SystemApi + public static final int METADATA_MAX_LENGTH = 2048; + + /** + * Manufacturer name of this Bluetooth device + * @hide + */ + @SystemApi + public static final int METADATA_MANUFACTURER_NAME = 0; + + /** + * Model name of this Bluetooth device + * @hide + */ + @SystemApi + public static final int METADATA_MODEL_NAME = 1; + + /** + * Software version of this Bluetooth device + * @hide + */ + @SystemApi + public static final int METADATA_SOFTWARE_VERSION = 2; + + /** + * Hardware version of this Bluetooth device + * @hide + */ + @SystemApi + public static final int METADATA_HARDWARE_VERSION = 3; + + /** + * Package name of the companion app, if any + * @hide + */ + @SystemApi + public static final int METADATA_COMPANION_APP = 4; + + /** + * URI to the main icon shown on the settings UI + * @hide + */ + @SystemApi + public static final int METADATA_MAIN_ICON = 5; + + /** + * Whether this device is an untethered headset with left, right and case + * @hide + */ + @SystemApi + public static final int METADATA_IS_UNTHETHERED_HEADSET = 6; + + /** + * URI to icon of the left headset + * @hide + */ + @SystemApi + public static final int METADATA_UNTHETHERED_LEFT_ICON = 7; + + /** + * URI to icon of the right headset + * @hide + */ + @SystemApi + public static final int METADATA_UNTHETHERED_RIGHT_ICON = 8; + + /** + * URI to icon of the headset charging case + * @hide + */ + @SystemApi + public static final int METADATA_UNTHETHERED_CASE_ICON = 9; + + /** + * Battery level (0-100), {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} + * is invalid, of the left headset + * @hide + */ + @SystemApi + public static final int METADATA_UNTHETHERED_LEFT_BATTERY = 10; + + /** + * Battery level (0-100), {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} + * is invalid, of the right headset + * @hide + */ + @SystemApi + public static final int METADATA_UNTHETHERED_RIGHT_BATTERY = 11; + + /** + * Battery level (0-100), {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} + * is invalid, of the headset charging case + * @hide + */ + @SystemApi + public static final int METADATA_UNTHETHERED_CASE_BATTERY = 12; + + /** + * Whether the left headset is charging + * @hide + */ + @SystemApi + public static final int METADATA_UNTHETHERED_LEFT_CHARGING = 13; + + /** + * Whether the right headset is charging + * @hide + */ + @SystemApi + public static final int METADATA_UNTHETHERED_RIGHT_CHARGING = 14; + + /** + * Whether the headset charging case is charging + * @hide + */ + @SystemApi + public static final int METADATA_UNTHETHERED_CASE_CHARGING = 15; + + /** + * URI to the enhanced settings UI slice, null or empty String means + * the UI does not exist + * @hide + */ + @SystemApi + public static final int METADATA_ENHANCED_SETTINGS_UI_URI = 16; + + /** * Broadcast Action: This intent is used to broadcast the {@link UUID} * wrapped as a {@link android.os.ParcelUuid} of the remote device after it * has been fetched. This intent is sent only when the UUIDs of the remote @@ -2028,4 +2159,61 @@ public final class BluetoothDevice implements Parcelable { Log.e(TAG, "createL2capCocSocket: PLEASE USE THE OFFICIAL API, createInsecureL2capChannel"); return createInsecureL2capChannel(psm); } + + /** + * Set a keyed metadata of this {@link BluetoothDevice} to a + * {@link String} value. + * Only bonded devices's metadata will be persisted across Bluetooth + * restart. + * Metadata will be removed when the device's bond state is moved to + * {@link #BOND_NONE}. + * + * @param key must be within the list of BluetoothDevice.METADATA_* + * @param value the string data to set for key. Must be less than + * {@link BluetoothAdapter#METADATA_MAX_LENGTH} characters in length + * @return true on success, false on error + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) + public boolean setMetadata(int key, String value) { + final IBluetooth service = sService; + if (service == null) { + Log.e(TAG, "Bluetooth is not enabled. Cannot set metadata"); + return false; + } + if (value.length() > METADATA_MAX_LENGTH) { + throw new IllegalArgumentException("value length is " + value.length() + + ", should not over " + METADATA_MAX_LENGTH); + } + try { + return service.setMetadata(this, key, value); + } catch (RemoteException e) { + Log.e(TAG, "setMetadata fail", e); + return false; + } + } + + /** + * Get a keyed metadata for this {@link BluetoothDevice} as {@link String} + * + * @param key must be within the list of BluetoothDevice.METADATA_* + * @return Metadata of the key as string, null on error or not found + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) + public String getMetadata(int key) { + final IBluetooth service = sService; + if (service == null) { + Log.e(TAG, "Bluetooth is not enabled. Cannot get metadata"); + return null; + } + try { + return service.getMetadata(this, key); + } catch (RemoteException e) { + Log.e(TAG, "getMetadata fail", e); + return null; + } + } }