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 
218         Log.i(TAG, "Starting MBB switch primary from " + currentPrimary + " to " + newPrimary
219                 + " by setting current primary's role to ROLE_CLIENT_SECONDARY_TRANSIENT");
220 
221         mWifiMetrics.incrementMakeBeforeBreakInternetValidatedCount();
222 
223         // Since role change is not atomic, we must first make the previous primary CMM into a
224         // secondary transient CMM. Thus, after this call to setRole() completes, there is no
225         // primary CMM and 2 secondary transient CMMs.
226         currentPrimary.setRole(
227                 ROLE_CLIENT_SECONDARY_TRANSIENT, ActiveModeWarden.INTERNAL_REQUESTOR_WS);
228         // immediately send fake disconnection broadcasts upon changing primary CMM's role to
229         // SECONDARY_TRANSIENT, because as soon as the CMM becomes SECONDARY_TRANSIENT, its
230         // broadcasts will never be sent out again (BroadcastQueue only sends broadcasts for the
231         // current primary CMM). This is to preserve the legacy single STA behavior.
232         mBroadcastQueue.fakeDisconnectionBroadcasts();
233         mMakeBeforeBreakInfo = new MakeBeforeBreakInfo(currentPrimary, newPrimary);
234     }
235 
onCaptivePortalDetected(@onNull ConcreteClientModeManager newPrimary)236     private void onCaptivePortalDetected(@NonNull ConcreteClientModeManager newPrimary) {
237         if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) {
238             return;
239         }
240         if (newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) {
241             return;
242         }
243 
244         ConcreteClientModeManager currentPrimary =
245                 mActiveModeWarden.getPrimaryClientModeManagerNullable();
246 
247         if (currentPrimary == null) {
248             Log.i(TAG, "onCaptivePortalDetected: Current primary is null, nothing to stop");
249         } else {
250             Log.i(TAG, "onCaptivePortalDetected: stopping current primary CMM");
251             currentPrimary.setWifiStateChangeBroadcastEnabled(false);
252             currentPrimary.stop();
253         }
254         // Once the currentPrimary teardown completes, recoverPrimary() will make the Captive
255         // Portal CMM the new primary, because it is the only SECONDARY_TRANSIENT CMM and no
256         // primary CMM exists.
257     }
258 
maybeContinueMakeBeforeBreak( @onNull ConcreteClientModeManager roleChangedClientModeManager)259     private void maybeContinueMakeBeforeBreak(
260             @NonNull ConcreteClientModeManager roleChangedClientModeManager) {
261         // not in the middle of MBB
262         if (mMakeBeforeBreakInfo == null) {
263             return;
264         }
265         // not the CMM we're looking for, keep monitoring
266         if (roleChangedClientModeManager != mMakeBeforeBreakInfo.oldPrimary) {
267             return;
268         }
269         try {
270             // if old primary didn't transition to secondary transient, abort the MBB attempt
271             if (mMakeBeforeBreakInfo.oldPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) {
272                 Log.i(TAG, "old primary is no longer secondary transient, aborting MBB: "
273                         + mMakeBeforeBreakInfo.oldPrimary);
274                 return;
275             }
276 
277             // if somehow the next primary is no longer secondary transient, abort the MBB attempt
278             if (mMakeBeforeBreakInfo.newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) {
279                 Log.i(TAG, "new primary is no longer secondary transient, abort MBB: "
280                         + mMakeBeforeBreakInfo.newPrimary);
281                 return;
282             }
283 
284             Log.i(TAG, "Continue MBB switch primary from " + mMakeBeforeBreakInfo.oldPrimary
285                     + " to " + mMakeBeforeBreakInfo.newPrimary
286                     + " by setting new Primary's role to ROLE_CLIENT_PRIMARY and reducing network"
287                     + " score");
288 
289             // TODO(b/180974604): In theory, newPrimary.setRole() could still fail, but that would
290             //  still count as a MBB success in the metrics. But we don't really handle that
291             //  scenario well anyways, see TODO below.
292             mWifiMetrics.incrementMakeBeforeBreakSuccessCount();
293 
294             // otherwise, actually set the new primary's role to primary.
295             mMakeBeforeBreakInfo.newPrimary.setRole(
296                     ROLE_CLIENT_PRIMARY, mFrameworkFacade.getSettingsWorkSource(mContext));
297 
298             // linger old primary
299             // TODO(b/160346062): maybe do this after the new primary was fully transitioned to
300             //  ROLE_CLIENT_PRIMARY (since setRole() is asynchronous)
301             mMakeBeforeBreakInfo.oldPrimary.setShouldReduceNetworkScore(true);
302         } finally {
303             // end the MBB attempt
304             mMakeBeforeBreakInfo = null;
305         }
306     }
307 
308     /** Dump fields for debugging. */
dump(FileDescriptor fd, PrintWriter pw, String[] args)309     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
310         pw.println("Dump of MakeBeforeBreakManager");
311         pw.println("mMakeBeforeBreakInfo=" + mMakeBeforeBreakInfo);
312     }
313 
314     /**
315      * Stop all ClientModeManagers with role
316      * {@link ActiveModeManager#ROLE_CLIENT_SECONDARY_TRANSIENT}.
317      *
318      * This is useful when an explicit connection was requested by an external caller
319      * (e.g. Settings, legacy app calling {@link android.net.wifi.WifiManager#enableNetwork}).
320      * We should abort any ongoing Make Before Break attempt to avoid interrupting the explicit
321      * connection.
322      *
323      * @param onStoppedListener triggered when all secondary transient CMMs have been stopped.
324      */
stopAllSecondaryTransientClientModeManagers(Runnable onStoppedListener)325     public void stopAllSecondaryTransientClientModeManagers(Runnable onStoppedListener) {
326         // no secondary transient CMM exists, trigger the callback immediately and return
327         if (mActiveModeWarden.getClientModeManagerInRole(ROLE_CLIENT_SECONDARY_TRANSIENT) == null) {
328             if (mVerboseLoggingEnabled) {
329                 Log.d(TAG, "No secondary transient CMM active, trigger callback immediately");
330             }
331             onStoppedListener.run();
332             return;
333         }
334 
335         // there exists at least 1 secondary transient CMM, but no primary
336         // TODO(b/177692017): Since switching roles is not atomic, there is a short period of time
337         //  during the Make Before Break transition when there are 2 SECONDARY_TRANSIENT CMMs and 0
338         //  primary CMMs. If this method is called at that time, it will destroy all CMMs, resulting
339         //  in no primary, and causing any subsequent connections to fail. Hopefully this does
340         //  not occur frequently.
341         if (mActiveModeWarden.getPrimaryClientModeManagerNullable() == null) {
342             Log.wtf(TAG, "Called stopAllSecondaryTransientClientModeManagers with no primary CMM!");
343         }
344 
345         mOnAllSecondaryTransientCmmsStoppedListeners.add(onStoppedListener);
346         mActiveModeWarden.stopAllClientModeManagersInRole(ROLE_CLIENT_SECONDARY_TRANSIENT);
347     }
348 
triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms()349     private void triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms() {
350         // not all secondary transient CMMs stopped, keep waiting
351         if (mActiveModeWarden.getClientModeManagerInRole(ROLE_CLIENT_SECONDARY_TRANSIENT) != null) {
352             return;
353         }
354 
355         if (mVerboseLoggingEnabled) {
356             Log.i(TAG, "All secondary transient CMMs stopped, triggering queued callbacks");
357         }
358 
359         for (Runnable onStoppedListener : mOnAllSecondaryTransientCmmsStoppedListeners) {
360             onStoppedListener.run();
361         }
362         mOnAllSecondaryTransientCmmsStoppedListeners.clear();
363     }
364 }
365