| /* |
| * 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. |
| * |
| * <p>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 = 5; |
| @VisibleForTesting |
| static final long MAX_LOCK_TIME_MILLIS = 10 * DateUtils.MINUTE_IN_MILLIS; |
| |
| private final WifiConfigManager mWifiConfigManager; |
| private final Map<ScanResultMatchInfo, Integer> 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. |
| * |
| * <p>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<ScanResultMatchInfo> 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. |
| * |
| * <p>This is called during the initialization step. |
| * |
| * @param networkList The list of networks to be added |
| */ |
| private void addToLock(Collection<ScanResultMatchInfo> 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. |
| * |
| * <p>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<ScanResultMatchInfo> networkList) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "Filtering lock with networks: " + networkList); |
| } |
| |
| boolean hasChanged = false; |
| Iterator<Map.Entry<ScanResultMatchInfo, Integer>> it = |
| mLockedNetworks.entrySet().iterator(); |
| while (it.hasNext()) { |
| Map.Entry<ScanResultMatchInfo, Integer> 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. |
| * |
| * <p>Based on the current initialization state of the lock, either adds or removes networks |
| * from the lock. |
| * |
| * <p>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<ScanResultMatchInfo> 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<Set<ScanResultMatchInfo>> 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<ScanResultMatchInfo, Integer> 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<Set<ScanResultMatchInfo>> { |
| |
| @Override |
| public Set<ScanResultMatchInfo> getData() { |
| return mLockedNetworks.keySet(); |
| } |
| |
| @Override |
| public void setData(Set<ScanResultMatchInfo> 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; |
| } |
| } |
| } |