1 /* 2 * Copyright (C) 2020 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 com.android.server.wifi; 18 19 import static com.android.server.wifi.ActiveModeManager.ROLE_CLIENT_PRIMARY; 20 import static com.android.server.wifi.ActiveModeManager.ROLE_CLIENT_SECONDARY_TRANSIENT; 21 22 import android.annotation.IntDef; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.annotation.TestApi; 26 import android.content.Context; 27 import android.util.Log; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.server.wifi.proto.nano.WifiMetricsProto; 31 32 import java.io.FileDescriptor; 33 import java.io.PrintWriter; 34 import java.lang.annotation.Retention; 35 import java.lang.annotation.RetentionPolicy; 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** 40 * Manages Make-Before-Break connection switching. 41 */ 42 public class MakeBeforeBreakManager { 43 private static final String TAG = "WifiMbbManager"; 44 45 private final ActiveModeWarden mActiveModeWarden; 46 private final FrameworkFacade mFrameworkFacade; 47 private final Context mContext; 48 private final ClientModeImplMonitor mCmiMonitor; 49 private final ClientModeManagerBroadcastQueue mBroadcastQueue; 50 private final WifiMetrics mWifiMetrics; 51 52 private final List<Runnable> mOnAllSecondaryTransientCmmsStoppedListeners = new ArrayList<>(); 53 private boolean mVerboseLoggingEnabled = false; 54 private @MbbInternalState int mInternalState = MBB_STATE_NONE; 55 56 /** No MBB has been initiated. Package private. */ 57 @VisibleForTesting 58 static final int MBB_STATE_NONE = 0; 59 60 /** Client manager with ROLE_CLIENT_SECONDARY_TRANSIENT has been created for MBB and trying 61 * to connect the new network. */ 62 @VisibleForTesting 63 static final int MBB_STATE_SECONDARY_TRANSIENT_CREATED = 1; 64 65 /** MBB has got the captive portal detected. */ 66 @VisibleForTesting 67 static final int MBB_STATE_CAPTIVE_PORTAL_DETECTED = 2; 68 69 /** MBB has got the internet validation on new network, start the role switch */ 70 @VisibleForTesting 71 static final int MBB_STATE_INTERNET_VALIDATED = 3; 72 73 /** MBB has got the internet validation failed notification. */ 74 @VisibleForTesting 75 static final int MBB_STATE_VALIDATION_FAILED = 4; 76 77 /** MBB has got the role changed event when both two roles are secondary transient. This 78 * happens in the middle of the old/new primary role switch */ 79 @VisibleForTesting 80 static final int MBB_STATE_ROLES_BEING_SWITCHED_BOTH_SECONDARY_TRANSIENT = 5; 81 82 /** MBB has got the role changed event when one role is primary and anther is secondary 83 * transient. This happens when the old/new primary role switch is done. */ 84 @VisibleForTesting 85 static final int MBB_STATE_ROLE_SWITCH_COMPLETE = 6; 86 87 /** @hide */ 88 @Retention(RetentionPolicy.SOURCE) 89 @IntDef(prefix = {"MBB_STATE_"}, value = { 90 MBB_STATE_NONE, 91 MBB_STATE_SECONDARY_TRANSIENT_CREATED, 92 MBB_STATE_CAPTIVE_PORTAL_DETECTED, 93 MBB_STATE_INTERNET_VALIDATED, 94 MBB_STATE_ROLES_BEING_SWITCHED_BOTH_SECONDARY_TRANSIENT, 95 MBB_STATE_ROLE_SWITCH_COMPLETE}) 96 private @interface MbbInternalState {} 97 98 private static class MakeBeforeBreakInfo { 99 @NonNull 100 public final ConcreteClientModeManager oldPrimary; 101 @NonNull 102 public final ConcreteClientModeManager newPrimary; 103 MakeBeforeBreakInfo( @onNull ConcreteClientModeManager oldPrimary, @NonNull ConcreteClientModeManager newPrimary)104 MakeBeforeBreakInfo( 105 @NonNull ConcreteClientModeManager oldPrimary, 106 @NonNull ConcreteClientModeManager newPrimary) { 107 this.oldPrimary = oldPrimary; 108 this.newPrimary = newPrimary; 109 } 110 111 @Override toString()112 public String toString() { 113 return "MakeBeforeBreakInfo{" 114 + "oldPrimary=" + oldPrimary 115 + ", newPrimary=" + newPrimary 116 + '}'; 117 } 118 } 119 120 @Nullable 121 private MakeBeforeBreakInfo mMakeBeforeBreakInfo = null; 122 MakeBeforeBreakManager( @onNull ActiveModeWarden activeModeWarden, @NonNull FrameworkFacade frameworkFacade, @NonNull Context context, @NonNull ClientModeImplMonitor cmiMonitor, @NonNull ClientModeManagerBroadcastQueue broadcastQueue, @NonNull WifiMetrics wifiMetrics)123 public MakeBeforeBreakManager( 124 @NonNull ActiveModeWarden activeModeWarden, 125 @NonNull FrameworkFacade frameworkFacade, 126 @NonNull Context context, 127 @NonNull ClientModeImplMonitor cmiMonitor, 128 @NonNull ClientModeManagerBroadcastQueue broadcastQueue, 129 @NonNull WifiMetrics wifiMetrics) { 130 mActiveModeWarden = activeModeWarden; 131 mFrameworkFacade = frameworkFacade; 132 mContext = context; 133 mCmiMonitor = cmiMonitor; 134 mBroadcastQueue = broadcastQueue; 135 mWifiMetrics = wifiMetrics; 136 137 mActiveModeWarden.registerModeChangeCallback(new ModeChangeCallback()); 138 mCmiMonitor.registerListener(new ClientModeImplListener() { 139 @Override 140 public void onInternetValidated(@NonNull ConcreteClientModeManager clientModeManager) { 141 MakeBeforeBreakManager.this.onInternetValidated(clientModeManager); 142 } 143 144 @Override 145 public void onCaptivePortalDetected( 146 @NonNull ConcreteClientModeManager clientModeManager) { 147 MakeBeforeBreakManager.this.onCaptivePortalDetected(clientModeManager); 148 } 149 150 @Override 151 public void onInternetValidationFailed( 152 @NonNull ConcreteClientModeManager clientModeManager, 153 boolean currentConnectionDetectedCaptivePortal) { 154 MakeBeforeBreakManager.this.onInternetValidationFailed(clientModeManager, 155 currentConnectionDetectedCaptivePortal); 156 } 157 }); 158 } 159 setVerboseLoggingEnabled(boolean enabled)160 public void setVerboseLoggingEnabled(boolean enabled) { 161 mVerboseLoggingEnabled = enabled; 162 } 163 164 private class ModeChangeCallback implements ActiveModeWarden.ModeChangeCallback { 165 @Override onActiveModeManagerAdded(@onNull ActiveModeManager activeModeManager)166 public void onActiveModeManagerAdded(@NonNull ActiveModeManager activeModeManager) { 167 if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) { 168 return; 169 } 170 if (!(activeModeManager instanceof ConcreteClientModeManager)) { 171 return; 172 } 173 // just in case 174 recoverPrimary(); 175 if (activeModeManager.getRole() == ROLE_CLIENT_SECONDARY_TRANSIENT) { 176 transitionToState(MBB_STATE_SECONDARY_TRANSIENT_CREATED); 177 } else { 178 Log.w(TAG, " MBB expects ROLE_CLIENT_SECONDARY_TRANSIENT but got " 179 + activeModeManager.getRole()); 180 } 181 } 182 183 @Override onActiveModeManagerRemoved(@onNull ActiveModeManager activeModeManager)184 public void onActiveModeManagerRemoved(@NonNull ActiveModeManager activeModeManager) { 185 if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) { 186 return; 187 } 188 if (!(activeModeManager instanceof ConcreteClientModeManager)) { 189 return; 190 } 191 // if either the old or new primary stopped during MBB, abort the MBB attempt 192 ConcreteClientModeManager clientModeManager = 193 (ConcreteClientModeManager) activeModeManager; 194 if (mVerboseLoggingEnabled) { 195 if (mInternalState == MBB_STATE_VALIDATION_FAILED) { 196 Log.w(TAG, " ClientModeManager " + clientModeManager + " removed because" 197 + " internet validation failed."); 198 } 199 } 200 if (mMakeBeforeBreakInfo != null) { 201 boolean oldPrimaryStopped = clientModeManager == mMakeBeforeBreakInfo.oldPrimary; 202 boolean newPrimaryStopped = clientModeManager == mMakeBeforeBreakInfo.newPrimary; 203 if (oldPrimaryStopped || newPrimaryStopped) { 204 Log.i(TAG, "MBB CMM stopped, aborting:" 205 + " oldPrimary=" + mMakeBeforeBreakInfo.oldPrimary 206 + " stopped=" + oldPrimaryStopped 207 + " newPrimary=" + mMakeBeforeBreakInfo.newPrimary 208 + " stopped=" + newPrimaryStopped); 209 mMakeBeforeBreakInfo = null; 210 } 211 } 212 recoverPrimary(); 213 triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms(); 214 transitionToState(MBB_STATE_NONE); 215 } 216 217 @Override onActiveModeManagerRoleChanged(@onNull ActiveModeManager activeModeManager)218 public void onActiveModeManagerRoleChanged(@NonNull ActiveModeManager activeModeManager) { 219 if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) { 220 return; 221 } 222 if (!(activeModeManager instanceof ConcreteClientModeManager)) { 223 return; 224 } 225 ConcreteClientModeManager clientModeManager = 226 (ConcreteClientModeManager) activeModeManager; 227 recoverPrimary(); 228 maybeContinueMakeBeforeBreak(clientModeManager); 229 triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms(); 230 } 231 } 232 getInternalStateStr(@bbInternalState int state)233 private String getInternalStateStr(@MbbInternalState int state) { 234 switch (state) { 235 case MBB_STATE_NONE: 236 return "MBB_STATE_NONE"; 237 case MBB_STATE_SECONDARY_TRANSIENT_CREATED: 238 return "MBB_STATE_SECONDARY_TRANSIENT_CREATED"; 239 case MBB_STATE_CAPTIVE_PORTAL_DETECTED: 240 return "MBB_STATE_CAPTIVE_PORTAL_DETECTED"; 241 case MBB_STATE_INTERNET_VALIDATED: 242 return "MBB_STATE_INTERNET_VALIDATED"; 243 case MBB_STATE_ROLES_BEING_SWITCHED_BOTH_SECONDARY_TRANSIENT: 244 return "MBB_STATE_ROLES_BEING_SWITCHED_BOTH_SECONDARY_TRANSIENT"; 245 case MBB_STATE_ROLE_SWITCH_COMPLETE: 246 return "MBB_STATE_ROLE_SWITCH_COMPLETE"; 247 default: 248 return "UNKNOWN MBB_STATE"; 249 } 250 } 251 transitionToState(@bbInternalState int state)252 private void transitionToState(@MbbInternalState int state) { 253 if (mVerboseLoggingEnabled) { 254 Log.v(TAG, "MBB state transition: from = " + getInternalStateStr(mInternalState) 255 + ", to = " + getInternalStateStr(state)); 256 } 257 mInternalState = state; 258 } 259 260 // Get internal MBB state. Package private. 261 @TestApi getInternalState()262 @MbbInternalState int getInternalState() { 263 return mInternalState; 264 } 265 266 /** 267 * Failsafe: if there is no primary CMM but there exists exactly one CMM in 268 * {@link ActiveModeManager#ROLE_CLIENT_SECONDARY_TRANSIENT}, or multiple and MBB is not 269 * in progress (to avoid interfering with MBB), make it primary. 270 */ recoverPrimary()271 private void recoverPrimary() { 272 // already have a primary, do nothing 273 if (mActiveModeWarden.getPrimaryClientModeManagerNullable() != null) { 274 return; 275 } 276 List<ConcreteClientModeManager> secondaryTransientCmms = 277 mActiveModeWarden.getClientModeManagersInRoles(ROLE_CLIENT_SECONDARY_TRANSIENT); 278 // exactly 1 secondary transient, or > 1 secondary transient and MBB is not in progress 279 if (secondaryTransientCmms.size() == 1 280 || (mMakeBeforeBreakInfo == null && secondaryTransientCmms.size() > 1)) { 281 ConcreteClientModeManager manager = secondaryTransientCmms.get(0); 282 manager.setRole(ROLE_CLIENT_PRIMARY, mFrameworkFacade.getSettingsWorkSource(mContext)); 283 Log.i(TAG, "recoveryPrimary kicking in, making " + manager + " primary and stopping" 284 + " all other SECONDARY_TRANSIENT ClientModeManagers"); 285 mWifiMetrics.incrementMakeBeforeBreakRecoverPrimaryCount(); 286 // tear down the extra secondary transient CMMs (if they exist) 287 for (int i = 1; i < secondaryTransientCmms.size(); i++) { 288 secondaryTransientCmms.get(i).stop(); 289 } 290 } 291 } 292 293 /** 294 * A ClientModeImpl instance has been validated to have internet connection. This will begin the 295 * Make-Before-Break transition to make this the new primary network. 296 * 297 * Change the previous primary ClientModeManager to role 298 * {@link ActiveModeManager#ROLE_CLIENT_SECONDARY_TRANSIENT} and change the new 299 * primary to role {@link ActiveModeManager#ROLE_CLIENT_PRIMARY}. 300 * 301 * @param newPrimary the corresponding ConcreteClientModeManager instance for the ClientModeImpl 302 * that had its internet connection validated. 303 */ onInternetValidated(@onNull ConcreteClientModeManager newPrimary)304 private void onInternetValidated(@NonNull ConcreteClientModeManager newPrimary) { 305 if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) { 306 return; 307 } 308 if (newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) { 309 return; 310 } 311 312 ConcreteClientModeManager currentPrimary = 313 mActiveModeWarden.getPrimaryClientModeManagerNullable(); 314 315 if (currentPrimary == null) { 316 Log.e(TAG, "changePrimaryClientModeManager(): current primary CMM is null!"); 317 newPrimary.setRole( 318 ROLE_CLIENT_PRIMARY, mFrameworkFacade.getSettingsWorkSource(mContext)); 319 return; 320 } 321 if (newPrimary.getPreviousRole() == ROLE_CLIENT_PRIMARY) { 322 Log.i(TAG, "Don't start MBB when internet is validated on the lingering " 323 + "secondary."); 324 return; 325 } 326 327 Log.i(TAG, "Starting MBB switch primary from " + currentPrimary + " to " + newPrimary 328 + " by setting current primary's role to ROLE_CLIENT_SECONDARY_TRANSIENT"); 329 330 mWifiMetrics.incrementMakeBeforeBreakInternetValidatedCount(); 331 332 // Since role change is not atomic, we must first make the previous primary CMM into a 333 // secondary transient CMM. Thus, after this call to setRole() completes, there is no 334 // primary CMM and 2 secondary transient CMMs. 335 currentPrimary.setRole( 336 ROLE_CLIENT_SECONDARY_TRANSIENT, ActiveModeWarden.INTERNAL_REQUESTOR_WS); 337 // immediately send fake disconnection broadcasts upon changing primary CMM's role to 338 // SECONDARY_TRANSIENT, because as soon as the CMM becomes SECONDARY_TRANSIENT, its 339 // broadcasts will never be sent out again (BroadcastQueue only sends broadcasts for the 340 // current primary CMM). This is to preserve the legacy single STA behavior. 341 mBroadcastQueue.fakeDisconnectionBroadcasts(); 342 mMakeBeforeBreakInfo = new MakeBeforeBreakInfo(currentPrimary, newPrimary); 343 transitionToState(MBB_STATE_INTERNET_VALIDATED); 344 } 345 346 /** 347 * Notify MBB manager the internet validation has failed. This may come before 348 * onInternetValidated and mMakeBeforeBreakInfo is null. 349 * @param clientModeManager client mode manager for current connection. 350 * @param currentConnectionDetectedCaptivePortal whether current connection has detected 351 * captive portal. 352 */ onInternetValidationFailed(ConcreteClientModeManager clientModeManager, boolean currentConnectionDetectedCaptivePortal)353 private void onInternetValidationFailed(ConcreteClientModeManager clientModeManager, 354 boolean currentConnectionDetectedCaptivePortal) { 355 final ConcreteClientModeManager secondaryCcm = mActiveModeWarden.getClientModeManagerInRole( 356 ROLE_CLIENT_SECONDARY_TRANSIENT); 357 // There has to be at least one ROLE_CLIENT_SECONDARY_TRANSIENT. 358 if (secondaryCcm == null) { 359 Log.w(TAG, "No ClientModeManager with ROLE_CLIENT_SECONDARY_TRANSIENT exist!" 360 + " current state: " + getInternalStateStr(mInternalState)); 361 return; 362 } 363 364 Log.w(TAG, "Internet validation failed during MBB," 365 + " disconnecting ClientModeManager=" + clientModeManager); 366 mWifiMetrics.logStaEvent( 367 clientModeManager.getInterfaceName(), 368 WifiMetricsProto.StaEvent.TYPE_FRAMEWORK_DISCONNECT, 369 WifiMetricsProto.StaEvent.DISCONNECT_MBB_NO_INTERNET); 370 mWifiMetrics.incrementMakeBeforeBreakNoInternetCount(); 371 372 // If the role has already switched, switch the roles back 373 if (clientModeManager.getRole() == ROLE_CLIENT_PRIMARY) { 374 clientModeManager.setRole(ROLE_CLIENT_SECONDARY_TRANSIENT, 375 mFrameworkFacade.getSettingsWorkSource(mContext)); 376 secondaryCcm.setRole(ROLE_CLIENT_PRIMARY, 377 mFrameworkFacade.getSettingsWorkSource(mContext)); 378 } 379 // Disconnect the client mode manager. 380 clientModeManager.disconnect(); 381 382 if (mVerboseLoggingEnabled) { 383 Log.v(TAG, "onInternetValidationFailed (" + getInternalStateStr(mInternalState) 384 + "), removeClientModeManager role: " + clientModeManager.getRole()); 385 } 386 // This should trigger {@link ModeChangeCallback#onActiveModeManagerRemoved} to abort MBB. 387 mActiveModeWarden.removeClientModeManager(clientModeManager); 388 transitionToState(MBB_STATE_VALIDATION_FAILED); 389 } 390 onCaptivePortalDetected(@onNull ConcreteClientModeManager newPrimary)391 private void onCaptivePortalDetected(@NonNull ConcreteClientModeManager newPrimary) { 392 if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) { 393 return; 394 } 395 if (newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) { 396 return; 397 } 398 399 ConcreteClientModeManager currentPrimary = 400 mActiveModeWarden.getPrimaryClientModeManagerNullable(); 401 402 if (currentPrimary == null) { 403 Log.i(TAG, "onCaptivePortalDetected: Current primary is null, nothing to stop"); 404 } else { 405 Log.i(TAG, "onCaptivePortalDetected: stopping current primary CMM"); 406 currentPrimary.setWifiStateChangeBroadcastEnabled(false); 407 currentPrimary.stop(); 408 } 409 // Once the currentPrimary teardown completes, recoverPrimary() will make the Captive 410 // Portal CMM the new primary, because it is the only SECONDARY_TRANSIENT CMM and no 411 // primary CMM exists. 412 transitionToState(MBB_STATE_CAPTIVE_PORTAL_DETECTED); 413 } 414 maybeContinueMakeBeforeBreak( @onNull ConcreteClientModeManager roleChangedClientModeManager)415 private void maybeContinueMakeBeforeBreak( 416 @NonNull ConcreteClientModeManager roleChangedClientModeManager) { 417 // not in the middle of MBB 418 if (mMakeBeforeBreakInfo == null) { 419 // The new STA iface has been changed to primary. 420 transitionToState(MBB_STATE_ROLE_SWITCH_COMPLETE); 421 return; 422 } 423 // not the CMM we're looking for, keep monitoring 424 if (roleChangedClientModeManager != mMakeBeforeBreakInfo.oldPrimary) { 425 return; 426 } 427 try { 428 // if old primary didn't transition to secondary transient, abort the MBB attempt 429 if (mMakeBeforeBreakInfo.oldPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) { 430 Log.i(TAG, "old primary is no longer secondary transient, aborting MBB: " 431 + mMakeBeforeBreakInfo.oldPrimary); 432 return; 433 } 434 435 // if somehow the next primary is no longer secondary transient, abort the MBB attempt 436 if (mMakeBeforeBreakInfo.newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) { 437 Log.i(TAG, "new primary is no longer secondary transient, abort MBB: " 438 + mMakeBeforeBreakInfo.newPrimary); 439 return; 440 } 441 // The old primary has been changed to secondary transient. 442 transitionToState(MBB_STATE_ROLES_BEING_SWITCHED_BOTH_SECONDARY_TRANSIENT); 443 Log.i(TAG, "Continue MBB switch primary from " + mMakeBeforeBreakInfo.oldPrimary 444 + " to " + mMakeBeforeBreakInfo.newPrimary 445 + " by setting new Primary's role to ROLE_CLIENT_PRIMARY and reducing network" 446 + " score of old primary"); 447 448 // TODO(b/180974604): In theory, newPrimary.setRole() could still fail, but that would 449 // still count as a MBB success in the metrics. But we don't really handle that 450 // scenario well anyways, see TODO below. 451 mWifiMetrics.incrementMakeBeforeBreakSuccessCount(); 452 453 // otherwise, actually set the new primary's role to primary. 454 mMakeBeforeBreakInfo.newPrimary.setRole( 455 ROLE_CLIENT_PRIMARY, mFrameworkFacade.getSettingsWorkSource(mContext)); 456 457 // linger old primary 458 // TODO(b/160346062): maybe do this after the new primary was fully transitioned to 459 // ROLE_CLIENT_PRIMARY (since setRole() is asynchronous) 460 mMakeBeforeBreakInfo.oldPrimary.setShouldReduceNetworkScore(true); 461 } finally { 462 // end the MBB attempt 463 mMakeBeforeBreakInfo = null; 464 } 465 } 466 467 /** Dump fields for debugging. */ dump(FileDescriptor fd, PrintWriter pw, String[] args)468 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 469 pw.println("Dump of MakeBeforeBreakManager"); 470 pw.println("mMakeBeforeBreakInfo=" + mMakeBeforeBreakInfo); 471 pw.println("mInternalState " + getInternalStateStr(mInternalState)); 472 } 473 474 /** 475 * Stop all ClientModeManagers with role 476 * {@link ActiveModeManager#ROLE_CLIENT_SECONDARY_TRANSIENT}. 477 * 478 * This is useful when an explicit connection was requested by an external caller 479 * (e.g. Settings, legacy app calling {@link android.net.wifi.WifiManager#enableNetwork}). 480 * We should abort any ongoing Make Before Break attempt to avoid interrupting the explicit 481 * connection. 482 * 483 * @param onStoppedListener triggered when all secondary transient CMMs have been stopped. 484 */ stopAllSecondaryTransientClientModeManagers(Runnable onStoppedListener)485 public void stopAllSecondaryTransientClientModeManagers(Runnable onStoppedListener) { 486 // no secondary transient CMM exists, trigger the callback immediately and return 487 if (mActiveModeWarden.getClientModeManagerInRole(ROLE_CLIENT_SECONDARY_TRANSIENT) == null) { 488 if (mVerboseLoggingEnabled) { 489 Log.d(TAG, "No secondary transient CMM active, trigger callback immediately"); 490 } 491 onStoppedListener.run(); 492 return; 493 } 494 495 // there exists at least 1 secondary transient CMM, but no primary 496 // TODO(b/177692017): Since switching roles is not atomic, there is a short period of time 497 // during the Make Before Break transition when there are 2 SECONDARY_TRANSIENT CMMs and 0 498 // primary CMMs. If this method is called at that time, it will destroy all CMMs, resulting 499 // in no primary, and causing any subsequent connections to fail. Hopefully this does 500 // not occur frequently. 501 if (mActiveModeWarden.getPrimaryClientModeManagerNullable() == null) { 502 Log.wtf(TAG, "Called stopAllSecondaryTransientClientModeManagers with no primary CMM!"); 503 } 504 505 mOnAllSecondaryTransientCmmsStoppedListeners.add(onStoppedListener); 506 mActiveModeWarden.stopAllClientModeManagersInRole(ROLE_CLIENT_SECONDARY_TRANSIENT); 507 } 508 triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms()509 private void triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms() { 510 // not all secondary transient CMMs stopped, keep waiting 511 if (mActiveModeWarden.getClientModeManagerInRole(ROLE_CLIENT_SECONDARY_TRANSIENT) != null) { 512 return; 513 } 514 515 if (mVerboseLoggingEnabled) { 516 Log.i(TAG, "All secondary transient CMMs stopped, triggering queued callbacks"); 517 } 518 519 for (Runnable onStoppedListener : mOnAllSecondaryTransientCmmsStoppedListeners) { 520 onStoppedListener.run(); 521 } 522 mOnAllSecondaryTransientCmmsStoppedListeners.clear(); 523 } 524 525 } 526