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 /** 18 * TODO: Move this to services.jar 19 * and make the contructor package private again. 20 * @hide 21 */ 22 23 package android.server; 24 25 import android.bluetooth.BluetoothA2dp; 26 import android.bluetooth.BluetoothAdapter; 27 import android.bluetooth.BluetoothDevice; 28 import android.bluetooth.BluetoothUuid; 29 import android.bluetooth.IBluetoothA2dp; 30 import android.os.ParcelUuid; 31 import android.content.BroadcastReceiver; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.IntentFilter; 35 import android.media.AudioManager; 36 import android.os.Handler; 37 import android.os.Message; 38 import android.provider.Settings; 39 import android.util.Log; 40 41 import java.io.FileDescriptor; 42 import java.io.PrintWriter; 43 import java.util.HashMap; 44 import java.util.HashSet; 45 import java.util.Set; 46 47 public class BluetoothA2dpService extends IBluetoothA2dp.Stub { 48 private static final String TAG = "BluetoothA2dpService"; 49 private static final boolean DBG = true; 50 51 public static final String BLUETOOTH_A2DP_SERVICE = "bluetooth_a2dp"; 52 53 private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; 54 private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; 55 56 private static final String BLUETOOTH_ENABLED = "bluetooth_enabled"; 57 58 private static final int MESSAGE_CONNECT_TO = 1; 59 60 private static final String PROPERTY_STATE = "State"; 61 62 private static final String SINK_STATE_DISCONNECTED = "disconnected"; 63 private static final String SINK_STATE_CONNECTING = "connecting"; 64 private static final String SINK_STATE_CONNECTED = "connected"; 65 private static final String SINK_STATE_PLAYING = "playing"; 66 67 private static int mSinkCount; 68 69 private final Context mContext; 70 private final IntentFilter mIntentFilter; 71 private HashMap<BluetoothDevice, Integer> mAudioDevices; 72 private final AudioManager mAudioManager; 73 private final BluetoothService mBluetoothService; 74 private final BluetoothAdapter mAdapter; 75 private int mTargetA2dpState; 76 77 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 78 @Override 79 public void onReceive(Context context, Intent intent) { 80 String action = intent.getAction(); 81 BluetoothDevice device = 82 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 83 if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { 84 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 85 BluetoothAdapter.ERROR); 86 switch (state) { 87 case BluetoothAdapter.STATE_ON: 88 onBluetoothEnable(); 89 break; 90 case BluetoothAdapter.STATE_TURNING_OFF: 91 onBluetoothDisable(); 92 break; 93 } 94 } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { 95 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 96 BluetoothDevice.ERROR); 97 switch(bondState) { 98 case BluetoothDevice.BOND_BONDED: 99 if (getSinkPriority(device) == BluetoothA2dp.PRIORITY_UNDEFINED) { 100 setSinkPriority(device, BluetoothA2dp.PRIORITY_ON); 101 } 102 break; 103 case BluetoothDevice.BOND_NONE: 104 setSinkPriority(device, BluetoothA2dp.PRIORITY_UNDEFINED); 105 break; 106 } 107 } else if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { 108 if (getSinkPriority(device) == BluetoothA2dp.PRIORITY_AUTO_CONNECT && 109 isSinkDevice(device)) { 110 // This device is a preferred sink. Make an A2DP connection 111 // after a delay. We delay to avoid connection collisions, 112 // and to give other profiles such as HFP a chance to 113 // connect first. 114 Message msg = Message.obtain(mHandler, MESSAGE_CONNECT_TO, device); 115 mHandler.sendMessageDelayed(msg, 6000); 116 } 117 } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { 118 synchronized (this) { 119 if (mAudioDevices.containsKey(device)) { 120 int state = mAudioDevices.get(device); 121 handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); 122 } 123 } 124 } else if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) { 125 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); 126 if (streamType == AudioManager.STREAM_MUSIC) { 127 BluetoothDevice sinks[] = getConnectedSinks(); 128 if (sinks.length != 0 && isPhoneDocked(sinks[0])) { 129 String address = sinks[0].getAddress(); 130 int newVolLevel = 131 intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); 132 int oldVolLevel = 133 intent.getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0); 134 String path = mBluetoothService.getObjectPathFromAddress(address); 135 if (newVolLevel > oldVolLevel) { 136 avrcpVolumeUpNative(path); 137 } else if (newVolLevel < oldVolLevel) { 138 avrcpVolumeDownNative(path); 139 } 140 } 141 } 142 } 143 } 144 }; 145 146 147 private boolean isPhoneDocked(BluetoothDevice device) { 148 // This works only because these broadcast intents are "sticky" 149 Intent i = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); 150 if (i != null) { 151 int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED); 152 if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { 153 BluetoothDevice dockDevice = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 154 if (dockDevice != null && device.equals(dockDevice)) { 155 return true; 156 } 157 } 158 } 159 return false; 160 } 161 162 public BluetoothA2dpService(Context context, BluetoothService bluetoothService) { 163 mContext = context; 164 165 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 166 167 mBluetoothService = bluetoothService; 168 if (mBluetoothService == null) { 169 throw new RuntimeException("Platform does not support Bluetooth"); 170 } 171 172 if (!initNative()) { 173 throw new RuntimeException("Could not init BluetoothA2dpService"); 174 } 175 176 mAdapter = BluetoothAdapter.getDefaultAdapter(); 177 178 mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); 179 mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 180 mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); 181 mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); 182 mIntentFilter.addAction(AudioManager.VOLUME_CHANGED_ACTION); 183 mContext.registerReceiver(mReceiver, mIntentFilter); 184 185 mAudioDevices = new HashMap<BluetoothDevice, Integer>(); 186 187 if (mBluetoothService.isEnabled()) 188 onBluetoothEnable(); 189 mTargetA2dpState = -1; 190 } 191 192 @Override 193 protected void finalize() throws Throwable { 194 try { 195 cleanupNative(); 196 } finally { 197 super.finalize(); 198 } 199 } 200 201 private final Handler mHandler = new Handler() { 202 @Override 203 public void handleMessage(Message msg) { 204 switch (msg.what) { 205 case MESSAGE_CONNECT_TO: 206 BluetoothDevice device = (BluetoothDevice) msg.obj; 207 // check bluetooth is still on, device is still preferred, and 208 // nothing is currently connected 209 if (mBluetoothService.isEnabled() && 210 getSinkPriority(device) == BluetoothA2dp.PRIORITY_AUTO_CONNECT && 211 lookupSinksMatchingStates(new int[] { 212 BluetoothA2dp.STATE_CONNECTING, 213 BluetoothA2dp.STATE_CONNECTED, 214 BluetoothA2dp.STATE_PLAYING, 215 BluetoothA2dp.STATE_DISCONNECTING}).size() == 0) { 216 log("Auto-connecting A2DP to sink " + device); 217 connectSink(device); 218 } 219 break; 220 } 221 } 222 }; 223 224 private int convertBluezSinkStringtoState(String value) { 225 if (value.equalsIgnoreCase("disconnected")) 226 return BluetoothA2dp.STATE_DISCONNECTED; 227 if (value.equalsIgnoreCase("connecting")) 228 return BluetoothA2dp.STATE_CONNECTING; 229 if (value.equalsIgnoreCase("connected")) 230 return BluetoothA2dp.STATE_CONNECTED; 231 if (value.equalsIgnoreCase("playing")) 232 return BluetoothA2dp.STATE_PLAYING; 233 return -1; 234 } 235 236 private boolean isSinkDevice(BluetoothDevice device) { 237 ParcelUuid[] uuids = mBluetoothService.getRemoteUuids(device.getAddress()); 238 if (uuids != null && BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSink)) { 239 return true; 240 } 241 return false; 242 } 243 244 private synchronized boolean addAudioSink (BluetoothDevice device) { 245 String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); 246 String propValues[] = (String []) getSinkPropertiesNative(path); 247 if (propValues == null) { 248 Log.e(TAG, "Error while getting AudioSink properties for device: " + device); 249 return false; 250 } 251 Integer state = null; 252 // Properties are name-value pairs 253 for (int i = 0; i < propValues.length; i+=2) { 254 if (propValues[i].equals(PROPERTY_STATE)) { 255 state = new Integer(convertBluezSinkStringtoState(propValues[i+1])); 256 break; 257 } 258 } 259 mAudioDevices.put(device, state); 260 handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTED, state); 261 return true; 262 } 263 264 private synchronized void onBluetoothEnable() { 265 String devices = mBluetoothService.getProperty("Devices"); 266 mSinkCount = 0; 267 if (devices != null) { 268 String [] paths = devices.split(","); 269 for (String path: paths) { 270 String address = mBluetoothService.getAddressFromObjectPath(path); 271 BluetoothDevice device = mAdapter.getRemoteDevice(address); 272 ParcelUuid[] remoteUuids = mBluetoothService.getRemoteUuids(address); 273 if (remoteUuids != null) 274 if (BluetoothUuid.containsAnyUuid(remoteUuids, 275 new ParcelUuid[] {BluetoothUuid.AudioSink, 276 BluetoothUuid.AdvAudioDist})) { 277 addAudioSink(device); 278 } 279 } 280 } 281 mAudioManager.setParameters(BLUETOOTH_ENABLED+"=true"); 282 mAudioManager.setParameters("A2dpSuspended=false"); 283 } 284 285 private synchronized void onBluetoothDisable() { 286 if (!mAudioDevices.isEmpty()) { 287 BluetoothDevice[] devices = new BluetoothDevice[mAudioDevices.size()]; 288 devices = mAudioDevices.keySet().toArray(devices); 289 for (BluetoothDevice device : devices) { 290 int state = getSinkState(device); 291 switch (state) { 292 case BluetoothA2dp.STATE_CONNECTING: 293 case BluetoothA2dp.STATE_CONNECTED: 294 case BluetoothA2dp.STATE_PLAYING: 295 disconnectSinkNative(mBluetoothService.getObjectPathFromAddress( 296 device.getAddress())); 297 handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); 298 break; 299 case BluetoothA2dp.STATE_DISCONNECTING: 300 handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTING, 301 BluetoothA2dp.STATE_DISCONNECTED); 302 break; 303 } 304 } 305 mAudioDevices.clear(); 306 } 307 308 mAudioManager.setParameters(BLUETOOTH_ENABLED + "=false"); 309 } 310 311 public synchronized boolean connectSink(BluetoothDevice device) { 312 mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, 313 "Need BLUETOOTH_ADMIN permission"); 314 if (DBG) log("connectSink(" + device + ")"); 315 316 if (!mBluetoothService.isEnabled()) return false; 317 318 // ignore if there are any active sinks 319 if (lookupSinksMatchingStates(new int[] { 320 BluetoothA2dp.STATE_CONNECTING, 321 BluetoothA2dp.STATE_CONNECTED, 322 BluetoothA2dp.STATE_PLAYING, 323 BluetoothA2dp.STATE_DISCONNECTING}).size() != 0) { 324 return false; 325 } 326 327 if (mAudioDevices.get(device) == null && !addAudioSink(device)) 328 return false; 329 330 int state = mAudioDevices.get(device); 331 332 switch (state) { 333 case BluetoothA2dp.STATE_CONNECTED: 334 case BluetoothA2dp.STATE_PLAYING: 335 case BluetoothA2dp.STATE_DISCONNECTING: 336 return false; 337 case BluetoothA2dp.STATE_CONNECTING: 338 return true; 339 } 340 341 String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); 342 if (path == null) 343 return false; 344 345 // State is DISCONNECTED 346 handleSinkStateChange(device, state, BluetoothA2dp.STATE_CONNECTING); 347 348 if (!connectSinkNative(path)) { 349 // Restore previous state 350 handleSinkStateChange(device, mAudioDevices.get(device), state); 351 return false; 352 } 353 return true; 354 } 355 356 public synchronized boolean disconnectSink(BluetoothDevice device) { 357 mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, 358 "Need BLUETOOTH_ADMIN permission"); 359 if (DBG) log("disconnectSink(" + device + ")"); 360 361 String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); 362 if (path == null) { 363 return false; 364 } 365 366 int state = getSinkState(device); 367 switch (state) { 368 case BluetoothA2dp.STATE_DISCONNECTED: 369 return false; 370 case BluetoothA2dp.STATE_DISCONNECTING: 371 return true; 372 } 373 374 // State is CONNECTING or CONNECTED or PLAYING 375 handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTING); 376 if (!disconnectSinkNative(path)) { 377 // Restore previous state 378 handleSinkStateChange(device, mAudioDevices.get(device), state); 379 return false; 380 } 381 return true; 382 } 383 384 public synchronized boolean suspendSink(BluetoothDevice device) { 385 mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, 386 "Need BLUETOOTH_ADMIN permission"); 387 if (DBG) log("suspendSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState); 388 if (device == null || mAudioDevices == null) { 389 return false; 390 } 391 String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); 392 Integer state = mAudioDevices.get(device); 393 if (path == null || state == null) { 394 return false; 395 } 396 397 mTargetA2dpState = BluetoothA2dp.STATE_CONNECTED; 398 return checkSinkSuspendState(state.intValue()); 399 } 400 401 public synchronized boolean resumeSink(BluetoothDevice device) { 402 mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, 403 "Need BLUETOOTH_ADMIN permission"); 404 if (DBG) log("resumeSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState); 405 if (device == null || mAudioDevices == null) { 406 return false; 407 } 408 String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); 409 Integer state = mAudioDevices.get(device); 410 if (path == null || state == null) { 411 return false; 412 } 413 mTargetA2dpState = BluetoothA2dp.STATE_PLAYING; 414 return checkSinkSuspendState(state.intValue()); 415 } 416 417 public synchronized BluetoothDevice[] getConnectedSinks() { 418 mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 419 Set<BluetoothDevice> sinks = lookupSinksMatchingStates( 420 new int[] {BluetoothA2dp.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING}); 421 return sinks.toArray(new BluetoothDevice[sinks.size()]); 422 } 423 424 public synchronized BluetoothDevice[] getNonDisconnectedSinks() { 425 mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 426 Set<BluetoothDevice> sinks = lookupSinksMatchingStates( 427 new int[] {BluetoothA2dp.STATE_CONNECTED, 428 BluetoothA2dp.STATE_PLAYING, 429 BluetoothA2dp.STATE_CONNECTING, 430 BluetoothA2dp.STATE_DISCONNECTING}); 431 return sinks.toArray(new BluetoothDevice[sinks.size()]); 432 } 433 434 public synchronized int getSinkState(BluetoothDevice device) { 435 mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 436 Integer state = mAudioDevices.get(device); 437 if (state == null) 438 return BluetoothA2dp.STATE_DISCONNECTED; 439 return state; 440 } 441 442 public synchronized int getSinkPriority(BluetoothDevice device) { 443 mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 444 return Settings.Secure.getInt(mContext.getContentResolver(), 445 Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), 446 BluetoothA2dp.PRIORITY_UNDEFINED); 447 } 448 449 public synchronized boolean setSinkPriority(BluetoothDevice device, int priority) { 450 mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, 451 "Need BLUETOOTH_ADMIN permission"); 452 if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { 453 return false; 454 } 455 return Settings.Secure.putInt(mContext.getContentResolver(), 456 Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority); 457 } 458 459 private synchronized void onSinkPropertyChanged(String path, String []propValues) { 460 if (!mBluetoothService.isEnabled()) { 461 return; 462 } 463 464 String name = propValues[0]; 465 String address = mBluetoothService.getAddressFromObjectPath(path); 466 if (address == null) { 467 Log.e(TAG, "onSinkPropertyChanged: Address of the remote device in null"); 468 return; 469 } 470 471 BluetoothDevice device = mAdapter.getRemoteDevice(address); 472 473 if (name.equals(PROPERTY_STATE)) { 474 int state = convertBluezSinkStringtoState(propValues[1]); 475 if (mAudioDevices.get(device) == null) { 476 // This is for an incoming connection for a device not known to us. 477 // We have authorized it and bluez state has changed. 478 addAudioSink(device); 479 } else { 480 int prevState = mAudioDevices.get(device); 481 handleSinkStateChange(device, prevState, state); 482 } 483 } 484 } 485 486 private void handleSinkStateChange(BluetoothDevice device, int prevState, int state) { 487 if (state != prevState) { 488 if (state == BluetoothA2dp.STATE_DISCONNECTED || 489 state == BluetoothA2dp.STATE_DISCONNECTING) { 490 mSinkCount--; 491 } else if (state == BluetoothA2dp.STATE_CONNECTED) { 492 mSinkCount ++; 493 } 494 mAudioDevices.put(device, state); 495 496 checkSinkSuspendState(state); 497 mTargetA2dpState = -1; 498 499 if (getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF && 500 state == BluetoothA2dp.STATE_CONNECTING || 501 state == BluetoothA2dp.STATE_CONNECTED) { 502 // We have connected or attempting to connect. 503 // Bump priority 504 setSinkPriority(device, BluetoothA2dp.PRIORITY_AUTO_CONNECT); 505 } 506 507 Intent intent = new Intent(BluetoothA2dp.ACTION_SINK_STATE_CHANGED); 508 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 509 intent.putExtra(BluetoothA2dp.EXTRA_PREVIOUS_SINK_STATE, prevState); 510 intent.putExtra(BluetoothA2dp.EXTRA_SINK_STATE, state); 511 mContext.sendBroadcast(intent, BLUETOOTH_PERM); 512 513 if (DBG) log("A2DP state : device: " + device + " State:" + prevState + "->" + state); 514 } 515 } 516 517 private synchronized Set<BluetoothDevice> lookupSinksMatchingStates(int[] states) { 518 Set<BluetoothDevice> sinks = new HashSet<BluetoothDevice>(); 519 if (mAudioDevices.isEmpty()) { 520 return sinks; 521 } 522 for (BluetoothDevice device: mAudioDevices.keySet()) { 523 int sinkState = getSinkState(device); 524 for (int state : states) { 525 if (state == sinkState) { 526 sinks.add(device); 527 break; 528 } 529 } 530 } 531 return sinks; 532 } 533 534 private boolean checkSinkSuspendState(int state) { 535 boolean result = true; 536 537 if (state != mTargetA2dpState) { 538 if (state == BluetoothA2dp.STATE_PLAYING && 539 mTargetA2dpState == BluetoothA2dp.STATE_CONNECTED) { 540 mAudioManager.setParameters("A2dpSuspended=true"); 541 } else if (state == BluetoothA2dp.STATE_CONNECTED && 542 mTargetA2dpState == BluetoothA2dp.STATE_PLAYING) { 543 mAudioManager.setParameters("A2dpSuspended=false"); 544 } else { 545 result = false; 546 } 547 } 548 return result; 549 } 550 551 private void onConnectSinkResult(String deviceObjectPath, boolean result) { 552 // If the call was a success, ignore we will update the state 553 // when we a Sink Property Change 554 if (!result) { 555 if (deviceObjectPath != null) { 556 String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); 557 BluetoothDevice device = mAdapter.getRemoteDevice(address); 558 int state = getSinkState(device); 559 handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); 560 } 561 } 562 } 563 564 @Override 565 protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 566 if (mAudioDevices.isEmpty()) return; 567 pw.println("Cached audio devices:"); 568 for (BluetoothDevice device : mAudioDevices.keySet()) { 569 int state = mAudioDevices.get(device); 570 pw.println(device + " " + BluetoothA2dp.stateToString(state)); 571 } 572 } 573 574 private static void log(String msg) { 575 Log.d(TAG, msg); 576 } 577 578 private native boolean initNative(); 579 private native void cleanupNative(); 580 private synchronized native boolean connectSinkNative(String path); 581 private synchronized native boolean disconnectSinkNative(String path); 582 private synchronized native boolean suspendSinkNative(String path); 583 private synchronized native boolean resumeSinkNative(String path); 584 private synchronized native Object []getSinkPropertiesNative(String path); 585 private synchronized native boolean avrcpVolumeUpNative(String path); 586 private synchronized native boolean avrcpVolumeDownNative(String path); 587 } 588