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