| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.wifi; |
| |
| import static com.android.server.wifi.ActiveModeManager.ROLE_CLIENT_PRIMARY; |
| import static com.android.server.wifi.ActiveModeManager.ROLE_CLIENT_SECONDARY_TRANSIENT; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.util.Log; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Manages Make-Before-Break connection switching. |
| */ |
| public class MakeBeforeBreakManager { |
| private static final String TAG = "WifiMbbManager"; |
| |
| private final ActiveModeWarden mActiveModeWarden; |
| private final FrameworkFacade mFrameworkFacade; |
| private final Context mContext; |
| private final ClientModeImplMonitor mCmiMonitor; |
| private final ClientModeManagerBroadcastQueue mBroadcastQueue; |
| private final WifiMetrics mWifiMetrics; |
| |
| private final List<Runnable> mOnAllSecondaryTransientCmmsStoppedListeners = new ArrayList<>(); |
| private boolean mVerboseLoggingEnabled = false; |
| |
| private static class MakeBeforeBreakInfo { |
| @NonNull |
| public final ConcreteClientModeManager oldPrimary; |
| @NonNull |
| public final ConcreteClientModeManager newPrimary; |
| |
| MakeBeforeBreakInfo( |
| @NonNull ConcreteClientModeManager oldPrimary, |
| @NonNull ConcreteClientModeManager newPrimary) { |
| this.oldPrimary = oldPrimary; |
| this.newPrimary = newPrimary; |
| } |
| |
| @Override |
| public String toString() { |
| return "MakeBeforeBreakInfo{" |
| + "oldPrimary=" + oldPrimary |
| + ", newPrimary=" + newPrimary |
| + '}'; |
| } |
| } |
| |
| @Nullable |
| private MakeBeforeBreakInfo mMakeBeforeBreakInfo = null; |
| |
| public MakeBeforeBreakManager( |
| @NonNull ActiveModeWarden activeModeWarden, |
| @NonNull FrameworkFacade frameworkFacade, |
| @NonNull Context context, |
| @NonNull ClientModeImplMonitor cmiMonitor, |
| @NonNull ClientModeManagerBroadcastQueue broadcastQueue, |
| @NonNull WifiMetrics wifiMetrics) { |
| mActiveModeWarden = activeModeWarden; |
| mFrameworkFacade = frameworkFacade; |
| mContext = context; |
| mCmiMonitor = cmiMonitor; |
| mBroadcastQueue = broadcastQueue; |
| mWifiMetrics = wifiMetrics; |
| |
| mActiveModeWarden.registerModeChangeCallback(new ModeChangeCallback()); |
| mCmiMonitor.registerListener(new ClientModeImplListener() { |
| @Override |
| public void onInternetValidated(@NonNull ConcreteClientModeManager clientModeManager) { |
| MakeBeforeBreakManager.this.onInternetValidated(clientModeManager); |
| } |
| |
| @Override |
| public void onCaptivePortalDetected( |
| @NonNull ConcreteClientModeManager clientModeManager) { |
| MakeBeforeBreakManager.this.onCaptivePortalDetected(clientModeManager); |
| } |
| }); |
| } |
| |
| public void setVerboseLoggingEnabled(boolean enabled) { |
| mVerboseLoggingEnabled = enabled; |
| } |
| |
| private class ModeChangeCallback implements ActiveModeWarden.ModeChangeCallback { |
| @Override |
| public void onActiveModeManagerAdded(@NonNull ActiveModeManager activeModeManager) { |
| if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) { |
| return; |
| } |
| if (!(activeModeManager instanceof ConcreteClientModeManager)) { |
| return; |
| } |
| // just in case |
| recoverPrimary(); |
| } |
| |
| @Override |
| public void onActiveModeManagerRemoved(@NonNull ActiveModeManager activeModeManager) { |
| if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) { |
| return; |
| } |
| if (!(activeModeManager instanceof ConcreteClientModeManager)) { |
| return; |
| } |
| // if either the old or new primary stopped during MBB, abort the MBB attempt |
| ConcreteClientModeManager clientModeManager = |
| (ConcreteClientModeManager) activeModeManager; |
| if (mMakeBeforeBreakInfo != null) { |
| boolean oldPrimaryStopped = clientModeManager == mMakeBeforeBreakInfo.oldPrimary; |
| boolean newPrimaryStopped = clientModeManager == mMakeBeforeBreakInfo.newPrimary; |
| if (oldPrimaryStopped || newPrimaryStopped) { |
| Log.i(TAG, "MBB CMM stopped, aborting:" |
| + " oldPrimary=" + mMakeBeforeBreakInfo.oldPrimary |
| + " stopped=" + oldPrimaryStopped |
| + " newPrimary=" + mMakeBeforeBreakInfo.newPrimary |
| + " stopped=" + newPrimaryStopped); |
| mMakeBeforeBreakInfo = null; |
| } |
| } |
| recoverPrimary(); |
| triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms(); |
| } |
| |
| @Override |
| public void onActiveModeManagerRoleChanged(@NonNull ActiveModeManager activeModeManager) { |
| if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) { |
| return; |
| } |
| if (!(activeModeManager instanceof ConcreteClientModeManager)) { |
| return; |
| } |
| ConcreteClientModeManager clientModeManager = |
| (ConcreteClientModeManager) activeModeManager; |
| recoverPrimary(); |
| maybeContinueMakeBeforeBreak(clientModeManager); |
| triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms(); |
| } |
| } |
| |
| /** |
| * Failsafe: if there is no primary CMM but there exists exactly one CMM in |
| * {@link ActiveModeManager#ROLE_CLIENT_SECONDARY_TRANSIENT}, or multiple and MBB is not |
| * in progress (to avoid interfering with MBB), make it primary. |
| */ |
| private void recoverPrimary() { |
| // already have a primary, do nothing |
| if (mActiveModeWarden.getPrimaryClientModeManagerNullable() != null) { |
| return; |
| } |
| List<ConcreteClientModeManager> secondaryTransientCmms = |
| mActiveModeWarden.getClientModeManagersInRoles(ROLE_CLIENT_SECONDARY_TRANSIENT); |
| // exactly 1 secondary transient, or > 1 secondary transient and MBB is not in progress |
| if (secondaryTransientCmms.size() == 1 |
| || (mMakeBeforeBreakInfo == null && secondaryTransientCmms.size() > 1)) { |
| ConcreteClientModeManager manager = secondaryTransientCmms.get(0); |
| manager.setRole(ROLE_CLIENT_PRIMARY, mFrameworkFacade.getSettingsWorkSource(mContext)); |
| Log.i(TAG, "recoveryPrimary kicking in, making " + manager + " primary and stopping" |
| + " all other SECONDARY_TRANSIENT ClientModeManagers"); |
| mWifiMetrics.incrementMakeBeforeBreakRecoverPrimaryCount(); |
| // tear down the extra secondary transient CMMs (if they exist) |
| for (int i = 1; i < secondaryTransientCmms.size(); i++) { |
| secondaryTransientCmms.get(i).stop(); |
| } |
| } |
| } |
| |
| /** |
| * A ClientModeImpl instance has been validated to have internet connection. This will begin the |
| * Make-Before-Break transition to make this the new primary network. |
| * |
| * Change the previous primary ClientModeManager to role |
| * {@link ActiveModeManager#ROLE_CLIENT_SECONDARY_TRANSIENT} and change the new |
| * primary to role {@link ActiveModeManager#ROLE_CLIENT_PRIMARY}. |
| * |
| * @param newPrimary the corresponding ConcreteClientModeManager instance for the ClientModeImpl |
| * that had its internet connection validated. |
| */ |
| private void onInternetValidated(@NonNull ConcreteClientModeManager newPrimary) { |
| if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) { |
| return; |
| } |
| if (newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) { |
| return; |
| } |
| |
| ConcreteClientModeManager currentPrimary = |
| mActiveModeWarden.getPrimaryClientModeManagerNullable(); |
| |
| if (currentPrimary == null) { |
| Log.e(TAG, "changePrimaryClientModeManager(): current primary CMM is null!"); |
| newPrimary.setRole( |
| ROLE_CLIENT_PRIMARY, mFrameworkFacade.getSettingsWorkSource(mContext)); |
| return; |
| } |
| if (newPrimary.getPreviousRole() == ROLE_CLIENT_PRIMARY) { |
| Log.i(TAG, "Don't start MBB when internet is validated on the lingering " |
| + "secondary."); |
| return; |
| } |
| |
| Log.i(TAG, "Starting MBB switch primary from " + currentPrimary + " to " + newPrimary |
| + " by setting current primary's role to ROLE_CLIENT_SECONDARY_TRANSIENT"); |
| |
| mWifiMetrics.incrementMakeBeforeBreakInternetValidatedCount(); |
| |
| // Since role change is not atomic, we must first make the previous primary CMM into a |
| // secondary transient CMM. Thus, after this call to setRole() completes, there is no |
| // primary CMM and 2 secondary transient CMMs. |
| currentPrimary.setRole( |
| ROLE_CLIENT_SECONDARY_TRANSIENT, ActiveModeWarden.INTERNAL_REQUESTOR_WS); |
| // immediately send fake disconnection broadcasts upon changing primary CMM's role to |
| // SECONDARY_TRANSIENT, because as soon as the CMM becomes SECONDARY_TRANSIENT, its |
| // broadcasts will never be sent out again (BroadcastQueue only sends broadcasts for the |
| // current primary CMM). This is to preserve the legacy single STA behavior. |
| mBroadcastQueue.fakeDisconnectionBroadcasts(); |
| mMakeBeforeBreakInfo = new MakeBeforeBreakInfo(currentPrimary, newPrimary); |
| } |
| |
| private void onCaptivePortalDetected(@NonNull ConcreteClientModeManager newPrimary) { |
| if (!mActiveModeWarden.isStaStaConcurrencySupportedForMbb()) { |
| return; |
| } |
| if (newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) { |
| return; |
| } |
| |
| ConcreteClientModeManager currentPrimary = |
| mActiveModeWarden.getPrimaryClientModeManagerNullable(); |
| |
| if (currentPrimary == null) { |
| Log.i(TAG, "onCaptivePortalDetected: Current primary is null, nothing to stop"); |
| } else { |
| Log.i(TAG, "onCaptivePortalDetected: stopping current primary CMM"); |
| currentPrimary.setWifiStateChangeBroadcastEnabled(false); |
| currentPrimary.stop(); |
| } |
| // Once the currentPrimary teardown completes, recoverPrimary() will make the Captive |
| // Portal CMM the new primary, because it is the only SECONDARY_TRANSIENT CMM and no |
| // primary CMM exists. |
| } |
| |
| private void maybeContinueMakeBeforeBreak( |
| @NonNull ConcreteClientModeManager roleChangedClientModeManager) { |
| // not in the middle of MBB |
| if (mMakeBeforeBreakInfo == null) { |
| return; |
| } |
| // not the CMM we're looking for, keep monitoring |
| if (roleChangedClientModeManager != mMakeBeforeBreakInfo.oldPrimary) { |
| return; |
| } |
| try { |
| // if old primary didn't transition to secondary transient, abort the MBB attempt |
| if (mMakeBeforeBreakInfo.oldPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) { |
| Log.i(TAG, "old primary is no longer secondary transient, aborting MBB: " |
| + mMakeBeforeBreakInfo.oldPrimary); |
| return; |
| } |
| |
| // if somehow the next primary is no longer secondary transient, abort the MBB attempt |
| if (mMakeBeforeBreakInfo.newPrimary.getRole() != ROLE_CLIENT_SECONDARY_TRANSIENT) { |
| Log.i(TAG, "new primary is no longer secondary transient, abort MBB: " |
| + mMakeBeforeBreakInfo.newPrimary); |
| return; |
| } |
| |
| Log.i(TAG, "Continue MBB switch primary from " + mMakeBeforeBreakInfo.oldPrimary |
| + " to " + mMakeBeforeBreakInfo.newPrimary |
| + " by setting new Primary's role to ROLE_CLIENT_PRIMARY and reducing network" |
| + " score"); |
| |
| // TODO(b/180974604): In theory, newPrimary.setRole() could still fail, but that would |
| // still count as a MBB success in the metrics. But we don't really handle that |
| // scenario well anyways, see TODO below. |
| mWifiMetrics.incrementMakeBeforeBreakSuccessCount(); |
| |
| // otherwise, actually set the new primary's role to primary. |
| mMakeBeforeBreakInfo.newPrimary.setRole( |
| ROLE_CLIENT_PRIMARY, mFrameworkFacade.getSettingsWorkSource(mContext)); |
| |
| // linger old primary |
| // TODO(b/160346062): maybe do this after the new primary was fully transitioned to |
| // ROLE_CLIENT_PRIMARY (since setRole() is asynchronous) |
| mMakeBeforeBreakInfo.oldPrimary.setShouldReduceNetworkScore(true); |
| } finally { |
| // end the MBB attempt |
| mMakeBeforeBreakInfo = null; |
| } |
| } |
| |
| /** Dump fields for debugging. */ |
| public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| pw.println("Dump of MakeBeforeBreakManager"); |
| pw.println("mMakeBeforeBreakInfo=" + mMakeBeforeBreakInfo); |
| } |
| |
| /** |
| * Stop all ClientModeManagers with role |
| * {@link ActiveModeManager#ROLE_CLIENT_SECONDARY_TRANSIENT}. |
| * |
| * This is useful when an explicit connection was requested by an external caller |
| * (e.g. Settings, legacy app calling {@link android.net.wifi.WifiManager#enableNetwork}). |
| * We should abort any ongoing Make Before Break attempt to avoid interrupting the explicit |
| * connection. |
| * |
| * @param onStoppedListener triggered when all secondary transient CMMs have been stopped. |
| */ |
| public void stopAllSecondaryTransientClientModeManagers(Runnable onStoppedListener) { |
| // no secondary transient CMM exists, trigger the callback immediately and return |
| if (mActiveModeWarden.getClientModeManagerInRole(ROLE_CLIENT_SECONDARY_TRANSIENT) == null) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "No secondary transient CMM active, trigger callback immediately"); |
| } |
| onStoppedListener.run(); |
| return; |
| } |
| |
| // there exists at least 1 secondary transient CMM, but no primary |
| // TODO(b/177692017): Since switching roles is not atomic, there is a short period of time |
| // during the Make Before Break transition when there are 2 SECONDARY_TRANSIENT CMMs and 0 |
| // primary CMMs. If this method is called at that time, it will destroy all CMMs, resulting |
| // in no primary, and causing any subsequent connections to fail. Hopefully this does |
| // not occur frequently. |
| if (mActiveModeWarden.getPrimaryClientModeManagerNullable() == null) { |
| Log.wtf(TAG, "Called stopAllSecondaryTransientClientModeManagers with no primary CMM!"); |
| } |
| |
| mOnAllSecondaryTransientCmmsStoppedListeners.add(onStoppedListener); |
| mActiveModeWarden.stopAllClientModeManagersInRole(ROLE_CLIENT_SECONDARY_TRANSIENT); |
| } |
| |
| private void triggerOnStoppedListenersIfNoMoreSecondaryTransientCmms() { |
| // not all secondary transient CMMs stopped, keep waiting |
| if (mActiveModeWarden.getClientModeManagerInRole(ROLE_CLIENT_SECONDARY_TRANSIENT) != null) { |
| return; |
| } |
| |
| if (mVerboseLoggingEnabled) { |
| Log.i(TAG, "All secondary transient CMMs stopped, triggering queued callbacks"); |
| } |
| |
| for (Runnable onStoppedListener : mOnAllSecondaryTransientCmmsStoppedListeners) { |
| onStoppedListener.run(); |
| } |
| mOnAllSecondaryTransientCmmsStoppedListeners.clear(); |
| } |
| } |