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