huangcm
2025-08-25 2f2fd745743ad500687c6985119d523146531958
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
/*
 * 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;
        }
    }
}