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