1 /* 2 * Copyright (C) 2017 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 package com.android.server.wifi; 17 18 19 import android.annotation.NonNull; 20 import android.content.Context; 21 import android.hardware.wifi.hostapd.V1_0.HostapdStatus; 22 import android.hardware.wifi.hostapd.V1_0.HostapdStatusCode; 23 import android.hardware.wifi.hostapd.V1_0.IHostapd; 24 import android.hidl.manager.V1_0.IServiceManager; 25 import android.hidl.manager.V1_0.IServiceNotification; 26 import android.net.wifi.WifiConfiguration; 27 import android.os.Handler; 28 import android.os.HwRemoteBinder; 29 import android.os.Looper; 30 import android.os.RemoteException; 31 import android.util.Log; 32 33 import com.android.internal.R; 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.server.wifi.WifiNative.HostapdDeathEventHandler; 36 import com.android.server.wifi.util.NativeUtil; 37 38 import java.util.ArrayList; 39 import java.util.HashMap; 40 import java.util.List; 41 import java.util.NoSuchElementException; 42 43 import javax.annotation.concurrent.ThreadSafe; 44 45 /** 46 * To maintain thread-safety, the locking protocol is that every non-static method (regardless of 47 * access level) acquires mLock. 48 */ 49 @ThreadSafe 50 public class HostapdHal { 51 private static final String TAG = "HostapdHal"; 52 @VisibleForTesting 53 public static final String HAL_INSTANCE_NAME = "default"; 54 55 private final Object mLock = new Object(); 56 private boolean mVerboseLoggingEnabled = false; 57 private final Handler mEventHandler; 58 private final boolean mEnableAcs; 59 private final boolean mEnableIeee80211AC; 60 private final List<android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange> 61 mAcsChannelRanges; 62 63 // Hostapd HAL interface objects 64 private IServiceManager mIServiceManager = null; 65 private IHostapd mIHostapd; 66 private HashMap<String, WifiNative.SoftApListener> mSoftApListeners = new HashMap<>(); 67 private HostapdDeathEventHandler mDeathEventHandler; 68 private ServiceManagerDeathRecipient mServiceManagerDeathRecipient; 69 private HostapdDeathRecipient mHostapdDeathRecipient; 70 // Death recipient cookie registered for current supplicant instance. 71 private long mDeathRecipientCookie = 0; 72 73 private final IServiceNotification mServiceNotificationCallback = 74 new IServiceNotification.Stub() { 75 public void onRegistration(String fqName, String name, boolean preexisting) { 76 synchronized (mLock) { 77 if (mVerboseLoggingEnabled) { 78 Log.i(TAG, "IServiceNotification.onRegistration for: " + fqName 79 + ", " + name + " preexisting=" + preexisting); 80 } 81 if (!initHostapdService()) { 82 Log.e(TAG, "initalizing IHostapd failed."); 83 hostapdServiceDiedHandler(mDeathRecipientCookie); 84 } else { 85 Log.i(TAG, "Completed initialization of IHostapd."); 86 } 87 } 88 } 89 }; 90 private class ServiceManagerDeathRecipient implements HwRemoteBinder.DeathRecipient { 91 @Override serviceDied(long cookie)92 public void serviceDied(long cookie) { 93 mEventHandler.post(() -> { 94 synchronized (mLock) { 95 Log.w(TAG, "IServiceManager died: cookie=" + cookie); 96 hostapdServiceDiedHandler(mDeathRecipientCookie); 97 mIServiceManager = null; // Will need to register a new ServiceNotification 98 } 99 }); 100 } 101 } 102 private class HostapdDeathRecipient implements HwRemoteBinder.DeathRecipient { 103 @Override serviceDied(long cookie)104 public void serviceDied(long cookie) { 105 mEventHandler.post(() -> { 106 synchronized (mLock) { 107 Log.w(TAG, "IHostapd/IHostapd died: cookie=" + cookie); 108 hostapdServiceDiedHandler(cookie); 109 } 110 }); 111 } 112 } 113 HostapdHal(Context context, Looper looper)114 public HostapdHal(Context context, Looper looper) { 115 mEventHandler = new Handler(looper); 116 mEnableAcs = context.getResources().getBoolean(R.bool.config_wifi_softap_acs_supported); 117 mEnableIeee80211AC = 118 context.getResources().getBoolean(R.bool.config_wifi_softap_ieee80211ac_supported); 119 mAcsChannelRanges = toAcsChannelRanges(context.getResources().getString( 120 R.string.config_wifi_softap_acs_supported_channel_list)); 121 122 mServiceManagerDeathRecipient = new ServiceManagerDeathRecipient(); 123 mHostapdDeathRecipient = new HostapdDeathRecipient(); 124 } 125 126 /** 127 * Enable/Disable verbose logging. 128 * 129 * @param enable true to enable, false to disable. 130 */ enableVerboseLogging(boolean enable)131 void enableVerboseLogging(boolean enable) { 132 synchronized (mLock) { 133 mVerboseLoggingEnabled = enable; 134 } 135 } 136 137 /** 138 * Uses the IServiceManager to check if the device is running V1_1 of the HAL from the VINTF for 139 * the device. 140 * @return true if supported, false otherwise. 141 */ isV1_1()142 private boolean isV1_1() { 143 synchronized (mLock) { 144 if (mIServiceManager == null) { 145 Log.e(TAG, "isV1_1: called but mServiceManager is null!?"); 146 return false; 147 } 148 try { 149 return (mIServiceManager.getTransport( 150 android.hardware.wifi.hostapd.V1_1.IHostapd.kInterfaceName, 151 HAL_INSTANCE_NAME) 152 != IServiceManager.Transport.EMPTY); 153 } catch (RemoteException e) { 154 Log.e(TAG, "Exception while operating on IServiceManager: " + e); 155 handleRemoteException(e, "getTransport"); 156 return false; 157 } 158 } 159 } 160 161 /** 162 * Link to death for IServiceManager object. 163 * @return true on success, false otherwise. 164 */ linkToServiceManagerDeath()165 private boolean linkToServiceManagerDeath() { 166 synchronized (mLock) { 167 if (mIServiceManager == null) return false; 168 try { 169 if (!mIServiceManager.linkToDeath(mServiceManagerDeathRecipient, 0)) { 170 Log.wtf(TAG, "Error on linkToDeath on IServiceManager"); 171 hostapdServiceDiedHandler(mDeathRecipientCookie); 172 mIServiceManager = null; // Will need to register a new ServiceNotification 173 return false; 174 } 175 } catch (RemoteException e) { 176 Log.e(TAG, "IServiceManager.linkToDeath exception", e); 177 mIServiceManager = null; // Will need to register a new ServiceNotification 178 return false; 179 } 180 return true; 181 } 182 } 183 184 /** 185 * Registers a service notification for the IHostapd service, which triggers intialization of 186 * the IHostapd 187 * @return true if the service notification was successfully registered 188 */ initialize()189 public boolean initialize() { 190 synchronized (mLock) { 191 if (mVerboseLoggingEnabled) { 192 Log.i(TAG, "Registering IHostapd service ready callback."); 193 } 194 mIHostapd = null; 195 if (mIServiceManager != null) { 196 // Already have an IServiceManager and serviceNotification registered, don't 197 // don't register another. 198 return true; 199 } 200 try { 201 mIServiceManager = getServiceManagerMockable(); 202 if (mIServiceManager == null) { 203 Log.e(TAG, "Failed to get HIDL Service Manager"); 204 return false; 205 } 206 if (!linkToServiceManagerDeath()) { 207 return false; 208 } 209 /* TODO(b/33639391) : Use the new IHostapd.registerForNotifications() once it 210 exists */ 211 if (!mIServiceManager.registerForNotifications( 212 IHostapd.kInterfaceName, "", mServiceNotificationCallback)) { 213 Log.e(TAG, "Failed to register for notifications to " 214 + IHostapd.kInterfaceName); 215 mIServiceManager = null; // Will need to register a new ServiceNotification 216 return false; 217 } 218 } catch (RemoteException e) { 219 Log.e(TAG, "Exception while trying to register a listener for IHostapd service: " 220 + e); 221 hostapdServiceDiedHandler(mDeathRecipientCookie); 222 mIServiceManager = null; // Will need to register a new ServiceNotification 223 return false; 224 } 225 return true; 226 } 227 } 228 229 /** 230 * Link to death for IHostapd object. 231 * @return true on success, false otherwise. 232 */ linkToHostapdDeath()233 private boolean linkToHostapdDeath() { 234 synchronized (mLock) { 235 if (mIHostapd == null) return false; 236 try { 237 if (!mIHostapd.linkToDeath(mHostapdDeathRecipient, ++mDeathRecipientCookie)) { 238 Log.wtf(TAG, "Error on linkToDeath on IHostapd"); 239 hostapdServiceDiedHandler(mDeathRecipientCookie); 240 return false; 241 } 242 } catch (RemoteException e) { 243 Log.e(TAG, "IHostapd.linkToDeath exception", e); 244 return false; 245 } 246 return true; 247 } 248 } 249 registerCallback( android.hardware.wifi.hostapd.V1_1.IHostapdCallback callback)250 private boolean registerCallback( 251 android.hardware.wifi.hostapd.V1_1.IHostapdCallback callback) { 252 synchronized (mLock) { 253 String methodStr = "registerCallback_1_1"; 254 try { 255 android.hardware.wifi.hostapd.V1_1.IHostapd iHostapdV1_1 = getHostapdMockableV1_1(); 256 if (iHostapdV1_1 == null) return false; 257 HostapdStatus status = iHostapdV1_1.registerCallback(callback); 258 return checkStatusAndLogFailure(status, methodStr); 259 } catch (RemoteException e) { 260 handleRemoteException(e, methodStr); 261 return false; 262 } 263 } 264 } 265 266 /** 267 * Initialize the IHostapd object. 268 * @return true on success, false otherwise. 269 */ initHostapdService()270 private boolean initHostapdService() { 271 synchronized (mLock) { 272 try { 273 mIHostapd = getHostapdMockable(); 274 } catch (RemoteException e) { 275 Log.e(TAG, "IHostapd.getService exception: " + e); 276 return false; 277 } catch (NoSuchElementException e) { 278 Log.e(TAG, "IHostapd.getService exception: " + e); 279 return false; 280 } 281 if (mIHostapd == null) { 282 Log.e(TAG, "Got null IHostapd service. Stopping hostapd HIDL startup"); 283 return false; 284 } 285 if (!linkToHostapdDeath()) { 286 mIHostapd = null; 287 return false; 288 } 289 // Register for callbacks for 1.1 hostapd. 290 if (isV1_1() && !registerCallback(new HostapdCallback())) { 291 mIHostapd = null; 292 return false; 293 } 294 } 295 return true; 296 } 297 298 /** 299 * Add and start a new access point. 300 * 301 * @param ifaceName Name of the interface. 302 * @param config Configuration to use for the AP. 303 * @param listener Callback for AP events. 304 * @return true on success, false otherwise. 305 */ addAccessPoint(@onNull String ifaceName, @NonNull WifiConfiguration config, @NonNull WifiNative.SoftApListener listener)306 public boolean addAccessPoint(@NonNull String ifaceName, @NonNull WifiConfiguration config, 307 @NonNull WifiNative.SoftApListener listener) { 308 synchronized (mLock) { 309 final String methodStr = "addAccessPoint"; 310 IHostapd.IfaceParams ifaceParams = new IHostapd.IfaceParams(); 311 ifaceParams.ifaceName = ifaceName; 312 ifaceParams.hwModeParams.enable80211N = true; 313 ifaceParams.hwModeParams.enable80211AC = mEnableIeee80211AC; 314 try { 315 ifaceParams.channelParams.band = getBand(config); 316 } catch (IllegalArgumentException e) { 317 Log.e(TAG, "Unrecognized apBand " + config.apBand); 318 return false; 319 } 320 if (mEnableAcs) { 321 ifaceParams.channelParams.enableAcs = true; 322 ifaceParams.channelParams.acsShouldExcludeDfs = true; 323 } else { 324 // Downgrade IHostapd.Band.BAND_ANY to IHostapd.Band.BAND_2_4_GHZ if ACS 325 // is not supported. 326 // We should remove this workaround once channel selection is moved from 327 // ApConfigUtil to here. 328 if (ifaceParams.channelParams.band == IHostapd.Band.BAND_ANY) { 329 Log.d(TAG, "ACS is not supported on this device, using 2.4 GHz band."); 330 ifaceParams.channelParams.band = IHostapd.Band.BAND_2_4_GHZ; 331 } 332 ifaceParams.channelParams.enableAcs = false; 333 ifaceParams.channelParams.channel = config.apChannel; 334 } 335 336 IHostapd.NetworkParams nwParams = new IHostapd.NetworkParams(); 337 // TODO(b/67745880) Note that config.SSID is intended to be either a 338 // hex string or "double quoted". 339 // However, it seems that whatever is handing us these configurations does not obey 340 // this convention. 341 nwParams.ssid.addAll(NativeUtil.stringToByteArrayList(config.SSID)); 342 nwParams.isHidden = config.hiddenSSID; 343 nwParams.encryptionType = getEncryptionType(config); 344 nwParams.pskPassphrase = (config.preSharedKey != null) ? config.preSharedKey : ""; 345 if (!checkHostapdAndLogFailure(methodStr)) return false; 346 try { 347 HostapdStatus status; 348 if (isV1_1()) { 349 android.hardware.wifi.hostapd.V1_1.IHostapd.IfaceParams ifaceParams1_1 = 350 new android.hardware.wifi.hostapd.V1_1.IHostapd.IfaceParams(); 351 ifaceParams1_1.V1_0 = ifaceParams; 352 if (mEnableAcs) { 353 ifaceParams1_1.channelParams.acsChannelRanges.addAll(mAcsChannelRanges); 354 } 355 android.hardware.wifi.hostapd.V1_1.IHostapd iHostapdV1_1 = 356 getHostapdMockableV1_1(); 357 if (iHostapdV1_1 == null) return false; 358 status = iHostapdV1_1.addAccessPoint_1_1(ifaceParams1_1, nwParams); 359 } else { 360 status = mIHostapd.addAccessPoint(ifaceParams, nwParams); 361 } 362 if (!checkStatusAndLogFailure(status, methodStr)) { 363 return false; 364 } 365 mSoftApListeners.put(ifaceName, listener); 366 return true; 367 } catch (RemoteException e) { 368 handleRemoteException(e, methodStr); 369 return false; 370 } 371 } 372 } 373 374 /** 375 * Remove a previously started access point. 376 * 377 * @param ifaceName Name of the interface. 378 * @return true on success, false otherwise. 379 */ removeAccessPoint(@onNull String ifaceName)380 public boolean removeAccessPoint(@NonNull String ifaceName) { 381 synchronized (mLock) { 382 final String methodStr = "removeAccessPoint"; 383 if (!checkHostapdAndLogFailure(methodStr)) return false; 384 try { 385 HostapdStatus status = mIHostapd.removeAccessPoint(ifaceName); 386 if (!checkStatusAndLogFailure(status, methodStr)) { 387 return false; 388 } 389 mSoftApListeners.remove(ifaceName); 390 return true; 391 } catch (RemoteException e) { 392 handleRemoteException(e, methodStr); 393 return false; 394 } 395 } 396 } 397 398 /** 399 * Registers a death notification for hostapd. 400 * @return Returns true on success. 401 */ registerDeathHandler(@onNull HostapdDeathEventHandler handler)402 public boolean registerDeathHandler(@NonNull HostapdDeathEventHandler handler) { 403 if (mDeathEventHandler != null) { 404 Log.e(TAG, "Death handler already present"); 405 } 406 mDeathEventHandler = handler; 407 return true; 408 } 409 410 /** 411 * Deregisters a death notification for hostapd. 412 * @return Returns true on success. 413 */ deregisterDeathHandler()414 public boolean deregisterDeathHandler() { 415 if (mDeathEventHandler == null) { 416 Log.e(TAG, "No Death handler present"); 417 } 418 mDeathEventHandler = null; 419 return true; 420 } 421 422 /** 423 * Clear internal state. 424 */ clearState()425 private void clearState() { 426 synchronized (mLock) { 427 mIHostapd = null; 428 } 429 } 430 431 /** 432 * Handle hostapd death. 433 */ hostapdServiceDiedHandler(long cookie)434 private void hostapdServiceDiedHandler(long cookie) { 435 synchronized (mLock) { 436 if (mDeathRecipientCookie != cookie) { 437 Log.i(TAG, "Ignoring stale death recipient notification"); 438 return; 439 } 440 clearState(); 441 if (mDeathEventHandler != null) { 442 mDeathEventHandler.onDeath(); 443 } 444 } 445 } 446 447 /** 448 * Signals whether Initialization completed successfully. 449 */ isInitializationStarted()450 public boolean isInitializationStarted() { 451 synchronized (mLock) { 452 return mIServiceManager != null; 453 } 454 } 455 456 /** 457 * Signals whether Initialization completed successfully. 458 */ isInitializationComplete()459 public boolean isInitializationComplete() { 460 synchronized (mLock) { 461 return mIHostapd != null; 462 } 463 } 464 465 /** 466 * Start the hostapd daemon. 467 * 468 * @return true on success, false otherwise. 469 */ startDaemon()470 public boolean startDaemon() { 471 synchronized (mLock) { 472 try { 473 // This should startup hostapd daemon using the lazy start HAL mechanism. 474 getHostapdMockable(); 475 } catch (RemoteException e) { 476 Log.e(TAG, "Exception while trying to start hostapd: " 477 + e); 478 hostapdServiceDiedHandler(mDeathRecipientCookie); 479 return false; 480 } catch (NoSuchElementException e) { 481 // We're starting the daemon, so expect |NoSuchElementException|. 482 Log.d(TAG, "Successfully triggered start of hostapd using HIDL"); 483 } 484 return true; 485 } 486 } 487 488 /** 489 * Terminate the hostapd daemon. 490 */ terminate()491 public void terminate() { 492 synchronized (mLock) { 493 final String methodStr = "terminate"; 494 if (!checkHostapdAndLogFailure(methodStr)) return; 495 try { 496 mIHostapd.terminate(); 497 } catch (RemoteException e) { 498 handleRemoteException(e, methodStr); 499 } 500 } 501 } 502 503 /** 504 * Wrapper functions to access static HAL methods, created to be mockable in unit tests 505 */ 506 @VisibleForTesting getServiceManagerMockable()507 protected IServiceManager getServiceManagerMockable() throws RemoteException { 508 synchronized (mLock) { 509 return IServiceManager.getService(); 510 } 511 } 512 513 @VisibleForTesting getHostapdMockable()514 protected IHostapd getHostapdMockable() throws RemoteException { 515 synchronized (mLock) { 516 return IHostapd.getService(); 517 } 518 } 519 520 @VisibleForTesting getHostapdMockableV1_1()521 protected android.hardware.wifi.hostapd.V1_1.IHostapd getHostapdMockableV1_1() 522 throws RemoteException { 523 synchronized (mLock) { 524 try { 525 return android.hardware.wifi.hostapd.V1_1.IHostapd.castFrom(mIHostapd); 526 } catch (NoSuchElementException e) { 527 Log.e(TAG, "Failed to get IHostapd", e); 528 return null; 529 } 530 } 531 } 532 getEncryptionType(WifiConfiguration localConfig)533 private static int getEncryptionType(WifiConfiguration localConfig) { 534 int encryptionType; 535 switch (localConfig.getAuthType()) { 536 case WifiConfiguration.KeyMgmt.NONE: 537 encryptionType = IHostapd.EncryptionType.NONE; 538 break; 539 case WifiConfiguration.KeyMgmt.WPA_PSK: 540 encryptionType = IHostapd.EncryptionType.WPA; 541 break; 542 case WifiConfiguration.KeyMgmt.WPA2_PSK: 543 encryptionType = IHostapd.EncryptionType.WPA2; 544 break; 545 default: 546 // We really shouldn't default to None, but this was how NetworkManagementService 547 // used to do this. 548 encryptionType = IHostapd.EncryptionType.NONE; 549 break; 550 } 551 return encryptionType; 552 } 553 getBand(WifiConfiguration localConfig)554 private static int getBand(WifiConfiguration localConfig) { 555 int bandType; 556 switch (localConfig.apBand) { 557 case WifiConfiguration.AP_BAND_2GHZ: 558 bandType = IHostapd.Band.BAND_2_4_GHZ; 559 break; 560 case WifiConfiguration.AP_BAND_5GHZ: 561 bandType = IHostapd.Band.BAND_5_GHZ; 562 break; 563 case WifiConfiguration.AP_BAND_ANY: 564 bandType = IHostapd.Band.BAND_ANY; 565 break; 566 default: 567 throw new IllegalArgumentException(); 568 } 569 return bandType; 570 } 571 572 /** 573 * Convert channel list string like '1-6,11' to list of AcsChannelRanges 574 */ 575 private List<android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange> toAcsChannelRanges(String channelListStr)576 toAcsChannelRanges(String channelListStr) { 577 ArrayList<android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange> acsChannelRanges = 578 new ArrayList<>(); 579 String[] channelRanges = channelListStr.split(","); 580 for (String channelRange : channelRanges) { 581 android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange acsChannelRange = 582 new android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange(); 583 try { 584 if (channelRange.contains("-")) { 585 String[] channels = channelRange.split("-"); 586 if (channels.length != 2) { 587 Log.e(TAG, "Unrecognized channel range, length is " + channels.length); 588 continue; 589 } 590 int start = Integer.parseInt(channels[0]); 591 int end = Integer.parseInt(channels[1]); 592 if (start > end) { 593 Log.e(TAG, "Invalid channel range, from " + start + " to " + end); 594 continue; 595 } 596 acsChannelRange.start = start; 597 acsChannelRange.end = end; 598 } else { 599 acsChannelRange.start = Integer.parseInt(channelRange); 600 acsChannelRange.end = acsChannelRange.start; 601 } 602 } catch (NumberFormatException e) { 603 // Ignore malformed value 604 Log.e(TAG, "Malformed channel value detected: " + e); 605 continue; 606 } 607 acsChannelRanges.add(acsChannelRange); 608 } 609 return acsChannelRanges; 610 } 611 612 /** 613 * Returns false if Hostapd is null, and logs failure to call methodStr 614 */ checkHostapdAndLogFailure(String methodStr)615 private boolean checkHostapdAndLogFailure(String methodStr) { 616 synchronized (mLock) { 617 if (mIHostapd == null) { 618 Log.e(TAG, "Can't call " + methodStr + ", IHostapd is null"); 619 return false; 620 } 621 return true; 622 } 623 } 624 625 /** 626 * Returns true if provided status code is SUCCESS, logs debug message and returns false 627 * otherwise 628 */ checkStatusAndLogFailure(HostapdStatus status, String methodStr)629 private boolean checkStatusAndLogFailure(HostapdStatus status, 630 String methodStr) { 631 synchronized (mLock) { 632 if (status.code != HostapdStatusCode.SUCCESS) { 633 Log.e(TAG, "IHostapd." + methodStr + " failed: " + status.code 634 + ", " + status.debugMessage); 635 return false; 636 } else { 637 if (mVerboseLoggingEnabled) { 638 Log.d(TAG, "IHostapd." + methodStr + " succeeded"); 639 } 640 return true; 641 } 642 } 643 } 644 handleRemoteException(RemoteException e, String methodStr)645 private void handleRemoteException(RemoteException e, String methodStr) { 646 synchronized (mLock) { 647 hostapdServiceDiedHandler(mDeathRecipientCookie); 648 Log.e(TAG, "IHostapd." + methodStr + " failed with exception", e); 649 } 650 } 651 652 private class HostapdCallback extends 653 android.hardware.wifi.hostapd.V1_1.IHostapdCallback.Stub { 654 @Override onFailure(String ifaceName)655 public void onFailure(String ifaceName) { 656 Log.w(TAG, "Failure on iface " + ifaceName); 657 WifiNative.SoftApListener listener = mSoftApListeners.get(ifaceName); 658 if (listener != null) { 659 listener.onFailure(); 660 } 661 } 662 } 663 } 664