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
17package com.android.server.wifi;
18
19import android.text.format.DateUtils;
20import android.util.ArrayMap;
21import android.util.Log;
22
23import com.android.internal.annotations.VisibleForTesting;
24
25import java.io.FileDescriptor;
26import java.io.PrintWriter;
27import java.util.Collection;
28import java.util.Iterator;
29import java.util.Map;
30import 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 */
37public 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 = 3;
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
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     */
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     */
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     */
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. */
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     */
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     */
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     */
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 */
251    public boolean isUnlocked() {
252        return mIsInitialized && mLockedNetworks.isEmpty();
253    }
254
255    /** Returns the data source for the WakeupLock config store data. */
256    public WakeupConfigStoreData.DataSource<Set<ScanResultMatchInfo>> getDataSource() {
257        return new WakeupLockDataSource();
258    }
259
260    /** Dumps wakeup lock contents. */
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. */
272    public void enableVerboseLogging(boolean enabled) {
273        mVerboseLoggingEnabled = enabled;
274    }
275
276    private class WakeupLockDataSource
277            implements WakeupConfigStoreData.DataSource<Set<ScanResultMatchInfo>> {
278
279        @Override
280        public Set<ScanResultMatchInfo> getData() {
281            return mLockedNetworks.keySet();
282        }
283
284        @Override
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