RemoteDevices.java revision c4fbd756e2645147470c486ae96f2253f5e13a52
1/*
2 * Copyright (C) 2012-2014 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
17package com.android.bluetooth.btservice;
18
19import android.bluetooth.BluetoothAdapter;
20import android.bluetooth.BluetoothAssignedNumbers;
21import android.bluetooth.BluetoothClass;
22import android.bluetooth.BluetoothDevice;
23import android.bluetooth.BluetoothHeadset;
24import android.bluetooth.BluetoothProfile;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.os.Handler;
30import android.os.Message;
31import android.os.ParcelUuid;
32import android.support.annotation.VisibleForTesting;
33import android.util.Log;
34
35import com.android.bluetooth.R;
36import com.android.bluetooth.Utils;
37import com.android.bluetooth.hfp.HeadsetHalConstants;
38
39import java.util.ArrayList;
40import java.util.HashMap;
41import java.util.LinkedList;
42import java.util.Queue;
43
44final class RemoteDevices {
45    private static final boolean DBG = false;
46    private static final String TAG = "BluetoothRemoteDevices";
47
48    // Maximum number of device properties to remember
49    private static final int MAX_DEVICE_QUEUE_SIZE = 200;
50
51    private static BluetoothAdapter sAdapter;
52    private static AdapterService sAdapterService;
53    private static ArrayList<BluetoothDevice> sSdpTracker;
54    private final Object mObject = new Object();
55
56    private static final int UUID_INTENT_DELAY = 6000;
57    private static final int MESSAGE_UUID_INTENT = 1;
58
59    private final HashMap<String, DeviceProperties> mDevices;
60    private Queue<String> mDeviceQueue;
61
62    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
63        @Override
64        public void onReceive(Context context, Intent intent) {
65            String action = intent.getAction();
66            switch (action) {
67                case BluetoothHeadset.ACTION_HF_INDICATORS_VALUE_CHANGED:
68                    onHfIndicatorValueChanged(intent);
69                    break;
70                case BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT:
71                    onVendorSpecificHeadsetEvent(intent);
72                    break;
73                case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
74                    onHeadsetConnectionStateChanged(intent);
75                    break;
76                default:
77                    Log.w(TAG, "Unhandled intent: " + intent);
78                    break;
79            }
80        }
81    };
82
83    RemoteDevices(AdapterService service) {
84        sAdapter = BluetoothAdapter.getDefaultAdapter();
85        sAdapterService = service;
86        sSdpTracker = new ArrayList<BluetoothDevice>();
87        mDevices = new HashMap<String, DeviceProperties>();
88        mDeviceQueue = new LinkedList<String>();
89    }
90
91    /**
92     * Init should be called before using this RemoteDevices object
93     */
94    void init() {
95        IntentFilter filter = new IntentFilter();
96        filter.addAction(BluetoothHeadset.ACTION_HF_INDICATORS_VALUE_CHANGED);
97        filter.addAction(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT);
98        filter.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "."
99                + BluetoothAssignedNumbers.PLANTRONICS);
100        filter.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "."
101                + BluetoothAssignedNumbers.APPLE);
102        filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
103        sAdapterService.registerReceiver(mReceiver, filter);
104    }
105
106    /**
107     * Clean up should be called when this object is no longer needed, must be called after init()
108     */
109    void cleanup() {
110        // Unregister receiver first, mAdapterService is never null
111        sAdapterService.unregisterReceiver(mReceiver);
112        reset();
113    }
114
115    /**
116     * Reset should be called when the state of this object needs to be cleared
117     * RemoteDevices is still usable after reset
118     */
119    void reset() {
120        if (sSdpTracker != null) {
121            sSdpTracker.clear();
122        }
123
124        if (mDevices != null) {
125            mDevices.clear();
126        }
127
128        if (mDeviceQueue != null) {
129            mDeviceQueue.clear();
130        }
131    }
132
133    @Override
134    public Object clone() throws CloneNotSupportedException {
135        throw new CloneNotSupportedException();
136    }
137
138    DeviceProperties getDeviceProperties(BluetoothDevice device) {
139        synchronized (mDevices) {
140            return mDevices.get(device.getAddress());
141        }
142    }
143
144    BluetoothDevice getDevice(byte[] address) {
145        DeviceProperties prop = mDevices.get(Utils.getAddressStringFromByte(address));
146        if (prop == null) {
147            return null;
148        }
149        return prop.getDevice();
150    }
151
152    DeviceProperties addDeviceProperties(byte[] address) {
153        synchronized (mDevices) {
154            DeviceProperties prop = new DeviceProperties();
155            prop.mDevice = sAdapter.getRemoteDevice(Utils.getAddressStringFromByte(address));
156            prop.mAddress = address;
157            String key = Utils.getAddressStringFromByte(address);
158            DeviceProperties pv = mDevices.put(key, prop);
159
160            if (pv == null) {
161                mDeviceQueue.offer(key);
162                if (mDeviceQueue.size() > MAX_DEVICE_QUEUE_SIZE) {
163                    String deleteKey = mDeviceQueue.poll();
164                    for (BluetoothDevice device : sAdapterService.getBondedDevices()) {
165                        if (device.getAddress().equals(deleteKey)) {
166                            return prop;
167                        }
168                    }
169                    debugLog("Removing device " + deleteKey + " from property map");
170                    mDevices.remove(deleteKey);
171                }
172            }
173            return prop;
174        }
175    }
176
177    class DeviceProperties {
178        private String mName;
179        private byte[] mAddress;
180        private int mBluetoothClass = BluetoothClass.Device.Major.UNCATEGORIZED;
181        private short mRssi;
182        private ParcelUuid[] mUuids;
183        private int mDeviceType;
184        private String mAlias;
185        private int mBondState;
186        private BluetoothDevice mDevice;
187        private boolean mIsBondingInitiatedLocally;
188        private int mBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
189
190        DeviceProperties() {
191            mBondState = BluetoothDevice.BOND_NONE;
192        }
193
194        /**
195         * @return the mName
196         */
197        String getName() {
198            synchronized (mObject) {
199                return mName;
200            }
201        }
202
203        /**
204         * @return the mClass
205         */
206        int getBluetoothClass() {
207            synchronized (mObject) {
208                return mBluetoothClass;
209            }
210        }
211
212        /**
213         * @return the mUuids
214         */
215        ParcelUuid[] getUuids() {
216            synchronized (mObject) {
217                return mUuids;
218            }
219        }
220
221        /**
222         * @return the mAddress
223         */
224        byte[] getAddress() {
225            synchronized (mObject) {
226                return mAddress;
227            }
228        }
229
230        /**
231         * @return the mDevice
232         */
233        BluetoothDevice getDevice() {
234            synchronized (mObject) {
235                return mDevice;
236            }
237        }
238
239        /**
240         * @return mRssi
241         */
242        short getRssi() {
243            synchronized (mObject) {
244                return mRssi;
245            }
246        }
247
248        /**
249         * @return mDeviceType
250         */
251        int getDeviceType() {
252            synchronized (mObject) {
253                return mDeviceType;
254            }
255        }
256
257        /**
258         * @return the mAlias
259         */
260        String getAlias() {
261            synchronized (mObject) {
262                return mAlias;
263            }
264        }
265
266        /**
267         * @param mAlias the mAlias to set
268         */
269        void setAlias(BluetoothDevice device, String mAlias) {
270            synchronized (mObject) {
271                this.mAlias = mAlias;
272                sAdapterService.setDevicePropertyNative(mAddress,
273                        AbstractionLayer.BT_PROPERTY_REMOTE_FRIENDLY_NAME, mAlias.getBytes());
274                Intent intent = new Intent(BluetoothDevice.ACTION_ALIAS_CHANGED);
275                intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
276                intent.putExtra(BluetoothDevice.EXTRA_NAME, mAlias);
277                sAdapterService.sendBroadcast(intent, AdapterService.BLUETOOTH_PERM);
278            }
279        }
280
281        /**
282         * @param mBondState the mBondState to set
283         */
284        void setBondState(int mBondState) {
285            synchronized (mObject) {
286                this.mBondState = mBondState;
287                if (mBondState == BluetoothDevice.BOND_NONE) {
288                    /* Clearing the Uuids local copy when the device is unpaired. If not cleared,
289                    cachedBluetoothDevice issued a connect using the local cached copy of uuids,
290                    without waiting for the ACTION_UUID intent.
291                    This was resulting in multiple calls to connect().*/
292                    mUuids = null;
293                }
294            }
295        }
296
297        /**
298         * @return the mBondState
299         */
300        int getBondState() {
301            synchronized (mObject) {
302                return mBondState;
303            }
304        }
305
306        /**
307         * @param isBondingInitiatedLocally wether bonding is initiated locally
308         */
309        void setBondingInitiatedLocally(boolean isBondingInitiatedLocally) {
310            synchronized (mObject) {
311                this.mIsBondingInitiatedLocally = isBondingInitiatedLocally;
312            }
313        }
314
315        /**
316         * @return the isBondingInitiatedLocally
317         */
318        boolean isBondingInitiatedLocally() {
319            synchronized (mObject) {
320                return mIsBondingInitiatedLocally;
321            }
322        }
323
324        int getBatteryLevel() {
325            synchronized (mObject) {
326                return mBatteryLevel;
327            }
328        }
329
330        /**
331         * @param batteryLevel the mBatteryLevel to set
332         */
333        void setBatteryLevel(int batteryLevel) {
334            synchronized (mObject) {
335                this.mBatteryLevel = batteryLevel;
336            }
337        }
338    }
339
340    private void sendUuidIntent(BluetoothDevice device) {
341        DeviceProperties prop = getDeviceProperties(device);
342        Intent intent = new Intent(BluetoothDevice.ACTION_UUID);
343        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
344        intent.putExtra(BluetoothDevice.EXTRA_UUID, prop == null ? null : prop.mUuids);
345        sAdapterService.sendBroadcast(intent, AdapterService.BLUETOOTH_ADMIN_PERM);
346
347        //Remove the outstanding UUID request
348        sSdpTracker.remove(device);
349    }
350
351    /**
352     * When bonding is initiated to remote device that we have never seen, i.e Out Of Band pairing,
353     * we must add device first before setting it's properties. This is a helper method for doing
354     * that.
355     */
356    void setBondingInitiatedLocally(byte[] address) {
357        DeviceProperties properties;
358
359        BluetoothDevice device = getDevice(address);
360        if (device == null) {
361            properties = addDeviceProperties(address);
362        } else {
363            properties = getDeviceProperties(device);
364        }
365
366        properties.setBondingInitiatedLocally(true);
367    }
368
369    /**
370     * Update battery level in device properties
371     * @param device The remote device to be updated
372     * @param batteryLevel Battery level Indicator between 0-100,
373     *                    {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} is error
374     */
375    @VisibleForTesting
376    void updateBatteryLevel(BluetoothDevice device, int batteryLevel) {
377        if (device == null || batteryLevel < 0 || batteryLevel > 100) {
378            warnLog("Invalid parameters device=" + String.valueOf(device == null)
379                    + ", batteryLevel=" + String.valueOf(batteryLevel));
380            return;
381        }
382        DeviceProperties deviceProperties = getDeviceProperties(device);
383        if (deviceProperties == null) {
384            deviceProperties = addDeviceProperties(Utils.getByteAddress(device));
385        }
386        synchronized (mObject) {
387            int currentBatteryLevel = deviceProperties.getBatteryLevel();
388            if (batteryLevel == currentBatteryLevel) {
389                debugLog("Same battery level for device " + device + " received " + String.valueOf(
390                        batteryLevel) + "%");
391                return;
392            }
393            deviceProperties.setBatteryLevel(batteryLevel);
394        }
395        sendBatteryLevelChangedBroadcast(device, batteryLevel);
396        Log.d(TAG, "Updated device " + device + " battery level to " + batteryLevel + "%");
397    }
398
399    /**
400     * Reset battery level property to {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} for a device
401     * @param device device whose battery level property needs to be reset
402     */
403    @VisibleForTesting
404    void resetBatteryLevel(BluetoothDevice device) {
405        if (device == null) {
406            warnLog("Device is null");
407            return;
408        }
409        DeviceProperties deviceProperties = getDeviceProperties(device);
410        if (deviceProperties == null) {
411            return;
412        }
413        synchronized (mObject) {
414            if (deviceProperties.getBatteryLevel() == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
415                debugLog("Battery level was never set or is already reset, device=" + device);
416                return;
417            }
418            deviceProperties.setBatteryLevel(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
419        }
420        sendBatteryLevelChangedBroadcast(device, BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
421        Log.d(TAG, "Reset battery level, device=" + device);
422    }
423
424    private void sendBatteryLevelChangedBroadcast(BluetoothDevice device, int batteryLevel) {
425        Intent intent = new Intent(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED);
426        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
427        intent.putExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL, batteryLevel);
428        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
429        sAdapterService.sendBroadcast(intent, AdapterService.BLUETOOTH_PERM);
430    }
431
432    void devicePropertyChangedCallback(byte[] address, int[] types, byte[][] values) {
433        Intent intent;
434        byte[] val;
435        int type;
436        BluetoothDevice bdDevice = getDevice(address);
437        DeviceProperties device;
438        if (bdDevice == null) {
439            debugLog("Added new device property");
440            device = addDeviceProperties(address);
441            bdDevice = getDevice(address);
442        } else {
443            device = getDeviceProperties(bdDevice);
444        }
445
446        if (types.length <= 0) {
447            errorLog("No properties to update");
448            return;
449        }
450
451        for (int j = 0; j < types.length; j++) {
452            type = types[j];
453            val = values[j];
454            if (val.length > 0) {
455                synchronized (mObject) {
456                    debugLog("Property type: " + type);
457                    switch (type) {
458                        case AbstractionLayer.BT_PROPERTY_BDNAME:
459                            device.mName = new String(val);
460                            intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED);
461                            intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice);
462                            intent.putExtra(BluetoothDevice.EXTRA_NAME, device.mName);
463                            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
464                            sAdapterService.sendBroadcast(intent, sAdapterService.BLUETOOTH_PERM);
465                            debugLog("Remote Device name is: " + device.mName);
466                            break;
467                        case AbstractionLayer.BT_PROPERTY_REMOTE_FRIENDLY_NAME:
468                            if (device.mAlias != null) {
469                                System.arraycopy(val, 0, device.mAlias, 0, val.length);
470                            } else {
471                                device.mAlias = new String(val);
472                            }
473                            break;
474                        case AbstractionLayer.BT_PROPERTY_BDADDR:
475                            device.mAddress = val;
476                            debugLog("Remote Address is:" + Utils.getAddressStringFromByte(val));
477                            break;
478                        case AbstractionLayer.BT_PROPERTY_CLASS_OF_DEVICE:
479                            device.mBluetoothClass = Utils.byteArrayToInt(val);
480                            intent = new Intent(BluetoothDevice.ACTION_CLASS_CHANGED);
481                            intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice);
482                            intent.putExtra(BluetoothDevice.EXTRA_CLASS,
483                                    new BluetoothClass(device.mBluetoothClass));
484                            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
485                            sAdapterService.sendBroadcast(intent, sAdapterService.BLUETOOTH_PERM);
486                            debugLog("Remote class is:" + device.mBluetoothClass);
487                            break;
488                        case AbstractionLayer.BT_PROPERTY_UUIDS:
489                            int numUuids = val.length / AbstractionLayer.BT_UUID_SIZE;
490                            device.mUuids = Utils.byteArrayToUuid(val);
491                            if (sAdapterService.getState() == BluetoothAdapter.STATE_ON) {
492                                sendUuidIntent(bdDevice);
493                            }
494                            break;
495                        case AbstractionLayer.BT_PROPERTY_TYPE_OF_DEVICE:
496                            // The device type from hal layer, defined in bluetooth.h,
497                            // matches the type defined in BluetoothDevice.java
498                            device.mDeviceType = Utils.byteArrayToInt(val);
499                            break;
500                        case AbstractionLayer.BT_PROPERTY_REMOTE_RSSI:
501                            // RSSI from hal is in one byte
502                            device.mRssi = val[0];
503                            break;
504                    }
505                }
506            }
507        }
508    }
509
510    void deviceFoundCallback(byte[] address) {
511        // The device properties are already registered - we can send the intent
512        // now
513        BluetoothDevice device = getDevice(address);
514        debugLog("deviceFoundCallback: Remote Address is:" + device);
515        DeviceProperties deviceProp = getDeviceProperties(device);
516        if (deviceProp == null) {
517            errorLog("Device Properties is null for Device:" + device);
518            return;
519        }
520
521        Intent intent = new Intent(BluetoothDevice.ACTION_FOUND);
522        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
523        intent.putExtra(BluetoothDevice.EXTRA_CLASS,
524                new BluetoothClass(deviceProp.mBluetoothClass));
525        intent.putExtra(BluetoothDevice.EXTRA_RSSI, deviceProp.mRssi);
526        intent.putExtra(BluetoothDevice.EXTRA_NAME, deviceProp.mName);
527
528        sAdapterService.sendBroadcastMultiplePermissions(intent, new String[]{
529                AdapterService.BLUETOOTH_PERM, android.Manifest.permission.ACCESS_COARSE_LOCATION
530        });
531    }
532
533    void aclStateChangeCallback(int status, byte[] address, int newState) {
534        BluetoothDevice device = getDevice(address);
535
536        if (device == null) {
537            errorLog("aclStateChangeCallback: device is NULL, address="
538                    + Utils.getAddressStringFromByte(address) + ", newState=" + newState);
539            return;
540        }
541        int state = sAdapterService.getState();
542
543        Intent intent = null;
544        if (newState == AbstractionLayer.BT_ACL_STATE_CONNECTED) {
545            if (state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_TURNING_ON) {
546                intent = new Intent(BluetoothDevice.ACTION_ACL_CONNECTED);
547            } else if (state == BluetoothAdapter.STATE_BLE_ON
548                    || state == BluetoothAdapter.STATE_BLE_TURNING_ON) {
549                intent = new Intent(BluetoothAdapter.ACTION_BLE_ACL_CONNECTED);
550            }
551            debugLog(
552                    "aclStateChangeCallback: Adapter State: " + BluetoothAdapter.nameForState(state)
553                            + " Connected: " + device);
554        } else {
555            if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
556                // Send PAIRING_CANCEL intent to dismiss any dialog requesting bonding.
557                intent = new Intent(BluetoothDevice.ACTION_PAIRING_CANCEL);
558                intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
559                intent.setPackage(sAdapterService.getString(R.string.pairing_ui_package));
560                sAdapterService.sendBroadcast(intent, sAdapterService.BLUETOOTH_PERM);
561            }
562            if (state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_TURNING_OFF) {
563                intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECTED);
564            } else if (state == BluetoothAdapter.STATE_BLE_ON
565                    || state == BluetoothAdapter.STATE_BLE_TURNING_OFF) {
566                intent = new Intent(BluetoothAdapter.ACTION_BLE_ACL_DISCONNECTED);
567            }
568            // Reset battery level on complete disconnection
569            if (sAdapterService.getConnectionState(device) == 0) {
570                resetBatteryLevel(device);
571            }
572            debugLog(
573                    "aclStateChangeCallback: Adapter State: " + BluetoothAdapter.nameForState(state)
574                            + " Disconnected: " + device);
575        }
576
577        if (intent != null) {
578            intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
579            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
580                    | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
581            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
582            sAdapterService.sendBroadcast(intent, sAdapterService.BLUETOOTH_PERM);
583        } else {
584            Log.e(TAG, "aclStateChangeCallback intent is null. deviceBondState: "
585                    + device.getBondState());
586        }
587    }
588
589
590    void fetchUuids(BluetoothDevice device) {
591        if (sSdpTracker.contains(device)) {
592            return;
593        }
594        sSdpTracker.add(device);
595
596        Message message = mHandler.obtainMessage(MESSAGE_UUID_INTENT);
597        message.obj = device;
598        mHandler.sendMessageDelayed(message, UUID_INTENT_DELAY);
599
600        sAdapterService.getRemoteServicesNative(Utils.getBytesFromAddress(device.getAddress()));
601    }
602
603    void updateUuids(BluetoothDevice device) {
604        Message message = mHandler.obtainMessage(MESSAGE_UUID_INTENT);
605        message.obj = device;
606        mHandler.sendMessage(message);
607    }
608
609    /**
610     * Handles headset connection state change event
611     * @param intent must be {@link BluetoothHeadset#ACTION_CONNECTION_STATE_CHANGED} intent
612     */
613    @VisibleForTesting
614    void onHeadsetConnectionStateChanged(Intent intent) {
615        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
616        if (device == null) {
617            Log.e(TAG, "onHeadsetConnectionStateChanged() remote device is null");
618            return;
619        }
620        if (intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED)
621                == BluetoothProfile.STATE_DISCONNECTED) {
622            // TODO: Rework this when non-HFP sources of battery level indication is added
623            resetBatteryLevel(device);
624        }
625    }
626
627    @VisibleForTesting
628    void onHfIndicatorValueChanged(Intent intent) {
629        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
630        if (device == null) {
631            Log.e(TAG, "onHfIndicatorValueChanged() remote device is null");
632            return;
633        }
634        int indicatorId = intent.getIntExtra(BluetoothHeadset.EXTRA_HF_INDICATORS_IND_ID, -1);
635        int indicatorValue = intent.getIntExtra(BluetoothHeadset.EXTRA_HF_INDICATORS_IND_VALUE, -1);
636        if (indicatorId == HeadsetHalConstants.HF_INDICATOR_BATTERY_LEVEL_STATUS) {
637            updateBatteryLevel(device, indicatorValue);
638        }
639    }
640
641    /**
642     * Handle {@link BluetoothHeadset#ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intent
643     * @param intent must be {@link BluetoothHeadset#ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intent
644     */
645    @VisibleForTesting
646    void onVendorSpecificHeadsetEvent(Intent intent) {
647        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
648        if (device == null) {
649            Log.e(TAG, "onVendorSpecificHeadsetEvent() remote device is null");
650            return;
651        }
652        String cmd =
653                intent.getStringExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD);
654        if (cmd == null) {
655            Log.e(TAG, "onVendorSpecificHeadsetEvent() command is null");
656            return;
657        }
658        int cmdType =
659                intent.getIntExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE,
660                        -1);
661        // Only process set command
662        if (cmdType != BluetoothHeadset.AT_CMD_TYPE_SET) {
663            debugLog("onVendorSpecificHeadsetEvent() only SET command is processed");
664            return;
665        }
666        Object[] args = (Object[]) intent.getExtras()
667                .get(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS);
668        if (args == null) {
669            Log.e(TAG, "onVendorSpecificHeadsetEvent() arguments are null");
670            return;
671        }
672        int batteryPercent = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
673        switch (cmd) {
674            case BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT:
675                batteryPercent = getBatteryLevelFromXEventVsc(args);
676                break;
677            case BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV:
678                batteryPercent = getBatteryLevelFromAppleBatteryVsc(args);
679                break;
680        }
681        if (batteryPercent != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
682            updateBatteryLevel(device, batteryPercent);
683            infoLog("Updated device " + device + " battery level to " + String.valueOf(
684                    batteryPercent) + "%");
685        }
686    }
687
688    /**
689     * Parse
690     *      AT+IPHONEACCEV=[NumberOfIndicators],[IndicatorType],[IndicatorValue]
691     * vendor specific event
692     * @param args Array of arguments on the right side of assignment
693     * @return Battery level in percents, [0-100], {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
694     *         when there is an error parsing the arguments
695     */
696    @VisibleForTesting
697    static int getBatteryLevelFromAppleBatteryVsc(Object[] args) {
698        if (args.length == 0) {
699            Log.w(TAG, "getBatteryLevelFromAppleBatteryVsc() empty arguments");
700            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
701        }
702        int numKvPair;
703        if (args[0] instanceof Integer) {
704            numKvPair = (Integer) args[0];
705        } else {
706            Log.w(TAG, "getBatteryLevelFromAppleBatteryVsc() error parsing number of arguments");
707            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
708        }
709        if (args.length != (numKvPair * 2 + 1)) {
710            Log.w(TAG, "getBatteryLevelFromAppleBatteryVsc() number of arguments does not match");
711            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
712        }
713        int indicatorType;
714        int indicatorValue = -1;
715        for (int i = 0; i < numKvPair; ++i) {
716            Object indicatorTypeObj = args[2 * i + 1];
717            if (indicatorTypeObj instanceof Integer) {
718                indicatorType = (Integer) indicatorTypeObj;
719            } else {
720                Log.w(TAG, "getBatteryLevelFromAppleBatteryVsc() error parsing indicator type");
721                return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
722            }
723            if (indicatorType
724                    != BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL) {
725                continue;
726            }
727            Object indicatorValueObj = args[2 * i + 2];
728            if (indicatorValueObj instanceof Integer) {
729                indicatorValue = (Integer) indicatorValueObj;
730            } else {
731                Log.w(TAG, "getBatteryLevelFromAppleBatteryVsc() error parsing indicator value");
732                return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
733            }
734            break;
735        }
736        return (indicatorValue < 0 || indicatorValue > 9) ? BluetoothDevice.BATTERY_LEVEL_UNKNOWN
737                : (indicatorValue + 1) * 10;
738    }
739
740    /**
741     * Parse
742     *      AT+XEVENT=BATTERY,[Level],[NumberOfLevel],[MinutesOfTalk],[IsCharging]
743     * vendor specific event
744     * @param args Array of arguments on the right side of SET command
745     * @return Battery level in percents, [0-100], {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
746     *         when there is an error parsing the arguments
747     */
748    @VisibleForTesting
749    static int getBatteryLevelFromXEventVsc(Object[] args) {
750        if (args.length == 0) {
751            Log.w(TAG, "getBatteryLevelFromXEventVsc() empty arguments");
752            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
753        }
754        Object eventNameObj = args[0];
755        if (!(eventNameObj instanceof String)) {
756            Log.w(TAG, "getBatteryLevelFromXEventVsc() error parsing event name");
757            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
758        }
759        String eventName = (String) eventNameObj;
760        if (!eventName.equals(
761                BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT_BATTERY_LEVEL)) {
762            infoLog("getBatteryLevelFromXEventVsc() skip none BATTERY event: " + eventName);
763            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
764        }
765        if (args.length != 5) {
766            Log.w(TAG, "getBatteryLevelFromXEventVsc() wrong battery level event length: "
767                    + String.valueOf(args.length));
768            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
769        }
770        if (!(args[1] instanceof Integer) || !(args[2] instanceof Integer)) {
771            Log.w(TAG, "getBatteryLevelFromXEventVsc() error parsing event values");
772            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
773        }
774        int batteryLevel = (Integer) args[1];
775        int numberOfLevels = (Integer) args[2];
776        if (batteryLevel < 0 || numberOfLevels < 0 || batteryLevel > numberOfLevels) {
777            Log.w(TAG, "getBatteryLevelFromXEventVsc() wrong event value, batteryLevel="
778                    + String.valueOf(batteryLevel) + ", numberOfLevels=" + String.valueOf(
779                    numberOfLevels));
780            return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
781        }
782        return batteryLevel * 100 / numberOfLevels;
783    }
784
785    private final Handler mHandler = new Handler() {
786        @Override
787        public void handleMessage(Message msg) {
788            switch (msg.what) {
789                case MESSAGE_UUID_INTENT:
790                    BluetoothDevice device = (BluetoothDevice) msg.obj;
791                    if (device != null) {
792                        sendUuidIntent(device);
793                    }
794                    break;
795            }
796        }
797    };
798
799    private static void errorLog(String msg) {
800        Log.e(TAG, msg);
801    }
802
803    private static void debugLog(String msg) {
804        if (DBG) {
805            Log.d(TAG, msg);
806        }
807    }
808
809    private static void infoLog(String msg) {
810        if (DBG) {
811            Log.i(TAG, msg);
812        }
813    }
814
815    private static void warnLog(String msg) {
816        Log.w(TAG, msg);
817    }
818
819}
820