1 /*
2  * Copyright 2017 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 android.text.format.DateUtils;
20 import android.util.ArrayMap;
21 import android.util.Log;
22 
23 import com.android.internal.annotations.VisibleForTesting;
24 
25 import java.io.FileDescriptor;
26 import java.io.PrintWriter;
27 import java.util.Collection;
28 import java.util.Iterator;
29 import java.util.Map;
30 import java.util.Set;
31 
32 /**
33  * A lock to determine whether Wifi Wake can re-enable Wifi.
34  *
35  * <p>Wakeuplock manages a list of networks to determine whether the device's location has changed.
36  */
37 public class WakeupLock {
38 
39     private static final String TAG = WakeupLock.class.getSimpleName();
40 
41     @VisibleForTesting
42     static final int CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT = 5;
43     @VisibleForTesting
44     static final long MAX_LOCK_TIME_MILLIS = 10 * DateUtils.MINUTE_IN_MILLIS;
45 
46     private final WifiConfigManager mWifiConfigManager;
47     private final Map<ScanResultMatchInfo, Integer> mLockedNetworks = new ArrayMap<>();
48     private final WifiWakeMetrics mWifiWakeMetrics;
49     private final Clock mClock;
50 
51     private boolean mVerboseLoggingEnabled;
52     private long mLockTimestamp;
53     private boolean mIsInitialized;
54     private int mNumScans;
55 
WakeupLock(WifiConfigManager wifiConfigManager, WifiWakeMetrics wifiWakeMetrics, Clock clock)56     public WakeupLock(WifiConfigManager wifiConfigManager, WifiWakeMetrics wifiWakeMetrics,
57                       Clock clock) {
58         mWifiConfigManager = wifiConfigManager;
59         mWifiWakeMetrics = wifiWakeMetrics;
60         mClock = clock;
61     }
62 
63     /**
64      * Sets the WakeupLock with the given {@link ScanResultMatchInfo} list.
65      *
66      * <p>This saves the wakeup lock to the store and begins the initialization process.
67      *
68      * @param scanResultList list of ScanResultMatchInfos to start the lock with
69      */
setLock(Collection<ScanResultMatchInfo> scanResultList)70     public void setLock(Collection<ScanResultMatchInfo> scanResultList) {
71         mLockTimestamp = mClock.getElapsedSinceBootMillis();
72         mIsInitialized = false;
73         mNumScans = 0;
74 
75         mLockedNetworks.clear();
76         for (ScanResultMatchInfo scanResultMatchInfo : scanResultList) {
77             mLockedNetworks.put(scanResultMatchInfo, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
78         }
79 
80         Log.d(TAG, "Lock set. Number of networks: " + mLockedNetworks.size());
81 
82         mWifiConfigManager.saveToStore(false /* forceWrite */);
83     }
84 
85     /**
86      * Maybe sets the WakeupLock as initialized based on total scans handled.
87      *
88      * @param numScans total number of elapsed scans in the current WifiWake session
89      */
maybeSetInitializedByScans(int numScans)90     private void maybeSetInitializedByScans(int numScans) {
91         if (mIsInitialized) {
92             return;
93         }
94         boolean shouldBeInitialized = numScans >= CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT;
95         if (shouldBeInitialized) {
96             mIsInitialized = true;
97 
98             Log.d(TAG, "Lock initialized by handled scans. Scans: " + numScans);
99             if (mVerboseLoggingEnabled) {
100                 Log.d(TAG, "State of lock: " + mLockedNetworks);
101             }
102 
103             // log initialize event
104             mWifiWakeMetrics.recordInitializeEvent(mNumScans, mLockedNetworks.size());
105         }
106     }
107 
108     /**
109      * Maybe sets the WakeupLock as initialized based on elapsed time.
110      *
111      * @param timestampMillis current timestamp
112      */
maybeSetInitializedByTimeout(long timestampMillis)113     private void maybeSetInitializedByTimeout(long timestampMillis) {
114         if (mIsInitialized) {
115             return;
116         }
117         long elapsedTime = timestampMillis - mLockTimestamp;
118         boolean shouldBeInitialized = elapsedTime > MAX_LOCK_TIME_MILLIS;
119 
120         if (shouldBeInitialized) {
121             mIsInitialized = true;
122 
123             Log.d(TAG, "Lock initialized by timeout. Elapsed time: " + elapsedTime);
124             if (mNumScans == 0) {
125                 Log.w(TAG, "Lock initialized with 0 handled scans!");
126             }
127             if (mVerboseLoggingEnabled) {
128                 Log.d(TAG, "State of lock: " + mLockedNetworks);
129             }
130 
131             // log initialize event
132             mWifiWakeMetrics.recordInitializeEvent(mNumScans, mLockedNetworks.size());
133         }
134     }
135 
136     /** Returns whether the lock has been fully initialized. */
isInitialized()137     public boolean isInitialized() {
138         return mIsInitialized;
139     }
140 
141     /**
142      * Adds the given networks to the lock.
143      *
144      * <p>This is called during the initialization step.
145      *
146      * @param networkList The list of networks to be added
147      */
addToLock(Collection<ScanResultMatchInfo> networkList)148     private void addToLock(Collection<ScanResultMatchInfo> networkList) {
149         if (mVerboseLoggingEnabled) {
150             Log.d(TAG, "Initializing lock with networks: " + networkList);
151         }
152 
153         boolean hasChanged = false;
154 
155         for (ScanResultMatchInfo network : networkList) {
156             if (!mLockedNetworks.containsKey(network)) {
157                 mLockedNetworks.put(network, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
158                 hasChanged = true;
159             }
160         }
161 
162         if (hasChanged) {
163             mWifiConfigManager.saveToStore(false /* forceWrite */);
164         }
165 
166         // Set initialized if the lock has handled enough scans, and log the event
167         maybeSetInitializedByScans(mNumScans);
168     }
169 
170     /**
171      * Removes networks from the lock if not present in the given {@link ScanResultMatchInfo} list.
172      *
173      * <p>If a network in the lock is not present in the list, reduce the number of scans
174      * required to evict by one. Remove any entries in the list with 0 scans required to evict. If
175      * any entries in the lock are removed, the store is updated.
176      *
177      * @param networkList list of present ScanResultMatchInfos to update the lock with
178      */
removeFromLock(Collection<ScanResultMatchInfo> networkList)179     private void removeFromLock(Collection<ScanResultMatchInfo> networkList) {
180         if (mVerboseLoggingEnabled) {
181             Log.d(TAG, "Filtering lock with networks: " + networkList);
182         }
183 
184         boolean hasChanged = false;
185         Iterator<Map.Entry<ScanResultMatchInfo, Integer>> it =
186                 mLockedNetworks.entrySet().iterator();
187         while (it.hasNext()) {
188             Map.Entry<ScanResultMatchInfo, Integer> entry = it.next();
189 
190             // if present in scan list, reset to max
191             if (networkList.contains(entry.getKey())) {
192                 if (mVerboseLoggingEnabled) {
193                     Log.d(TAG, "Found network in lock: " + entry.getKey().networkSsid);
194                 }
195                 entry.setValue(CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
196                 continue;
197             }
198 
199             // decrement and remove if necessary
200             entry.setValue(entry.getValue() - 1);
201             if (entry.getValue() <= 0) {
202                 Log.d(TAG, "Removed network from lock: " + entry.getKey().networkSsid);
203                 it.remove();
204                 hasChanged = true;
205             }
206         }
207 
208         if (hasChanged) {
209             mWifiConfigManager.saveToStore(false /* forceWrite */);
210         }
211 
212         if (isUnlocked()) {
213             Log.d(TAG, "Lock emptied. Recording unlock event.");
214             mWifiWakeMetrics.recordUnlockEvent(mNumScans);
215         }
216     }
217 
218     /**
219      * Updates the lock with the given {@link ScanResultMatchInfo} list.
220      *
221      * <p>Based on the current initialization state of the lock, either adds or removes networks
222      * from the lock.
223      *
224      * <p>The lock is initialized after {@link #CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT}
225      * scans have been handled, or after {@link #MAX_LOCK_TIME_MILLIS} milliseconds have elapsed
226      * since {@link #setLock(Collection)}.
227      *
228      * @param networkList list of present ScanResultMatchInfos to update the lock with
229      */
update(Collection<ScanResultMatchInfo> networkList)230     public void update(Collection<ScanResultMatchInfo> networkList) {
231         // update is no-op if already unlocked
232         if (isUnlocked()) {
233             return;
234         }
235         // Before checking handling the scan, we check to see whether we've exceeded the maximum
236         // time allowed for initialization. If so, we set initialized and treat this scan as a
237         // "removeFromLock()" instead of an "addToLock()".
238         maybeSetInitializedByTimeout(mClock.getElapsedSinceBootMillis());
239 
240         mNumScans++;
241 
242         // add or remove networks based on initialized status
243         if (mIsInitialized) {
244             removeFromLock(networkList);
245         } else {
246             addToLock(networkList);
247         }
248     }
249 
250     /** Returns whether the WakeupLock is unlocked */
isUnlocked()251     public boolean isUnlocked() {
252         return mIsInitialized && mLockedNetworks.isEmpty();
253     }
254 
255     /** Returns the data source for the WakeupLock config store data. */
getDataSource()256     public WakeupConfigStoreData.DataSource<Set<ScanResultMatchInfo>> getDataSource() {
257         return new WakeupLockDataSource();
258     }
259 
260     /** Dumps wakeup lock contents. */
dump(FileDescriptor fd, PrintWriter pw, String[] args)261     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
262         pw.println("WakeupLock: ");
263         pw.println("mNumScans: " + mNumScans);
264         pw.println("mIsInitialized: " + mIsInitialized);
265         pw.println("Locked networks: " + mLockedNetworks.size());
266         for (Map.Entry<ScanResultMatchInfo, Integer> entry : mLockedNetworks.entrySet()) {
267             pw.println(entry.getKey() + ", scans to evict: " + entry.getValue());
268         }
269     }
270 
271     /** Set whether verbose logging is enabled. */
enableVerboseLogging(boolean enabled)272     public void enableVerboseLogging(boolean enabled) {
273         mVerboseLoggingEnabled = enabled;
274     }
275 
276     private class WakeupLockDataSource
277             implements WakeupConfigStoreData.DataSource<Set<ScanResultMatchInfo>> {
278 
279         @Override
getData()280         public Set<ScanResultMatchInfo> getData() {
281             return mLockedNetworks.keySet();
282         }
283 
284         @Override
setData(Set<ScanResultMatchInfo> data)285         public void setData(Set<ScanResultMatchInfo> data) {
286             mLockedNetworks.clear();
287             for (ScanResultMatchInfo network : data) {
288                 mLockedNetworks.put(network, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
289             }
290             // lock is considered initialized if loaded from store
291             mIsInitialized = true;
292         }
293     }
294 }
295