/* * Copyright 2017 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 android.text.format.DateUtils; import android.util.ArrayMap; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * A lock to determine whether Wifi Wake can re-enable Wifi. * *

Wakeuplock manages a list of networks to determine whether the device's location has changed. */ public class WakeupLock { private static final String TAG = WakeupLock.class.getSimpleName(); @VisibleForTesting static final int CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT = 3; @VisibleForTesting static final long MAX_LOCK_TIME_MILLIS = 10 * DateUtils.MINUTE_IN_MILLIS; private final WifiConfigManager mWifiConfigManager; private final Map mLockedNetworks = new ArrayMap<>(); private final WifiWakeMetrics mWifiWakeMetrics; private final Clock mClock; private boolean mVerboseLoggingEnabled; private long mLockTimestamp; private boolean mIsInitialized; private int mNumScans; public WakeupLock(WifiConfigManager wifiConfigManager, WifiWakeMetrics wifiWakeMetrics, Clock clock) { mWifiConfigManager = wifiConfigManager; mWifiWakeMetrics = wifiWakeMetrics; mClock = clock; } /** * Sets the WakeupLock with the given {@link ScanResultMatchInfo} list. * *

This saves the wakeup lock to the store and begins the initialization process. * * @param scanResultList list of ScanResultMatchInfos to start the lock with */ public void setLock(Collection scanResultList) { mLockTimestamp = mClock.getElapsedSinceBootMillis(); mIsInitialized = false; mNumScans = 0; mLockedNetworks.clear(); for (ScanResultMatchInfo scanResultMatchInfo : scanResultList) { mLockedNetworks.put(scanResultMatchInfo, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT); } Log.d(TAG, "Lock set. Number of networks: " + mLockedNetworks.size()); mWifiConfigManager.saveToStore(false /* forceWrite */); } /** * Maybe sets the WakeupLock as initialized based on total scans handled. * * @param numScans total number of elapsed scans in the current WifiWake session */ private void maybeSetInitializedByScans(int numScans) { if (mIsInitialized) { return; } boolean shouldBeInitialized = numScans >= CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT; if (shouldBeInitialized) { mIsInitialized = true; Log.d(TAG, "Lock initialized by handled scans. Scans: " + numScans); if (mVerboseLoggingEnabled) { Log.d(TAG, "State of lock: " + mLockedNetworks); } // log initialize event mWifiWakeMetrics.recordInitializeEvent(mNumScans, mLockedNetworks.size()); } } /** * Maybe sets the WakeupLock as initialized based on elapsed time. * * @param timestampMillis current timestamp */ private void maybeSetInitializedByTimeout(long timestampMillis) { if (mIsInitialized) { return; } long elapsedTime = timestampMillis - mLockTimestamp; boolean shouldBeInitialized = elapsedTime > MAX_LOCK_TIME_MILLIS; if (shouldBeInitialized) { mIsInitialized = true; Log.d(TAG, "Lock initialized by timeout. Elapsed time: " + elapsedTime); if (mNumScans == 0) { Log.w(TAG, "Lock initialized with 0 handled scans!"); } if (mVerboseLoggingEnabled) { Log.d(TAG, "State of lock: " + mLockedNetworks); } // log initialize event mWifiWakeMetrics.recordInitializeEvent(mNumScans, mLockedNetworks.size()); } } /** Returns whether the lock has been fully initialized. */ public boolean isInitialized() { return mIsInitialized; } /** * Adds the given networks to the lock. * *

This is called during the initialization step. * * @param networkList The list of networks to be added */ private void addToLock(Collection networkList) { if (mVerboseLoggingEnabled) { Log.d(TAG, "Initializing lock with networks: " + networkList); } boolean hasChanged = false; for (ScanResultMatchInfo network : networkList) { if (!mLockedNetworks.containsKey(network)) { mLockedNetworks.put(network, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT); hasChanged = true; } } if (hasChanged) { mWifiConfigManager.saveToStore(false /* forceWrite */); } // Set initialized if the lock has handled enough scans, and log the event maybeSetInitializedByScans(mNumScans); } /** * Removes networks from the lock if not present in the given {@link ScanResultMatchInfo} list. * *

If a network in the lock is not present in the list, reduce the number of scans * required to evict by one. Remove any entries in the list with 0 scans required to evict. If * any entries in the lock are removed, the store is updated. * * @param networkList list of present ScanResultMatchInfos to update the lock with */ private void removeFromLock(Collection networkList) { if (mVerboseLoggingEnabled) { Log.d(TAG, "Filtering lock with networks: " + networkList); } boolean hasChanged = false; Iterator> it = mLockedNetworks.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); // if present in scan list, reset to max if (networkList.contains(entry.getKey())) { if (mVerboseLoggingEnabled) { Log.d(TAG, "Found network in lock: " + entry.getKey().networkSsid); } entry.setValue(CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT); continue; } // decrement and remove if necessary entry.setValue(entry.getValue() - 1); if (entry.getValue() <= 0) { Log.d(TAG, "Removed network from lock: " + entry.getKey().networkSsid); it.remove(); hasChanged = true; } } if (hasChanged) { mWifiConfigManager.saveToStore(false /* forceWrite */); } if (isUnlocked()) { Log.d(TAG, "Lock emptied. Recording unlock event."); mWifiWakeMetrics.recordUnlockEvent(mNumScans); } } /** * Updates the lock with the given {@link ScanResultMatchInfo} list. * *

Based on the current initialization state of the lock, either adds or removes networks * from the lock. * *

The lock is initialized after {@link #CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT} * scans have been handled, or after {@link #MAX_LOCK_TIME_MILLIS} milliseconds have elapsed * since {@link #setLock(Collection)}. * * @param networkList list of present ScanResultMatchInfos to update the lock with */ public void update(Collection networkList) { // update is no-op if already unlocked if (isUnlocked()) { return; } // Before checking handling the scan, we check to see whether we've exceeded the maximum // time allowed for initialization. If so, we set initialized and treat this scan as a // "removeFromLock()" instead of an "addToLock()". maybeSetInitializedByTimeout(mClock.getElapsedSinceBootMillis()); mNumScans++; // add or remove networks based on initialized status if (mIsInitialized) { removeFromLock(networkList); } else { addToLock(networkList); } } /** Returns whether the WakeupLock is unlocked */ public boolean isUnlocked() { return mIsInitialized && mLockedNetworks.isEmpty(); } /** Returns the data source for the WakeupLock config store data. */ public WakeupConfigStoreData.DataSource> getDataSource() { return new WakeupLockDataSource(); } /** Dumps wakeup lock contents. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("WakeupLock: "); pw.println("mNumScans: " + mNumScans); pw.println("mIsInitialized: " + mIsInitialized); pw.println("Locked networks: " + mLockedNetworks.size()); for (Map.Entry entry : mLockedNetworks.entrySet()) { pw.println(entry.getKey() + ", scans to evict: " + entry.getValue()); } } /** Set whether verbose logging is enabled. */ public void enableVerboseLogging(boolean enabled) { mVerboseLoggingEnabled = enabled; } private class WakeupLockDataSource implements WakeupConfigStoreData.DataSource> { @Override public Set getData() { return mLockedNetworks.keySet(); } @Override public void setData(Set data) { mLockedNetworks.clear(); for (ScanResultMatchInfo network : data) { mLockedNetworks.put(network, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT); } // lock is considered initialized if loaded from store mIsInitialized = true; } } }