• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.NonNull;
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.util.Log;
26 
27 import java.io.FileDescriptor;
28 import java.io.PrintWriter;
29 import java.util.ArrayList;
30 import java.util.List;
31 
32 /**
33  * Manages Make-Before-Break connection switching.
34  */
35 public class MakeBeforeBreakManager {
36     private static final String TAG = "WifiMbbManager";
37 
38     private final ActiveModeWarden mActiveModeWarden;
39     private final FrameworkFacade mFrameworkFacade;
40     private final Context mContext;
41     private final ClientModeImplMonitor mCmiMonitor;
42     private final ClientModeManagerBroadcastQueue mBroadcastQueue;
43     private final WifiMetrics mWifiMetrics;
44 
45     private final List<Runnable> mOnAllSecondaryTransientCmmsStoppedListeners = new ArrayList<>();
46     private boolean mVerboseLoggingEnabled = false;
47 
48     private static class MakeBeforeBreakInfo {
49         @NonNull
50         public final ConcreteClientModeManager oldPrimary;
51         @NonNull
52         public final ConcreteClientModeManager newPrimary;
53 
MakeBeforeBreakInfo( @onNull ConcreteClientModeManager oldPrimary, @NonNull ConcreteClientModeManager newPrimary)54         MakeBeforeBreakInfo(
55                 @NonNull ConcreteClientModeManager oldPrimary,
56                 @NonNull ConcreteClientModeManager newPrimary) {
57             this.oldPrimary = oldPrimary;
58             this.newPrimary = newPrimary;
59         }
60 
61         @Override
toString()62         public String toString() {
63             return "MakeBeforeBreakInfo{"
64                     + "oldPrimary=" + oldPrimary
65                     + ", newPrimary=" + newPrimary
66                     + '}';
67         }
68     }
69 
70     @Nullable
71     private MakeBeforeBreakInfo mMakeBeforeBreakInfo = null;
72 
MakeBeforeBreakManager( @onNull ActiveModeWarden activeModeWarden, @NonNull FrameworkFacade frameworkFacade, @NonNull Context context, @NonNull ClientModeImplMonitor cmiMonitor, @NonNull ClientModeManagerBroadcastQueue broadcastQueue, @NonNull WifiMetrics wifiMetrics)73     public MakeBeforeBreakManager(
74             @NonNull ActiveModeWarden activeModeWarden,
75             @NonNull FrameworkFacade frameworkFacade,
76             @NonNull Context context,
77             @NonNull ClientModeImplMonitor cmiMonitor,
78             @NonNull ClientModeManagerBroadcastQueue broadcastQueue,
79             @NonNull WifiMetrics wifiMetrics) {
80         mActiveModeWarden = activeModeWarden;
81         mFrameworkFacade = frameworkFacade;
82         mContext = context;
83         mCmiMonitor = cmiMonitor;
84         mBroadcastQueue = broadcastQueue;
85         mWifiMetrics = wifiMetrics;
86 
87         mActiveModeWarden.registerModeChangeCallback(new ModeChangeCallback());
88         mCmiMonitor.registerListener(new ClientModeImplListener() {
89             @Override
90             public void onInternetValidated(@NonNull ConcreteClientModeManager clientModeManager) {
91                 MakeBeforeBreakManager.this.onInternetValidated(clientModeManager);
92             }
93 
94             @Override
95             public void onCaptivePortalDetected(
96                     @NonNull ConcreteClientModeManager clientModeManager) {
97                 MakeBeforeBreakManager.this.onCaptivePortalDetected(clientModeManager);
98             }
99         });
100     }
101 
setVerboseLoggingEnabled(boolean enabled)102     public void setVerboseLoggingEnabled(boolean enabled) {
103         mVerboseLoggingEnabled = enabled;
104     }
105 
106     private class ModeChangeCallback implements ActiveModeWarden.ModeChangeCallback {
107         @Override
onActiveModeManagerAdded(@onNull ActiveModeManager activeModeManager)108         public void onActiveModeManagerAdded(@NonNull ActiveModeManager activeModeManager) {
109             if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) {
110                 return;
111             }
112             if (!(activeModeManager instanceof ConcreteClientModeManager)) {
113                 return;
114             }
115             // just in case
116             recoverPrimary();
117         }
118 
119         @Override
onActiveModeManagerRemoved(@onNull ActiveModeManager activeModeManager)120         public void onActiveModeManagerRemoved(@NonNull ActiveModeManager activeModeManager) {
121             if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) {
122                 return;
123             }
124             if (!(activeModeManager instanceof ConcreteClientModeManager)) {
125                 return;
126             }
127             // if either the old or new primary stopped during MBB, abort the MBB attempt
128             ConcreteClientModeManager clientModeManager =
129                     (ConcreteClientModeManager) activeModeManager;
130             if (mMakeBeforeBreakInfo != null) {
131                 boolean oldPrimaryStopped = clientModeManager == mMakeBeforeBreakInfo.oldPrimary;
132                 boolean newPrimaryStopped = clientModeManager == mMakeBeforeBreakInfo.newPrimary;
133                 if (oldPrimaryStopped || newPrimaryStopped) {
134                     Log.i(TAG, "MBB CMM stopped, aborting:"
135                             + " oldPrimary=" + mMakeBeforeBreakInfo.oldPrimary
136                             + " stopped=" + oldPrimaryStopped
137                             + " newPrimary=" + mMakeBeforeBreakInfo.newPrimary
138                             + " stopped=" + newPrimaryStopped);
139                     mMakeBeforeBreakInfo = null;
140                 }
141             }
142             recoverPrimary();
143             triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms();
144         }
145 
146         @Override
onActiveModeManagerRoleChanged(@onNull ActiveModeManager activeModeManager)147         public void onActiveModeManagerRoleChanged(@NonNull ActiveModeManager activeModeManager) {
148             if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) {
149                 return;
150             }
151             if (!(activeModeManager instanceof ConcreteClientModeManager)) {
152                 return;
153             }
154             ConcreteClientModeManager clientModeManager =
155                     (ConcreteClientModeManager) activeModeManager;
156             recoverPrimary();
157             maybeContinueMakeBeforeBreak(clientModeManager);
158             triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms();
159         }
160     }
161 
162     /**
163      * Failsafe: if there is no primary CMM but there exists exactly one CMM in
164      * {@link ActiveModeManager#ROLE_CLIENT_SECONDARY_TRANSIENT}, or multiple and MBB is not
165      * in progress (to avoid interfering with MBB), make it primary.
166      */
recoverPrimary()167     private void recoverPrimary() {
168         // already have a primary, do nothing
169         if (mActiveModeWarden.getPrimaryClientModeManagerNullable() != null) {
170             return;
171         }
172         List<ConcreteClientModeManager> secondaryTransientCmms =
173                 mActiveModeWarden.getClientModeManagersInRoles(ROLE_CLIENT_SECONDARY_TRANSIENT);
174         // exactly 1 secondary transient, or > 1 secondary transient and MBB is not in progress
175         if (secondaryTransientCmms.size() == 1
176                 || (mMakeBeforeBreakInfo == null && secondaryTransientCmms.size() > 1)) {
177             ConcreteClientModeManager manager = secondaryTransientCmms.get(0);
178             manager.setRole(ROLE_CLIENT_PRIMARY, mFrameworkFacade.getSettingsWorkSource(mContext));
179             Log.i(TAG, "recoveryPrimary kicking in, making " + manager + " primary and stopping"
180                     + " all other SECONDARY_TRANSIENT ClientModeManagers");
181             mWifiMetrics.incrementMakeBeforeBreakRecoverPrimaryCount();
182             // tear down the extra secondary transient CMMs (if they exist)
183             for (int i = 1; i < secondaryTransientCmms.size(); i++) {
184                 secondaryTransientCmms.get(i).stop();
185             }
186         }
187     }
188 
189     /**
190      * A ClientModeImpl instance has been validated to have internet connection. This will begin the
191      * Make-Before-Break transition to make this the new primary network.
192      *
193      * Change the previous primary ClientModeManager to role
194      * {@link ActiveModeManager#ROLE_CLIENT_SECONDARY_TRANSIENT} and change the new
195      * primary to role {@link ActiveModeManager#ROLE_CLIENT_PRIMARY}.
196      *
197      * @param newPrimary the corresponding ConcreteClientModeManager instance for the ClientModeImpl
198      *                   that had its internet connection validated.
199      */
onInternetValidated(@onNull ConcreteClientModeManager newPrimary)200     private void onInternetValidated(@NonNull ConcreteClientModeManager newPrimary) {
201         if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) {
202             return;
203         }
204         if (newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) {
205             return;
206         }
207 
208         ConcreteClientModeManager currentPrimary =
209                 mActiveModeWarden.getPrimaryClientModeManagerNullable();
210 
211         if (currentPrimary == null) {
212             Log.e(TAG, "changePrimaryClientModeManager(): current primary CMM is null!");
213             newPrimary.setRole(
214                     ROLE_CLIENT_PRIMARY, mFrameworkFacade.getSettingsWorkSource(mContext));
215             return;
216         }
217         if (newPrimary.getPreviousRole() == ROLE_CLIENT_PRIMARY) {
218             Log.i(TAG, "Don't start MBB when internet is validated on the lingering "
219                     + "secondary.");
220             return;
221         }
222 
223         Log.i(TAG, "Starting MBB switch primary from " + currentPrimary + " to " + newPrimary
224                 + " by setting current primary's role to ROLE_CLIENT_SECONDARY_TRANSIENT");
225 
226         mWifiMetrics.incrementMakeBeforeBreakInternetValidatedCount();
227 
228         // Since role change is not atomic, we must first make the previous primary CMM into a
229         // secondary transient CMM. Thus, after this call to setRole() completes, there is no
230         // primary CMM and 2 secondary transient CMMs.
231         currentPrimary.setRole(
232                 ROLE_CLIENT_SECONDARY_TRANSIENT, ActiveModeWarden.INTERNAL_REQUESTOR_WS);
233         // immediately send fake disconnection broadcasts upon changing primary CMM's role to
234         // SECONDARY_TRANSIENT, because as soon as the CMM becomes SECONDARY_TRANSIENT, its
235         // broadcasts will never be sent out again (BroadcastQueue only sends broadcasts for the
236         // current primary CMM). This is to preserve the legacy single STA behavior.
237         mBroadcastQueue.fakeDisconnectionBroadcasts();
238         mMakeBeforeBreakInfo = new MakeBeforeBreakInfo(currentPrimary, newPrimary);
239     }
240 
onCaptivePortalDetected(@onNull ConcreteClientModeManager newPrimary)241     private void onCaptivePortalDetected(@NonNull ConcreteClientModeManager newPrimary) {
242         if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) {
243             return;
244         }
245         if (newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) {
246             return;
247         }
248 
249         ConcreteClientModeManager currentPrimary =
250                 mActiveModeWarden.getPrimaryClientModeManagerNullable();
251 
252         if (currentPrimary == null) {
253             Log.i(TAG, "onCaptivePortalDetected: Current primary is null, nothing to stop");
254         } else {
255             Log.i(TAG, "onCaptivePortalDetected: stopping current primary CMM");
256             currentPrimary.setWifiStateChangeBroadcastEnabled(false);
257             currentPrimary.stop();
258         }
259         // Once the currentPrimary teardown completes, recoverPrimary() will make the Captive
260         // Portal CMM the new primary, because it is the only SECONDARY_TRANSIENT CMM and no
261         // primary CMM exists.
262     }
263 
maybeContinueMakeBeforeBreak( @onNull ConcreteClientModeManager roleChangedClientModeManager)264     private void maybeContinueMakeBeforeBreak(
265             @NonNull ConcreteClientModeManager roleChangedClientModeManager) {
266         // not in the middle of MBB
267         if (mMakeBeforeBreakInfo == null) {
268             return;
269         }
270         // not the CMM we're looking for, keep monitoring
271         if (roleChangedClientModeManager != mMakeBeforeBreakInfo.oldPrimary) {
272             return;
273         }
274         try {
275             // if old primary didn't transition to secondary transient, abort the MBB attempt
276             if (mMakeBeforeBreakInfo.oldPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) {
277                 Log.i(TAG, "old primary is no longer secondary transient, aborting MBB: "
278                         + mMakeBeforeBreakInfo.oldPrimary);
279                 return;
280             }
281 
282             // if somehow the next primary is no longer secondary transient, abort the MBB attempt
283             if (mMakeBeforeBreakInfo.newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) {
284                 Log.i(TAG, "new primary is no longer secondary transient, abort MBB: "
285                         + mMakeBeforeBreakInfo.newPrimary);
286                 return;
287             }
288 
289             Log.i(TAG, "Continue MBB switch primary from " + mMakeBeforeBreakInfo.oldPrimary
290                     + " to " + mMakeBeforeBreakInfo.newPrimary
291                     + " by setting new Primary's role to ROLE_CLIENT_PRIMARY and reducing network"
292                     + " score");
293 
294             // TODO(b/180974604): In theory, newPrimary.setRole() could still fail, but that would
295             //  still count as a MBB success in the metrics. But we don't really handle that
296             //  scenario well anyways, see TODO below.
297             mWifiMetrics.incrementMakeBeforeBreakSuccessCount();
298 
299             // otherwise, actually set the new primary's role to primary.
300             mMakeBeforeBreakInfo.newPrimary.setRole(
301                     ROLE_CLIENT_PRIMARY, mFrameworkFacade.getSettingsWorkSource(mContext));
302 
303             // linger old primary
304             // TODO(b/160346062): maybe do this after the new primary was fully transitioned to
305             //  ROLE_CLIENT_PRIMARY (since setRole() is asynchronous)
306             mMakeBeforeBreakInfo.oldPrimary.setShouldReduceNetworkScore(true);
307         } finally {
308             // end the MBB attempt
309             mMakeBeforeBreakInfo = null;
310         }
311     }
312 
313     /** Dump fields for debugging. */
dump(FileDescriptor fd, PrintWriter pw, String[] args)314     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
315         pw.println("Dump of MakeBeforeBreakManager");
316         pw.println("mMakeBeforeBreakInfo=" + mMakeBeforeBreakInfo);
317     }
318 
319     /**
320      * Stop all ClientModeManagers with role
321      * {@link ActiveModeManager#ROLE_CLIENT_SECONDARY_TRANSIENT}.
322      *
323      * This is useful when an explicit connection was requested by an external caller
324      * (e.g. Settings, legacy app calling {@link android.net.wifi.WifiManager#enableNetwork}).
325      * We should abort any ongoing Make Before Break attempt to avoid interrupting the explicit
326      * connection.
327      *
328      * @param onStoppedListener triggered when all secondary transient CMMs have been stopped.
329      */
stopAllSecondaryTransientClientModeManagers(Runnable onStoppedListener)330     public void stopAllSecondaryTransientClientModeManagers(Runnable onStoppedListener) {
331         // no secondary transient CMM exists, trigger the callback immediately and return
332         if (mActiveModeWarden.getClientModeManagerInRole(ROLE_CLIENT_SECONDARY_TRANSIENT) == null) {
333             if (mVerboseLoggingEnabled) {
334                 Log.d(TAG, "No secondary transient CMM active, trigger callback immediately");
335             }
336             onStoppedListener.run();
337             return;
338         }
339 
340         // there exists at least 1 secondary transient CMM, but no primary
341         // TODO(b/177692017): Since switching roles is not atomic, there is a short period of time
342         //  during the Make Before Break transition when there are 2 SECONDARY_TRANSIENT CMMs and 0
343         //  primary CMMs. If this method is called at that time, it will destroy all CMMs, resulting
344         //  in no primary, and causing any subsequent connections to fail. Hopefully this does
345         //  not occur frequently.
346         if (mActiveModeWarden.getPrimaryClientModeManagerNullable() == null) {
347             Log.wtf(TAG, "Called stopAllSecondaryTransientClientModeManagers with no primary CMM!");
348         }
349 
350         mOnAllSecondaryTransientCmmsStoppedListeners.add(onStoppedListener);
351         mActiveModeWarden.stopAllClientModeManagersInRole(ROLE_CLIENT_SECONDARY_TRANSIENT);
352     }
353 
triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms()354     private void triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms() {
355         // not all secondary transient CMMs stopped, keep waiting
356         if (mActiveModeWarden.getClientModeManagerInRole(ROLE_CLIENT_SECONDARY_TRANSIENT) != null) {
357             return;
358         }
359 
360         if (mVerboseLoggingEnabled) {
361             Log.i(TAG, "All secondary transient CMMs stopped, triggering queued callbacks");
362         }
363 
364         for (Runnable onStoppedListener : mOnAllSecondaryTransientCmmsStoppedListeners) {
365             onStoppedListener.run();
366         }
367         mOnAllSecondaryTransientCmmsStoppedListeners.clear();
368     }
369 }
370