TelephonyConnection.java revision 3f7f717a2295f9ae5cc6a34ed404ec22e94012da
1/* 2 * Copyright (C) 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.services.telephony; 18 19import android.net.Uri; 20import android.os.AsyncResult; 21import android.os.Handler; 22import android.os.Message; 23import android.telecom.AudioState; 24import android.telecom.Connection; 25import android.telecom.PhoneAccount; 26import android.telecom.PhoneCapabilities; 27 28import com.android.internal.telephony.Call; 29import com.android.internal.telephony.CallStateException; 30import com.android.internal.telephony.Connection.PostDialListener; 31import com.android.internal.telephony.Phone; 32import com.android.internal.telephony.imsphone.ImsPhoneConnection; 33 34import java.lang.Override; 35import java.util.Objects; 36 37/** 38 * Base class for CDMA and GSM connections. 39 */ 40abstract class TelephonyConnection extends Connection { 41 private static final int MSG_PRECISE_CALL_STATE_CHANGED = 1; 42 private static final int MSG_RINGBACK_TONE = 2; 43 private static final int MSG_HANDOVER_STATE_CHANGED = 3; 44 private static final int MSG_DISCONNECT = 4; 45 46 private final Handler mHandler = new Handler() { 47 @Override 48 public void handleMessage(Message msg) { 49 switch (msg.what) { 50 case MSG_PRECISE_CALL_STATE_CHANGED: 51 Log.v(TelephonyConnection.this, "MSG_PRECISE_CALL_STATE_CHANGED"); 52 updateState(); 53 break; 54 case MSG_HANDOVER_STATE_CHANGED: 55 Log.v(TelephonyConnection.this, "MSG_HANDOVER_STATE_CHANGED"); 56 AsyncResult ar = (AsyncResult) msg.obj; 57 com.android.internal.telephony.Connection connection = 58 (com.android.internal.telephony.Connection) ar.result; 59 setOriginalConnection(connection); 60 break; 61 case MSG_RINGBACK_TONE: 62 Log.v(TelephonyConnection.this, "MSG_RINGBACK_TONE"); 63 // TODO: This code assumes that there is only one connection in the foreground 64 // call, in other words, it punts on network-mediated conference calling. 65 if (getOriginalConnection() != getForegroundConnection()) { 66 Log.v(TelephonyConnection.this, "handleMessage, original connection is " + 67 "not foreground connection, skipping"); 68 return; 69 } 70 setRingbackRequested((Boolean) ((AsyncResult) msg.obj).result); 71 break; 72 case MSG_DISCONNECT: 73 updateState(); 74 break; 75 } 76 } 77 }; 78 79 private final PostDialListener mPostDialListener = new PostDialListener() { 80 @Override 81 public void onPostDialWait() { 82 Log.v(TelephonyConnection.this, "onPostDialWait"); 83 if (mOriginalConnection != null) { 84 setPostDialWait(mOriginalConnection.getRemainingPostDialString()); 85 } 86 } 87 }; 88 89 /** 90 * Listener for listening to events in the {@link com.android.internal.telephony.Connection}. 91 */ 92 private final com.android.internal.telephony.Connection.Listener mOriginalConnectionListener = 93 new com.android.internal.telephony.Connection.ListenerBase() { 94 @Override 95 public void onVideoStateChanged(int videoState) { 96 setVideoState(videoState); 97 } 98 99 /** 100 * The {@link com.android.internal.telephony.Connection} has reported a change in local 101 * video capability. 102 * 103 * @param capable True if capable. 104 */ 105 @Override 106 public void onLocalVideoCapabilityChanged(boolean capable) { 107 setLocalVideoCapable(capable); 108 } 109 110 /** 111 * The {@link com.android.internal.telephony.Connection} has reported a change in remote 112 * video capability. 113 * 114 * @param capable True if capable. 115 */ 116 @Override 117 public void onRemoteVideoCapabilityChanged(boolean capable) { 118 setRemoteVideoCapable(capable); 119 } 120 121 /** 122 * The {@link com.android.internal.telephony.Connection} has reported a change in the 123 * video call provider. 124 * 125 * @param videoProvider The video call provider. 126 */ 127 @Override 128 public void onVideoProviderChanged(VideoProvider videoProvider) { 129 setVideoProvider(videoProvider); 130 } 131 132 /** 133 * Used by the {@link com.android.internal.telephony.Connection} to report a change in the 134 * audio quality for the current call. 135 * 136 * @param audioQuality The audio quality. 137 */ 138 @Override 139 public void onAudioQualityChanged(int audioQuality) { 140 setAudioQuality(audioQuality); 141 } 142 }; 143 144 private com.android.internal.telephony.Connection mOriginalConnection; 145 private Call.State mOriginalConnectionState = Call.State.IDLE; 146 147 /** 148 * Determines if the {@link TelephonyConnection} has local video capabilities. 149 * This is used when {@link TelephonyConnection#updateCallCapabilities()}} is called, 150 * ensuring the appropriate {@link PhoneCapabilities} are set. Since {@link PhoneCapabilities} 151 * can be rebuilt at any time it is necessary to track the video capabilities between rebuild. 152 * The {@link PhoneCapabilities} (including video capabilities) are communicated to the telecom 153 * layer. 154 */ 155 private boolean mLocalVideoCapable; 156 157 /** 158 * Determines if the {@link TelephonyConnection} has remote video capabilities. 159 * This is used when {@link TelephonyConnection#updateCallCapabilities()}} is called, 160 * ensuring the appropriate {@link PhoneCapabilities} are set. Since {@link PhoneCapabilities} 161 * can be rebuilt at any time it is necessary to track the video capabilities between rebuild. 162 * The {@link PhoneCapabilities} (including video capabilities) are communicated to the telecom 163 * layer. 164 */ 165 private boolean mRemoteVideoCapable; 166 167 /** 168 * Determines the current audio quality for the {@link TelephonyConnection}. 169 * This is used when {@link TelephonyConnection#updateCallCapabilities}} is called to indicate 170 * whether a call has the {@link android.telecom.CallCapabilities#VoLTE} capability. 171 */ 172 private int mAudioQuality; 173 174 protected TelephonyConnection(com.android.internal.telephony.Connection originalConnection) { 175 if (originalConnection != null) { 176 setOriginalConnection(originalConnection); 177 } 178 } 179 180 @Override 181 public void onAudioStateChanged(AudioState audioState) { 182 // TODO: update TTY mode. 183 if (getPhone() != null) { 184 getPhone().setEchoSuppressionEnabled(); 185 } 186 } 187 188 @Override 189 public void onStateChanged(int state) { 190 Log.v(this, "onStateChanged, state: " + Connection.stateToString(state)); 191 } 192 193 @Override 194 public void onDisconnect() { 195 Log.v(this, "onDisconnect"); 196 hangup(android.telephony.DisconnectCause.LOCAL); 197 } 198 199 @Override 200 public void onSeparate() { 201 Log.v(this, "onSeparate"); 202 if (mOriginalConnection != null) { 203 try { 204 mOriginalConnection.separate(); 205 } catch (CallStateException e) { 206 Log.e(this, e, "Call to Connection.separate failed with exception"); 207 } 208 } 209 } 210 211 @Override 212 public void onAbort() { 213 Log.v(this, "onAbort"); 214 hangup(android.telephony.DisconnectCause.LOCAL); 215 } 216 217 @Override 218 public void onHold() { 219 performHold(); 220 } 221 222 @Override 223 public void onUnhold() { 224 performUnhold(); 225 } 226 227 @Override 228 public void onAnswer(int videoState) { 229 Log.v(this, "onAnswer"); 230 if (isValidRingingCall() && getPhone() != null) { 231 try { 232 getPhone().acceptCall(videoState); 233 } catch (CallStateException e) { 234 Log.e(this, e, "Failed to accept call."); 235 } 236 } 237 } 238 239 @Override 240 public void onReject() { 241 Log.v(this, "onReject"); 242 if (isValidRingingCall()) { 243 hangup(android.telephony.DisconnectCause.INCOMING_REJECTED); 244 } 245 super.onReject(); 246 } 247 248 @Override 249 public void onPostDialContinue(boolean proceed) { 250 Log.v(this, "onPostDialContinue, proceed: " + proceed); 251 if (mOriginalConnection != null) { 252 if (proceed) { 253 mOriginalConnection.proceedAfterWaitChar(); 254 } else { 255 mOriginalConnection.cancelPostDial(); 256 } 257 } 258 } 259 260 public void performHold() { 261 Log.v(this, "performHold"); 262 // TODO: Can dialing calls be put on hold as well since they take up the 263 // foreground call slot? 264 if (Call.State.ACTIVE == mOriginalConnectionState) { 265 Log.v(this, "Holding active call"); 266 try { 267 Phone phone = mOriginalConnection.getCall().getPhone(); 268 Call ringingCall = phone.getRingingCall(); 269 270 // Although the method says switchHoldingAndActive, it eventually calls a RIL method 271 // called switchWaitingOrHoldingAndActive. What this means is that if we try to put 272 // a call on hold while a call-waiting call exists, it'll end up accepting the 273 // call-waiting call, which is bad if that was not the user's intention. We are 274 // cheating here and simply skipping it because we know any attempt to hold a call 275 // while a call-waiting call is happening is likely a request from Telecom prior to 276 // accepting the call-waiting call. 277 // TODO: Investigate a better solution. It would be great here if we 278 // could "fake" hold by silencing the audio and microphone streams for this call 279 // instead of actually putting it on hold. 280 if (ringingCall.getState() != Call.State.WAITING) { 281 phone.switchHoldingAndActive(); 282 } 283 284 // TODO: Cdma calls are slightly different. 285 } catch (CallStateException e) { 286 Log.e(this, e, "Exception occurred while trying to put call on hold."); 287 } 288 } else { 289 Log.w(this, "Cannot put a call that is not currently active on hold."); 290 } 291 } 292 293 public void performUnhold() { 294 Log.v(this, "performUnhold"); 295 if (Call.State.HOLDING == mOriginalConnectionState) { 296 try { 297 // Here's the deal--Telephony hold/unhold is weird because whenever there exists 298 // more than one call, one of them must always be active. In other words, if you 299 // have an active call and holding call, and you put the active call on hold, it 300 // will automatically activate the holding call. This is weird with how Telecom 301 // sends its commands. When a user opts to "unhold" a background call, telecom 302 // issues hold commands to all active calls, and then the unhold command to the 303 // background call. This means that we get two commands...each of which reduces to 304 // switchHoldingAndActive(). The result is that they simply cancel each other out. 305 // To fix this so that it works well with telecom we add a minor hack. If we 306 // have one telephony call, everything works as normally expected. But if we have 307 // two or more calls, we will ignore all requests to "unhold" knowing that the hold 308 // requests already do what we want. If you've read up to this point, I'm very sorry 309 // that we are doing this. I didn't think of a better solution that wouldn't also 310 // make the Telecom APIs very ugly. 311 312 if (!hasMultipleTopLevelCalls()) { 313 mOriginalConnection.getCall().getPhone().switchHoldingAndActive(); 314 } else { 315 Log.i(this, "Skipping unhold command for %s", this); 316 } 317 } catch (CallStateException e) { 318 Log.e(this, e, "Exception occurred while trying to release call from hold."); 319 } 320 } else { 321 Log.w(this, "Cannot release a call that is not already on hold from hold."); 322 } 323 } 324 325 public void performConference(TelephonyConnection otherConnection) {} 326 327 protected abstract int buildCallCapabilities(); 328 329 protected final void updateCallCapabilities() { 330 int newCallCapabilities = buildCallCapabilities(); 331 newCallCapabilities = applyVideoCapabilities(newCallCapabilities); 332 newCallCapabilities = applyAudioQualityCapabilities(newCallCapabilities); 333 newCallCapabilities = applyConferenceTerminationCapabilities(newCallCapabilities); 334 335 if (getCallCapabilities() != newCallCapabilities) { 336 setCallCapabilities(newCallCapabilities); 337 } 338 } 339 340 protected final void updateAddress() { 341 updateCallCapabilities(); 342 if (mOriginalConnection != null) { 343 Uri address = getAddressFromNumber(mOriginalConnection.getAddress()); 344 int presentation = mOriginalConnection.getNumberPresentation(); 345 if (!Objects.equals(address, getAddress()) || 346 presentation != getAddressPresentation()) { 347 Log.v(this, "updateAddress, address changed"); 348 setAddress(address, presentation); 349 } 350 351 String name = mOriginalConnection.getCnapName(); 352 int namePresentation = mOriginalConnection.getCnapNamePresentation(); 353 if (!Objects.equals(name, getCallerDisplayName()) || 354 namePresentation != getCallerDisplayNamePresentation()) { 355 Log.v(this, "updateAddress, caller display name changed"); 356 setCallerDisplayName(name, namePresentation); 357 } 358 } 359 } 360 361 void onRemovedFromCallService() { 362 // Subclass can override this to do cleanup. 363 } 364 365 void setOriginalConnection(com.android.internal.telephony.Connection originalConnection) { 366 Log.v(this, "new TelephonyConnection, originalConnection: " + originalConnection); 367 if (mOriginalConnection != null) { 368 getPhone().unregisterForPreciseCallStateChanged(mHandler); 369 getPhone().unregisterForRingbackTone(mHandler); 370 getPhone().unregisterForHandoverStateChanged(mHandler); 371 getPhone().unregisterForDisconnect(mHandler); 372 } 373 mOriginalConnection = originalConnection; 374 getPhone().registerForPreciseCallStateChanged( 375 mHandler, MSG_PRECISE_CALL_STATE_CHANGED, null); 376 getPhone().registerForHandoverStateChanged( 377 mHandler, MSG_HANDOVER_STATE_CHANGED, null); 378 getPhone().registerForRingbackTone(mHandler, MSG_RINGBACK_TONE, null); 379 getPhone().registerForDisconnect(mHandler, MSG_DISCONNECT, null); 380 mOriginalConnection.addPostDialListener(mPostDialListener); 381 mOriginalConnection.addListener(mOriginalConnectionListener); 382 383 // Set video state and capabilities 384 setVideoState(mOriginalConnection.getVideoState()); 385 setLocalVideoCapable(mOriginalConnection.isLocalVideoCapable()); 386 setRemoteVideoCapable(mOriginalConnection.isRemoteVideoCapable()); 387 setVideoProvider(mOriginalConnection.getVideoProvider()); 388 setAudioQuality(mOriginalConnection.getAudioQuality()); 389 390 updateState(); 391 } 392 393 protected void hangup(int telephonyDisconnectCode) { 394 if (mOriginalConnection != null) { 395 try { 396 // Hanging up a ringing call requires that we invoke call.hangup() as opposed to 397 // connection.hangup(). Without this change, the party originating the call will not 398 // get sent to voicemail if the user opts to reject the call. 399 if (isValidRingingCall()) { 400 Call call = getCall(); 401 if (call != null) { 402 call.hangup(); 403 } else { 404 Log.w(this, "Attempting to hangup a connection without backing call."); 405 } 406 } else { 407 // We still prefer to call connection.hangup() for non-ringing calls in order 408 // to support hanging-up specific calls within a conference call. If we invoked 409 // call.hangup() while in a conference, we would end up hanging up the entire 410 // conference call instead of the specific connection. 411 mOriginalConnection.hangup(); 412 } 413 } catch (CallStateException e) { 414 Log.e(this, e, "Call to Connection.hangup failed with exception"); 415 } 416 } 417 } 418 419 com.android.internal.telephony.Connection getOriginalConnection() { 420 return mOriginalConnection; 421 } 422 423 protected Call getCall() { 424 if (mOriginalConnection != null) { 425 return mOriginalConnection.getCall(); 426 } 427 return null; 428 } 429 430 Phone getPhone() { 431 Call call = getCall(); 432 if (call != null) { 433 return call.getPhone(); 434 } 435 return null; 436 } 437 438 private boolean hasMultipleTopLevelCalls() { 439 int numCalls = 0; 440 Phone phone = getPhone(); 441 if (phone != null) { 442 if (!phone.getRingingCall().isIdle()) { 443 numCalls++; 444 } 445 if (!phone.getForegroundCall().isIdle()) { 446 numCalls++; 447 } 448 if (!phone.getBackgroundCall().isIdle()) { 449 numCalls++; 450 } 451 } 452 return numCalls > 1; 453 } 454 455 private com.android.internal.telephony.Connection getForegroundConnection() { 456 if (getPhone() != null) { 457 return getPhone().getForegroundCall().getEarliestConnection(); 458 } 459 return null; 460 } 461 462 /** 463 * Checks to see the original connection corresponds to an active incoming call. Returns false 464 * if there is no such actual call, or if the associated call is not incoming (See 465 * {@link Call.State#isRinging}). 466 */ 467 private boolean isValidRingingCall() { 468 if (getPhone() == null) { 469 Log.v(this, "isValidRingingCall, phone is null"); 470 return false; 471 } 472 473 Call ringingCall = getPhone().getRingingCall(); 474 if (!ringingCall.getState().isRinging()) { 475 Log.v(this, "isValidRingingCall, ringing call is not in ringing state"); 476 return false; 477 } 478 479 if (ringingCall.getEarliestConnection() != mOriginalConnection) { 480 Log.v(this, "isValidRingingCall, ringing call connection does not match"); 481 return false; 482 } 483 484 Log.v(this, "isValidRingingCall, returning true"); 485 return true; 486 } 487 488 protected void updateState() { 489 if (mOriginalConnection == null) { 490 return; 491 } 492 493 Call.State newState = mOriginalConnection.getState(); 494 Log.v(this, "Update state from %s to %s for %s", mOriginalConnectionState, newState, this); 495 if (mOriginalConnectionState != newState) { 496 mOriginalConnectionState = newState; 497 switch (newState) { 498 case IDLE: 499 break; 500 case ACTIVE: 501 setActive(); 502 break; 503 case HOLDING: 504 setOnHold(); 505 break; 506 case DIALING: 507 case ALERTING: 508 setDialing(); 509 break; 510 case INCOMING: 511 case WAITING: 512 setRinging(); 513 break; 514 case DISCONNECTED: 515 setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause( 516 mOriginalConnection.getDisconnectCause())); 517 close(); 518 break; 519 case DISCONNECTING: 520 break; 521 } 522 } 523 updateCallCapabilities(); 524 updateAddress(); 525 } 526 527 private void close() { 528 Log.v(this, "close"); 529 if (getPhone() != null) { 530 getPhone().unregisterForPreciseCallStateChanged(mHandler); 531 getPhone().unregisterForRingbackTone(mHandler); 532 getPhone().unregisterForHandoverStateChanged(mHandler); 533 } 534 mOriginalConnection = null; 535 destroy(); 536 } 537 538 /** 539 * Applies the video capability states to the CallCapabilities bit-mask. 540 * 541 * @param capabilities The CallCapabilities bit-mask. 542 * @return The capabilities with video capabilities applied. 543 */ 544 private int applyVideoCapabilities(int capabilities) { 545 int currentCapabilities = capabilities; 546 if (mRemoteVideoCapable) { 547 currentCapabilities = applyCapability(currentCapabilities, 548 PhoneCapabilities.SUPPORTS_VT_REMOTE); 549 } else { 550 currentCapabilities = removeCapability(currentCapabilities, 551 PhoneCapabilities.SUPPORTS_VT_REMOTE); 552 } 553 554 if (mLocalVideoCapable) { 555 currentCapabilities = applyCapability(currentCapabilities, 556 PhoneCapabilities.SUPPORTS_VT_LOCAL); 557 } else { 558 currentCapabilities = removeCapability(currentCapabilities, 559 PhoneCapabilities.SUPPORTS_VT_LOCAL); 560 } 561 return currentCapabilities; 562 } 563 564 /** 565 * Applies the audio capabilities to the {@code CallCapabilities} bit-mask. A call with high 566 * definition audio is considered to have the {@code VoLTE} call capability as VoLTE uses high 567 * definition audio. 568 * 569 * @param callCapabilities The {@code CallCapabilities} bit-mask. 570 * @return The capabilities with the audio capabilities applied. 571 */ 572 private int applyAudioQualityCapabilities(int callCapabilities) { 573 int currentCapabilities = callCapabilities; 574 575 if (mAudioQuality == 576 com.android.internal.telephony.Connection.AUDIO_QUALITY_HIGH_DEFINITION) { 577 currentCapabilities = applyCapability(currentCapabilities, PhoneCapabilities.VoLTE); 578 } else { 579 currentCapabilities = removeCapability(currentCapabilities, PhoneCapabilities.VoLTE); 580 } 581 582 return currentCapabilities; 583 } 584 585 /** 586 * Applies capabilities specific to conferences termination to the 587 * {@code CallCapabilities} bit-mask. 588 * 589 * @param callCapabilities The {@code CallCapabilities} bit-mask. 590 * @return The capabilities with the IMS conference capabilities applied. 591 */ 592 private int applyConferenceTerminationCapabilities(int callCapabilities) { 593 int currentCapabilities = callCapabilities; 594 595 // An IMS call cannot be individually disconnected or separated from its parent conference 596 boolean isImsCall = getOriginalConnection() instanceof ImsPhoneConnection; 597 if (!isImsCall) { 598 currentCapabilities |= 599 PhoneCapabilities.DISCONNECT_FROM_CONFERENCE 600 | PhoneCapabilities.SEPARATE_FROM_CONFERENCE; 601 } 602 603 return currentCapabilities; 604 } 605 606 /** 607 * Returns the local video capability state for the connection. 608 * 609 * @return {@code True} if the connection has local video capabilities. 610 */ 611 public boolean isLocalVideoCapable() { 612 return mLocalVideoCapable; 613 } 614 615 /** 616 * Returns the remote video capability state for the connection. 617 * 618 * @return {@code True} if the connection has remote video capabilities. 619 */ 620 public boolean isRemoteVideoCapable() { 621 return mRemoteVideoCapable; 622 } 623 624 /** 625 * Sets whether video capability is present locally. Used during rebuild of the 626 * {@link PhoneCapabilities} to set the video call capabilities. 627 * 628 * @param capable {@code True} if video capable. 629 */ 630 public void setLocalVideoCapable(boolean capable) { 631 mLocalVideoCapable = capable; 632 updateCallCapabilities(); 633 } 634 635 /** 636 * Sets whether video capability is present remotely. Used during rebuild of the 637 * {@link PhoneCapabilities} to set the video call capabilities. 638 * 639 * @param capable {@code True} if video capable. 640 */ 641 public void setRemoteVideoCapable(boolean capable) { 642 mRemoteVideoCapable = capable; 643 updateCallCapabilities(); 644 } 645 646 /** 647 * Sets the current call audio quality. Used during rebuild of the 648 * {@link PhoneCapabilities} to set or unset the {@link PhoneCapabilities#VoLTE} capability. 649 * 650 * @param audioQuality The audio quality. 651 */ 652 public void setAudioQuality(int audioQuality) { 653 mAudioQuality = audioQuality; 654 updateCallCapabilities(); 655 } 656 657 /** 658 * Obtains the current call audio quality. 659 */ 660 public int getAudioQuality() { 661 return mAudioQuality; 662 } 663 664 void resetStateForConference() { 665 if (getState() == Connection.STATE_HOLDING) { 666 if (mOriginalConnection.getState() == Call.State.ACTIVE) { 667 setActive(); 668 } 669 } 670 } 671 672 boolean setHoldingForConference() { 673 if (getState() == Connection.STATE_ACTIVE) { 674 setOnHold(); 675 return true; 676 } 677 return false; 678 } 679 680 private static Uri getAddressFromNumber(String number) { 681 // Address can be null for blocked calls. 682 if (number == null) { 683 number = ""; 684 } 685 return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null); 686 } 687 688 /** 689 * Applies a capability to a capabilities bit-mask. 690 * 691 * @param capabilities The capabilities bit-mask. 692 * @param capability The capability to apply. 693 * @return The capabilities bit-mask with the capability applied. 694 */ 695 private int applyCapability(int capabilities, int capability) { 696 int newCapabilities = capabilities | capability; 697 return newCapabilities; 698 } 699 700 /** 701 * Removes a capability from a capabilities bit-mask. 702 * 703 * @param capabilities The capabilities bit-mask. 704 * @param capability The capability to remove. 705 * @return The capabilities bit-mask with the capability removed. 706 */ 707 private int removeCapability(int capabilities, int capability) { 708 int newCapabilities = capabilities & ~capability; 709 return newCapabilities; 710 } 711} 712