• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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 
17 package android.bluetooth;
18 
19 import android.Manifest;
20 import android.annotation.Nullable;
21 import android.annotation.RequiresPermission;
22 import android.annotation.SdkConstant;
23 import android.annotation.SdkConstant.SdkConstantType;
24 import android.annotation.SystemApi;
25 import android.annotation.UnsupportedAppUsage;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.os.Binder;
29 import android.os.Handler;
30 import android.os.IBinder;
31 import android.os.Looper;
32 import android.os.Message;
33 import android.os.RemoteException;
34 import android.util.Log;
35 
36 import java.util.ArrayList;
37 import java.util.List;
38 
39 /**
40  * Public API for controlling the Bluetooth Headset Service. This includes both
41  * Bluetooth Headset and Handsfree (v1.5) profiles.
42  *
43  * <p>BluetoothHeadset is a proxy object for controlling the Bluetooth Headset
44  * Service via IPC.
45  *
46  * <p> Use {@link BluetoothAdapter#getProfileProxy} to get
47  * the BluetoothHeadset proxy object. Use
48  * {@link BluetoothAdapter#closeProfileProxy} to close the service connection.
49  *
50  * <p> Android only supports one connected Bluetooth Headset at a time.
51  * Each method is protected with its appropriate permission.
52  */
53 public final class BluetoothHeadset implements BluetoothProfile {
54     private static final String TAG = "BluetoothHeadset";
55     private static final boolean DBG = true;
56     private static final boolean VDBG = false;
57 
58     /**
59      * Intent used to broadcast the change in connection state of the Headset
60      * profile.
61      *
62      * <p>This intent will have 3 extras:
63      * <ul>
64      * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
65      * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li>
66      * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
67      * </ul>
68      * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
69      * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
70      * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
71      *
72      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
73      * receive.
74      */
75     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
76     public static final String ACTION_CONNECTION_STATE_CHANGED =
77             "android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED";
78 
79     /**
80      * Intent used to broadcast the change in the Audio Connection state of the
81      * A2DP profile.
82      *
83      * <p>This intent will have 3 extras:
84      * <ul>
85      * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
86      * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li>
87      * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
88      * </ul>
89      * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
90      * {@link #STATE_AUDIO_CONNECTED}, {@link #STATE_AUDIO_DISCONNECTED},
91      *
92      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission
93      * to receive.
94      */
95     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
96     public static final String ACTION_AUDIO_STATE_CHANGED =
97             "android.bluetooth.headset.profile.action.AUDIO_STATE_CHANGED";
98 
99     /**
100      * Intent used to broadcast the selection of a connected device as active.
101      *
102      * <p>This intent will have one extra:
103      * <ul>
104      * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
105      * be null if no device is active. </li>
106      * </ul>
107      *
108      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
109      * receive.
110      *
111      * @hide
112      */
113     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
114     @UnsupportedAppUsage
115     public static final String ACTION_ACTIVE_DEVICE_CHANGED =
116             "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED";
117 
118     /**
119      * Intent used to broadcast that the headset has posted a
120      * vendor-specific event.
121      *
122      * <p>This intent will have 4 extras and 1 category.
123      * <ul>
124      * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote Bluetooth Device
125      * </li>
126      * <li> {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD} - The vendor
127      * specific command </li>
128      * <li> {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} - The AT
129      * command type which can be one of  {@link #AT_CMD_TYPE_READ},
130      * {@link #AT_CMD_TYPE_TEST}, or {@link #AT_CMD_TYPE_SET},
131      * {@link #AT_CMD_TYPE_BASIC},{@link #AT_CMD_TYPE_ACTION}. </li>
132      * <li> {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS} - Command
133      * arguments. </li>
134      * </ul>
135      *
136      * <p> The category is the Company ID of the vendor defining the
137      * vendor-specific command. {@link BluetoothAssignedNumbers}
138      *
139      * For example, for Plantronics specific events
140      * Category will be {@link #VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY}.55
141      *
142      * <p> For example, an AT+XEVENT=foo,3 will get translated into
143      * <ul>
144      * <li> EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD = +XEVENT </li>
145      * <li> EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE = AT_CMD_TYPE_SET </li>
146      * <li> EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS = foo, 3 </li>
147      * </ul>
148      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission
149      * to receive.
150      */
151     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
152     public static final String ACTION_VENDOR_SPECIFIC_HEADSET_EVENT =
153             "android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT";
154 
155     /**
156      * A String extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT}
157      * intents that contains the name of the vendor-specific command.
158      */
159     public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD =
160             "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_CMD";
161 
162     /**
163      * An int extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT}
164      * intents that contains the AT command type of the vendor-specific command.
165      */
166     public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE =
167             "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE";
168 
169     /**
170      * AT command type READ used with
171      * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE}
172      * For example, AT+VGM?. There are no arguments for this command type.
173      */
174     public static final int AT_CMD_TYPE_READ = 0;
175 
176     /**
177      * AT command type TEST used with
178      * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE}
179      * For example, AT+VGM=?. There are no arguments for this command type.
180      */
181     public static final int AT_CMD_TYPE_TEST = 1;
182 
183     /**
184      * AT command type SET used with
185      * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE}
186      * For example, AT+VGM=<args>.
187      */
188     public static final int AT_CMD_TYPE_SET = 2;
189 
190     /**
191      * AT command type BASIC used with
192      * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE}
193      * For example, ATD. Single character commands and everything following the
194      * character are arguments.
195      */
196     public static final int AT_CMD_TYPE_BASIC = 3;
197 
198     /**
199      * AT command type ACTION used with
200      * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE}
201      * For example, AT+CHUP. There are no arguments for action commands.
202      */
203     public static final int AT_CMD_TYPE_ACTION = 4;
204 
205     /**
206      * A Parcelable String array extra field in
207      * {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intents that contains
208      * the arguments to the vendor-specific command.
209      */
210     public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS =
211             "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_ARGS";
212 
213     /**
214      * The intent category to be used with {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT}
215      * for the companyId
216      */
217     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY =
218             "android.bluetooth.headset.intent.category.companyid";
219 
220     /**
221      * A vendor-specific command for unsolicited result code.
222      */
223     public static final String VENDOR_RESULT_CODE_COMMAND_ANDROID = "+ANDROID";
224 
225     /**
226      * A vendor-specific AT command
227      *
228      * @hide
229      */
230     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XAPL = "+XAPL";
231 
232     /**
233      * A vendor-specific AT command
234      *
235      * @hide
236      */
237     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV = "+IPHONEACCEV";
238 
239     /**
240      * Battery level indicator associated with
241      * {@link #VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV}
242      *
243      * @hide
244      */
245     public static final int VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL = 1;
246 
247     /**
248      * A vendor-specific AT command
249      *
250      * @hide
251      */
252     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT = "+XEVENT";
253 
254     /**
255      * Battery level indicator associated with {@link #VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT}
256      *
257      * @hide
258      */
259     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT_BATTERY_LEVEL = "BATTERY";
260 
261     /**
262      * Headset state when SCO audio is not connected.
263      * This state can be one of
264      * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
265      * {@link #ACTION_AUDIO_STATE_CHANGED} intent.
266      */
267     public static final int STATE_AUDIO_DISCONNECTED = 10;
268 
269     /**
270      * Headset state when SCO audio is connecting.
271      * This state can be one of
272      * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
273      * {@link #ACTION_AUDIO_STATE_CHANGED} intent.
274      */
275     public static final int STATE_AUDIO_CONNECTING = 11;
276 
277     /**
278      * Headset state when SCO audio is connected.
279      * This state can be one of
280      * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
281      * {@link #ACTION_AUDIO_STATE_CHANGED} intent.
282      */
283 
284     /**
285      * Intent used to broadcast the headset's indicator status
286      *
287      * <p>This intent will have 3 extras:
288      * <ul>
289      * <li> {@link #EXTRA_HF_INDICATORS_IND_ID} - The Assigned number of headset Indicator which
290      * is supported by the headset ( as indicated by AT+BIND command in the SLC
291      * sequence) or whose value is changed (indicated by AT+BIEV command) </li>
292      * <li> {@link #EXTRA_HF_INDICATORS_IND_VALUE} - Updated value of headset indicator. </li>
293      * <li> {@link BluetoothDevice#EXTRA_DEVICE} - Remote device. </li>
294      * </ul>
295      * <p>{@link #EXTRA_HF_INDICATORS_IND_ID} is defined by Bluetooth SIG and each of the indicators
296      * are given an assigned number. Below shows the assigned number of Indicator added so far
297      * - Enhanced Safety - 1, Valid Values: 0 - Disabled, 1 - Enabled
298      * - Battery Level - 2, Valid Values: 0~100 - Remaining level of Battery
299      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to receive.
300      *
301      * @hide
302      */
303     public static final String ACTION_HF_INDICATORS_VALUE_CHANGED =
304             "android.bluetooth.headset.action.HF_INDICATORS_VALUE_CHANGED";
305 
306     /**
307      * A int extra field in {@link #ACTION_HF_INDICATORS_VALUE_CHANGED}
308      * intents that contains the assigned number of the headset indicator as defined by
309      * Bluetooth SIG that is being sent. Value range is 0-65535 as defined in HFP 1.7
310      *
311      * @hide
312      */
313     public static final String EXTRA_HF_INDICATORS_IND_ID =
314             "android.bluetooth.headset.extra.HF_INDICATORS_IND_ID";
315 
316     /**
317      * A int extra field in {@link #ACTION_HF_INDICATORS_VALUE_CHANGED}
318      * intents that contains the value of the Headset indicator that is being sent.
319      *
320      * @hide
321      */
322     public static final String EXTRA_HF_INDICATORS_IND_VALUE =
323             "android.bluetooth.headset.extra.HF_INDICATORS_IND_VALUE";
324 
325     public static final int STATE_AUDIO_CONNECTED = 12;
326 
327     private static final int MESSAGE_HEADSET_SERVICE_CONNECTED = 100;
328     private static final int MESSAGE_HEADSET_SERVICE_DISCONNECTED = 101;
329 
330     private Context mContext;
331     private ServiceListener mServiceListener;
332     private volatile IBluetoothHeadset mService;
333     private BluetoothAdapter mAdapter;
334 
335     private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
336             new IBluetoothStateChangeCallback.Stub() {
337                 public void onBluetoothStateChange(boolean up) {
338                     if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
339                     if (!up) {
340                         doUnbind();
341                     } else {
342                         doBind();
343                     }
344                 }
345             };
346 
347     /**
348      * Create a BluetoothHeadset proxy object.
349      */
BluetoothHeadset(Context context, ServiceListener l)350     /*package*/ BluetoothHeadset(Context context, ServiceListener l) {
351         mContext = context;
352         mServiceListener = l;
353         mAdapter = BluetoothAdapter.getDefaultAdapter();
354 
355         IBluetoothManager mgr = mAdapter.getBluetoothManager();
356         if (mgr != null) {
357             try {
358                 mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
359             } catch (RemoteException e) {
360                 Log.e(TAG, "", e);
361             }
362         }
363 
364         doBind();
365     }
366 
doBind()367     private boolean doBind() {
368         synchronized (mConnection) {
369             if (mService == null) {
370                 if (VDBG) Log.d(TAG, "Binding service...");
371                 try {
372                     return mAdapter.getBluetoothManager().bindBluetoothProfileService(
373                             BluetoothProfile.HEADSET, mConnection);
374                 } catch (RemoteException e) {
375                     Log.e(TAG, "Unable to bind HeadsetService", e);
376                 }
377             }
378         }
379         return false;
380     }
381 
doUnbind()382     private void doUnbind() {
383         synchronized (mConnection) {
384             if (mService != null) {
385                 if (VDBG) Log.d(TAG, "Unbinding service...");
386                 try {
387                     mAdapter.getBluetoothManager().unbindBluetoothProfileService(
388                             BluetoothProfile.HEADSET, mConnection);
389                 } catch (RemoteException e) {
390                     Log.e(TAG, "Unable to unbind HeadsetService", e);
391                 } finally {
392                     mService = null;
393                 }
394             }
395         }
396     }
397 
398     /**
399      * Close the connection to the backing service.
400      * Other public functions of BluetoothHeadset will return default error
401      * results once close() has been called. Multiple invocations of close()
402      * are ok.
403      */
404     @UnsupportedAppUsage
close()405     /*package*/ void close() {
406         if (VDBG) log("close()");
407 
408         IBluetoothManager mgr = mAdapter.getBluetoothManager();
409         if (mgr != null) {
410             try {
411                 mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback);
412             } catch (RemoteException re) {
413                 Log.e(TAG, "", re);
414             }
415         }
416         mServiceListener = null;
417         doUnbind();
418     }
419 
420     /**
421      * Initiate connection to a profile of the remote bluetooth device.
422      *
423      * <p> Currently, the system supports only 1 connection to the
424      * headset/handsfree profile. The API will automatically disconnect connected
425      * devices before connecting.
426      *
427      * <p> This API returns false in scenarios like the profile on the
428      * device is already connected or Bluetooth is not turned on.
429      * When this API returns true, it is guaranteed that
430      * connection state intent for the profile will be broadcasted with
431      * the state. Users can get the connection state of the profile
432      * from this intent.
433      *
434      * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
435      * permission.
436      *
437      * @param device Remote Bluetooth Device
438      * @return false on immediate error, true otherwise
439      * @hide
440      */
441     @SystemApi
442     @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN)
connect(BluetoothDevice device)443     public boolean connect(BluetoothDevice device) {
444         if (DBG) log("connect(" + device + ")");
445         final IBluetoothHeadset service = mService;
446         if (service != null && isEnabled() && isValidDevice(device)) {
447             try {
448                 return service.connect(device);
449             } catch (RemoteException e) {
450                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
451                 return false;
452             }
453         }
454         if (service == null) Log.w(TAG, "Proxy not attached to service");
455         return false;
456     }
457 
458     /**
459      * Initiate disconnection from a profile
460      *
461      * <p> This API will return false in scenarios like the profile on the
462      * Bluetooth device is not in connected state etc. When this API returns,
463      * true, it is guaranteed that the connection state change
464      * intent will be broadcasted with the state. Users can get the
465      * disconnection state of the profile from this intent.
466      *
467      * <p> If the disconnection is initiated by a remote device, the state
468      * will transition from {@link #STATE_CONNECTED} to
469      * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the
470      * host (local) device the state will transition from
471      * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to
472      * state {@link #STATE_DISCONNECTED}. The transition to
473      * {@link #STATE_DISCONNECTING} can be used to distinguish between the
474      * two scenarios.
475      *
476      * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
477      * permission.
478      *
479      * @param device Remote Bluetooth Device
480      * @return false on immediate error, true otherwise
481      * @hide
482      */
483     @SystemApi
484     @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN)
disconnect(BluetoothDevice device)485     public boolean disconnect(BluetoothDevice device) {
486         if (DBG) log("disconnect(" + device + ")");
487         final IBluetoothHeadset service = mService;
488         if (service != null && isEnabled() && isValidDevice(device)) {
489             try {
490                 return service.disconnect(device);
491             } catch (RemoteException e) {
492                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
493                 return false;
494             }
495         }
496         if (service == null) Log.w(TAG, "Proxy not attached to service");
497         return false;
498     }
499 
500     /**
501      * {@inheritDoc}
502      */
503     @Override
getConnectedDevices()504     public List<BluetoothDevice> getConnectedDevices() {
505         if (VDBG) log("getConnectedDevices()");
506         final IBluetoothHeadset service = mService;
507         if (service != null && isEnabled()) {
508             try {
509                 return service.getConnectedDevices();
510             } catch (RemoteException e) {
511                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
512                 return new ArrayList<BluetoothDevice>();
513             }
514         }
515         if (service == null) Log.w(TAG, "Proxy not attached to service");
516         return new ArrayList<BluetoothDevice>();
517     }
518 
519     /**
520      * {@inheritDoc}
521      */
522     @Override
getDevicesMatchingConnectionStates(int[] states)523     public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
524         if (VDBG) log("getDevicesMatchingStates()");
525         final IBluetoothHeadset service = mService;
526         if (service != null && isEnabled()) {
527             try {
528                 return service.getDevicesMatchingConnectionStates(states);
529             } catch (RemoteException e) {
530                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
531                 return new ArrayList<BluetoothDevice>();
532             }
533         }
534         if (service == null) Log.w(TAG, "Proxy not attached to service");
535         return new ArrayList<BluetoothDevice>();
536     }
537 
538     /**
539      * {@inheritDoc}
540      */
541     @Override
getConnectionState(BluetoothDevice device)542     public int getConnectionState(BluetoothDevice device) {
543         if (VDBG) log("getConnectionState(" + device + ")");
544         final IBluetoothHeadset service = mService;
545         if (service != null && isEnabled() && isValidDevice(device)) {
546             try {
547                 return service.getConnectionState(device);
548             } catch (RemoteException e) {
549                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
550                 return BluetoothProfile.STATE_DISCONNECTED;
551             }
552         }
553         if (service == null) Log.w(TAG, "Proxy not attached to service");
554         return BluetoothProfile.STATE_DISCONNECTED;
555     }
556 
557     /**
558      * Set priority of the profile
559      *
560      * <p> The device should already be paired.
561      * Priority can be one of {@link BluetoothProfile#PRIORITY_ON} or
562      * {@link BluetoothProfile#PRIORITY_OFF},
563      *
564      * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
565      * permission.
566      *
567      * @param device Paired bluetooth device
568      * @param priority
569      * @return true if priority is set, false on error
570      * @hide
571      */
572     @SystemApi
573     @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN)
setPriority(BluetoothDevice device, int priority)574     public boolean setPriority(BluetoothDevice device, int priority) {
575         if (DBG) log("setPriority(" + device + ", " + priority + ")");
576         final IBluetoothHeadset service = mService;
577         if (service != null && isEnabled() && isValidDevice(device)) {
578             if (priority != BluetoothProfile.PRIORITY_OFF
579                     && priority != BluetoothProfile.PRIORITY_ON) {
580                 return false;
581             }
582             try {
583                 return service.setPriority(device, priority);
584             } catch (RemoteException e) {
585                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
586                 return false;
587             }
588         }
589         if (service == null) Log.w(TAG, "Proxy not attached to service");
590         return false;
591     }
592 
593     /**
594      * Get the priority of the profile.
595      *
596      * <p> The priority can be any of:
597      * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF},
598      * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
599      *
600      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
601      *
602      * @param device Bluetooth device
603      * @return priority of the device
604      * @hide
605      */
606     @UnsupportedAppUsage
getPriority(BluetoothDevice device)607     public int getPriority(BluetoothDevice device) {
608         if (VDBG) log("getPriority(" + device + ")");
609         final IBluetoothHeadset service = mService;
610         if (service != null && isEnabled() && isValidDevice(device)) {
611             try {
612                 return service.getPriority(device);
613             } catch (RemoteException e) {
614                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
615                 return PRIORITY_OFF;
616             }
617         }
618         if (service == null) Log.w(TAG, "Proxy not attached to service");
619         return PRIORITY_OFF;
620     }
621 
622     /**
623      * Start Bluetooth voice recognition. This methods sends the voice
624      * recognition AT command to the headset and establishes the
625      * audio connection.
626      *
627      * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}.
628      * If this function returns true, this intent will be broadcasted with
629      * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_CONNECTING}.
630      *
631      * <p> {@link #EXTRA_STATE} will transition from
632      * {@link #STATE_AUDIO_CONNECTING} to {@link #STATE_AUDIO_CONNECTED} when
633      * audio connection is established and to {@link #STATE_AUDIO_DISCONNECTED}
634      * in case of failure to establish the audio connection.
635      *
636      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
637      *
638      * @param device Bluetooth headset
639      * @return false if there is no headset connected, or the connected headset doesn't support
640      * voice recognition, or voice recognition is already started, or audio channel is occupied,
641      * or on error, true otherwise
642      */
startVoiceRecognition(BluetoothDevice device)643     public boolean startVoiceRecognition(BluetoothDevice device) {
644         if (DBG) log("startVoiceRecognition()");
645         final IBluetoothHeadset service = mService;
646         if (service != null && isEnabled() && isValidDevice(device)) {
647             try {
648                 return service.startVoiceRecognition(device);
649             } catch (RemoteException e) {
650                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
651             }
652         }
653         if (service == null) Log.w(TAG, "Proxy not attached to service");
654         return false;
655     }
656 
657     /**
658      * Stop Bluetooth Voice Recognition mode, and shut down the
659      * Bluetooth audio path.
660      *
661      * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}.
662      * If this function returns true, this intent will be broadcasted with
663      * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_DISCONNECTED}.
664      *
665      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
666      *
667      * @param device Bluetooth headset
668      * @return false if there is no headset connected, or voice recognition has not started,
669      * or voice recognition has ended on this headset, or on error, true otherwise
670      */
stopVoiceRecognition(BluetoothDevice device)671     public boolean stopVoiceRecognition(BluetoothDevice device) {
672         if (DBG) log("stopVoiceRecognition()");
673         final IBluetoothHeadset service = mService;
674         if (service != null && isEnabled() && isValidDevice(device)) {
675             try {
676                 return service.stopVoiceRecognition(device);
677             } catch (RemoteException e) {
678                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
679             }
680         }
681         if (service == null) Log.w(TAG, "Proxy not attached to service");
682         return false;
683     }
684 
685     /**
686      * Check if Bluetooth SCO audio is connected.
687      *
688      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
689      *
690      * @param device Bluetooth headset
691      * @return true if SCO is connected, false otherwise or on error
692      */
isAudioConnected(BluetoothDevice device)693     public boolean isAudioConnected(BluetoothDevice device) {
694         if (VDBG) log("isAudioConnected()");
695         final IBluetoothHeadset service = mService;
696         if (service != null && isEnabled() && isValidDevice(device)) {
697             try {
698                 return service.isAudioConnected(device);
699             } catch (RemoteException e) {
700                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
701             }
702         }
703         if (service == null) Log.w(TAG, "Proxy not attached to service");
704         return false;
705     }
706 
707     /**
708      * Indicates if current platform supports voice dialing over bluetooth SCO.
709      *
710      * @return true if voice dialing over bluetooth is supported, false otherwise.
711      * @hide
712      */
isBluetoothVoiceDialingEnabled(Context context)713     public static boolean isBluetoothVoiceDialingEnabled(Context context) {
714         return context.getResources().getBoolean(
715                 com.android.internal.R.bool.config_bluetooth_sco_off_call);
716     }
717 
718     /**
719      * Get the current audio state of the Headset.
720      * Note: This is an internal function and shouldn't be exposed
721      *
722      * @hide
723      */
724     @UnsupportedAppUsage
getAudioState(BluetoothDevice device)725     public int getAudioState(BluetoothDevice device) {
726         if (VDBG) log("getAudioState");
727         final IBluetoothHeadset service = mService;
728         if (service != null && !isDisabled()) {
729             try {
730                 return service.getAudioState(device);
731             } catch (RemoteException e) {
732                 Log.e(TAG, e.toString());
733             }
734         } else {
735             Log.w(TAG, "Proxy not attached to service");
736             if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
737         }
738         return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
739     }
740 
741     /**
742      * Sets whether audio routing is allowed. When set to {@code false}, the AG will not route any
743      * audio to the HF unless explicitly told to.
744      * This method should be used in cases where the SCO channel is shared between multiple profiles
745      * and must be delegated by a source knowledgeable
746      * Note: This is an internal function and shouldn't be exposed
747      *
748      * @param allowed {@code true} if the profile can reroute audio, {@code false} otherwise.
749      * @hide
750      */
setAudioRouteAllowed(boolean allowed)751     public void setAudioRouteAllowed(boolean allowed) {
752         if (VDBG) log("setAudioRouteAllowed");
753         final IBluetoothHeadset service = mService;
754         if (service != null && isEnabled()) {
755             try {
756                 service.setAudioRouteAllowed(allowed);
757             } catch (RemoteException e) {
758                 Log.e(TAG, e.toString());
759             }
760         } else {
761             Log.w(TAG, "Proxy not attached to service");
762             if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
763         }
764     }
765 
766     /**
767      * Returns whether audio routing is allowed. see {@link #setAudioRouteAllowed(boolean)}.
768      * Note: This is an internal function and shouldn't be exposed
769      *
770      * @hide
771      */
getAudioRouteAllowed()772     public boolean getAudioRouteAllowed() {
773         if (VDBG) log("getAudioRouteAllowed");
774         final IBluetoothHeadset service = mService;
775         if (service != null && isEnabled()) {
776             try {
777                 return service.getAudioRouteAllowed();
778             } catch (RemoteException e) {
779                 Log.e(TAG, e.toString());
780             }
781         } else {
782             Log.w(TAG, "Proxy not attached to service");
783             if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
784         }
785         return false;
786     }
787 
788     /**
789      * Force SCO audio to be opened regardless any other restrictions
790      *
791      * @param forced Whether or not SCO audio connection should be forced: True to force SCO audio
792      * False to use SCO audio in normal manner
793      * @hide
794      */
setForceScoAudio(boolean forced)795     public void setForceScoAudio(boolean forced) {
796         if (VDBG) log("setForceScoAudio " + String.valueOf(forced));
797         final IBluetoothHeadset service = mService;
798         if (service != null && isEnabled()) {
799             try {
800                 service.setForceScoAudio(forced);
801             } catch (RemoteException e) {
802                 Log.e(TAG, e.toString());
803             }
804         } else {
805             Log.w(TAG, "Proxy not attached to service");
806             if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
807         }
808     }
809 
810     /**
811      * Check if at least one headset's SCO audio is connected or connecting
812      *
813      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
814      *
815      * @return true if at least one device's SCO audio is connected or connecting, false otherwise
816      * or on error
817      * @hide
818      */
isAudioOn()819     public boolean isAudioOn() {
820         if (VDBG) log("isAudioOn()");
821         final IBluetoothHeadset service = mService;
822         if (service != null && isEnabled()) {
823             try {
824                 return service.isAudioOn();
825             } catch (RemoteException e) {
826                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
827             }
828         }
829         if (service == null) Log.w(TAG, "Proxy not attached to service");
830         return false;
831 
832     }
833 
834     /**
835      * Initiates a connection of headset audio to the current active device
836      *
837      * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}.
838      * If this function returns true, this intent will be broadcasted with
839      * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_CONNECTING}.
840      *
841      * <p> {@link #EXTRA_STATE} will transition from
842      * {@link #STATE_AUDIO_CONNECTING} to {@link #STATE_AUDIO_CONNECTED} when
843      * audio connection is established and to {@link #STATE_AUDIO_DISCONNECTED}
844      * in case of failure to establish the audio connection.
845      *
846      * Note that this intent will not be sent if {@link BluetoothHeadset#isAudioOn()} is true
847      * before calling this method
848      *
849      * @return false if there was some error such as there is no active headset
850      * @hide
851      */
852     @UnsupportedAppUsage
connectAudio()853     public boolean connectAudio() {
854         final IBluetoothHeadset service = mService;
855         if (service != null && isEnabled()) {
856             try {
857                 return service.connectAudio();
858             } catch (RemoteException e) {
859                 Log.e(TAG, e.toString());
860             }
861         } else {
862             Log.w(TAG, "Proxy not attached to service");
863             if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
864         }
865         return false;
866     }
867 
868     /**
869      * Initiates a disconnection of HFP SCO audio.
870      * Tear down voice recognition or virtual voice call if any.
871      *
872      * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}.
873      * If this function returns true, this intent will be broadcasted with
874      * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_DISCONNECTED}.
875      *
876      * @return false if audio is not connected, or on error, true otherwise
877      * @hide
878      */
879     @UnsupportedAppUsage
disconnectAudio()880     public boolean disconnectAudio() {
881         final IBluetoothHeadset service = mService;
882         if (service != null && isEnabled()) {
883             try {
884                 return service.disconnectAudio();
885             } catch (RemoteException e) {
886                 Log.e(TAG, e.toString());
887             }
888         } else {
889             Log.w(TAG, "Proxy not attached to service");
890             if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
891         }
892         return false;
893     }
894 
895     /**
896      * Initiates a SCO channel connection as a virtual voice call to the current active device
897      * Active handsfree device will be notified of incoming call and connected call.
898      *
899      * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}.
900      * If this function returns true, this intent will be broadcasted with
901      * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_CONNECTING}.
902      *
903      * <p> {@link #EXTRA_STATE} will transition from
904      * {@link #STATE_AUDIO_CONNECTING} to {@link #STATE_AUDIO_CONNECTED} when
905      * audio connection is established and to {@link #STATE_AUDIO_DISCONNECTED}
906      * in case of failure to establish the audio connection.
907      *
908      * @return true if successful, false if one of the following case applies
909      *  - SCO audio is not idle (connecting or connected)
910      *  - virtual call has already started
911      *  - there is no active device
912      *  - a Telecom managed call is going on
913      *  - binder is dead or Bluetooth is disabled or other error
914      * @hide
915      */
916     @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
917     @UnsupportedAppUsage
startScoUsingVirtualVoiceCall()918     public boolean startScoUsingVirtualVoiceCall() {
919         if (DBG) log("startScoUsingVirtualVoiceCall()");
920         final IBluetoothHeadset service = mService;
921         if (service != null && isEnabled()) {
922             try {
923                 return service.startScoUsingVirtualVoiceCall();
924             } catch (RemoteException e) {
925                 Log.e(TAG, e.toString());
926             }
927         } else {
928             Log.w(TAG, "Proxy not attached to service");
929             if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
930         }
931         return false;
932     }
933 
934     /**
935      * Terminates an ongoing SCO connection and the associated virtual call.
936      *
937      * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}.
938      * If this function returns true, this intent will be broadcasted with
939      * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_DISCONNECTED}.
940      *
941      * @return true if successful, false if one of the following case applies
942      *  - virtual voice call is not started or has ended
943      *  - binder is dead or Bluetooth is disabled or other error
944      * @hide
945      */
946     @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
947     @UnsupportedAppUsage
stopScoUsingVirtualVoiceCall()948     public boolean stopScoUsingVirtualVoiceCall() {
949         if (DBG) log("stopScoUsingVirtualVoiceCall()");
950         final IBluetoothHeadset service = mService;
951         if (service != null && isEnabled()) {
952             try {
953                 return service.stopScoUsingVirtualVoiceCall();
954             } catch (RemoteException e) {
955                 Log.e(TAG, e.toString());
956             }
957         } else {
958             Log.w(TAG, "Proxy not attached to service");
959             if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
960         }
961         return false;
962     }
963 
964     /**
965      * Notify Headset of phone state change.
966      * This is a backdoor for phone app to call BluetoothHeadset since
967      * there is currently not a good way to get precise call state change outside
968      * of phone app.
969      *
970      * @hide
971      */
972     @UnsupportedAppUsage
phoneStateChanged(int numActive, int numHeld, int callState, String number, int type, String name)973     public void phoneStateChanged(int numActive, int numHeld, int callState, String number,
974             int type, String name) {
975         final IBluetoothHeadset service = mService;
976         if (service != null && isEnabled()) {
977             try {
978                 service.phoneStateChanged(numActive, numHeld, callState, number, type, name);
979             } catch (RemoteException e) {
980                 Log.e(TAG, e.toString());
981             }
982         } else {
983             Log.w(TAG, "Proxy not attached to service");
984             if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
985         }
986     }
987 
988     /**
989      * Send Headset of CLCC response
990      *
991      * @hide
992      */
clccResponse(int index, int direction, int status, int mode, boolean mpty, String number, int type)993     public void clccResponse(int index, int direction, int status, int mode, boolean mpty,
994             String number, int type) {
995         final IBluetoothHeadset service = mService;
996         if (service != null && isEnabled()) {
997             try {
998                 service.clccResponse(index, direction, status, mode, mpty, number, type);
999             } catch (RemoteException e) {
1000                 Log.e(TAG, e.toString());
1001             }
1002         } else {
1003             Log.w(TAG, "Proxy not attached to service");
1004             if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
1005         }
1006     }
1007 
1008     /**
1009      * Sends a vendor-specific unsolicited result code to the headset.
1010      *
1011      * <p>The actual string to be sent is <code>command + ": " + arg</code>. For example, if {@code
1012      * command} is {@link #VENDOR_RESULT_CODE_COMMAND_ANDROID} and {@code arg} is {@code "0"}, the
1013      * string <code>"+ANDROID: 0"</code> will be sent.
1014      *
1015      * <p>Currently only {@link #VENDOR_RESULT_CODE_COMMAND_ANDROID} is allowed as {@code command}.
1016      *
1017      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
1018      *
1019      * @param device Bluetooth headset.
1020      * @param command A vendor-specific command.
1021      * @param arg The argument that will be attached to the command.
1022      * @return {@code false} if there is no headset connected, or if the command is not an allowed
1023      * vendor-specific unsolicited result code, or on error. {@code true} otherwise.
1024      * @throws IllegalArgumentException if {@code command} is {@code null}.
1025      */
sendVendorSpecificResultCode(BluetoothDevice device, String command, String arg)1026     public boolean sendVendorSpecificResultCode(BluetoothDevice device, String command,
1027             String arg) {
1028         if (DBG) {
1029             log("sendVendorSpecificResultCode()");
1030         }
1031         if (command == null) {
1032             throw new IllegalArgumentException("command is null");
1033         }
1034         final IBluetoothHeadset service = mService;
1035         if (service != null && isEnabled() && isValidDevice(device)) {
1036             try {
1037                 return service.sendVendorSpecificResultCode(device, command, arg);
1038             } catch (RemoteException e) {
1039                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
1040             }
1041         }
1042         if (service == null) {
1043             Log.w(TAG, "Proxy not attached to service");
1044         }
1045         return false;
1046     }
1047 
1048     /**
1049      * Select a connected device as active.
1050      *
1051      * The active device selection is per profile. An active device's
1052      * purpose is profile-specific. For example, in HFP and HSP profiles,
1053      * it is the device used for phone call audio. If a remote device is not
1054      * connected, it cannot be selected as active.
1055      *
1056      * <p> This API returns false in scenarios like the profile on the
1057      * device is not connected or Bluetooth is not turned on.
1058      * When this API returns true, it is guaranteed that the
1059      * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
1060      * with the active device.
1061      *
1062      * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
1063      * permission.
1064      *
1065      * @param device Remote Bluetooth Device, could be null if phone call audio should not be
1066      * streamed to a headset
1067      * @return false on immediate error, true otherwise
1068      * @hide
1069      */
1070     @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN)
1071     @UnsupportedAppUsage
setActiveDevice(@ullable BluetoothDevice device)1072     public boolean setActiveDevice(@Nullable BluetoothDevice device) {
1073         if (DBG) {
1074             Log.d(TAG, "setActiveDevice: " + device);
1075         }
1076         final IBluetoothHeadset service = mService;
1077         if (service != null && isEnabled() && (device == null || isValidDevice(device))) {
1078             try {
1079                 return service.setActiveDevice(device);
1080             } catch (RemoteException e) {
1081                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
1082             }
1083         }
1084         if (service == null) {
1085             Log.w(TAG, "Proxy not attached to service");
1086         }
1087         return false;
1088     }
1089 
1090     /**
1091      * Get the connected device that is active.
1092      *
1093      * <p>Requires {@link android.Manifest.permission#BLUETOOTH}
1094      * permission.
1095      *
1096      * @return the connected device that is active or null if no device
1097      * is active.
1098      * @hide
1099      */
1100     @RequiresPermission(android.Manifest.permission.BLUETOOTH)
1101     @UnsupportedAppUsage
getActiveDevice()1102     public BluetoothDevice getActiveDevice() {
1103         if (VDBG) {
1104             Log.d(TAG, "getActiveDevice");
1105         }
1106         final IBluetoothHeadset service = mService;
1107         if (service != null && isEnabled()) {
1108             try {
1109                 return service.getActiveDevice();
1110             } catch (RemoteException e) {
1111                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
1112             }
1113         }
1114         if (service == null) {
1115             Log.w(TAG, "Proxy not attached to service");
1116         }
1117         return null;
1118     }
1119 
1120     /**
1121      * Check if in-band ringing is currently enabled. In-band ringing could be disabled during an
1122      * active connection.
1123      *
1124      * @return true if in-band ringing is enabled, false if in-band ringing is disabled
1125      * @hide
1126      */
1127     @RequiresPermission(android.Manifest.permission.BLUETOOTH)
isInbandRingingEnabled()1128     public boolean isInbandRingingEnabled() {
1129         if (DBG) {
1130             log("isInbandRingingEnabled()");
1131         }
1132         final IBluetoothHeadset service = mService;
1133         if (service != null && isEnabled()) {
1134             try {
1135                 return service.isInbandRingingEnabled();
1136             } catch (RemoteException e) {
1137                 Log.e(TAG, Log.getStackTraceString(new Throwable()));
1138             }
1139         }
1140         if (service == null) {
1141             Log.w(TAG, "Proxy not attached to service");
1142         }
1143         return false;
1144     }
1145 
1146     /**
1147      * Check if in-band ringing is supported for this platform.
1148      *
1149      * @return true if in-band ringing is supported, false if in-band ringing is not supported
1150      * @hide
1151      */
isInbandRingingSupported(Context context)1152     public static boolean isInbandRingingSupported(Context context) {
1153         return context.getResources().getBoolean(
1154                 com.android.internal.R.bool.config_bluetooth_hfp_inband_ringing_support);
1155     }
1156 
1157     private final IBluetoothProfileServiceConnection mConnection =
1158             new IBluetoothProfileServiceConnection.Stub() {
1159         @Override
1160         public void onServiceConnected(ComponentName className, IBinder service) {
1161             if (DBG) Log.d(TAG, "Proxy object connected");
1162             mService = IBluetoothHeadset.Stub.asInterface(Binder.allowBlocking(service));
1163             mHandler.sendMessage(mHandler.obtainMessage(
1164                     MESSAGE_HEADSET_SERVICE_CONNECTED));
1165         }
1166 
1167         @Override
1168         public void onServiceDisconnected(ComponentName className) {
1169             if (DBG) Log.d(TAG, "Proxy object disconnected");
1170             doUnbind();
1171             mHandler.sendMessage(mHandler.obtainMessage(
1172                     MESSAGE_HEADSET_SERVICE_DISCONNECTED));
1173         }
1174     };
1175 
1176     @UnsupportedAppUsage
isEnabled()1177     private boolean isEnabled() {
1178         return mAdapter.getState() == BluetoothAdapter.STATE_ON;
1179     }
1180 
isDisabled()1181     private boolean isDisabled() {
1182         return mAdapter.getState() == BluetoothAdapter.STATE_OFF;
1183     }
1184 
isValidDevice(BluetoothDevice device)1185     private static boolean isValidDevice(BluetoothDevice device) {
1186         return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
1187     }
1188 
log(String msg)1189     private static void log(String msg) {
1190         Log.d(TAG, msg);
1191     }
1192 
1193     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
1194         @Override
1195         public void handleMessage(Message msg) {
1196             switch (msg.what) {
1197                 case MESSAGE_HEADSET_SERVICE_CONNECTED: {
1198                     if (mServiceListener != null) {
1199                         mServiceListener.onServiceConnected(BluetoothProfile.HEADSET,
1200                                 BluetoothHeadset.this);
1201                     }
1202                     break;
1203                 }
1204                 case MESSAGE_HEADSET_SERVICE_DISCONNECTED: {
1205                     if (mServiceListener != null) {
1206                         mServiceListener.onServiceDisconnected(BluetoothProfile.HEADSET);
1207                     }
1208                     break;
1209                 }
1210             }
1211         }
1212     };
1213 }
1214