• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.content.BroadcastReceiver;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.database.ContentObserver;
25 import android.net.ConnectivityManager;
26 import android.net.LinkProperties;
27 import android.net.NetworkInfo;
28 import android.net.wifi.RssiPacketCountInfo;
29 import android.net.wifi.SupplicantState;
30 import android.net.wifi.WifiInfo;
31 import android.net.wifi.WifiManager;
32 import android.os.Message;
33 import android.os.Messenger;
34 import android.os.SystemClock;
35 import android.provider.Settings;
36 import android.util.LruCache;
37 
38 import com.android.internal.util.AsyncChannel;
39 import com.android.internal.util.Protocol;
40 import com.android.internal.util.State;
41 import com.android.internal.util.StateMachine;
42 
43 import java.io.FileDescriptor;
44 import java.io.PrintWriter;
45 import java.text.DecimalFormat;
46 
47 /**
48  * WifiWatchdogStateMachine monitors the connection to a WiFi network. When WiFi
49  * connects at L2 layer, the beacons from access point reach the device and it
50  * can maintain a connection, but the application connectivity can be flaky (due
51  * to bigger packet size exchange).
52  * <p>
53  * We now monitor the quality of the last hop on WiFi using packet loss ratio as
54  * an indicator to decide if the link is good enough to switch to Wi-Fi as the
55  * uplink.
56  * <p>
57  * When WiFi is connected, the WiFi watchdog keeps sampling the RSSI and the
58  * instant packet loss, and record it as per-AP loss-to-rssi statistics. When
59  * the instant packet loss is higher than a threshold, the WiFi watchdog sends a
60  * poor link notification to avoid WiFi connection temporarily.
61  * <p>
62  * While WiFi is being avoided, the WiFi watchdog keep watching the RSSI to
63  * bring the WiFi connection back. Once the RSSI is high enough to achieve a
64  * lower packet loss, a good link detection is sent such that the WiFi
65  * connection become available again.
66  * <p>
67  * BSSID roaming has been taken into account. When user is moving across
68  * multiple APs, the WiFi watchdog will detect that and keep watching the
69  * currently connected AP.
70  * <p>
71  * Power impact should be minimal since much of the measurement relies on
72  * passive statistics already being tracked at the driver and the polling is
73  * done when screen is turned on and the RSSI is in a certain range.
74  *
75  * @hide
76  */
77 public class WifiWatchdogStateMachine extends StateMachine {
78 
79     private static final boolean DBG = false;
80 
81     private static final int BASE = Protocol.BASE_WIFI_WATCHDOG;
82 
83     /* Internal events */
84     private static final int EVENT_WATCHDOG_TOGGLED                 = BASE + 1;
85     private static final int EVENT_NETWORK_STATE_CHANGE             = BASE + 2;
86     private static final int EVENT_RSSI_CHANGE                      = BASE + 3;
87     private static final int EVENT_SUPPLICANT_STATE_CHANGE          = BASE + 4;
88     private static final int EVENT_WIFI_RADIO_STATE_CHANGE          = BASE + 5;
89     private static final int EVENT_WATCHDOG_SETTINGS_CHANGE         = BASE + 6;
90     private static final int EVENT_BSSID_CHANGE                     = BASE + 7;
91     private static final int EVENT_SCREEN_ON                        = BASE + 8;
92     private static final int EVENT_SCREEN_OFF                       = BASE + 9;
93 
94     /* Internal messages */
95     private static final int CMD_RSSI_FETCH                         = BASE + 11;
96 
97     /* Notifications from/to WifiStateMachine */
98     static final int POOR_LINK_DETECTED                             = BASE + 21;
99     static final int GOOD_LINK_DETECTED                             = BASE + 22;
100 
101     /*
102      * RSSI levels as used by notification icon
103      * Level 4  -55 <= RSSI
104      * Level 3  -66 <= RSSI < -55
105      * Level 2  -77 <= RSSI < -67
106      * Level 1  -88 <= RSSI < -78
107      * Level 0         RSSI < -88
108      */
109 
110     /**
111      * WiFi link statistics is monitored and recorded actively below this threshold.
112      * <p>
113      * Larger threshold is more adaptive but increases sampling cost.
114      */
115     private static final int LINK_MONITOR_LEVEL_THRESHOLD = WifiManager.RSSI_LEVELS - 1;
116 
117     /**
118      * Remember packet loss statistics of how many BSSIDs.
119      * <p>
120      * Larger size is usually better but requires more space.
121      */
122     private static final int BSSID_STAT_CACHE_SIZE = 20;
123 
124     /**
125      * RSSI range of a BSSID statistics.
126      * Within the range, (RSSI -> packet loss %) mappings are stored.
127      * <p>
128      * Larger range is usually better but requires more space.
129      */
130     private static final int BSSID_STAT_RANGE_LOW_DBM  = -105;
131 
132     /**
133      * See {@link #BSSID_STAT_RANGE_LOW_DBM}.
134      */
135     private static final int BSSID_STAT_RANGE_HIGH_DBM = -45;
136 
137     /**
138      * How many consecutive empty data point to trigger a empty-cache detection.
139      * In this case, a preset/default loss value (function on RSSI) is used.
140      * <p>
141      * In normal uses, some RSSI values may never be seen due to channel randomness.
142      * However, the size of such empty RSSI chunk in normal use is generally 1~2.
143      */
144     private static final int BSSID_STAT_EMPTY_COUNT = 3;
145 
146     /**
147      * Sample interval for packet loss statistics, in msec.
148      * <p>
149      * Smaller interval is more accurate but increases sampling cost (battery consumption).
150      */
151     private static final long LINK_SAMPLING_INTERVAL_MS = 1 * 1000;
152 
153     /**
154      * Coefficients (alpha) for moving average for packet loss tracking.
155      * Must be within (0.0, 1.0).
156      * <p>
157      * Equivalent number of samples: N = 2 / alpha - 1 .
158      * We want the historic loss to base on more data points to be statistically reliable.
159      * We want the current instant loss to base on less data points to be responsive.
160      */
161     private static final double EXP_COEFFICIENT_RECORD  = 0.1;
162 
163     /**
164      * See {@link #EXP_COEFFICIENT_RECORD}.
165      */
166     private static final double EXP_COEFFICIENT_MONITOR = 0.5;
167 
168     /**
169      * Thresholds for sending good/poor link notifications, in packet loss %.
170      * Good threshold must be smaller than poor threshold.
171      * Use smaller poor threshold to avoid WiFi more aggressively.
172      * Use smaller good threshold to bring back WiFi more conservatively.
173      * <p>
174      * When approaching the boundary, loss ratio jumps significantly within a few dBs.
175      * 50% loss threshold is a good balance between accuracy and reponsiveness.
176      * <=10% good threshold is a safe value to avoid jumping back to WiFi too easily.
177      */
178     private static final double POOR_LINK_LOSS_THRESHOLD = 0.5;
179 
180     /**
181      * See {@link #POOR_LINK_LOSS_THRESHOLD}.
182      */
183     private static final double GOOD_LINK_LOSS_THRESHOLD = 0.1;
184 
185     /**
186      * Number of samples to confirm before sending a poor link notification.
187      * Response time = confirm_count * sample_interval .
188      * <p>
189      * A smaller threshold improves response speed but may suffer from randomness.
190      * According to experiments, 3~5 are good values to achieve a balance.
191      * These parameters should be tuned along with {@link #LINK_SAMPLING_INTERVAL_MS}.
192      */
193     private static final int POOR_LINK_SAMPLE_COUNT = 3;
194 
195     /**
196      * Minimum volume (converted from pkt/sec) to detect a poor link, to avoid randomness.
197      * <p>
198      * According to experiments, 1pkt/sec is too sensitive but 3pkt/sec is slightly unresponsive.
199      */
200     private static final double POOR_LINK_MIN_VOLUME = 2.0 * LINK_SAMPLING_INTERVAL_MS / 1000.0;
201 
202     /**
203      * When a poor link is detected, we scan over this range (based on current
204      * poor link RSSI) for a target RSSI that satisfies a target packet loss.
205      * Refer to {@link #GOOD_LINK_TARGET}.
206      * <p>
207      * We want range_min not too small to avoid jumping back to WiFi too easily.
208      */
209     private static final int GOOD_LINK_RSSI_RANGE_MIN = 3;
210 
211     /**
212      * See {@link #GOOD_LINK_RSSI_RANGE_MIN}.
213      */
214     private static final int GOOD_LINK_RSSI_RANGE_MAX = 20;
215 
216     /**
217      * Adaptive good link target to avoid flapping.
218      * When a poor link is detected, a good link target is calculated as follows:
219      * <p>
220      *      targetRSSI = min { rssi | loss(rssi) < GOOD_LINK_LOSS_THRESHOLD } + rssi_adj[i],
221      *                   where rssi is within the above GOOD_LINK_RSSI_RANGE.
222      *      targetCount = sample_count[i] .
223      * <p>
224      * While WiFi is being avoided, we keep monitoring its signal strength.
225      * Good link notification is sent when we see current RSSI >= targetRSSI
226      * for targetCount consecutive times.
227      * <p>
228      * Index i is incremented each time after a poor link detection.
229      * Index i is decreased to at most k if the last poor link was at lease reduce_time[k] ago.
230      * <p>
231      * Intuitively, larger index i makes it more difficult to get back to WiFi, avoiding flapping.
232      * In experiments, (+9 dB / 30 counts) makes it quite difficult to achieve.
233      * Avoid using it unless flapping is really bad (say, last poor link is < 1 min ago).
234      */
235     private static final GoodLinkTarget[] GOOD_LINK_TARGET = {
236         /*                  rssi_adj,       sample_count,   reduce_time */
237         new GoodLinkTarget( 0,              3,              30 * 60000   ),
238         new GoodLinkTarget( 3,              5,              5  * 60000   ),
239         new GoodLinkTarget( 6,              10,             1  * 60000   ),
240         new GoodLinkTarget( 9,              30,             0  * 60000   ),
241     };
242 
243     /**
244      * The max time to avoid a BSSID, to prevent avoiding forever.
245      * If current RSSI is at least min_rssi[i], the max avoidance time is at most max_time[i]
246      * <p>
247      * It is unusual to experience high packet loss at high RSSI. Something unusual must be
248      * happening (e.g. strong interference). For higher signal strengths, we set the avoidance
249      * time to be low to allow for quick turn around from temporary interference.
250      * <p>
251      * See {@link BssidStatistics#poorLinkDetected}.
252      */
253     private static final MaxAvoidTime[] MAX_AVOID_TIME = {
254         /*                  max_time,           min_rssi */
255         new MaxAvoidTime(   30 * 60000,         -200      ),
256         new MaxAvoidTime(   5  * 60000,         -70       ),
257         new MaxAvoidTime(   0  * 60000,         -55       ),
258     };
259 
260     /* Framework related */
261     private Context mContext;
262     private ContentResolver mContentResolver;
263     private WifiManager mWifiManager;
264     private IntentFilter mIntentFilter;
265     private BroadcastReceiver mBroadcastReceiver;
266     private AsyncChannel mWsmChannel = new AsyncChannel();
267     private WifiInfo mWifiInfo;
268     private LinkProperties mLinkProperties;
269 
270     /* System settingss related */
271     private static boolean sWifiOnly = false;
272     private boolean mPoorNetworkDetectionEnabled;
273 
274     /* Poor link detection related */
275     private LruCache<String, BssidStatistics> mBssidCache =
276             new LruCache<String, BssidStatistics>(BSSID_STAT_CACHE_SIZE);
277     private int mRssiFetchToken = 0;
278     private int mCurrentSignalLevel;
279     private BssidStatistics mCurrentBssid;
280     private VolumeWeightedEMA mCurrentLoss;
281     private boolean mIsScreenOn = true;
282     private static double sPresetLoss[];
283 
284     /* WiFi watchdog state machine related */
285     private DefaultState mDefaultState = new DefaultState();
286     private WatchdogDisabledState mWatchdogDisabledState = new WatchdogDisabledState();
287     private WatchdogEnabledState mWatchdogEnabledState = new WatchdogEnabledState();
288     private NotConnectedState mNotConnectedState = new NotConnectedState();
289     private VerifyingLinkState mVerifyingLinkState = new VerifyingLinkState();
290     private ConnectedState mConnectedState = new ConnectedState();
291     private OnlineWatchState mOnlineWatchState = new OnlineWatchState();
292     private LinkMonitoringState mLinkMonitoringState = new LinkMonitoringState();
293     private OnlineState mOnlineState = new OnlineState();
294 
295     /**
296      * STATE MAP
297      *          Default
298      *         /       \
299      * Disabled      Enabled
300      *             /     \     \
301      * NotConnected  Verifying  Connected
302      *                         /---------\
303      *                       (all other states)
304      */
WifiWatchdogStateMachine(Context context, Messenger dstMessenger)305     private WifiWatchdogStateMachine(Context context, Messenger dstMessenger) {
306         super("WifiWatchdogStateMachine");
307         mContext = context;
308         mContentResolver = context.getContentResolver();
309         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
310 
311         mWsmChannel.connectSync(mContext, getHandler(), dstMessenger);
312 
313         setupNetworkReceiver();
314 
315         // the content observer to listen needs a handler
316         registerForSettingsChanges();
317         registerForWatchdogToggle();
318         addState(mDefaultState);
319             addState(mWatchdogDisabledState, mDefaultState);
320             addState(mWatchdogEnabledState, mDefaultState);
321                 addState(mNotConnectedState, mWatchdogEnabledState);
322                 addState(mVerifyingLinkState, mWatchdogEnabledState);
323                 addState(mConnectedState, mWatchdogEnabledState);
324                     addState(mOnlineWatchState, mConnectedState);
325                     addState(mLinkMonitoringState, mConnectedState);
326                     addState(mOnlineState, mConnectedState);
327 
328         if (isWatchdogEnabled()) {
329             setInitialState(mNotConnectedState);
330         } else {
331             setInitialState(mWatchdogDisabledState);
332         }
333         setLogRecSize(25);
334         setLogOnlyTransitions(true);
335         updateSettings();
336     }
337 
makeWifiWatchdogStateMachine(Context context, Messenger dstMessenger)338     public static WifiWatchdogStateMachine makeWifiWatchdogStateMachine(Context context, Messenger dstMessenger) {
339         ContentResolver contentResolver = context.getContentResolver();
340 
341         ConnectivityManager cm = (ConnectivityManager) context.getSystemService(
342                 Context.CONNECTIVITY_SERVICE);
343         sWifiOnly = (cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false);
344 
345         // Watchdog is always enabled. Poor network detection can be seperately turned on/off
346         // TODO: Remove this setting & clean up state machine since we always have
347         // watchdog in an enabled state
348         putSettingsGlobalBoolean(contentResolver, Settings.Global.WIFI_WATCHDOG_ON, true);
349 
350         WifiWatchdogStateMachine wwsm = new WifiWatchdogStateMachine(context, dstMessenger);
351         wwsm.start();
352         return wwsm;
353     }
354 
setupNetworkReceiver()355     private void setupNetworkReceiver() {
356         mBroadcastReceiver = new BroadcastReceiver() {
357             @Override
358             public void onReceive(Context context, Intent intent) {
359                 String action = intent.getAction();
360                 if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) {
361                     obtainMessage(EVENT_RSSI_CHANGE,
362                             intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200), 0).sendToTarget();
363                 } else if (action.equals(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION)) {
364                     sendMessage(EVENT_SUPPLICANT_STATE_CHANGE, intent);
365                 } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
366                     sendMessage(EVENT_NETWORK_STATE_CHANGE, intent);
367                 } else if (action.equals(Intent.ACTION_SCREEN_ON)) {
368                     sendMessage(EVENT_SCREEN_ON);
369                 } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
370                     sendMessage(EVENT_SCREEN_OFF);
371                 } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
372                     sendMessage(EVENT_WIFI_RADIO_STATE_CHANGE,intent.getIntExtra(
373                             WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN));
374                 }
375             }
376         };
377 
378         mIntentFilter = new IntentFilter();
379         mIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
380         mIntentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
381         mIntentFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
382         mIntentFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
383         mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
384         mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
385         mContext.registerReceiver(mBroadcastReceiver, mIntentFilter);
386     }
387 
388     /**
389      * Observes the watchdog on/off setting, and takes action when changed.
390      */
registerForWatchdogToggle()391     private void registerForWatchdogToggle() {
392         ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
393             @Override
394             public void onChange(boolean selfChange) {
395                 sendMessage(EVENT_WATCHDOG_TOGGLED);
396             }
397         };
398 
399         mContext.getContentResolver().registerContentObserver(
400                 Settings.Global.getUriFor(Settings.Global.WIFI_WATCHDOG_ON),
401                 false, contentObserver);
402     }
403 
404     /**
405      * Observes watchdogs secure setting changes.
406      */
registerForSettingsChanges()407     private void registerForSettingsChanges() {
408         ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
409             @Override
410             public void onChange(boolean selfChange) {
411                 sendMessage(EVENT_WATCHDOG_SETTINGS_CHANGE);
412             }
413         };
414 
415         mContext.getContentResolver().registerContentObserver(
416                 Settings.Global.getUriFor(Settings.Global.WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED),
417                 false, contentObserver);
418     }
419 
dump(FileDescriptor fd, PrintWriter pw, String[] args)420     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
421         super.dump(fd, pw, args);
422         pw.println("mWifiInfo: [" + mWifiInfo + "]");
423         pw.println("mLinkProperties: [" + mLinkProperties + "]");
424         pw.println("mCurrentSignalLevel: [" + mCurrentSignalLevel + "]");
425         pw.println("mPoorNetworkDetectionEnabled: [" + mPoorNetworkDetectionEnabled + "]");
426     }
427 
isWatchdogEnabled()428     private boolean isWatchdogEnabled() {
429         boolean ret = getSettingsGlobalBoolean(
430                 mContentResolver, Settings.Global.WIFI_WATCHDOG_ON, true);
431         if (DBG) logd("Watchdog enabled " + ret);
432         return ret;
433     }
434 
updateSettings()435     private void updateSettings() {
436         if (DBG) logd("Updating secure settings");
437 
438         // Unconditionally disable poor network avoidance, since this mechanism is obsolete
439         mPoorNetworkDetectionEnabled = false;
440     }
441 
442     /**
443      * Default state, guard for unhandled messages.
444      */
445     class DefaultState extends State {
446         @Override
enter()447         public void enter() {
448             if (DBG) logd(getName());
449         }
450 
451         @Override
processMessage(Message msg)452         public boolean processMessage(Message msg) {
453             switch (msg.what) {
454                 case EVENT_WATCHDOG_SETTINGS_CHANGE:
455                     updateSettings();
456                     if (DBG) logd("Updating wifi-watchdog secure settings");
457                     break;
458                 case EVENT_RSSI_CHANGE:
459                     mCurrentSignalLevel = calculateSignalLevel(msg.arg1);
460                     break;
461                 case EVENT_WIFI_RADIO_STATE_CHANGE:
462                 case EVENT_NETWORK_STATE_CHANGE:
463                 case EVENT_SUPPLICANT_STATE_CHANGE:
464                 case EVENT_BSSID_CHANGE:
465                 case CMD_RSSI_FETCH:
466                 case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED:
467                 case WifiManager.RSSI_PKTCNT_FETCH_FAILED:
468                     // ignore
469                     break;
470                 case EVENT_SCREEN_ON:
471                     mIsScreenOn = true;
472                     break;
473                 case EVENT_SCREEN_OFF:
474                     mIsScreenOn = false;
475                     break;
476                 default:
477                     loge("Unhandled message " + msg + " in state " + getCurrentState().getName());
478                     break;
479             }
480             return HANDLED;
481         }
482     }
483 
484     /**
485      * WiFi watchdog is disabled by the setting.
486      */
487     class WatchdogDisabledState extends State {
488         @Override
enter()489         public void enter() {
490             if (DBG) logd(getName());
491         }
492 
493         @Override
processMessage(Message msg)494         public boolean processMessage(Message msg) {
495             switch (msg.what) {
496                 case EVENT_WATCHDOG_TOGGLED:
497                     if (isWatchdogEnabled())
498                         transitionTo(mNotConnectedState);
499                     return HANDLED;
500                 case EVENT_NETWORK_STATE_CHANGE:
501                     Intent intent = (Intent) msg.obj;
502                     NetworkInfo networkInfo = (NetworkInfo)
503                             intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
504 
505                     switch (networkInfo.getDetailedState()) {
506                         case VERIFYING_POOR_LINK:
507                             if (DBG) logd("Watchdog disabled, verify link");
508                             sendLinkStatusNotification(true);
509                             break;
510                         default:
511                             break;
512                     }
513                     break;
514             }
515             return NOT_HANDLED;
516         }
517     }
518 
519     /**
520      * WiFi watchdog is enabled by the setting.
521      */
522     class WatchdogEnabledState extends State {
523         @Override
enter()524         public void enter() {
525             if (DBG) logd(getName());
526         }
527 
528         @Override
processMessage(Message msg)529         public boolean processMessage(Message msg) {
530             Intent intent;
531             switch (msg.what) {
532                 case EVENT_WATCHDOG_TOGGLED:
533                     if (!isWatchdogEnabled())
534                         transitionTo(mWatchdogDisabledState);
535                     break;
536 
537                 case EVENT_NETWORK_STATE_CHANGE:
538                     intent = (Intent) msg.obj;
539                     NetworkInfo networkInfo =
540                             (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
541                     if (DBG) logd("Network state change " + networkInfo.getDetailedState());
542 
543                     mWifiInfo = (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
544                     updateCurrentBssid(mWifiInfo != null ? mWifiInfo.getBSSID() : null);
545 
546                     switch (networkInfo.getDetailedState()) {
547                         case VERIFYING_POOR_LINK:
548                             mLinkProperties = (LinkProperties) intent.getParcelableExtra(
549                                     WifiManager.EXTRA_LINK_PROPERTIES);
550                             if (mPoorNetworkDetectionEnabled) {
551                                 if (mWifiInfo == null || mCurrentBssid == null) {
552                                     loge("Ignore, wifiinfo " + mWifiInfo +" bssid " + mCurrentBssid);
553                                     sendLinkStatusNotification(true);
554                                 } else {
555                                     transitionTo(mVerifyingLinkState);
556                                 }
557                             } else {
558                                 sendLinkStatusNotification(true);
559                             }
560                             break;
561                         case CONNECTED:
562                             transitionTo(mOnlineWatchState);
563                             break;
564                         default:
565                             transitionTo(mNotConnectedState);
566                             break;
567                     }
568                     break;
569 
570                 case EVENT_SUPPLICANT_STATE_CHANGE:
571                     intent = (Intent) msg.obj;
572                     SupplicantState supplicantState = (SupplicantState) intent.getParcelableExtra(
573                             WifiManager.EXTRA_NEW_STATE);
574                     if (supplicantState == SupplicantState.COMPLETED) {
575                         mWifiInfo = mWifiManager.getConnectionInfo();
576                         updateCurrentBssid(mWifiInfo.getBSSID());
577                     }
578                     break;
579 
580                 case EVENT_WIFI_RADIO_STATE_CHANGE:
581                     if (msg.arg1 == WifiManager.WIFI_STATE_DISABLING) {
582                         transitionTo(mNotConnectedState);
583                     }
584                     break;
585 
586                 default:
587                     return NOT_HANDLED;
588             }
589 
590             return HANDLED;
591         }
592     }
593 
594     /**
595      * WiFi is disconnected.
596      */
597     class NotConnectedState extends State {
598         @Override
enter()599         public void enter() {
600             if (DBG) logd(getName());
601         }
602     }
603 
604     /**
605      * WiFi is connected, but waiting for good link detection message.
606      */
607     class VerifyingLinkState extends State {
608 
609         private int mSampleCount;
610 
611         @Override
enter()612         public void enter() {
613             if (DBG) logd(getName());
614             mSampleCount = 0;
615             if (mCurrentBssid != null) mCurrentBssid.newLinkDetected();
616             sendMessage(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0));
617         }
618 
619         @Override
processMessage(Message msg)620         public boolean processMessage(Message msg) {
621             switch (msg.what) {
622                 case EVENT_WATCHDOG_SETTINGS_CHANGE:
623                     updateSettings();
624                     if (!mPoorNetworkDetectionEnabled) {
625                         sendLinkStatusNotification(true);
626                     }
627                     break;
628 
629                 case EVENT_BSSID_CHANGE:
630                     transitionTo(mVerifyingLinkState);
631                     break;
632 
633                 case CMD_RSSI_FETCH:
634                     if (msg.arg1 == mRssiFetchToken) {
635                         mWsmChannel.sendMessage(WifiManager.RSSI_PKTCNT_FETCH);
636                         sendMessageDelayed(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0),
637                                 LINK_SAMPLING_INTERVAL_MS);
638                     }
639                     break;
640 
641                 case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED:
642                     if (mCurrentBssid == null || msg.obj == null) {
643                         break;
644                     }
645                     RssiPacketCountInfo info = (RssiPacketCountInfo) msg.obj;
646                     int rssi = info.rssi;
647                     if (DBG) logd("Fetch RSSI succeed, rssi=" + rssi);
648 
649                     long time = mCurrentBssid.mBssidAvoidTimeMax - SystemClock.elapsedRealtime();
650                     if (time <= 0) {
651                         // max avoidance time is met
652                         if (DBG) logd("Max avoid time elapsed");
653                         sendLinkStatusNotification(true);
654                     } else {
655                         if (rssi >= mCurrentBssid.mGoodLinkTargetRssi) {
656                             if (++mSampleCount >= mCurrentBssid.mGoodLinkTargetCount) {
657                                 // link is good again
658                                 if (DBG) logd("Good link detected, rssi=" + rssi);
659                                 mCurrentBssid.mBssidAvoidTimeMax = 0;
660                                 sendLinkStatusNotification(true);
661                             }
662                         } else {
663                             mSampleCount = 0;
664                             if (DBG) logd("Link is still poor, time left=" + time);
665                         }
666                     }
667                     break;
668 
669                 case WifiManager.RSSI_PKTCNT_FETCH_FAILED:
670                     if (DBG) logd("RSSI_FETCH_FAILED");
671                     break;
672 
673                 default:
674                     return NOT_HANDLED;
675             }
676             return HANDLED;
677         }
678     }
679 
680     /**
681      * WiFi is connected and link is verified.
682      */
683     class ConnectedState extends State {
684         @Override
enter()685         public void enter() {
686             if (DBG) logd(getName());
687         }
688 
689         @Override
processMessage(Message msg)690         public boolean processMessage(Message msg) {
691             switch (msg.what) {
692                 case EVENT_WATCHDOG_SETTINGS_CHANGE:
693                     updateSettings();
694                     if (mPoorNetworkDetectionEnabled) {
695                         transitionTo(mOnlineWatchState);
696                     } else {
697                         transitionTo(mOnlineState);
698                     }
699                     return HANDLED;
700             }
701             return NOT_HANDLED;
702         }
703     }
704 
705     /**
706      * RSSI is high enough and don't need link monitoring.
707      */
708     class OnlineWatchState extends State {
709         @Override
enter()710         public void enter() {
711             if (DBG) logd(getName());
712             if (mPoorNetworkDetectionEnabled) {
713                 // treat entry as an rssi change
714                 handleRssiChange();
715             } else {
716                 transitionTo(mOnlineState);
717             }
718         }
719 
handleRssiChange()720         private void handleRssiChange() {
721             if (mCurrentSignalLevel <= LINK_MONITOR_LEVEL_THRESHOLD && mCurrentBssid != null) {
722                 transitionTo(mLinkMonitoringState);
723             } else {
724                 // stay here
725             }
726         }
727 
728         @Override
processMessage(Message msg)729         public boolean processMessage(Message msg) {
730             switch (msg.what) {
731                 case EVENT_RSSI_CHANGE:
732                     mCurrentSignalLevel = calculateSignalLevel(msg.arg1);
733                     handleRssiChange();
734                     break;
735                 default:
736                     return NOT_HANDLED;
737             }
738             return HANDLED;
739         }
740     }
741 
742     /**
743      * Keep sampling the link and monitor any poor link situation.
744      */
745     class LinkMonitoringState extends State {
746 
747         private int mSampleCount;
748 
749         private int mLastRssi;
750         private int mLastTxGood;
751         private int mLastTxBad;
752 
753         @Override
enter()754         public void enter() {
755             if (DBG) logd(getName());
756             mSampleCount = 0;
757             mCurrentLoss = new VolumeWeightedEMA(EXP_COEFFICIENT_MONITOR);
758             sendMessage(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0));
759         }
760 
761         @Override
processMessage(Message msg)762         public boolean processMessage(Message msg) {
763             switch (msg.what) {
764                 case EVENT_RSSI_CHANGE:
765                     mCurrentSignalLevel = calculateSignalLevel(msg.arg1);
766                     if (mCurrentSignalLevel <= LINK_MONITOR_LEVEL_THRESHOLD) {
767                         // stay here;
768                     } else {
769                         // we don't need frequent RSSI monitoring any more
770                         transitionTo(mOnlineWatchState);
771                     }
772                     break;
773 
774                 case EVENT_BSSID_CHANGE:
775                     transitionTo(mLinkMonitoringState);
776                     break;
777 
778                 case CMD_RSSI_FETCH:
779                     if (!mIsScreenOn) {
780                         transitionTo(mOnlineState);
781                     } else if (msg.arg1 == mRssiFetchToken) {
782                         mWsmChannel.sendMessage(WifiManager.RSSI_PKTCNT_FETCH);
783                         sendMessageDelayed(obtainMessage(CMD_RSSI_FETCH, ++mRssiFetchToken, 0),
784                                 LINK_SAMPLING_INTERVAL_MS);
785                     }
786                     break;
787 
788                 case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED:
789                     if (mCurrentBssid == null) {
790                         break;
791                     }
792                     RssiPacketCountInfo info = (RssiPacketCountInfo) msg.obj;
793                     int rssi = info.rssi;
794                     int mrssi = (mLastRssi + rssi) / 2;
795                     int txbad = info.txbad;
796                     int txgood = info.txgood;
797                     if (DBG) logd("Fetch RSSI succeed, rssi=" + rssi + " mrssi=" + mrssi + " txbad="
798                             + txbad + " txgood=" + txgood);
799 
800                     // skip the first data point as we want incremental values
801                     long now = SystemClock.elapsedRealtime();
802                     if (now - mCurrentBssid.mLastTimeSample < LINK_SAMPLING_INTERVAL_MS * 2) {
803 
804                         // update packet loss statistics
805                         int dbad = txbad - mLastTxBad;
806                         int dgood = txgood - mLastTxGood;
807                         int dtotal = dbad + dgood;
808 
809                         if (dtotal > 0) {
810                             // calculate packet loss in the last sampling interval
811                             double loss = ((double) dbad) / ((double) dtotal);
812 
813                             mCurrentLoss.update(loss, dtotal);
814 
815                             if (DBG) {
816                                 DecimalFormat df = new DecimalFormat("#.##");
817                                 logd("Incremental loss=" + dbad + "/" + dtotal + " Current loss="
818                                         + df.format(mCurrentLoss.mValue * 100) + "% volume="
819                                         + df.format(mCurrentLoss.mVolume));
820                             }
821 
822                             mCurrentBssid.updateLoss(mrssi, loss, dtotal);
823 
824                             // check for high packet loss and send poor link notification
825                             if (mCurrentLoss.mValue > POOR_LINK_LOSS_THRESHOLD
826                                     && mCurrentLoss.mVolume > POOR_LINK_MIN_VOLUME) {
827                                 if (++mSampleCount >= POOR_LINK_SAMPLE_COUNT)
828                                     if (mCurrentBssid.poorLinkDetected(rssi)) {
829                                         sendLinkStatusNotification(false);
830                                         ++mRssiFetchToken;
831                                     }
832                             } else {
833                                 mSampleCount = 0;
834                             }
835                         }
836                     }
837 
838                     mCurrentBssid.mLastTimeSample = now;
839                     mLastTxBad = txbad;
840                     mLastTxGood = txgood;
841                     mLastRssi = rssi;
842                     break;
843 
844                 case WifiManager.RSSI_PKTCNT_FETCH_FAILED:
845                     // can happen if we are waiting to get a disconnect notification
846                     if (DBG) logd("RSSI_FETCH_FAILED");
847                     break;
848 
849                 default:
850                     return NOT_HANDLED;
851             }
852             return HANDLED;
853         }
854    }
855 
856     /**
857      * Child state of ConnectedState indicating that we are online and there is nothing to do.
858      */
859     class OnlineState extends State {
860         @Override
enter()861         public void enter() {
862             if (DBG) logd(getName());
863         }
864 
865         @Override
processMessage(Message msg)866         public boolean processMessage(Message msg) {
867             switch (msg.what) {
868                 case EVENT_SCREEN_ON:
869                     mIsScreenOn = true;
870                     if (mPoorNetworkDetectionEnabled)
871                         transitionTo(mOnlineWatchState);
872                     break;
873                 default:
874                     return NOT_HANDLED;
875             }
876             return HANDLED;
877         }
878     }
879 
updateCurrentBssid(String bssid)880     private void updateCurrentBssid(String bssid) {
881         if (DBG) logd("Update current BSSID to " + (bssid != null ? bssid : "null"));
882 
883         // if currently not connected, then set current BSSID to null
884         if (bssid == null) {
885             if (mCurrentBssid == null) return;
886             mCurrentBssid = null;
887             if (DBG) logd("BSSID changed");
888             sendMessage(EVENT_BSSID_CHANGE);
889             return;
890         }
891 
892         // if it is already the current BSSID, then done
893         if (mCurrentBssid != null && bssid.equals(mCurrentBssid.mBssid)) return;
894 
895         // search for the new BSSID in the cache, add to cache if not found
896         mCurrentBssid = mBssidCache.get(bssid);
897         if (mCurrentBssid == null) {
898             mCurrentBssid = new BssidStatistics(bssid);
899             mBssidCache.put(bssid, mCurrentBssid);
900         }
901 
902         // send BSSID change notification
903         if (DBG) logd("BSSID changed");
904         sendMessage(EVENT_BSSID_CHANGE);
905     }
906 
calculateSignalLevel(int rssi)907     private int calculateSignalLevel(int rssi) {
908         int signalLevel = WifiManager.calculateSignalLevel(rssi, WifiManager.RSSI_LEVELS);
909         if (DBG)
910             logd("RSSI current: " + mCurrentSignalLevel + " new: " + rssi + ", " + signalLevel);
911         return signalLevel;
912     }
913 
sendLinkStatusNotification(boolean isGood)914     private void sendLinkStatusNotification(boolean isGood) {
915         if (DBG) logd("########################################");
916         if (isGood) {
917             mWsmChannel.sendMessage(GOOD_LINK_DETECTED);
918             if (mCurrentBssid != null) {
919                 mCurrentBssid.mLastTimeGood = SystemClock.elapsedRealtime();
920             }
921             if (DBG) logd("Good link notification is sent");
922         } else {
923             mWsmChannel.sendMessage(POOR_LINK_DETECTED);
924             if (mCurrentBssid != null) {
925                 mCurrentBssid.mLastTimePoor = SystemClock.elapsedRealtime();
926             }
927             logd("Poor link notification is sent");
928         }
929     }
930 
931     /**
932      * Convenience function for retrieving a single secure settings value as a
933      * boolean. Note that internally setting values are always stored as
934      * strings; this function converts the string to a boolean for you. The
935      * default value will be returned if the setting is not defined or not a
936      * valid boolean.
937      *
938      * @param cr The ContentResolver to access.
939      * @param name The name of the setting to retrieve.
940      * @param def Value to return if the setting is not defined.
941      * @return The setting's current value, or 'def' if it is not defined or not
942      *         a valid boolean.
943      */
getSettingsGlobalBoolean(ContentResolver cr, String name, boolean def)944     private static boolean getSettingsGlobalBoolean(ContentResolver cr, String name, boolean def) {
945         return Settings.Global.getInt(cr, name, def ? 1 : 0) == 1;
946     }
947 
948     /**
949      * Convenience function for updating a single settings value as an integer.
950      * This will either create a new entry in the table if the given name does
951      * not exist, or modify the value of the existing row with that name. Note
952      * that internally setting values are always stored as strings, so this
953      * function converts the given value to a string before storing it.
954      *
955      * @param cr The ContentResolver to access.
956      * @param name The name of the setting to modify.
957      * @param value The new value for the setting.
958      * @return true if the value was set, false on database errors
959      */
putSettingsGlobalBoolean(ContentResolver cr, String name, boolean value)960     private static boolean putSettingsGlobalBoolean(ContentResolver cr, String name, boolean value) {
961         return Settings.Global.putInt(cr, name, value ? 1 : 0);
962     }
963 
964     /**
965      * Bundle of good link count parameters
966      */
967     private static class GoodLinkTarget {
968         public final int RSSI_ADJ_DBM;
969         public final int SAMPLE_COUNT;
970         public final int REDUCE_TIME_MS;
GoodLinkTarget(int adj, int count, int time)971         public GoodLinkTarget(int adj, int count, int time) {
972             RSSI_ADJ_DBM = adj;
973             SAMPLE_COUNT = count;
974             REDUCE_TIME_MS = time;
975         }
976     }
977 
978     /**
979      * Bundle of max avoidance time parameters
980      */
981     private static class MaxAvoidTime {
982         public final int TIME_MS;
983         public final int MIN_RSSI_DBM;
MaxAvoidTime(int time, int rssi)984         public MaxAvoidTime(int time, int rssi) {
985             TIME_MS = time;
986             MIN_RSSI_DBM = rssi;
987         }
988     }
989 
990     /**
991      * Volume-weighted Exponential Moving Average (V-EMA)
992      *    - volume-weighted:  each update has its own weight (number of packets)
993      *    - exponential:      O(1) time and O(1) space for both update and query
994      *    - moving average:   reflect most recent results and expire old ones
995      */
996     private class VolumeWeightedEMA {
997         private double mValue;
998         private double mVolume;
999         private double mProduct;
1000         private final double mAlpha;
1001 
VolumeWeightedEMA(double coefficient)1002         public VolumeWeightedEMA(double coefficient) {
1003             mValue   = 0.0;
1004             mVolume  = 0.0;
1005             mProduct = 0.0;
1006             mAlpha   = coefficient;
1007         }
1008 
update(double newValue, int newVolume)1009         public void update(double newValue, int newVolume) {
1010             if (newVolume <= 0) return;
1011             // core update formulas
1012             double newProduct = newValue * newVolume;
1013             mProduct = mAlpha * newProduct + (1 - mAlpha) * mProduct;
1014             mVolume  = mAlpha * newVolume  + (1 - mAlpha) * mVolume;
1015             mValue   = mProduct / mVolume;
1016         }
1017     }
1018 
1019     /**
1020      * Record (RSSI -> pakce loss %) mappings of one BSSID
1021      */
1022     private class BssidStatistics {
1023 
1024         /* MAC address of this BSSID */
1025         private final String mBssid;
1026 
1027         /* RSSI -> packet loss % mappings */
1028         private VolumeWeightedEMA[] mEntries;
1029         private int mRssiBase;
1030         private int mEntriesSize;
1031 
1032         /* Target to send good link notification, set when poor link is detected */
1033         private int mGoodLinkTargetRssi;
1034         private int mGoodLinkTargetCount;
1035 
1036         /* Index of GOOD_LINK_TARGET array */
1037         private int mGoodLinkTargetIndex;
1038 
1039         /* Timestamps of some last events */
1040         private long mLastTimeSample;
1041         private long mLastTimeGood;
1042         private long mLastTimePoor;
1043 
1044         /* Max time to avoid this BSSID */
1045         private long mBssidAvoidTimeMax;
1046 
1047         /**
1048          * Constructor
1049          *
1050          * @param bssid is the address of this BSSID
1051          */
BssidStatistics(String bssid)1052         public BssidStatistics(String bssid) {
1053             this.mBssid = bssid;
1054             mRssiBase = BSSID_STAT_RANGE_LOW_DBM;
1055             mEntriesSize = BSSID_STAT_RANGE_HIGH_DBM - BSSID_STAT_RANGE_LOW_DBM + 1;
1056             mEntries = new VolumeWeightedEMA[mEntriesSize];
1057             for (int i = 0; i < mEntriesSize; i++)
1058                 mEntries[i] = new VolumeWeightedEMA(EXP_COEFFICIENT_RECORD);
1059         }
1060 
1061         /**
1062          * Update this BSSID cache
1063          *
1064          * @param rssi is the RSSI
1065          * @param value is the new instant loss value at this RSSI
1066          * @param volume is the volume for this single update
1067          */
updateLoss(int rssi, double value, int volume)1068         public void updateLoss(int rssi, double value, int volume) {
1069             if (volume <= 0) return;
1070             int index = rssi - mRssiBase;
1071             if (index < 0 || index >= mEntriesSize) return;
1072             mEntries[index].update(value, volume);
1073             if (DBG) {
1074                 DecimalFormat df = new DecimalFormat("#.##");
1075                 logd("Cache updated: loss[" + rssi + "]=" + df.format(mEntries[index].mValue * 100)
1076                         + "% volume=" + df.format(mEntries[index].mVolume));
1077             }
1078         }
1079 
1080         /**
1081          * Get preset loss if the cache has insufficient data, observed from experiments.
1082          *
1083          * @param rssi is the input RSSI
1084          * @return preset loss of the given RSSI
1085          */
presetLoss(int rssi)1086         public double presetLoss(int rssi) {
1087             if (rssi <= -90) return 1.0;
1088             if (rssi > 0) return 0.0;
1089 
1090             if (sPresetLoss == null) {
1091                 // pre-calculate all preset losses only once, then reuse them
1092                 final int size = 90;
1093                 sPresetLoss = new double[size];
1094                 for (int i = 0; i < size; i++) sPresetLoss[i] = 1.0 / Math.pow(90 - i, 1.5);
1095             }
1096             return sPresetLoss[-rssi];
1097         }
1098 
1099         /**
1100          * A poor link is detected, calculate a target RSSI to bring WiFi back.
1101          *
1102          * @param rssi is the current RSSI
1103          * @return true iff the current BSSID should be avoided
1104          */
poorLinkDetected(int rssi)1105         public boolean poorLinkDetected(int rssi) {
1106             if (DBG) logd("Poor link detected, rssi=" + rssi);
1107 
1108             long now = SystemClock.elapsedRealtime();
1109             long lastGood = now - mLastTimeGood;
1110             long lastPoor = now - mLastTimePoor;
1111 
1112             // reduce the difficulty of good link target if last avoidance was long time ago
1113             while (mGoodLinkTargetIndex > 0
1114                     && lastPoor >= GOOD_LINK_TARGET[mGoodLinkTargetIndex - 1].REDUCE_TIME_MS)
1115                 mGoodLinkTargetIndex--;
1116             mGoodLinkTargetCount = GOOD_LINK_TARGET[mGoodLinkTargetIndex].SAMPLE_COUNT;
1117 
1118             // scan for a target RSSI at which the link is good
1119             int from = rssi + GOOD_LINK_RSSI_RANGE_MIN;
1120             int to = rssi + GOOD_LINK_RSSI_RANGE_MAX;
1121             mGoodLinkTargetRssi = findRssiTarget(from, to, GOOD_LINK_LOSS_THRESHOLD);
1122             mGoodLinkTargetRssi += GOOD_LINK_TARGET[mGoodLinkTargetIndex].RSSI_ADJ_DBM;
1123             if (mGoodLinkTargetIndex < GOOD_LINK_TARGET.length - 1) mGoodLinkTargetIndex++;
1124 
1125             // calculate max avoidance time to prevent avoiding forever
1126             int p = 0, pmax = MAX_AVOID_TIME.length - 1;
1127             while (p < pmax && rssi >= MAX_AVOID_TIME[p + 1].MIN_RSSI_DBM) p++;
1128             long avoidMax = MAX_AVOID_TIME[p].TIME_MS;
1129 
1130             // don't avoid if max avoidance time is 0 (RSSI is super high)
1131             if (avoidMax <= 0) return false;
1132 
1133             // set max avoidance time, send poor link notification
1134             mBssidAvoidTimeMax = now + avoidMax;
1135 
1136             if (DBG) logd("goodRssi=" + mGoodLinkTargetRssi + " goodCount=" + mGoodLinkTargetCount
1137                     + " lastGood=" + lastGood + " lastPoor=" + lastPoor + " avoidMax=" + avoidMax);
1138 
1139             return true;
1140         }
1141 
1142         /**
1143          * A new BSSID is connected, recalculate target RSSI threshold
1144          */
newLinkDetected()1145         public void newLinkDetected() {
1146             // if this BSSID is currently being avoided, the reuse those values
1147             if (mBssidAvoidTimeMax > 0) {
1148                 if (DBG) logd("Previous avoidance still in effect, rssi=" + mGoodLinkTargetRssi
1149                         + " count=" + mGoodLinkTargetCount);
1150                 return;
1151             }
1152 
1153             // calculate a new RSSI threshold for new link verifying
1154             int from = BSSID_STAT_RANGE_LOW_DBM;
1155             int to = BSSID_STAT_RANGE_HIGH_DBM;
1156             mGoodLinkTargetRssi = findRssiTarget(from, to, GOOD_LINK_LOSS_THRESHOLD);
1157             mGoodLinkTargetCount = 1;
1158             mBssidAvoidTimeMax = SystemClock.elapsedRealtime() + MAX_AVOID_TIME[0].TIME_MS;
1159             if (DBG) logd("New link verifying target set, rssi=" + mGoodLinkTargetRssi + " count="
1160                     + mGoodLinkTargetCount);
1161         }
1162 
1163         /**
1164          * Return the first RSSI within the range where loss[rssi] < threshold
1165          *
1166          * @param from start scanning from this RSSI
1167          * @param to stop scanning at this RSSI
1168          * @param threshold target threshold for scanning
1169          * @return target RSSI
1170          */
findRssiTarget(int from, int to, double threshold)1171         public int findRssiTarget(int from, int to, double threshold) {
1172             from -= mRssiBase;
1173             to -= mRssiBase;
1174             int emptyCount = 0;
1175             int d = from < to ? 1 : -1;
1176             for (int i = from; i != to; i += d)
1177                 // don't use a data point if it volume is too small (statistically unreliable)
1178                 if (i >= 0 && i < mEntriesSize && mEntries[i].mVolume > 1.0) {
1179                     emptyCount = 0;
1180                     if (mEntries[i].mValue < threshold) {
1181                         // scan target found
1182                         int rssi = mRssiBase + i;
1183                         if (DBG) {
1184                             DecimalFormat df = new DecimalFormat("#.##");
1185                             logd("Scan target found: rssi=" + rssi + " threshold="
1186                                     + df.format(threshold * 100) + "% value="
1187                                     + df.format(mEntries[i].mValue * 100) + "% volume="
1188                                     + df.format(mEntries[i].mVolume));
1189                         }
1190                         return rssi;
1191                     }
1192                 } else if (++emptyCount >= BSSID_STAT_EMPTY_COUNT) {
1193                     // cache has insufficient data around this RSSI, use preset loss instead
1194                     int rssi = mRssiBase + i;
1195                     double lossPreset = presetLoss(rssi);
1196                     if (lossPreset < threshold) {
1197                         if (DBG) {
1198                             DecimalFormat df = new DecimalFormat("#.##");
1199                             logd("Scan target found: rssi=" + rssi + " threshold="
1200                                     + df.format(threshold * 100) + "% value="
1201                                     + df.format(lossPreset * 100) + "% volume=preset");
1202                         }
1203                         return rssi;
1204                     }
1205                 }
1206 
1207             return mRssiBase + to;
1208         }
1209     }
1210 }
1211